Systems in “Momentum”

 

In This Post
  • Description of the Engine Architecture in Momentum
  • Detailed description of the Reflection System used in Momentum (lots of code samples).
Onto the article:
The framework of the engine created for Momentum was made in-house, using various online resources and books to research a framework that would best fit what we needed to do; the first part of this article will cover the architecture of the engine, and the 2nd part will cover the various systems and technologies added to the engine as they were needed.
 

Engine Architecture

Our engine utilizes a component-based architecture that also concurrently uses an event-based system to update all of our objects (which are essentially just containers of components).
 
The core of the engine manages the systems in use by the engine, and the current state (or level) the game is in.  All systems are added to the engine before starting the main game loop. Systems update through the engine, and manipulate objects in the game through events (or in the case of Audio and Graphics, they serve as a framework through which their respective components function properly).
 
Game states, spaces, and objects are all derived from an Entity class (see Fig. 1). This class contains a map of events, which any component contained in an object or a space may be “listening to”; upon initialization, a component would “hook” itself to a certain event (adding itself to that event’s map of components). When that event (see Fig. 2) is “triggered” , either as an Update event, or a miscellaneous event through logic, all components that may be listening to that event will be triggered as well, calling their appropriate functions with the message the original event was triggered with. Upon destruction, components auto-unhook themselves from these events.
 
Here’s a step by step through the process:
  1. Game loop starts
    1. Update all systems
    2. Update all spaces
      1. Trigger “Update” event on all spaces
        1. All components hooked to the Update event call their “Update” functions
    3. Draw all spaces
      1. Trigger “Draw” event on all spaces
        1. All components hooked to the Draw event call their “Draw” functions
    4. Check all spaces for objects to delete, delete as necessary
 
So, in this way, all objects just hold the components they are made up of, and components can be swapped in and out of any object as deemed necessary. The pictures above (and the framework) were first created using Visual Studio’s UML Model builder, to help provide visual examples.
 
In addition to this, in order to make our engine data-driven (and to allow us to use a level editor), we created a factory for our engine that uses the JSON file format to serialize and initialize the components and objects at run-time. Later on, we also implemented a level editor using the WxWidgets library. Both of these systems were implemented by other members of my team, and to do them correctly I added a lot of additional functionalities to the engine.
 

Reflection System

While planning out our level editor, we came across the problem of needing to know how to break down our various data types at runtime; to do this, I implemented a Reflection system in our engine, a system that allows us to know what members any component contains, and how to interact with them, at run-time, and provide a general interface through which all components can easily be added. (This is not a step-by-step of how to do reflection: I’m still experimenting and learning things about how to do it better, or efficiently use the system, and it would take many, many posts to accurately cover it all.)
 

To start, you need to have a general way to represent any data of a registered type, base (int, float, etc.) or otherwise (any classes or structs.) This would be your Variant struct, and it should have templated constructors that either store the data inside of its own buffer by copying the memory directly, or, if the size is larger than a pointer, storing the address of the data. This way, you can use the Variant struct to represent any data type.

#include "typeinfo.h"

namespace Reflection
{
  class Variant
  {
  public:
    static const int MAX_SIZE = sizeof(double);
    
    Variant();

    Variant(Variant const&);
    Variant& operator =(Variant const&);

    template<typename T>
    Variant(T const&);
    
    template<typename T>
    Variant(T*);

    template<typename T>
    Variant& operator =(T const&);

    template<typename T>
    Variant& operator =(T*);


    template<typename T>
    T& As();

    void* AddressOf() const;
    void CopyObject(void const* rhs);
    TypeInfo * GetType()const { return type; }

    template<typename T>
    static Variant MakeVariant(T* obj);
  private:
    union
    {
      void* ptr;
      char data[MAX_SIZE];
    };
    TypeInfo * type;
    friend class Greater;
    friend class Lesser;
  };

}

