Samuli Natri - Software Developer
menu

Unity ECS - Custom Components

How to trigger "poisoned" effect and make enemies follow the player. All systems are jobified.

Notes:

 Bootstrap.cs

using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine;

public class Bootstrap
{
    public static MeshInstanceRenderer DeadLook;
    public static MeshInstanceRenderer PlayerPoisonedLook;
    public static MeshInstanceRenderer TigerPoisonedLook;
    
    private static EntityArchetype _playerArchetype;
    private static EntityArchetype _tigerArchetype;
    private static EntityArchetype _snakeArchetype;

    private const int Amount = 5;

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    public static void Init()
    {
        var entityManager = World.Active.GetOrCreateManager<EntityManager>();
        CreateArchetypes(entityManager);
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
    public static void InitWithScene()
    {
        var entityManager = World.Active.GetOrCreateManager<EntityManager>();
        CreateActors(entityManager);
    }

    private static void CreateActors(EntityManager entityManager)
    {
        var player = entityManager.CreateEntity(_playerArchetype);
        entityManager.SetSharedComponentData(player, GetLook("PlayerLook"));
        entityManager.SetComponentData(player, new Position {Value = new float3(-5.0f, 5.0f, 0.0f)});
        entityManager.SetComponentData(player, new Health {Value = 1000});
        entityManager.SetComponentData(player, new Extra {Type = 0, Sick = 0});
        
        PlayerPoisonedLook = GetLook("PlayerPoisonedLook");
        TigerPoisonedLook = GetLook("TigerPoisonedLook");
        DeadLook = GetLook("DeadLook");

        for (int i = 0; i < Amount; i++)
        {
            var tiger = entityManager.CreateEntity(_tigerArchetype);
            entityManager.SetSharedComponentData(tiger, GetLook("TigerLook"));
            entityManager.SetComponentData(tiger, new Health {Value = 1500});
            entityManager.SetComponentData(tiger, new Extra {Type = 1, Sick = 0});
            entityManager.SetComponentData(tiger, 
                new Position {Value = new float3(Random.Range(6.0f, 10.0f), 
                    Random.Range(-2.0f, 8.0f), 0.0f)});
        }
        
        for (int i = 0; i < Amount; i++)
        {
            var snake = entityManager.CreateEntity(_snakeArchetype);
            entityManager.SetSharedComponentData(snake, GetLook("SnakeLook"));
            entityManager.SetComponentData(snake, new Health {Value = 1500});
            entityManager.SetComponentData(snake, new Extra {Type = 2});
            entityManager.SetComponentData(snake, 
                new Position {Value = new float3(Random.Range(0.0f, 2.0f), 
                    Random.Range(-2.0f, 8.0f), 0.0f)});
        }
    }

    private static void CreateArchetypes(EntityManager entityManager)
    {
        _playerArchetype = entityManager.CreateArchetype(
            typeof(TransformMatrix),
            typeof(Position),
            typeof(MeshInstanceRenderer),
            typeof(PlayerInput),
            typeof(Alive),
            typeof(Health),
            typeof(Extra)
            
        );

        _tigerArchetype = entityManager.CreateArchetype(
            typeof(TransformMatrix),
            typeof(Position),
            typeof(MeshInstanceRenderer),
            typeof(Alive),
            typeof(Tiger),
            typeof(Health),
            typeof(Extra)
        );

        _snakeArchetype = entityManager.CreateArchetype(
            typeof(TransformMatrix),
            typeof(Position),
            typeof(MeshInstanceRenderer),
            typeof(Alive),
            typeof(Snake),
            typeof(Health),
            typeof(Extra)
        );
    }

    private static MeshInstanceRenderer GetLook(string protoType)
    {
        var prototype = GameObject.Find(protoType);
        var look = prototype.GetComponent<MeshInstanceRendererComponent>().Value;
        Object.Destroy(prototype);
        return look;
    }
}

Components.cs

using Unity.Entities;
using Unity.Mathematics;

public struct Tiger : IComponentData {}
public struct Snake : IComponentData {}
public struct Poisoned : IComponentData {}
public struct Alive : IComponentData {}

public struct PlayerInput : IComponentData
{
    public float2 Move;
}

public struct Health : IComponentData
{
    public int Value;
}

public struct Extra : IComponentData
{
    public int Type;
    public int Sick;
}

 

PlayerInputSystem.cs

using Unity.Burst;
using Unity.Entities;
using Unity.Jobs;
using UnityEngine;

public class PlayerInputSystem : JobComponentSystem
{
    [BurstCompile]
    private struct PlayerInputJob : IJobProcessComponentData<PlayerInput>
    {
        public float Horizontal;
        public float Vertical;

