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& scene)
{
auto& 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& ctx)
{
ctx.handler.RegisterHandlerCallback(m_sceneChanged.MakeRegisterCallback());
}
void SceneSystem::ChangeScene(Scene* pScene)
{
m_sceneChanged.Execute([pScene](ISceneChangedHandler& 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&> handler)
{
const auto checkHandler = [handler](BaseEditorSystem& system)
{
if (auto* pHandler = dynamic_cast<HandlerT*>(&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.