Spec System - Advanced Usage
The basic functionality described in Spec System - Basic Usage would be sufficient to create the most basic of game objects, but the real power of the system is in the advanced usage.
Advanced usage details methods of:
- Using Spec Decorators to create highly complex composited objects
- Controlling the data the client receives
- Using "headers" to customize the default fields displayed in the spec interface
Spec decorators are classes intended to be GLOMmed onto a spec to extend functionality and/or store immutable data. As we have previously suggested, we feel it is good class design to have each general category of spec decorator have a common parent. Spec decorators often have Spec Helper classes that are the classes to be added to the SpecDerivedObject on instantiation, Spec Helper classes do NOT have their Spec Decorators as parents but often implement a tree of classes that mirror the Spec Decorator's inheritance tree.
It bears repeating that GLOMmed classes are not allowed to create a method conflict, meaning that two child classes of the same parent may not exist simultaneously on a node
So what do we mean by a "category of spec decorators"? Typically your spec decorator will handle a fairly specialized functionality, which you will want to allow children classes to override to extend.
An example of a decorator category in Hero's Journey ItemSpecOracle would be "visualizerSpecDecorator", which handles visualizing an item (meaning, it handles the 3D visual represention of an item) on the appropriate clients using either propbuckets using the BucketVisualizerSpecDecorator and character geometry using the GeometryVisualizerSpecDecorator or DynamicGeometryVisualizerSpecDecorator. With the exception of the DynamicGeometryVisualizerSpecDecorator which uses the decorator helper class dynamicvisualizeable, they use the helper class visualizeable.
When we constructed our WeaponSpec, you may have noticed that there is no information for where the item would be equiped for use. Assume our designers have stated that not all items are equipable, which rules out adding that functionality at the itemSpec level.
Lets examine our hypothetical weaponSpec:
We could implement the functionality in weaponSpec class, but what if we want different weapons to have dramatically different behaviors? Different behaviors would require we implement a lot of different specs that inherit from weaponSpec and override the functionality, a perfectly acceptable design but modern MMO object design tends to favor a more flexible implementation using the composition of behaviors.
Adding the Decorator Helper Class to a Spec Derived Object
Each of the class method scripts receive a callback when a Spec Derived Object is instantiated. During this callback, decorators have the opportunity to do any initialization they require -- such as GLOMming on a helper class to the newly instantiated object.
In your spec decorator's class method script, implement the shared function OnInstantiationFromSpec.
// EquipableSpecDecoratorClassMethods script shared function OnInstantiationFromSpec( specDerivedObject as NodeRef ) if not ( specDerivedObject is kindof Equipable ) GlomClass( "Equipable", specDerivedObject ) . .
Add a new Spec Decorator to the Decorator List in the Default GUI
The default spec oracle GUI uses a call to your spec oracle's class method script at the method getSpecDecoratorClasses() to retrieve a list of spec decorator classes. For the purposes of our example, we edit the ItemSpecOracleClassMethods script and add the name of the class to the method.
method getSpecDecoratorClasses() as List of String // return a list of classes that can be glommed to prototypes for the spec valid as List of String add back "equipableSpecDecorator" to valid return valid .
Controlling the data the client receives
It is considered to be a good design practice in the development of MMOs to limit the data to which the player client has access to only the data it should know. Specs support a variety of methods for serialization ranging from full serialization of the entire spec node to a more limited serialization based on just the fields specified by the classes that comprise the spec.
The client generally receives a spec via one of two routes, either from a file in the repository or via transmission in a remote call.
The methods for Spec Serialization are:
You can also over ride these methods to produce a customized serialization string, if for example you wanted to create a shared function in your class methods to marshal only fields specified as "player fields" (or whatever).
The MarshalSpec method utilizes the MarshalUtils (Required System) script to serialize the spec, using the method MarshalNodeWithClassSpecifiedFields. This results is a marshal string that includes ONLY the fields specified in the class method scripts of all classes present on the spec by calling a shared function.
This method is useful when transmitting a spec to the player client via remote call, so that no unnecessary information is transmitted. It's worth noting that sending a spec to a player client via a remote call is probably the least common usage pattern for specs; generally specs are made available to the client by loading a file from the repository (see below).
// Implemented in the baseSpec shared function OnMarshalClassSpecifiedFields( n as NodeRef, marshalString references String ) // Called by the MarshalNodeWithClassSpecifiedFields and MarshalPrototypeWithClassSpecifiedFields in the classMethods // scripts for the node n. Your class method script should use marshalNode/PrototypeAppendField to add the specific // fields you want. // MarshalAppendField( marshalString, n, "SpecKey" ) .
Generally used to send the entire spec node to the client for the purposes of GameMaster tools, it does exactly what it you would expect. The marshal string is constructed by the MarshalUtils function MarshalNodeWithClass.
This method is used to create the marshal string for the repository when the UpdateSpecData method is called to update the repository file. The only significant difference is it calls the method OnlyMarshalSpecifiedFieldsToRepository on the spec to determine whether it is supposed to marshal only the class specified fields or the entire spec to the repository. The default behavior is to marshal the full spec to the repository.
The spec system implements a common GUI ont he client for displaying a list of specs in a given spec oracle. There are features for adding, removing, searching and editing Specs which can all be extended to customize for your particular spec implementation. This GUI starting point greatly speeds up the implementation of specs by providing the generic parts of the GUI automatically.
Customizing the GUI Headers
By default the GUI displays a list of the specKey and the arbitrary name assigned to the prototype that represents the spec. The protoype's name is not particularly useful and you often wish to have additional columns of information displayed. This is accomplished using what we call "collectionHeaders".
A collectionHeader is a combination of the name of a field that is common to all of the elements of your collection (spec oracle) and a display name assigned to that field.
Adding a Header
You can easily enhance the generic GUI for Spec Oracles by adding a Collection Header.
Lets say we want to add Name and Description as columns in the spec oracle interface for our ItemSpecOracle.
var oracle = $SPECORACLEUTILS._GetOracleFromType( "_fxSpecOracle" ) oracle.AddCollectionHeader( "displayName", "Name", true ) oracle.AddCollectionHeader( "displayDescription", "Description", true )
An example from the _FXSpecOracle:
NOTE: The columns titles are incorrect. The second argument to the above methods is the string that gets displayed. Hence, the columns actually display Name and Description.
For large number of specs, it is often desirable to have the spec oracle index them so that "queries" may be done using the spec oracle without HeroEngine loading all of the prototypes(specs) into memory. The spec system supports this by providing a callback to the class method scripts of the spec oracle whenever a spec is updated.
There are basically two different ways you could index specs:
- Use the spec systems built in indexing mechanism
- Write a custom indexing mechanism
Built-in Spec Indexing
Specs are automatically indexed (and reindexed) when they are updated via the UI or via a script that properly calls the spec's _OnUpdateSpecData() method to notify the spec it has been modified. One of the operations that is performed in _OnUpdateSpecData() is a call to reindex the spec. The spec system (by default) indexes specs based on their classes (base class and any glommed classes) and on whether or not the spec is deleted.
method _OnUpdateSpecData( spec as NodeRef of Class baseSpec ) // This method is called from the spec's NotifyOracleOnUpdateSpecData() providing the spec oracle the opportunity // to update indexing, caching, etc. me._reindexSpec( spec ) var dataCache = me._getSpecOracleDataCachePrototype() if dataCache <> None dataCache._addSpecHeaderValues( spec.SpecKey ) . // Notify the class methods scripts of the spec oracle that a spec was just updated classMethods as List of ScriptRef = AllPrototypeScriptsWithFunction( me, "OnUpdateSpecData" ) foreach c in classMethods c:OnUpdateSpecData( me, spec ) . .
Additional custom indexes could be added in one of several ways:
- Override your base spec class's _indexSpec() method (it is recommended you continue to index based on class and deleted status if you do...)
- Implement the shared function OnUpdateSpecData() in the decorator classes (this is nice because the base class's overriding method is not required to break encapsulation to create custom indexing for the spec oracle's decorator classes)
- ...combination of both
shared function OnUpdateSpecData( oracle as NodeRef of Class SpecOracle, spec as NodeRef of Class baseSpec ) // Implement in the class method scripts of any of the classes that comprise your specoracle that need to execute // some behavior when a spec is updated. // // Provides the opportunity to index the spec based on whatever criteria the indexing classes implement for indexing // classes and whatever upkeep might be necessary. spec._addSpecKeyToIndex( "my_index", spec.speckey ) .
Custom Spec Indexing
To take advantage of this capability, implement the following shared function in any of the classes of your spec oracle (these classes may be inherited or composited via glomming ):
shared function OnUpdateSpecData( oracle as NodeRef of Class specOracle, spec as NodeRef of Class baseSpec ) // Implement in the class method scripts of any of the classes that comprise your // specoracle that need to execute some behavior when a spec is updated. // .
One way of implementing indexes for your spec oracle would be one or more classes it inherits (or are glommed on) that contain a field that is an map and the methods necessary to query that map. For example, it might be desireable to have a map of spec names to the spec key. To implement this, we would create a class itemSpecOracleNameIndex and add a field(itemSpecNameIndex) that is a
lookuplist indexed by string of id.
// Hypothetical itemSpecOracleNameIndexClassMethods script // // For the purposes of this example, I am assuming names are unique. The index would be structured differently if non-unique // names are possible // shared function OnUpdateSpecData( oracle as NodeRef of Class specOracle, spec as NodeRef of Class baseSpec ) where oracle is kindof itemSpecOracleNameIndex foreach name in oracle.itemSpecNameIndex if itemSpecNameIndex[name] = spec.specKey // ok found the right index for the updated spec now check the name to see if it matches if tolower( name ) <> spec.displayName // name mismatch remove old index remove name from oracle.itemSpecNameIndex // index it using the new name oracle.itemSpecNameIndex[ spec.DisplayName ] = spec.speckey . . . . . method QueryItemSpecsForMatchOn( matchON as string ) as list of id matchedSpecKeys as list of ID foreach name in me.itemSpecNameIndex // findstring for match on the string on, if match add it to a list of spec keys . return matchedSpecKeys .