Spec System - Advanced Usage

From HEWIKI
Jump to: navigation, search

Contents

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.

Overview

Advanced usage details methods of:


Spec Decorators

Aside.gif There is a school of thought where all game objects share a parent class and additional behaviors are composited to form a functional game object. Happily, the spec system fully supports this design.

Instead of one or more base classes per spec oracle, all specs share the same base class and additional behaviors would be implemented in spec decorators.



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.

Hero's Journey's Visualizable Spec Decorator

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:


Weapon Spec

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.

Starting to sound like an ideal fit for a Spec Decorator for our ItemSpecOracle.

Spec Decorator Class Structure

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).

MarshalSpec

Aside.gif The MarshalUtils function MarshalNodeWithClassSpecifiedFields does a recursive search through the class hierarchy of a node to gather ALL classes (including their parents), resulting in each class only needing to append any fields it adds allowing its parent classes to add their fields when they get the function call.



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" )
.

MarshalFullSpec

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.

MarshalSpecToRepository

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.

Spec GUI

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.
_FXSpecOracle Headers

Indexing Specs

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:

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:

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.
  //   
 
.
Aside.gif The difference in implementation (method vs shared function) lies in the ability of shared functions to be implemented in multiple class method scripts, essentially allowing the classes of a spec oracle to encapsulate the behaviors they execute on an update.


If implemented as a method, only one class could get a callback and it would have to implement whatever functionality it needed to handle all of the indexes for the spec oracle either by calling the classes one by one or implementing all of the functionality itself.



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
.

See also

Personal tools
Namespaces
Variants
Actions
Navigation
Toolbox