I have this system running to get my input into ECS world. (The event subscription syntax is just a wrapper around Unity’s new input system.)
internal partial struct InputHandlerSystem : ISystem
{
private bool shoot;
private bool changedInOnUpdate;
public void OnCreate(ref SystemState state)
{
Input.Gameplay.Shoot.OnEvent += HandleShoot;
}
public void OnDestroy(ref SystemState state)
{
Input.Gameplay.Shoot.OnEvent -= HandleShoot;
}
private void HandleShoot(InputAction.CallbackContext context)
{
if (context.started)
{
shoot = true;
Debug.Log($"shoot = {shoot}"); // This log always shows "shoot = true"
}
}
public void OnUpdate(ref SystemState state)
{
Debug.Log($"shoot = {shoot}"); // This log always shows "shoot = false"
Debug.Log($"changedInOnUpdate = {changedInOnUpdate}"); // This log shows the value changing as expected.
changedInOnUpdate = !changedInOnUpdate;
}
}
The thing I’m seeking to understand is in the comments in the code. In HandleShoot, I set the member boolean shoot to true and see its value is true in the log, but in OnUpdate, the value of shoot is always false. On the other hand, if I change the value of a member boolean in OnUpdate, then the changed value seems to stick until the next frame, as I’d expect.
Why does changing members work in OnUpdate but not in my input event handler method? What is going on behind the scenes here? (I currently see no additional Unity-generated source for InputHandlerSystem for me to look into - I can see it for other systems in my code, but this one doesn’t have any.)
Subscribing to this event creates a closure with a copy of your ISystem struct. That’s why the changed bool isn’t written back to the system state in ECS-managed memory. I’d recommend against using C# events in this context – simply check the input in the OnUpdate method. For example, branch based on inputAction.WasPerformedThisFrame().
(More broadly, I wouldn’t recommend C# events in almost any context, but that’s one of my more controversial takes.)
Thanks. That makes total sense - the delegate is populated externally to the struct and therefore takes only a copy.
I was originally using what you suggested (WasPerformedThisFrame) in this system’s OnUpdate but I need inputs to register in some systems that run in the fixed step simulation group. At a high framerate, WasPerformedThisFrame would be true, then false in the next frame, and this would all happen between a single fixed step, so that the fixed step systems would never even see that the action had been performed. I could put input handling inside a fixed step system, but that introduces other issues.
I’ll have to figure out another way! I guess if I’m really married to this event-driven way of doing things, I could store & set the members on a singleton entity component instead. My other thought is creating a buffer to store performed inputs that certain “consumers” can register to consume, which might be necessary given the fixed step issue in the prev paragraph.
On that other note you mentioned… I’m curious! I love C# events and have used them at scale on various projects (I’m new to ECS, so that was on regular object-oriented code). What don’t you like about them & why, and what alternatives do you prefer (whether for OOP or ECS)?
(Hi to everyone who already knows me as a notorious event hater.)
There’s some things I don’t like about events. I mostly mean it specifically relating to writing Unity (MonoBehaviour-based) gameplay code. They’re probably fine in other contexts, e.g. .NET code, engine/editor events, etc.
The subscribe/unsubscribe pattern is just plain annoying to write. I hate the inevitable unreadably huge “subscription walls”.
Events require you to have this weird dependency where the subscribers need to be aware of the publisher object. The code is just ugly, tightly-coupled, and in my experience it plants the seed for even more ugly code.
No ability to handle exceptions. In an ideal world, event handlers never throw any exceptions, which is about as realistic of an assumption as not ever having any kind of bug. In the real world you’ll have an unassigned component ref somewhere eventually, and your game will stop working. (Some might say this is preferable to the game continuing to work in a possibly semi-broken state so that you’re forced to fix the bug. It’s not, and your QA guys will strangle you for it, because now they can’t work.)
Serializable UnityEvents are especially triple quadruple cursed.
In Unity-flavored OOP land, I much prefer this sort of pattern:
Events are emitted to components in the hierarchy, which is 90% of my use cases.
For other scopes, you can keep a list/set of interfaces, adding/removing from it in OnEnable/OnDisable similar to how you’d do with events.
I have a framework for making it less cumbersome to gather and track (MonoBehaviour-based) components from the hierarchy and globally, but it’s still a bit ugly and error-prone so I don’t advertise it much.
The subscriber component doesn’t have to know anything about the publisher object, and vice versa.
You can easily tell what events the component is handling by looking at the top of the file.
On the publisher side, you can more easily tell where the objects you’re notifying are coming from.
You can have multiple publisher types, e.g. separate component implementations for PC and NPC components that publish the same events.
You can add more methods to the interface.
Maybe they’re not events at all, even.
For example, you can add a int Priority { get; } and sort the listeners before invoking some method.
You can gather subscribers on demand by calling GetComponentsInChildren at invoke time. You can even gather the components and “publish” the “event” to a GameObject without knowing anything about it. You might need it once in a blue moon (e. g. generic pooling system callbacks).
You can easily access the subscriber list, e. g. for debugging.
You can customize the way handlers are invoked (e.g. add logging, exception handling, etc.)
None of this is super fast, but neither are events.
As for ECS, the situation looks completely different there. I don’t have strong preferences yet.
I’m not certain what’s the best best way to handle this one. You can keep a bool ShotRequested that is set to true when the input is detected, and reset to false after it is consumed in fixed update. Not sure what the event buffer gets you over that. Maybe an int if you anticipate that the input can occur multiple times in between fixed updates. And yeah, probably best to put it in a component. (Not sure if singleton…)
Thanks for the detailed elaboration! We agree at least on avoiding serializable UnityEvents
Yeah, I’m still experimenting with approaches there. Anything truly synchronous-event-delegate-like in ECS is going to badly hit performance, it seems.
That’s effectively what I ended up doing.
The buffer would just be a way to keep everything in a singleton-like pattern for the input handler, where anything that wants to consume input could “register” to be a consumer of certain inputs from the buffer, and then the input handler produces as many as are needed into the buffer (could use an int counter for this instead, if I don’t need additional data from the inputs). The point being, if multiple systems need to consume a single input, then it’s not just the first one that consumes it (turning the bool false), but all of the systems that registered, meaning as development progresses I don’t have to keep track of which systems need which inputs & change boolean values, I just have to ensure they register to that buffer.