        public void Execute(ref PlayerInput input)
        {
            input.Move.x = Horizontal;
            input.Move.y = Vertical;
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new PlayerInputJob
        {
            Horizontal = Input.GetAxis("Horizontal"),
            Vertical = Input.GetAxis("Vertical")
        };

        return job.Schedule(this, 1, inputDeps);
    }
}

PlayerMovementSystem.cs

using Unity.Burst;
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Jobs;

public class PlayerMovementSystem : JobComponentSystem
{   
    [BurstCompile]
    private struct PlayerMovementJob : IJobProcessComponentData<Position, PlayerInput, Alive>
    {	
        public float Dt;
        public float Speed;
		
        public void Execute(ref Position position, ref PlayerInput playerInput, ref Alive alive)
        {
            position.Value += Dt * Speed * new float3(playerInput.Move.x, playerInput.Move.y, 0.0f);
        }
    }
    
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new PlayerMovementJob
        {
            Speed = 5.0f,
            Dt = Time.deltaTime
        };

        return job.Schedule(this, 1, inputDeps);
    }
}

TigerCollisionSystem.cs

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

public class TigerCollisionSystem : JobComponentSystem
{ 
    private class CollisionBarrier : BarrierSystem
    {
    }
    
    private struct PlayerGroup
    { 
        [ReadOnly] public ComponentDataArray<Position> Position;
        [ReadOnly] public ComponentDataArray<PlayerInput> Input;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public EntityArray PlayerEntity;
        public int Length;
    }
    
    private struct TigerGroup
    {
        [ReadOnly] public ComponentDataArray<Position> Position;
        [ReadOnly] public ComponentDataArray<Tiger> Tiger;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public EntityArray TigerEntity;
        public int Length;
    }

    [Inject] private CollisionBarrier _collisionBarrier;
    [Inject] private PlayerGroup _playerGroup;
    [Inject] private TigerGroup _TigerGroup;
    
    /*[BurstCompile]*/
    private struct TigerCollisionJob : IJob
    {
        [ReadOnly] public ComponentDataArray<Position> TigerPosition;
        [ReadOnly] public ComponentDataArray<Position> PlayerPosition;
        [ReadOnly] public EntityArray PlayerEntity;
        [ReadOnly] public EntityArray TigerEntity;
        public EntityCommandBuffer Commands;

