2018.ENG - Unity Academy - ECS and Data Orientied Design
2018.ENG - Unity Academy - ECS and Data Orientied Design
2
Problem
3
Typical Implementation of OO
● Class hierarchies
● Virtual functions
● Encapsulation often violated since stuff Needs To Know
● “One Thing At A Time” approach
● Late decisions
4
This is going to be…
OOP party like it’s 1999
5
Simple OO component system: Component
// Component base class. Knows about the parent game object, and has some virtual methods.
class Component
{
public:
Component() : m_GameObject(nullptr) {}
virtual ~Component() {}
virtual void Start() {}
virtual void Update(double time, float deltaTime) {}
private:
GameObject* m_GameObject;
};
6
Simple OO component system: GameObject
// Game object class. Has an array of components.
class GameObject
{
public:
GameObject(const std::string&& name) : m_Name(name) { }
~GameObject() { for (auto c : m_Components) delete c; }
// get a component of type T, or null if it does not exist on this game object
template<typename T>
T* GetComponent()
{
for (auto i : m_Components) { T* c = dynamic_cast<T*>(i); if (c != nullptr) return c; }
return nullptr;
}
7
Simple OO component system: Utilities
// Finds all components of given type in the whole scene
template<typename T>
static ComponentVector FindAllComponentsOfType()
{
ComponentVector res;
for (auto go : s_Objects)
{
T* c = go->GetComponent<T>();
if (c != nullptr) res.emplace_back(c);
}
return res;
}
// Find one component of given type in the scene (returns first found one)
template<typename T>
static T* FindOfType()
{
for (auto go : s_Objects)
{
T* c = go->GetComponent<T>();
if (c != nullptr) return c;
}
return nullptr;
}
8
Simple OO component system: various components
// 2D position: just x,y coordinates
struct PositionComponent : public Component
{
float x, y;
};
// Sprite: color, sprite index (in the sprite atlas), and scale for rendering it
struct SpriteComponent : public Component
{
float colorR, colorG, colorB;
int spriteIndex;
float scale;
};
9
Simple OO component system: various components
// Move around with constant velocity. When reached world bounds, reflect back from them.
struct MoveComponent : public Component
{
float velx, vely;
WorldBoundsComponent* bounds;
MoveComponent(float minSpeed, float maxSpeed)
{
/* … */
}
10
Simple OO component system: components logic
virtual void Update(double time, float deltaTime) override
{
// get Position component on our game object
PositionComponent* pos = GetGameObject().GetComponent<PositionComponent>();
// update position based on movement velocity & delta time
pos->x += velx * deltaTime;
pos->y += vely * deltaTime;
// check against world bounds; put back onto bounds and mirror
// the velocity component to "bounce" back
if (pos->x < bounds->xMin) { velx = -velx; pos->x = bounds->xMin; }
if (pos->x > bounds->xMax) { velx = -velx; pos->x = bounds->xMax; }
if (pos->y < bounds->yMin) { vely = -vely; pos->y = bounds->yMin; }
if (pos->y > bounds->yMax) { vely = -vely; pos->y = bounds->yMax; }
}
11
Simple OO component system: game update loop
void GameUpdate(sprite_data_t* data, double time, float deltaTime)
{
// go through all objects
for (auto go : s_Objects)
{
// Update all their components
go->Update(time, deltaTime);
// For objects that have a Position & Sprite on them: write out
// their data into destination buffer that will be rendered later on.
PositionComponent* pos = go->GetComponent<PositionComponent>();
SpriteComponent* sprite = go->GetComponent<SpriteComponent>();
if (pos != nullptr && sprite != nullptr)
{
/* … emit data for sprite rendering … */
}
}
}
12
Let’s make a simple “game” with this!
13
Let’s make a simple “game” with this!
14
Issues with OO design: where to put code?
15
Issues with OO design: where to put code?
16
Issues with OO design: hard to know what does what
● Ever opened a Unity project and tried to figure out how it works?
● …yeah, that :)
● “game logic” scattered around in million components, with no overview
17
Issues with OO design: “messy base class” problem
EntityType entityType() const override;
19
Issues with OO design: “messy base class” problem
// … continued …
float health() const override;
float maxHealth() const override;
DamageBarType damageBar() const override;
float healthPercentage() const;
20
Issues with OO design: “messy base class” problem
// … continued …
void playEmote(HumanoidEmote emote) override;
void beginPrimaryFire();
void beginAltFire();
void endPrimaryFire();
void endAltFire();
void beginTrigger();
void endTrigger();
This is not the best OO design, and it certainly is possible to make a better one.
But also, often code ends up being like this, even if no one wanted it that way.
21
Issues with OO design: performance
23
Issues with OO design: typical memory view
https://software.intel.com/en-us/articles/get-started-with-the-unity-entity-component-system-ecs-c-sharp-job-system-and-burst-compiler
24
Issues with OO design: optimizability
25
Issues with OO design: testability
26
Intermission
27
CPU performance trends*
* from https://www.karlrupp.net/2018/02/42-years-of-microprocessor-trend-data/
28
CPU-RAM performance gap*
* multiply by a billion!
31
The Suspense
Alternatives to Traditional OO
32
Does Code and Data need to go together?
// this?
class ThingThatAvoids
{
void AvoidOtherThing(ThingToAvoid* thing);
};
// why not this instead? does not even need to be in a class
void DoAvoidStuff(ThingThatAvoids* who, ThingToAvoid* whom);
// or this?
class ThingToAvoid
{
void MakeAvoidMe(ThingThatAvoids* who);
};
33
Data First
“The purpose of all programs, and all parts of those programs, is to transform
data from one form to another.”
“If you don’t understand the data, you don’t understand the problem.”
— Mike Acton
35
When there is One, there is Many
36
When there is One, there is Many
https://twitter.com/bmcnett/status/1043285997998432256
37
When there is One, there is Many
https://twitter.com/bmcnett/status/1043332565308923904
38
When there is One, there is Many
39
The Grand Unveil
40
Data Oriented Design (DOD)
41
DOD Resources
● Data-Oriented Design (Or Why You Might Be Shooting Yourself in The Foot
With OOP) blog post, Noel Llopis
● Practical Examples in Data Oriented Design slides, Niklas Gray
● Data-Oriented Design and C++ video, Mike Acton
● Typical C++ Bullshit slide gallery, Mike Acton
● Data-Oriented Design blog post & links, Adam Sawicki
42
The Grand Unveil, Act II
43
Is traditional Unity GO/Component setup ECS?
44
Entity-Component-System (ECS)
https://en.wikipedia.org/wiki/Entity-component-system
45
ECS Resources
46
Yeah I’ve no idea what to write here by now
ECS/DOD Example
47
Recall our simple “game”
51
First: Fix Stupidities, take 2
● GetComponent inside inner loop of Avoid component, cache that too.
● 309ms → 78ms! (commit)
52
Where time is spent now?
● Let’s use a Profiler.
● I’m on Mac, so Xcode Instruments.
53
Let’s make some Systems: AvoidanceSystem
● Avoid & AvoidThis components are almost only data now,
● System knows all things it will operate on
// When present, tells things that have Avoid component to avoid this object
struct AvoidThisComponent : public Component
{
float distance;
};
// "Avoidance system" works out interactions between objects that have AvoidThis and Avoid
// components. Objects with Avoid component:
// - when they get closer to AvoidThis than AvoidThis::distance, they bounce back,
// - also they take sprite color from the object they just bumped into
struct AvoidanceSystem
{
// things to be avoided: distances to them, and their position components
std::vector<float> avoidDistanceList;
std::vector<PositionComponent*> avoidPositionList;
// objects that avoid: their position components
std::vector<PositionComponent*> objectList;
54
// …
Let’s make some Systems: AvoidanceSystem
● Here’s the logic code of the system
● 78ms → 69ms (commit)
void UpdateSystem(double time, float deltaTime)
{
// go through all the objects
for (size_t io = 0, no = objectList.size(); io != no; ++io)
{
PositionComponent* myposition = objectList[io];
55
Let’s make some Systems: MoveSystem
● Similar, let’s make a MoveSystem
// Move around with constant velocity. When reached world bounds, reflect back from them.
struct MoveComponent : public Component
{
float velx, vely;
};
struct MoveSystem
{
WorldBoundsComponent* bounds;
std::vector<PositionComponent*> positionList;
std::vector<MoveComponent*> moveList;
/* … */
56
Let’s make some Systems: MoveSystem
● Here’s the logic of the MoveSystem
● 69ms → 83ms (commit).
● What?!
void UpdateSystem(double time, float deltaTime)
{
// go through all the objects
for (size_t io = 0, no = positionList.size(); io != no; ++io)
{
PositionComponent* pos = positionList[io];
MoveComponent* move = moveList[io];
// update position based on movement velocity & delta time
pos->x += move->velx * deltaTime;
pos->y += move->vely * deltaTime;
// check against world bounds; put back onto bounds and mirror the velocity component to "bounce" back
if (pos->x < bounds->xMin) { move->velx = -move->velx; pos->x = bounds->xMin; }
if (pos->x > bounds->xMax) { move->velx = -move->velx; pos->x = bounds->xMax; }
if (pos->y < bounds->yMin) { move->vely = -move->vely; pos->y = bounds->yMin; }
if (pos->y > bounds->yMax) { move->vely = -move->vely; pos->y = bounds->yMax; }
}
}
57
Ok what is going on?
● Profiler again:
58
Lessons so far
● Optimizing one place can make things slower for unexpected reasons.
● Out-of-order CPUs, caches, prefetching, … maybe? I did not dig in here :/
● C++ RTTI (dynamic_cast) can be really slow.
● We use it in GameObject::GetComponent.
// get a component of type T, or null if it does not exist on this game object
template<typename T>
T* GetComponent()
{
for (auto i : m_Components) { T* c = dynamic_cast<T*>(i); if (c != nullptr) return c; }
return nullptr;
}
59
Let’s stop using C++ RTTI then
● If we had a “Type” enum, and each Component stored the Type…
● 83ms → 54ms (commit), yay.
enum ComponentType
{
kCompPosition,
kCompSprite,
kCompWorldBounds,
kCompMove,
kCompAvoid,
kCompAvoidThis,
};
// ...
ComponentType m_Type;
60
So far:
● Update performance: 6x faster (330ms→54ms), yay!
● Memory usage: increased 310MB→363MB
● Component pointer caches, type IDs in each component, …
● Lines of code: more 400→500
61
Avoid & AvoidThis Components, who needs them?
● That’s right. No one!
● Just register objects directly with AvoidanceSystem.
● 54ms → 46ms, 363MB→325MB, 500→455lines (commit)
62
Actually, who needs Component hierarchy?
● Just have component fields in GameObject
● 46ms→43ms update, 398→112ms startup, 325MB→218MB, 455→350lines (commit)
// each object has data for all possible components,
// as well as flags indicating which ones are actually present.
struct GameObject
{
GameObject(const std::string&& name)
: m_Name(name), m_HasPosition(0), m_HasSprite(0), m_HasWorldBounds(0), m_HasMove(0) { }
~GameObject() {}
std::string m_Name;
// data for all components
PositionComponent m_Position;
SpriteComponent m_Sprite;
WorldBoundsComponent m_WorldBounds;
MoveComponent m_Move;
// flags for every component, indicating whether this object "has it"
int m_HasPosition : 1;
int m_HasSprite : 1;
int m_HasWorldBounds : 1;
int m_HasMove : 1;
}; 63
Stop allocating individual GameObjects
● vector<GameObject*> → vector<GameObject>
● 43ms update, 112→99ms startup, 218MB→203MB (commit)
64
Geez how many intermissions you plan to have here?!
65
Typical layout: Array-of-Structures (AoS)
● Some objects, and arrays of them.
● Simple to understand and manage.
● Great… iff we need all the data from each object.
// structure
struct Object
{
string name;
Vector3 position;
Quaternion rotation;
float speed;
float health;
};
// array of structures
vector<Object> allObjects;
66
How does data look like in memory?
struct Object // 60 bytes:
{
string name; // 24 bytes
Vector3 position; // 12 bytes
Quaternion rotation; // 16 bytes
float speed; // 4 bytes
float health; // 4 bytes
};
67
What if we don’t need all data?
● If we have a system that only needs object position & speed…
- Hey CPU, read me position of first object!
- Sure, it’s right here…
68
What if we don’t need all data?
● If we have a system that only needs object position & speed…
- Hey CPU, read me position of first object!
- Sure, it’s right here… lemme read the whole cache line from memory for you!
69
What if we don’t need all data?
● If we have a system that only needs object position & speed…
- Uh ok, get me position of second object then
- Will do!
70
What if we don’t need all data?
● If we have a system that only needs object position & speed…
- Uh ok, get me position of second object then
- Will do! Here’s the whole cache line for you again!
71
What if we don’t need all data?
● If we have a system that only needs object position & speed…
● We end up reading everything from memory,
● But we only needed 16 bytes out of 60 in every object.
● 74% of all memory traffic we did not even need!
72
Flip it: Structure-of-Arrays (SoA)
● Separate arrays for each data member.
● Arrays need to be kept in sync.
● “The object” no longer exists; data accessed through an index.
// structure of arrays
struct Objects
{
vector<string> names; // 24 bytes each
vector<Vector3> positions; // 12 bytes each
vector<Quaternion> rotations; // 16 bytes each
vector<float> speeds; // 4 bytes each
vector<float> healths; // 4 bytes each
};
73
How does data look like in memory?
struct Objects
{
vector<string> names; // 24 bytes each
vector<Vector3> positions; // 12 bytes each
vector<Quaternion> rotations; // 16 bytes each
vector<float> speeds; // 4 bytes each
vector<float> healths; // 4 bytes each
};
64 bytes (typical CPU cache line)
74
Reading partial data in SoA
● If we have a system that only needs object position & speed…
- Hey CPU, read me position of first object!
- Sure, it’s right here…
75
Reading partial data in SoA
● If we have a system that only needs object position & speed…
- Hey CPU, read me position of first object!
- Sure, it’s right here… lemme read the whole cache line from memory for you!
- (narrator) and so positions for next 4 objects got read into CPU cache too
76
SoA data layout transformation
● Is fairly common
● Careful to not overdo it though!
● At some point the # of individual arrays can get counterproductive
● Structure-of-Arrays-of-Structures (SoAoS), etc. :)
77
Back to us: SoA layout for component data
● No longer a GameObject class, just an EntityID
● 43ms→31ms update, 99→94ms startup, 350→375 lines (commit)
// /* … */
78
So what have we got?
80
Question & Homework time!
Book
Zero Days Dead
of the by by Unity’s
Scatter – MadeDemo
with Team
Unity — Made with Unity