Confirm I'm using components correctly, and references?

There are a few things I’d like to make sure; this is a test I’m doing for a powered cartridge, it should have a power draw and a power storage entity, think something like a N64 cartridge with a battery.

  1. CreateEntity is being used properly in both systems, to create a new Entity of the given type (power, cartridge) and add it to the entity manager.
  2. SystemAPI HasComponent and GetComponent is the proper 1.0 implementation of getting a reference to Entity data.
  3. Should I bother with the static PowerStorageComponent storage;? And do I need any property references outside the OnUpdate to be static?
  4. That I’m correctly grabbing the Entity for the PowerStorage and then decreasing the power when the Cartridge state is on. Also am I correctly using in or should it be ref for: in PowerEntityComponent powerEntity
  5. Entity powerEntity, in the cartridge system is the proper way to “Hold” a reference to a Entity that is “being used by” the cartridge system.
  6. I also feel like components like PowerStateComponent ([On, Off, Hybernate]) make PoweredComponent (Can power) obsolete 'If an Entity has a PowerStateComponent it means it can be powered, so am I adding too much data?
  7. Any other things I may be missing or should be doing differently?
public struct PowerStorageComponent : IComponentData
{
    public byte CurrentCharge;
    public byte Capacity;
}

public struct PowerOutputComponent : IComponentData
{
    public byte Value;
}

public partial class PowerSystem : SystemBase
{
    protected override void OnCreate()
    {
    }

    protected override void OnDestroy()
    {
    }

    protected override void OnUpdate()
    {
    }

    void CreateEntity(byte charge, byte capacity, byte powerOutput)
    {
        Entity entity = EntityManager.CreateEntity(
            typeof(PowerStorageComponent),
            typeof(PowerOutputComponent)
        );

        EntityManager.SetComponentData(entity, new PowerStorageComponent { CurrentCharge = charge, Capacity = capacity });
        EntityManager.SetComponentData(entity, new PowerOutputComponent { Value = powerOutput });
    }
}

public struct CartridgeReaderTypeComponent : IComponentData
{
    public enum ECartridgeType : byte
    {
        QuantumDataSlot,
        ChronoRelaySlot,
        NeuroLinkSlot,
        CryptoShieldSlot,
        GravitechSlot,
        BioGeneSlot,
        StealthDriveSlot,
        AquaPulseSlot,
        RetroWaveSlot,
        EcoFusionSlot
    }

    public ECartridgeType Value;
}

public struct PowerStateComponent : IComponentData
{
    public enum State : byte
    {
        PowerOn,
        PowerOff, 
        Hybernation
    }

    public State CurrentState;
}


public partial class CartridgeReaderSystem : SystemBase
{
    static PowerStorageComponent storage;
    protected override void OnCreate()
    {
    }

    protected override void OnDestroy()
    {
    }

    protected override void OnUpdate()
    {
        Entities.WithAll<PowerStateComponent, PowerConsumptionRateComponent, PowerEntityComponent>()
            .ForEach((
                Entity entity,
                ref PowerStateComponent powerState,
                ref PowerConsumptionRateComponent powerConsumptionRate,
                in PowerEntityComponent powerEntity) =>
            {
                if (powerState.CurrentState == PowerStateComponent.State.PowerOn)
                {
                    if (powerEntity.Value != Entity.Null)
                    {
                        if (SystemAPI.HasComponent<PowerStorageComponent>(powerEntity.Value))
                        {
                             storage = SystemAPI.GetComponent<PowerStorageComponent>(powerEntity.Value);
                            if (storage.CurrentCharge >= powerConsumptionRate.Value)
                            {
                                storage.CurrentCharge -= powerConsumptionRate.Value;
                            }
                            else
                            {
                                powerState.CurrentState = PowerStateComponent.State.PowerOff;
                            }
                        }

                    }
                }
            })
            .ScheduleParallel();
    }

