Virtual Universe 2 Programmer’s Reference
Version 2.0
December 5, 1996
Spectrum Holobyte, Inc.
Virtual Universe 2 Programmer’s Reference
Introduction
System Configuration
vu2.h:
apitypes.h:
Conventions
Overview
Chapter 1: Getting Started
Includes and Defines
Vu Required Globals
The Entity Type Table
Vu Required Functions
Main Function
Chapter 2: Architecture of the Virtual Universe 2 System
Overview
Entity Database
Message Management
Entity Database
VuEntity Sub-component
VuCollection
VuDriver Sub-component
VuThread Sub-component
VuSession Sub-component
Message Management
VuMessage Sub-component
VuMessageQueue Sub-component
Chapter 3: VuEntity Use and Implementation
General Class Organization
VuEntity Class Definition
Creating a New VuEntity Subclass
Step 1: Define the entity types and the type table data
Step 2: Define the new class
Step 3: Implement the new class
Step 4: Implement VuxCreateEntity
Chapter 4: VuCollection Use and Implementation
VuCollection Class Definition
VuIterator Class Definition
VuFilter Class Definition
Creating a New VuFilter Subclass
Step 1: Define the new class
Step 2: Implement the new class
Chapter 5: VuMessage Use and Implementation
VuMessage Constant Definitions
VuMessageQueue Class Definition
VuMessageFilter Class Definition
VuMessage Class Definition
Creating a New VuMessage Subclass
Step 1: Define the message type
Step 2: Define the new message class
Step 3: Implement the new message class
Step 4: Implement or update the VuxCreateMessage function
Chapter 6: VuDriver Use and Implementation
VuSlaveSmootherSettings Class Definition
VuDriver Class Definition
Creating a New VuDriver Subclass
Step 1: Define the new driver class
Step 2: Implement the new driver class
Step 3: Modify the transfer event handler for the associated entity
Step 4: Modify the VuxCreateEntity function to create and add a slave
Step 5: Hook up the driver after creating a local entity
Chapter 7: VuThread Use and Implementation
VuThread Class Definition
VuMainThread Class Definition
Using the VuMainThread Class In a Networked Environment
Creating and Using a Custom VuThread Class
Step 1: Define the new class
Step 2: Implement the new class
Chapter 8: VuSession Use and Implementation
VuSessionEntity Class Definition
VuGroupEntity Class Definition
VuPlayerPoolGroup Class Definition
VuSessionsIterator Class Definition
Creating a New VuGroupEntity Subclass
Chapter 9: Frequently Asked Questions
Question 1: How do I get all entities from the network?
Question 2: How can I tell when I’ve received all entities from the network?
Step 1: Define new unique message types
Step 2: Define some globals
Step 3: Define the new message classes
Step 4: Implement the new classes
Step 5: Use the new classes (and globals) in our game connection and main loop code
Question 3: How do I save/restore entities to/from a file?
Step 1: Create the callback functions
Step 2: Call the database save and restore functions
Question 4: How do I manage my own collection?
Step 1: Define the new collection subclass
Step 2: Implement the new class
Step 3: Use the new class interface
Question 5: How do I transfer entity ownership?
Question 6: How to I ensure that two entities are managed by the same session?
Question 7: How do I do my own load balancing?
Step 1: Create a new group entity subclass
Step 2: Implement the new class
Question 8: How do I manage multiple domains?
Question 9: How do I do entity Aggregation and Deaggregation?
Tool 1: VuManageEvent, VuUnmanageEvent, and VuReleaseEvent
Tool 2: Entity association
Tool 3: VuAntiDatabase
Tool 4: VuGridTree
Chapter 10: Sample Application Notes
Test
NetTest
NetWatch
Virtual Universe 2 Programmer’s Reference
Vu2 is designed to be used as a run time entity database in real-time games. The network support provided is an implementation of an asynchronous distributed system. As such, no server is required (though the game can implement and require one), and there is no requirement to frame based blocking on network events (though the game can implement its own rendezvous system on top of Vu). Though the events and entities are obviously designed around a real time vehicle simulation, it has been used for systems as mundane as a game building tool, which managed files and textures instead of F16’s, and which used the network facility only to turn the tool into a workgroup tool, where entities could be locked on the network while being edited, and the latest results be exported to others on the net as soon as the changes were saved.
Vu2 was designed and implemented from the ground up as an object-oriented system. It uses C++ as the host language, and though the code should be portable (except for the network layer), it was designed for and only tested on a Windows 95 platform. In general, the use of C++ features were kept to a minimum, both to keep the design simple and easy to understand, and to give today’s compilers less of an opportunity to screw things up.
The Virtual Universe System consists of two main components -- the Entity Database and the Event Manager. Although the operation of these two components is somewhat segregated, they are designed to work together, and it is inadvisable to attempt to operate the Entity Database without the Event Manager. This is primarily because of the way the Entity Database works to ensure that an entity is not released (and it’s allocated memory freed) until after all threads and internal Vu contexts have been informed of the pending delete.
One of the driving factors in the design of Vu and the selection of C++ as the implementation language was to ensure that Vu could very easily be customized to fit the needs of the target game without modifying the Vu code itself. This is accomplished primarily though the judicious use of inheritance and strategically placed virtual functions. In addition, Vu provides some key helper components, such as filters and iterators, which are small classes which work with the other Vu classes to modify the behavior of those components. For example, a VuFilter subclass can specify the sort order for an ordered collection (such as a VuOrderedList or a VuRedBlackTree), as well as set membership (specifying whether or not a given entity belongs in a collection at all).
Vu2 was tested with MS Visual C++ version 4.1, though the source release uses a makefile approach (using nmake) rather than the MSVC project files. This allows the most flexibility in good source code control and independence from client project configuration. Some client projects have extended the makefiles, merging the Vu library with their own environment, while others have integrated the Vu source distribution into their own project files.
The Vu package is delivered with the following directory structure:
\VU2 top level directory. Any readme files that describe changes to the system will be placed here.
\VU2\INCLUDE this contains the internal Vu header files (vu.h, vucoll.h, vudriver.h,
vuentity.h, vuevent.h, vumath.h, vusessn.h). All of these files are included by
vu2.h (below).
\VU2\INCLUDE2 this contains the exported header files vu2.h, apitypes.h, and netapi.h.
All of these files are considered modifyable by the client project.
\VU2\SRC this contains the source modules for the Virtual Universe System.
\VU2\LIB this contains the library modules for the Virtual Universe System.
\VU2\COMMS this contains the source modules for the Windows 95 comms layer supplied
with Vu2. This can be replaced or turned off as needed.
\VU2\VUTEST this contains a simple sanity test program, source and makefile. It does not
test comms, but does test basic collection functionality.
\VU2\NETTEST this contains a simple net test program, source and makefile. It exercises most
of the Vu system, including network connectivity, entity transfer, and collision.
\VU2\NETWATCH this contains a simple netwatch application, source, and makefile. This
app can be useful for debugging network connectivity problems.
The \VU2\INCLUDE2 directory contains the files that Game Developers will need to include in their source code if they wish to use the Virtual Universe system. See the INCLUDE FILES section for a detailed description.
The \VU2\SRC directory contains the source code for the entire Vu system:
vu.cpp the Vu initialization and ‘thread’ implementation.
vuentity.cpp the base entity class implementation.
vudriver.cpp the ‘driver’ implementation, including dead reckoning and smoothing.
vusessn.cpp the VuSessionEntity and VuGroupEntity implementation
vucoll.cpp the VuCollection implementation (the entity database)
vuevent.cpp the Event and Event Queue implementation
vu_priv.h certain definitions and declarations needed by Vu, but not by clients.
While the emphasis for the Virtual Universe has been directed towards multi-player situations, single player systems can also use the Virtual Universe to help organize data.
System Configuration
The following bullets list the game developer configurable elements of the Vu system.
If you are compiling this system from the source code, the following define switches are supported:
VU_USE_COMMS -- define this to activate comms support. If inactive, the event system will
never dispatch messages to the comms layer nor poll for events.
VU_AUTO_UPDATE -- Sets up and maintains a stream of VuFullUpdate events, send at standard
time intervals defined in the type table where time is marked by the
vuxGameTime variable.
VU_THREAD_SAFE -- Causes activation of VuEnterCriticalSection and VuExitCriticalSection,
which must be defined by the game programmer.
VU_AUTO_COLLISION -- Turns on automatic collision detection. This tests for collision between
all entities in the database which have the collision_ flag set to TRUE, every
frame. Most games will want to limit the scope of this a bit, although Vu does
a fair job of limiting collision checks to only those entities which are near to
each other (see VuGridTree for more details on this).
This #include file contains macros, typedef’s and data structures that are used through-out the Vu system. Game Developers should include this file and use the same data types to assure data compatibility. Note that scalar values are typedef’d to float in the distribution, but can easily be changed to double or converted to a fixed point class type implementation. Note that VU_SESSION_ID and VU_ID are defined as class types, with provided cast operations to unsigned short and VU_KEY (a typedef of unsigned long), respectively. In most cases, most compilers will to the appropriate automatic conversion, but explicit casting is required where varargs are used (as in printf).
Vu strictly adheres to several conventions. Understanding these conventions makes it easier to understand the code.
Naming Conventions
Below are the naming conventions used in Vu. All names use descriptive intercap, and underscores are only used in macro names and as a trailer to member variables.
Return Value Conventions
In general, return values fall into five general categories:
One of the most significant features of the Vu Entity Database is its ability to manage entity utilization without reference counting or garbage collection. Very basically, entities are allocated using the new operator of C++ and then inserted into the Vu system by calling vuDatabase->Insert() with the new entity. Vu is then responsible for calling the delete operator when it is safe to do so. As a result, application code should not call delete on VuEntity’s which have been added to the entity database. This is a slight divergence from the usual mode of memory management, but adopting the Vu convention does pay off. There are also two other Vu components with slightly different memory management conventions as well. All three are summarized below:
Allocation: use standard new operator
Notification: vuDatabase->Insert(ent) – adds to Vu database
Deallocation: vuDatabase->Remove(ent) – removes from Vu database, creates and posts a VuDeleteEvent, and puts it on a kill queue for delete at a future time.
Allocation: use standard new operator
Notification: VuMessageQueue::Post(message) – posts message to all interested queues
Deallocation: No action required. Vu does this automatically after all queues have dispatched it.
Allocation: any method – can even be done on stack
Notification: Pass filter pointer to Vu function (usually a constructor or iterator function). Vu will make a copy on the heap if it needs to keep it around.
Deallocation: user must free the filters they pass to Vu, Vu will take care of the copy.
The following figure illustrates how the Vu Entity Database and Event Manager work together with the game code and communications layer.
This section briefly explains the bare minimum skeleton required to integrate and run with Vu 2.0. The code provided here can be used as a starting point for integrating the game code with Vu, or copy/edit of the test code found in vu2\test or the zoomers code found in vu2\nettest.
The sample provided here is really bare bones. The entity class used is just the raw VuEntity, which has only the most basic of variables. In addition, only one type of entity is provided, game time uses a positively silly increment scheme, and the main loop is also a toy example. Since the nature of the game loop and time keeping are generally unique for each game (and generally involve a fair amount of setup code) the simple schemes provided here should be a sufficient place holder. Finally, this example assumes that VU_USE_COMMS is not defined (see the chapter on VuSession to get information on how to connect to a network).
The organization of this example will include brief snippets of code with interspersed annotation.
This example includes stdio.h (for printf) and vu2.h (for vu). As stated earlier, we are assuming that VU_USE_COMMS was not defined. For that matter, let’s assume that none of the configuration options are defined.
#include <stdio.h>
#include "vu2.h"
These defines create our entity type so that we can determine at run time what kind of an object we have from it’s VuEntity pointer. This allows us to safely cast to a more specific class type given a generic pointer. This is quite useful as all of the iterators return pointers to the VuEntity rather than the specific subclass. In addition, associating a type with a class allows us to create an instance of the correct class type from a data stream received from the network or from a file. Note that the entity type is assigned at a value higher than the value of VU_LAST_ENTITY_TYPE. It is important that the game programmer adhere to this practice so as not to conflict with internal Vu entity types.
The domain defined is a number between 1 and 31. It is used to properly set the vuxLocalDomain variable domain below. Basically, the vuxLocalDomain is a bitmask which indicates which kind of entities the current program is capable of managing. Each entity notes what domain it belongs in, which allows the program to quickly identify whether or not the entity can be correctly handled or managed. This subject will be dealt with in more detail in the FAQ.
The database size is the size of the hash table used as the Vu database. This number should be a prime number, and should be around 10%-20% larger than the largest expected database size, though performance will not suffer greatly if this value is exceeded.
#define VU_SAMPLE_ENTITY_TYPE (VU_LAST_ENTITY_TYPE + 1)
#define VU_SAMPLE_DOMAIN 1
#define SAMPLE_DB_SIZE 101 // size of database hash table
Vu requires that the application define, initialize and maintain four global variables. Of these, only vuxCurrentTime needs constant update. All of these variables are defined as external because Vu itself has no way of knowing what the proper values of these variables should be. vuxCurrentTime is the global which defines the current game time as used by all entities for position, movement, smoothing, etc. vuxSlaveSmootherSettings is needed for dead reckoning and smoothing in a network environment and is rather meaningless in a single player environment like the one we have in this example (but is still required, and is fairly small and as such is included in this example). vuxLocalDomain was discussed above, while vuxGameName is just a string to uniquely identify network packets from this game when they are broadcast over the net (so that other games also using Vu which happen to receive these packets can drop them).
VU_TIME vuxCurrentTime = 0;
VuSlaveSmootherSettings *vuxSlaveSettings = 0;
ulong vuxLocalDomain =
((1<<VU_GLOBAL_DOMAIN) | (1<<VU_SAMPLE_DOMAIN));
char *vuxGameName = "Vu2Sample";
The entity type table in our toy example only defines the VU_SAMPLE_ENTITY_TYPE identified in the defines section above. This is the data for the VuEntityType field of our VuEntity. This table is used to return a pointer value from the VuxType() global function (below). Note that our example here uses a static table, but also that Vu only accesses this table through the VuxType() function. As such, it is perfectly okay for an application to populate this table with data read from a file instead of the static method used here. The only requirement is that the VuxType() function return a valid pointer which is guaranteed to remain valid from the time Vu is initialized until Vu is shut down.
VuEntityType vuTypeTable[] = {
{ VU_SAMPLE_ENTITY_TYPE, // class id
1, (SM_SCALAR)10.0, // collision type, radius
{1,2,3,4,5,6,7,8}, // class info
VU_TICS_PER_SECOND*10, // update rate
VU_TICS_PER_SECOND*60, // update tolerance
0x0, 100, // damage seed, hitpoints
1, 0, // major & minor rev numbers
VU_CREATE_PRIORITY_BASE+1, // create priority
VU_SAMPLE_DOMAIN, // management domain
TRUE, // transferable
FALSE, // local
TRUE, // tangible
TRUE, // collidable
},
};
Very quickly, the elements of this table mean the following:
Vu requires that the application define six global functions.
VuMessage *
VuxCreateMessage(ushort type)
{
return 0;
}
VuxCreateMessage is responsible for translating a message type to a newly allocated instance of the appropriate message. As we are defining no custom message types, returning 0 is the expected behavior. Where a valid pointer is returned, Vu will call the Read() method of that message with a datastream to populate the message instance.
void
VuxGameTimeSubtract(ulong delta)
{
vuxCurrentTime += delta;
}
void
VuxGameTimeAdd(ulong delta)
{
vuxCurrentTime -= delta;
}
These two messages are called when Vu detects a time difference between the network clock and the local machine’s clock. The network keeps time in milliseconds, and so if the local game time is kept in different units, conversion will need to take place.
VuEntityType *
VuxType(ushort id)
{
VuEntityType *retval = 0;
switch (id) {
case VU_SAMPLE_ENTITY_TYPE:
retval = &vuTypeTable[0];
break;
}
return retval;
}
This is the VuxType function mentioned above. It takes an id and maps it to a VuEntityType pointer. As mentioned above, this value must be a valid pointer over the lifetime of the execution of Vu, but may otherwise be set using any method.
VuEntity *
VuxCreateEntity(ushort type, ushort size, VU_BYTE *data)
{
VuEntity *retval = 0;
switch (type) {
case VU_SAMPLE_ENTITY_TYPE:
retval = new VuEntity(&data);
break;
}
return retval;
}
VuxCreateEntity is used to translate a type and data into a valid newly allocated and populated class instance of the given type. This is used for translating a data stream received from the network or from a file into a valid local entity pointer. Where Vu calls this function, it takes care of adding it to the database, so there is no need for this function to do so.
The main function provided here glues everything together. As the code itself is relatively small and easily understood, this annotation will not delve into much detail.
int
main(int argc, char *argv[])
{
VuEntity *sample;
// initialize the slave smoother settings
vuxSlaveSettings = new VuSlaveSmootherSettings(
(SM_SCALAR)2.0, // x tolerance
(SM_SCALAR)2.0, // y tolerance
(SM_SCALAR)2.0, // z tolerance
(SM_SCALAR)0.57, // yaw tolerance
(SM_SCALAR)0.57, // pitch tolerance
(SM_SCALAR)0.57, // roll tolerance
(SM_SCALAR)10.0, // max Jump distance
(SM_SCALAR)1.57, // max Jump Angle
VU_TICS_PER_SECOND*1); // lookahead time (seconds)
// create and initialized the main thread
VuMainThread myThread(SAMPLE_DB_SIZE);
// create a sample entity
sample = new VuEntity(VU_SAMPLE_ENTITY_TYPE);
// initialize important variables
sample->SetPosition((SM_SCALAR)0.0, (SM_SCALAR)0.0,
(SM_SCALAR)0.0);
sample->SetDelta((SM_SCALAR)0.0, (SM_SCALAR)0.0,
(SM_SCALAR)0.0);
// insert entity into the vu database
vuDatabase->Insert(sample);
VuDatabaseIterator iter;
printf("printing database \n");
for (sample = iter.GetFirst(); sample; test = iter.GetNext()) {
printf("Testing: 0x%lx, type = %d\n", (VU_KEY)test->Id(),
test->Type());
}
while (vuxCurrentTime < 500000) {
vuxCurrentTime++;
myThread.Update(vuxCurrentTime);
}
delete vuxSlaveSettings;
return 0;
}
The only "magic" parts of this sample are in the initialization of the VuMainThread and the database iteration. When the VuMainThread is initialized, it also initializes all internal variables needed by Vu (most notably the vuDatabase pointer used later in the function). Note that despite its name, this object does not start up a Windows 95 thread or any such thing, but is rather a software representation of the Vu context for such a thread. For multithreaded access to Vu, each thread should have its own VuThread object, and each thread should call the VuThread Update() function periodically to allow Vu to do cleanup, message posting and processing, etc. Note also that the database size is passed into the constructor for the VuMainThread. This value is just passed on to the vuDatabase constructor.
The other "magic" part is the iterator which is used to iterate across the database. This iterator is an external context to the database collection which allows multiple pieces of code to simultaneously iterate across the same collection without impacting each other. In fact, because of the way Vu manages its memory, it is even possible for different contexts or threads to remove entities from a collection without impacting each other. The only odd thing about iterators is that the GetFirst/GetNext calls are made on the iterator rather than the collection. But then this is only odd because it is unfamiliar. Once this pattern falls into general use, it becomes as easy to use as stdio.
Finally, cleanup for Vu happens when the destructor for the VuMainThread is called. Because we created our main thread on the stack, this happens last, just before we return from main. This is exactly as it should be, as the Vu cleanup should happen after all other vu components are cleaned up. Vu will also purge is database on shutdown and call the destructor for each of these entities.
Going forward, this document will discuss some of the other features of the Vu system when the components are introduced. For example, we will look at how to create our own entities when we look at the VuEntity class. But first, we will look at the design of the Vu system as a whole and how each of the parts fit together.
Chapter 2: Architecture of the Virtual Universe 2 System
This section will discuss the architecture of the Vu2 system. It will briefly discuss each of the components, and describe their relationship to each other. As this is a C++ component design, this section will use object-oriented language and inheritance diagrams to illustrate the concepts.
The basic design of VU is broken into two primary areas: Entity database and message management. The Entity Database includes many sub-components (VuEntity, VuCollection, VuDriver, VuThread, and VuSession) which will each be briefly discussed here and in more detail in the section dedicated to that sub-component. Message management provides a class based representation of events and requests as well as a standardized mechanism of posting and dispatching these messages in a network and a standalone environment.
The Entity Database is the largest component of the design, and is composed of several shallow class tree definitions. These are designed to be used together, but if some features are not needed or used, then some of them (such as smoothing and dead reckoning, and in fact the entire VuDriver class) can be removed without much trouble. The base classes defined are listed below:
It is important to note that most of the central functionality required by the game must be written by the game developer, but the interface provided by VU will make it simple for the game to save to / restore from disk, serialize over the network, transfer ownership of entities between machines, etc.
At its simplest level, all that the event module of Vu provides is a message queue and a well defined asynchronous interface to messages from remote machines. Vu also provides a class based interface to events, a large collection of pre-defined event types, and a simple method to add new event types. The base classes defined as a part of the Event Management component are listed below:
Note that the event queue interface will not support multiple indices. Instead, the interface will logically present a single queue to each thread (or code segment) which requires access. Events which are queued will be sent to each applicable queue with a single PostVuMessage call.
The Entity Database provides an interface to game data, organized into game objects. Game objects which are managed by VU are called VuEntities, named after the base class for all such objects managed by VU, the VuEntity class. The Entity Database provides functionality where possible, and interfaces where functionality cannot be easily or adequately provided by VU. In addition, the C++ inheritance and virtual function mechanism makes it relatively easy to override default VU implementation. Even so, virtual functions will be used as sparingly as possible so as to minimize the performance hit for common but trivial class data access.
The functionality and interface feature set provided by the Entity Database follows:
What follows is a discussion of each provided class tree in the Entity Database. The presentation begins with a class hierarchy diagram, followed by a brief discussion of the components and their roles. The class definition and the interface functions are discussed in more detail in the chapter dedicated to that component.
VuEntity Sub-component
Class diagram notation is relatively simple. Base classes are positioned at the top of the tree in ovals. Subclasses appear underneath, with arrows pointing to the immediate parent class. Classes supplied by VU are given in solid ovals, classes which must be supplied by the game developer are given in dashed ovals. Below is the VuEntity Class tree:
The base class for all entities managed by VU is the VuEntity. In addition, each ‘type’ of object in the game can have its own entity type, represented by an instance of a VuEntityType. Note that there is no intention to directly correlate a C++ class definition with a VuEntityType, though it should be possible to map a VuEntityType to a VuEntity subclass, just not the other way around.
The classes in this diagram are as follows:
The VuCollection classes provide entity collection, filtering, sorting, and iteration. In addition, it uses a simple database locking and mark and sweep garbage collection mechanism to provide safe access for multiple threads. The class hierarchy itself is given in figure 3.
The class in this diagram fall roughly into three categories, collections, iterators, and filters. In the figure, the dashed lines between iterators and collections indicate the association between the iterator an a filter. As is true with any class, iterators may be used with any subclass of the associated class. For example, a VuListIterator can be used with a VuOrderedList or VuFilteredList as well as a VuLinkedList. Following is a brief discussion of the three categories.
VuCollection Sub-component
The VuCollection class provides an interface to all collections of VuEntities. There is one special VuCollection, the vuDatabase (a global instance of the VuDatabase class). This collection is the superset of all entities managed by the game. The game notifies Vu of the existence of an entity by inserting the entity into the vuDatabae, and of the entities dismissal by removing it from the vuDatabase. Vu takes care of ensuring that from that time forward, all VuCollections and VuIterators currently active will accurately reflect the state of that object in a thread safe manner. In the case of entity removal, iterators currently pointing at the removed entity will not change, but the memory will not be release until after the iterator is no longer pointing at the entity. Also, iterators and Find() functions will not return entities which have been removed from the vuDatabase.
Briefly, then, here are the classes in the VuCollection heirarchy:
VuIterator
A VuIterator is responsible for maintaining the iteration context of an iteration. Making this context external to the collection itself makes it trivial to have multiple simultaneous iterations on a single collection. Instead of providing a GetFirst/GetNext interface on the collection itself, the GetFirst/GetNext interface is in the iterator. That way, subsequent calls to GetFirst will not affect the current node pointer as is the case with built in iteration. When the programmer wants to iterate across a collection, then, he should first create an iterator of the appropriate type on that collection, and then iterate using the iterator.
There is one base class and six basic iterator types:
It is possible and even likely that the game programmer will need to create specialized iterators. This can be accomplished by inheriting from the desired type and overloading the appropriate virtual functions. For more information on this, see the section on VuCollection or the FAQ section.
VuFilter
The VuFilter is the VuCollection component which is designed to customize access and behavior of VuCollection and VuIterator classes. A VuFilter defines set membership and relative sort order evaluation. Vu provides a few filter types which are used by Vu and may be used by the game programmer, though their actual utility may be somewhat limited. The VuCollection section will discuss in more detail how to construct specialized filters.
The filter classes supplied with Vu are the following:
A couple notes about the use of filters. Filters may be used either with a collection or with an iterator. If used with a collection, it defines set membership. In this case, since each collection is checked on insertion and removal of an entity to the vuDatabase, insertion and removal become more expensive operations. Removal, especially needs to be handled with care, and most notably with the collections with order-N Find operations (such as the linked lists). This is an important concern because removal of an entity that is not present in the collection presents the worst case traversal. To help alleviate this, there is a virtual method in the VuFilter called the RemoveTest. This test returns TRUE by default, and should return TRUE if the entity in question could ever have been in the collection, and FALSE otherwise. For example, if type is immutable, and your game has a custom collection of all F-16s, a VuFilter on this collection could have a RemoveTest which checked the type field, and returned FALSE for all non F-16 entities. This allows a trivial rejection test prior to embarking on the worst case iteration. Setting this RemoveTest return value incorrectly will have disastrous consequences, however, as the entity will remain in the collection, and the pointer will be deleted out from under it, causing an eventual program crash.
The other way to use filters is on iteration. The GetFirst and GetNext functions of iterators take an optional filter, where the filter tests for set membership on iteration. Any entity which does not pass the supplied filter will be skipped. This interface results in slower iteration (more tests), but no maintenance of another list.
The VuDriver hierarchy provides an abstract interface to the entity motion modeling system. Since this tree is completely separate from the VuEntity tree, it is also possible to very easily change the motion model for an object at run time. While it is unlikely that a missile will suddenly start behaving like a plane or a building, it is quite possible that a plane, missile, or mech will start getting its motion data from local modeling rather than network updates.
The VuDriver class is pure virtual abstract class which provides an interface to two types of motion models: master (physics modeling) and slave (driven by network updates).
The primary purpose of the VuDriver is to do entity position consistency management on the network. It can be thought of as an appendage to the VuEntity, and an optional one at that. It should be noted at this point that the VuEntity -VuDriver relationship is one of the few constructs that has architectural implications for the game programmer. The VuDriver is set up to work best as the repository for the physics modeling of the respective VuEntities. This modeling should be done in a subclass of the VuMaster. Taking this approach allows Vu to swap a VuMaster out for a VuSlave on remote machines where no physics modeling is done (because it is already being done on the remote machine). In addition, the VuDriver hierarchy performs dead reckoning (to reduce network update requirements, as well as providing cheap pseudo physics) and smoothing for VuSlave entities. Finally, using the VuDriver architecture as designed makes it relatively simple to perform event record and playback. This is accomplished by storing away VuPositionUpdateEvents as they come through, and then rerunning the game, instanciating each VuEntity with a VuSlave as its driver, and then feeding the event stream to the entities just as if all the entities were receiving the stored events as network updates.
The VuDriver class heirarchy is shown in figure 4:
A brief explanation of the various VuDriver classes follows:
One final note on the architecture of the VuDriver versus the VuEntity: In many cases, this design will result in parallel class trees, where, for example, there is a MyClassEntity class there will also be a MyClassDriver class. While such duplicity is rarely a desirable design characteristic (because it splits the data for single entity between multiple places), in this case, the payoff of motion modeling independence more than makes up for this slight design inconvenience. Note also that if this simply is not acceptable, then it is possible to ditch the entire VuDriver hierarchy. In this case, the game developer is responsible for manually perform all dead reckoning, smoothing, position and orientation updates, and generating position updates, as well as reconciling remote versus local physics modeling of the entity.
The VuThread sub-component is a fairly small but very important part of the Vu architecture. Each VuThread instance is a Vu software representation of a thread of control in the game. Note that the VuThread class does not itself provide any implementation of Windows 95 threads, but does provide a context which allows multiple threads to put a ‘bookmark’ in Vu which indicates which events that thread has processed, so Vu can know when all threads or software contexts have had an opportunity to handle a particular event. This is especially important with the VuDelete event, as all threads must have an opportunity to cleanup local references to the deleted entity. The best feature of this approach is that provided that thread event processing is done in the thread’s inner loop, it is guaranteed that no references to the deleted entity will be on the stack at the time of removal.
Without further ado, review the VuThread class hierarchy in figure 5:
And the class discussion:
Note that the VuMainThread also provides the interface to joining groups. This is done to provide a centralized, accessible interface that would otherwise require yet another set of global variables. Note that the act of joining a group also connects one to the network if this has not yet been done. More on this in the following section.
This sub-component is also rather small, but is similarly important for multi-player and network play. There are two basic types: the session and the group. Very simply, a group represents a collection of sessions, while a session represents a single player (or machine) on the network. Both groups and sessions are subclasses of VuEntity. This is true despite the fact that neither sessions nor groups are tangible objects in anyway, and as such have no real position or orientation. As a result, all such position and orientation data for these entities is meaningless. However, because they are VuEntities, Vu can use the Vu collection and iteration classes without modification. In addition, any state changes on these components can be communicated via the same messages as are used for other entities. This simplified the code quite a bit. The application programmer, however, needs to be aware of the fact that Vu entities such as sessions and groups will reside in the vuDatabase alongside those entities added by the game programmer. Running a flight model on a VuSessionEntity will have unpredictable results, so make certain to check the type returned by a interator, or create a filter or filtered collection for iteration of game entities.
Figure 6 contains the VuSession class tree:
This class tree has the VuEntity class in dashed lines, as that is discussed in more detail elsewhere in this document. Below are the classes of this sub-component:
Note that before a session joins a group, all entities currently owned by that session must be transferred to other sessions or removed, and must be purged locally. The exception to this rule is the local VuSessionEntity, and Vu handles the transition required here.
The message management component of Vu has many classes, but the design is rather simple. Basically, each message type is its own class, and the data of the class is the data for the message. Messages fall into three main categories: requests, errors, and events. Events represent a change in the state of a VuEntity, while requests represent a request for data or for another session to take action on an entity it manages. These events can be targeted either at a specific session (machine) or at a group of machines which may or may not include the sender. The VuMessageQueue class is responsible for accepting these messages and then dispatching them in a first in, first out queue.
The VuMessage class tree is quite large, given that there is one class for each message type, but it really is quite simple, given that it is a rather flat heirarchy. Figure 7 has an abbreviated class tree:
The classes of the VuMessage tree are enumerated below:
Before closing out this discussion of the VuMessage, I should say a few words about routing. When a message is created, one of two fields must be specified: target session or routing. The routing is a bitfield with five settings:
If the target session is set, it implies a routing of VU_SPECIFIED_TARGET, and vice versa. In most cases, events are routed using the VU_GROUP_TARGET routing only, and thus not handled by the source station (example: VuPositionUpdateEvent, as the position data is already by definition set and correct). Some Vu events use the VU_LOCAL_TARGET only (VuTimerEvent and VuReleaseEvent). This is how non-network events are implemented. The VuDeleteEvent are handled both by the sending and all destination stations, thus ensuring cleanup of the entity on all contexts on all machines. Since the route setting is a bitfield, multiple flags can be set. Thus an single event can be sent to the local machine, all machines in the local group, and a specified target.
Where a message is sent to a specified target, Vu will use a reliable messaging protocol, if one is available. For messages sent to multiple targets (group and remote), an unreliable broadcast protocol will be used (if available). The VU_BCGROUP_TARGET will use a multi-cast protocol to send the message to multiple stations, but the exact mechanism for specifying this is not yet defined. For now, setting the routing to VU_BCGROUP_TARGET acts the same as specifying VU_GROUP_TARGET.
The VuMessageQueue component is responsible for maintaining each of the message queues used by the system. Each instance of the VuMessageQueue is responsible for maintaining the dispatching of the messages from that queue, while any event posted to any queue is inserted into each of these instances. This way, each write goes into each queue, while all reads are handled by each queue at their own leisure. In most cases, an instance of a VuMessageQueue will be associated with each VuThread, and hence each actual software thread in the program. All VuMessageQueues, including the one associated with the VuMainThread, may specify a VuMessageFilter which can filter out messages by any criterion available. Note that the VuDeleteEvent and VuRemoveEvent cannot be filtered out, as Vu needs these to be posted and handled in order to guarantee safe cleanup of entities (though the game programmer does not have to do any special processing of these events).
A quick note about message handling: Except for messages which pass none of the message filters in any of the message queues, VuMessages are Activated on Post, and Handled on the first Dispatch call. Subsequent Dispatch calls have no effect. This is done because this would execute the same code multiple times, as each entity by default has no notion of which thread it is associated with when run, and in fact may be handled by multiple threads. Note also that the VuMessageQueue will modify the routing field of the VuEvent as a normal part of its processing of the message, so this field should not be checked or modified after creation.
The VuMessageQueue class tree is shown in Figure 8:
The classes in this sub-component are enumerated below:
This concludes the brief overview of the Vu architecture. Further discussion of the use and implementation of these components can be found in the sections which follow.
Chapter 3: VuEntity Use and Implementation
The VuEntity class is the base class for all game objects (entities) handled by the Vu system. In Vu, the base class has a superset of the needed data by most of the game objects ultimately used by most games. While this approach means that there will necessarily be some data which is not important to some entities or some games, it does simplify the handling of the objects quite a bit. The alternative would be a relatively deep object hierarchy for just the Vu portion of the class tree. By keeping location and orientation data for all entities regardless or whether that data is applicable to the object, we ensure that worst case mishandling of entities is to view and modify meaningless data (rather than scribbling over real data with meaningless values).
As this is the first section to examine one of the Vu class definitions, this is as good a time as any to discuss class construction conventions used throughout the Vu system:
Class member organization -- Classes are always laid out using the following template:
ClassName : public ParentClass {
friend Class declarations
public:
constructor definition (if this class is not pure virtual)
destructor definition
other public interface functions
protected:
constructor definition (subclass interface)
other subclass, friend class and in-class function definition
private:
in-class and friend only interface functions (very rare)
disallowed automatic functions (such as assignment operators)
// DATA (all data comes at the end, and is denoted by comment)
public:
public data (very rare, except for structures)
protected:
class data (most common)
private:
in-class or friend data (rare)
};
So the class interface is broken into three blocks: friend declaration, function interface, then data. Within each section, data is organized from most public to most private in order to present the interface of use first, so that it can more easily be located by users of the class. Data is kept together for two reasons: first, it makes the class look more like a C structure (which is what a class actually is, despite what C++ OOP fanatics tell you), and second, keeping the data together makes the class definition more closely resemble the memory footprint of the object as viewed in a debugger.
Below is the VuEntity class definition, along with annotation for each of the interface functions:
class VuEntity {
These are the friend functions and classes of VuEntity. In general, these can be ignored.
friend int VuReferenceEntity(VuEntity *ent);
friend int VuDeReferenceEntity(VuEntity *ent);
friend class VuDatabase;
friend class VuAntiDatabase;
friend class VuCreateEvent;
public:
There are three versions of the VuEntity constructor. Although these are public, the only time they would be called by anything other than a subclass is when instantiating a raw VuEntity (as is done in the Sample entity in the Getting Started section, above). In practical terms, then, these constructors will never be called by the game programmer directly.
VuEntity(ushort typeindex);
The typeindex constructor takes a type value which is used to set the VuEntityType pointer (finding this pointer via a call the VuxEntityType function).
VuEntity(VU_BYTE **stream);
This constructor takes a VU_BYTE stream double pointer. The data is read in assuming that it was put in with a Save() function call or other compatible means. The stream pointer is advanced to the end of the entity data as a side effect of this function call.
VuEntity(FILE *file);
This constructor is similar to the stream version, except that it operates on a file pointer instead of a VU_BYTE stream. As is the case with the stream version, this will advance the file pointer to the end of the expected entity record.
The following functions are grouped together and are called setters because they set member variables VuEntity class. In most cases, these are implemented as inline functions to eliminate the function call overhead. In this document the inline implementation has been removed to improve readability, but the actual class definition in vuentity.h has the inline expansion on the line of the definition. Functions will only be annotated where explanation seems necessary.
void SetPosition(BIG_SCALAR x, BIG_SCALAR y, BIG_SCALAR z);
void SetYPR(SM_SCALAR yaw, SM_SCALAR pitch, SM_SCALAR roll);
void SetDelta(SM_SCALAR dx, SM_SCALAR dy, SM_SCALAR dz);
void SetYPRDelta(SM_SCALAR dyaw, SM_SCALAR dpitch, SM_SCALAR droll);
void SetUpdateRate(VU_TIME updateRate);
The update rate is used by Vu only to determine when automatic updates happen. This interface function can be used to change the interval between these automatic updates.
void SetUpdateTime(VU_TIME currentTime);
Vu uses the SetUpdateTime interface function to mark the last time the DriveEntity function was called on the entity. Vu does not actually use this value for anything, so game programmers can also set this time as needed.
void SetTransmissionTime(VU_TIME currentTime);
The SetTransmissionTime function is called whenever an update of any sort is generated and sent over the network.
void SetCollisionCheckTime(VU_TIME currentTime);
The SetCollisionCheckTime function is called with vuxCurrentTime whenever a collision check is executed by Vu on an entity. If the game developer performs his own collision check it is relatively important that this function be called, as the time delta between collision checks determines how large the collision region should be.
void SetOwnerId(VU_SESSION_ID ownerId);
SetOwnerId is used to transfer an entity to some other machine. Vu uses this function to update the owner id value when such a transfer takes place. In most cases, the game developer should use the Push or Pull request messages instead of setting the owner id directly.
void InflictDamage(VU_ID shooterId, int hits,
VU_DAMAGE hitlocation = 0);
InflictDamage is the VuEntity interface to the generic damage handling provided at the abstract level. If the entity is locally managed, then Vu sends out a VuDamageUpdateEvent. Otherwise, it simply updates the applicable data fields. Note that the hit location has a default argument of 0 which means no location specified.
virtual void SetUserState(ushort newState);
SetUserState is the interface function for changing the user state field. This field is defined as a ushort by Vu, but never set and never used, so this field can be set by the game developer and it will not affect Vu in any significant way. Also, the interpretation of this field can be anything from an enum to a bitfield. If the owner of this entity is the local session, then a VuStateUpdateEvent will be generated.
void SetEntityType(ushort entityType);
SetEntityType will set the entity type of the entity, re-seed initial values with those found in the entity type returned from the VuxEntityType function call.
void SetAssociation(VU_ID assoc);
SetAssociation associates an entity with another. What this does is ensure that in a entity re-distribution (by way of the Distribute call in VuGroupEntity), that the entity with the association will be managed (owned) by the same session as the associated entity. Note that this association cannot chain, and that if the other entity is removed, then the association is (obviously) ineffective. For more details on this, see the FAQ section.
The following functions are collectively called getters because they return internal values of a class. Together with setters, these two kinds of functions are called accessors. As with setters, most of these functions are inline implementations, which (along with a reasonable compiler) ensures that access is just as efficient as direct variable access.
VU_ID Id();
The Id function returns the unique id of the entity. This id is guaranteed to be unique within the context of the VuGroupEntity to whom the owning session belongs.
VU_BYTE Domain();
The Domain represents the set of manageable entities that this entity belongs to. In almost all cases, all entities associated with a particular software release should have the same Domain. When an entity is to be transferred to another session, Vu checks the Domain of the entity with the domain mask of the target session. Only those sessions which pass the domain test can assume management of a transferred entity. The value 0 is used by Vu to define Vu entities which everyone knows about by default. This value represents a bit position in an unsigned long integer, so developer assigned values should be between 1-31.
VU_BOOL IsTangible();
IsTangible is one of the flags stored in the entity. Vu does not itself use it for anything, but it can be used with a VuStandardFilter to filter out Vu defined entities (session and group) when performing motion modeling or other game activities. Logically, if this flag is set, then it means that the entity can be seen and or touched. All Vu defined entities have this flag set to FALSE.
VU_BOOL IsCollidable();
IsCollidable is a flag which is used by Vu whenever automatic collision detection is defined. In this case, each of the entities with this flag set are checked with collision with each other each time Update is called on the VuMainThread. Note that the number of collisions is cut down dramatically by use of the VuGridTree. Also note that most game developers will want to perform their own collision checks, but may want to make use of this flag. All Vu defined entities have this flag set to FALSE.
VuFlagBits Flags();
Flags returns the flags structure of the VuEntity. Note that this structure can be used to check the collidable and tangible flags, but the other getters are provided for convenience.
ushort FlagValue();
Vu also returns a ushort coercion of the flags structure for bitfield operations. This is useful if the developer wants to quickly check whether any of a set of flags are set (with a bitwise AND operation.
VU_SESSION_ID OwnerId();
The OwnerId is the session id of the session which currently owns the VuEntity. Any newly created entity will have an owner id equal to its creator id.
ushort VuState();
Currently, the VuState is only used by Vu to mark whether the entity is active in the vuDatabase, or is in a pending delete state. In most cases, the game developer need not test the VuState of an entity, as Vu takes care of it automatically.
ushort UserState();
As was mentioned above in the setters section, the UserState is not used by Vu, but is maintained by Vu. The user can define the ushort to mean anything.
ushort Type();
The Type field of the VuEntity is used to associate an entity with a user defined type (the VuEntityType) which contains initial seed values as well as some other data fields which are meaningful to the game developer.
VU_BOOL IsLocal();
The IsLocal convenience function returns TRUE if the OwnerId is the same as the vuLocalSession (the Id of the local session). In other places of this document, this has been referred to as local management of an entity.
VU_ID Association();
The Association returns the Id of the associated entity. This association is used only to ensure that this entity is has the same Owner as the associated entity.
BIG_SCALAR Xpos();
BIG_SCALAR YPos();
BIG_SCALAR ZPos();
SM_SCALAR Yaw();
SM_SCALAR Pitch();
SM_SCALAR Roll();
SM_SCALAR XDelta();
SM_SCALAR YDelta();
SM_SCALAR ZDelta();
SM_SCALAR YawDelta();
SM_SCALAR PitchDelta();
SM_SCALAR RollDelta();
The preceeding getters all return current position and orientation data of the entity. Note that this data reflects the modeled/dead reckoned/smoothed data as of the LastUpdateTime (see below), and not the "most accurate" network data. The accurate source data is usually kept in the VuDriver.
VU_DAMAGE Damage();
Damage returns the contents of the damage field. Vu by default treats this as a bitfield when updating the field, but otherwise takes no special action on this field.
int HitPoints();
Like Damage, Hitpoints is dutifully updated and returned by Vu (except that hitpoints is treated as a damage pool, where hit effects are subtracted from available hitpoints). It is up to the game developer to determine that hit effect and also to determine how the changing value of HitPoints affects the VuEntity.
VU_TIME UpdateRate();
UpdateRate is used by Vu when VU_AUTO_UPDATE is defined. The value here is acquired from the VuEntityType table, and determines the period between automatic updates.
VU_TIME LastUpdateTime();
LastUpdateTime indicates the last time an update was received from the network, or the last time an DriveEntity or AutoDriveEntity call was made on the entity.
VU_TIME LastTransmissionTime();
LastTransmissionTime is used by Vu when VU_AUTO_UPDATE is set, and is used to determine when the next automatic update should occur. The value is updated by Vu whenever a VuFullUpdate is sent out on the network.
VU_TIME LastCollisionCheckTime();
The LastCollisionCheckTime is used by Vu when VU_AUTO_COLLISION_CHECK is set, and is used to determine how far the entity could have moved in the interval between then, now, and the next expected collision check time.
VuEntityType *EntityType();
EntityType returns the pointer to the entity type field. This pointer is guaranteed to be valid, but may be the special VU_UNKNOWN_ENTITY_TYPE entry, which has lots of zeros in the structure fields. To ensure that this does not happen make certain that the VuxEntityType call returns the correct value.
The following functions take action on the VuEntity, performing tests, executing the driver, sending updates, storing the entity image to a file or buffer, or handling events. Many of these functions are virtual, which means that their behavior can be customized very easily by the game developer by overloading the function in the subclasses.
VU_TIME DriveEntity(VU_TIME currentTime);
The DriveEntity call invokes the Exec function of the VuDriver if one is present, and does simple position and orientation delta dead reckoning otherwise. The value returned is the next time DriveEntity should be called. Vu does not call this function.
void AutoDriveEntity(VU_TIME currentTime);
AutoDriveEntity invokes the AutoExec function of the VuDriver if one is present. It is expected that AutoExec implementations will be less CPU intensive than Exec (or at least equivalent). For example, AutoExec might perform dead reckoning motion modeling while Exec does full blown physics modeling. For more on this, see the section on VuDriver.
VuDriver *EntityDriver();
EntityDriver is really a getter which returns a pointer to the current VuDriver. Note that if the driver was never set, the value returned will be 0. This function is located here to be grouped with the other Driver related functions.
VuDriver *SetDriver(VuDriver *newdriver);
SetDriver is used to set the driver pointer of the entity to the value passed in. This pointer should be allocated from the heap (or by using some other persistent allocation scheme) and it will be deleted in the destructor of the entity. The return value of this function will be a pointer to the old driver, which should be deleted by the caller. Vu will not handle perform entity destruction in this case. Note that the first call to this function for each entity will always return 0.
VU_BOOL CollisionCheck(VuEntity *other, SM_SCALAR deltatime);
CollisionCheck performs a simple (though reasonably accurate) three space collision check between the target entity and the entity passed it. This function returns TRUE if a collision is detected, and FALSE otherwise. If the VU_AUTO_COLLISION_CHECK flag is set, then Vu calls this function if the collisionType_ of the VuEntityType for the entity is set to VU_DEFAULT_COLLIDABLE, otherwise the CustomCollisionCheck will be called.
virtual VU_BOOL CustomCollisionCheck(VuEntity *other, SM_SCALAR deltatime);
This virtual function by default just calls CollisionCheck, but can be overloaded by a subclass to perform a more accurate or rigorous collision test.
virtual VU_BOOL TerrainCollisionCheck();
TerrainCollisionCheck is another virtual function which can be used to test collision with the terrain system. As Vu itself has not build in terrain system, this function by default return FALSE. Note that Vu never calls this function internally.
VU_BOOL LineCollisionCheck(
BIG_SCALAR x1, BIG_SCALAR y1, BIG_SCALAR z1,
BIG_SCALAR x2, BIG_SCALAR y2, BIG_SCALAR z2,
SM_SCALAR timeDelta, SM_SCALAR sizeFactor );
LineCollisionCheck runs a line between two points and then tests to see if it collided with the hit box of the entity in the time specified. The sizeFactor is used as a multiple of the entity size to make this test more or less likely to succeed (for difficulty adjustment, normally), where a value of 1.0 results in no adjustment. This function returns TRUE if a collision was detected, and FALSE otherwise.
The following functions are the serialization interface and are used by Vu to store the entity to disk and store an image of the entity to a buffer for transmission over the network. These functions are all virtual, but the calling convention is slightly different, as subclasses should first explicitly invoke the parent class version of the function, and then add their own data to the end. For an example of this see the sample entity definition later in this section.
virtual int SaveSize();
The SaveSize function should return the number of bytes written to the file or stream in the Save functions.
virtual int Save(VU_BYTE **stream);
The stream version of the Save function stores the entity image into the VU_BYTE stream passed in. This stored image can be then passed over the network and then used in the stream version of the object constructor to create a copy of the entity on a remote machine. The stream pointer is advanced to the end of the written record as a result of this function, and the number of bytes written is the return value. Note that no data allocation is performed by this function, and passing in an invalid pointer (or a stream of insufficient size) will result in Vu scribbling all over random memory.
virtual int Save(FILE *file);
The file version of the Save function is identical to the stream version, except that it operates on an open file instead. Note that the file must be opened and writeable or the Save will fail. As with the stream version, the file pointer is altered, and the value returned is the number of bytes written.
virtual VU_ERRCODE Handle(VuErrorMessage *error);
virtual VU_ERRCODE Handle(VuPushRequest *msg);
virtual VU_ERRCODE Handle(VuPullRequest *msg);
virtual VU_ERRCODE Handle(VuEvent *event);
virtual VU_ERRCODE Handle(VuFullUpdateEvent *event);
virtual VU_ERRCODE Handle(VuPositionUpdateEvent *event);
virtual VU_ERRCODE Handle(VuStateUpdateEvent *event);
virtual VU_ERRCODE Handle(VuDamageUpdateEvent *event);
virtual VU_ERRCODE Handle(VuEntityCollisionEvent *event);
virtual VU_ERRCODE Handle(VuTransferEvent *event);
virtual VU_ERRCODE Handle(VuSessionEvent *event);
The VuMessage handlers all handle messages of particular types. The VuEvent version is the catch-all version for otherwise unhandled events (including any developer defined events). The return value indicates whether the event was handled successfully, not handled or generated an error condition. All of the default handlers update the local data (if applicable). The Push and Pull requests check for compatibility of the entity with the target session, returning success (and generating the VuTransferEvent) if there is a fit, and returning the failure code otherwise.
The protected functions can be called by subclasses, but should be done so with care. Read the functions descriptions for discussion of potential complications.
protected:
The destructor is protected because it should only be called by the internal Vu functions and components. This is the only class where the destructor is protected rather than public because Vu does all of the needed cleanup on VuEntities. It is virtual to ensure that destruction of an entity calls the destructor of the deepest subclass and thus performs all the needed cleanup.
virtual ~VuEntity();
The ChangeId call is used to change the id of a session (or other entity) when an entity joins a group and has its sessionId_ changed. In general once a session has joined a group, all entity ids should remain constant. Vu uses this function to resolve rare cases where a create event is received for an entity whose Id is already assigned. The argument passed in is a pointer to the other, conflicting entity. Note that if the id of the object in question does change, then this change will be communicated to other sessions via a new create message. There will be no delete message sent out as this would result in removal of the conflicting entity instead of the new entity.
virtual void ChangeId(VuEntity *other);
The SetVuState function call is used by Vu internally to change the memory state of a VuEntity. This function will probably never be called by the game developer.
void SetVuState(VU_BYTE newState);
// DATA
protected:
struct ShareData {
VU_ID id_;
VU_SESSION_ID ownerId_; // owning session
union {
ushort value_;
VuFlagBits breakdown_;
} flags_;
VU_BYTE domain_;
VU_BYTE vuState_;
ushort userState_;
ushort entityType_;
BIG_SCALAR x_, y_, z_;
SM_SCALAR dx_, dy_, dz_;
SM_SCALAR yaw_, pitch_, roll_;
SM_SCALAR dyaw_, dpitch_, droll_;
VU_TIME updateInterval_;
VU_DAMAGE damage_;
int hitpoints_;
VU_ID lastShooter_;
VU_ID association_;
} share_;
// local data
ushort refcount_;
VU_TIME lastUpdateTime_;
VU_TIME lastCollisionCheckTime_;
VU_TIME lastTransmissionTime_;
VuEntityType *entityTypePtr_;
VuDriver *driver_;
The data portion of the VuEntity is relatively large (weighing in at about 100 bytes), but contains little fat. The data is broken into two parts, the shared data (which is encompassed in a separate structure for easy copying to a stream) and the local data, which is separated out. In almost all cases, the data of the VuEntity class should not be directly modified. Use the accessor functions instead. Note that this is especially true of the refcount_ field, as changing this value directly could have dire consequences.
};
Creating a New VuEntity Subclass
Creating a VuEntity subclass is relatively simple, but there are a few steps which are required. These will be covered in detail in this section, where we will create a sample class, the SampleBuildingEntity class, which represents a building object in our sample game..
Step 1: Define the entity types and the type table data
For our sample entity class, we will create two entity types, the SAMPLE_TENT_ENTITY_TYPE and the SAMPLE_FACTORY_ENTITY_TYPE. These two entity types differ only by size, but everything else is the same. We start our counting at +2 to account for the VU_SAMPLE_ENTITY_TYPE presented earlier.
#define SAMPLE_TENT_ENTITY_TYPE (VU_LAST_ENTITY_TYPE + 2)
#define SAMPLE_FACTORY_ENTITY_TYPE (VU_LAST_ENTITY_TYPE + 3)
The EntityType table is populated with reasonably appropriate data for our two entity types. Note that the data differs only in the class id, collision radius, class info, and hitpoints. The class info is a relatively arbitrary field which can be used to classify entities. Values put in here are rather arbitrary and in fact are not used by default by Vu. They could be used by the game developer to classify an unfamiliar entity as being similar to a known type, so that a new type of F-16 for instance, could at least be rendered as an F-16 of a known type.
VuEntityType sampleTypeTable[] = {
{ SAMPLE_TENT_ENTITY_TYPE, // class id
1, (SM_SCALAR)10.0, // collision type, radius
{1,1,0,0,0,0,0,0}, // class info
VU_TICS_PER_SECOND*30, // update rate
VU_TICS_PER_SECOND*60, // update tolerance
0x0, 10, // damage seed, hitpoints
1, 0, // major & minor rev numbers
VU_CREATE_PRIORITY_BASE+100, // create priority
VU_GLOBAL_DOMAIN, // management domain
TRUE, // transferable
FALSE, // local
TRUE, // tangible
TRUE, // collidable
},
{ SAMPLE_FACTORY_ENTITY_TYPE, // class id
1, (SM_SCALAR)200.0, // collision type, radius
{1,2,0,0,0,0,0,0}, // class info
VU_TICS_PER_SECOND*30, // update rate
VU_TICS_PER_SECOND*60, // update tolerance
0x0, 1000, // damage seed, hitpoints
1, 0, // major & minor rev numbers
VU_CREATE_PRIORITY_BASE+100, // create priority
VU_GLOBAL_DOMAIN, // management domain
TRUE, // transferable
FALSE, // local
TRUE, // tangible
TRUE, // collidable
},
};
Next, we must supply an implementaiton of the VuxType function. Note that as entity types are added, this function will need to be updated.
VuEntityType *
VuxType(ushort id)
{
VuEntityType *retval = 0;
switch (id) {
case SAMPLE_TENT_ENTITY_TYPE:
retval = &sampleTypeTable[0];
break;
case SAMPLE_FACTORY_ENTITY_TYPE:
retval = &sampleTypeTable[1];
break;
}
return retval;
}
Our building class is rather simple. In this case we will only add an address and a color (we will assume an enumerated value, but will just store it as an int). In order to make this work, we need to define three constructors (one which takes source data, and one for each of the data stream constructors). We will also need to overload the three save functions so that we can add our local data to the datastream when we store to a file or send to the network. Note that the simple accessors are inlined. The more complex SetAddress function (which performs cleanup and allocation) is not inlined.
class SampleBuildingEntity : public VuEntity {
public:
// constructors & destructor
SampleBuildingEntity(ushort type, char *address, int color);
SampleBuildingEntity(VU_BYTE **stream);
SampleBuildingEntity(FILE *file);
virtual ~SampleBuildingEntity();
// virtual function interface
virtual int SaveSize();
virtual int Save(VU_BYTE **stream);
virtual int Save(FILE *file);
// accessors
char *Address() { return address_; }
int Color() { return color_; }
void SetAddress(char *address);
void SetColor(int color) { color_ = color; }
// DATA
protected:
// shared data
char *address_;
int color_;
};
Step 3: Implement the new class
Since this is a simple class, our implementation is relatively short. Annotation of the implementation will be given where appropriate.
SampleBuildingEntity::SampleBuildingEntity(
ushort type, char *address, int color)
: VuEntity(type)
{
address_ = 0;
SetAddress(address);
color_ = color;
}
The data version of the constructor sets the address to 0 prior to calling the SetAddress function in order to ensure that that function does not free random memory.
SampleBuildingEntity::SampleBuildingEntity(VU_BYTE **stream)
: VuEntity(stream)
{
VU_BYTE len;
len = (VU_BYTE)**stream; *stream += sizeof(len);
callsign_ = new char[len + 1];
memcpy(address_, *stream, len); *stream += len;
address_[len] = '\0'; // null terminate
memcpy(&color_, *stream, sizeof(int)); *stream += sizeof(int);
}
Since the parent class constructor is called first (with the stream data passed up to it), by the time the stream version of the constructor gets to this code, the stream will already have been advanced to the start of this data, so we just read in the stuff we need (in the same order as it is written in the matching Save function (see below). Note that string data is stored in a length encoded format, and that our implicit maximum string length (since it is stored in a VU_BYTE) is 255.
SampleBuildingEntity::SampleBuildingEntity(FILE *file)
: VuEntity(file)
{
VU_BYTE len;
fread(&len, sizeof(len), 1, file);
address_ = new char[len + 1];
fread(address_, len, 1, file);
address_[len] = '\0'; // null terminate
fread(&color_, sizeof(int), 1, file);
}
The file version of the constructor is very similar to the stream version, except that we are operating on a file instead of a stream (duh).
SampleBuildingEntity::~SampleBuildingEntity()
{
delete [] address_;
address_ = 0;
}
Our destructor frees the address_ and sets the pointer to 0, just in case.
int
SampleBuildingEntity::SaveSize()
{
return
VuEntity::SaveSize()
+ strlen(address_)+1
+ sizeof(color_);
}
SaveSize is the first of the save interface functions. It uses the convention of adding the local class data to the parent class data. Note that this convention should be followed for classes which nest for more than one level. Call the parent class version first, then add your own data in the same order as it will be restored.
int
SampleBuildingEntity::Save(VU_BYTE **stream)
{
VU_BYTE len = strlen(address_);
VuEntity::Save(stream);
**stream = len; *stream += sizeof(len);
memcpy(*stream, address_, len); *stream += len;
memcpy(*stream, &color_, sizeof(int));*stream += sizeof(color_);
return SampleBuildingEntity::SaveSize();
}
The stream version of save calls the parent version of save first, and then adds the local data. This order is very important, or the unpacking in the constructor will not happen properly.
int
SampleBuildingEntity::Save(FILE *file)
{
int retval = 0;
VU_BYTE len = 0;
if (file) {
retval = VuEntity::Save(file);
len = strlen(address_);
retval += fwrite(&len, sizeof(len), 1, file);
retval += fwrite(address_, len, 1, file);
retval += fwrite(&color_, sizeof(color_), 1, file);
}
return retval;
}
The file version is very similar to the stream version, except that it also handles write errors on each operation. Note that it is possible to get a partially successful write operation.
void
SampleBuildingEntity::SetAddress(char *address)
{
delete [] address_;
if (!address) {
address = "";
}
address_ = new char[strlen(address) + 1];
strcpy(address_, address);
}
Our final member function, SetAddress, allocates space for the new value and copies it in. It also deletes the old copy. Note that this version depends on the C++ safety of deleting a NULL pointer (guaranteed by the language spec to always be harmless, so this is not a risk). Also note that if the user tries to set the pointer to NULL, we set it to an empty string. This makes our save and restore functions work without special casing.
Step 4: Implement VuxCreateEntity
The VuxCreateEntity function must be defined by the game developer and must translate a type and data stream pointer to an allocated, fully populated entity. The implementation is relatively simple, but any time a new type is added, this function must be updated.
VuEntity *
VuxCreateEntity(ushort type, ushort size, VU_BYTE *data)
{
VuEntity *retval = 0;
switch (type) {
case SAMPLE_TENT_ENTITY_TYPE:
case SAMPLE_FACTORY_ENTITY_TYPE:
retval = new SampleBuildingEntity(&data);
break;
}
return retval;
}
Chapter 4: VuCollection Use and Implementation
The VuCollection class tree and its associated helper classes (VuIterator and VuFilter) provide storage, sorting, and retrieval functionality for VuEntities. The VuCollection system also contains the VuDatabase class, which is the repository for all active VuEntities in a game. In addition, the VuDatabase (and other components of the VuCollection system) perform enough memory management to ensure that entities can be safely deleted. This is performed by minimal use of explicit memory locks (reference counts), relying instead on an implicit reference system.
As is true with most of the Vu class trees, the VuCollection hierarchy is relatively flat. The deepest tree is the linked list tree, where each subclass adds a small amount of functionality (filtering and ordering). This
section will only cover those classes that provide significant new interfaces. Subclass examples covered will include only the VuFilter, as that is the developer configurable component of VuCollection system. Note that it is possible to implement custom collections or iterators as well. Custom collections are needed for special purposes, such as creating a collection whose membership is determined by developer code rather than by a Filter (target lists, for example). This will be covered in the FAQ section later in the document.
Below is the definition of VuCollection, VuDatabase, and VuRedBlackTree. Although there are many more classes in the VuCollection hierarchy, these three classes almost all of the interface functions provided by this class tree. Annotation of these classes will only discuss newly added functions.
class VuCollection {
friend class VuCollectionManager;
The VuCollectionManager class is purely internal to Vu, and is used to perform memory management and collection consistency (to ensure that collections with filters correctly update their membership when receiving an event which affects that membership). The game developer should never need to know the details of this class’s implementation.
public:
virtual ~VuCollection();
The destructor is declared as virtual to follow the general rule: a function with any virtual function should have a virtual destructor. This ensures that calling the destructor on the abstract handle (with a VuCollection pointer, for example) calls all of the correct cleanup code.
virtual VU_ERRCODE Handle(VuMessage *msg);
The Handle function is provided in the VuCollection interface to allow subclasses to automatically update their membership based on the contents of the message. By default, this function returns VU_NO_OP. If overloaded, implementations usually pass the message on to the associated VuFilter’s Notice function (see VuFilter, below).
virtual VU_ERRCODE Insert(VuEntity *entity) = 0;
The Insert function adds the given entity to the current collection. Note that those collections with a VuFilter will Test the entity against the filter prior to insertion. If the Test function returns TRUE, the entity is added, otherwise it is not.
virtual VU_ERRCODE Remove(VuEntity *entity) = 0;
For those collections with a filter, the Remove function (entity version) will first call RemoveTest on the filter with the passed in entity. If this function returns false, then no action is taken and this function returns VU_NO_OP. (For obvious reasons, a false negative here could be disastrous.) If the RemoveTest passes (or is not executed), the Remove function will attempt to find and remove the entity. This removal places the entity (and it’s node) on a kill queue, ensuring that any iteration currently active on that collection will not break. The Remove function returns VU_SUCCESS if the entity is removed, VU_NO_OP if the entity was not found, and VU_ERROR if the entity was found but not removed.
virtual VU_ERRCODE Remove(VU_ID entityId) = 0;
The VU_ID version of Remove is identical in functionality to the entity version, except that in cases where collections are not stored by an Id derived key (which would result in an order-N search for the entity), the entity pointer is first retrieved from the vuDatabase, and then the pointer version of Remove is called.
virtual int Purge() = 0;
The Purge function call removes all entities from the given collection, and returns the number of entities removed.
virtual int Count() = 0;
The Count function returns a count of the entities in the collection. Note that the cost of this function call is sometimes order-N, and sometimes order-0 (zero).
virtual VuEntity *Find(VU_ID entityId) = 0;
The Find function takes an entityId and returns the VuEntity pointer. If the entity is not found in the collection, this function returns 0. Note that the efficiency of this function depends entirely on the implementation of the collection (and whether the Id is used as a key). The VuLinkedList (and it’s children) are order-N, while the VuRedBlackTree is order-log-N, and the VuHashTable is order-1. Note also that implementations which do not use the Id as a key (and are not order-N already) usually call Find on the vuDatabase first, then use the returned pointer to find the entity, returning 0 if the entity is not found.
protected:
VuCollection(VU_BOOL do_register = TRUE);
The constructor takes a do_register flag which is used to determine whether or not the collection is registered with Vu. This flag is set to TRUE for VuCollections which are contained in other VuCollections (such as the VuRedBlackTrees in the VuGridTree). It is advised that the game developer never set this flag to true as it will also turn off the automatic cleanup of entities on destruction. Setting this flag to TRUE in a subclass constitutes unsupported use of the interface.
// DATA
private:
VuCollection *nextcoll_;
The nextcoll_ member of the VuCollection is used by the VuCollectionManager to chain collections together. This member should not be referenced or set by game developers (and as such, this member is private).
};
The VuDatabase class is derived from VuHashTable (not shown here). It takes some custom constructor values (such as tableSize and key) which it passes on to the hash table. It also adds a Save and Restore API.
class VuDatabase : public VuHashTable {
public:
VuDatabase(uint tableSize, uint key = VU_DEFAULT_HASH_KEY);
The tableSize (passed on to VuHashTable) is exactly that – the size of the hash table. For greatest efficiency, this value should be 10-20% larger than the greatest number of entities supported, and should be a prime number. The key is a multiple used to space out sequential elements in the table and give a better distribution. The default value is 59, which should be fine for most needs.
virtual ~VuDatabase();
virtual VU_ERRCODE Handle(VuMessage *msg);
virtual VU_ERRCODE Insert(VuEntity *entity);
The only note on the insert function is that it does not test to see if an entity already exists in the database. If an entity exists in the database more than once, then bad things will happen, so be careful.
virtual VU_ERRCODE Remove(VuEntity *entity);
virtual VU_ERRCODE Remove(VU_ID entityId);
VU_ERRCODE Save(char *filename,
VU_ERRCODE (*headercb)(FILE *),
VU_ERRCODE (*savecb)(FILE *, VuEntity *));
The Save function calls the Save method on each of the elements in the database. It uses the stdio file API to perform file open and write operations. After successfully opening the file, and prior to invoking any of the VuEntity save functions, it calls the passed in headercb with the newly opened file (this step is skipped if the headercb is 0). If this function returns an error, then the save is aborted and an error returned. Otherwise, for each element in the database, this function first calls the savecb function (if non-0) prior to invoking that entity’s Save function. If the savecb returns an error, then the database will skip that record, but will continue with the next entity.
VU_ERRCODE Restore(char *filename,
VU_ERRCODE (*headercb)(FILE *),
VuEntity * (*restorecb)(FILE *));
The Restore function performs the opposite of the Save functionality. Note that calling this function multiple times will have unpredictable results (see Insert, above).
private:
virtual int Purge();
The VuDatabase version of Purge is private because the timing of this call is very important, and should only be called by the VuDatabase in its destructor.
};
The VuGridTree is a special purpose collection created to maintain and return proximity sets. It consists of an array of VuRedBlackTrees, where one dimension (X by default) determines the array entry, and another dimension (Y by default) is used to sort elements in the VuRedBlackTree within that X row. More magic happens inside the VuGridIterator (covered below), but this interface provides the all important Move function.
class VuGridTree : public VuCollection {
friend class VuGridIterator;
friend class VuFullGridIterator;
friend class VuCollectionManager;
public:
VuGridTree(VuBiKeyFilter *filter, uint numrows,
BIG_SCALAR center, BIG_SCALAR radius, VU_BOOL wrap = FALSE);
The constructor takes a VuBiKeyFilter (which is copied and stored) in order to translate coordinate values (stored as BIG_SCALAR types) to VU_KEY values. The numrows parameter specifies the number of VuRedBlackTrees used in this collection. This is used together with the center and radius to determine the positioning and size of these bins, while the wrap parameter tells what to do with elements that fall off the edge of the outside rows: if wrap is TRUE, then those outside the top filter up the bottom row and vice versa; if wrap is FALSE, then those elements which fall outside the top are still inserted into the topmost VuRedBlackTree, while those that fall outside the bottom are inserted into the bottom.
virtual ~VuGridTree();
virtual VU_ERRCODE Insert(VuEntity *entity);
virtual VU_ERRCODE Remove(VuEntity *entity);
virtual VU_ERRCODE Remove(VU_ID entityId);
virtual VU_ERRCODE Move(VuEntity *entity,
BIG_SCALAR coord1, BIG_SCALAR coord2);
The Move function is called by the VuEntity::SetPostion function. It must be called every time the position of the entity changes or the collection will be unable to find the entity without an exhaustive order-N search. This call is of medium expense, but has been optimized quite a bit.
virtual int Purge();
virtual int Count();
virtual VuEntity *Find(VU_ID entityId);
virtual VuEntity *FindByKey(VU_KEY key1, VU_KEY key2);
FindByKey will locate the first entity which matches both keys passed in. Note that it is very possible to have two or more entities at the same location.
protected:
VuRedBlackTree *Row(VU_KEY key1);
int CheckIntegrity(VuRBNode *node, int column);
CheckIntegrity is a debug function which ensures that all elements in the collection are where they should be in the GridTree.
// DATA
protected:
VuRedBlackTree *table_;
VuBiKeyFilter *filter_;
int rowcount_;
VU_KEY bottom_;
VU_KEY top_;
VU_KEY rowheight_;
VU_BOOL wrap_;
private:
VuGridTree *nextgrid_;
};
Below is the definition of the VuIterator class and its significant subclasses (those which add new interface functions).
class VuIterator {
friend class VuCollectionManager;
The VuCollectionManager needs friend access in order to set and test the nextiter_ private member, which is used to maintain the list of active iterators.
public:
VuIterator(VuCollection *coll);
In the abstract, the VuIterator refers to a VuCollection. This collection is stored at the base class level to allow access by the VuCollectionManager.
virtual ~VuIterator();
virtual VuEntity *CurrEnt() = 0;
The CurrEnt function call returns a pointer to the entity currently pointed to by the iterator. This will also be the same entity last returned by one of GetFirst/GetNext. This function is one of the rare virtual accessors. It is needed by the VuCollectionManager, and will not likely be often called elsewhere.
virtual VU_BOOL IsReferenced(VuEntity *ent) = 0;
The IsReferenced function call returns TRUE if the given entity is currently pointed to by the iterator (see CurrEnt) and FALSE otherwise.
virtual VU_ERRCODE Cleanup();
The Cleanup call is used to prepare the Iterator for a massive purge or cleanup. In general, it puts the iterator in the same state as it is in on startup prior to the first call to GetFirst.
protected:
VuCollection *collection_;
The collection_ pointer can safely be downcast by subclasses of the VuIterator because the value used to set this value is strongly typed (see the subclass constructors for confirmation of this).
private:
VuIterator *nextiter_;
};
The VuListIterator is given here as a sample implementation of an iterator – one which also defines the rest of the VuIterator component interface.
class VuListIterator : public VuIterator {
public:
VuListIterator(VuLinkedList *coll);
Note that the collection passed in (and passed up to the VuIterator constructor) is of the VuLinkedList type. This allows component VuListIterator member functions to safely downcast the collection_ pointer of the parent class.
virtual ~VuListIterator();
The iterator accessors are the heart of the iterators. These functions are not virtual (to improve efficiency), and are often inlined.
VuEntity *GetFirst();
GetFirst gets and returns the first element in the collection. It also initializes the iterator context. Note that for unordered collections (such as VuLinkedList and VuHashTable) which entity is returned from this call is rather arbitrary. Note that this iterator is also used for the VuOrderedList (since internal accessible data is the same as it’s parents). For ordered lists, this function will return the member with the lowest Compare value. If there are no elements in the collection, this function returns 0.
VuEntity *GetNext();
GetNext returns the next entity in the collection. After the last entity is returned, subsequent calls to GetNext will return 0. Calling GetNext before calling GetFirst will have unpredictable results.
VuEntity *GetFirst(VuFilter *filter);
The filter version of GetFirst returns the first element which passes the Test function of the filter passed in. If no elements pass this filter, then this function returns 0. This function also initializes the iterator context.
VuEntity *GetNext(VuFilter *filter);
The filter version of the GetNext call returns the next entity which passes the passed in filter. Note that there is no requirement that the same filter be used for GetFirst and GetNext, though this is not likely to be a real useful characteristic. As with the other version of GetNext, this function returns 0 after the last entity which passed the filter is returned.
virtual VuEntity *CurrEnt();
virtual VU_BOOL IsReferenced(VuEntity *ent);
virtual VU_ERRCODE Cleanup();
protected:
VuLinkNode *curr_;
};
The VuRBIterator (which iterates on a VuRedBlackTree) is presented here because of the VU_KEY version of the GetFirst function, and because of the RemoveCurrent interface function.
class VuRBIterator : public VuIterator {
friend class VuCollectionManager;
public:
VuRBIterator(VuRedBlackTree *coll);
virtual ~VuRBIterator();
VuEntity *GetFirst();
VuEntity *GetFirst(VU_KEY min);
The VU_KEY version of the GetFirst function will return the first element whose VU_KEY is at least as great as the value passed in. The key of the element is either that derived from the VuRedBlackTree’s filter (if one is active) or the VU_KEY coercion of the Id otherwise. If no entity in the collection has at least the minimum value passed in, then this function returns 0.
VuEntity *GetNext();
VuEntity *GetFirst(VuFilter *filter);
VuEntity *GetNext(VuFilter *filter);
virtual VuEntity *CurrEnt();
virtual VU_BOOL IsReferenced(VuEntity *ent);
virtual VU_ERRCODE Cleanup();
void RemoveCurrent();
The RemoveCurrent function call will remove the element that the iterator is currently pointing at (see CurrEnt, above). This interface is present here and not generally because not all iterators have enough context to perform this operation cheaply.
protected:
VuRBIterator(VuCollection *coll);
protected:
VuRBNode *curnode_;
VuLinkNode *curlink_;
VuRBIterator *rbnextiter_;
};
The VuGridIterator is presented here because it has a fairly unique constructor (in order to set the initial context) and it specializes the VuRBIterator in interesting ways. In addition, it uses function hiding (instead of overloading) which some compilers will flag with a warning.
class VuGridIterator : public VuRBIterator {
friend class VuCollectionManager;
public:
VuGridIterator(VuGridTree *coll, VuEntity *origin,
BIG_SCALAR radius);
The GridIterator takes an entity to specify the center of the specified region, and a ‘radius’ to specify how far to venture from the center when extracting values. Note that this radius is dimensional (greater of x-distance and y-distance) rather than linear (square root of x-squared + y-squared).
virtual ~VuGridIterator();
VuEntity *GetFirst();
VuEntity *GetNext();
VuEntity *GetFirst(VuFilter *filter);
VuEntity *GetNext(VuFilter *filter);
The above accessors hide rather than overload the VuRBIterator methods. Some compilers will flag this with a warning. The reason is that if the developer uses a VuGridIterator which has been cast to a VuRBIterator and calls these functions, it will call the VuRBIterator functions, not these. It is unlikely that this will happen in practice, but it is good to know in case strange behavior starts presenting itself.
protected:
VuRedBlackTree *curRB_;
VU_KEY key1min_;
VU_KEY key1max_;
VU_KEY key2min_;
VU_KEY key2max_;
VU_KEY key1cur_;
};
Below is the definition of the VuFilter, VuKeyFilter, and VuBiKeyFilter classes. All of these are abstract classes which require subclassing to be functional.
class VuFilter {
public:
virtual ~VuFilter();
virtual int Compare(VuEntity *ent1, VuEntity *ent2) = 0;
The Compare function is one of the two central functions of the VuFilter class. It takes two entities, and returns an integer which indicates if they are equal, and if not, which is larger. A negative return value (less than 0) indicates that ent1 is less than ent2. A zero return value indicates that they are equal. A positive value (greater than 0) indicates that ent1 is greater than ent2.
virtual VU_BOOL Test(VuEntity *ent) = 0;
The Test function is the other central VuFilter function. It returns TRUE if the entity passes the filter (and belongs in the collection, or whatever) and FALSE otherwise.
virtual VU_BOOL RemoveTest(VuEntity *ent);
The RemoveTest is similar to the Test function, except that it returns TRUE if the entity could ever have passed the Test function, regardless of whether it does not. For example, one might create a filter based on distance, whose Test function returns TRUE if the entity is within some linear distance of some point (the point data would need to be maintained within this specialized filter). Just because an entity does not fit inside this region currently does not mean that it never did. The default implementation returns TRUE.
virtual VU_BOOL Notice(VuMessage *event);
The Notice function is used to determine whether a particular event can cause an entity to change from passing Test to failing (or vice versa). If the event is capable of causing such a change, this function returns TRUE. If the event could not (or does not) cause this change, then this function returns FALSE.
virtual VuFilter *Copy() = 0;
The Copy function is used by collections and other classes (not iterators) which require a persistent image of the filter data. This function allocates (using new) and returns a copy of the current filter. It is the responsibility of the caller to call delete on the returned pointer.
protected:
VuFilter();
VuFilter(VuFilter *other);
};
The VuKeyFilter is similar to the VuFilter, except that it has an interface function to extract and return a VU_KEY value from an entity. This is most useful for using non-Id sort keys for the VuRedBlackTree.
class VuKeyFilter : public VuFilter {
public:
virtual ~VuKeyFilter();
virtual VU_BOOL Test(VuEntity *ent) = 0;
virtual int Compare(VuEntity *ent1, VuEntity *ent2);
The Compare function is implemented at this level and it compares the two entities by passing each to Key and comparing the results.
virtual VuFilter *Copy() = 0;
virtual VU_KEY Key(VuEntity *ent);
This is the Key function, which takes an entity and returns a VU_KEY. The default implementation returns the VU_KEY coercion of the entity’s Id.
protected:
VuKeyFilter();
VuKeyFilter(VuKeyFilter *other);
};
The VuBiKeyFilter adds a few functions to the VuKeyFilter from which it is derived. It was designed to translate an entity’s position into two key values for storing in a VuGridTree.
class VuBiKeyFilter : public VuKeyFilter {
public:
virtual ~VuBiKeyFilter();
virtual VU_BOOL Test(VuEntity *ent) = 0;
virtual VuFilter *Copy() = 0;
virtual VU_KEY Key(VuEntity *ent);
The Key function implementation by default returns Key2.
virtual VU_KEY Key1(VuEntity *ent);
The Key1 function implementation by default returns CoordToKey1 of the entity’s x- position.
virtual VU_KEY Key2(VuEntity *ent);
The Key2 function implementation by default returns CoordToKey2 of the entity’s y- position.
virtual VU_KEY CoordToKey1(BIG_SCALAR coord) = 0;
virtual VU_KEY CoordToKey2(BIG_SCALAR coord) = 0;
The CoordToKeyX functions should translate a scalar coordinate to a VU_KEY value. There is no default implementation of these functions.
virtual VU_KEY Distance1(BIG_SCALAR dist) = 0;
virtual VU_KEY Distance2(BIG_SCALAR dist) = 0;
The DistanceX functions are similar to the CoordToKeyX functions, except that they return raw distance translations with no offset from the origin (as the other functions must provide).
protected:
VuBiKeyFilter() {}
VuBiKeyFilter(VuBiKeyFilter *other) {}
};
Creating a New VuFilter Subclass
Creating a new VuFilter class is rather simple, but a few things are required. Most importantly, all pure virtual functions of the VuFilter class must be defined. For our sample filter, we will say that we need a filter which will give us all of those VuEntities which we defined (filtering out the Vu defined entities, such as VuGroupEntity and VuSessionEntity), and which are managed by the local machine.
Our SampleFilter class just defines our class. Note that we don’t have any data in this class. The virtual function implementation determine our behavior (especially the Test function). Since we add no new functions or data in this class, we’ll save annotation for the implementation (Step 2).
class SampleFilter : public VuFilter {
public:
SampleFilter();
virtual ~SampleFilter();
virtual VU_BOOL Test(VuEntity *ent);
virtual VU_BOOL RemoveTest(VuEntity *ent);
virtual int Compare(VuEntity *ent1, VuEntity *ent2);
virtual VU_BOOL Notice(VuMessage *event);
virtual VuFilter *Copy();
protected:
SampleFilter(SampleFilter *other);
};
Step 2: Implement the new class
The implementation is somewhat interesting in that there isn’t much code here. The constructors and destructors don’t do anything. Only Test and RemoveTest have interesting code, and this is what creates our required differentiation.
SampleFilter::SampleFilter()
: VuFilter()
{
// empty
}
SampleFilter::SampleFilter(SampleFilter *other)
: VuFilter(other)
{
// empty
}
SampleFilter::~SampleFilter()
{
// empty
}
VU_BOOL
SampleFilter::Test(VuEntity *ent)
{
return (ent->Type() > VU_LAST_ENTITY_TYPE && ent->IsLocal())
? TRUE : FALSE;
}
This Test function returns TRUE if our entity is local and it’s type falls into our allowed range (see the VuEntity sample implementation in the previous section).
VU_BOOL
SampleFilter::RemoveTest(VuEntity *ent)
{
return (ent->Type() > VU_LAST_ENTITY_TYPE) ? TRUE : FALSE;
}
The RemoveTest, however, only tests the entity type. It does this because it is possible for management of an entity to change during run time. This can be noticed as a result of a VuTransferEvent, or a VuFullUpdateEvent (See the Notice function notes).
int
SampleFilter::Compare(VuEntity *ent1, VuEntity *ent2)
{
return (int)ent1->Id() - (int)ent2->Id();
}
Our Compare function simply returns the difference between these two entity’s Id’s.
VU_BOOL
SampleFilter::Notice(VuMessage *event)
{
if ((1<<event->Type()) &
(VU_FULL_UPDATE_EVENT | VU_TRANSFER_EVENT)) {
return TRUE;
}
return FALSE;
}
The Notice function tests the type of the message to determine whether this message could cause an entity’s values could result in a change in the result of a call to the Test message (see Test function notes, above).
VuFilter *
SampleFilter::Copy()
{
return new SampleFilter(this);
}
And that’s it! Now, any instance of this class can be used in a collection or iterator. Note that collections take care of calling the Copy function, so the user of the collection need not do so (in fact, doing so will likely result in a memory leak).
Chapter 5: VuMessage Use and Implementation
The VuMessage class tree is the most extensive tree in the game, in part because of the simplicity of the design. Basically, each type of message has its own class (in addition to some common abstract classes). This is also one of the class trees most likely to be extended by the game developer, as it is likely that not all transmission needs will be satisfied by the standard Vu messages. Note however, that the VuFullUpdateEvent should cover many specialized needs. For this reason, our sample message subclass implementation will be somewhat contrived.
To reiterate the general design found in the architecture section of this document: Messages fall into three main categories. There is one error message class, three request messages, and the rest are entity state change events. Note that the base class in the tree is called VuMessage, but that the header file is called vuevent.h. This is true for historical reasons only. Hopefully this will not cause too much confusion.
VuMessage Constant Definitions
The first batch of #defines define the message types. These are selected so that the type of a message can be converted to a bit field flag by shifting a 1 into an unsigned long by the type and then tested against a bitfield with a bitwise and operation. The maximum reserved Vu event type is 19, which leaves 12 user defined event types in the standard unsigned long. For more event types, this requires secondary or tertiary bitfields for testing. The vuevent header file also defines several predefined bitfield combinations for convenience.
#define VU_UNKNOWN_MESSAGE 0
Messages which are of an unknown type have the associated flag. This is used to tag messages which Vu is unable to handle. Note that this event type is maskable, though Vu by definition will not handle messages of this type.
#define VU_ERROR_MESSAGE 1
The error message is unique. All error messages have an error code, which, together with the context, identifies the nature of the error. Vu defines a small number of errors, and also reserves number-space for future Vu error codes.
#define VU_GET_REQUEST_MESSAGE 2
#define VU_PUSH_REQUEST_MESSAGE 3
#define VU_PULL_REQUEST_MESSAGE 4
The request messages are enumerated below.
#define VU_TIMER_EVENT 5
#define VU_RELEASE_EVENT 6
The above two events are defined as internal. These events will never be broadcast over the net.
#define VU_DELETE_EVENT 7
#define VU_UNMANAGE_EVENT 8
#define VU_MANAGE_EVENT 9
#define VU_CREATE_EVENT 10
#define VU_SESSION_EVENT 11
#define VU_TRANSFER_EVENT 12
#define VU_STATE_UPDATE_EVENT 13
#define VU_POSITION_UPDATE_EVENT 14
#define VU_FULL_UPDATE_EVENT 15
#define VU_DAMAGE_UPDATE_EVENT 16
#define VU_ENTITY_COLLISION_EVENT 17
#define VU_GROUND_COLLISION_EVENT 18
Avove are the network event types. An event (as opposed to a message) represents a state change in an entity. Network events can (and will) be sent over the network.
#define VU_LAST_EVENT 19
The VU_LAST_EVENT #define is used to demarcate the end of the Vu event number-space. All user defined events should be larger than this #define.
Vu defines several bitfields which represent masks for combinations of events.
#define VU_VU_MESSAGE_BITS 0x000ffffe
#define VU_REQUEST_MSG_BITS 0x0000001c
#define VU_VU_EVENT_BITS 0x000fffe0
#define VU_DELETE_EVENT_BITS 0x000000c0
#define VU_CREATE_EVENT_BITS 0x00000600
#define VU_TIMER_EVENT_BITS 0x00000020
#define VU_INTERNAL_EVENT_BITS 0x00000060
#define VU_EXTERNAL_EVENT_BITS 0x000fff80
#define VU_USER_MESSAGE_BITS 0xfff00000
Below are the pre-defined Vu error codes. Also defined is the last error type reserved for definition by Vu. Game developers assigning error codes should ensure that their codes are larger than the VU_LAST_ERROR.
#define VU_UNKNOWN_ERROR 0
#define VU_NO_SUCH_ENTITY_ERROR 1
#define VU_CANT_MANAGE_ENTITY_ERROR 2
#define VU_DONT_MANAGE_ENTITY_ERROR 3
#define VU_CANT_TRANSFER_ENTITY_ERROR 4
#define VU_LAST_ERROR 99
Vu also defines timer types. Again, user timers should have a types larger than the VU_LAST_TIMER value.
#define VU_UNKNOWN_TIMER 0
#define VU_DELETE_TIMER 1
#define VU_LAST_TIMER 99
Routing is used to determine where to send a message. VU_LOCAL_TARGET is sent to the local machine (only), while VU_REMOTE_TARGET is sent to all remote machines. This is a bitfield, so combinations of these can be set simultaneously. In general, the specifics of this is maintained by Vu rather than the developer.
#define VU_NO_TARGET 0x00
#define VU_LOCAL_TARGET 0x01
#define VU_REMOTE_TARGET 0x02
#define VU_GROUP_TARGET 0x04
#define VU_SPECIFIED_TARGET 0x10
#define VU_BCGROUP_TARGET 0x20
#define VU_ALL_TARGET 0x03
#define VU_LGROUP_TARGET 0x05
The last two defined targets are combinations of targets which are commonly used to specify all machines on the network (including the sender) and all machines in the current group (including the sender).
Finally, session event subtypes are also defined in the header file. Just to be a little inconsistent, these are set as an enumerated value rather than by way of #defines.
enum vuEventTypes {
VU_SESSION_UNKNOWN_SUBTYPE = 0,
VU_SESSION_ID_CHANGE,
VU_SESSION_JOIN_GROUP,
VU_SESSION_LEAVE_GROUP,
VU_SESSION_CHANGE_GROUP,
VU_SESSION_CHANGE_CALLSIGN,
VU_SESSION_DISTRIBUTE_ENTITIES,
};
For the most part, the specifics of these values need not be known (except for the namespace issue), but these values appear frequently in the VuMessage source code, and hence they appear in this document.
VuMessageQueue Class Definition
The VuMessageQueue is covered first because it is the repository for unprocessed events in the Vu system. In general, it is not expected that the developer will be subclassing from VuMessageQueue. All needed customization can be performed by creating custom VuMessageFilters (see below) and VuThread. Steps required to perform customization of a queue will be covered in the section on VuThreads.
class VuMessageQueue {
public:
VuMessageQueue(int queueSize, VuMessageFilter *filter = 0);
The queue size is the maximum number of messages which can be stored in the circular queue. If an attempt is made to post an event to a full queue, Vu will quickly Dispatch the message about to be stomped, and then stomp it. This in general is a bad thing, but overwriting the end of the event queue is worse. The user can also pass in a message filter, which is copied and referred to on each Post. Note that delete events should not be filtered out.
~VuMessageQueue();
VuMessage *PeekVuMessage();
PeekVuMessage returns the message which is due to be processed. It does not alter the contents of the pointer or of the queue.
VuMessage *DispatchVuMessage();
DispatchVuMessage processes the message. The first time dispatch is called, Vu performs its handling of the message (including the invocation of the Handle method(s)). The final call to dispatch for a particular message will result in its destruction prior to return from the function, so the pointer would be to invalid data. The return value, then can only be used reliably to test whether or not any event was dispatched.
int DispatchAllMessages();
DispatchAllMessages purges the event queue by calling DispatchVuMessage repeatedly until there are no messages left in the queue. If the user is performing no special processing of the messages, this is a fine way to empty the queue.
static void PostVuMessage(VuMessage *event);
static void FlushAllQueues();
The static functions are provided to allow operations which affect all of the event queues. Since the post operation sends the given event to all queues, it falls into this category. The FlushAllQueues performs DispatchAllMessages operations on all of the active VuMessageQueues.
protected:
void AddMessage(VuMessage *event);
The AddMessage call is only called by PostVuMessage. Developers should never need to call this function directly.
// DATA
protected:
VuMessage **head_; // also queue mem store
VuMessage **read_;
VuMessage **write_;
VuMessage **tail_;
VuTimerEvent *timerlisthead_;
VuMessageFilter *filter_;
private:
static VuMessageQueue *queuecollhead_;
VuMessageQueue *nextqueue_;
};
VuMessageFilter Class Definition
The VuMessageFilter class is used to prevent certain messages from being posted to a particular VuMessageQueue. This is similar to the VuFilter in design and use, except that it operates on messages instead of entities.
class VuMessageFilter {
public:
VuMessageFilter() { }
virtual ~VuMessageFilter() { }
virtual VU_BOOL Test(VuMessage *event) = 0;
The Test function should return TRUE for messages which should be posted to the queue, and FALSE otherwise.
virtual VuMessageFilter *Copy() = 0;
The Copy function is used by the VuMessageQueue to create a local copy of the message filter passed into its constructor. This function should allocate and return a copy of the current filter. The caller (VuMessageQueue, usually) will call the destructor.
};
The VuNullMessageFilter passes all messages. No messages are filtered out, and all passed in are posted to the queue. This is the filter type which is used by VuMessageQueue if a 0 is passed in as the filter pointer (the default).
class VuNullMessageFilter : public VuMessageFilter {
public:
VuNullMessageFilter() : VuMessageFilter() { }
virtual ~VuNullMessageFilter() { }
virtual VU_BOOL Test(VuMessage *);
virtual VuMessageFilter *Copy();
};
The VuMessageTypeFilter takes a bitfield in the constructor which can be tested against the message type to determine whether or not the message should be posted to the queue. Note that the bitfield passed in should include the delete messages if the associated thread/queue uses entities, collections, or iterators.
class VuMessageTypeFilter : public VuMessageFilter {
public:
VuMessageTypeFilter(ulong bitfield);
virtual ~VuMessageTypeFilter();
virtual VU_BOOL Test(VuMessage *event);
virtual VuMessageFilter *Copy();
protected:
ulong msgTypeBitfield_;
};
The VuMessage class tree contains many classes. This discussion will enumerate all classes, and annotate special features or functions.
class VuMessage {
friend class VuMessageQueue;
public:
virtual ~VuMessage();
ushort Type();
The Type field returned by this function is the same type which is defined in the #define section and which is passed into the VuMessage constructor (below).
VU_MSG_ID MsgId();
The MsgId is generated at message construction time and is guaranteed to be unique within the context of a group. This can be used to associate error messages with requests, among other things.
VU_SESSION_ID Sender();
The Sender returns the session id of the origin of the message. This is extracted from the message id.
VU_BOOL IsLocal();
IsLocal returns TRUE if the sender is the local session, and FALSE otherwise.
VU_ID EntityId();
VuEntity *Entity();
All messages may have an entity associated with them. This is stored as an Id, and this can be retrieved by calling the EntityId function (for the Id) or the Entity function (for the pointer). If the entity cannot be found, then Entity() returns 0. Also the developer can specify that no entity is associated with an event. The Id returned by EntityId in this case is a special id, the VU_NULL_ENTITY_ID.
virtual int Size() = 0;
The Size function returns the number of bytes written by the event when Write is called (and the number of bytes read in by Read).
int Read(VU_BYTE **buf);
Read populates the current event with the data in the buffer under the assumption that that data had been generated by a previous call to Write. This function advances the buf pointer to the end of the VuMessage data.
int Write(VU_BYTE **buf);
Write copies the message data to the passed in buffer and advances the buf pointer to the end of the written data.
VU_ERRCODE Dispatch();
Dispatch sets in motion the Vu message handling of the current message.
int Ref();
int UnRef();
Vu uses reference counting to keep track of messages. Each time an event is posted to a queue, it’s reference count is incremented by one. Each time a given event is dispatched, its reference count is decremented by one. When a message’s reference count falls to zero, that message is deleted. It is not expected that game developers will need to call Ref and UnRef directly unless he needs the event to remain resident past the last Dispatch.
Protected functions are generally used to perform message processing at various stages.
protected:
VuMessage(ushort type, VU_ID entityId, VU_SESSION_ID dest);
VuMessage(ushort type, VU_ID entityId,
VU_BYTE routing = VU_NO_TARGET);
virtual VU_ERRCODE Activate(VuEntity *ent);
Activate is called when the entity is posted to the event queue. Among other things, this sets the entity pointer.
virtual VU_ERRCODE Process() = 0;
Process is called the first time an entity is Dispatched. Subsequent calls to Dispatch (for other queues, for example) do not call Process.
virtual int Encode(VU_BYTE **buf) = 0;
virtual int Decode(VU_BYTE **buf) = 0;
Encode is the implementation of the Write operation, while Decode is the implementation of the Read operation.
// DATA
private:
VU_BYTE refcnt_; // vu references
protected:
VU_BYTE routing_;
ushort type_;
VU_MSG_ID msgid_;
VU_SESSION_ID destination_;
VU_ID entityId_;
VuEntity *ent_; // local scratch variable
};
class VuErrorMessage : public VuMessage {
public:
VuErrorMessage();
As with virtually all messages, the VuErrorMessage has two constructor types. One takes no arguments, and usually created as a result of a call to VuxCreateMessage (or similar). These objects are invariably populated by a subsequent call to Read.
VuErrorMessage(int errorType, VU_MSG_ID srcmsgid,
VU_ID entityId, VU_SESSION_ID dest);
The local version of the VuErrorMessage constructor takes an error type, source message id (the id of the message which caused the error condition), an entity id (the id of the entity associated with the error, and a destination field, which specifies message routing. This routing field should be populated with one of the VU_XXX_TARGET #define values given earlier in this section.
virtual ~VuErrorMessage();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
Size, Decode, and Encode are just the VuErrorMessage implementations of the VuMessage abstract functions.
ushort ErrorType();
VU_MSG_ID SourceMessageId();
VU_SESSION_ID SourceMessageOrigin();
The ErrorType is either one of the standard Vu error codes given earlier in the #define section, or some developer defined error. Source message id and source message origin are the other accessors which return the data passed in or read in the Read call.
protected:
virtual VU_ERRCODE Process();
protected:
VU_MSG_ID srcmsgid_;
short etype_;
};
The VuRequestMessage does not have too much associated with it. It just passes the interface on to the subclasses.
class VuRequestMessage : public VuMessage {
public:
virtual ~VuRequestMessage();
virtual int Size() = 0;
virtual int Decode(VU_BYTE **buf) = 0;
virtual int Encode(VU_BYTE **buf) = 0;
protected:
VuRequestMessage(ushort type);
VuRequestMessage(ushort type, VU_ID entityId, VU_SESSION_ID targetSession);
VuRequestMessage(ushort type, VU_ID entityId, VU_BYTE routing);
virtual VU_ERRCODE Process() = 0;
// DATA
protected:
// none
};
The VuGetRequest message is used to request data from a remote session. The message can either be targeted at a specific entity, in which case that entity returns it’s data, or at a set of stations, in which case each receiver returns its data. If the message is created with a VU_NULL_MESSAGE_ID, then the request should be interpreted as a request for ALL entities managed by that session. With these tools, a request message can ask for anywhere from one entity from a specific session to all entities from all sessions. Error messages will only be generated if the get message is targeted at a specific session which does not manage the requested entity.
class VuGetRequest : public VuRequestMessage {
public:
VuGetRequest();
VuGetRequest(VU_ID entityId, VU_SESSION_ID targetSession);
VuGetRequest(VU_ID entityId, VU_BYTE routing = VU_ALL_TARGET);
virtual ~VuGetRequest();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
};
VuPushRequest must be targeted at a specific session. This session should be the session which should begin managing the entity. An error message will be returned if the entity cannot be transferred for some reason (such as the target session is unable to manage the entity). Success will be indicated by way of a VuEntityTransfer event generated by the target session.
class VuPushRequest : public VuRequestMessage {
public:
VuPushRequest();
VuPushRequest(VU_ID entityId, VU_SESSION_ID targetSession);
virtual ~VuPushRequest();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
};
VuPullRequest is similar to VuPushRequest, except that it is targeted at the session which currently managed the entity and is a request for that manager to relinquish control of the entity to the requestor. Again, the response will be one of VuErrorMessage or VuTransferEvent.
class VuPullRequest : public VuRequestMessage {
public:
VuPullRequest();
VuPullRequest(VU_ID entityId, VU_SESSION_ID targetSession);
virtual ~VuPullRequest();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
};
VuEvent is the abstract class for all entity state change events. It contains a few fields which are set on activate. These fields include the position information (x, y, z) as well as the update time. These fields can be used to filter out messages which are too distant from the position of the player to be of interest.
class VuEvent : public VuMessage {
public:
virtual ~VuEvent();
virtual int Size() = 0;
virtual int Decode(VU_BYTE **buf) = 0;
virtual int Encode(VU_BYTE **buf) = 0;
protected:
VuEvent(ushort type, VU_ID entityId, VU_SESSION_ID dest);
VuEvent(ushort type, VU_ID entityId,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual VU_ERRCODE Activate(VuEntity *ent);
virtual VU_ERRCODE Process() = 0;
// DATA
public:
// these fields are filled in on Activate()
VU_TIME updateTime_;
BIG_SCALAR x_, y_, z_;
};
The VuCreateEvent is used to announce the creation of a new entity to other sessions in a group (and possibly to other threads in a program, but this is unusual). The Create event works by writing all of the entity’s data into a buffer using that entity’s Save function (stream version). This stream is then sent over the wire and used as source data for the stream version of the entity’s constructor.
class VuCreateEvent : public VuEvent {
public:
VuCreateEvent();
VuCreateEvent(VuEntity *entity, VU_SESSION_ID dest);
VuCreateEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuCreateEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
VuEntity *EventData() { return expandedData_; }
protected:
VuCreateEvent(ushort type);
VuCreateEvent(ushort type, VuEntity *ent, VU_SESSION_ID dest);
VuCreateEvent(ushort type, VuEntity *ent, VU_BYTE routing);
virtual VU_ERRCODE Process();
// data
protected:
VuEntity *expandedData_;
ushort vutype_; // entity type
ushort size_;
VU_BYTE *data_;
};
The VuManageEvent is very similar to a VuCreateEvent (it contains the same create data). In fact, it only differs by context. A manage event is used to create an entity which already existed logically, but just was not managed by anyone. This goes with the VuUnmanageEvent which is used to indicate that the entity is again not managed by any session, but is not actually gone.
class VuManageEvent : public VuCreateEvent {
public:
VuManageEvent();
VuManageEvent(VuEntity *entity, VU_SESSION_ID dest);
VuManageEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuManageEvent();
// data
protected:
// none
};
The VuDeleteEvent is sent by the manager of an entity when that entity is removed from the managing session’s database. The delete event is a critical part of Vu’s memory management scheme, as the destructor of the delete event is where the entity is deleted (well, actually, where Vu performs its last dereference). As the delete event is not destroyed until after all queues (and hence threads) have processed the message, this ensures that all threads have (1) seen the delete event (in order to purge global pointer references) and (2) returned to the top of their call stack (to the inner event loop), which ensures that there are no references to the entity pointer on the stack. Other than these two nifty characteristics, the delete event is really nothing special.
class VuDeleteEvent : public VuEvent {
public:
VuDeleteEvent();
VuDeleteEvent(VuEntity *entity);
virtual ~VuDeleteEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
virtual VU_ERRCODE Activate(VuEntity *ent);
// data
protected:
// none
};
The VuUnmanageEvent is used to indicate that an entity will soon be going away. The entity will be destroyed at the time indicated by the mark in the constructor (at which time a VuDeleteEvent will be called). Any session which wishes to keep this entity active needs to request ownership with a VuPullRequest. If this pull succeeds, then the entity will be Released instead of Deleted by the former owner, and the new session will assume ownership.
class VuUnmanageEvent : public VuEvent {
public:
VuUnmanageEvent();
VuUnmanageEvent(VuEntity *entity, VU_TIME mark,
VU_SESSION_ID dest);
VuUnmanageEvent(VuEntity *entity, VU_TIME mark,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuUnmanageEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
// time of removal
VU_TIME mark_;
};
The VuReleaseEvent is one of two internal (non-net) messages provided by Vu. This message indicates that an entity which is not managed by the local session has been removed from the local database. This allows cleanup of the entity in the same fashion as is provided by the VuDeleteEvent.
class VuReleaseEvent : public VuEvent {
public:
VuReleaseEvent(VuEntity *entity);
virtual ~VuReleaseEvent();
virtual int Size();
// all these are stubbed out here, as this is not a net message
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Activate(VuEntity *ent);
virtual VU_ERRCODE Process();
// data
protected:
// none
};
The VuTransferEvent is used to indicate that the specified entity is now managed by a different session. Functions and data are pretty standard.
class VuTransferEvent : public VuEvent {
public:
VuTransferEvent();
VuTransferEvent(VuEntity *entity, VU_SESSION_ID dest);
VuTransferEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuTransferEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
VU_SESSION_ID newOwnerId_;
};
The VuStateUpdateEvent is used to indicate that the user state of the associated entity has changed. The vu state is also included in this event though it is not currently used.
class VuStateUpdateEvent : public VuEvent {
public:
VuStateUpdateEvent();
VuStateUpdateEvent(VuEntity *entity, VU_SESSION_ID dest);
VuStateUpdateEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuStateUpdateEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
ushort vustate_;
ushort userstate_;
};
The VuPositionUpdateEvent is probably in practice the most frequently sent message in the Vu system. It indicates that an entity’s location has changed. Note that the VuDriver class structure provides facilities for minimizing this kind of traffic by providing dead reckoning and smoothing functionality. For more information on this, see the VuDriver section. Note that this event contains position delta, orientation, and orientation delta information. Position information is also present, but is specified in the parent (VuEvent) class.
class VuPositionUpdateEvent : public VuEvent {
public:
VuPositionUpdateEvent();
VuPositionUpdateEvent(VuEntity *entity, VU_SESSION_ID dest);
VuPositionUpdateEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuPositionUpdateEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
SM_SCALAR dx_, dy_, dz_;
SM_SCALAR yaw_, pitch_, roll_;
SM_SCALAR dyaw_, dpitch_, droll_;
};
The VuFullUpdateEvent is kind of a tricky implementation. It inherits from VuCreateEvent for two reasons: (1) the data is complete, and is written using the entity’s Save function (stream version), same as VuCreateEvent; and (2) when an entity receives a full update event on an entity it has not heard about (and needs to add to its local database), Vu ‘morphs’ its type to a VuCreateEvent by changing the eventType_ field. This allows Vu to pretend it received a create event (and to create the entity from the event data) without making an explicit request to the owning session. This cuts down on network traffic and also provides a rather seamless way to handle missed packets.
class VuFullUpdateEvent : public VuCreateEvent {
public:
VuFullUpdateEvent();
VuFullUpdateEvent(VuEntity *entity, VU_SESSION_ID dest);
VuFullUpdateEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuFullUpdateEvent();
The WasCreated function call returns TRUE if the update event resulted in an insert into the local database, and FALSE otherwise.
VU_BOOL WasCreated();
protected:
virtual VU_ERRCODE Activate(VuEntity *ent);
// data
protected:
// none
};
The VuDamageUpdateEvent is the standard Vu damage state update. It includes data fields for shooter id (for kill recording or scoring) as well as the standard Vu damage fields. The values passed in are intended to reflect damage inflicted, not health remaining.
class VuDamageUpdateEvent : public VuEvent {
public:
VuDamageUpdateEvent();
VuDamageUpdateEvent(VuEntity *entity, VU_ID shooterId,
VU_DAMAGE hitlocation, int hiteffect, VU_SESSION_ID dest);
VuDamageUpdateEvent(VuEntity *entity, VU_ID shooterId,
VU_DAMAGE hitlocation, int hiteffect,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuDamageUpdateEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
VU_ID shooterId_;
VU_DAMAGE hitLocation_;
int hitEffect_;
};
The VuEntityCollisionEvent is the Vu provided event for communicating that two entity’s have collided. The primary entity is the entity which detected the collision, while the other entity (specified by an id) is the entity collided with. This event can also be used to specify hit location and hit effect.
class VuEntityCollisionEvent : public VuEvent {
public:
VuEntityCollisionEvent();
VuEntityCollisionEvent(VuEntity *entity, VU_ID otherId,
VU_DAMAGE hitLocation, int hitEffect,
VU_SESSION_ID dest);
VuEntityCollisionEvent(VuEntity *entity, VU_ID otherId,
VU_DAMAGE hitLocation, int hitEffect,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuEntityCollisionEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
VU_ID otherId_;
VU_DAMAGE hitLocation_; // affects damage
int hitEffect_; // affects hitpoints/health
};
The VuGroundCollisionEvent is defined by Vu for completeness. As Vu does not perform ground collision, this event will never be generated by Vu.
class VuGroundCollisionEvent : public VuEvent {
public:
VuGroundCollisionEvent();
VuGroundCollisionEvent(VuEntity *entity, VU_SESSION_ID dest);
VuGroundCollisionEvent(VuEntity *entity,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuGroundCollisionEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
// none
};
The VuSessionEvent is a generic event type for various events pertaining to sessions and groups. Session events have a subtype which indicates the nature of the session message. This subtype is enumerated in the #defines section above. All session event subtypes defined are generated and handled automatically by Vu.
class VuSessionEvent : public VuEvent {
public:
VuSessionEvent();
VuSessionEvent(VuEntity *entity, ushort subtype,
VU_SESSION_ID dest);
VuSessionEvent(VuEntity *entity, ushort subtype,
VU_BYTE routing = VU_REMOTE_TARGET);
virtual ~VuSessionEvent();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
ushort subtype_;
VU_ID group_;
char *callsign_;
};
The VuTimerEvent is a special internal event which is dispatched the first time DispatchVuMessage is called on the holding queue after the time specified by the mark passed in has been reached or passed. The stored time is compared with vuxCurrentTime to make this determination. Note that this is the only time when Peek and Dispatch could conceivably return different events (wrap the Peek-Dispatch pair with a critical section if this is a problem). It is also possible to automatically post a secondary event when the timer is first dispatched. This event is passed into the timer event constructor. Note that this event is never sent over the network.
class VuTimerEvent : public VuEvent {
friend class VuMessageQueue;
public:
VuTimerEvent(VuEntity *entity, VU_TIME mark, ushort type,
VuEvent *event=0);
virtual ~VuTimerEvent();
virtual int Size();
// all these are stubbed out here, as this is not a net message
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// data
public:
// time of firing
VU_TIME mark_;
ushort ttype_;
// event to launch on firing
VuEvent *event_;
private:
VuTimerEvent *next_;
};
Creating a New VuMessage Subclass
In this section, we will only cover the creation of a subclass of VuMessage. Subclasses of VuMessageFilter will be covered in the section on VuThread.
For our sample message, we will create a class called the SampleChatMessage, which is used to send a text message to one or more sessions in a network environment.
Step 1: Define the message type
This step is relatively simple. We need to create a message type which does not conflict with the Vu message types. This can be done as illustrated below.
#define SAMPLE_CHAT_MESSAGE (VU_LAST_EVENT + 1)
Step 2: Define the new message class
Our new message class inherits directly from VuMessage (as it does not fall into any of the other three pre-defined Vu message types). The local constructors take a message text and a target. By specifying the target or the routing, the message can be sent to one player (session) or the entire group.
class SampleChatMessage : public VuMessage {
public:
SampleChatMessage();
The empty constructor is called in VuxCreateMessage, and will subsequently be populated by a call to Decode.
SampleChatMessage(char *messageText, VU_SESSION_ID dest);
The targeted version of the constructor takes a message text and destination session id. The message text is copied to ensure that it doesn’t get deleted out from underneath us.
SampleChatMessage(char *messageText,
VU_BYTE routing = VU_REMOTE_TARGET);
The routing version of the local constructor also takes the message text. The routing parameter is defaulted so that if the caller does not specify a target, then this message will be sent to all remote sessions.
virtual ~SampleChatMessage();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
The transmission functions are pretty standard in definition.
protected:
virtual VU_ERRCODE Process();
For our sample message, we want to know when we’ve received a message, so we’ll just implement the Process message and use printf to print it out. This is covered in the source (below).
public:
char * messageText_;
Here we got lazy. Rather than make an accessor interface, we have just made the data public. For an event (which is really just a fancy data structure), this doesn’t seem too evil.
};
Step 3: Implement the new message class
Our implementation of this class is not all that long, but does fill out a bit because of the three constructors and the complexity of the transmission functions.
SampleChatMessage::SampleChatMessage()
: VuMessage(SAMPLE_CHAT_MESSAGE)
{
messageText_ = new char[1];
*messageText_ = 0;
}
We are being safe here. It is extremely unlikely that we will ever access the messageText_ pointer set here until after Decode is called. Nonetheless, we practice safe programming. Note that the type of the message is passed on to the VuMessage class.
SampleChatMessage::SampleChatMessage(char *messageText,
VU_SESSION_ID dest)
: VuMessage(SAMPLE_CHAT_MESSAGE, vuLocalSession, dest)
{
if (!messageText) messageText = "";
int len = strlen(messageText);
messageText_ = new char[len + 1];
strcpy(messageText_, messageText);
}
The targeted version of our constructor copies the string passed in, and ensures that we will have a valid string (in case some joker passed in a NULL pointer). Note the values passed on the VuMessage class: we specify our type, note that the local session is the originator of the message (see Process, below), and of course pass on the dest.
SampleChatMessage::SampleChatMessage(char *messageText,
VU_BYTE routing)
: VuMessage(SAMPLE_CHAT_MESSAGE, vuLocalSession, routing)
{
if (!messageText) messageText = "";
int len = strlen(messageText);
messageText_ = new char[len + 1];
strcpy(messageText_, messageText);
}
This function is virtually identical to the targeted version of the constructor. The only difference is that we pass on routing information rather than target data.
SampleChatMessage::~SampleChatMessage()
{
delete [] messageText_;
}
The only thing to note here is the brain-damaged array destruction notation of C++. Don’t like the syntax, never will, but it’s here to stay.
int
SampleChatMessage::Size()
{
return sizeof(VU_BYTE) + // routing_
sizeof(VU_MSG_ID) + // message id
sizeof(VU_SESSION_ID) + // destination_
sizeof(VU_ID) + // entityId_
strlen(messageText_)+1;
}
The Size function returns the size of the message image as it would be written to disk. Note that our size is variable, depending on the length of our string. Also note that the size will likely change after a call to Decode.
int
SampleChatMessage::Decode(VU_BYTE **buf)
{
memcpy(&routing_, *buf, sizeof(VU_BYTE));
*buf += sizeof(VU_BYTE);
memcpy(&msgid_, *buf, sizeof(VU_MSG_ID));
*buf += sizeof(VU_MSG_ID);
memcpy(&destination_, *buf, sizeof(VU_SESSION_ID));
*buf += sizeof(VU_SESSION_ID);
memcpy(&entityId_, *buf, sizeof(VU_ID)); *buf += sizeof(VU_ID);
VU_BYTE len;
memcpy(&len, *buf, sizeof(len)); *buf += sizeof(len);
delete [] messageText_;
messageText_ = new VU_BYTE[len+1];
memcpy(messageText_, *buf, len); *buf += len;
messageText_[len] = 0;
int retval = Size();
return retval;
}
The Decode function does what decode should do: it copies data from the buffer into the local entity. Note that because of the way message decode’s are generally implemented, we have no VuMessage::Decode function to fall back on. So, here we get the data we care about (or which Vu needs, at least). This function modifies the buf pointer and returns the number of bytes read.
int
SampleChatMessage::Encode(VU_BYTE **buf)
{
int retval = Size();
memcpy(*buf, &routing_, sizeof(VU_BYTE));
*buf += sizeof(VU_BYTE);
memcpy(*buf, &msgid_, sizeof(VU_MSG_ID));
*buf += sizeof(VU_MSG_ID);
memcpy(*buf, &destination_, sizeof(VU_SESSION_ID));
*buf += sizeof(VU_SESSION_ID);
memcpy(*buf, &entityId_, sizeof(VU_ID)); *buf += sizeof(VU_ID);
VU_BYTE len = strlen(messageText_);
memcpy(*buf, &len, sizeof(len)); *buf += sizeof(len);
memcpy(*buf, messageText_, len); *buf += len;
return retval;
}
Encode performs the inverse of the Decode operation. This function modifies the buf pointer and returns the number of bytes written.
VU_ERRCODE
SampleChatMessage::Process()
{
if (ent_ && ent_->Type() == VU_SESSION_ENTITY_TYPE) {
printf("Received the following message from '%s'\n '%s'\n",
(VuSessionEntity *)ent_->Callsign(),
messageText_);
return VU_SUCCESS;
}
return VU_ERROR;
}
Our final function prints out the received message. Note that this message will appear on the remote targeted machine (if one is specified), or all remote machines (default routing call). This message will only appear on the local machine if the routing includes the VU_LOCAL_TARGET bit. This function will return an error if the sending session is not known, or the type was set inappropriately.
Step 4: Implement or update the VuxCreateMessage function
Our final step is to implement or update the VuxCreateMessage function, which translates a message type into a newly allocated pointer to a message of the appropriate type. If we don’t know the message type, the proper behavior is to return 0, which is exactly what is done here. Vu will automatically handle population of the message with a subsequent call to Decode with the event data.
VuMessage *
VuxCreateMessage(ushort type)
{
VuMessage *retval = 0;
switch(type) {
case SAMPLE_CHAT_MESSAGE:
retval = new SampleChatMessage;
break;
}
return retval;
}
This ends our coverage of the VuMessage use and customization.
Chapter 6: VuDriver Use and Implementation
The VuDriver is an ancillary part of the VuEntity object. It is a separate class whose instances are contained in the VuEntity class. According to the design, it does not make any sense to have a VuDriver without an associated VuEntity. However, this separation does make it much easier to change the behavior of an entity (for example, changing an object’s motion modelling from flying to falling), and it also provides Vu with a way to seamlessly provide dead reckoning and smoothing functionality for network updates in such a way that the game code does not need to adjust to the different conditions (beyond initializing them properly).
This section will cover the various classes in the VuDriver class tree, and then present a sample master sublcass implementation for our SAMPLE_TENT_ENTITY class (defined in the VuEntity section, above).
VuSlaveSmootherSettings Class Definition
The VuSlaveSmootherSettings class is the repository for certain global thresholds and smoothing values. Vu makes use of a defined global variable, vuxSlaveSmootherSettings, which must be set and initialized by the game developer. The class itself is described below:
class VuSlaveSmootherSettings {
public:
VuSlaveSmootherSettings(SM_SCALAR x, SM_SCALAR y, SM_SCALAR z,
SM_SCALAR yaw, SM_SCALAR pitch, SM_SCALAR roll,
SM_SCALAR maxJumpDist, SM_SCALAR maxJumpAngle,
VU_TIME lookaheadTime);
The constructor is big, and takes enough data to fill all of the elements of the structure. Once the constructor is called, no more values need be set.
~VuSlaveSmootherSettings();
SM_SCALAR xtol_, ytol_, ztol_;
The x, y, and z tolerance values define the point at which the dead-reckoned data and the real physics modelled data are out of sync enough to justify an update. The values specified here provide the default values used for all master drivers. Note that values of zero will result in position update generation if there is any difference between the values, and negative tolerance values will cause updates to be sent every time an entity’s position changes at all.
SM_SCALAR yawtol_, pitchtol_, rolltol_;
The yaw, pitch, and roll tolerances represent maximum orientation thresholds which will force a position update generation if data is out of sync.
SM_SCALAR maxJumpDist_;
SM_SCALAR maxJumpAngle_;
The maxJumpDist_ and maxJumpAngle_ are used to determine the thresholds at which smoothing kicks in. If the difference between any of the true values (received in a position or full update) exceed the maximum values specified here, then smoothing is activated to force a gradual transition between the true value and the current dead-reckoned value. If the difference for all position and angle values are less then the maximums, then the entity will simply warp to the new position.
SM_SCALAR lookaheadTime_;
The lookaheadTime_ is used to specify how long a smoothing cycle will last. This time is expressed in seconds (hence the scalar value).
VU_TIME lookaheadVuTime_;
The lookaheadVuTime_ is the value passed in, and is the same lookahead time expressed in VU_TIME units (where a value of 1 does not neccesarily map to one second).
};
The VuDriver class tree has a few classes, but most of the functions (and hence the interface) is given in the VuDriver class. Subclasses introduce a few additional functions which will be explained when presented. These are most often functions used by the driver hierarchy, either internally or by subclasses.
class VuDriver {
public:
virtual ~VuDriver();
virtual int Type() = 0;
The Type function returns the driver type. Note that this value is returned by the virtual function and is not stored in the class data. Vu needs this function to be able to distinguish between a master and a slave entity. The developer can further specialize this function by specifying the type of the master, but it is unlikely there will be a need for this.
virtual void Exec(VU_TIME timestamp) = 0;
Exec is the function called by the VuEntity::DriveEntity function. The expectation is that for master implementations, this will be the full blown physics implementation.
virtual void AutoExec(VU_TIME timestamp) = 0;
AutoExec is the function called by the VuEntity::AutoDriveEntity function. Where appropriate, this should be a cheaper version of the Exec function (just performing dead reckoning, for example). Slave entities always perform dead reckoning/smoothing and so execute the same code in both of these functions.
virtual VU_ERRCODE Handle(VuEvent *event) = 0;
virtual VU_ERRCODE Handle(VuFullUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuPositionUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuDamageUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuStateUpdateEvent *event) = 0;
The event handlers are provided to allow the driver to handle certain events (plus a generic event handler for user specified events). The slave handles full and position update events. Other events generally are not handled.
VuEntity *Entity();
The VuDriver keeps around a pointer to the associated VuEntity so that it is possible to get back to the source entity (for updating of the stored position and orientation, primarily).
protected:
VuDriver(VuEntity *entity);
// Data
protected:
VuEntity *entity_;
};
The VuDeadReckon class contains all dead reckoning code. This is the parent for both the master and the slave because the slave moves by dead reckoning, while the master needs dead reckoning in order to determine whether or not the actual position and the position the slave drivers are using are far enough off to justify generation of a position update.
class VuDeadReckon : public VuDriver {
public:
VuDeadReckon(VuEntity *entity);
virtual ~VuDeadReckon();
virtual int Type() = 0;
virtual VU_ERRCODE Handle(VuEvent *event) = 0;
virtual VU_ERRCODE Handle(VuFullUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuPositionUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuStateUpdateEvent *event) = 0;
virtual VU_ERRCODE Handle(VuDamageUpdateEvent *event) = 0;
All of the pure virtual functions are specified here because they are not defined here. C++ requires that pure virtual functions which are not defined in a subclass must be redeclared as pure virtual in that subclass.
virtual void Exec(VU_TIME timestamp);
The Exec function here just calls ExecDR.
virtual void AutoExec(VU_TIME timestamp);
The AutoExec function calls ExecDR and then updates the entity’s position and orientation with the dead reckon data.
virtual void ExecDR(VU_TIME timestamp);
The ExecDR function performs simple dead reckoning on the entity. This dead reckoning does not take into account any envrionmental forces (such as gravity) or barriers (such as terrain), so this may have to be specialized by the game developer.
// DATA
protected:
BIG_SCALAR drx_, dry_, drz_;
SM_SCALAR drxdelta_, drydelta_, drzdelta_;
SM_SCALAR dryaw_, drpitch_, drroll_;
SM_SCALAR dryawdelta_, drpitchdelta_, drrolldelta_;
VU_TIME lastUpdateTime_;
The VuDeadReckon data is all of the data needed to perform accurate dead reckoning independent of the represented position in the world. This separate accounting must be maintained here because the dead reckoning will often differ from the entity data in the case of a master driver or a smoothing slave.
};
The VuMaster class is also a pure vitual class, but it defines most of the functionality required by a master driver. The ExecModel is newly introduced and is virtual. Subclasses must define this. In addition, the event handlers by default are stubbed out to do nothing. If any special action is required, the subclasses should overload these.
class VuMaster : public VuDeadReckon {
public:
VuMaster(VuEntity *entity);
virtual ~VuMaster();
virtual int Type();
The Type function is defined here to return VU_GENERIC_MASTER_DRIVER.
virtual void Exec(VU_TIME timestamp);
The Exec function of VuMaster first calls ExecModel to get the new actual values. Next, it calls ExecDR to update then dead reckoning data. If the ExecModel function returned FALSE (to indicate that it was not run), then the entity position is updated with the dead reckon data. Otherwise the new entity positions are compared against the dead reckoned positions. If they are found to be out of tolerance, then a position update event is generated and posted.
VU_BOOL CheckTolerance();
CheckTolerance compares the difference between the dead reckon data and the model generated data. If the locally specified tolerances are exceeded, then this function returns TRUE. Otherwise, it returns FALSE.
void SetPosTolerance(SM_SCALAR x, SM_SCALAR y, SM_SCALAR z);
void SetRotTolerance(
SM_SCALAR yaw, SM_SCALAR pitch, SM_SCALAR roll);
By default, tolerances for a master are retrieved from the vuxSlaveSmootherSettings structure. These two functions allow the developer to assign difference tolerances for each instance of a driver.
virtual VU_BOOL ExecModel(VU_TIME timestamp) = 0;
The ExecModel function call is where physics modeling for entities should be performed. Since this is a game function, Vu provides the virtual interface here, and calls it from Exec. The function should also update the position and orientation values (and deltas) of the associated entity. The function should return TRUE if the model was run (and entity positions updated) and FALSE otherwise.
void ExecModelWithDR();
virtual VU_ERRCODE Handle(VuEvent *event);
virtual VU_ERRCODE Handle(VuFullUpdateEvent *event);
virtual VU_ERRCODE Handle(VuPositionUpdateEvent *event);
virtual VU_ERRCODE Handle(VuStateUpdateEvent *event);
virtual VU_ERRCODE Handle(VuDamageUpdateEvent *event);
All of the handlers are stubbed out and return VU_NO_OP. This is done because the master is by definition the source of the data (and hence the source of the event). As such, the event must have been generated by the local driver.
// DATA
protected:
BIG_SCALAR xtol_, ytol_, ztol_;
SM_SCALAR yawtol_, pitchtol_, rolltol_;
};
The VuSlave class uses dead reckoning for basic motion updates, and also implements a smoothing algorithm to correct error without warping. The smoothing parameters are stored in the vuxSlaveSmootherSettings class instance and (unlike the master tolerance values) are not configurable per driver.
class VuSlave : public VuDeadReckon {
public:
VuSlave(VuEntity *entity);
virtual ~VuSlave();
virtual int Type();
This Type function returns VU_SLAVE_DRIVER.
SM_SCALAR SmoothLinear(BIG_SCALAR value, SM_SCALAR delta,
BIG_SCALAR truevalue, SM_SCALAR truedelta);
SM_SCALAR SmoothAngle(SM_SCALAR value, SM_SCALAR delta,
SM_SCALAR truevalue, SM_SCALAR truedelta);
The Smooth functions implement the smoothing algorithm. This is called once for each scalar value and it work by modifying the delta value is such a way that it will rendezvous with the true dead reckoned value at the time specified in the lookaheadTime_ field of the vuxSlaveSmootherSettings class.
virtual void Exec(VU_TIME timestamp);
Exec performs the dead reckoning update on the entity and the determines whether it is time to turn off smoothing and readjust the dead-reckoning values.
virtual void AutoExec(VU_TIME timestamp);
The AutoExec function for VuSlave just calls Exec.
virtual VU_ERRCODE Handle(VuEvent *event);
The VuEvent handler does nothing and just returns VU_NO_OP.
virtual VU_ERRCODE Handle(VuFullUpdateEvent *event);
virtual VU_ERRCODE Handle(VuPositionUpdateEvent *event);
The VuFullUpdateEvent and VuPositionUpdateEvent functions extract the new update values and initiate the smoothing function. At the very least, the dead reckoning values are updated. These functions return VU_SUCCESS or VU_NO_OP (if unable to process the update).
virtual VU_ERRCODE Handle(VuStateUpdateEvent *event);
virtual VU_ERRCODE Handle(VuDamageUpdateEvent *event);
The other event handlers do nothing and return VU_NO_OP.
protected:
VU_BOOL DoSmoothing();
The DoSmoothing function returns TRUE if smoothing needs to be done and FALSE otherwise.
// DATA
protected:
BIG_SCALAR truex_, truey_, truez_;
SM_SCALAR truexdelta_, trueydelta_, truezdelta_;
SM_SCALAR trueyaw_, truepitch_, trueroll_;
SM_SCALAR trueyawdelta_, truepitchdelta_, truerolldelta_;
VU_TIME smoothEndTime_;
These values are kept around so we know when to end smoothing, and when smoothing is done, what to change the dead-reckon values back to.
};
Creating a New VuDriver Subclass
In order for the whole driver construct to be useful, motion modeling must per performed in a game developer implemented subclass of VuMaster. Here we will present the specification and implementation of just such a driver. Our sample driver will work with the SAMPLE_TENT_ENTITY first presented in the VuEntity section. For our purposes, we’ll say that tents are buildings that move around from time to time. We’ll also say that when an tent moves, it moves by warping (we won’t change the delta values). Note that, depending on our vuxSlaveSmootherSettings, the tents will appear to move more smoothy on remote machines. As this is a toy example we won’t worry about that. We’ll also simplify our motion by just moving a constant offset every warp, and we’ll warp every hour.
Step 1: Define the new driver class
Our tent driver class inherits from VuMaster and defines the AutoExec and ExecModel functions. We also keep track of when our next move should occur in the nextMove_ variable.
class TentDriver : public VuMaster {
public:
TentDriver(VuEntity *entity);
virtual ~TentDriver();
virtual void AutoExec(VU_TIME timestamp);
We define the AutoExec function to run the regular version of Exec (see below).
virtual VU_BOOL ExecModel(VU_TIME timestamp);
// DATA
protected:
VU_TIME nextMove_;
The nextMove_ variable keeps track of when we should move our tent again.
};
Step 2: Implement the new driver class
The implementation is also fairly simple. Real drivers will likely be more complicated than this as real drivers have to worry about modeling things in such a way that the move realistically or in a fun way or whatever.
TentDriver::TentDriver(VuEntity *entity)
: VuMaster(entity)
{
nextMove_ = vuxCurrentTime + (VU_TICS_PER_SECOND * 3600);
The nextMove_ variable is initialized to force the next update 3600 seconds (one hour) from initialization.
}
TentDriver::~TentDriver()
{
// empty
}
void
TentDriver::AutoExec(VU_TIME timestamp)
{
Exec(timestamp);
}
VU_BOOL
TentDriver::ExecModel(VU_TIME timestamp)
{
If our time is past when we should move, then update our position. The Exec function (which calls this) will take care of generating the position update event.
if (timestamp > nextMove_) {
BIG_SCALAR x = entity_->XPos();
BIG_SCALAR y = entity_->YPos();
BIG_SCALAR z = entity_->ZPos();
entity_->SetPosition(x + 500.0, y + 200.0, z);
nextMove_ += (VU_TICS_PER_SECOND * 3600);
}
lastUpdateTime_ = timestamp;
entity_->SetUpdateTime(lastUpdateTime_);
We will always updat the timestamp and always return TRUE, as we are running the model even if we don’t move.
return TRUE;
}
Pretty simple. A slightly more complicated example can be found in the Zoomers application, but that’s yet another toy example.
Step 3: Modify the transfer event handler for the associated entity
In this bit of code we will assume that the transfer event handle function was defined in the SampleBuildingEntity header file given in the VuEntity section. Here we implement it and ensure that when management of the entity changes to or from the local session, the driver is updated properly.
VU_ERRCODE
SampleBuildingEntity::Handle(VuTransferEvent *event)
{
SetOwnerId(event->newOwnerId_);
We always update the local data given in the event.
if (event->newOwnerId_ == vuLocalSession &&
Type() == SAMPLE_TENT_ENTITY_TYPE && EntityDriver() &&
EntityDriver()->Type() == VU_SLAVE_DRIVER) {
This bit of code is run when we assume ownership of an entity which is of the tent type and which used to have a slave driver.
TentDriver *driver = new TentDriver(this);
VuDriver *olddriver = SetDriver(driver);
delete olddriver;
These three lines create a new TentDriver, swap it out with the existing driver, and delete the old driver.
} else if (event->newOwnerId_ != vuLocalSession &&
EntityDriver() &&
EntityDriver()->Type() == VU_GENERIC_MASTER_DRIVER) {
This branch will be executed if we transfer management away and we previously had a driver (indicated by the presence and type of the driver). Note that in general, the handler will not need to differentiate between types of drivers in this case.
VuSlave *driver = new VuSlave(this);
VuDriver *olddriver = SetDriver(driver);
delete olddriver;
These three lines create a new SlaveDriver, swap it out with the existing driver, and delete the old driver.
}
return VU_SUCCESS;
}
Step 4: Modify the VuxCreateEntity function to create and add a slave
For this step, we will modify the VuxCreateEntity function given in the VuEntity section.
VuEntity *
VuxCreateEntity(ushort type, ushort size, VU_BYTE *data)
{
VuEntity *retval = 0;
switch (type) {
case SAMPLE_TENT_ENTITY_TYPE:
retval = new SampleBuildingEntity(&data);
VuSlave *slave = new VuSlave(retval);
retval->SetDriver(slave);
The above two lines are the difference here. We create and insert a slave driver for entities created via the VuxCreateEntity call (which occurs only when creating an entity as a result of receipt of a VuCreateEvent.
break;
case SAMPLE_FACTORY_ENTITY_TYPE:
retval = new SampleBuildingEntity(&data);
break;
}
return retval;
}
Step 5: Hook up the driver after creating a local entity
This step will be taken out of context a bit, but note that drivers must be set whenever local entities are created, and must be freed whenever the drivers are changed (as a result of a transfer event, for example). We will make a dummy function which creates a new tent entity with the values passed in, creates and adds a driver entity.
VuEntity *
MakeALocalTent(BIG_SCALAR x, BIG_SCALAR y, BIG_SCALAR z,
char *address, int color)
{
SampleBuildingEntity *pEnt = new
SampleBuildingEntity(SAMPLE_TENT_ENTITY_TYPE, address, color);
pEnt->SetPosition(x, y, z);
vuDatabase->Insert(pEnt);
TentDriver *driver = new TentDriver(pEnt);
pEnt->SetDriver(driver);
return pEnt;
}
Chapter 7: VuThread Use and Implementation
The VuThread class is a Vu representation of a thread of control in a game. The VuThread object itself does not in any way implement or activate OS threads. These thread objects do not even need to be associated with an actual OS thread, though they were designed to work with them. The heart of the VuThread class is the Update function, which allows Vu to perform cleanup for that control context at that time. This function call should be made from within the inner loop of the thread, and should be called often (once per loop is sufficient).
There is also a special subclass of the VuThread object called the VuMainThread. There should only be one of these objects active in a given game, and calling Update on this thread will perform several special operations if the appropriate flags are set:
The include file which defines the VuThread class is (perhaps inappropriately) called vu.h. This header file also has global variable declarations and some other miscellaneous pieces. This section will only discuss the VuThread classes and interface functions. The other pieces have either been discussed elsewhere or are obvious.
class VuThread {
public:
VuThread(VuMessageFilter *filter,
int queueSize = VU_DEFAULT_QUEUE_SIZE);
VuThread(int queueSize = VU_DEFAULT_QUEUE_SIZE);
Both of these versions of the constructor do the same thing. Two versions exist to allow any combiniation of specifying a filter (or not) and specifying a queue size. The queue size is actually relatively important, as if the queue ever wraps (more outstanding events than are in the queue), then those events which would have been overwritten are automatically dispatched (denying the thread a chance to perform special message handling). Note that the queue size specifies the number of messages which can appear in the associated queue, not the size of the queue in bytes. The default queue size is relatively small (100), and so this value should be increased for slow threads.
virtual ~VuThread();
VuMessageQueue *Queue();
The Queue accessor returns a pointer to the internal message queue of the thread object.
virtual void Update(VU_TIME currentTime);
The Update function is the heart of the VuThread class. By default, this function calls the DispatchAllMessages function of the associated message queue.
// data
protected:
VuMessageQueue *messageQueue_;
};
The VuMainThread class is special in that every program should create one and only one of these after general Vu initialization but before any use of the non-initialization Vu interface. The constructor of the VuMainThread creates several vu global variables, including the vuDatabase.
class VuMainThread : public VuThread {
public:
VuMainThread(int dbSize, VuMessageFilter *filter,
int queueSize = VU_DEFAULT_QUEUE_SIZE);
VuMainThread(int dbSize, int queueSize = VU_DEFAULT_QUEUE_SIZE);
The most important variable passed into the VuMainThread constructor is the dbSize. For optimum performance, this value should be about 10-20% higher than the maximum number of entities expected to eventually appear in the database (local and remote entities). The other parameters are just passed on to the parent class.
virtual ~VuMainThread();
virtual void Update(VU_TIME currentTime);
The Update function of the VuMainThread still calls DispatchAllMessages, but then performs other housekeeping, including reading external messages and generating auto updates.
VU_ERRCODE JoinGroup(VuGroupEntity *ent=0);
VU_ERRCODE LeaveGroup();
JoinGroup and LeaveGroup are the primary interface for joining and leaving groups for the local session. The default parameter for JoinGroup will cause the player to joing the VuPlayerPoolGroup. Calling JoinGroup for the first time will also connect the player to the network (the group in this case should be the default). Calling JoinGroup while the player is connected to another group will cause an implicit call to LeaveGroup (on the old group).
void DoCollisionCheck(VU_TIME currentTime);
DoCollisionCheck only does anything if the VU_AUTO_COLLISION flag was set. Even then, the collision detection scheme is somewhat naïve. Every time the function is called, it check for collisions between all of those entities with the collidable flag set. It avoids the O-squared problem, however, by utilizing the collision grid (which must be defined and created by the game developer). A better use for this function is to use it as an example implementation, and then adapt as needed to better fit the needs of the specific game.
protected:
void Init(int dbSize);
This Init function is just a common repository for the VuMainThread initialization code. It should be called by the constructors only, and only once.
// data
protected:
#ifdef VU_AUTO_UPDATE
VuRedBlackTree * autoUpdateList_;
#endif VU_AUTO_UPDATE
The autoUpdateList_ is a red-black tree of locally managed entities, sorted by next scheduled update time. This is used by Update to determine which entities are due to send out a VuFullUpdateEvent. After sending the message, the entity is removed and re-inserted into the list with a new update time.
#ifdef VU_AUTO_COLLISION
BIG_SCALAR maxLastCollisionRadius_;
#endif // VU_AUTO_COLLISION
The maxLastCollisionRadius_ is used to determine how large the hit bubble needs to be to ensure that all entities are adequately checked against the proper set.
};
Using the VuMainThread Class In a Networked Environment
In the Getting Started section, we covered creation and use of the VuMainThread in a single player environment. Here, we will cover the basic steps needed to hook Vu up to a network and interact with other sessions in a multi-player environment. Since all of the code in this case is concentrated in the initialization routines, this "how-to" section will not be broken into steps.
int rc = myThread->JoinGroup();
This first call to JoinGroup also performs network initialization and re-assigns the session Id if needed (to ensure that it is unique). The return code is a VU_ERR_CODE, and it will comply with the appropriate return values..
if (rc < 0) {
printf("Error: ComInit Failed");
} else {
VuMessage *req = new VuGetRequest(vuNullId);
VuMessageQueue::PostVuMessage(req);
The VuGetRequest with a null Id (passed to all sessions in the group – the default), will ask that all sessions active in the group send a full update event for all of the entities that they manage. These events will be transformed into create events locally, allowing the local session to generate an accurate picture of the network database.
}
Later, to join an active group (which presumably has an active or planned network game) simply call JoinGroup on the desired group.
Creating and Using a Custom VuThread Class
In a single-threaded environment, it is possible to customize handling of messages by writing custom VuEntity derived Handle functions and creating custom message classes. Whenever multiple threads are active, it is possible to define which messages are processed by which threads by use of the VuMessageFilter, but in many multi-threaded environments, some extra steps may be required if each thread wishes to handle the same event differently. This is the purpose of the VuThread subclass. For our example, we will create a class called the SampleThread which will just watch for a particular type of message and print out a notice of the receipt of this event. This is just a toy example, but it does illustrate the techniques needed for more serious applications.
Our SampleThread class definition is very simple. It requires no data and we need to overload only the constructor, destructor, and Update functions.
class SampleThread : public VuThread {
public:
SampleThread(VuMessageFilter *filter,
int queueSize = VU_DEFAULT_QUEUE_SIZE);
We’ll be a bit lazy here and only define the more fully qualified thread constructor. The presumption here is that most secondary thread objects will want to define a filter, which is probably a reasonable assumption to make.
virtual ~SampleThread();
virtual void Update(VU_TIME currentTime);
Our Update function is where we perform our specialization. See the definition below.
// data
protected:
// none
};
Step 2: Implement the new class
Implementation of our SampleThread is also somewhat trivial. For more sophisiticated needs, the Update function will have more logic.
SampleThread::SampleThread(VuMessageFilter *filter, int queueSize)
: VuThread(filter, queueSize)
{
// empty
}
SampleThread::~SampleThread()
{
// empty
}
void
SampleThread::Update(VU_TIME currentTime)
{
VuMessage *msg = 0;
Since we are looking for particular messages, we can’t use the DispatchAllMessages call that VuThread uses. We need to manually peek at each one, perform special handling, and then dispatch it.
while (msg = Queue()->PeekVuMessage()) {
While we have a message to process…
if (msg->Type() == VU_SESSION_EVENT) {
printf("Received a session event on special thread\n");
Check to see if it’s the kind we’re looking for, and print out a message if it is…
}
while (msg != Queue()->DispatchVuMessage())
; // flush timer events
Ok… this is a little tricky. The only time that messages can be inserted to the head of a queue are if they are a timer event. Timer events will fire on the first call to DispatchVuMessage after their time has been reached. We need to ensure that in the time between our call to PeekVuMessage and DispatchVuMessage, no timer events have jumped to the head of the queue as this would result in performing our special handling of the same message more than once. This inner loop will ensure that the the we will dispatch the same message we processed earlier in the loop. (Also note that one could lock out changes to vuxCurrentTime, but this seems dangerous. Otherwise, surrounding this loop with a critical section would not neccessarily prevent update of vuxCurrentTime and thus would not fix our problem.)
}
}
And that’s it!
Chapter 8: VuSession Use and Implementation
The VuSession sub-component is comprised of the VuSessionEntity, VuGroupEntity, VuPlayerPoolGroup and the VuSessionsIterator classes. Other than the iterator, all of the classes inherit from VuEntity, and these are used by the Vu system to track sessions (which represent players and their machines in a network environment) and groups (which represent collections of sessions, which can either be games or holding areas of gamers, such as chat rooms). In this chapter we will discuss the interface functions introduced by the classes of this sub-component.
VuSessionEntity Class Definition
Below is the VuSessionEntity class definition, along with annotation of new (or modified) functions.
class VuSessionEntity : public VuEntity {
public:
VuSessionEntity(ulong domainMask, char *callsign);
The domainMask passed in (and stored) is a bitmask of the domains which can be managed by that session entity. This is the mechanism used by sessions to communicate which entities they are capable of managing. The callsign (also stored) is the player’s ‘handle’ by which he will be identified to other players. Note that Vu does not ensure uniqueness of handles.
VuSessionEntity(VU_BYTE **stream);
VuSessionEntity(FILE *file);
The stream and file versions of the constructor are used to create a session entity from net data or a file image stored on disk. Note that restoring sessions from disk must be handled with care as sessions from disk are not neccessarily active in the current game and may have different session ids. It can be used successfully to keep track of all of the players in a network game and their names, but should not be inserted into the vuDatabase.
virtual ~VuSessionEntity();
The destructor of the session entity just cleans up local memory. It does not close any active connections. For the local entity, this is done in the destructor of the VuMainThread.
virtual int SaveSize();
virtual int Save(VU_BYTE **stream);
virtual int Save(FILE *file);
The above three functions comprise the serialization interface.
ulong DomainMask();
VU_SESSION_ID SessionId();
char *Callsign();
ulong LoadMetric();
VU_ID GroupId();
VuGroupEntity *Group();
The above five functions are the getters for VuSessionEntity. GroupId returns the group with which a session is currently associated. Group returns a pointer to that group, or 0 if the group associated with the GroupId is not in the local vuDatabase. LoadMetric is not used by default (and is assigned a value of 1), but it can be used for load balancing.
void SetCallsign(char *callsign);
SetCallsign changes the current session’s callsign and (if local) sends out the appropriate update message.
void SetLoadMetric(ulong newMetric);
This function sets the load metric (but does not automatically send out an update). By default, the LoadMetric is set to a value of 1, and is not used. This variable exists to provide a standard hook for doing load balancing (see the FAQ).
VU_ERRCODE JoinGroup(VuGroupEntity *newgroup);
JoinGroup is used to connect to the network, change the group associated with a session, or updating a remote session’s group association. This function takes care of the id change (if required by the low level comms layer), and flushing outstanding queues. In general this function should only be called by Vu.
VU_ERRCODE LeaveGroup();
LeaveGroup should only be called when shutting down a session (as it will leave the session without a Group and disconnect it from the network).
virtual VU_ERRCODE Handle(VuEvent *event);
VuSessionEntity watches for entity destruction events in order to force a redistribution of newly unmanaged entities when an session goes away.
virtual VU_ERRCODE Handle(VuSessionEvent *event);
The VuSessionEvent handler implementation performs specialized handling of session events.
protected:
VuSessionEntity(int typeindex,
ulong domainMask, char *callsign);
ushort OpenSession();
void CloseSession();
OpenSession is used to connect to the low level communications layer and open a session. CloseSession does the reverse.
// DATA
protected:
VU_SESSION_ID sessionId_;
ulong domainMask_;
char *callsign_;
VU_BYTE loadMetric_;
VU_ID groupId_;
VuGroupEntity *group_;
The group_ pointer is kept around to avoid multiple calls to the vuDatabase Find function. Since this pointer is valid as long as the group is active, and the session must be in a group, this should be fairly safe.
};
VuGroupEntity Class Definition
Below is the VuGroupEntity class definition.
class VuGroupEntity : public VuEntity {
friend class VuSessionsIterator;
friend class VuSessionEntity;
The VuGroupEntity grants friend access to the VuSessionsIterator (which needs access to the interal list of sessions) and the SessionEntity, which needs to call the Distribute function.
public:
VuGroupEntity(ulong domainMask,
char *gamename, char *groupname);
VuGroupEntity(VU_BYTE **stream);
VuGroupEntity(FILE *file);
virtual ~VuGroupEntity();
virtual int SaveSize();
virtual int Save(VU_BYTE **stream);
virtual int Save(FILE *file);
Unlike the VuSessionEntity save and restore functions, groups can be restored and made active like any other entity in Vu. However, the list of sessions in the group needs to be treated carefully, as the list in the default VuGroupEntity contains those that are currently in the group, while the image stored to disk will be the list that was active when the group was saved.
ulong DomainMask();
By default, Vu does nothing with the DomainMask value stored in the group. As with VuSessionEntity, this is a bitmask, and can be used by the game developer as a basis for granting or refusing a session’s requested access to a group.
char *GameName();
GameName can be used to identify the type of game represented by a group. By default, this should be the same as the controlling vuxGameName, but this can be over-ridden by setting this name in the constructor. Once set, this value should not change.
char *GroupName();
GroupName is an alpha-numeric value which identifies the name of the group (or game instance). This can be changed at run time and this change will be communicated to other sessions on the network.
ushort MaxSessions();
MaxSessions is a restriction imposed on the group which forces the group to deny a new member from joining a group if the SessionCount of the group equals or exceeds the value of MaxSessions.
ushort SessionCount();
SessionCount returns a count of the sessions currently in the group.
void SetGroupName(char *groupname);
void SetMaxSessions(VU_BYTE max);
The setters change the values returned by the getters above. SetGroupName will automatically generate an update event, but the other does not. In this case the game developer should generate a VuFullUpdate event to note the change (if this is deemed necessary).
VU_BOOL SessionInGroup(VuSessionEntity *session);
SessionInGroup returns TRUE if the given session is in the group, and FALSE otherwise.
virtual VU_ERRCODE AddSession(VuSessionEntity *session);
virtual VU_ERRCODE AddSession(VU_ID sessionId);
virtual VU_ERRCODE RemoveSession(VuSessionEntity *session);
virtual VU_ERRCODE RemoveSession(VU_ID sessionId);
AddSession and RemoveSession are used to request addition of a session to or removal from, the local group. VuSessionEntity calls these functions when using the Join/LeaveGroup functions. They are virtual and can be over-ridden to use additional or different criteria for granting or refusing access.
virtual VU_ERRCODE Handle(VuSessionEvent *event);
The Handle function for VuSessionEvent is used to communicate special group changes to other sessions. Note that this is the same event as is used by VuSessionEntity. The data is interpreted slightly differently, but the context is known because of the target of the event (if the target is a session, then it’s a session event; if it’s a group, then it’s a group event).
protected:
VuGroupEntity(int type, ulong managementDomain,
char *gamename, char *groupname);
virtual VU_ERRCODE Distribute(VuSessionEntity *ent);
The Distribute function call is used to cause an automatic transfer of entities around the network. The default implementation spreads entities uniformly between all sessions in the group, weighting the assignment by the LoadFactor of each session (default is 1 for all sessions). If the session pointer passed in is NULL, then all entities are spread out roughly randomly between the legal sessions in the group. If a session is specified, then only entities owned by that session are re-distributed. In any case, entities are never transferred to sessions which claim to be unable to manage them due to their management domain setting. Also, any entities which have the transferrable flag set to FALSE which must be transferred away from a specified session will be removed from the database. Finally, entities which have an association with another entity (see the Association function of VuEntity) will be transferred to the same session as their association. Note that this determination is not recursive: This re-determination only applies the transfer algorithm to the parent, without doing the association check. Since this is a virtual function, child classes of VuGroupEntity can implement more sophisticated load balancing which takes into account connection quality and machine speed (or at least to distribute to more than one session).
// DATA
protected:
// shared data
ulong domainMask_;
char *gameName_;
char *groupName_;
ushort sessionMax_;
VuOrderedList *sessionCollection_;
The data of the VuGroupEntity is all shareable and saveable. The details have already been discussed in the accessor functions above.
};
VuPlayerPoolGroup Class Definition
The VuPlayerPoolGroup is a specialization of the VuGroupEntity. Like the VuMainThread, each active game executable will create one and only one of them, except that Vu takes care of creating the VuPlayerPoolGroup. This entity is special in that it is local and is not owned by any entity. This works by ensuring that Vu creates the default group in the same way when initializing Vu. The VuPlayerPoolGroup represents the pool of players not currently assigned to any particular game, and it can be used as a chat room, if desired. If the local session joins the default group, it is the VuPlayerPoolGroup that will be joined.
class VuPlayerPoolGroup : public VuGroupEntity {
public:
VuPlayerPoolGroup(ulong managementDomain);
virtual ~VuPlayerPoolGroup();
virtual int SaveSize();
virtual int Save(VU_BYTE **stream);
virtual int Save(FILE *file);
The VuPlayerPoolGroup cannot be saved (since it is generated at start time) nor transferred (since every session creates one). All of the serialization functions are stubbed out in their implementation.
private:
VuPlayerPoolGroup(VU_BYTE **stream);
VuPlayerPoolGroup(FILE *file);
The stream and file constructors are private for this class to ensure that they are not called (since this class is local and not saveable). These functions should never be called.
// DATA
protected:
// none
};
VuSessionsIterator Class Definition
The VuSessionsIterator class is used to iterate over all of the sessions in a group. It is very similar to the other iterators, except that it takes a group in the constructor (to identify the group whose sessions are desired) and the GetFirst/GetNext functions return VuSessionEntity pointers instead of generic VuEntity pointers.
class VuSessionsIterator : public VuIterator {
public:
VuSessionsIterator(VuGroupEntity *group=0);
If the group is not specified, then this iterator operates on the group associated with the local session. If the local session does not have a local group, then God help you.
virtual ~VuSessionsIterator();
VuSessionEntity *GetFirst();
VuSessionEntity *GetNext();
GetFirst/GetNext operate just like the functions of the same name in the VuListIterator, except that they return pointers to VuSessionEntities. We can do this safely because only VuSessionEntities are added to the VuGroupEntity’s ordered list.
virtual VuEntity *CurrEnt();
virtual VU_BOOL IsReferenced(VuEntity *ent);
virtual VU_ERRCODE Cleanup();
The other interface functions are exactly like the linked list functions of the same name.
protected:
VuLinkNode *curr_;
};
Creating a New VuGroupEntity Subclass
This section will not cover the steps required to make a new subclass, as Vu already provides a good sample implemenation in the VuPlayerPoolGroup class. The game developer must define a new class and implement it. The most important function is the Distribute function – without which there is little reason for creation of a new subclass. The other functions are straightforward.
Chapter 9: Frequently Asked Questions
This chapter will present many of the questions that were asked by early developers using the Vu2 system that require techniques which are not covered in the earlier chapters of this document. The answers provided will include steps and sample source where appropriate. Note that it is quite possible that some techniques which may be applied to the Vu2 system will not be answered here, but that does not necessarily mean that the technique in question is not a valid one. One general caution: As with any library, misuse can result in unpredictable (and often catestrophic) behavior.
Question 1: How do I get all entities from the network?
This was covered in the chapter on VuThread, but is fundamental enough to be repeated here. The bit of code is really quite simple, and is given below:
VuMessage *req = new VuGetRequest(vuNullId);
VuMessageQueue::PostVuMessage(req);
As with all messages, posting it to the static message queue ensures that it will be processed and cleaned up. By specifying the vuNullId, the program is requesting all entities, and by using the default routing for the VuGetRequest, we are requesting all entities from all sessions in the local group. This operation should be performed shortly after joining a group.
Note that it is possible for sessions to acquire the entire database from the network without a request message by simply listening to the perioding full updates if the VU_AUTO_UPDATE flag is set. In this case, the database will be current once the longest update cycle has been closed. Unfortunately, there is not an easy way to tell what this time is, though there is a way to tell when we’ve sent all the entities as a response to a get request (see question 2).
Question 2: How can I tell when I’ve received all entities from the network?
By default, Vu does not provide an end of transmission message or any other way to determine when we’ve finished sending all of our messages. The philosophy behind this as a default behavior is that we are working in a fluid system. While a response to a get request will send all currently managed data, it is always possible to dynamically create another, and programs need to handle this case anyway. Also, dynamic handling of response messages allows immediate return from the get request message without a special state where we’ve requested network data but haven’t received any yet.
That said, some systems work better when they have a more complete image of the network database to work from. There is a little extra work required to make this happen, and a sample implementation of this is provided below. One important note to make here is that this system depends on reliable and ordered transmission of response data. Whether this is true or not depends on the implementation of the transport layer.
Step 1: Define new unique message types
Here we define two new messages which will allow us to send and process confirmed request messages. We start our namespace addition at 2 to account for the chat message defined in an earlier chapter.
#define SAMPLE_EOT_MESSAGE (VU_LAST_EVENT + 2)
#define SAMPLE_CONFIRMED_GET_MESSAGE (VU_LAST_EVENT + 3)
Our implementation will make use of three globals:
int waitingOnSessionCount = -1;
VU_BOOL doneLoading = FALSE;
VU_MSG_ID currentConfirmedGet;
waitingOnSessionCount keeps track of the number of sessions which have yet to send all of their data, the doneLoading flag reports when we’re clear to leave the waiting for data state, and the currentConfirmedGet is just a variable to ensure that we don’t cross ourselves with multiple simultaneous confirmed get requests, or by other random SampleEOTMessages which we process for whatever reason.
Step 3: Define the new message classes
In order to make this work correctly, we need to define two new classes. The first is the SampleEOTMessage, which is used to mark the end of transmission of the response to a get message. We keep track of the source message id so that the receiver can connect the response to the earlier request (this code resides in the Process method of the SampleEOTMessage class).
class SampleEOTMessage : public VuMessage {
public:
SampleEOTMessage();
SampleEOTMessage(VU_MSG_ID srcmsgid, VU_SESSION_ID dest);
virtual ~SampleEOTMessage();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// DATA
protected:
VU_MSG_ID srcmsgid_;
};
The other new class is the SampleConfirmedGetRequest. We create this class primarily to allow the target station(s) to correctly respond to the reqeust by sending their managed entities plus a SampleEOTMessage at the end. This is performed in the Process function. Note that this class has no data. All behavior is encapsulated in the implementation.
class SampleConfirmedGetRequest : public VuGetRequest {
public:
SampleConfirmedGetRequest();
virtual ~SampleConfirmedGetRequest();
virtual int Size();
virtual int Decode(VU_BYTE **buf);
virtual int Encode(VU_BYTE **buf);
protected:
virtual VU_ERRCODE Process();
// DATA
protected:
// none
};
Step 4: Implement the new classes
Although we have presented sample message subclass implementations earlier in this document earlier, there are a few new twists that are used here. They will be discussed where presented.
SampleEOTMessage::SampleEOTMessage()
: VuMessage(SAMPLE_EOT_MESSAGE),
srcmsgid_(vuNullId)
{
// empty
}
SampleEOTMessage::SampleEOTMessage(VU_MSG_ID srcmsgid,
VU_SESSION_ID dest)
: VuMessage(SAMPLE_EOT_MESSAGE, vuNullId, dest),
srcmsgid_(srcmsgid)
{
// empty
}
SampleEOTMessage::~SampleEOTMessage()
{
// empty
}
int
SampleEOTMessage::Size()
{
return sizeof(this);
}
We return the size of the class because of our implementation of the Decode and Encode functions presented below.
int
SampleEOTMessage::Encode(VU_BYTE **buf)
{
int retval = Size();
// use bitwise assignment operator
(SampleEOTMessage &)**buf = *this;
*buf += retval;
return retval;
}
The Encode implementation uses a trick which bears explaining. The use of the bitwise assignment operator (after casting the buffer pointer to a compatible type) copies everything in the class (except for the virtual table pointer) to the memory of the cast pointer. This implementation has two disadvantages: first, it works because of an obscure trick of C++, and second, it copies (and sends) unneeded memory to the message buffer. This is offset by two advantages: first, the implementation of the bitwise assignment operator is usually just a memcpy, so it’s fairly efficient; second, this is boilerplate code – this same code can be copied to any Encode implementation and it will function in the same fashion. The only change required is to change the type of the cast (the compiler will flag this with an error if not done). Note that this just copies the structure, so embeded pointers (such as the VuEntity pointer in VuMessage and the string in our SampleChatMessage) will not have meaningful data, so where this data needs to be transferred accurately, choose the longhand implementation demonstrated in SampleChatMessage.
int
SampleEOTMessage::Decode(VU_BYTE **buf)
{
int retval = Size();
// use bitwise assignment operator
*this = (SampleEOTMessage &)**buf;
*buf += retval;
return retval;
}
The Decode implementation uses the same trick as Encode, but in reverse.
VU_ERRCODE
SampleEOTMessage::Process()
{
if (srcmsgid_ == currentConfirmedGet) {
waitingOnSessionCount--;
After checking to confirm that the message received is in response to the request we made (and are tracking), we decrement the global receipts counter.
}
if (waitingOnSessionCount <= 0) {
doneLoading = TRUE;
Here we set the flag for processing in our main loop (step 5, below). Note that there is a small chance for a problem in our computation if sessions are entering or leaving while we are waiting for responses. If this is a concern, then perfrom special processing of the VuSessionEvent and VuDeleteEvent to adjust the confirmedReceipts value accordingly.
}
return VU_SUCCESS;
}
The SampleConfirmedGetRequest message is a special version of the get request message. It will specify that all sessions in the current group return all of their managed entities. In addition, this request is processed on the receiving station (see the Process implementation below) and generates a SampleEOTMessage at the end.
SampleConfirmedGetRequest::SampleConfirmedGetRequest()
: VuRequestMessage(SAMPLE_CONFIRMED_GET_MESSAGE,
vuNullId, VU_GROUP_TARGET)
{
// empty
}
SampleConfirmedGetRequest::~SampleConfirmedGetRequest()
{
// empty
}
int
SampleConfirmedGetRequest::Size()
{
return sizeof(this);
}
int
SampleConfirmedGetRequest::Encode(VU_BYTE **buf)
{
int retval = Size();
// use bitwise assignment operator
(SampleConfirmedGetRequest &)**buf = *this;
*buf += retval;
return retval;
}
int
SampleConfirmedGetRequest::Decode(VU_BYTE **buf)
{
int retval = Size();
// use bitwise assignment operator
*this = (SampleConfirmedGetRequest &)**buf;
*buf += retval;
return retval;
}
Note the same boilerplate code in these implementations of Size, Encode, and Decode as were presented in the SampleEOTMessage. The only changes are the type of the cast of the byte stream buffer pointer.
VU_ERRCODE
SampleConfirmedGetRequest::Process()
{
If (Sender() != vuLocalSession) {
This test ensures that we don’t send a response to ourselves.
VuMessage *resp = 0;
VuFlagBits mask = { 1, 1, 1, 1, 1 }; // always match...
VuStandardFilter filter(mask, TRUE); // ... all local ents
This filter will ensure that we only return all entities managed by the local session (which is exactly what was requested).
VuDatabaseIterator iter;
VuEntity *ent = iter.GetFirst(&filter);
while (ent) {
resp = new VuFullUpdateEvent(ent);
VuMessageQueue::PostVuMessage(resp, Sender());
ent = iter.GetNext(&filter);
}
The above loop sends out all the locally managed entities to the requestor.
resp = new SampleEOTMessage(srcmsgid_, Sender());
VuMessageQueue::PostVuMessage(resp);
After we’ve sent all our data, we send out our special EOT message.
return VU_SUCCESS;
}
return VU_NO_OP;
}
Step 5: Use the new classes (and globals) in our game connection and main loop code
Finally, we use our new classes and globals in two places. The first of these is in our join group code. After we successfully join a group, we send out our new SampleConfirmedGetRequest and initialize all of our globals. Other globals (such as the new program state) should be set up here. Note that the confirmedReceipts is set to count - 1 instead of count. This is done because we will not receive an update from ourselves, and we are counted in the SessionCount.
int rc = myThread->JoinGroup(newGroup);
if (rc) {
VuMessage *req = new SampleConfirmedGetRequest;
currentConfirmedGet = req->MsgId();
doneLoading = FALSE;
waitingOnSessionCount =
vuLocalSessionEntity->Group()->SessionCount() - 1;
VuMessageQueue::PostVuMessage(req);
}
The other place we need to put new code is in the main loop or sub-loop where we check the status of the doneLoading flag to determine if we can transition from our waiting state to our normal state. The details of this transition are program specific, so only a sketch is provided here.
// check for completion of load
if (doneLoading == TRUE) {
// do regular processing
} else {
// just return
}
Question 3: How do I save/restore entities to/from a file?
Generally speaking, there are two main methods for saving entities to a file. The first is to iterate across the database, storing the entity type and invoking the Save function for each. Restoring in this case is accomplished by performing the reverse operation: reading the type, and then for each stored entity, create an entity of the appropriate type with the file pointer. In this case, the game developer is responsible for demarking the entities (or terminating the file) and affixing any special header information.
The other method for storing and retrieving the database is to use the Save and Restore functions of the vuDatabase. This is arguably a simpler interface since it is a bit more formalized, but the same steps are required. In addition, the vuDatabase save and restore functions automatically demarkate entity records and also mark the end of file. To use the vuDatabase interface, perform the following steps:
Step 1: Create the callback functions
The callback functions are relatively simple. There are read and write callbacks for the database header (written and restored before any entities are saved), and read and write callbacks for each entity, which allows entity header and footer data to be written. Our sample code will utilize printf to tell us where we are doing stuff.
static int saveCount = 0;
static int restoreCount = 0;
saveCount and restoreCount are initialized in WriteHeaderCB and ReadHeaderCB and are updated in SaveCB and RestoreCB respectively. It is only used for printing purposes, but could be used for checksum.
VU_ERRCODE
WriteHeaderCB(FILE *file)
{
short count = vuDatabase->Count();
saveCount = 0;
printf("Called WriteHeaderCB, %d ents\n", count);
fwrite(&count, sizeof(short), 1, file);
return VU_SUCCESS;
}
Our sample WriteHeaderCB stores the count of the entities in the database.
VU_ERRCODE
ReadHeaderCB(FILE *file)
{
short count = -1;
restoreCount = 0;
fread(&count, sizeof(short), 1, file);
printf("Called ReadHeaderCB; %d ents\n", count);
return VU_SUCCESS;
}
Our ReadHeaderCB just reads the count stored in the write callback. Note that this value is not used (except to note the value). If the game developer wanted to use this value (as a checksum, for instance), then it would need to be stored in a global variable.
VU_ERRCODE
SaveCB(FILE *file, VuEntity *ent)
{
ushort type = ent->Type();
if (type > VU_LAST_ENTITY_TYPE) {
printf("Saving entity #%d, id 0x%lx\n", ++saveCount,
(VU_KEY)ent->Id());
fwrite(&type, sizeof(ushort), 1, file);
return VU_SUCCESS;
}
return VU_ERROR;
}
The SaveCB performs a very important function: it stores the type of the given entity to the file at the current position. This is needed by the corresponding RestoreCB to build the correct type. We only save our entities (those with a type greater than VU_LAST_ENTITY_TYPE) because we are not performing specialized processing of the session and group entities (which would otherwise be inserted into the database – clearly a bad thing). Note, however, that it does not call the Save function on the entity. The VuDatabase::Save function does this automatically.
VuEntity *
RestoreCB(FILE *file)
{
ushort type = 0;
VuEntity *retval = 0;
static int i = 0;
fread(&type, sizeof(ushort), 1, file);
switch(type) {
case SAMPLE_TENT_ENTITY_TYPE:
case SAMPLE_FACTORY_ENTITY_TYPE:
retval = new SampleBuildingEntity(file);
printf("Restoring entity #%d, id 0x%lx\n", ++restoreCount,
retval->Id());
break;
default:
printf("Found unknown entity type %d\n", type);
break;
}
return retval;
}
RestoreCB checks the type, and if it’s one of ours, then we create it with the file handle. Note that we do create the entity (which is a slightly different paradigm than the SaveCB, which lets the caller invoke the Save function). Also note that we do not insert the entity into the database – the VuDatabase::Restore function does this.
Step 2: Call the database save and restore functions
At this point it is worth repeating the warning about multiple instances of the same entity in the database. Vu does not prevent the developer from inserting multiple copies of the same entity into the database more than once, but if this does occur then Vu will exhibit unpredictable and perhaps fatal behavior. Prior to restoring, it is advisable to remove all entities from the local database (except for sessions and groups).
char *filename = "sample.sav";
vuDatabase->Save(filename, WriteHeaderCB, SaveCB);
The Save function takes a filename (not a file pointer), and our header writer callback and save callback. It is legal to pass in a NULL for the header writer callback, in which case no header is written. The return value is a VU_ERRCODE, which will return VU_ERROR if the file could not be opened, and VU_SUCCESS otherwise.
vuDatabase->Restore(filename, ReadHeaderCB, RestoreCB);
The Restore function reads in the sample data stored by the save function. As with the Save function, the ReadHeaderCB is optional, but if data is written by the writer, then it must be read by the reader. Vu will not skip any data in the file.
Question 4: How do I manage my own collection?
Some of the early developers wanted a way to have private lists (or other collections) whose membership was detemined by logic in the code instead of the provided filter interface. A good example of the need for someting like this is a target list. The most obvious solution would be unregistered lists, but the disadvantage of these is that unregistered collections will not be able to take advantage of any of the thread safety or memory management features of the Vu system. The solution is to custom subclass of the desired collection type with a few modifications. Our sample collection, which we’ll call the SampleTargetList, will be a linked list with specialized insert functions.
Step 1: Define the new collection subclass
The SampleTargetList is a very simple class. The only thing we are adding is an overload of the virtual Insert function, and a new ForcedInsert function to replace it. The class has no data (and needs none).
class SampleTargetList : public VuLinkedList {
public:
SampleTargetList();
virtual ~SampleTargetList();
virtual VU_ERRCODE Insert(VuEntity *entity);
We need to overload the Insert function to prevent Vu calls to this collection from inserting anything.
VU_ERRCODE ForcedInsert(VuEntity *entity);
Then we create our own insert function to add things to the list, as we have disabled the regular function.
// DATA
protected:
// none
};
Step 2: Implement the new class
SampleTargetList::SampleTargetList()
: VuLinkedList()
{
// empty
}
SampleTargetList::~SampleTargetList()
{
// empty
}
VU_ERRCODE
SampleTargetList::Insert(VuEntity *entity)
{
return VU_NO_OP;
}
We have overloaded the insert function to do nothing. Since we don’t insert the entity, and this is normal behavior, we return VU_NO_OP.
VU_ERRCODE
SampleTargetList::ForcedInsert(VuEntity *entity)
{
return VuLinkedList::Insert(entity);
}
Our new insert function, called ForcedInsert, simply invokes the parent class Insert function, which inserts the entity into the collection.
Step 3: Use the new class interface
When Vu uses the interface, it will use the standard Insert function call, which will never insert an entity into the database. The line of code below assumes myTargetList is a pointer to a SampleTargetList, and that myEntity is a pointer to a VuEntity (or subclass). The lines below illustrate insertion and removal from our newly defined list.
myTargetList->ForcedInsert(myEntity);
// . . .
myTargetList->Remove(myEntity);
Note that iteration across this list can use the standard VuListIterator.
Question 5: How do I transfer entity ownership?
Entity transfer can happen as a result of several built-in mechanisms. The first of these are the VuPushRequest and VuPullRequest messages. VuPushRequest is used to ask a remote session to assume management of a local entity, which VuPullRequest is to ask a remote session to grant management of the referenced entity to the requesting session. In either case, the receiver of the message has the responsibility of changing the ownerId (and generating the corresponding VuEntityTransferEvent), though the VuEntity::Handle functions for VuPushRequest and VuPullRequest perform this function automatically. If the entity cannot be transferred for whatever reason, then the receiver of the request has the responsibility to send an error response. Again, the default VuEntity::Handle implementations ensure that this can happen. Note also that the game developer can overload these handlers to place new restrictions on transfers, but the same rules should be followed.
The other way to tranfer entities is through the VuGroupEntity::Distrubute function call. This call will either re-distribute all of the entities from a single station to others in the same group, or it will redistribute all of the entities in the group to new stations. The first case is used for disaster recovery where a given station has unexpectedly left the game (dropped connection or crashed) while the second case is used for load balancing. In these cases, the default implementation does not use the VuPushRequest or VuPullRequest interface. Instead, each entity which assumes ownership sets the ownerId to the local sesion id, and sends out the VuEntityTransferEvent to inform other stations of the change. Since all stations should be using the same re-distribution algorithm, there should be no contention for this re-distribution, and minimal message traffic. For more details on load balancing, see question 7.
Question 6: How to I ensure that two entities are managed by the same session?
Sometimes it is advantageous to ensure that entities are managed by the same session. This is useful where accurate tracking is required (for example: a fast moving missile and its fast-moving target), or where some of the data required by an entity is only kept on the master station of an associated entity, or in the case where entity aggregation and deaggregation is performed.
Vu provides a SetAssociation interface to the VuEntity which allows the game developer to associate one entity with another. This ensures the following:
Altough this is useful, the association does not automatically ensure the following:
These two characteristics mean that it is up to the game developer to transfer associated groups of entities in the correct order: ‘parent’ first, followed by ‘children’. Also, ‘parents’ should never have any association. In these cases, all entities should associate with what would be the parent of the parent. This is the only way to ensure corrent behavior. Also, under no circumstances should entities associate with each other. This will just prevent any transfer and will probably ensure that the two entities end up managed by different sessions after a Distribution.
Question 7: How do I do my own load balancing?
The default load balancing system distributes the entities more or less equally among all of the sessions in the current group. While this may work for most systems, it will not work well for systems which use a client-server model (where most or all of the entities are managed on a dedicated server) or a network of systems with widely varying connectivity or CPU capacity. In these cases, it might be nice to give a larger share of the entities to the machines with a higher capacity. To do this, modify the LoadFactor of the high capacity session is higher than those of lower capacity sessions. There are two important things to remember when setting these values:
Note that by default, Vu assigns a LoadFactor of 1 to all sessions. Note also that the Distribute call reassigns all entities, so adjusting the LoadFactor based on the number of entities managed will cause skewed results for future load balancing calls unless a custom load balancing method is used.
If it is determined that the load balancing algorithm provided by Vu is just fundamentally unsatisfactory for the game’s needs, the game developer can write his own load balancing algorithm. This is accomplished by doing the following:
Step 1: Create a new group entity subclass
For our purposes, we will define the class skeleton (including the #define for our new type) but not the new typetable entries or modifications to the VuxCreateEntity function call. These other steps are still requried, but have been covered elsewhere.
#define SAMPLE_GROUP_TYPE (VU_LAST_ENTITY_TYPE + 4)
class SampleGroup : public VuGroupEntity {
public:
SampleGroup(ulong managementDomain, char *gamename,
char *groupname);
SampleGroup(VU_BYTE **stream);
SampleGroup(FILE *file);
virtual ~SampleGroup();
virtual int SaveSize();
virtual int Save(VU_BYTE **stream);
virtual int Save(FILE *file);
protected:
virtual VU_ERRCODE Distribute(VuSessionEntity *ent);
// DATA
protected:
// none
};
Step 2: Implement the new class
Our SampleGroup class is mostly skeletal, but is actually rather complete. Only the Distribute function is artificially stubbed out. The other functions would need to change if we stored networkable/storable data in this class, but that’s it.
SampleGroup::SampleGroup(ulong managementDomain,
char *gamename, char *groupname)
: VuGroupEntity(SAMPLE_GROUP_TYPE, managementDomain,
gamename, groupname)
{
// empty
}
SampleGroup::SampleGroup(VU_BYTE **stream)
: VuGroupEntity(stream)
{
// empty
}
SampleGroup::SampleGroup(FILE *file)
: VuGroupEntity(file)
{
// empty
}
SampleGroup::~SampleGroup()
{
// empty
}
int
SampleGroup::SaveSize()
{
return VuGroupEntity::SaveSize();
}
int
SampleGroup::Save(VU_BYTE **stream)
{
return VuGroupEntity::Save(stream);
}
int
SampleGroup::Save(FILE *file)
{
return VuGroupEntity::Save(file);
}
The serialization functions just call the parent functions, but if any data were to be added at this level, this code should be updated to copy the appropriate data (see the sample VuEntity subclass implementation for examples of how to do this).
VU_ERRCODE
SampleGroup::Distribute(VuSessionEntity *ent)
{
// custom distribution algorithm goes here...
}
The Distribute function of the SampleGroup class is where we would implement our custom load balancing algorithm. Since this mechanism is very problem dependent, we will just leave this implementation at the skeleton level.
Question 8: How do I manage multiple domains?
Management domains are a way of allowing plug-compatible products to work together in a networked environment. Special allowance is needed to ensure that entities can be transferred only to other stations which are capable of managing them. For example, a flight simulator which models F-16 characteristics may be able to display Apache helicopters, but still be unable to accurately model their motion (and vice versa). Providing that both of these products use compatible versions of Vu, they can still play in common games as long as their management domains are properly set up.
The first step in ensuring compatability is to ensure that name space mapping does not overlap. The management domain bitmask namespace has 32 bits, which means that only 32 domains can be defined for a particular product line. The VuSessionEntity stores a 32 bit management domain bitmask (called domainMask_) which has one bit set for each domain which that session can manage. Each VuEntity has a VU_BYTE field (called managementDomain_) which identifies which bit in the domainMask must be set for a session to manage that entity. Note that the managementDomain_, as an 8-bit value, can identify 256 bit positions, but that Vu can only use the first 32. Setting a managmentDomain_ to a value higher than 32 will likely cause unexpected results. When assigning domains, remember that values extend from 0-31, and that Vu defines the value 0 to be a global namespace, which means that all products in the line must be capbable of managing entities so designated (Vu defined entities fall into this category). This leaves 31 bits for user defined domains, and the task of ensuring correct definition and use of these is left up to the game developer. The managementDomains for VuEntities should be defined in the VuEntityType table, as this value is copied to the entity structure on initialization.
Once domains have been defined and assigned to entities in the system, the game developer must ensure that all sessions have a correct domainMask which represents which domains they can manage. This must be set up by setting the external variable vuxLocalDomain. Vu will use this value to initialize the domainMask of the vuLocalSessionEntity.
Finally, the game developer must ensure that entities which are explicitly requested via a VuPullRequest can in fact be managed by the local session. Vu will take care of ensuring domain compliance in all other cases. Note that Vu does not provide any special facilities for ensuring that entities can be displayed by systems of another Domain, or even that they have a valid VuEntityType table entry. For evolving systems, the game developer might wish to implement the VuEntityType table as a file based component or even as a shareable entity (or collection) itself. This sort of implementation would allow dynamic creation and handling of new entity types, though display is another problem altogether.
Question 9: How do I do entity Aggregation and Deaggregation?
Entity aggregation and deaggregation is the practice of grouping entities which are not near any players on the network into larger, more abstract groups so as to not load that network with updates to small units which nobody really cares about. Deaggregation is the act of breaking one of these abstract units into component units for display when a player moves near to one of these units, while aggregation is the act of removing the component units and reflecting this fact back in the parent unit. Ideally, if a player flew over a unit (causing deaggregation), destroyed a small component of that unit (say, a tank), then flew away (causing aggregation), if that player (or another) were to fly back over that unit, they should be able to see the unit in its reduced strength, and (better yet) see the destroyed tank.
One important note: Vu does not perform any of the above functions. The problems of which units to aggregate, how to fold in data that can be accurately deaggregated again is best solved by the game developer as a design consideration. Chances are good that generic solutions would be too general to be useful. Also, any generic solution provided here would be quite long and would also be too general to be useful. Vu does provide many facilities which make it possible to perform aggregation and deaggregation, and this section will discuss these facilities and ways they might be used.
Tool 1: VuManageEvent, VuUnmanageEvent, and VuReleaseEvent
These two events were created specificially to facilitate aggregation and deaggregation. A VuManageEvent is used to indicate that an entity (which was always there) is now being managed by some session on the network. A VuUnmanageEvent is used to indicate that an entity will soon not be managed by the current manager. If no entity assumes ownership of the entity by the time specified, then it will be deleted. The VuReleaseEvent is used to silently purge an entity from the local session’s database without requiring deletion from the owning session’s database. This is an important tool because it implies that all sessions on the net do not necessarily need to have the same database image at the same time. One important note to make here is that entities which are unmanaged (and deleted) and then subsequently re-managed will not necessarily have the same entity Ids. This can be accomplished, but requires some additional accounting and name space reservation.
As discussed in FAQ #6, entities can ensure that they are managed by the same session by associating a child session with the parent. It is quite possible that deaggregated entities will require specialized access to the parent instance, and entity association can guarantee functional access to this data.
The VuAntiDatabase works behind the scenes, but is an important component in environments where entities exist but are not instantiated in all sessions. At a minimum, Vu adds entities to the local anti-database which have the session’s Id in the creator field of the entity Id, but which are not resident in that session’s vuDatabase. This ensures that Vu will not attempt to create an entity whose Id is already in use, but just not instantiated locally (this is the only thing Vu uses the VuAntiDatabase for). It could also be used to preserve the id’s of de-aggregated entities which are not currently managed by anyone, but Vu does not currently do this, and the game developer would face a bit of work to take advantage of this.
While the VuGridTree is used for collision detection, it can also be used as a relatively fast proximity test. When deciding whether or not to deaggregate an entity, one could check each aggregate against each player periodically, or one could store aggregates in a VuGridTree, and then use the iterator (passing in each player entity) to find all of the aggregates within a given range of that player entity.
Chapter 10: Sample Application Notes
This chapter will briefly discuss each of the three applications distributed with the Vu system. It will discuss what they test, and what they accomplish. One of the applications, Netwatch, is actually an implementation of a packet sniffer which may be of some use to game developers using Vu and trying to debug network connectivity or communication problems.
The test directory contains enough code to implement the vutest.exe executable. This test program is a simple exerciser of basic non-networked Vu functionality including the vuDatabase, collections, entities, and database save and restore. It also tests ordered lists and the red-black tree. The latter is printed out in a nice format which indicates the depth of each node to verify that node balancing is done correctly.
The nettest directory contains the zoomers application. This tests most of the Vu functionality, including networking, the grid tree, collision detection, automatic updates, entity transfer and load balancing. Note that it is not possible to run more than one zoomers session concurrently on the same machine as they both attempt to grab the same socket.
The netwatch directory contains a simple passive network sniffer application. It uses the same network initialization code as nettest, so it is not possible to run netwatch concurrently with nettest on the same system. However, netwatch does not register with the network, so Vu and netwatch apps will not see each other. Netwatch can even be configured to scan packets from multiple games running on the same network. For a quick primer on the various configuration settings allowable for netwatch, execute the program with the ‘-?’ option.