Entity-Component systems

Started by
55 comments, last by CrazyCdn 6 years, 4 months ago
2 hours ago, Finalspace said:

So to make this more cache friendly, make 3 functions:

- One which just computes all the matrices

- One which sort your models based on visibility

- One which draws the sorted models

Thanks :)

You are absolutely right. It will look cleaner and smaller as well to use some helpers.

 

My largest concern, however, were the many pointer indirections.

 

I guess I will start with creating a reflection API first. Next, I will move my subentities to components and use some more by value storage. And, I will start refactoring all my render passes as well. Maybe, I can reduce the number as well.

🧙

Advertisement

Does someone have some good references to reflection, especially introspection in C++17. The only thing C++17 adds that is helpful in this regard are structured bindings. So, I know that it is not going to work out of the box. I have seen some code depending on macros that need to be used for defining new structures or adapting existing ones, but that just doesn't look right. Alternatively, some APIs depend on accessing Clang's AST, but that is way to compiler specific.

 

Introspection can be handy for generating the UI of each component in an editor, but could you enforce constraints (the value must be in [0, 1]) and guard the correct setting of dirty bits (transformation matrices)?

🧙

It honestly depends how in depth you want to go with reflection, in reality it can be extremely simple, to complex like code generation. Personally, I'd stick with a custom solution instead of using C++17, but that's just me.

This reflection system was started from Insomniac Games and expanded over time :

https://github.com/HeliumProject/Reflect

A little self promotion , this is a small blog about my old reflection system, it's had a major refactor and looks quite literally nothing like this now, but it worked at the time

http://www.ademolathompson.com/archetypesystem/

C++ Reflection is quite a large topic, so there are dozens of papers , blogs, etc about this exact topic if you google it.

A basic C++ implementation could be like this




class CClass
{
	typedef CObject*(*ClassConstructorType);

	CClass(const char* Name, .... ClassConstructorType Ref):
	...

	CObject* ConstructInstance() const 
	{
		return (*m_ClassConstructorRef)();
	}

private:

	// member parameters like size, alignment, etc
	uint32 	ClassSize;
	uint32  ClassAlignment;
	ClassConstructorType m_ClassConstructorRef;
	std::vector<CProperty*>	Properties;
    std::vector<CFunction*> Functions;
	std::string	ClassName;
    CClass* m_SuperClass;
};
      
// base class for all properties (int...float...etc)
      
class CProperty
{
      // functions to construct the value type, destruct the value type, copy, etc
 };
      

 // base class for all "reflected types"
      
 class CObject
 {
    virtual CClass* GetClass() const {};  
      ....
  };
     
   // in the header
class CDerivedClass : public CObject
{
      DECLARE_CLASS(CDerivedClass,CObject)
      
};
 
