Get a high-level overview of the Entity Component System (ECS) and turn-based game loops, and see a proof of concept built using ECS. These slides will cover some of the pitfalls and also show concepts of ECS in a slightly exotic context.
Speaker:
Florian Uhde - Three Eyed Games
Watch the session on YouTube: https://youtu.be/mL4qrt-15TE
3. Setting the stage!
3
— Not the next Civilization game
— Code focussed
— No ECS experience is fine
— Agenda
– My background
– Concepts of this talk
– Non-turn-based prototype with ECS
– Turn it turn-based
6. Turn Based Game Loop
6
— Continuous update
— Poll all events
— Change game world
Game world continuously changes
every frame
while (true)
{
processInput();
update(elapsed);
render();
}
System A
System Y
System Z
…
…
7. 7
— Frame != Turn
— We have continuous systems: update every frame
— We have turn based systems: update every turn
Turn Based Game Loop
8. Turn Based Game Loop
8
— Continuous update
— Poll all events
— Wait for player action
— Update other actors only when
player makes a move
Game world only changes in
response to player action
while (true)
{
action = processInput();
if(action != null)
{
update(elapsed);
}
render();
}
Player Action
TB System
TB System
…
9. — Composition vs
Inheritance
— Atomic parts
— Filtering
— Systematic design
Entity
Component
System
Unity ECS
9
DATA
DATA
10. Unity ECS
10
— Quite useable today
— High degree of control over gameworld (Savegames!)
— Performance and data layout
— Path into future awesomeness of Unity
12. Example Project Setup
12
— Unity 2019.2.6f1
— 3D Template
— Packages:
– Entities 0.1.1 [preview]
– Hybrid Renderer 0.1.1 [preview]
— Content from different assets:
– PolyWorks FullPack, GalaxyBox1, SurrounDead - Free Sample, Bitgem Texture pack
— https://github.com/floAr/UniteCPH_TurnBasedPrototypeECS
13. “Flying Gardener”
13
— Grid based world
— Two kind of actors:
– Player(s)
– NPCs (Evil Snails)
— 2DOF Movement
14. Creating actors
14
— Actors move on grid in
game world
— Current- and target
position
[Serializable]
public struct ActorComponent : IComponentData
{
public float3 position;
public float3 target_positon;
}
public void Convert(Entity entity,
EntityManager dstManager,
GameObjectConversionSystem conversionSystem)
{
float3 grid_pos = new float3(
math.floor(transform.position.x),
0,
math.floor(transform.position.z)
);
dstManager.AddComponentData(entity,
new ActorComponent()
{
position = grid_pos,
target_positon = grid_pos
});
}
15. Creating actors
15
— Actors move on grid in
game world
— Current- and target
position
[Serializable]
public struct ActorComponent : IComponentData
{
public float3 position;
public float3 target_positon;
}
public void Convert(Entity entity,
EntityManager dstManager,
GameObjectConversionSystem conversionSystem)
{
float3 grid_pos = new float3(
math.floor(transform.position.x),
0,
math.floor(transform.position.z)
);
dstManager.AddComponentData(entity,
new ActorComponent()
{
position = grid_pos,
target_positon = grid_pos
});
}
16. Creating actors
16
— Actors move on grid in
game world
— Current- and target
position
— Convert from
MonoBehaviour to
Component
[Serializable]
public struct ActorComponent : IComponentData
{
public float3 position;
public float3 target_positon;
}
public void Convert(Entity entity,
EntityManager dstManager,
GameObjectConversionSystem conversionSystem)
{
float3 grid_pos = new float3(
math.floor(transform.position.x),
0,
math.floor(transform.position.z)
);
dstManager.AddComponentData(entity,
new ActorComponent()
{
position = grid_pos,
target_positon = grid_pos
});
}
17. Creating actors
17
— Actors move on grid in
game world
— Current- and target
position
— Convert from
MonoBehaviour to
Component
[Serializable]
public struct ActorComponent : IComponentData
{
public float3 position;
public float3 target_positon;
}
public void Convert(Entity entity,
EntityManager dstManager,
GameObjectConversionSystem conversionSystem)
{
float3 grid_pos = new float3(
math.floor(transform.position.x),
0,
math.floor(transform.position.z)
);
dstManager.AddComponentData(entity,
new ActorComponent()
{
position = grid_pos,
target_positon = grid_pos
});
}
18. — Show player prefab with scripts be converted to entity
— Highlight entity debugger
[Editor / Live Code]
18
19. Moving actors
19
— Interpolate between
current and target
position
struct ActorSystemJob : IJobForEach<Translation, Rotation, ActorComponent>
{
public float deltaTime;
public void Execute(ref Translation translation,
ref Rotation rotation,
ref ActorComponent actor )
{
var direction = actor.target_positon - actor.position;
if (math.length(direction) > 0.1f)
{
direction = math.normalize(direction);
rotation.Value= quaternion.LookRotation(direction, math.up());
translation.Value = translation.Value + direction * deltaTime;
actor.position = translation.Value;
}
else
{
translation.Value = actor.target_positon;
actor.position = translation.Value;
}
}
}
20. Moving actors
20
— Interpolate between
current and target
position
— Change Translation and
Rotation accordingly
struct ActorSystemJob : IJobForEach<Translation, Rotation, ActorComponent>
{
public float deltaTime;
public void Execute(ref Translation translation,
ref Rotation rotation,
ref ActorComponent actor )
{
var direction = actor.target_positon - actor.position;
if (math.length(direction) > 0.1f)
{
direction = math.normalize(direction);
rotation.Value= quaternion.LookRotation(direction, math.up());
translation.Value = translation.Value + direction * deltaTime;
actor.position = translation.Value;
}
else
{
translation.Value = actor.target_positon;
actor.position = translation.Value;
}
}
}
21. Moving actors
21
— Interpolate between
current and target
position
— Change Translation and
Rotation accordingly
— Execute on all entities
with Translation,
Rotation and
ActorComponent
struct ActorSystemJob : IJobForEach<Translation, Rotation, ActorComponent>
{
public float deltaTime;
public void Execute(ref Translation translation,
ref Rotation rotation,
ref ActorComponent actor )
{
var direction = actor.target_positon - actor.position;
if (math.length(direction) > 0.1f)
{
direction = math.normalize(direction);
rotation.Value= quaternion.LookRotation(direction, math.up());
translation.Value = translation.Value + direction * deltaTime;
actor.position = translation.Value;
}
else
{
translation.Value = actor.target_positon;
actor.position = translation.Value;
}
}
}
22. Moving actors
22
— Interpolate between
current and target
position
— Change Translation and
Rotation accordingly
— Execute on all entities
with Translation,
Rotation and
ActorComponent
struct ActorSystemJob : IJobForEach<Translation, Rotation, ActorComponent>
{
public float deltaTime;
public void Execute(ref Translation translation,
ref Rotation rotation,
ref ActorComponent actor )
{
var direction = actor.target_positon - actor.position;
if (math.length(direction) > 0.1f)
{
direction = math.normalize(direction);
rotation.Value= quaternion.LookRotation(direction, math.up());
translation.Value = translation.Value + direction * deltaTime;
actor.position = translation.Value;
}
else
{
translation.Value = actor.target_positon;
actor.position = translation.Value;
}
}
}
23. Moving actors
23
— Interpolate between
current and target
position
— Change Translation and
Rotation accordingly
— Execute on all entities
with Translation,
Rotation and
ActorComponent
— Pipe external data in
and schedule the job
public class ActorSystem : JobComponentSystem
{
[BurstCompile]
struct ActorSystemJob
{ […] }
protected override JobHandle OnUpdate(JobHandle inputDependencies)
{
var job = new ActorSystemJob();
job.deltaTime = UnityEngine.Time.deltaTime;
return job.Schedule(this, inputDependencies);
}
}
24. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
24
— Create MoveIntention
25. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
25
— Create MoveIntention
26. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
26
— Create MoveIntention
— System that generate
random intentions (for
our non-player actors)
27. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
27
— Create MoveIntention
— System that generate
random intentions (for
our non-player actors)
28. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
28
— Create MoveIntention
— System that generate
random intentions (for
our non-player actors)
29. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
29
— Create MoveIntention
— System that generate
random intentions (for
our non-player actors)
— Component changes
need synchronization
30. public struct MoveIntention : IComponentData
{
public int2 direction_xz;
}
protected override void OnUpdate()
{
Entities.WithAll<ActorComponent>().ForEach((Entity id) =>
{
var direction = generateMove();
var intent = new MoveIntention()
{
direction_xz = direction
};
PostUpdateCommands.AddComponent<MoveIntention>(id, intent);
});
}
Moving actors
30
— Create MoveIntention
— System that generate
random intentions (for
our non-player actors)
— Component changes
need synchronization
31. New scene -> grid + player + enemies
[Editor / Live Code]
31
32. — Player input / way to pass the turn
— Communicate between systems when allowed to run
— Variant 1: Manual control
— Variant 2: Component based control
Changes:
— PlayerComponent
— InputSystem: WASD -> MoveIntention
Bringing things in order. The player update
lock
32
34. Variant 1: Manual Update
34
— Remove turn-based systems from
ECS update loop
— Manually trigger update after
each turn
— [DisableAutoCreation] of turn
based systems
— World.GetOrCreateSystem()
— System.Update()
TB System
TB System
Player Action
…
35. Variant 1: Manual Update
35
— Remove turn-based systems from
ECS update loop
— Manually trigger update after
each turn
— [DisableAutoCreation] of turn
based systems
— World.GetOrCreateSystem()
— System.Update()
TB System
TB System
Player Action
…
36. [Disable on Load] Move and RandomMove
Manually call them from TurnBasedGameLoop
[Editor / Live Code]
36
37. Variant 1: Manual Update
37
One column body text
lorem ipsum dolor sit
amet, consectetur
adipiscing elit. Nunc
lacinia, nisi ac vehicula
pellentesque, justo tellus
dignissim velit, nec
rhoncus tellus lorem id
sapien.
Fine grained control
Low footprint
Manual effort for each system
Does not scale well
38. Variant 2: Interlocked Components
38
— Components to communicate
— Player has AwaitAction
— InputSystem consumes
AwaitAction
— If no player is waiting,
TurnBasedSystem hand out
ReadyToHandle flag
— After one frame every other
system updated so we remove
the flag again
Player Action
TB System
TB System
…
39. Variant 2: Interlocked Components
39
— Components to communicate
— Player has AwaitAction
— InputSystem consumes
AwaitAction
— If no player is waiting,
TurnBasedSystem hand out
ReadyToHandle flag
— After one frame every other
system updated so we remove
the flag again
Player Action
TB System
TB System
…
40. [Editor / Live Code]
40
Player gets await flag
Input system consumes await flag
Talk about Query
If no one is waiting:
Hand out ready to handle tokens
Refresh await action token
Include handle token in move system
41. Variant 2: Interlocked Components
41
One column body text
lorem ipsum dolor sit
amet, consectetur
adipiscing elit. Nunc
lacinia, nisi ac vehicula
pellentesque, justo tellus
dignissim velit, nec
rhoncus tellus lorem id
sapien.
Scales nicely
Frontloading effort
Extensible
Multiple players out of the box
Complexity
Order related problems
42. What could be next?
42
— Gameplay!
— Alternative turn modes: Timed, multiple actions
— Integrate awesome Unity packages:
– Deterministic, stateless Physics
– Mulitplayer
– Live Play
– …
43. Recap
43
— ECS building blocks
— MonoBehaviours ECS
— Turn based game loop
— Player action triggers world
change
— Variants of turn based systems
45. Bonus Slide! Local Multiplayer
— Give ID to PlayerComponent
— Filter InputSystem based on ID
— MoveSystem -> WithAny
46. Bonus Slide! Update Groups
46
— Use attributes to sort systems
into update order
— [UpdateAfter] [UpdateBefore]
Player Action
TB System
TB System
…