Decoupling in ECS

In the monobehaviour environment we have lots of tools (namely events) for decoupling different “sections” of our code. In ECS this is not as easy. This seems highly problematic. One of the main benefits of decoupling your code is maintainability. This is less of a concern when using ECS because you tend to write more maintainable code anyway, but it would still be a nice tool to have.

Something else to consider is assets… right now I’ve witten a stat system (a system which maintains a buffer of stats derived from arbitrary sources such as equipment, the talent system, and buffs / debuffs), a talent system which grants stats and does some other stuff, a “container” system which covers inventory, equipment, etc, and a few other systems. I’m not looking to turn these into assets but if I were I struggle to imagine how I would.

As it stands my talent and my container (think equipment) systems are aware of aspects of my stat system. When a piece of equipment is added or removed it must trigger a stat recalculation, and same when a talent is allocated or deallocated. In monobehaviour world I would fire an event. In ECS world I call something like

commandBuffer.AppendToBuffer(entity, new EquipStatItem { item = itemEntity });

which works fine but EquipStatItem is a stat-system type… Ideally the container-system should not be aware of it at all. If I wanted to sell my container system or my talent system I would have to give them the stat system too. Again, this isn’t bad (for me) but I am aspiring to create decoupled plugins that can be theoretically drag-and-dropped from one project to another and this seems basically impossible.

Am I doing something wrong? I’ve seen some events-in-ecs systems but without having a defacto Unity-backed option it feels weird to build a bunch of stuff on top of them.

1 Like

If I understand correctly, what you are asking is that for some component A, how to make an A-read system react to changes from an A-write system, without A-write system and A-read system knowing about each other.

One way you can go about this is to make EquipStatItem non-system-specific. I.e. The container system does the AppendToBuffer without a specific system in mind. Then an arbitary number of other systems can read the buffer to know about what items are added. Finally, a “reset” system clears the buffer at the beginning of each frame.

One caveat for this setup is that you have to make sure all writers run before all readers in a single frame. This is trival when you only have one component. Just have an AWriterSystemGroup and AReaderSystemGroup and place one before the other. However this does get complicated with multiple components. Suppose you have components A and B, and you have systems A-write, A-read-B-write, B-read, B-write, etc. What groups do you make here?

One idea I have and haven’t tried is to make dummy systems that mark the boundry of writes. E.g. Have a AWriteFinish : ISystem. Then all systems which write to A will [UpdateBefore(AWriteFinish)], and all systems which read A will [UpdateAfter(AWriteFinish)]. You can extend this easily to cover multiple components, and theoretically Unity will automatically solve the system ordering for you according to these constraints.

@Blargenflargle there are many ways to handle events in ECS. I would argue that ECS is actually better at it than OOP, because it’s easier to achieve a message queue pattern (A sends a message, B reads it, with neither A or B depending on each other).

You might be interested in reading this thread . Where @PhilSA goes over 8 event patterns in DOTS.

If you’re looking for a ready tool, I’d recommend the event package from @tertle , as it’s likely to be a very fast general solution. It uses NativeStreams, and it support parallel reading and writing of events. It looks like the documentation is being updated for Entities 1.0, but here’s an older version.

1 Like

I’m mashing terms here and causing confusion. My stat “system” is not a single ECS system; It is multiple components, archetypes, and ECS systems. The same goes for my talent and container “systems”. I want to call them plugins but without being technically separable, I’m not sure “plugin” really fits. From here on out I will call them plugins to avoid confusion.

I’ve implemented this pattern for a temporary event stand-in… just add an rpc to the corresponding nativelist and it will be sent in the next OnUpdate. This pattern has many problems but for now it works.