        public void Execute()
        {
            for (int i = 0; i < PlayerEntity.Length; i++)
            {
                for (int j = 0; j < TigerEntity.Length; j++)
                {
                    float dist = math.distance(PlayerPosition[i].Value, TigerPosition[j].Value);

                    if (dist < 1.0f)
                    {
                        Commands.SetComponent(PlayerEntity[i], new Health{ Value = -10 });
                   /*     Commands.RemoveComponent<Alive>(PlayerEntity[i]);
                        Commands.SetSharedComponent(PlayerEntity[i], Bootstrap.DeadLook);*/
                    }
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return new TigerCollisionJob()
        {
            PlayerEntity = _playerGroup.PlayerEntity,
            TigerEntity = _TigerGroup.TigerEntity,
            TigerPosition = _TigerGroup.Position,
            PlayerPosition = _playerGroup.Position,
            Commands = _collisionBarrier.CreateCommandBuffer(),
        }.Schedule(inputDeps);
    }
}

TigerMovementSystem.cs

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;

public class TigerMovementSystem : JobComponentSystem
{

    private struct PlayerGroup
    {
        [ReadOnly] public ComponentDataArray<PlayerInput> Input;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public EntityArray PlayerEntity;
        public int Length;
    }

    private struct TigerGroup
    {
        [ReadOnly] public ComponentDataArray<Tiger> Tiger;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public EntityArray TigerEntity;
        public int Length;
    }

    [Inject] private PlayerGroup _playerGroup;
    [Inject] private TigerGroup _tigerGroup;

    [Inject] private ComponentDataFromEntity<Position> _position;

    [BurstCompile]
    private struct TigerMovementJob : IJob
    {
        public float Dt;
        public float Speed;
        public ComponentDataFromEntity<Position> Position;
        [ReadOnly] public EntityArray PlayerEntity;
        [ReadOnly] public EntityArray TigerEntity;

        public void Execute()
        {
            for (int t = 0; t < TigerEntity.Length; t++)
            {
                for (int p = 0; p < PlayerEntity.Length; p++)
                {
                    var dir = math.normalize(Position[PlayerEntity[p]].Value - Position[TigerEntity[t]].Value);
                    var tigerPos = Position[TigerEntity[t]];
                    tigerPos.Value += dir * Speed * Dt;
                    Position[TigerEntity[t]] = tigerPos;
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return new TigerMovementJob()
        {
            Dt = Time.deltaTime,
            Speed = 2.0f,
            Position = _position,
            PlayerEntity = _playerGroup.PlayerEntity,
            TigerEntity = _tigerGroup.TigerEntity,
        }.Schedule(inputDeps);
    }
}

 SnakeCollisionSystem.cs

using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Jobs;

public class SnakeCollisionSystem : JobComponentSystem
{ 
    private class CollisionBarrier : BarrierSystem
    {
    }
    
    private struct ActorGroup
    { 
        [ReadOnly] public ComponentDataArray<Position> Position;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public SubtractiveComponent<Poisoned> Poisoned;
        [ReadOnly] public SubtractiveComponent<Snake> Snake;
        [ReadOnly] public EntityArray ActorEntity;
        public int Length;
    }
    
    private struct SnakeGroup
    {
        [ReadOnly] public ComponentDataArray<Position> Position;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
        [ReadOnly] public ComponentDataArray<Snake> Snake;
        [ReadOnly] public EntityArray SnakeEntity;
        public int Length;
    }

    [Inject] private CollisionBarrier _collisionBarrier;
    [Inject] private ActorGroup _actorGroup;
    [Inject] private SnakeGroup _snakeGroup;

    [Inject] private ComponentDataFromEntity<Extra> _extra;
    
    /*[BurstCompile]*/
    private struct SnakeCollisionJob : IJob
    {
        [ReadOnly] public EntityArray ActorEntity;
        [ReadOnly] public EntityArray SnakeEntity;
        [ReadOnly] public ComponentDataArray<Position> ActorPosition;
        [ReadOnly] public ComponentDataArray<Position> SnakePosition;
        public ComponentDataFromEntity<Extra> Extra;
        
        public EntityCommandBuffer Commands;

        public void Execute()
        {
            for (int a = 0; a < ActorEntity.Length; a++)
            {
                for (int s = 0; s < SnakeEntity.Length; s++)
                {
                    float dist = math.distance(ActorPosition[a].Value, SnakePosition[s].Value);

                    if (dist < 1.0f)
                    {          
                        var extra = Extra[ActorEntity[a]];

                        if (extra.Sick != 1)
                        {
                            Commands.AddComponent(ActorEntity[a], new Poisoned());
                            extra.Sick = 1;
                            Extra[ActorEntity[a]] = extra;
                        }
                        
                        switch (extra.Type)
                        {
                            case 0:
                                Commands.SetSharedComponent(ActorEntity[a], Bootstrap.PlayerPoisonedLook);
                                break;
                            case 1:
                                Commands.SetSharedComponent(ActorEntity[a], Bootstrap.TigerPoisonedLook);
                                break;
                        }
                        
                        Commands.SetComponent(SnakeEntity[s], new Health{ Value = -10 });
                    }
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return new SnakeCollisionJob()
        {
            ActorEntity = _actorGroup.ActorEntity,
            SnakeEntity = _snakeGroup.SnakeEntity,
            ActorPosition = _actorGroup.Position,
            SnakePosition = _snakeGroup.Position,
            Extra = _extra,
            Commands = _collisionBarrier.CreateCommandBuffer(),
        }.Schedule(inputDeps);
    }
}

PoisonedSystem.cs

using Unity.Entities;
using Unity.Burst;
using Unity.Jobs;

public class PoisonedSystem: JobComponentSystem
{
    [BurstCompile]
    private struct PoisonedJob : IJobProcessComponentData<Health, Poisoned>
    {
        public void Execute(ref Health health, ref Poisoned poisoned)
        {
            health.Value -= 5;
        }
    }
    
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        var job = new PoisonedJob{};
				
        return job.Schedule(this, 1, inputDeps);
    }
}

DeadSystem.cs

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;

public class DeadSystem : JobComponentSystem
{
    private class DeleteBarrier : BarrierSystem
    {
    }

    private struct Data
    {
        [ReadOnly] public EntityArray Entity;
        [ReadOnly] public ComponentDataArray<Health> Health;
        [ReadOnly] public ComponentDataArray<Alive> Alive;
    }

    [Inject] private Data _data;
    [Inject] private DeleteBarrier _deleteBarrier;

    /*[BurstCompile]*/
    private struct DeleteJob : IJob
    {
        [ReadOnly] public EntityArray Entity;
        [ReadOnly] public ComponentDataArray<Health> Health;
        public EntityCommandBuffer Commands;

        public void Execute()
        {
            for (int i = 0; i < Entity.Length; ++i)
            {
                if (Health[i].Value < 0.0f)
                {
                    Commands.RemoveComponent<Alive>(Entity[i]);
                    Commands.SetSharedComponent(Entity[i], Bootstrap.DeadLook);
                }
            }
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        return new DeleteJob
        {
            Entity = _data.Entity,
            Health = _data.Health,
            Commands = _deleteBarrier.CreateCommandBuffer(),
        }.Schedule(inputDeps);
    }
}