Spec System - Basic Usage
- See also: Spec System - Advanced Usage
The Spec System provides a way to administer definitions for game objects and other types of non-object game concepts.
The Spec System, a part of HeroEngine's MMO Foundation Framework, provides a flexible and robust framework, primarily for the definition of efficient game objects. It implements a hybrid model where immutable (unchangeable) data is shared by objects via a spec, and mutable (changeable) data lives with the instantiated object itself. Another HeroEngine Required System, The Fx System, is also implemented using the spec system.
The Spec System is made up of Spec Oracles, which are collections of related definitions. Each such definition is called a "spec" or "template". A Spec System may have purely immutable, informational (non-object) prototypes, or it may use a mixture of mutable and immutable data to define game concepts and objects. The framework as a whole includes support for the automatic propagation of immutable data the client should know, via a bandwidth-optimized pathway. It uses a mixture of mutable and immutable data storage, including the $SPECORACLEUTILS system node.
|Please note, There is no requirement to use the HeroEngine Spec System for the creation of your game objects.|
Everything the Spec System does could be implemented in a different way by your development staff. Our general feeling at HeroEngine is that you could re-create the wheel, but why would you want to? As a company we have invested time and thought into the design of the Spec System, to solve a very complex problem in a manner particularly suited to take advantage of the HeroEngine development environment. You, as a licensee, can gain from this prior work, by using the resulting framework if you so choose.
The representation of objects in an MMO is one of the most important decisions that needs to be made early in the development process. Careful design here is critical. The Spec System helps implement the basic design patterns necessary in any object representation.
- Data that, on a per object base, can differ and be modified. Examples of common mutable data in an MMO: Durability, Temporary enhancements, damage taken, or number of charges available in a magic item.
- Mutable data is represented uniquely for each object. This provides the maximum amount of flexibility for game mechanics, but must be designed with care, as it may also lead to data bloat. Data bloat affects memory usage, database load, bandwidth between server and client, and more.
- Data that all objects share, and cannot change on a per object basis. Examples of common immutable data in an MMO: Name, Description, DPS.
- Immutable data is represented once, regardless of how many objects share that representation. The result is that game objects which use immutable data can have smaller memory footprints and require less bandwidth. However, the tradeoff is that using immutable data can can also result in a lack of flexibility in game mechanics.
The distinction between immutable and mutable data directly affects the memory footprint of game objects, game mechanics, and required bandwidth for an MMO. It also has consequences for how easy/hard it may be to correct data errors in items. The following lists the pro's and con's of each:
- Able to represent dynamic data, e.g. flexible at runtime
- Game objects are virtually unlimited from a design point of view
- Data is unique without a common reference, so global updates to all objects requires a lot more code to implement
- Large memory footprint (per object store)
- Minimal Memory Footprint
- Changes to a template are efficient, as they apply to all objects referencing that template
- Unable to represent per-object data that changes at runtime, e.g., inflexible at runtime
- Game objects are limited from a design point of view
Static objects are objects that are fully based on their template, or spec. None of their data is actually stored with the object, rather it is a part of the template and exists as one copy to which all objects derived from the template read.
The greatest benefit of static objects is the extremely small memory footprint of an instantiated object, they only need a reference to their template. The unfortunate consequence however is that static game objects do not make particularly interesting items for players to find and use.
Unique objects are objects whose data is fully mutable. Even though the swords depicted below have identical data, there are three copies of the name, description etc in memory.
The primary benefit of unique items is that it enables designers, allowing them to design systems that can potentially change every value of an object without affecting any other object.
Spec Derived Objects
The Spec System is part of the HeroEngine Required Systems designed to facilitate the creation of instantiated objects that are a mix of immutable and mutable data. A general category of specs is organized by what is called a SpecOracle, which is in charge of; indexing all specs assigned to it, creating new specs, and servicing requests to retrieve a spec by a specKey.
Each spec contains all of the immutable data ( i.e. it is a template ) for an object and a reference field called a specKey. An instantiated object, called a specDerivedObject has any mutable data and a specKey referencing the spec it should use for immutable data.
The Spec System
What problem(s) does this solve?
- Pattern for reducing the memory footprint of game objects
- Expression of game objects as a mix of immutable and mutable data
- Generic Interface for Editing Template(Spec) data
- (Optional) Efficient communication of template data to the client
- Transmission of mutable game object data to the client
- Caching game objects on the client
- Concept of inventory or "containers" for a character
- Implementation of "items"
Transmission of mutable game object data to the client
There are a variety of ways to communicate mutable game object to the client, but in the end they all come down to the basic idea of serializing (marshalling) the data and passing it to the client. Some required systems, such as HSL node collections, have mechanisms encapsulating the communication with the client.
See also: Node Collections
Caching Game Objects On the Client
Caching game objects on the client generally entails the creation of a "root" node (often a collection) for a category of game objects that is associated to some well known node on the client.
Well Known Nodes on the Client:
- The world anchor (SYSTEM.INFO.WORLDANCHOR)
Inventory and "Containers"
The implementation of Inventory and/or "Containers" is highly dependant upon the design of your game. Consequently, there is no generic version of these concepts and the implementation rests on your developers. The reference copy of Hero's Journey includes an implementation of Inventory and Containers for use as an example of how you might choose to implement your own.
Implementation of Items
The implementation of "items", game objects that characters possess and use, is extremely dependant upon the design of your game. The Spec System is a framework that supports a way of implementing items, the design of your items is up to you.
- NOT dynamic in production worlds
- Server-side representation of a spec: a prototype
- Client-side representation of a spec: a file in the repository
- Data Agnostic
- Does not handle the transmission of instantiated objects
NOT dynamic in production worlds
Altering a spec results in a DOM coordination between all area instances and requires all connected players to rerequest the new repository file the next time they attempt to use it. Either of those events would be potentially capable of dragging your infrastructure to a screeching halt, which of course is unacceptable in a production(live) environment.
In a development world on the other hand, changing the specs is fully supported and of course necessary for development.
Server-side representation of a spec: a prototype
On the server a spec is a prototype, which has as its parent the class baseSpec, to which additional classes may be GLOMmed to hold immutable (in production environments) data. As with all prototypes, you may not change the prototype in production worlds but it is permissible to change them in development.
Client-side representation of a spec: a file in the repository
Specs on the client are represented by files in the repository. The spec system handles reading those files into memory and caching the data for subsequent read accesses. Requesting a spec on the client should (almost) always be considered an asynchronous operation, while the system does cache the data you never know when the system may have determined it was necessary to purge the node from the cache necessitating a new asynchronous I/O request.
The spec system is generic in nature, it has no preconceived notions about what the data is or how to use the data. The spec system is not an implementation of items or any other kind of game object for your game, it supports a way of implementing game objects.
Does not handle the transmission of instantiated objects
Mutable data, that stored as part of an instantiated object, is not automatically communicated to the client. Your game systems must make the determination as to when and how a particular game object is communicated to the client, the spec system will already have the immutable data the client should know (if any) available.
Lets assume you have been tasked to create the structures that represent a weapon for your game. Now we know from the discussions on this page that we could create items that are fully static or fully dynamic, but our designers want items that are a mixture between the two types. The designers have said that item attributes like; name, description and average damage-per-second will remain the same for all items of type X but that items can break requiring a durability field to track the current wear.
The spec system is ideal for solving problems of this nature, it handles the little details of how specs are stored and managed and lets you get down to business actually creating your items.
There is one component that must be updated:
- The SpecOracleUtils system node's game specific implementation
There are three major components to be implemented:
- The SpecOracle Class Structure
- The Spec Class Structure
- The SpecDerivedObject Class Structure
SpecOracleUtils System Node
The SpecOracleUtils system node provides system level functionality for the spec system both on the server and the client.
The Spec system uses the system node "SpecOracleUtils" to find the data for the spec oracles on the client. In order for the Spec System to know about your new oracle you must edit the game specific implementation of SpecOracleUtils and update a few methods.
See also: System Nodes
Creating a game specific implementation of SpecOracleUtils
If your game does not yet have a game-specific implementation, simply create a <MYGame>_SpecOracleUtils Class and GLOM it onto the SPECORACLEUTILS prototype located in the client DOM (This can be done by using the F5 Hotspot menu in the graphical game window and locating under System Nodes Configuration Menu SPECORACLEUTILS and 'adding' your new class to it). The System Node Configuration GUI is the preferred method to do that because it handles the communication to update any instantiations of a System Node to reflect the changes you have made.
For example, for Hero's Journey we might create the class HJ_SpecOracleUtils in whose class method's script we would implement any of the methods we need to override/extend.
Update the Game Specific Implementation of SpecOracleUtils
Most method calls to the
$SPECORACLEUTILS node result in calls to methods that may be implemented in your game specific implementation class to extend or override the Required System functionality. Adding a new spec oracle requires the extension by the game specific implementation to take advantage of all of the Required System functionality.
Implement a Clientside Singleton Getter
On the server the spec oracle already exists in the form of its prototype, and may be retrieved using the
getPrototype() external function or the
$SPECORACLEUTILS._GetSpecOracleByClass method. Clientside, the _specOracleUtilsClassMethods needs a getter that implements the Singleton pattern (i.e. it should get the existing oracle node if any, or create it if none exists).
_SpecOracleUtilsClassMethods has a built-in pattern for this, requiring only a few lines of code assuming you wish to use the existing structure.
method GetItemSpecOracle() as NodeRef of Class ItemSpecOracle itemSpecOracle as NodeRef of Class ItemSpecOracle = $SPECORACLEUTILS._GetSpecOracleByClass( "itemSpecOracle" ) return itemSpecOracle .
Add your Oracle to the (ClientSide) List of All Oracles
Some of the Required System functionality of the spec system, such as the default GUIs or finding a specOracle by its class, require a way to find all specOracles. This is achieved by assembling a list of known oracle IDs. The gamespecific implementation for SpecOracleUtils should implement the
method GetAllSpecOracles() adding the new specOracle to the list of IDs using the getter implemented above.
method HE_GetAllSpecOracles( oracles references List of ID ) as Boolean // This method is called to retrieve all of the oracles of which the spec system should be // aware. The default GUIs and a variety of utility functions depend on your oracle // being included in this list // add back me.GetItemSpecOracle() to oracles return true .
Add your Oracle to the (ClientSide) HE_GetSpecOracleByClass
The spec system uses the class of the oracle as the universal key for finding the oracle node on both the client and server. For the clientside implementation, your gamespecific implementation of the SpecOracleUtils should include a
method HE_GetSpecOracleByClass() that when given a class name runs through testing all of the "known" oracle nodes to find the one that matches that class.
Implementation of this method enables the generic methods of the Required System ($SPECORACLEUTILS system node) to retrieve a reference to your spec oracle node.
method HE_GetSpecOracleByClass( oracleClass as String, specOracle references NodeRef of Class SpecOracle ) as Boolean Assert( ClassExists( oracleClass ), "Invalid class name provided to GetSpecOracleByClass") oracles as List of NodeRef of Class SpecOracle = $SPECORACLEUTILS._GetKnownSpecOracles() oTest as NodeRef foreach o in oracles otest = o when toLower( oracleClass ) is "itemspecoracle" if otest is kindof ItemSpecOracle specOracle = otest break . . . . if specOracle <> None return true . return false .
Using $SPECORACLEUTILS to Get a Reference to a Spec Oracle
Once the game specific implementation of methods is completed, getting a reference to your spec oracle is one line of code.
var oracle = $SPECORACLEUTILS._GetOracleFromType( "ItemSpecOracle" )
The Spec Oracle
Spec Oracles are nothing more complicated than a node in charge of keeping track of the specs for a particular system. We call them oracles because they function kind-of like a database in that they expose their data (specs) to requests/queries from other systems.
Please note, the following sections detail only the most basic implementation of a spec oracle for our example spec oracle. Advanced topics are located in the Advanced Usage and Reference Sections.
Creating the Spec Oracle Class Structure
The class structure should be defined in both the client and server DOMs, and with only a few minor additions required for the client DOM's definition.
If the class structure is not defined on the client, you will not be able to take advantage of the inherited capabilities of Spec Oracles including: a common GUI interface for editing specs, the automatic loading of files(if any) representing your specs from the repository to a local cache.
|Spec oracles should generally follow the naming convention of <type> + SpecOracle|
So, all we really need to do is create a new class named
itemSpecOracle in the server DOM that has as its parent the class
SpecOracle. Once done, we then need to create the same class on the client that has as its parents the classes
For more in-depth instructions look at: Creating a new Spec Oracle Step by Step
Creating the Instantiation of the ItemSpecOracle (a prototype)
Now that we have the class structure defined, we must create the prototype that will be the instantiation of our new oracle on the server. The oracle should share the same name as the class from which it is instantiated (i.e. our class is ItemSpecOracle so our prototype will be named ItemSpecOracle)
Create the itemSpecOracle prototype
Using the CLI
- \cpfc ItemSpecOracle, ItemSpecOracle; description="The instantiation of the class itemSpecOracle as this prototype handles all of the operations of the item spec oracle for our game."
Using Organizer panel
- Not yet implemented
Create/Update the ItemSpecOracleClassMethods Script
There are a number of critical methods that must be overriden by your class methods script in order to create a functional Spec Oracle. The easy way to create your class method script is to use the provided template script TemplateSpecOracleClassMethods and follow the instructions in the comments for each method.
Specify a File Extension
Specs require a unique file extension, which is specified in the class methods script. When a spec is saved to the repository it is assigned a name of its <SpecKey> + FileExtension. The client spec oracle uses the well known naming convention to read in its spec files when one is requested by its key.
method getSpecFileExtension() as String // It is intended that each child class override this method with their own // return ".itemSpec" .
Specify Base Classes
Base classes are those classes from which the most basic form of a spec may be generated, the provided Required System scripts require this method be implemented for the default GUI to function properly.
method getValidBaseClasses() as List of String // return a list of classes that prototypes for the spec can be created from // Base classes can be simple, like the fundamental class or they may // be complex with a number of decorator classes already specified as parent classes // valid as List of String add back "itemspec" to valid add back "weaponSpec" to valid return valid .
Specify Oracle Class
The oracle class is used to find the oracle nodes both on the server and client, this requires the implementation of a method that specifies that class.
method getSpecOracleClass() as String // Prototypes on the server should be named on the server the same as this name // client oracle nodes are located using the class. // return "ItemSpecOracle" .
Specify SpecName Prefix
Specs are given arbitrarily unique names using the combination of the PreFix + <SpecKey>, this prefix should be unique to your spec oracle. We are going to choose "itemSpec" here so that all of our item specs will have prototypes named ItemSpec + <SpecKey>
method getSpecNamePrefix() as String // A unique prefix for this specOracle used in the naming of the prototypes (specs) created for this oracle // commonly this is the same as the name of the fundamental base class from which all specs in this oracle // are derived. // return "itemSpec" .
Specify the Fundamental Base Class
As a general rule, it is useful to have all of the specs in your oracle inherit from a common parent (which in turn has baseSpec as its parent). The spec oracle has a method that returns this fundamental base class for your oracle.
For our ItemSpecOracle, the class ItemSpec is going to be our fundamental class.
method getSpecClass() as String // It is intended that each child class override this method with their own // this should return the fundamental base class for specs in this oracle. // return "itemSpec" .
Using the Spec Oracle
Retrieving a reference to a Spec Oracle
If you followed along the "type" for your spec oracle should be the same as the class name from which the spec's oracle prototype was created (and should be what you choose for a name for the prototype). For the purposes of our example, it is "ItemSpecOracle", and retrieving your oracle requires only a single line of code.
var oracle = $SPECORACLEUTILS._GetOracleFromType( "ItemSpecOracle" )
Retrieving a Spec using a Spec Oracle
Assuming you know the specKey for the spec you wish to retrieve and you have a reference to your oracle, a few lines of code will retrieve a reference to the spec on the server.
Get a Spec on the Server
// Retrieve a spec on the server oracle.GetSpecByKey( key )
Get a Spec on the Client
Assuming the spec oracle is set up to write out its specs as files to the repository, the client has access to that data using the client-side spec oracle.
Retrieving a spec on the client is a bit more complicated because the process is asynchronous. This requires you provide a listener to the oracle when you request the spec by its key, which will some time later produce a callback.
var listener = oracle.createScriptListener( CallbackScript, false ) // optionally you could create a nodeListener using the oracle.createNodeListener( nodeToNOtifyID, false) // A node listener gets a callback in all of its class method scripts in a shared function oracle.RequestSpecByKey( key, listener )
// In the callbackScript shared function EventRaisedNotify( obsSubject as NodeRef of Class ObsSubject, obsListener as NodeRef of Class ObsListener, data as NodeRef ) // Data is a reference to the spec, do whatever you need now that it is ready. // ask the subject to remove the listener because we do not want any further events obsSubject.removeListener( obslistener ) .
// Class method script receiving a nodelistener's event shared function EventRaisedNotify( obsSubject as NodeRef of Class ObsSubject, obsListener as NodeRef of Class ObsListener, data as NodeRef ) // Data is a reference to the spec, do whatever you need now that it is ready. // ask the subject to remove the listener because we do not want any further events obsSubject.removeListener( obsListener ) .
The spec consists of all of the immutable data the specOracle is supposed to know about the "template" data for a game object. This data is often less complete on the client, since it generally requires less information to function.
- Spec Base Classes
- Spec base classes are the fundamental classes from which a new spec for a given oracle may be created.
- Spec Decorator Class
- A spec decorator class is a class that extends the functionality of a spec beyond the shared functionality of the Spec Base Class. Spec decorators are used to composite complex behaviors together, to create fully realized specifications.
- Decorator Helper Class
- Decorator helper classes are classes a decorater composites onto a spec derived object to provide it with the functionality required by the spec decorator.
Please note, the following sections detail only the most basic implementation of a spec for our example itemspec. Advanced topics are located in the Advanced Usage and Reference Sections.
The creation of a new spec entails the following steps:
- Create the class structure
- Add the spec class to your specOracle's validBaseClasses method
The Class Structure
We consider it to be a good practice to create a class which all of the specs in your spec oracle have in common. This allows for the implementation of any shared functionality in that fundamental class. For the design of our hypothetical itemSpecOracle we are going to use the class itemSpec as the fundamental class for all of our specs.
The challenging part is designing functionality you want to implement as part of your fundamental class (and in the children thereof) and what functionality will reside in decorator classes for your spec.
The spec system requires all specs be derived from the parent class baseSpec, which suggests we create a class structure where the itemSpec class inherits from baseSpec and weaponSpec inherit from items.
Class Methods script for the Fundamental Class
The fast way to create the class methods is to create the new script using the TemplateBaseSpecClassMethods template. There are a number of methods in here that must be implemented, and many that are almost required to support the functionality of an object instantiated from the spec.
The critical ones provide functionality for:
- Finding the SpecOracle
- Handling the Instantiation of a Spec Derived Object
Finding the SpecOracle
Specs need to know how to find their spec oracle, on the server this is as simple as requesting the prototype using the external function
method getMySpecOracle() as NodeRef of Class SpecOracle // Specs need to know how to find their specOracle // on the server the oracle is represented by a prototype // which is created (by convention) using the same name as the classname // // It is critical this method be implemented and return your specOracle node // return getPrototype( "DefaultSpecOracle" ) .
Handling the Instantiation of a SpecDerivedObject
When an object is instantiated from a spec, the base class and all of the decorator classes (classes glommed onto the spec) receive a callback in a shared function allowing them the opportunity to do any initialization they might require for the instantiated object to function.
The primary use of this callback is to GLOM "helper" classes on the instantiated object to support the extended functionality of the spec's decorator classes.
shared function OnInstantiationFromSpec( derivedObject as NodeRef ) // Shared function called in the base and decorator classes of a spec when an object is instantiated // do any initialization/setup required by the decorator class // // This is typically used to GLOM helper classes that support the functionality of the spec's // base or decorator classes. // // if not ( derivedObject is kindof MyHelperClass ) // GlomClass( derivedObject, "MyHelperClass" ) // . .
Instantiation of a Mutable Game Object
Assuming you already have a reference to the spec, instantiating a game object (also known as the spec derived object) from that spec is simple.
var myObject = spec.CreateFromSpec()
The Spec Derived Object
A spec derived object is the mutable instantiation of a game object for a particular spec. The spec derived object generally does NOT share any of the classes that make up the spec, rather it is a composition of "helper" classes that implement extended functionality and fields dicated by the spec's classes (base and decorator).
The base class of a spec derived object must have the class SpecDerivedObject as a parent.