- Description of the Engine Architecture in Momentum
- Detailed description of the Reflection System used in Momentum (lots of code samples).
Engine Architecture
- Game loop starts
- Update all systems
- Update all spaces
- Trigger “Update” event on all spaces
- All components hooked to the Update event call their “Update” functions
- Trigger “Update” event on all spaces
- Draw all spaces
- Trigger “Draw” event on all spaces
- All components hooked to the Draw event call their “Draw” functions
- Trigger “Draw” event on all spaces
- Check all spaces for objects to delete, delete as necessary
Reflection System
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);
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 } } }
- 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.