Chapter 4.2. Registered Variables

Peter Dalton, Smart Bomb Interactive

Inter-system communication is a critical consideration in a game engine, often dictating the broad architecture of the code base. In practice, sacrifices are often made to allow one system to know about the internal workings of another in order to accommodate better communication. Although sometimes this can be appropriate, it often results in a loss of modularity. These compromised systems lose their “black box” characteristics and become harder to maintain and replace. This gem will present a solution to this problem by demonstrating a technique for linking up shared variables across disparate systems. This allows systems to define a set of inputs or control variables that can be seamlessly linked to variables in other systems.

It is important to recognize that this is not a messaging system, nor is it meant to replace one. Rather, it is a system that allows a programmer to control communication across various systems without requiring blind casts, global variables, or a flat class hierarchy. This technique allows for basic variable types, such as integers and floats, as well as complex data types, such as arrays, classes, and other user-defined types. This technique has been successfully utilized to facilitate the necessary communication required to control animation systems, user interface parameters, display shader parameters, and various other systems where variable manipulation is required.

Getting Started

The basic idea is to create a wrapper for a variable and then allow these wrapped variables to be linked together. The code that is dependent upon the variables can be implemented without any special considerations. Rather than just accessing the value that has been wrapped, the registered variable will walk the chain of linked variables and provide access to the appropriate variable. When building registered variables, there are several key goals to keep in mind.

  • Keep the registered variable seamless. To make a registered variable truly useful, it needs to be easy to work with. The goal is to make it transparent to the programmer whether they are using a regular integer or our newly created integer registered variable. Operator overloading will be the key here.

  • Allow one registered variable to be linked to another. We are going to allow registered variables to set redirectors, or in other words, allow registered variables to be chained together.

  • Tracking of a “dirty state.”To enhance the usefulness of a registered variable, we will include a dirty state in the variable. This provides users with knowledge of when the variable has actually changed, which is useful for run-time optimizations.

  • Custom run-time type information. This will become necessary when we start registering variables together. It allows us to confidently cast to a specific type without the need for blind casts.

  • Provide a way to link registered variables directly. We will provide an initial, explicit method for linking variables together. This method is important when dealing with specific situations where the control variables are well defined.

  • Provide a way to link registered variables indirectly. As our systems grow in complexity, we want to allow for variables to be generically linked together without either system knowing the internal details of the other. This indirect method will become the key to dealing with complex situations where all control variables are not well defined or are ambiguous.

Assumptions

The code we will present is taken from a commercial Xbox 360 engine. It utilizes several routines and data structures provided by the base engine that are beyond the scope of this gem. These dependencies are minimal; however, we need to explicitly mention them in order to avoid confusion. A basic implementation of these data structures has been included on the accompanying CD-ROM.

TArrays

The TArray class is a templated array holder. It is used to hold links to other registered variables.

FNames

This class implements a string token system. It is a holder for all of the strings that exist within the game engine. Each string is assigned a unique identifier by which it can then be referenced and compared against other FNames with a constant cost of O(1). This functionality is the key to implementing the required run-time type information and the means by which registered variables are given unique names for linking.

The Base Class: RegisteredVar

The base class from which all registered variables will be derived is the RegisteredVar class. This class provides all of the support for linking registered variables together and tracking the dirty state. Here only key portions of the RegisteredVar class are shown; a complete implementation can be found on the accompanying CD-ROM.

class RegisteredVar
{
public:
    // Provides IsA<>() and GetClassType() routines, described later.
    DECLARE_BASEREGISTERED_VARIABLE( RegisteredVar );

    RegisteredVar() : m_bDirty(false), m_pRedirector(null) {}
    virtual ~RegisteredVar()
    {
        if (m_pRedirector)
            m_pRedirector->m_References.RemoveItem( this );
        while (m_References.Num())
            m_References[0]->SetRedirector( null, false );
    }
    void SetRedirector( RegisteredVar* InRedir )
    {
        if (InRedir!=this && (!InRedir || (InRedir->IsA( GetClassType() )
            && !InRedir->IsRegistered( this ))))
        {
            if (m_pRedirector)
                m_pRedirector->m_References.RemoveItem( this );
            m_pRedirector = InRedir;
            if (m_pRedirector)
                m_pRedirector->m_References.AddItem( this );
        }
    }
    void SetDirty( bool InDirty, bool InRecurse=false );
    bool IsDirty() const;
    void SetFName( FName InName ) { m_Name = InName; }
    FName GetFName() const { return m_Name; }
protected:
    template<class T> T* GetBaseVariable() const
    {
        return m_pRedirector ?
            m_pRedirector->GetBaseVariable<T>() : (T*)this;
    }
    FName m_Name;
    bool m_bDirty;
    RegisteredVar* m_pRedirector;
    TArray<RegisteredVar*> m_References;
};

