Part 2: Unity ECS - project design
Updated 10 days ago
Think about project with ECS architecture
Part 1: Unity ECS - briefly about ecs Part 2: Unity ECS - project design Part 3: Unity ECS - operations on Entities Part 4: Unity ECS - ECS and Jobs
Just before we start, I just want to mention this is very simple way of thinking (beginner-friendly) and doesn't cover any pattern for creating systems, just an example thinking scheme.

How to think in ECS?

You probably were afraid of how to design your game.. These components, systems, entities seems be hard to think about them. Well, it's not. Actually it's pretty simple. Entities: What do we need to make game alive? Obviously, some entities that will do something. This is trivial, just think what Entity do we need? Some Baloon? What components needs baloon? Position, Renderer, maybe something more but we can live with just these two. Components: We have very simple data container which have just some data. What parameters we need to make game? Position? DamageValue? Health? Probably we will check something with booleans, hey, they all are just primitives, nothing scary. Let's talk about systems. Systems: Think about their behaviour like that: do the same as you've done before with monobehaviours with just one difference. The difference is loop, nothing really more (maybe just injection). We loop over all entities that match your group, work as your worked before in MonoBehaviour.Update Let's take example of baloons that float up
// We are in MonoBehaviour script private void Update() { // We can cache this Vector on Awake, but still, every script has new vector // So it's 12 bytes (3 floats each 4 bytes) PER SCRIPT from vector transform.position += new Vector3(0, 1, 0); }
// We are in ComponentSystem struct Group { public ComponentDataArray<Position> Positions; } [Inject] Group data; protected override void OnUpdate() { for (int i = 0; i < data.Length; i++) { // When Unity uses ref returns from C#7 it'll make the extra assignment redundant. var position = data.Positions[i]; // We can cache float3 so we'll have only one variable // If so, in contrast to MonoBehaviour - it's just 12 bytes OVERALL - we have it cached in system position += new float3(0, 1, 0); data.Positions[i] = position; } }
It seems a much more work? It just looks like that. We've got much more efficient code. We take care of data and we're cache friendly. Say for example we have 10000 baloons. Each has monobehaviour, and its Update that's so expensive, isn't it? (Will it even run? Haha) 120000 bytes from just vectors. With ECS that's just piece of cake. We still have just 12 bytes from our float3 (used as vector)
Okey let's take a look at sample project, how can we structure it?

Duck Shooter Example

Fantastic small game - we'll just shoot to angry ducks, that's all. What do we need? The simple thinking scheme:

Components: Just data container

  • Position - { float2 Pos } - must have for ducks and our crosshair to place them where we want
  • MouseInput - { bool Fire, int2 Pos } - we will check where is mouse position and when fire button was pressed
  • Health - { float Value } - some ducks you can oneshot, others can be stronger
  • MoveSpeed - { float Value } - some ducks can be faster than the others
  • DuckTag - { } - empty struct, just for proper filtering
  • PlayerTag - { } - empty struct, just for proper filtering

SharedComponents: shared component between entities, mainly "readonly" (See docs THERE)

  • SpriteRenderer { Material mat, Image img } - we don't want to shoot to invisible ducks, do we?

Systems: operate on all entities matching the given group of components (each entity has each component from group)

  • InputSystem (operates on group of { MouseInput } ) (Behaviour => MouseInput = Input.Mouse, Fire = Input.ButtonDown)
  • CrosshairSystem (operates on group of { MouseInput, Position, PlayerTag } ) (Behaviour => Position = MouseInput)
  • MoveSystem (operates on group of { Position, MoveSpeed } ) (Behaviour => Position += MoveSpeed)
  • ShootSystem (operates on group1 of { Position, Health, DuckTag } + group2 of { Position, MouseInput, PlayerTag } ) (Behaviour => if(group2.MouseInput.Fire && group2.Position == group1.Position) group1.Health -= 1)
  • DuckDeathSystem (operates on group of { Health } ) (Behaviour => if(health <= 0) Destroy(thisEntity))
  • GameConditionSystem (operates on group of { DuckTag } ) (Behaviour => [Always update] (attribute that prevent group from being disabled it there is no matching entity); if(group.Length <= 0) GameOver())

Entities: just index

  • (Player(Crosshair)) - has components { PlayerTag, Position, MouseInput }
  • (Duck0) - has components { DuckTag, Position, Health, SpriteRenderer, MoveSpeed }
  • (Duck1) - has components { DuckTag, Position, Health, SpriteRenderer, MoveSpeed }
  • (Duck2) - has components { DuckTag, Position, Health, SpriteRenderer, MoveSpeed }
  • (…)

What's next? Part 3: Unity ECS - operating on Entities

Give me some feedback in comment section, don't worry I won't hate you if you show me some "anomaly" in my article. If you enjoy my article - like it and follow me. It'll motivate me to write more articles. See you.

Tomasz Piowczyk
Student Unity Developer - Programmer