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();
}
}