NativeList<TalentAllocationRequestRpc> talentRpcs = new NativeList<TalentAllocationRequestRpc>(Allocator.Persistent);
    NativeList<PressContainerSlotRpc> containerRpcs = new NativeList<PressContainerSlotRpc>(Allocator.Persistent);

    protected override void OnDestroy()
    {
        talentRpcs.Dispose();
        containerRpcs.Dispose();
    }

    protected override void OnUpdate()
    {
        Setup();

        var commandBuffer = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(World.Unmanaged);

        for (int i = 0; i < talentRpcs.Length; i++)
        {
            var rpc = talentRpcs[i];
            var rpcEntity = commandBuffer.CreateEntity();
            commandBuffer.AddComponent<SendRpcCommandRequestComponent>(rpcEntity);
            commandBuffer.AddComponent(rpcEntity, rpc);
        }
        talentRpcs.Clear();

        for (int i = 0; i < containerRpcs.Length; i++)
        {
            var rpc = containerRpcs[i];
            var rpcEntity = commandBuffer.CreateEntity();
            commandBuffer.AddComponent<SendRpcCommandRequestComponent>(rpcEntity);
            commandBuffer.AddComponent(rpcEntity, rpc);
        }
        containerRpcs.Clear();

It is fine that ECS might be theoretically better than OOP at events but without an official event system sponsored by Unity the problem for assets (or just sharing “plugins” with your friends) remains; There is no official way to properly decouple sections of your code. @tertle seems like an extremely knowledgeable and competent dev. I won’t feel bad when I inevitably implement their event system. But that’s a solution for me; not for the community. The best case scenario is that Unity implements a vanilla event system. The more likely (but still good) outcome is that tertles plugin becomes a widely adopted pseudo standard (like Mirror was for networking). The worst case (and very possible) scenario is that no standard is adopted and everyone just does their own thing.

Keep in mind that all of this is coming from me hooking UI into the ECS world, something almost every game is going to have to do. Everyone is going to have to solve this problem so why isn’t there an out-of-the-box solution? This should not be a library I add later.

Event systems goes against the ECS design. You can have decoupling in ECS; you just need to add more components. What you don’t have is the OOP idea of encapsulation.

This topic of creating independent packages is something I struggle with too.
The solution i would envision is some mechanism where i can alias two ComponentTypes that are in different packages and define some kind of translation between them. Kind of like an adapter. This way each package could have its own Components but you dont have to copy data between virtually equal components just to “connect” packages together.

We need a way to decouple Data and not so much Logic (Events).

Like a interface?

Example:
Component in PackageA: Speed
Component in PackageB: Velocity
Component used by User: UnitSpeed

Now the User does not want to copy data from UnitSpeed over into Speed and Velocity just to use those packages.
It would be optimal to be able to define that UnitSpeed is exactly the same as Speed and Velocity in memory.
Can we do that with an interface somehow? Then yes. I havent managed it on my own yet.

You can do this sort of thing with DynamicComponentTypeHandle, but it is not always pretty.

Yes DynamicHandles and generic Systems are a solution to this issue. But that solution depends on every package “boundary” beeing defined in a generic way. Which let’s be honest is never going to happen. The user himself can’t do anything to “connect” packages.

While it is a nice sentiment that event systems are against the ECS “design,” they are very good for shipping games. I understand that it’s not perfect ECS but the fact remains that I need my UI to “talk” to ECS, and my UI “talks” in events. This needs a standard solution.

“More components” is subobtimal. Lets say I have two plugins I want to talk to each other. For this exercise I’ll say my stat plugin and my character controller plugin. The character controller must be aware of the speed stat which is held in a StatContainer buffer.

I could create some kind of middle component, say a “Speed” component that the stat plugin writes to and the character controller reads from, but this is still the same problem but with more components. This is decoupling in the weakest sense. I suppose it does work.

I would be interested in seeing examples of this but ideally I wouldn’t introduce anything that makes my projects harder to maintain.

1 Like

So similar to how in classic OOP, you need interfaces for different packages to call into each other, in ECS, you need components (data) that all the different packages are aware of and can use to talk to each other automatically. Otherwise, it is up to the user to connect the pieces together. That does sometimes mean you end up with duplicate components. I don’t think there is a good solution right now. It is going to require a bunch of package developers to standardize on a set of basic components that everyone depends upon. At least with assembly definition files, you can detect the presence of another package and specify a scripting define for your assembly. That allows you to make a dependency on other components optional.

2 Likes

My UI/key or mouse input talks to ECS in inputs not events. For example, is button W up or down is a bool in a struct that sets a singleton component and has nothing to do with an event system. My input struct is not necessarily a 1 to 1 match for my singleton component.

If you could show a code snippet of how you process UI “inputs” this way I’d appreciate it.

I cannot say if this will be the exact final design but this should give you an idea of my MonoBehaviour side of things. Each player will get a inputData struct which then gets converted into a singleton component, but not 1 to1, which contains all of the data for all of the players. The server needs to take the data and share it around the network.

The data gets optimized for network transport and ends up as a byte array. For example if bool hasShootingData is false then some of the data does not need to get serialized for networking. Events are not really compatible with lock-step networking.

public struct buttonOrHotkeyPressData
{
    public enum ButtonOrHotkeyPressType
    {
        startDirectControlSprinting, startFire, endFire,
        moveI, runI, moveToContactI, moveToContactF, SneakF, AssaultF, AttackF, OverrunF, moveToContactS, firingLineS, advanceS, retreatS, moveV, moveToContactV
    };

    public readonly ButtonOrHotkeyPressType buttonPress;
    public readonly float3 rayDirectionCam;
    public readonly float3 rayOriginCam;
    public readonly bool appendPath;
    public readonly int unitID;

    public buttonOrHotkeyPressData(ButtonOrHotkeyPressType buttonPress, int unitID, bool appendPath, float3 rayDirectionCam, float3 rayOriginCam)
    {
        this.unitID = unitID;
        this.buttonPress = buttonPress;
        this.appendPath = appendPath;
        this.rayDirectionCam = rayDirectionCam;
        this.rayOriginCam = rayOriginCam;
    }
}
public struct inputData
{
    public readonly buttonOrHotkeyPressData[] buttonPressesWithData;
    public readonly buttonOrHotkeyPressData.ButtonOrHotkeyPressType[] buttonPresses;
    public readonly bool hasSelectionData;
    public readonly float2 startPointSelectionWorld;
    public readonly float2 endPointSelectionWorld;
    public readonly bool hasMoveData;
    public readonly float2 moveData;
    public readonly byte playerIDForThisData;
    public readonly bool hasShootingData;
    public readonly float3 shootingPos;
    public readonly float3 shootingDirection;

    public inputData(buttonOrHotkeyPressData[] buttonPressesWithData, buttonOrHotkeyPressData.ButtonOrHotkeyPressType[] buttonPresses, bool hasSelectionData, float2 startPointSelectionWorld, float2 endPointSelectionWorld,
        bool hasMoveData, float2 moveData, bool hasShootingData, float3 shootingPos, float3 shootingDirection, byte playerIDForThisData)
    {
        this.hasMoveData = hasMoveData;
        this.moveData = moveData;
        this.buttonPressesWithData = buttonPressesWithData;
        this.buttonPresses = buttonPresses;
        this.playerIDForThisData = playerIDForThisData;
        this.hasShootingData = hasShootingData;
        this.shootingPos = shootingPos;
        this.shootingDirection = shootingDirection;
        this.hasSelectionData = hasSelectionData;
        this.startPointSelectionWorld = startPointSelectionWorld;
        this.endPointSelectionWorld = endPointSelectionWorld;
    }
}

You could somewhat archive this with aspects. Both ow them would have to be optional and you would have to tests which component is present on an entity so performance would suffer a bit. And you would need some component in aspects that is not optional.