Polymorphism is one of those OOP foundation that’s hard to shake off. It’s easy to do and it’s very intuitive. We use it to refactor code to make it more organized. We use it to manage different behaviors while maintaining only a single interface. We also use it to make full blown authoring editors that affects runtime behavior by complementing it with reflection.
However, it can’t be done in an environment where reference types are not allowed (Unity’s HPC#). It can be replicated in another way in ECS and that’s what is this post about.
OOP Version
Let’s say we have a framework of projectiles. Let’s also say that our game’s world is steam punk with magic. We want to be able to support both bullets and magic projectiles like fireball. Our OOP code might look like this:
public abstract class Projectile {
// Common projectile properties
protected Vector2 position;
private int damage;
// Each projectile type may implement its own movement
public abstract void Move();
// Each projectile might have different effects on impact
public abstract void OnImpact();
public int Damage {
get {
return this.damage;
}
}
}
public class Bullet : Projectile {
private readonly Vector2 direction;
private readonly float speed;
public Bullet(Vector2 direction, float speed) {
this.direction = direction.normalized;
this.speed = speed;
}
public override void Move() {
// Move by speed in straight line
this.position += this.speed * Time.deltaTime * this.direction;
}
public override void OnImpact() {
// Maybe just destroy the bullet here
}
}
public class Fireball : Projectile {
private readonly float initialVelocity;
private readonly float angle;
private readonly float gravity;
private readonly float vX;
private readonly float vYPart;
private float polledTime;
public Fireball(float initialVelocity, float angle, float gravity) {
this.initialVelocity = initialVelocity;
this.angle = angle;
this.gravity = gravity;
// Cache
this.vX = this.initialVelocity * Mathf.Cos(this.angle);
this.vYPart = this.initialVelocity * Mathf.Sin(this.angle);
}
public override void Move() {
// Move by projectile motion
// There are better ways to do this but just bare with me
this.polledTime += Time.deltaTime;
// Update X
this.position.x += this.vX * Time.deltaTime;
// Update Y
float vY = this.vYPart - this.gravity * this.polledTime;
this.position.y += vY * Time.deltaTime;
}
public override void OnImpact() {
// Destroy the projectile then send a request to show a fireball impact particle effect
// at the current position
}
}
Then we may implement the class that handles projectiles like this:
public class ProjectileManager {
private readonly List<Projectile> projectiles = new List<Projectile>();
public void Add(Projectile projectile) {
this.projectiles.Add(projectile);
}
public void Update() {
for (int i = 0; i < this.projectiles.Count; ++i) {
this.projectiles[i].Move();
}
CheckForCollisions();
}
private void CheckForCollisions() {
// Let's just say a list of collisions exists
foreach(Collision c in this.collisions) {
// Apply damage if health component exists
if(c.Health != null) {
c.Health.Value -= c.Projectile.Damage;
}
// Execute custom on impact routines
c.Projectile.OnImpact();
}
}
}
This contrived projectile system should be easy to follow. There are bullets that move in straight line at a certain direction and fireballs that move in projectile motion. Both of them can be handled by ProjectileManager as they inherit the Projectile base class.
ECS Version
In Unity’s pure ECS, classes can’t be used and inheritance is certainly not available either. But I consider that a good thing as a different mindset is needed in ECS. We must forget OOP when modelling game elements using ECS.
Let’s start with our projectile component:
public struct Projectile : IComponentData {
public float2 position;
public readonly int damage;
public Projectile(float2 position, int damage) {
this.position = position;
this.damage = damage;
}
}
Our intent with this component is that any entity that has this is considered as a projectile. This component can be used by systems to filter only entities with such component and then execute general or common logic that can be applied to all projectiles. Think of it as code found in the base class.
Next are the components that represents the subclasses:
public struct Bullet : IComponentData {
public readonly float2 direction;
public readonly float speed;
public Bullet(float2 direction, float speed) {
this.direction = math.normalize(direction);
this.speed = speed;
}
}
public struct Fireball : IComponentData {
public readonly float initialVelocity;
public readonly float angle;
public readonly float gravity;
public readonly float vX;
public readonly float vYPart;
public float polledTime;
public Fireball(float initialVelocity, float angle, float gravity) {
this.initialVelocity = initialVelocity;
this.angle = angle;
this.gravity = gravity;
// Cache
this.vX = this.initialVelocity * math.cos(this.angle);
this.vYPart = this.initialVelocity * math.sin(this.angle);
this.polledTime = 0;
}
}
We just moved their data to their own components. To model a bullet projectile, we create an entity with Projectile and Bullet components. The same is true for a fireball projectile.
// Create a bullet
Entity bullet = entityManager.CreateEntity(typeof(Projectile), typeof(Bullet));
// Create a fireball
Entity fireball = entityManager.CreateEntity(typeof(Projectile), typeof(Fireball));
From here, we can then define the different movement systems:
// Using ComponentSystem here instead of JobComponentSystem so that it's
// easier to understand
public class BulletMoveSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), typeof(Bullet));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Bullet bullet) {
projectile.position = projectile.position + (bullet.speed * Time.deltaTime * bullet.direction);
});
}
}
public class FireballMoveSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), typeof(Fireball));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Fireball fireball) {
// Move by projectile motion
fireball.polledTime += Time.deltaTime;
float2 newPosition = projectile.position;
newPosition.x += fireball.vX * Time.deltaTime;
float vY = fireball.vYPart - fireball.gravity * fireball.polledTime;
newPosition.y += vY * Time.deltaTime;
projectile.position = newPosition;
});
}
}
To handle the different “on impact” logic, a separate system could handle checking the collision detection then add a Collided tag component to entities that have collided. Separate systems would then handle damage dealing and “on impact” routines. Here’s the system that handles collision detection:
// Component that is added to entities that have collided
public struct Collided : IComponentData {
public readonly Entity other; // The other entity that we collided with
}
public class ProjectileCollisionDetectionSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), ComponentType.Exclude<Collided>());
}
protected override void OnUpdate() {
// Adds Collided component to entities that have collided
}
}
The system that applies damage could look like this:
// Component representing health
public struct Health : IComponentData {
public int amount;
}
public class ProjectileDamageSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), typeof(Collided));
}
protected override void OnUpdate() {
ComponentDataFromEntity<Health> allHealth = GetComponentDataFromEntity<Health>();
this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile, ref Collided collided) {
// Apply damage
Health health = allHealth[collided.other];
health.amount -= projectile.damage;
allHealth[collided.other] = health; // Modify
});
}
}
The “on impact” routines can also be implemented in their own systems:
public class BulletOnImpactSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), typeof(Collided), typeof(Bullet));
}
protected override void OnUpdate() {
// Just destroy them
this.EntityManager.DestroyEntity(this.query);
}
}
public class FireballOnImpactSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Projectile), typeof(Collided), typeof(Fireball));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Projectile projectile) {
// Request fireball particle effect at projectile.position
});
// Then destroy them
this.EntityManager.DestroyEntity(this.query);
}
}
At this point, we have replicated the logic from OOP to its ECS version.
The more ideal ECS solution
From our initial refactoring, we can make changes to some components to make them more reusable.
One area that we can improve is movement. Instead of using Bullet component for straight direction movement, why not define it to its own component like StraightDirectionMovement. This way, we can reuse it to other elements in the game that requires this kind of movement. Before we can do this, we also need to remove the position property from Projectile and use a separate component representing it. This is what the new movement system will look like:
// Holds the projectile's position
public struct Position : IComponentData {
public float2 value;
}
public struct StraightDirectionMovement : IComponentData {
public readonly float2 direction;
public readonly float speed;
public StraightDirectionMovement(float2 direction, float speed) {
this.direction = math.normalize(direction);
this.speed = speed;
}
}
public class StraightDirectionMovementSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Position), typeof(StraightDirectionMovement));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Position position, ref StraightDirectionMovement movement) {
position.value = position.value + (movement.speed * Time.deltaTime * movement.direction);
});
}
}
In the same way, Fireball’s projectile motion movement could also turned into its own component. Say we call it ProjectileMotionMovement:
public struct ProjectileMotionMovement : IComponentData {
public readonly float initialVelocity;
public readonly float angle;
public readonly float gravity;
public readonly float vX;
public readonly float vYPart;
public float polledTime;
public ProjectileMotionMovement(float initialVelocity, float angle, float gravity) {
this.initialVelocity = initialVelocity;
this.angle = angle;
this.gravity = gravity;
// Cache
this.vX = this.initialVelocity * math.cos(this.angle);
this.vYPart = this.initialVelocity * math.sin(this.angle);
this.polledTime = 0;
}
}
public class ProjectileMotionMovementSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Position), typeof(ProjectileMotionMovement));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Position position, ref ProjectileMotionMovement movement) {
// Move by projectile motion
movement.polledTime += Time.deltaTime;
float2 newPosition = position.value;
newPosition.x += movement.vX * Time.deltaTime;
float vY = movement.vYPart - movement.gravity * movement.polledTime;
newPosition.y += vY * Time.deltaTime;
position.value = newPosition;
});
}
}
Another dimension that we can improve is the routines on impact. Instead of having BulletOnImpactSystem which only works on entities with Bullet component, why not make it more reusable. Let’s use a component named DestroyOnCollision instead:
// A tag component that identifies an entity to be removed on collision
public struct DestroyOnCollision : IComponentData {
}
public class DestroyOnCollisionSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Collided), typeof(DestroyOnCollision));
}
protected override void OnUpdate() {
// Just destroy them
this.EntityManager.DestroyEntity(this.query);
}
}
Requesting a particle effect like what happens for fireball’s impact could also be its own component and system. Let’s say we have a component named RequestParticleEffectOnCollision:
public struct RequestParticleEffectOnCollision : IComponentData {
// Used to identify what particle effect to deploy
public readonly int effectId;
public RequestParticleEffectOnCollision(int effectId) {
this.effectId = effectId;
}
}
[UpdateBefore(typeof(DestroyOnCollisionSystem))]
public class RequestParticleEffectOnCollisionSystem : ComponentSystem {
private EntityQuery query;
protected override void OnCreate() {
this.query = GetEntityQuery(typeof(Position), typeof(Collided), typeof(RequestParticleEffectOnCollision));
}
protected override void OnUpdate() {
this.Entities.With(this.query).ForEach(delegate(ref Position position, ref RequestParticleEffectOnCollision effectRequest) {
// Request the particle effect at position.value
});
// Destruction of the entity will now be handled by DestroyOnCollisionSystem
}
}
With the systems above in place, modelling a bullet object in the game will now look like this:
Entity bullet = entityManager.CreateEntity(typeof(Position),
typeof(Projectile),
typeof(StraightDirectionMovement),
typeof(DestroyOnCollision));
Notice how we have completely removed the concept of “Bullet”. A bullet is now composed of components that make up its behavior. It’s also the same for Fireball:
Entity fireball = entityManager.CreateEntity(typeof(Position),
typeof(Projectile),
typeof(ProjectileMotionMovement),
typeof(RequestParticleEffectOnCollision),
typeof(DestroyOnCollision));
Last thoughts
It’s obvious to see that turning OOP to ECS requires more code. All I can say to that is… it is what it is. Unfortunately, we’re just using C# constructs to kind of model ECS. There’s no such thing as an ECS aware programming language (yet) that will dramatically reduce all this code. I consider it a trade off. I get the benefit of highly modular and efficient code but at the expense of verbosity.
Honestly, verbosity is not a steep price to pay. I get to have code that could be as fast as it can be without switching to another more complex code like C++, which is verbose by itself.
If you like my posts, please subscribe to my mailing list and be among the first to know what I’m up to. I’ll send you a free game upon subscription. 🙂
Greetings Marnel,
Thanks for this thoughtful post. I wonder if you have any tests/speed comparisons, with doing the code this way, for use in ECS. My only question is: (from your past posts I assume that ECS is so much faster) but what is the ratio of previously using C# OOP code vs. using the transposed/transformed code for ECS to accomplish the same thing?
Just curious, and thank-you for maintaining this blog and for the learning you share with it.
Justin of JustinTime Studios
LikeLike
For our game, we only ported some systems like rendering and pathfinding. Maybe that’s about 5-10% of code. It’s unrealistic to turn the whole game into pure ECS. We’ll have to rewrite most of it from scratch which is irrational to do at this point.
LikeLike
A quick question about your final two blocks of code where you’re actually building the bullet and fireball entities. Given that you moved the position data out of the Projectile component to the new Position component wouldn’t both entities require Position components as well? It looks like you’re adding in Unity’s built in Translation component in place of that but you might just want to stick to the Position component there for consistency with the rest of the code as it might throw some people off. Unless I’m somehow mistaken in my understanding of this which is entirely possible.
LikeLike
Both are using Translation. You’re right, though. Let me edit that.
LikeLike