    void CreateEntity(
        CartridgeReaderTypeComponent.ECartridgeType entityType,
        PowerStateComponent.State powerStateComponent,
        byte powerConsumptionRate,
        Entity powerEntity,
        Entity pluggedEntity
    )
    {
        Entity entity = EntityManager.CreateEntity(
            typeof(CartridgeReaderTypeComponent),
            typeof(PoweredComponent),
            typeof(PowerStateComponent),
            typeof(PowerConsumptionRateComponent),
            typeof(PowerEntityComponent),
            typeof(PluggableEntityComponent)
        );

        EntityManager.SetComponentData(entity, new CartridgeReaderTypeComponent { Value = entityType });
        EntityManager.SetComponentData(entity, new PowerStateComponent { CurrentState = powerStateComponent });
        EntityManager.SetComponentData(entity, new PowerConsumptionRateComponent { Value = powerConsumptionRate });
        EntityManager.SetComponentData(entity, new PowerEntityComponent { Value = powerEntity });
        EntityManager.SetComponentData(entity, new PluggableEntityComponent { Value = pluggedEntity });
}

Looks fine. One needs to know here that there are other ways of creating entities and some are faster than this one (if you ever need that in the future).


This SystemAPI is relatively new addition to ECS (it’s less than 2 years old I think). It works like a declaration of intent where source code-generation fills all the details automagically for you in hidden companion source files.
Before the time of SystemAPI we had a ComponentLookup<T> (SystemAPI likely uses that as well) and it is still a valid workflow of reading/writing to random components. If you ever try to use IJobEntity then you will come across it for sure.


RW access to a static field by multiple threads (.ScheduleParallel();) without locking creates a race condition; will produce different output on different computers or on different runs even - so let’s avoid that.

Note:
Unity’s job system with all it’s initially annoying/confusing (later: awesome) JobHandle dependency chains everywhere is a infrastructure built to solve this single problem and does that better than most of us ever could.


Negative. This code never writes to powerEntity.Value so no component changes value here really:

storage = SystemAPI.GetComponent<PowerStorageComponent>(powerEntity.Value);
if (storage.CurrentCharge >= powerConsumptionRate.Value)
{
    storage.CurrentCharge -= powerConsumptionRate.Value;
}
else
{
    powerState.CurrentState = PowerStateComponent.State.PowerOff;
}

Compare:

var storage = SystemAPI.GetComponent<PowerStorageComponent>(powerEntity.Value);
if (storage.CurrentCharge >= powerConsumptionRate.Value)
{
    storage.CurrentCharge -= powerConsumptionRate.Value;
    SystemAPI.SetComponent( powerEntity.Value , storage );
}

