State Management

Managing state in a game or simulation can be complex. For a simulation, there are several different kinds of state we might need to consider:

  1. State which is created from user input, and will affect the main simulation (m_willShoot = true)
  2. State which represents the core of the simulation (std::vector<Bullets>)
  3. State which is used to visualise the results of the simulation (std::vector<BulletFragments>)

When considering the overall structure of a simulation, there are various points where real-time input can be supplied. This input must be “normalized” in some way, before it can affect the main game state. The goal of this step is to make sure that the main simulation is deterministic when given the same sets of input.

Finally, when rendering a simulation, there may be effects or additional state (such as particle systems) which are inconsequential to the main simulation state.

Example

Here is a very simple example:

typedef double TimeT;
#define abstract = 0
 
// fixed timestamp to ensure determinism 
// - if determinism is not important, this can be changed easily.
const TimeT TIME_STEP = 1.0/120.0;
 
struct Bullet
{
    TimeT startTime;
    // ... velocity, direction, power, etc
};
 
class IWorldSimulation
{
public:
    virtual ~IWorldSimulation (); // implementation skipped
    virtual const std::vector<Bullet> & bullets () const abstract;
};
 
class IWorldEvents
{
public:
    virtual ~IWorldEvents (); // implementation skipped
    virtual bool willShoot () const abstract;
}
 
class IWorldRenderer
{
public:
    virtual ~IWorldEvents (); // implementation skipped
    virtual void render (const IWorldSimulation &) abstract;
};
 
struct WorldEvents : virtual public IWorldEvents
{
    bool m_willShoot;
 
    virtual bool willShoot () const
    {
        return m_willShoot;
    }
};
 
class WorldSimulation : virtual public IWorldSimulation
{
private:
    TimeT m_currentTime;
 
    std::vector<Bullet> m_bullets;
 
public:
    // .. appropriate constructures, destructors, etc left to the imagination
 
    void update (const IWorldEvents & events)
    {
        TimeT oldTime = m_currentTime;
        m_currentTime += TIME_STEP;
 
        if (events.willShoot()) {
            Bullet b;
            b.startTime = m_currentTime;
 
            m_bullets.push_back(b)
        }
 
        foreach(Bullet & bullet, m_bullets) {
            // update bullet velocity / position
        }
    }
 
    const std::vector<Bullet> & bullets () const
    {
        return m_bullets;
    }
};
 
class WorldRenderer
{
protected:
    // Implementation left up to the imagination
    BulletParticleEffect m_particles;
 
public:
    virtual void render (const IWorldSimulation &) const = 0;
    {
        foreach(Bullet & bullet, worldSimulation.bullets()) {
            // render bullet, etc
 
            m_particles.addEffect(bullet);
        }
    }
};
 
class Application
{
    // Assume the constructor sets up everything
    // Allocating all required objects, such as worldSimulation and worldRenderer.
    // Setting up some sort of runloop for firing callbacks at appropriate times
 
private:
    IWorldSimulation * worldSimulation;
 
    // We can drop in any type of renderer - opengl renderer, directx renderer, software renderer, etc.
    IWorldRenderer * worldRenderer;
 
    // called at 120 hz
    void updateSimulation ()
    {
        IWorldEvents * events = processUserInput();
        // or... 
        // IWorldEvents * events = processNetworkInput();
 
        worldState->update(worldEvents);
    }
 
    // called at 60 hz, 30 hz, etc or as required
    void renderSimulation ()
    {
        worldRenderer->render(worldSimulation);
    }
};

Benefits

The benefits of this kind of structure should be apparant, however I'll spell them out:

  • Strict interfaces (IWorldSimulation, IWorldEvents, IWorldRenderer) are very helpful:
    • Helps to maintain encapsulation of unrelated state.
      • This is a problem because it can reduce the reliability and reproducibility of the simulation.
      • Simulation can be repeated deterministically given the same set of input.
    • Allows for different types of renders (i.e. different platforms), state input (i.e. from a network)
      • Not all simulations need visual output or visual output may only be desirable when testing.
      • For a complex physical simulation, it might be run across many nodes, and therefore visual simulation needs to be done differently when the computation is distributed vs when it is run on a local machine.
    • Clear boundaries of functionality, and separation of different pieces of the code
      • Improved clarity of code, because separate functional units can be produced and maintained separately.
      • Better support for SCM, and easier for people to work on individual portions of code/functionality.
      • Easier to test code with unit tests, etc.
  • Separation of updating the simulation and rendering the simulation.
    • Can run simulation and rendering at different speeds, depending on requirements.
    • Often useful for visual updates to be synced with screen refreshes, which might be 60hz, 30hz, PAL/NTSC, etc depending on platform.
    • Can have a clear separation between server and client state when dealing with networked simulations.

State Synchronisation

When dealing with state, synchronisation between multiple simulations may become important. For example, with a client-server model network game, or when distributing the simulation processing.

There are two approaches to state synchronisation within this model, which ultimately are two sides of the same coin.

Event based state management

We maintain state by sending the same events to every simulation. If events are dropped or lost, or not applied in time, the simulation may become incorrect, although this can be corrected by sending a “keyframe” of the simulation state to bring the simulation back on track.

  • An event signals some sort of state change to the world
  • State change may need processing to apply
  • Event based system is generally easier to implement
    • Events can minimize traffic as much data can be evaluated on the client
    • Events from clients can be
  • Events sent to clients can be filtered programatically based on proximity or relevance, thus reducing bandwidth even more.

An example of an event is an encapsulated transmission of the IWorldEvents object. This contains a discrete event that changes the state of the world.

Synchronization based state management

We maintain state by keeping track of individual state values, and noting when they change. These changes are then serialised and set out across the wire. This is more heavy handed than event based state management, however it has the benefits of being simpler to implement - i.e. there are a limited set of messages to transmit, but it is verbose in that a single event may cause many objects to change.

  • Synchronize state between client and server
  • Server maintains world state
    • Set of objects, relationships and state
  • Client needs to know a portion of the world state
    • Subset of objects, relationships and state
  • Client state needs to be updated
    • Change state (primitive type change)
    • Instantiate object
    • Destroy object
    • Update relationships

An example of a synchronised system is the Cocoa bindings system (Mac OS X UI framework). This is done using key paths. Key paths are strings such as myAccount.owner.name. A key path represents an individual piece of state that can change and be updated. When a user interface element is bound to a key path, it (simply) will be informed if the value changes. Conversely, if the user interface element has its value changed, it will update the value at the key path.

Discussion

ArianaDion, 2009/05/14 03:10

thans for the tip

Enter your comment