// in the cpp file
IMPLEMENT_CLASS(CDerivedClass)
      
    
  #define DECLARE_CLASS(TClassType,TSuperClassType)\
     typedef TSuperClassType Super;\
      virtual CClass* GetClass() const override { return GetStaticClass(); }
      static CClass* GetStaticClass();\
      static CObject* _ClassConstructor() { return new TClassType(); }
      
    #define IMPLEMENT_CLASS(TClassType)\
     CClass* TClassType:GetStaticClass()\
     {
       static CClass* ReturnClass = nullptr;
       if(ReturnClass==nullptr)
       {
      	 ReturnClass = new CClass(#TClassType,sizeof(TClassType),AlignOf(TClassType),#_ClassConstructor);
         ... add the properties, etc
        }
      	return ReturnClass;
     }
      

 

1 hour ago, AxeGuywithanAxe said:

This reflection system was started from Insomniac Games and expanded over time :

https://github.com/HeliumProject/Reflect

Seems similar to Boost's Hana (though Hana seems less verbose for the user).

 

This does not require the macros for structs/classes (Reflect, Hana): magic_get

This makes me also realize, I should use some POD handles instead of unique_ptr, shared_ptr and ComPtr.

🧙

The systems like Hana can help provide a few things, but ultimately the compilation model is not amenable for reflection and invocation. 

Under the traditional compilation model that has been around since the 1960s and were incorporated in the original tools, everything is about small and tight.  Functions are inlined, unused elements are completely elided, template entries may never be generated, link-time optimizations may merge and strip functions. The compilers and linkers do everything they can to reduce and remove the information at every level, exactly the opposite of what reflection needs.

Incorporating all that information so other code can use it with reflection requires significant work.  Adding a bunch of interfaces through the standard compilers (usually meaning macro magic or template magic) usually does pretty good but is not perfect. To get it working everywhere requires a fundamental change to the compilation and linking model, like Microsoft did with C++/CLI compilation. They target the CLI which preserves all those elements rather than a tightly-compiled executable that removes them wherever possible.

 

13 minutes ago, frob said:

The systems like Hana can help provide a few things, but ultimately the compilation model is not amenable for reflection and invocation. 

Under the traditional compilation model that has been around since the 1960s and were incorporated in the original tools, everything is about small and tight.  Functions are inlined, unused elements are completely elided, template entries may never be generated, link-time optimizations may merge and strip functions. The compilers and linkers do everything they can to reduce and remove the information at every level, exactly the opposite of what reflection needs.

Incorporating all that information so other code can use it with reflection requires significant work.  Adding a bunch of interfaces through the standard compilers (usually meaning macro magic or template magic) usually does pretty good but is not perfect. To get it working everywhere requires a fundamental change to the compilation and linking model, like Microsoft did with C++/CLI compilation. They target the CLI which preserves all those elements rather than a tightly-compiled executable that removes them wherever possible.

C++ is what it is, we make do with what we have and hope for the best :(

I am currently the most intrigued with magic_get

(Except I need to get rid of my XMVECTOR/XMMATRIX, string, ComPtr, unique_ptr and shared_ptr in these structs/classes. For the scripts, this is a real bummer. The ComPtr and shared_ptr is also a bummer since I used these for my resources. There are also some proposals for constexpr strings, but that is not something for tomorrow.)

 

It seems to be a very long road to C++20 which "would" resolve everything (well at least the introspection part of full reflection).

🧙

15 minutes ago, frob said:

is not amenable for reflection and invocation. 

That is especially a problem for a property editor.

🧙

As you mention ComPtr, COM provides an interface to allow explicitly exposed members. It is normally a small thing to make that happen.  It is not automatic, you must still explicitly expose them, but for a property editor using C++ objects this is usually a good solution.

It is far less comprehensive than reflection and invocation systems. The objects intentionally expose their prescribed interface and it will not vanish due to optimization.

I had an idea for an ECS I want to try out.  Now this is just a quick write up of the general idea...


#define NUMBER_OF_COMPONENTS 64

struct TransformComponent
{
	uint64_t m_ParentID;
	uint64_t m_ID;  // Note: Don't really need this, could be used for reshuffling memory using indirection?
	glm::mat4 m_Transform;
};

struct DataFolder
{
	std::vector<TransformComponent*> m_Transforms;
    std::vector<PositionComponent*> m_Positions;
    //...
};

struct Entity
{
	uint64_t m_ID; 
    // Requires an enum or such to map out values, but this way it can support multiple components if the system allows it...
    std::bitset<NUMBER_OF_COMPONENTS> m_Components;    
    bool m_Active = true;
};
      
class PhysicsSystem
{
	public:
      // This tells this component system where to store it's data, how big its memory pool is and where that memory starts.
      Physics( const DataFolder& data_folder, const std::size_t& memory_size, void *memory_start );
      
      // Returns the pointer to the memory.  Guess it could be a handle made up of an ID and the position in the data, or a reference.
      // Flips TransformComponent bit inside the entity and gets the entities ID to associate with the component.
      // You can pass the position/pointer on to other systems that need to reference it too, could have in the DataFolder a m_AudioTransformReference vector too.
      TransformComponent* CreateTransform( Entity& entity, const glm::mat4& inital_value );
      
      // This system allows multiple components per entity:
      std::vector<TransformComponent*> Find( const Entity& entity );
      
	private:
      // ...
};

This way memory can be kept completely together, if you free up a component there could be reference to open slots so they get allocated first for example.  Or if you hand back handles which is my plan part of the handle will be an index value into the array.  This setup makes multithreading easy say when applying all the animation transforms after physics has been done.  You can go wide, the memory is in cache friendly ordering already.

Working with this in an editor is also fairly easy, just keep track of all the entities and you know what systems you need to search to get their component data.  Entities are lightweight as they should be, no reflection is needed at all.  *shrugs*.  Doesn't suffer from inheritance or virtual functions either.

"Those who would give up essential liberty to purchase a little temporary safety deserve neither liberty nor safety." --Benjamin Franklin

1 hour ago, Mike2343 said:

This way memory can be kept completely together, if you free up a component there could be reference to open slots so they get allocated first for example. 

So it doesnt keep memory completely together, except in one very specific case where N components are removed and then immediately N new components are created.  Right?

This topic is closed to new replies.

Advertisement