Skip to content

Missile Command in Unity ECS Part 2: Systems

In part two of this article, I describe the systems that operate on entities which required to create in the classic game Missile Command.  Check out part 1 to learn about the entity data.

In this series, I take games and rebuild them using Unity’s Entity Component System (ECS)

Play it in WebGL here.  For full source code of this project, checkout my github

Table of Contents

Missile Creation and Movement

In order to create a challenge in the game, we have to spawn the enemy missiles in randomized positions and move them directly toward the player’s buildings.

public class MissileSpawnSystem : SystemBase
{
    protected override void OnUpdate()
    {
        EntityQuery query = EntityManager.CreateEntityQuery(typeof(GameSettings));
        if (query.CalculateEntityCount() == 0) return;
        
        GameSettings settings = EntityManager.CreateEntityQuery(typeof(GameSettings)).GetSingleton<GameSettings>();
        
        if (m_spawnTimer >= settings.spawnRate)
        {
            EntityQuery buildingQuery = EntityManager.CreateEntityQuery(typeof(Building), typeof(Translation));
            if (buildingQuery.CalculateEntityCount() == 0)
            {
                // all buildings have been destroyed, game over
                return;
            }
    
            NativeArray<Translation> buildingPositions = buildingQuery.ToComponentDataArray<Translation>(Allocator.TempJob);                
            for (int i = 0; i < settings.spawns; i++)
            {
                Entity missile = EntityManager.Instantiate(GamePrefabsAuthoring.Missile);
                // randomize x pos
                Translation pos = EntityManager.GetComponentData<Translation>(missile);
                pos.Value.x = m_random.NextFloat(settings.posMin, settings.posMax);
                pos.Value.y = settings.spawnYPos;
                pos.Value.z = 0;
                EntityManager.SetComponentData<Translation>(missile, pos);
    
                // randomize speed
                Speed s = EntityManager.GetComponentData<Speed>(missile);
                s.value = m_random.NextFloat(settings.speedMin, settings.speedMax);
                EntityManager.SetComponentData<Speed>(missile, s);

                // move the missile towards a random building
                Rotation rot = EntityManager.GetComponentData<Rotation>(missile);
                Translation target = buildingPositions[m_random.NextInt(buildingPositions.Length)];
                float3 dir = math.normalize(target.Value - pos.Value);
                EntityManager.SetComponentData<Direction>(missile, new Direction { value = dir });
                //float z = math.degrees(math.atan2(dir.y, dir.x));
                
            }
            m_spawnTimer = 0;
            buildingPositions.Dispose();
        }
        m_spawnTimer += Time.DeltaTime;
    }
} 

Each of the missiles has a direction to move along, so now we used a system to move along that direction 

public class MissileMovementSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float deltaTime = World.Time.DeltaTime;
        Entities.ForEach((ref Translation translation, in Direction direction, in Speed speed) =>
        {
            // move straight along the missile direction
            translation.Value += direction.value * speed.value * deltaTime;
        }).Schedule();
    }
} 

Defense Creation, Growth, and Destruction

The player can click on the screen to spawn defense entities that will grow in size and remain on screen for a few seconds. If the bounding volumes of the missile and the defense overlap, the missile is destroyed.  

public class DefenseSpawnSystem : SystemBase
{
    EntityQueryDesc playerQueryDesc;
    protected override void OnCreate()
    {
        base.OnCreate();
        playerQueryDesc = new EntityQueryDesc()
        {
            All = new ComponentType[] { typeof(Player), typeof(AttackSpeed), typeof(Ready) }
        };
    }

    protected override void OnUpdate()
    {
        EntityQuery query = EntityManager.CreateEntityQuery(playerQueryDesc);
        if (query.CalculateEntityCount() == 0) return;

        Entity player = query.GetSingletonEntity();
        Ready playerReadiness = query.GetSingleton<Ready>();
        if (playerReadiness.value && Input.GetMouseButtonDown(0))
        {
            // the player's reload timer is up and they've clicked the mouse

            // spawn the defense entity and position it where the mouse is
            Entity defense = EntityManager.Instantiate(GamePrefabsAuthoring.Defense);
            Translation defensePos = EntityManager.GetComponentData<Translation>(defense);
            defensePos.Value = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            defensePos.Value.z = 0;
            EntityManager.SetComponentData<Translation>(defense, defensePos);
            playerReadiness.value = false;
            EntityManager.SetComponentData<Ready>(player, playerReadiness);
        }
    }
} 