There are two key elements to getting this class correct. The first is preventing dangling pointers in the destructor. The important consideration here is that since we are going to be linking up registered variables blindly between systems, we do not want to end up pointing to a registered variable that has been deleted. This scenario would result in dangling pointers to invalid memory addresses and severe headaches. To prevent this, we create a link back to the referencing registered variable so that we can clean it up when the referenced registered variable is deleted. This would normally create a doubly linked list; however, in our case it is common for multiple registered variables to redirect to a single registered variable, thus creating the need for an array of pointers as illustrated in Figure 4.2.1

This diagram illustrates how registered variables will be linked together and how we will also be tracking referencing registered variables to avoid dangling pointers.

Figure 4.2.1. This diagram illustrates how registered variables will be linked together and how we will also be tracking referencing registered variables to avoid dangling pointers.

The second key to keep in mind is that anytime you access a registered variable, you need to ask yourself an important question: Should I be working with “this” copy of the registered variable or should I forward the request to the redirected registered variable? If you decide that the correct answer is to work on the redirected registered variable, the GetBaseVariable() routine will retrieve the base registered variable that should be used.

Single Variable Values versus Array Variable Values

The next step is to divide all the variables into two distinct classifications: single value types and arrays. The first classification, single value types, encompasses integers, floats, user-defined types, and so on and will be the focus of the examples provided. The second, array types, will encompass arrays of integers, floats, user-defined types, and so on. The implementation of array types is very similar to single values types with just a few minor alterations. The implementation of array types has been provided on the accompanying CD-ROM. Having made this distinction, we will now create a templated base class that provides 99 percent of the functionality required by any variable type.

template<class T, class RegVar>
class RegisteredVarType : public RegisteredVar
{
public:
    RegisteredVarType();

    T Get() { return GetBaseVariable<RegVar>()->m_Value; }
    const T& Get() const { return GetBaseVariable<RegVar>()->m_Value; }
    void Set( const T& InV )
    { GetBaseVariable<RegVar>()->SetDirectly( InV ); }
    operator T() const { return GetBaseVariable<RegVar>()->m_Value; }
    operator T&() { return GetBaseVariable<RegVar>()->m_Value; }

    void operator=( const RegVar& InV ) { Set( InV.Get() ); }

    // Implement comparison operators >,<,>=,<=,==,!=, see CD-ROM.
    bool operator>( const T& InV ) { return Get() > InV; }

    // Implement mathematic operators /,*,+,-, see CD-ROM.
    T operator/( const T& InV ) const { return Get() / InV; }

    // Implement assignment operators /=,*=,+=,-=, see CD-ROM.
    RegVar& operator/=( const T& InV )
    { Set( Get() / InV ); return *(RegVar*)this; }

protected:
    void SetDirectly( const T& InValue )
    {
        if (m_Value != InValue)
        {
            m_Value = InValue;
            SetDirty( true );
        }
        for (int ii = 0; ii < m_Parents.Num(); ++ii)
            ((RegVar*)m_References[ii])->SetDirectly( InValue );
    }
    T m_Value;
};

Examining the code should illustrate the emphasis placed on providing the appropriate overloaded operators to allow the programmer to seamlessly use registered variables. The programmer should not have to change the code whether they are using a standard variable or a registered variable. This ensures that the registered variable is used correctly and seamlessly. It also makes it easy to add and remove registered variables from a system since only the variable definition and linking code needs to be updated.

Another important consideration is the SetDirectly() routine used by the Set() method. The SetDirectly() routine first determines whether the value is actually different than the current value and sets the dirty flag if appropriate. This dirty flag allows the owner of the variable to effectively track when the state of the variable has truly changed, thus allowing for run-time optimizations.

