Advertisement

System's Dependency on Entity Component System

Started by May 04, 2023 06:26 PM
3 comments, last by LorenzoGatti 1 year, 7 months ago

Hello, I'm Junior Game Engine dev.

In few days, I have studied ECS, and I tried to set up for my project.

But, I wondered how to manage system on ECS.

A few systems need dependency, like RenderSystem must run after MoveSystem.

Then, On ECS, the Systems have specific sequence?? or just set up dependency systems?

I Simply thought, just make linked list that connect Systemslike, using System's Interface pointer.

But, It's expected to be complex and difficult on bigger project.

And, linked list cannot use parrellel method for multiple systems.

Cause, just run single systems step by step.

Finally, I thougth, just use Directed Acyclic Graph

that look like the system's attributes are Sequencial and Dependencial.

## ISystem - System's Interface, all of system inheritance this object.

Linked List

Just Connect Prev ISystem's Pointer with Next ISystem's Pointer

Vector

Register System the last index, and access using index

Graph - Directed Acyclic Graph

Just Register dependencial System on other System.

run by dependencial distance which is zero or smaller

Then, What do you think about managing Systems on ECS??

I would stay away from linked lists, they are not a good fit for most problems on modern CPU/memory systems. Graphs also seem more complexity than you need with similar issues as linked lists. A graph still needs to be turned into a distinct evaluation order of systems when it is executed.

I would stick with a simple vector or array of systems that are always updated in a consistent order. The order is probably defined at the start of your program and is determined by the natural dependencies between the systems. For instance, you would always want to update physics after animation (so that kinematic objects can be moved by animations), yet before graphics rendering, so that graphics use the latest object positions.

Below is rough outline of how my engine works. Systems have a shared interface with virtual functions for update/add/remove, and get added to the engine in the proper order when it is configured. You can also configure inter-system dependencies at this point by giving a system a pointer to another. The engine is primarily a container for components and systems, and also handles update ordering and notifying systems of components getting added/removed. Any type of component can be stored in the engine, and components are mostly just data. Systems implement the logic. Systems have access to the engine in their update() functions so that they can add/remove components from the engine, query all components of a certain type, or access other systems.

class EngineSystem
{
	virtual void update( Engine& engine, float dt ) {}
	virtual void addComponent( type, void* ) {}
	virtual void removeComponent( type, void* ) {}
};

class Engine
{
public:
	void addComponent( type, void* );
	void addSystem( type, void* );
	void update( float dt )
	{
		// Update systems in order.
	}
private:
	set<Type,void*> components; // Some kind of multi-type component set
	std::vector<EngineSystem*> systems; // Updated in the same order
};

class PhysicsSystem
{
	virtual void update( Engine& engine, float dt )
	{
		// move kinematic objects
		// tick simulation (possibly async)
		// read current transforms into scene objects
	}
};

class GraphicsSystem
{
	virtual void update( Engine& engine, float dt )
	{
		// read final transforms from scene objects
		// render scene
	}
};

int main()
{
	Engine engine;
	PhysicsSystem physics;
	GraphicsSystem graphics;
	engine.addSystem( &physics );
	engine.addSystem( &graphics ); // update after physics
	
	// Add scene to engine. SceneSystem adds child objects to engine.
	Scene scene;
	engine.addComponent( Type::of<Scene>(), &scene );
	
	while (running)
	{
		engine.update( deltaTime );
	}
}

As for multithreading the main engine update, I would leave that to an implementation detail of the individual systems rather than trying to architect a one-size-fits-all solution in the engine. For instance, it might be possible to render graphics on a separate thread from the main update. Inside the GraphicsSystem::update() you would copy the current interpolated objects state to some internal storage, then start to render a frame on a background render thread (maybe wait if previous frame isn't done yet). This way you can render frame N while frame N+1 is doing Engine::update(). For physics, it might have its own internal threading that splits the work among multiple worker threads, but still be synchronous with the main Engine::update(). The AudioSystem will use its own internal real-time thread for mixing the audio samples which is always running in the background. The specific needs of each type of system are different, and thus require a different multithreading solution.

Advertisement

Aressera said:
om linked lists, they are not a good fit for most problems on modern CPU/memory systems.

Aressera said:
The order is probably defined at the start of your program and is determined by the natural dependencies between the systems.

Thanks for your opinion.

I thought that I was over-thinking about the dependencies between systems.

And, I missed to think about Cache locality on CPU.

Like you said, in the case of multi-threading, the engine do not use multi-thread for systems, like parellel running together Physics System, Rendering System, but use multi-thread for each inner system, like your said rendering system has multi-threading.

I had a headcache while thinking about it by myself, but I just heared your opinion, my problem is looked like solved.

Thank you very much.

Regarding linked lists, the performance impact of iterating over a few systems once per frame with a linked list is negligible unless you go out of your way to perform unnecessary work (e.g. traversing all elements of a list repeatedly from the beginning to access elements by index).

The data structure containing the systems (in Aressera's example, some internal part of "engine") is going to either stay in L1 or L2 cache forever because it is small or to be evicted from cache every frame because some system accesses a lot of memory, irrespective of how it is implemented.

What's far more wrong with this abuse of linked lists is attempting to use a single “previous” or “next” pointer to other systems to approximate the multiple dependencies of a DAG.You need to really represent a DAG and compute a correct scheduling of system updates from it if you want to update multiple systems in parallel, otherwise any correct sequential ordering will be fine.

Omae Wa Mou Shindeiru

This topic is closed to new replies.

Advertisement