  • ref means RW (read-write) access
  • in means RO (read-only) access

Note: RW access flags component data T types as “dirty” internally and this wakes up systems with WithChangeFilter<T>(). So prefer RO access.


Yes. Components with Entity field are completely valid and widely used by existing systems (parent-child systems etc.).


I’m not 100% sure what is the intention for these 2 separate components but if you have a toggle,bool-like value of something then you may want to consider using IEnableableComponent. And if that component contains no value then it becomes a tag-component and takes close to zero space while still being useful on/off value.

if (storage.CurrentCharge >= powerConsumptionRate.Value)
{
    // ...
}
else SystemAPI.SetComponentEnabled<IsPowered>( entity , false );

No need to add components your read to the .WithAll<T1,...>() list. Use .WithAll<T1,...>() to add components you won’t read but your still require them to be on an entity.
Tag-components is a good use case for this - these won’t show up in ForEach(( here ) => {} because there is nothing to read there. For example: .WithAll<IsPowered>() or .WithAll<Prefab>() (to look for prefabs) are useful uses of this.

GIF 06.12.2023 22-48-49

Here is how I would do this:

using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;

namespace Sandbox
{
    public partial class CartridgeReaderSystem : SystemBase
    {
        protected override void OnCreate () {}
        protected override void OnDestroy () {}
        protected override void OnUpdate ()
        {
            Entities
                .WithAll<IsPowered>()
                .ForEach( ( Entity entity , in CartridgeReader cartridgeReader ) =>
                {
                    Debug.Log($"{nameof(CartridgeReader)} ({(entity.Index)},{(entity.Version)}) {cartridgeReader.Type} is powered and works!");
                } )
                .WithBurst()
                .ScheduleParallel();
        }
    }
}

Since this system is called CartridgeReaderSystem it’s better to makes sure it describes it’s sole responsibility and other responsibilities (like power consumption) are moved to separate, dedicated (as) single-responsibility (as one can) systems.

cartridge system components.cs

using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;

namespace Sandbox
{
    /// <summary>
    /// power storage unit
    /// </summary>
    public struct PowerStorage : IComponentData, IEnableableComponent// enableable component
    {
        public float Charge;
        public float Capacity;
        public float Output;
    }
    // note: to maximize efficient memory use cluster fields together in a components ONLY when they all are read together in a single job. For example: `Capacity` here is a candidate to be moved out to a separate component because it is not used together.

    /// <summary> points to an entity that will receive power </summary>
    [InternalBufferCapacity( 0 )]// 0 forces this buffer to be stored outside chunk (not next to other component data) as native collection
    public struct PowerLoadPointer : IBufferElementData
    {
        public Entity Value;
    }

    /// <summary> points back to a power storage </summary>
    public struct PowerSupplyPointer : IComponentData
    {
        public Entity Value;
    }

    public struct IsPowered : IComponentData, IEnableableComponent {}// enableable tag-component

    public struct PowerConsumptionRate : IComponentData
    {
        public float Value;
    }

    public struct CartridgeReader : IComponentData
    {
        public ECartridgeType Type;
    }

    public enum ECartridgeType : byte
    {
        UNDEFINED = 0 ,
        QuantumDataSlot ,
        ChronoRelaySlot ,
        NeuroLinkSlot ,
        CryptoShieldSlot ,
        GravitechSlot ,
        BioGeneSlot ,
        StealthDriveSlot ,
        AquaPulseSlot ,
        RetroWaveSlot ,
        EcoFusionSlot
    }

}

PowerConsumptionSystem.cs

using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Burst;

namespace Sandbox
{
    [BurstCompile]
    [UpdateInGroup( typeof(FixedStepSimulationSystemGroup) )]
    public partial struct PowerConsumptionSystem : ISystem
    {
		[BurstCompile]
		public void OnCreate ( ref SystemState state ) {}

		[BurstCompile]
		public void OnDestroy ( ref SystemState state ) {}

		[BurstCompile]
		public void OnUpdate ( ref SystemState state )
		{
            var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer( state.WorldUnmanaged );
            var ecbpw = ecb.AsParallelWriter();
            
            state.Dependency = new SeeIfPowerSupplyGotRechargedWhenDisabledJob{
                ECBPW = ecbpw ,
            }.Schedule( state.Dependency );

            state.Dependency = new PowerConsumptionJob{
                PowerConsumptionLookup = SystemAPI.GetComponentLookup<PowerConsumptionRate>( isReadOnly:true ) ,
                DeltaTime = SystemAPI.Time.DeltaTime ,
                ECBPW = ecbpw ,
            }.Schedule( state.Dependency );
        }

        [BurstCompile]
        partial struct PowerConsumptionJob : IJobEntity
        {
            [ReadOnly] public ComponentLookup<PowerConsumptionRate> PowerConsumptionLookup;
            [ReadOnly] public float DeltaTime;
            public EntityCommandBuffer.ParallelWriter ECBPW;
            public void Execute (
                [EntityIndexInQuery] int entityIndexInQuery ,
                in Entity entity ,
                ref PowerStorage powerStorage ,
                in DynamicBuffer<PowerLoadPointer> loads
            )
            {
                // @TODO: implement use of powerStorage.Output
                foreach( var loadPtr in loads )
                {
                    if( powerStorage.Charge>0 )
                    {
                        float consumptionRate = PowerConsumptionLookup[ loadPtr.Value ].Value;
                        powerStorage.Charge = math.max( powerStorage.Charge - consumptionRate * DeltaTime , 0 );
                    }
                    else
                    {
                        // power lost, disable everything and exit
                        foreach( var loadToDisablePtr in loads )
                        {
                            ECBPW.SetComponentEnabled<IsPowered>( entityIndexInQuery , loadToDisablePtr.Value , false );
                        }
                        ECBPW.SetComponentEnabled<PowerStorage>( entityIndexInQuery , entity , false );
                        return;
                    }
                }
            }
        }

        [WithDisabled( typeof(PowerStorage) )]
        [WithChangeFilter( typeof(PowerStorage) )]
        [BurstCompile]
        partial struct SeeIfPowerSupplyGotRechargedWhenDisabledJob : IJobEntity
        {
            public EntityCommandBuffer.ParallelWriter ECBPW;
            public void Execute (
                [EntityIndexInQuery] int entityIndexInQuery ,
                in Entity entity ,
                ref PowerStorage powerStorage ,
                in DynamicBuffer<PowerLoadPointer> loads
            )
            {
                if( powerStorage.Charge>0 )
                {
                    foreach( var loadToDisablePtr in loads )
                    {
                        ECBPW.SetComponentEnabled<IsPowered>( entityIndexInQuery , loadToDisablePtr.Value , true );
                    }
                    ECBPW.SetComponentEnabled<PowerStorage>( entityIndexInQuery , entity , true );
                }
            }
        }

    }

}

ISystems are harder to work with than SystemBase but I try to use them because these can be Burst-compiled to take as little main thread’s time as possible.

PowerComponentsMaintenanceSystem.cs

using Unity.Entities;
using Unity.Collections;
using Unity.Mathematics;
using Unity.Burst;

namespace Sandbox
{
    [BurstCompile]
    [UpdateInGroup( typeof(FixedStepSimulationSystemGroup) , OrderFirst=true )]
    public partial struct PowerComponentsMaintenanceSystem : ISystem
    {
        [BurstCompile]
		public void OnCreate ( ref SystemState state ) {}

		[BurstCompile]
		public void OnDestroy ( ref SystemState state ) {}

		[BurstCompile]
		public void OnUpdate ( ref SystemState state )
		{
            var ecb = SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer( state.WorldUnmanaged );
            var ecbpw = ecb.AsParallelWriter();
            var lookupIsPowered = SystemAPI.GetComponentLookup<IsPowered>( isReadOnly:true );
            var lookupPowerSupply = SystemAPI.GetComponentLookup<PowerSupplyPointer>( isReadOnly:true );
            var lookupPowerLoad = SystemAPI.GetBufferLookup<PowerLoadPointer>( isReadOnly:true );

            state.Dependency = new AddMissingComponentsJob{
                ECBPW = ecbpw ,
                LookupIsPowered = lookupIsPowered ,
                LookupPowerSupply = lookupPowerSupply ,
            }.ScheduleParallel( state.Dependency );

            state.Dependency = new RemoveUnusedComponentsJob{
                ECBPW = ecbpw ,
                LookupPowerLoad = lookupPowerLoad ,
            }.ScheduleParallel( state.Dependency );
        }

        [WithChangeFilter( typeof(PowerLoadPointer) )]
        [BurstCompile]
        partial struct AddMissingComponentsJob : IJobEntity
        {
            public EntityCommandBuffer.ParallelWriter ECBPW;
            [ReadOnly] public ComponentLookup<IsPowered> LookupIsPowered;
            [ReadOnly] public ComponentLookup<PowerSupplyPointer> LookupPowerSupply;
            public void Execute (
                [EntityIndexInQuery] int entityIndexInQuery ,
                in Entity entity ,
                in DynamicBuffer<PowerLoadPointer> loads
            )
            {
                foreach( var loadPtr in loads )
                {
                    Entity e = loadPtr.Value;
                    if( !LookupPowerSupply.HasComponent(e) )
                    {
                        ECBPW.AddComponent( entityIndexInQuery , e , new PowerSupplyPointer{
                            Value = entity
                        } );
                        
                    }
                    if( !LookupIsPowered.HasComponent(e) )
                    {
                        ECBPW.AddComponent<IsPowered>( entityIndexInQuery , e );
                    }
                }
            }
        }

        [WithChangeFilter( typeof(PowerSupplyPointer) )]
        [BurstCompile]
        partial struct RemoveUnusedComponentsJob : IJobEntity
        {
            public EntityCommandBuffer.ParallelWriter ECBPW;
            [ReadOnly] public BufferLookup<PowerLoadPointer> LookupPowerLoad;
            public void Execute (
                [EntityIndexInQuery] int entityIndexInQuery ,
                in Entity entity ,
                in PowerSupplyPointer powerSupplyPtr
            )
            {
                if( powerSupplyPtr.Value==Entity.Null || !LookupPowerLoad.HasBuffer(powerSupplyPtr.Value) )
                {
                    ECBPW.RemoveComponent<PowerSupplyPointer>( entityIndexInQuery , entity );
                }
                else
                {
                    bool found = false;
                    foreach( var loadPtr in LookupPowerLoad[powerSupplyPtr.Value] )
                    if( loadPtr.Value==entity )
                    {
                        found = true;
                        break;
                    }
                    if( !found )
                    {
                        ECBPW.RemoveComponent<PowerSupplyPointer>( entityIndexInQuery , entity );
                    }
                }
            }
        }
    }

}

CartridgeReaderAuthoring.cs

using UnityEngine;
using Unity.Entities;

namespace Sandbox.Authoring
{
    public class CartridgeReaderAuthoring : MonoBehaviour
    {
        
        [SerializeField] ECartridgeType _cartridgeType;
        [SerializeField][Min(0)] float _powerConsumptionRate = 1;

        public class Baker : Baker<CartridgeReaderAuthoring>
		{
			public override void Bake ( CartridgeReaderAuthoring authoring )
			{
				Entity entity = this.GetEntity( authoring , TransformUsageFlags.None );
                AddComponent( entity , new CartridgeReader{
                    Type = authoring._cartridgeType ,
                } );
                AddComponent( entity , new PowerConsumptionRate{
                    Value = authoring._powerConsumptionRate ,
                } );
            }
        }

    }
}

PowerSourceAuthoring.cs

using UnityEngine;
using Unity.Entities;

namespace Sandbox.Authoring
{
    public class PowerSourceAuthoring : MonoBehaviour
    {

        [SerializeField][Min(0)] float _capacity = 100;
        [SerializeField][Range(0,1)] float _currentCharge = 0.9f;
        [SerializeField][Min(0)] float _output = 10;
        [SerializeField] GameObject[] _loads = new GameObject[0];
        
        public class Baker : Baker<PowerSourceAuthoring>
		{
			public override void Bake ( PowerSourceAuthoring authoring )
			{
				Entity entity = this.GetEntity( authoring , TransformUsageFlags.None );
				
                AddComponent( entity , new PowerStorage{
                    Capacity = authoring._capacity ,
                    Charge = authoring._currentCharge * authoring._capacity ,
                    Output = authoring._output ,
                } );

                var powerLinks = AddBuffer<PowerLoadPointer>( entity );
                foreach( GameObject go in authoring._loads )
                {
                    powerLinks.Add( new PowerLoadPointer{
                        Value = GetEntity( go , TransformUsageFlags.None )
                    } );
                }
            }
        }

    }
}


Here is how I set it up for baking. Notice that reader (“device”) & power storage (“psu”) can be both a put on single entity/gameobject or on 2 separate ones - you just need to make sure Loads point to a “device” gameobject.


PS: Sorry I wouldn’t help with your questions earlier. I hope this helps a bit.

If you have trouble creating power & load entity setup, then here is a more familiar example to get you started:

using UnityEngine;
using Unity.Entities;

namespace Sandbox
{
    public class BasicTest : MonoBehaviour
    {
        
        EntityManager _entityManager;
        Entity _loadEntity, _powerEntity;

        void Start ()
        {
            _entityManager = World.DefaultGameObjectInjectionWorld.EntityManager;

            _loadEntity = _entityManager.CreateEntity();
            _entityManager.AddComponentData( _loadEntity , new PowerConsumptionRate{
                Value = 1 ,
            } );

            _powerEntity = _entityManager.CreateEntity();
            _entityManager.AddComponentData( _powerEntity , new PowerStorage{
                Capacity = 100 ,
                Charge = 100 ,
                Output = 10 ,
            } );
            var loads = _entityManager.AddBuffer<PowerLoadPointer>( _powerEntity );
            loads.Add( new PowerLoadPointer{
                Value = _loadEntity
            } );
            
            // NOTE:
            // this don't have to be 2 separate entities
            // you can fit these components on a single entity just fine if you want
        }

        void Update ()
        {
            var powerStorage = _entityManager.GetComponentData<PowerStorage>( _powerEntity );
            Debug.Log($"{nameof(PowerStorage)} ({_powerEntity.Index},{_powerEntity.Version}) at {powerStorage.Charge:0.00} charge");
        }

    }
}