A common optimization, when dealing with shader parameter blocks within DirectX, is to prevent the blocks from being invalidated and rebuilt unless absolutely necessary. Thus, if you have a variable controlling the state of a shader, you will want to make sure that the variable has actually changed before processing it. You should also notice that there is no automatic means by which the dirty flag is cleared. To clear the flag, the owner of the variable will need to explicitly call the SetDirty( false ) routine when the owner is done dealing with the change. Since the dirty flag is stored in each variable, the owner of the variable can deal with the flag in its own way. In the case of a variable controlling a shader parameter, we would not want to handle the variable change and rebuild the state block until the material is required by the renderer. However, another variable might also be linked to this state and want to handle the change immediately. It is also safe for the owner to choose to ignore the dirty flag if it isn’t required.

The SetDirectly() routine also has the task of copying the value to the entire chain of linked registered variables. This feature is important to retain the most recent value in the event that a registered variable clears its redirector either explicitly or if the redirector is deleted. If the value was not copied, we would see a pop from the old value to whatever value is currently stored. While this might not be a critical issue, it can cause undesired behavior, as the variable might appear un-initialized. Copying the value is also useful when debugging, allowing for the value to be easily shown in the watch window without digging through a list of linked variables.

Type-Specific Registered Variable

At this point we have built all the base classes required, and creating registered variables is now straightforward.

class RegisteredVarBOOL
    : public RegisteredVarType<bool, RegisteredVarBOOL>
{
DECLARE_REGISTERED_VARIABLE( RegisteredVarBOOL, RegisteredVarType );
    RegisteredVarBOOL& operator=( const bool& InValue )
    { Set( InValue ); return *this; }
};

class RegisteredVarFLOAT
    : public RegisteredVarType<float, RegisteredVarFLOAT>
{
DECLARE_REGISTERED_VARIABLE(RegisteredVarFLOAT, RegisteredVarType );
    RegisteredVarFLOAT & operator=( const float& InValue )
    { Set( InValue ); return *this; }
};

The listings above add support for both the standard Boolean and float types. Implementing additional types is as simple as duplicating the provided code and updating the names and types appropriately. Note that the operator=() was not specified within the templated base class RegisteredVarType in order to resolve conflicts when using the Visual Studio 2008 C++ compiler.

Setting a Registered Variable Directly

We’ll now look at a simple example to illustrate what registered variables can do. We have a weapon class attached to a vehicle class, and the vehicle needs to tell the weapon when to fire. If we create a Boolean registered variable within the vehicle and link it to the weapon, we can then just manipulate the variable within the vehicle and control the state of the weapon. Also, if we have multiple components that need to know about the weapon firing, such as AI logic, user interfaces, or game code, we now only have one variable that needs to be updated to keep everyone in sync. In contrast, without using registered variables we would need to create a Fire() function within the weapon and call it to start and stop firing. We would also need to manually notify all other systems that the weapon is firing. The registered variable approach has the advantage that once the variables are correctly registered, it is much easier to control communication.

class Weapon
{
    void SetFireRegVar( RegisteredVar* InVar )
    {
        m_Fire.SetRedirector( InVar );
    }
    void HeartBeat( float InDeltaTime )
    {
        if (m_Fire) FireWeapon();
    }
    RegisteredVarBOOL m_Fire;
};
class Vehicle
{
    void Initialize()
    {
        m_MyWeapon.SetFireRegVar( &m_FireWeapon );
    }
    void HeartBeat( float InDeltaTime )
    {
        m_FireWeapon = DoWeWantToFire();
    }
    bool DoWeWantToFire();
    RegisteredVarBOOL m_FireWeapon;
    Weapon m_MyWeapon;
};

IsA Functionality

The DECLARE_REGISTERED_VARIABLE macro requires further explanation to assist in understanding the implementation. The purpose of this macro is to provide type information for the registered variable. It ensures that we do not link two registered variables together that are not of the same basic type. It also allows us to determine the type of register variable that we have given only a pointer to the base class RegisteredVar.

#define DECLARE_REGISTERED_VARIABLE( InClass, InBaseClass )               
  protected:                                                              
       typedef InClass ThisClass;                                         
       typedef InBaseClass Super;                                         
  public:                                                                 
       virtual FName GetSuperClassType( FName InComponentType ) const     
       {                                                                  
           FName SuperType = NAME_None;                                   
           if (InClass::StaticGetClassType() == InComponentType)          
               SuperType = Super::StaticGetClassType();                   
           else if (InComponentType ! = NAME_None)                        
               SuperType = Super::GetSuperClassType ( InComponentType );  
           return SuperType == InComponentType ? NAME_None : SuperType;   
       }                                                                  
       virtual FName GetClassType() const                                 
       {                                                                  
           return InClass::StaticGetClassType();                          
       }                                                                  
       static FName StaticGetClassType()                                  
       {                                                                  
           static FName TypeName = FName( STRING( InClass));             
           return TypeName;                                               
       }