We use a simple sine wave to make the defense grow over time

public class DefenseGrowthSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float dt = World.Time.DeltaTime;

        // use a sine wave to change the scale of the defense over time
        Entities.ForEach((ref NonUniformScale scale, ref Radius radius, ref Wave wave) =>
        {
            scale.Value = wave.amplitude * math.sin(wave.frequency * wave.time + wave.phase);
            radius.value = scale.Value.y;
            wave.time += dt; 

        }).Schedule();
    }
} 

The defense only lasts for a given number of seconds, so we need a system to countdown the lifetime

[UpdateInGroup(typeof(LateSimulationSystemGroup))]
public class LifetimeCountdownSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float dt = World.Time.DeltaTime;
        Entities.ForEach((ref Lifetime lifetime, ref DeletionMark mark) =>
        {
            lifetime.value -= dt;
            if (lifetime.value <= 0)
            {
                mark.value = 1;
            }
        }).Schedule();
    }
}

[UpdateInGroup(typeof(LateSimulationSystemGroup))]
[UpdateAfter(typeof(LifetimeCountdownSystem))]
public class DeletionMarkSystem : SystemBase
{
    protected override void OnUpdate()
    {
        Entities
            .WithStructuralChanges()
            .WithoutBurst()
            .ForEach((Entity e, in DeletionMark mark) =>
        {
            if (mark.value != 0)
            {
                EntityManager.DestroyEntity(e);
            }
        }).Run();
        //handle.Complete();
    }
} 

Collision

When the bounding volumes of the missile and the defense overlap, the missile is destroyed. 

When the bounding volumes of the missile and the building overlap, the missile is destroyed and the health of the building is reduced by the amount of damage the building does. 

public class MissileCollisionSystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimCommandBufferSystem;
    protected override void OnCreate()
    {
        base.OnCreate();
        endSimCommandBufferSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        // find all of the buildings
        NativeArray<Entity> buildings = EntityManager.CreateEntityQuery(
            typeof(Building),
            typeof(HealthInt),
            typeof(Radius))
            .ToEntityArray(Allocator.TempJob);

        if (buildings.Length == 0)
        {
            buildings.Dispose();
            return;
        }

        // find all the defenses
        NativeArray<Entity> defenses = EntityManager.CreateEntityQuery(
            typeof(Defense),
            typeof(Radius))
            .ToEntityArray(Allocator.TempJob);

        Entity score = EntityManager.CreateEntityQuery(typeof(Score)).GetSingletonEntity();
        
        Entities                
            .WithReadOnly(buildings)
            .WithReadOnly(defenses)
            .WithDeallocateOnJobCompletion(buildings)
            .WithDeallocateOnJobCompletion(defenses)                
            .WithAll<Missile>()
            .ForEach((Entity missile, int entityInQueryIndex, ref Direction dir, ref DeletionMark mark,
                in Translation translation, in Radius radius, in Damage damage) =>
        {
            for (int i = 0; i < buildings.Length; i++)
            {
                Radius buildingRadius = GetComponentDataFromEntity<Radius>(true)[buildings[i]];
                Translation buildingPos = GetComponentDataFromEntity<Translation>(true)[buildings[i]];

                if (math.distance(translation.Value, buildingPos.Value) <= radius.value + buildingRadius.value)
                {
                    // hit the building, reduce health
                    HealthInt health = GetComponentDataFromEntity<HealthInt>(false)[buildings[i]];
                    health.curr -= (int)damage.value;
                    SetComponent<HealthInt>(buildings[i], health);                        
                    mark.value = 1;
                }
            }
            
            for (int i = 0; i < defenses.Length; i++)
            {
                NonUniformScale defenseRadius = GetComponentDataFromEntity<NonUniformScale>(true)[defenses[i]];
                Translation defensePos = GetComponentDataFromEntity<Translation>(true)[defenses[i]];
                if (math.distance(translation.Value, defensePos.Value) <= radius.value + defenseRadius.Value.x * 0.25f)
                {
                    // missile is overlapping the defense, mark the missile as destroyed and the player scores a point
                    mark.value = 1;
                    Score s = GetComponent<Score>(score);
                    s.value += 1;
                    SetComponent<Score>(score, s);
                }
            }

            if (translation.Value.y < -3.5f)
            {
                // below the ground
                mark.value = 1;
            }
            

        }).Schedule();
    }
}