Registering ECS Systems in a Plugin System

Started by
4 comments, last by Shaarigan 3 years, 9 months ago

I'm trying to design a plugin system to interact with my ECS. My one problem is how to handle systems in the game loop. Currently it's single threaded but I aim to have it be multi-threaded, and either way I need to manage the dependencies and order. The two systems I can think of are:

  • Systems register themselves to a list, but the application DLL chooses the order by their name. Additional plugins (mods, etc) can be added either before or after the core game loop
  • Systems reference the other systems that need to run before, and so they order themselves on initialization. I think this is similar to what Unity does?

Does anyone have any prior experience implementing a system like this? If so how do you handle your game loop?

Also, if anyone has any suggestions in general about designing a plugin system for modern games, that would be great.

Thanks in advance

Advertisement

I think in general you'll want to make most things as explicit as possible, even with a generic plugin-system.

What I mean by that is, instead of implicitely depending on systems executing in a given order, try to tie their execution together using a method of having system A say “i need to run my function A when system B executed event C”.

One way of doing it, which probably scales best when you can anticipate graph-like dependencies is what you describe that unity does. Have each system specificy which other systems it needs, and have some solver that calculates in which order they must run.

A much simpler approach would be to use event-handling, to allow systems to have other systems hook themselves to concrete events:

class CullingSystem
{
    Action<Entity[]> onPostCull;
}

So when you want to do something with all culled entities, subscribe to that action.

Another way, that I recently adopted mainly for my editor is having an interface that you can implement to receive a notification of a specific action:

class IPostCullHandler
{
    virtual void OnPostCull(span<Entity> visibleEntities) = 0;
}

And then you have a little bit of template magic to automatically reflect on those when registering a system (as I'm mainly running C++); and afterwards you simply invoke them like a Vector<IPostCullHandler*> from within the culling-system.
(Obviously depending on the language and the ecosystem you can have this easier or harder)

And for everything else, when you really don't need an explicit order, you just throw it into a generic update-loop; why not. But if you have to start bothering with that certain things depend on other things being run, you are probably better off with an approach as outlined above. I personally still use a division of updates into PreUpdate/Update/PostUpdate, which is not the cleanest of designs but still works fine in general (though I think I could get rid of it if I adapt the interface-solution from my editor back in to the core-engine). I can definately say that from a design and usability standpoint, the interface-approach has really pannded out well and enabled me to easily extend the functionality of the editor w/o many issues and without ever having to think about orders of execution.

Hope that gives you a few ideas.

Thanks for the advice @juliean - I'll probably go with a similar approach - but I was thinking of having a central manager control them, because I think it would be easier to multithread with that information and I could more easily put things into stages and synchronize them for use in a job system.

Quick question though, how do you notify the CullingSystem that you want to add an action after? Is there a message system that tells it to set the callback? I'm contemplating how to reference systems when different teams can come up with different names. Currently I'm thinking of either GUIDs, strings, or ints based off the order in which they're registered. Also not sure whether to decide these compile time (better performance but possible collision) or runtime

KarimIO said:
Thanks for the advice @juliean - I'll probably go with a similar approach - but I was thinking of having a central manager control them, because I think it would be easier to multithread with that information and I could more easily put things into stages and synchronize them for use in a job system.

Yeah, maybe thats better for multithreading (or even required as I initially said if you have dependencies on multiple results instead of just one). I've gotting away with not using multithreading except for a few corner-cases here and there for now, so I don't really have much expertise in it.

KarimIO said:
Quick question though, how do you notify the CullingSystem that you want to add an action after? Is there a message system that tells it to set the callback? I'm contemplating how to reference systems when different teams can come up with different names. Currently I'm thinking of either GUIDs, strings, or ints based off the order in which they're registered. Also not sure whether to decide these compile time (better performance but possible collision) or runtime

I'm not entirely sure if I understand the question, but if you are just asking how things are hooked up, then depending on the approach:

A) With the Action/delegate-based approach, it would be the responsibility of the system that wants the callback to register itself. Something like:

void ReflectionRenderSystem::Init(conts game::SceneContext&amp; scene)
{
	auto&amp; cullSystem = CullSystem::Get(scene);
	cullSystem.onPostCull += BIND_MEMBER(OnPostCull);
}

void ReflectionRenderSystem::OnPostCull(std::span<Entity*> visibleEntities) { ... }

This is a pseudo-realworld example from my codebase; init would be called after all systems have been constructed so they are accessible (but you could use some other kind of dependency-injection approach)

B) With the interfaces, its a bit more complicated. But what it boils down to, is the CullSystem having a specializated container which is a wrapper around vector<IPostCullHandler*> as I mentioned, and it would then register that container with the class where all systems are registered/spawned:

using SceneChangedHandler = HandlerContainer<ISceneChangedHandler, true>;

void SceneSystem::OnInit(const EditorContext&amp; ctx)
{
	ctx.handler.RegisterHandlerCallback(m_sceneChanged.MakeRegisterCallback());
}

void SceneSystem::ChangeScene(Scene* pScene)
{
	m_sceneChanged.Execute([pScene](ISceneChangedHandler&amp; handler)
	{
		handler.OnSceneChanged(pScene);
	});
}

(Just posting an actual example from my editor so I have to type less :P)
Calling then handler-container will then run the lambda I pass for each registered handler.

And to actually get those handlers (which I guess your question is aimed at), from user-perspective you derive the interface:

class EntitySystem final :
	public EditorSystem<EntitySystem>,
	public ISceneChangedHandler {}

And then its a case of a little bit of c++-fuckery to automatically register those:

template<typename HandlerT>
void HandlerManager::RegisterHandlerCallback(core::Action<HandlerT&amp;> handler)
{
	const auto checkHandler = [handler](BaseEditorSystem&amp; system)
	{
		if (auto* pHandler = dynamic_cast<HandlerT*>(&amp;system))
			handler(*pHandler);
	};

	m_vCheckHandler.EmplaceBack(checkHandler);
}

Thats what the callback for registering the container actually does. I store a functor that I can generically invoke by iterating over an vector containing all the potentially callbacks; using a dynamic_cast on the systems base-type to check if a handler is present. This is called for every system when it is instantiated; and through clever use of captures/delegates will automatically put each callback into the vector inside whever HandlerContainer is.

So, again I'm not sure if thats what you asked for (and/or if I confused you with all that c++-hackery), but I'm not sure since you mentioned GUIDs/strings for identifying where that would fit in. Essentially my system is setup automatically using language constructs, without the need for managing any type of explicit mapping; and in that way is actually quite natural (minus the code that is responsible for making it work in the background that is; also it kind of relies on that systems are never destroyed individually as this would mean we would need a lot more logic for removing callbacks again).

EDIT: Also if this weren't c++ you could use runtime-reflection to get the handlers instead. Kind of like unity does it with IPointerUpHandler ie; which is were I got the original idea from btw.

KarimIO said:
and either way I need to manage the dependencies and order

In an ECS environment, Systems should never ever have dependencies onto each other. In this case your ECS desing is simply wrong! Entities are carrier objects that don't have data nor code but belong to components. Components have the data that is related to an entity and Systems operatte on data, not entities.

(Please note that Unity is not using a real ECS rather than something based on it)

This topic is closed to new replies.

Advertisement