-
Notifications
You must be signed in to change notification settings - Fork 31
Why Use an Entity Component System
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.
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
.
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();
}
}
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.
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.