The next thing you need is a way to store the data type. You can store any data using Variants, but unless you have a way of getting the correct type data from the Variant, it’ll be useless. Let’s call this “type information” TypeInfo. Now, in order to be able to break down data types at runtime without having any information about the data type, you can’t just have TypeInfo store the type, and then call it a day. You need to break down the type into its constituent parts; breaking all of its members down into base data types is the only way to ensure that the type can be generally accessed from anywhere.  To do this, you need to add another struct that can go inside TypeInfo and hold the information of all the members a type may have. This is where Properties come in. A Property is just a member of a type that you want to be able to access. Properties will also have getters and setters for the data type they are given. (This is the nit and grit part of how Reflection works; if I ever get to detailing it, I will link the post to this text, which will be blue if I’ve done so. If not, stay tuned!).  

Here are the interfaces for those two classes:

namespace Reflection
{
  class Property;

  class TypeInfo
  {
  public:
    std::string name;

    size_t size;
    TypeInfo* parent;

    Property* getProperty(std::string const &);
    Property& addProperty(std::string const& name, Property*);

    typedef std::unordered_map<std::string, Property*> Properties;

    Properties& getPropertys() { return props; }

    bool isType(TypeInfo * type) const;
  private:
    Properties props;
  };
}

namespace Reflection
{
  class Variant;
  class TypeInfo;

  class Property
  {
  public:
    typedef void(*PropertyFn)(Variant& inst, Variant& in_out);

    Property(TypeInfo*, PropertyFn get, PropertyFn set);

    Property& SetSerialized(bool serialized);

    PropertyFn getter;
    PropertyFn setter;

    TypeInfo* info_;

    bool serialized_;
  };
}

The TypeInfo of a type will contain the information about all the Properties that type contains, and the Properties will contain the TypeInfo of their members, and so on, until you have totally broken down the class to its base data types; this is where you can actually interact with the data. Once you get down to a Property that is a base data type, you will be able to get the data (in Variant form), without needing to know the data type you got it from.

I’m sure that last paragraph was very dense, so here’s some code examples to help (The definitions for the class used are further down the page, if you want clarification).

