• entries
21
28
• views
34011

# A simple and fast dynamic type system

1308 views

Last article:

https://www.gamedev.net/blog/1930/entry-2260651-ecs-iii-queries-and-horrible-component-serialization/

The type system:

As I've been briefly talking about last time, the need for handling things like serializing components automatically brought up the need for a custom dynamic type system. What are the requirements of this type system?

- Must allow to write generic code for handling any type (i.e. a GUI value box that takes any type and allow modification of it)
- Should be as fast in comparion to pure type access than possible (so no virtual, no RTTI)
- Type info must be serializable (safe across compilers, so again no RTTI)
- Inheritance information is not required (the intended use doesn't account for base/derived classes)
- No class should have to be modified to support the type-system (so no deriving of base classes, etc..)

So C++ RTTI is out of the question anyways. The way I ended up with is by having a type-id with a static and a dynamic type. Whats the difference? This is the static type:enum class VariableType{ UNKNOWN = -1, BOOL, FLOAT, INT, STRING, OBJECT, ENUM, STRUCT};// query the VariableType of any c++-typegetVariableType(); // BOOLgetVariableType(); // STRINGgetVariableType(); // OBJECTgetVariableType(); // ENUM
Those are the "primitive" types known to the type system. As you can see, template magic will play a huge part in the type system. As to why I have exactly those values:

- The first four are all POD. The sepeartion between the POD-types is there because it makes it easier to deal with bool, float, int and string, which are all just stored by value in the type-system. As you'll see, the type-system realies heavily on a mechanism that calls a templated functor, which will receive the target c++-type, based on this enum. So having all POD-values seperate makes it very easy to write a generic routine, which you'll see later.

There is also a second component to the Type-ID, called "ImToLazyToThinkOfAGoodName", or just "ObjectType" (because thats what is was originally used for). Its just an unsigned int, and the use is explained below.

- Pretty much all non-POD classes are dealt with as "OBJECT". Objects cannot be created by the type system, they are instead referenced by a key from a pool. Objects support optional weak-reference counting (SFINEA ftw). Objects have to be registered with their C++-type and a name. They then get a dynamic ID (CRTP strikes again):ObjectRegistry::RegisterObject("Texture", textureAccessor);ObjectRegistry::RegisterObject("Cbuffer", nullptr);isObject(); // trueisObject(); // falsegetObjectType(); // = 0getObjectType(); // = 1getObjectType(); // error: is not an object
Internally there is an "Object" wrapper-class, which uses said CRPT I already employed in my Entity/Component system to generate a linear type-id. Not that the RegisterObject<>() function takes an "accessor" as second parameter, which handles the access of an object by its key. nullptr is passed for objects that have no pool or are not accessible by a key, like the runtime-state object of a script.

- Then there is ENUM. This is pretty straight-forward, its an enum (duh...). It also uses the type-id from before, just that registration and access differs a little bit:core::EnumRegistry::RegisterEnum(L"BlendMode");core::EnumRegistry::RegisterEnumValue(BlendMode::NORMAL, L"Normal");core::EnumRegistry::RegisterEnumValue(BlendMode::ALPHA, L"Alpha");core::EnumRegistry::RegisterEnumValue(BlendMode::ADD, L"Add");core::EnumRegistry::RegisterEnumValue(BlendMode::SUB, L"Sub");isEnum(); // trueisEnum(); // falsegetEnumType(); // 0getEnumType(); // compile error: Is not an enum
Enums use a similar wrapper class for the ID (and for storing the enum itself).

- Struct is a new type of thing I introduces a few weeks ago. One thing that was missing previously was aggreggation of types. For example, if I have a Vector3, previously all components would have to be used seperately (which sucks). Objects are out of the question because those are allocated on the stack and have a few other requirements that does not apply to aggregate types like vectors. The solution was to introduce a seperate concept:core::StructRegistry::RegisterStruct>("Vector2f");core::StructRegistry::RegisterStruct>("Vector2i");core::isEnum>(); // truecore::isEnum>(); // falsecore::getStructId>(); // 0core::getStructId>(); // 1core::getStructId(); // compile error: Is no struct
Very similar concept to the other two. But here is the tricky part: How do we differentiate between a struct and an object, on compile time, mind you! The (temporary) solution was to require a struct to have a very specific static method, which also describes the structs known members:const core::StructDeclaration& Vector2f::GetStructDeclaration(void){ core::StructDeclaration::EntryVector vEntries = { { L"X", core::generateTypeId(), offsetof(Vector2f, x) }, { L"Y", core::generateTypeId(), offsetof(Vector2f, y) } }; static const core::StructDeclaration declaration(vEntries); return declaration;}
I don't like it very much, because it requires modification of the class. However, I really don't see a good alternative here, and one could just write a wrapper-class for the type system, having this method and the real class composited. As a final note, I'd just like to put out there that structs are fully recursive - a struct can have any other type, as well as another struct. It just cannot have one of itself, for well known reasons

Usage:

So that is all the types that are currently used, maybe there will be more in the future. I've already put in the source-code how you can query the type of an object, enum, struct, etc... but is there also a way to use the type-system without having to explicitely know/tell what a type is? Sure:class ACCLIMATE_API TypeId // this is returned by the function below{public: TypeId(VariableType type, unsigned int id); VariableType GetType(void) const; unsigned int GetId(void) const; bool operator==(const TypeId& id) const; bool operator!=(const TypeId& id) const;};core::generateTypeId(); // FLOAT, -1core::generateTypeId(); // OBJECT, 0core::generateTypeId(); // ENUM, 0core::generateTypeId>(); // STRUCT, 1
Now we have both the static and dynamic type for any object. As I've writting "fast" type system in the headline, there you. Its just about comparing two integers to know if anything has a certain type. Most of those templates do not resolve at compile time, but even those only access a static integer variable for the dynamic type-id (and perform an increment on another one the first time it is accessed). I didn't make any benchmarks yet, but I belive this should beat RTTI any day.

For how we implement code that actually uses this, I'll tell next time. The article is long enough, so at the end, I'll just show you how to use this information in the "end-user" code:

Entities, Components, dynamic types, oh my!

To get back to the original problem, we want to be able to serialize components attributes without the user having to write XML (or any such code for that matter). Having the type-system available, this is very easy to do. We just make a "GetDeclaration"-method, similar to what a struct requires:const ecs::ComponentDeclaration& Collision::GenerateDeclaration(void){ const ecs::ComponentDeclaration::AttributeVector vAttributes = { { L"Size", math::Vector2(32, 32), offsetof(Collision, vSize) }, }; static const ecs::ComponentDeclaration declaration(L"Collision", vAttributes); return declaration;}
There is a lot of complex stuff going on in the background, which I'll partially enlighten you on later. But having this declaration available, one can now serialize the component in a somewhat safe manner (the offsetof is not very nice, I'm working on a better solution). Also note that after the name of the component "Size", there is a "math::Vector2(32, 32)". Normally you would write "core::generateTypeId()", but the former variant allows you to also set a default value for this component, which is then used i.e. in the editor when a new component is created.

Wrapping it up:

Boy, that got way long than I expected. But its a pretty complex system just design-wise, so I think its just fair. So next time, I'll explain more about the implementation of the system, i.e. how one can use this type-ID to execute code that depends on the static type, and possible about how we can use the type-id to store values in a variant-like manner. Thanks for reading!

There are no comments to display.