#define DECLARE_BASEREGISTERED_VARIABLE( InClass )                        
   DECLARE_REGISTERED_VARIABLE( InClass, InClass )                        
   template<class T> bool IsA() const                                     
       {                                                                  
           return IsA( T::StaticGetClassType() );                         
       }                                                                  
       bool IsA( const FName& InTypeName ) const                          
       {                                                                  
           for (FName Type = GetClassType(); Type != NAME_None;           
               Type = GetSuperClassType( Type ))                          
           {                                                              
               if (Type == InTypeName)                                    
                   return true;                                           
           }                                                              
           return false;                                                  
       }

While this code is being utilized here to provide IsA functionality for registered variables, it is generic in nature and can be used to provide RTTI functionality to any class or structure. The code is written in the form of a macro to prevent the code from being duplicated due to it being required at every level of the inheritance chain. An important consideration is to recognize that this implementation does not support multiple inheritance but could be extended to do so.

Setting a Registered Variable Indirectly

Now that we have basic RTTI information, we can safely link registered variables together without knowing the internals of other systems. Let’s examine another example.

Suppose we have a material used for rendering that has a parameter we can adjust to change its damage state. The damage state is defined within the material and used to determine how the material is rendered. In this example we would like to create a generic system in which a high-level object can register a variable with another object and have it correctly link to a control variable. We want a vehicle class to provide a variable to control the damage state of the material, and then the vehicle can drive the material’s control variable by simply modifying its own variable. In this example, adding a function or parameter to the Material class would not be desirable because it would lead to bloat and would not be applicable to all materials.

class RegisterVariableHolder
{
    virtual void RegisterVariable( RegisterVar& InVar ) {}
};
class BaseClass : public RegisteredVariableHolder
{
    virtual void RegisterVariables(RegisterVariableHolder& InHolder) {}
};
class Material : public BaseClass
{};
class DamageStateMaterial : public Material
{
    void Initialize()
    {
        m_DamageState.SetFName( "DamageState" );
    }
    virtual void RegisterVariable( RegisteredVar& InVar )
    {
        if (InVar.IsA<RegisteredVarFLOAT>() &&
            InVar.GetFName() == m_Trans.GetFName())
        {
            m_DamageState.SetRedirector( &InVar );
        }
    }
    RegisteredVarFLOAT m_DamageState;
};
class Vehicle : public BaseClass
{
    void Initialize()
    {
        m_VehicleDamageState.SetFName( "DamageState" );
        m_Fire.SetFName( "Fire" );
        RegisterVariables( *m_pMaterial );
    }
    virtual void RegisterVariables( RegisterVariableHolder& InHolder )
    {
        InHolder.RegisterVariable( m_VehicleDamageState );
        InHolder.RegisterVariable( m_Fire );
    }
    RegisteredVarFLOAT m_VehicleDamageState;
    RegisteredVarBOOL m_Fire;
    Material* m_pMaterial;
};

Now, whenever the vehicle changes m_VehicleDamageState, the material class’s m_DamageState variable will be automatically updated without the material being required to provide accessor routines or the vehicle knowing the type of material it has been assigned. The vehicle can also ignore the material since the only thing it needs to do is update its own registered variable. While the example is fairly simple, the principle can be applied to solve many more problems.

Conclusion

Within our game engine we have found registered variables to be an essential part of inter-system communication because they abstract the communication layer and minimize system dependencies. Registered variables are utilized to control the state of animation flow systems, expose data to user interfaces, such as hit points and ammo counts, and control material parameters, such as damage states and special rendering stages. We provide tools within the game’s editor to allow artists and level designers to specify exactly which registered variables should be linked together within the game. Systems have been designed to allow users to dynamically create new registered variables within the game editor and link them to any other appropriate registered variable. For us, this has opened the door for content builders to access any set of data within the game engine and gives them the necessary controls to manipulate gameplay.

We hope that you will have fun experimenting with the concept of registered variables and that you will find them useful in improving your code. You will find an implementation of the techniques presented on the CD-ROM.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset