Signal - an easy DOTS event system

Hi, wanted to share a simple event system for DOTS. You can use it directly or draw inspiration from it.

It’s basically three simple methods in a helper-system, to create a new signal, ask for a specific signal, or ask for the value of a specific signal.

What it does is create an entity of SignalComponent whenever SetSignal is called, reusing an old one if present. The Get’ers use LastSystemVersion of the calling system, to find events that are newer than last run.

All feedback and suggestions welcome. The code is as simple as its writer, I’m still a noob programmer.

*** Update 2021.04.05 ***

  1. Changed the logic of reusing existing entities when creating a new signal, and added a static variable that can changed based on need; reuseOldSignalsThresholdMultiplier

This multiplier is used on the delta of SignalSystem’s own version, which now runs once per frame. That means the variable is approximately a frame, and thus a value of 2 should cover most situations, and should be increased if there are systems not guaranteed to run every cycle.

  1. Methods are now static
    This gives cleaner and easier usage, and a bonus is that change filter on query is working, which is super useful when using the same technique on my Settings helper-system.

Also, GlobalSystemVersion isn’t any more unnecessary +1’d for every call to a method.

I tried to find a way to access the caller system’s LastSystemVersion, so that parameter could be omitted in the call, but I couldn’t get how the EntityQuery access it when checking the filter. Could possibly do it with reflection, but that would kill the speed. Any input welcome.

NOTE: As this is converted to static methods, this will only work on single world, as noted by @tertle . Visit his Event System for a more thorough solution.

Usage:
In your systems, simply call the methods. Concider adding a using static Vildauget.SignalsSystem; for easy access.

Create signal:

if (jumpInputAction.triggered) SetSignal(Signal.inputJump);

In another system, get the signal with:

var jumping = GetSignal(Signal.inputJump, LastSystemVersion);

The component and enum - add new signal types here:

public struct SignalComponent : IComponentData
    {
        public Signal signal;
        public uint version;
        public float value;
    }

    public enum Signal
    {
        save = 0,
        inputJump = 1,
        inputMoveX = 2,
        inputMoveY = 3,
        inputLookX = 4,
        inputLookY = 5,
        inputLookToggle = 6,

    }

And the helper system:

using Unity.Entities;
using Unity.Collections;
using static Unity.Mathematics.math;

namespace Vildauget
{
    public class SignalsSystem : SystemBase
    {
        static EntityQuery      query;
        static Entity           signalEntity;
        static EntityManager    manager;
        static SignalsSystem    signalsSystem;
        static uint             reuseOldSignalsThresholdMultiplier = 2;
    
        protected override void OnCreate()
        {
            manager = EntityManager;
            query = GetEntityQuery(ComponentType.ReadOnly<SignalComponent>());
            signalEntity = manager.CreateEntity(typeof(SignalComponent));       // used to instantiate new entities
            signalsSystem = World.GetExistingSystem<SignalsSystem>();           // static reference to access system's LastSystemVersion
        }

        protected override void OnUpdate() {}

        public static bool GetSignal(Signal signal, uint lastUpdate)
        {
            if (!query.HasFilter())
                query.AddChangedVersionFilter(ComponentType.ReadOnly<SignalComponent>());
        
            var signalComponents = query.ToComponentDataArray<SignalComponent>(Allocator.Temp);
        
            for (var i=0; i<signalComponents.Length;i++)
                if (signalComponents[i].signal == signal && asint(signalComponents[i].version - lastUpdate) > 0)
                    return true;
        
            return false;
        }

        public static float GetSignalValue(Signal signal, uint lastUpdate)
        {
            if (!query.HasFilter())
                query.AddChangedVersionFilter(ComponentType.ReadOnly<Signal>());
        
            var signalComponents = query.ToComponentDataArray<SignalComponent>(Allocator.Temp);
        
            for (var i=0; i<signalComponents.Length;i++)
                if (signalComponents[i].signal == signal  && asint(signalComponents[i].version - lastUpdate) > 0)
                    return signalComponents[i].value;
        
            return 0;
        }

        public static void SetSignal(Signal signal, float value=0)
        {
            if (query.HasFilter())
                query.ResetFilter();
        
            var signalComponents = query.ToComponentDataArray<SignalComponent>(Allocator.Temp);
            var signalEntities = query.ToEntityArray(Allocator.Temp);
        
            var systemVersionDelta = manager.GlobalSystemVersion - signalsSystem.LastSystemVersion;
            var oldSignalThreshold = manager.GlobalSystemVersion - systemVersionDelta * reuseOldSignalsThresholdMultiplier;

            // reuse outdated signal if present
            for (var i=0; i < signalComponents.Length; i++)
            {
                if (asint(oldSignalThreshold - signalComponents[i].version) > 0)
                {
                    manager.SetComponentData(signalEntities[i], new SignalComponent {signal = signal, version = manager.GlobalSystemVersion, value=value});
                    SetName(signalEntities[i], "Signal: " + signal.ToString());
                    return;
                }
            }
        
            // create new entity with signal if outdated signal wasn't found
            var newSignalEntity = manager.Instantiate(signalEntity);
            manager.SetComponentData(newSignalEntity, new SignalComponent {signal = signal, version = manager.GlobalSystemVersion, value=value});
            SetName(newSignalEntity, "Signal: " + signal.ToString());
        }
    
        static void SetName(Entity entity, string name)
        {
            #if UNITY_EDITOR
            manager.SetName(entity, name);
            #endif
        }
    }
}
3 Likes

Found an error in my understanding, and had to make the solution a bit more clumsy.

Problem is, when calling a method inside another system, then “LastSystemVersion” inside the the method is the owning system’s last run version, not the caller’s.

So, have to submit the caller’s LastSystemVersion into the methods. Clumsy, so a bit sad puppy, but it still works fine for this system.

That also means change filter cannot be trusted, so I had to remove them. Not important in this case as there always will be one chunk to read and most of the time at least one change, but am also building the same kind of logic for a lot of shared settings variables where it would be super useful to only update settings in a system when the data changed.

Any input or thoughts welcome.

Update 2021-04-05: see original post for update code.

Make methods static, when working with DOTS.

What?

2 Likes

Please be more constructive than just writing single word. That’s not contributing at all. If you know issue to OP case, please advise.

We know is advised to use static methods when working with DOTS and specifically with jobs. Granted there is no job in OP case scenario.

Thank you, @Antypodish . I managed to get the class working with static methods, by making static references to the EntityQuery, Entity, EntityManager and its own system.

This is great, since the new way of calling the methods means that GlobalSystemVersion isn’t upped every time a method is called, and also - testing shows that the Query Change Filter now works - at least I wasn’t able to break it. I’d imagine this will be faster as well, when getting alot of action going.

I still need to check each saved signal’s version against the caller’s last system version, and need to specify that in the call of the get’ers. If you have any idea of how I could access that without a parameter, I’d love to hear it. I tried looking through how the Entity Query gets it for its filter, but I couldn’t grasp it.

I was asking what because the suggestion made no sense to me so it needed more explanation. Static methods make little sense here and I would probably argue that any public static methods on a system that rely on the system state is just bad practice. It breaks the concept of world isolation.

The thing is now that you’ve made it static your solution no longer works for games with multiple worlds (e.g. multiplayer)

3 Likes

Why do not use just signal (request in my case) entities? If you need to do something you just create an entity without any fancy code.
Something like this:

if (Input.GetButtonDown("SaveGame"))
{
    var e = PostUpdateCommands.CreateEntity();
    PostUpdateCommands.AddComponent(e, new SaveRequest());
}

Add RequireForUpdate(saveRequest) and remove entity after processing in destination system.

Thank you for pointing that out. I’m working on a single world solution, so I’ll keep it as is, with the benefits I got, but noted that in the original post, and also linked to your event system on Github, that can handle it all, with the added complexity.

Good suggestion. As always, it depends on use case. For me personally, I don’t want that direct link between creator and consumer. I want any system to have the power to send a signal without caring if anyone else sent the same signal simultaneously, and more importantly, any number of systems can get the signal. That’s how the helper system helps out, and now, with only one line of code to set or get, the signal is available immediately to all systems.

Also building on this, I’m now using the same logic for all settings and states. I’ll share those if there’s interest, but it’s basically the same as signals, adjusted for the different use cases, including super easy save and load of all settings, as they’re all same components.