Skip to content
This repository has been archived by the owner on Dec 20, 2022. It is now read-only.

Why Use an Entity Component System

Andrew Macdonald edited this page Jan 30, 2017 · 6 revisions

We Got Lost

In my last year of college, for one of my final group projects, my team ran head-first into the limitations of Unity3D's GameObject / Component Relationship.

We were working on a competitive local-multiplayer king-of-the-kill brawler, inspired by Super Smash Bros. and Castle Crashers. Players could move their characters around the map, attack each other, and dash into each other, briefly stunning their enemies.

We split these mechanics up into reasonable components:

  • MovementComponent
  • AttackComponent
  • DashComponent

Everything was great... until we wanted to combine mechanics. We wanted to make a super-attack: when players input the correct combo, they would attack and dash into their enemy, dealing more damage & sending them flying backwards.

But, we asked ourselves, "Where should this logic go? In the AttackComponent? In the DashComponent? In a completely separate Component?!" We didn't have the answer at the time, and what we ended up writing felt like a dirty hack (like most things in game dev, unfortunately).

I knew there had to be better way.

On The Way To An ECS

In some spare time, after I had graduated, I puttered around with our code and the "cleanest" solution I came up with (before I learned about ECS) was to move all of the logic out the Movement, Attack and Dask Components into a new Player Component:

// PlayerComponent.cs
using UnityEngine;
	
[RequireComponent(typeof(MovementComponent)]
[RequireComponent(typeof(AttackComponent)]
[RequireComponent(typeof(DashComponent)]
public class PlayerComponent : MonoBehaviour
{
	MovementComponent movement;
	AttackComponent attack;
	DashComponent dash;
	
	void Start()
	{
        movement = GetComponent<MovementComponent>();
        attack = GetComponent<AttackComponent>();
        dash = GetComponent<DashComponent>();
	}
	
	void Update()
	{
		UpdateMovement();
		UpdateAttack();
		UpdateDash();
		UpdateSuperAttack();
	}
	
	void UpdateMovement()
	{
		// ...	
	}
	
	void UpdateAttack()
	{
		// ...	
	}
	
	void UpdateDash()
	{
		// ...	
	}
	
	void UpdateSuperAttack()
	{
		// Uses movement, attack & dash, no problem!
		
        // ...	
	}
}

This put all of the player logic in one, centralized place while still keeping the Components (and therefore their data) separate! However, RequireComponent() creates highly-coupled, inflexible code: if I made changes to any of these Components, I would need to triple-check that these changes didn't break the PlayerComponent.

Are We There Yet?

We can easily translate this rigid, highly-coupled PlayerComponent into EgoCS: we split the logic / mechanics into discrete systems:

// MovementSystem.cs
public class MovementSystem : EgoSystem<
    EgoConstraint<MovementComponent>
>{
    public override void Update()
	{
		// ...	
	}
}

// AttackSystem.cs
public class AttackSystem : EgoSystem<
    EgoConstraint<AttackComponent>
>{
    public override void Update()
    {
        // ...
    }
}

// DashSystem.cs
public class DashSystem : EgoSystem<
    EgoConstraint<DashComponent>
>{
    public override void Update()
    {
        // ...	
    }
}

// SuperAttackSystem.cs
public class SuperAttackSystem : EgoSystem<
    EgoConstraint<MovementComponent, AttackComponent, DashComponent>
>{
    public override void Update()
    {
        // ...
    }
}

// EgoInterface.cs
using UnityEngine;
   
public class EgoInterface : MonoBehaviour
{
    void Start()
    {
        // Add Systems Here:
        EgoSystems.Add(
            new MovementSystem(),
            new AttackSystem(),
            new DashSystem(),
            new SuperAttackSystem() );
        );

        EgoSystems.Start();
    }
    
    void Update()
    {
        EgoSystems.Update();
    }
}

How Does An ECS Help?

Simply put, ECSs keep everything maximally-decoupled and flexible; it separates Data and Logic into Components and Systems, respectively.

Changing which Components are attached to any GameObject will never break the logic in any system.

Changing which Components any System cares about, or the logic in any System's Update(), will not break the data in any Component.

The Harsh Truth

A game's design is always in flux, and can be changed at any time. The only time it won't be is when the budget runs out or you're getting close to release (and even then, there's DLC). Leonardo da Vinci famously said, "Art is never finished, only abandoned".

These design changes can come from anywhere: the project's scope changed, the project's budget changed, a mechanic that seemed amazing on paper turned out to be awful when implemented and played, or a mechanic needs to be ever so slightly tweaked (but just tweaked enough that it requires a rewrite; that was a fun three weeks).

Therefore, it's in our best interest to develop and program games that won't break when functionality is added to, or removed from them. The maximally-decoupled and flexible nature of an ECS does exactly that.