Data storage options
|
Design Considerations for Persisted Node Data Storage
Overview
This page details the many ways a scripter can persist (save) information on the server for the implementation of game mechanics and systems. There are many different ways to do this, as each has their pro and con tradeoffs. This page will guide you through the various options and explain when each is appropriate to use.
In general, every persistent node in the GOM is saved in the database. This represents the most fundemental way in which data is saved. However, one must consider how to get access to that node later to retrieve the data. Further, there are other ways to save data that might be more appropriate than this. This page will walk you through all of the HeroEngine features at your disposal for persisting data.
Granularity
Several of the data storage options detailed on this page require the creation of nodes to hold the data you want to persist. When a persistent node's fields are changed, this will ultimately cause a write to the database at some point. It is critically important to carefully control the amount of database "hits" that go on; too many and the server processes will begin to slow and eventually backup. Because of this some things must be carefully considered when using persistent nodes:
If you have, for example, a top level field field myBigComplexField
on a persisted node that is a list
or lookuplist of class y
, which in turn is composed of a lookuplist of class z
and some other fields... you need to be aware that ANY changes to a cell to a field in class z
causes the entire field myBigComplexField
to be serialized and saved to the database. Depending on the number of elements and frequency of write accesses, this can have extremely bad performance implications.
- Top Level Persisted Node
- field: myBigComplexField as lookuplist of class y
- class: Y
- field: classZLookup as lookuplist of class z
- class: Z
- field: mystring as list of string
- field: myints as list of integer
- class: Z
- field: otherClassYData as list of string
- field: classZLookup as lookuplist of class z
- class: Y
- field: myBigComplexField as lookuplist of class y
For applications that may require extremely frequent write access to a persisted node, it is best to make the write accesses as granular as possible.
- Granularity
- (attrib: The Free On-line Dictionary of Computing, 1993-2004 Denis Howe) The size of the units of code under consideration in some context. The term generally refers to the level of detail at which code is considered, e.g. "You can specify the granularity for this profiling tool". The most common computing use is in parallelism where "fine grain parallelism" means individual tasks are relatively small in terms of code size and execution time, "coarse grain" is the opposite. You talk about the "granularity" of the parallelism. The smaller the granularity, the greater the potential for parallelism and hence speed-up but the greater the overheads of synchronization and communication.
So taking our original field mybigComplexField, how do we fix it to be more granular?
Well, the first thing we should do is make the lowest level of related data (class z in this case) into its own persisted node. The field that previously was a lookuplist of class z is changed to be either a lookuplist of noderefs, or you can simply do away with it entirely and use the association engine.
Whether you use the association engine as the "list of class z's" or not, you must make a base hard association to the node of class z to make it load with the character or area. You could make additional soft associations to qualify the relationship more accurately. Depending on the number of persisted class z nodes you may find that looping through the list of associations to be less than efficient, in this case a lookuplist map is probably ideal.
This is great, but now what about class y? Well we can do the same thing to the lookuplist of class y, make persisted nodes out of the class y list objects and make associations or a lookuplist map of noderefs.
The end result of this is that changing a field on one of the class z nodes ONLY forces the serialization and writing of THAT field on THAT specific class z node.
Scope
When referring to the data's scope, it is a reference to where the data "exists" and what has access to read/write to the data. There are several possibilities for the scope: Local GOM, Global (all server GOMs), Local Client GOM, Client and Server. When talking about the scope of data, we do not include Inter-Process Communication because it blurs the line into everything being global.
Local GOM
Each instance of an area has what is called a Local GOM, it is here that all nodes in the area are loaded into memory. Persisted nodes may be loaded in only one GOM at any given time, it is that GOM that is permitted to make persisted changes. Non-persisted nodes may be altered in a local GOM, but those changes are neither reflected nor persisted in any other local GOM.
Play instances have no persisted nodes in them other than the account (and by extension the character and any nodes associated to it) which is what is called a "root" node. The area's root node is a non-persisted node in play instances.
Global
Data that is global in scope is available to all Local Server GOMs. However, several of the methods that provide global access to data have restrictions on write operations.
Local Client GOM
The client GOM starts with only the most basic information populated by HeroEngine to establish the minimum level of functionality. In practice, that means that the client knows the assets in an area and their properties (not to be confused with fields you may have added to an asset on the server via GLOMming) and any nodes scripts may have created.
Changes to nodes in the client GOM are not reflected to the server. Game systems often cache data in the client GOM to avoid round-trip communications with the server and expose interfaces for working with that data to communicate changes to the server if needed.
Client and Server
Several of the more specialized storage techniques have a scope of Client and Server, which means that both client and server have the opportunity to access the data. These methods all have restrictions on write access.
Technical Difficulty
The technical difficulty of a storage technique is meant as a gauge of the general level of scripting proficiency required to implement a system using that technique.
Read Access
Speed
The speed at which data may be read varies for the differing storage techniques.
Type
Access to the data may be Asynchronous, Synchronous, or a mixture where the first access is asynchronous and the data is cached to make subsequent access synchronous.
Write Access
Speed
The speed at which data may be written varies for the different storage techniques.
Type
Access to to the data for the purpose of writing may be Asynchronous, Synchronous or Disallowed.
Crash Tolerance
Crash tolerance refers to how well the data is capable of surviving a crash. For most methods of data storage this is mainly dependant upon the database and thus not an issue scripters need concern themselves overly.
Memory Footprint
Sometimes the choice of storage techniques is limited by the memory footprint of the data to be stored. Not all storage techniques are equally valid for all ranges of memory footprints. This is meant to be a general gauge as to the appropriate size of a data set, there is no hard and fast rule that must be followed.
However, even though there is no hard rule about what footprint is appropriate for a particular method of storage there are absolute limitations imposed by your hardware. For example, you simply can not load more objects into memory than you have available RAM.
Queryability
Queryability refers to how easy it is for other systems to access the data. Some storage techniques do not or can not expose the data except by explicitly adding code to do so.
Scaleability
Scaleability refers to how well a particular storage technique is suited to handling extremely large numbers of customers. This is often achieved by accepting compromises in other areas such as the read/write access, scope, memory footprint and so on.
See also: Node Persistence
Global Persisted Data Storage Options
Overview
A HeroEngine game is composed of many Area instances which are all individual processes. There needs to be a way for all of these area instances to store and retrieve data.
HeroEngine provides several potential repositories for data: Scripts, Prototypes, Node Delivery Service, System Areas, the World Server and the Repository. Of these, only System Areas (and by extension the World Server) are suitable for data that must have read/write access in all instances in a production (live) world. For data that only needs to be read, there are many more options for data storage.
Data Storage in Scripts
Summary
Storing data in script makes sense for a system when:
- Data never changes dynamically or in a production world
- Fast synchronous access to the data
- Data must be available in all instances
- Low technical level of coding is desirable
Scripts are particularly suited for storing what are in effect constants. Whether that is a simple boolean to a complex class variable. Data stored in scripts is not modifiable from HSL and can not be modified dynamically. Consequently, scripts are unsuited for storing data that needs to be modified via GM tools.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | low | Ability to Handle Large Data Sets | low |
Data Read Access | extremely fast Synchronous |
Data Write Access | none Data is static Data can not be modified via HSL |
Supported Frequency of Data Access | high | Queryability | low Support must be coded for each query |
Crash Tolerance | extremely high | ||
Scope | global or local client GOM Server scripts have a global scope Client scripts have a scope of local client GOM |
Design
Storing data in a script has a low design requirement, you must be able to call a function in the script and have it return the data in a format your script is able to interpret. If you intend other scripts to be able to access the data stored in your script, the access point (function) must be declared as public.
Storing data in a script does require that the data be copied into a variable (scalar or class variable), as with any computer language copying data is relatively slow operation. Consequently, scripts are poorly suited for large datasets.
public function myData() as lookuplist indexed by string of string myList as lookuplist indexed by string of string myList["Blue"] = "Blue is not the color of my house." myList["Red"] = "Red is a color found in our flower bed." myList["Green"] = "Green is sometimes the color of our grass." return myList .
Of course it is also possible to create an extremely complex class, populate it with all of the data it needs in your script and return it.
Class: MyReallyComplexClass Field: AreaInstance as class InstanceKey .AreaID .AreaInstanceNumber Field: ComplexInfo as class InfoTable .InfoLookupList .InfoList .InfoRequesterAccountID .InfoTable Field: AccountID as ID
public function myComplexData() as list of class MyReallyComplexClass myReallyComplexClassList as list of Class MyReallyComplexClass // Fill out the data for 0...n MyReallyComplexClasses and add them to a list return myReallyComplexClassList .
Example
Scripts provide a good place to store constants that do not need to change dynamically. During development of Hero's Journey we needed to test different types of creature navigation in and out of combat, such as snapping rotation vs animating the turn. Rarely changing configuration values like this could be stored in a prototype, but doing so would really be overkill when the desired behavior is the ability to swap between methods during development.
#define snaprotation public function SnapRotation() as boolean #if snaprotation return true #else return false #endif . public function DoTurn() if SnapRotation() // Just snap the character else // Send behavior to client to turn the character . .
While this represents a simplistic example, it would be equally valid to return a class variable rather than a boolean.
Data Storage In Prototypes
Summary
Storing data in prototype makes sense for a system when:
- Data rarely changes (and never changes outside of the development world)
- Fast synchronous access to the data
- Data must be available to all area instances
- Data is not expected to have a large memory footprint
The DOM allows us to construct classes and their fields, but it does not permit the setting of a default value. Prototypes provide the mechanic by which we can set up objects with default values. Prototypes are loaded into the GOM of an AreaInstance during its spinup, meaning they are in memory for the AreaInstance to read.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | medium | Data Write Access | None/Asynchronous No write access permitted in production(live) worlds Slow write access in the development environment Writing to a prototype is a coordinated DOM change |
Data Read Access | extremely fast Synchronous |
Memory Footprint | varies |
Queryability | high Data is exposed to all systems equally |
||
Crash Tolerance | extremely high | ||
Scope | global or local client GOM Server prototypes have a global scope Client prototypes have a scope of local client GOM |
Systems that Use Prototypes for Data Storage
- Spec Oracles
- Command Registration System (/register)
Design
As far as HeroScript is concerned, prototypes are handled very similarly to how you handle a node.
While prototypes are loaded on demand by HeroEngine, prototypes in constant use have a memory footprint equal to their footprint times the number of servers on a physical machine. Consequently, consideration must be given as to the memory footprint a prototype will occupy.
Gotcha's
Modifying a prototype triggers a coordinated change in the DOM, for this reason writing to prototypes is NOT permitted in code that will run in production(live). Consequently, prototypes are best for Global Data Storage that GameMasters will need to modify easily (via some GM verb) but ONLY in DEV.
- Coordinated Change
- Coordinated DOM changes freeze processing in ALL AreaInstances of a World until the update is complete.
Note: The coordinated change happens at the end of script execution for any fields marked "dirty". This means Sorting a list on a prototype does not cause any more coordinated events than is absolutely necessary.
Reference a Prototype
Getting a reference to a prototype is achieved through the use of the external function getPrototype(name as string) as noderef
.
myPrototype as noderef = getPrototype( "myPrototypename" )
Reading a Prototype
Reading a prototype to which you already have a reference is exactly like accessing the value of a field on a node.
myString as string = myPrototype.Name
Modify a Prototype
![]() |
It is NEVER permissible to modify a prototype in the production(live) worlds Contact your technical support list if you think you need to do so and we will come up with an alternative. |
Modifying a prototype to which you already have a reference is just like changing a field on a noderef.
myPrototype.myField = 100000
Example
Games commonly have what are referred to as "slash commands" or "verbs"; when sent via chat to the server such commands execute some sort of logic. The HeroEngine CommandHQ handles registering commands with the code that is invoked when the command is run.
For ease of use, it is convenient if adding a new command does not require changes to the CommandHQ code. Add/Remove operations only occur during development so the ability to modify the data in a production world was not needed. Commands are typically contextual in nature making them sensitive to which instance the code is running, so the map of command to code needed to be available in all instances.
Consequently, a prototype "ProtoCmdMaster" was choosen as the repository for the data for the command system.
Data Storage In Specs (MMO Foundation Framework)
Summary
Storing data in a spec makes sense for a system when:
- Data is immutable in nature and shared by many objects
- Data needs to be available in all servers and optionally to the client
- Data may need to be communicated to the client, and should be done so efficiently
- Synchronous access to the data on server
- Asynchronous access to data on the client
- Client may require a subset of the data available to the server
Specs are the marriage of several of the storage techniques detailed on this page. Immutable (unchanging) data is stored in prototypes on the server. The prototypes know how to write themselves out to repository files providing for an efficient transmission mechanism to the client that occurs only when a file changes. Objects instantiated from a spec are then stored as nodes dangling off of a root node such as the character or area root, it is these nodes that store mutable (changing) data that is transmitted to the client via Inter-Process Communication as needed.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | low | Data Write Access | None/Asynchronous No write access permitted in production(live) worlds Slow write access in the development environment Writing to a prototype is a coordinated DOM change |
Data Read Access (server) | extremely fast Synchronous |
Memory Footprint | varies Specs are loaded on demand. |
Queryability | high Data is exposed equally to all systems |
Data Read Access (client) | varies Asynchronous Slow I/O limited access for first read, subsequent requests are fast as the spec is cached in memory. |
Crash Tolerance | extremely high | ||
Scaleability | extremely high Reducing memory usage for shared data by referencing a template is critical to systems that have lots of objects with some amount of immutable data. |
||
Scope | client and server |
Systems that Use Specs for Data Storage
- _FxSpecOracle
Design
The spec system is a Required System included in HeroEngine, consequently there is a very low design requirement as most of the functionality is already in place. Detailing the specifics of using the spec system is beyond the scope of this page.
See also: Spec Oracles
Example
There were originally two basic strategies for the implementation of items in an MMOs:
- Each item is a unique copy of its data
- Each item is merely a reference to a template
There are performance issues with every item being a unique copy of its data, and there are design issues with all items being references to static templates. The limitations of the two strategies as absolutes lead to the more common implementation in todays MMOs of a blend between the two. As much immutable data as is possible is stored in a template and mutable data is stored as a unique instance of the data specific to a particular item.
Items in Hero's Journey are implemented as SpecDerivedObjects, which have a reference to a spec that serves as the repository for their templated data and mutable data stored as a part of the item itself.
Local Server Persisted Data Storage
Overview
Data storage for a local server is much easier than global data storage. You can store the data directly on the area root node or in nodes associated via hard associations to the root.
The biggest disadvantages are the limited availability of the information and potential for the area in which the data resides being down.
Data Storage with the Area Root Node
Summary
Storing data with the area root makes sense when:
- Data is specific to an area
- Persisted changes occur only in the edit instance
- Data needs to be read synchronously in instances of the area
- It is acceptable that changes to the data will not show up in running instances until they have been relaunched.
- Fast synchronous read access desired within the instance
- Asynchronous access is acceptable for write access
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | low | Memory Footprint | varies Some consideration should be given to the memory footprint of the data. |
Data Read Access | extremely fast Synchronous |
Ability to Handle Large Data Sets | low Data exists for each instance of the area, this becomes a potential scaleability issue. |
Supported Frequency of Data Access | high | Scope | local global Via Inter-Process Communication Possibile the area is not spun up |
Crash Tolerance | high | Data Write Access | varies Writing to a local node does not update all areainstances, nor does the data persist if the instance is not the edit instance. |
Design
There are several potential methods of storing the data with the area: creation of a node associated to the area root or a class that is GLOMmed onto the area root node.
Storing the data in a node requires that you; Instantiate a persisted node from a class containing the field(s) you need to store your data and associating that node to the area root node in the EDIT instance of your area. The node(s) must be associated to the area root with at least a base_hard_association and may optionally have additional soft associations to facilitate querying for the node(s). Using nodes to store the data has an advantage over storing the information in a class GLOMmed on, in that you may have multiple nodes to store data should your system require them.
Storing the data in a data class GLOMmed onto the area node is trivial to implement but has a disadvantage in that the root node becomes a harder to work with as it has more classes added to it increasing the odds of method conflicts and conflicts with how a particular field is used. Please note, when GLOMming it is important to make sure you are not reusing fields because we do not have multiple inheritance. (Or rather we do sort of, in that it is fine to inherit the same field from multiple classes, but you end up with just ONE of those fields leading to the possibility of multiple systems using the same field differently...very bad).
Example
Monster generators (aka Mongens) contain data that is specific to a particular area. Mongens are set up in the edit instance of the area as persisted nodes that are associated to the area root node with a base_hard_association. Consequently, those mongens exists in all play instances of a particular area, any changes in one play instance are local and are not persisted so that timers can fire, counters can increment all without changing the persisted version in the edit instance. In this way, we are guaranteed that each new instance of the area is set up properly in its default state.
Data Storage in System Areas
Summary
Storing data in a System Area makes sense for a system when:
- Data frequently changes
- Data must be modifiable in production(live) worlds
- Asynchronous access to the data is acceptable
- Data must be availible to all Instances
- Data may require significant processing
- Data may have a large memory footprint
A System Area is essentially an Area server that is dedicated to some sort of processing rather than managing players and geography. An Area can be designated to handle some particular game mechanic, system or data store. Additionally, multiple instances of a System Area can be spun up to dynamically distribute load.
System Areas are a clever way of utilizing the instanced nature of HeroEngine's Area servers to implement extremely scaleable systems and offload large data sets or computationally intense systems from other servers. The World Area instance is a special type of System Area, but there can only be one World Area server. Custom Area servers can be utilized to implement any sort of parallel processing that is required.
Advantages | Disadvantages | ||
---|---|---|---|
Scaleability | Very High to Extremely High | Technical Difficulty | High to Very High All of the common problems associated with database programming, such as collisions. Asynchronous programming |
Data Read/Write Access | very fast(r) fast(w) Asynchronous Access speed is dependent upon Inter-Process Communication speed (x2) plus the time required to query the data in the System Area via script. |
Crash Tolerance | varies Low to very high dependant on the skill of the system's coder. Ideally, system areas must be able to seamlessly recover from crashes |
Supported Frequency of Data Access | high to very high | ||
Queryability | high By the very nature of system areas, built-in support for querying data in the system area is almost a requirement. |
||
Ability to Handle Large Data Sets | very high | ||
Scope | local GOM Systems created using a system area by their nature require they expose easy to use interfaces. Consequently, the local scope is not a disadvantage from the perspective of a user of the system. |
Systems that Use System Areas for Data Storage
- Group Management
- Mission System Database
Design
System Areas communicate with normal areas through Inter-Process Communication. They do not handle users directly as do normal Areas.
The creation of a properly designed System Area is probably one of the most challenging scripting tasks possible in the HeroEngine environment. System areas mix the complexities of asynchronous environments with those of database design. Plus, often there is a need to implement a Client GUI allowing GameMasters to easily manipulate System Areas (or the data they manage).
Although complex, System Areas allow for you to design arbitrary units of computation that can scale well.
See also: Asynchronous Considerations
Example
Hero's Journey has an extremely complex quest system that assembles virtually unique quests for characters based on a large number of factors. This requires a significant amount of processing time as well as a very large data set from which a fully fledged quest is assembled. Those factors required the implementation of the first and most complex system area.
The quest system area acts as a repository for the quest part specifications and the processor in charge of assembling one or more potential quests for a character. System scaleability is achieved by the implementation of the edit instance as a router/administrator for 1...n play instances of itself. As the system load changes, the edit instance is able to spin up/down play instances and adjust its routing of requests to balance the load.
The play instance, each its own thread, handle the processing for querying and assembling quests for a particular character.
Data Storage at the World
Summary
Storing data in the World (area 0, instance 0) makes sense for a system when:
- Data may change
- Asynchronous access to the data is acceptable
- Data must be available to all instances (via Inter-Process Communication)
- Data has a relatively small memory footprint
- Extremely low processing time required
Typically the only data stored at the world level is data that is required for the world to handle the very limited functionality implemented there. When implementing a system that is going to store data in the world area instance, an evaluation should be performed to determine whether or not that data really belongs there rather than some other data storage option.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | Medium | Memory Footprint | restricted to low by policy |
Data Read/Write Access | very fast(r) fast(w) Asynchronous Access speed is dependent upon Inter-Process Communication speed (x2) plus the time required to query the data in the world via script. |
Ability to Handle Large Data Sets | low Data occupies memory in the world server and uses processing time to query the data. |
Supported Frequency of Data Access | medium | Queryability | varies The data is available on the world server to any process via Inter-Process Communication, but does not have built-in support unless a coder added it. |
Processing Restrictions | Processing required to work with your data on the world must be minimal in nature to ensure the scaleability of the world server. | ||
Scope | local GOM global via Inter-Process Communication |
Design
Assuming you understand asynchronous programming and Inter-Process Communication, storing data at the world level is a fairly simple procedure. You must create a persisted node from some class that has the field(s) you need for data storage. The node must be associated with the worldroot via at least a base_hard_association.
To access/write information you will perform Inter-Process Communication to the world to your script and perform whatever updates you wish to persist or retrieve whatever information you need.
Optionally, you could GLOM your dataclass onto the world root node directly.
See also: Asynchronous Considerations
Example
The travel system handles moving characters from one instance to the next, on occasion the destination instance is not currently running. To handle the case where an instance is not yet ready for a character, the travel system stores a queue on the world of pending travel requests. When an instance spins up, it checks the queue to see if there are any pending requests and processes them.
The world area instance is ideally suited to this particular data storage because the callbacks from the C++ Engine happen in the world area instance.
Data Persisted for a User
Overview
Data storage with the Account Node
Summary
Storing data with the account makes sense when:
- Data is specific to an account but not a specific character
- Data may change frequently
- Fast synchronous access for reading and writing desired and the data does not need to be accessible if the account is offline
- Asynchronous access is acceptable if other AreaInstances need to know about the data stored with an account node.
Data stored on or associated to the account node should be data that is shared between all characters of the account. Any character specific information should be stored with the character node instead of the account node.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | low | Memory Footprint | varies Some consideration should be given to the memory footprint of the data. |
Data Read/Write Access | extremely fast(r) fast(w) Synchronous |
Ability to Handle Large Data Sets | low Data probably exists for each account, this becomes a potential scaleability issue. |
Crash Tolerance | high | Scope | local GOM Only accessible when the account is connected. global Via Inter-Process Communication |
Design
There are several potential methods of storing the data with the account: creation of a node associated to the account node or a class that is GLOMmed onto the account node.
Instantiate a persisted node from a class containing the field(s) you need to store your data and associate that node to the _playerAccount node. If necessary, you may wish to consider creating your own association definition.
- Create Persisted Node From Class("classname")
- Add Association base_hard_association
- Add option soft associations to make finding your data object easier.
GLOM your data class onto the character. Please note, when GLOMming it is important to make sure you are not reusing fields because we do not have multiple inheritance. In other words, if your class has the account_name field and you glom that onto the _playerAccount node that already has that field you can mess up other systems if you change the field's value to something other than what the system requires.
For many applications that are specific/atomic in nature, it is safer to create a persisted node and create an association when working with the root nodes.
Data storage with the Character Node
Summary
Storing data with a character makes sense when:
- The data is specific to a character
- Data may change frequently
- Fast synchronous access for reading and writing and the data does not need to be accessible if the character is offline
- Asynchronous access is acceptable if other AreaInstances need to know about the data stored with a character node.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | low | Memory Footprint | varies Some consideration should be given to the memory footprint of the data. |
Data Read/Write Access | extremely fast(r) fast(w) Synchronous |
Ability to Handle Large Data Sets | low Data probably exists for each account, this becomes a potential scaleability issue. |
Crash Tolerance | high | Scope | local GOM Only accessible when the character is loaded. global Via Inter-Process Communication |
Design
There are several potential methods of storing the data with the character: creation of a node associated to the node or a class that is GLOMmed onto the node.
Instantiate a persisted node from a class containing the field(s) you need to store your data and associate that node to the _playerCharacter node. If necessary, you may wish to consider creating your own association definition.
GLOM your data class onto the character. Please note, when GLOMming it is important to make sure you are not reusing fields because we do not have multiple inheritance. (Or rather we do sort of, in that it is fine to inherit the same field from multiple classes, but you end up with just ONE of those fields leading to the possibility of multiple systems using the same field differently...very bad).
Example
Character abilities in Hero's Journey are at some level customizeable by the player. The basic implementation of immutable data is in the form of a Spec (check out storage in specs on this page) and the mutable data is stored in a node derived from the SpecDerivedObject class. Each character has an abilities node which is associated to the character node via a base_hard_association. Each ability is in turn associated to the character's abilities node.
This provides for character specific storage of mutable data for a particular ability, while maintaining the smallest possible memory footprint by storing the immutable data in a spec.
Data Storage in the Repository
Overview
HeroEngine's repository is a directory of files that exist on both server and client. If the client does not yet have a file, or the file is invalidated due to the existence of a update, the server automatically streams the file on demand to the client.
The implementation of the repository mechanics make it ideal for storing data is well suited for being cached locally to minimize bandwidth consuption.
Storing a Node in the Repository
Summary
Storing data in a node in the repository makes sense when:
- Data rarely changes
- Data is generic in nature, such that it makes sense that all clients share the same data
- Data needs to be available on both server and client
- Class structure exists both client and server with identical IDs (via world deploy).
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | medium | Data Read Access | varies Initial read is an I/O operation (asynchronous) and is slow, subsequent reads are extremely fast assuming you keep a reference to the node. |
Scaleability | extremely high Efficient transmission of rarely changing data. |
Data Write Access | slow Writing to the repository file is probably only valid in the development instance as it requires all of the clients redownload the file the first time they attempt to access the data. |
Supported Frequency of Data Access | high | ||
Scope | client and server |
Design
See also: Repository Node Serialization
Storing Arbitrary Data in the Repository
Summary
Storing arbitrary data in the repository makes sense when:
- Data rarely changes, data is generic in nature
- Data needs to be available on both server and client
- Data is arbitrary in nature and can be represented in a string.
Using the repository to store arbitrary data, it is possible to store anything that can be serialized into a string format. Depending on the complexity of the data, the serialization step may be a significant hurdle for implementation.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | medium | Data Read Access | varies Initial read is an I/O operation (asynchronous) and is slow, subsequent reads are extremely fast assuming you keep a reference to the node. |
Scaleability | extremely high Efficient transmission of rarely changing data. |
Data Write Access | slow Writing to the repository file is probably only valid in the development instance as it requires all of the clients redownload the file the first time they attempt to access the data. |
Supported Frequency of Data Access | high | ||
Scope | client and server |
Systems that use the repository to store aribrary data:
- Spec System
Design
Consideration must be given to the processing time required to deserialize the data when the repository file is read into memory. String manipulation is, as in any other language, relatively slow compared to other operations. An obvious optimization is caching the information once it is read into memory in a node, associated to a root node in some well known fashion for subsequent ease of reference.
The linked page provides details on the technical implementation of a system storing arbitrary data in the repository.
See also: Repository Data Storage
Example
Specs (see storage in specs on this page) store templated data that needs to be shared with the client. It would be inefficient to transmit that data each time a client logged into the game, so specs instead write themselves out to files in the repository in string form. On the client when a spec is requested, the file is read into memory and cached in a node.
Node Delivery Service
Storing Data using the Node Delivery Service
Summary
Storing data using the node delivery service makes sense when:
- Data is serverside
- Data should not always be loaded with the node to which it is linked
- Asynchronous access to the data is acceptable
- The frequency of read/write operations is very low.
At the most basic level, the node delivery serivce provides the mechanics for "mailing" a node between characters or servers. This has obvious applications for an for game systems like an auction system and in-game mail system.
Clever use of the node delivery service can significantly reduce the memory footprint of a character in memory, making this a potentially important tool in efforts to ensure maximum scaleability of a system that requires a large number of data nodes that do not always need to be loaded with the character.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | medium | Data Read Access | varies |
Scaleability | extremely high | Data Write Access | slow Asynchronous Writing data requires you first load the existing data node from a mailbox or create a new one and send it to a mailbox. |
Supported Frequency of Data Access | low Retrieving nodes stored in mail requires they be loaded from the database, this is slower than a normal external function. |
||
Scope | local global via Inter-Process Communication |
Design
See also: Node Delivery Service
Example
Hero's Journey in-game mail system uses the node delivery service allowing characters to "mail" nodes such as a sword to another character regardless of whether or not that character is ingame.
Local Server Non-Persisted Storage
Overview
Storing non-persisted data locally is essentially the same as persisted data, the only signficant difference is working with non-persisted nodes instead of persisted nodes. However, there is an additional option availible for the storage of non-persisted data and that is system nodes.
System Nodes
Summary
Storing data in a system node makes sense when:
- Data has a default value, which is only updated in a development world
- Easily accessible from script
- Fast synchronous access required
- Changes made to the data are not persisted
- Changes to the data need not be reflected to other instances
![]() |
The prototype that represents the system node, like all prototypes, may not to be modified in a production(live) world. It is only the non-persisted copies that may be modified. |
System nodes are a specialized type of prototype that the HSL compiler knows how to locate using a well-known address. When a system node is first referenced in a local, a non-persisted copy of the system node's prototype is created and thereafter used for all subsequent references. Each area server has its own copy of the data in a different non-persisted node permitting the local server to make any changes or anchor any supplementary nodes to it as needed.
The system node will have default values that are identical to the system node prototype's values when first instantiated. System nodes also serve as an object upon which you may call any methods that might exist for a particular system.
Advantages | Disadvantages | ||
---|---|---|---|
Technical Difficulty | medium | Data Write Access | varies non-persisted write access to the local copy of the prototype is permitted No write access permitted in production(live) worlds to the system node prototype Slow write access in the development environment Writing to a prototype is a coordinated DOM change |
Data Read Access | extremely fast Synchronous |
Memory Footprint | varies |
Supported Frequency of Data Access | high | ||
Queryability | high Data is exposed equally to all systems |
||
Crash Tolerance | very high | ||
Scope | global or local client GOM Server system nodes are global in scope Client system nodes are local client GOM in scope |
Design
The design for HSL implementations for HeroEngine required that licensee's be able to override and extend behaviors. Using an object oriented approach suited the design requirement but requires that a node exist upon which method calls may be made and as a repository for any data required by the system.
System nodes are based on a prototype, when a system node is first referenced in script (e.g. SYSTEM.NODE.BASECLIENT or short hand $BASECLIENT) the engine creates a non-persisted copy of the prototype in the local GOM and maps the SYSTEM.NODE.PrototypeName system variable to the id non-persisted copy. The non-persisted copy may then be modified without affecting the prototype (which is not allowed in production worlds) or the copies of any other GOMs.
See also: System Nodes
Example
Development of a clean HeroEngine for distribution to Licensees required provision for a licensee to override certain Required System functionality to implement whatever game-specific behavior is desired. System nodes, like SYSTEM.NODE.BASECLIENT, implement unique methods for the callbacks from the C++ engine that then process any required code and make calls to game specific implementations for extension/overriding behaviors.
For Hero's Journey, the game specific implementation of BaseClient is found in the HJBaseClientClassMethods script. The prototype named BaseClient, located in the server's persisted client DOM (use the | in the CLI to access this), has the HJBaseClient class GLOMmed on.
Taking a look at the _BaseClientClassMethods script's implementation of _Area_Load:
unique method _Area_Load(areaID as ID, instanceID as ID, AreaName as String) // Called when Area_Loading is done. // Note that AreaName is the area's name according to the Spec panel in HeroBlade, not // the name according to the Organizer panel. // Take away the splash screen. me._SetAreaName( areaName ) // This always happens because some Required Systems // use the area name handled as Boolean if hasMethod( me, "HE_Area_Load" ) // Game-specific implementation handled = me.HE_Area_Load( areaName ) . if not handled // default implementation(if any) if not // handled or the game specific implementation // does not exist . .
This example does not demonstrate a pattern likely to be duplicated in systems that are not part of the HeroEngine Required Systems, it does demonstrate how you can use system nodes as objects upon which methods may be called and non-persisted data may be anchored (via associations). The implementation of SYSTEM.NODE.Name and $Name enables a scripter to use the nodes without knowing or caring what the ID of the node might be in this particular local GOM.
Arbitrary Root Nodes
- See also: Arbitrary Root Node