int main(void)
{

  // this initializes all types logged in the reflection system
  Reflection::StaticInit::All();

  // this is an example class with example members
  TestClass temp;

  temp.testgetset = 10;
  temp.integer = 1;
  temp.thing.testString = "Hello";

  // variant has a constructor for any registered type
  // basically is just a reference, also has class typeinfo
  Reflection::Variant var(temp);

  // can get the typeinfo from the variant constructed from the registered class
  // these two lines are the same
  //Reflection::TypeInfo * TestClassType = Reflection::getTypeInfo<TestClass>();
  Reflection::TypeInfo * TestClassType = var.GetType();

  // this line is getting the property from this class, that has getters/setters
  Reflection::Property * testgetset = TestClassType->getProperty("testgetset");
  Reflection::Variant testgetsetout;

  // this gets the value of the property "testgetset" from the Variant var
  // and puts it in testgetsetout
  testgetset->getter(var, testgetsetout);

  // gotta interpret it separately as a float though, cause it's just the variant of the value
  // breakpoint here to see that it is the original 10.0f
  float value = testgetsetout.As<float>();


  value = 20;

  // using the setter you can then set that value, and you can breakpoint here
  // and see that the original value has indeed been changed to 20
  Reflection::Variant testgetsetin = value;
  testgetset->setter(var, testgetsetin);

Continuing, you need to create a number of new structs that allow you to provide ways to easily add components to this system (ref_DefineType and ref_DeclareType macros, that add all the necessary members and functions), and register POD struct types and break down the various members within them into base member types (External types, as compared to normal types, using the same macros). These would be the methods through which any and all classes or structs could be easily added to the reflection system. I could’ve done this without macros, but it would’ve been far more painful.
 
To tie the previous paragraph back to the TypeInfo paragraph, registering your data types and  their members through these macros will be registering their TypeInfos and their TypeInfo’s Properties.

Examples:

class TestStruct
{
public:
  std::string testString;
};

namespace Reflection
{
  // if there's an external type that is just a data struct, it's declared differently
  ref_DeclareExternalType(TestStruct);
  ref_DefineExternalType(TestStruct)
  {
    ref_AddMember(testString);
  }
}
class TestClass
{
public:
  // this macro goes in the class, it adds all the members required
  ref_DeclareType(TestClass);
  int integer;
  float testgetset;

  void SetFloat(float f)
  {
    testgetset = f;
  }
  float GetFloat(void) const
  {
    return testgetset;
  }
  TestStruct thing;
};

// every class has to do this
// for privates with get/setters, add them as properties
ref_DefineType(TestClass)
{
  ref_AddMember(integer);
  ref_AddMember(thing);
  ref_AddProperty(float, testgetset, GetFloat, SetFloat);
}

Finally, before I give you a few more examples, the last three things you need tie everything in the Reflection system together. You need to both have a map of all registered TypeInfos, and provide any registered class with a virtual GetTypeInfo function (returns the correct TypeInfo). The former is so you can provide a way to break down every possible data type registered in the reflection system while only needing a pointer to the class; the latter is so you can break down every type while only needing a pointer to the base class. This way, if a program needs to access data at runtime, and the only data types visible to it are base classes (any of which may be derived types), you can break them all down correctly. 

The last thing you need is a way to ensure that all of the TypeInfos for the various base classes are instantiated before you need to access them. (This can be established through the “ref_Register” macros, by giving all of the registered classes a function that adds their TypeInfo to the TypeInfoMap previously mentioned, and storing pointers to all of these functions in an array, and then calling them through a “ReflectionInit” function). You can see this function being called all the way at the start of these examples, before any TypeInfos are referenced.

Here are a few more examples of how to use the system:

 

  // this is the property that is the struct that is inside the testclass
  Reflection::Property * thing = TestClassType->getProperty("thing");
  
  using Reflection::Variant;

  // this gets the original "thing" value from the testclass, putting it in thingout
  Variant thingout;
  thing->getter(var, thingout);

  // this is the property inside the teststruct that is the teststring
  Reflection::Property * testString = thingout.GetType()->getProperty("testString");

  // this gets the value of the testString property from thingout
  // and puts it in testStringout
  Variant testStringout;
  testString->getter(thingout, testStringout);

  // this interprets the value in the testString variant as a string
  // breakpoint here to see that it is the original "Hello"
  std::string val = testStringout.As<std::string>();

  // this is setting the value of the property "testString" that is in thingout
  // to be the variant testStringin, which is set to "Not Hello"
  // breakpoint here to see that the original value has indeed been changed to "Not Hello"
  val = "Not Hello";
  Variant testStringin = val;
  testString->setter(thingout, testStringin);

  // this whole big loop just goes through the map of all types
  // gets their names and typeinfo, and their type info contains
  // all the properties that are inside THOSE types, 
  // and then you can go through THOSE properties,
  // and get the info of the properties inside their typeinfos
  // and so on and so forth until you have all the info
  // on all the data data that goes within
  // every single type registered with the reflection system
  auto& registeredTypes = Reflection::TypeInfoMap::types;
  for (auto& pair : registeredTypes)
  {
    std::string Name = pair.first;
    Reflection::TypeInfo * info = pair.second;
    auto & props = info->getPropertys();
    for (auto& prop_pair : props)
    {
      std::string prop_name = prop_pair.first;
      Reflection::Property * prop = prop_pair.second;
      Reflection::TypeInfo * prop_info = prop->info_;
      (void)prop_info;
      // queue recursion
      //  auto & props = prop_info->getPropertys();
      //  for (auto& prop_pair : props)
      //  {
      //  etc etc
    }
  }
}
          When a new component is added to the engine, it should be declared to the reflection system as a type in its header file. In the .cpp file, its public members should be listed (if a property is private, but should still be manipulated, you can add them as “properties” and give their setters and getters). This allows us to break down every component to their constituent parts.
          If a component holds another container, say, a Vector2D, that isn’t a core data type (float, int, bool, etc.), you can register that container as an “external type”, and you can go through the same method for breaking down components to break down external types. With this, any and all containers can be broken down to their core data types, giving us the ability to see and manipulate every part of any data type that is detailed through our reflection interface.
In addition to the Reflection system, I have also written
  • a recording and playback system, so the game records the player’s movement during gameplay, and can play back that movement through a ghost the next time the player plays that level
  • a robust menu system that auto-orders the entries within, so new levels can be easily added through text and the menu will adjust to any specific ordering and additional entries
  • an automatic resource loader, that loads all audio files and images within the appropriate folder into memory when the game starts
  • and a lot of gameplay code.

In the future, I plan to do a lot more to make the engine more robust, and continue to add more to help make the gameplay experience feel as awesome and as fast as it can be.