There’s been a lot of talk about Entity-Component-System (ECS) pattern lately. Even more interesting is that Unity is trying to go to this route when Unity 2018 comes. Part of a programmer’s career is trying out different stuff. I’m definitely going to try Unity’s ECS. But I do have some concerns and I’m hoping that someone could shed some light.

But Unity already has components?

Currently, Unity employs the “Composition” design pattern to model its entities. Some people would like to call it “Entity-Component” pattern, but I don’t like that term because it adds to the confusion. Most people use the terms “pattern”, “architecture”, or “system” interchangeably. For simplicity, I’d just refer to it as “Composition” pattern here. Some would say that Unity is not using “pure” ECS. I find that weird. Of course not. I don’t think that Unity’s pattern is trying to be ECS. It’s an entirely different design. They do have similarities but ECS is a completely different thing.

In composition pattern, you create different component classes where each class handles a different domain. A game entity then is defined as a combination of these classes. For example, a game character might have a Transform, SpriteRenderer, AI, CharacterController, and RigidBody components. Another entity, like say a static rock, may only have Transform, SpriteRenderer, and Collider components. It doesn’t need to have those other player related components. Basically, you’re like assembling components to model your entities. This pattern is more flexible compared to using inheritance. It needs no further explanation. There are lots of articles written about this problem.

How does ECS differ then?

In terms of flexibility and concept, composition and ECS are mostly the same. The defining difference, at least for me, is the layout of component data in memory and thus affects how it should be programmed. ECS was designed with how modern computers store data in memory and use that knowledge to increase performance.

While CPU speeds keep increasing, memory access speed has not keep up. Therefore, the performance of programs is dictated by how efficient they access data from RAM. This is the reason why CPUs have caching. The CPU gets a chunk of data from RAM and hopes that the instructions that it has to do uses data that can be found in this chunk. If not, it will have to release the current chunk and get another chunk from main memory where the target data resides. Remember that RAM access is slower. If the CPU frequently fetches data from RAM, this means that it spends most of its time waiting for the fetch instead or executing instructions. This page from Game Programming Patterns explains this very well.

If you want high performance software, you’d want to have more cache hits and reduce cache misses. Ideally, you would want the bytes of your data to be aligned next to each other as this increases the chance of cache hits. This is also referred to as “cache coherence”. However, OOP languages like C# does not usually work like this. When you instantiate an object through a class, you’re allocating memory on the heap. This does not guarantee ordered alignment. The actual block of bytes of that class can be allocated in different places. You have no control over this. The C# runtime automatically does this for you. Internally, what you get in your variable is just a pointer of the memory address where the data is stored. Say you have an array of Enemy class objects. What you get is just an array of pointers. The block of bytes that represents each Enemy is not necessarily in order.

Fortunately, C# has structs. When you declare an array of a struct, you are guaranteed that the bytes of those instances are laid out next to each other. (There may be some cases that it’s not. I’m not a C# compiler expert.) This is where ECS comes in. Instead of having components as classes with methods, components are reduced to only having data. Since now it’s only data, components can no longer have polymorphic capabilities, thus components can be declared as structs. Logic now resides in the “System” part of ECS. A system collects entities with particular components that it needs. The system then executes on this set of components which in memory are ordered next to each other. This promotes more cache hits and theoretically makes the program run more efficient.

In code, composition looks like this (not necessarily Unity):

class Motion : Component { private Entity owner; private Transform transform; private float speed; private float acceleration; private Vector3 direction; public void Init() { this.transform = this.owner.GetComponent<Transform>(); } ... public void Update() { this.speed += this.acceleration * Time.deltaTime; this.transform.position += this.direction * this.speed * Time.deltaTime; } ... } Entity runner = new Entity(); runner.AddComponent<Transform>(); runner.AddComponent<Motion>; entitiesManager.Add(runner); ... entitiesManager.Update(); // Motion.Update() is invoked here

In ECS, it would look like this (not necessarily based on an existing framework):

struct Motion : IComponent { public float speed; public float acceleration; public Vector3 direction; } // Framework or engine automatically injects entities // that has both Transform and Motion components class MovementSystem : System { [Inject] private Container<Transform> transforms; [Inject] private Container<Motion> motions; public void Update() { // This is faster because memory access doesn't jump too much to different locations for(int i = 0; i < this.EntityCount; ++i) { Update(ref this.transforms[i], ref this.motions[i]); } } private void Update(ref Transform transform, ref Motion motion) { motion.speed += motion.acceleration * Time.deltaTime; transform.position += motion.direction * motion.speed * Time.deltaTime; } } Entity runner = new Entity(); runner.AddComponent<Transform>(); runner.AddComponent<Motion>(); Ecs.AddEntity(runner); Ecs.AddSystem(new MovementSystem()); // ... More systems // MovementSystem.Update() is invoked here Ecs.Update();

My concerns

I haven’t really programmed a full game in ECS. I’ve made a prototype that’s not really a game. Just trying to get a grasp how it works. I’m not an expert of ECS. The concerns I’m listing here are purely subjective. I’m hoping that someone who has worked on a full game using ECS can share their experience to bust these concerns.

At the top of my head, my biggest concern is maintainability. I’m big on maintainability. The OOP way is my comfort zone. It’s where I’m good at. I’ve developed patterns with it to answer most problems. Switching to ECS is already a maintenance problem by virtue that it’s not something I’m used to.

In OOP, concepts like information hiding and encapsulation results to consistent invariants. These concepts have the largest influence on how I approach programming. I think of programming like making little inventions with few user interface as much as possible. ECS just throws all of that away. Every time I expose data, I cry a little bit inside.

Sure, I could still make some data private and provide methods for consistent mutation in some instances. But in ECS, I can no longer have a component that has a reference to other components and provide methods for clean mutation among these components. The data of these components have to be exposed and the mutation logic will be implemented in systems. This implies that any other systems have access to such data and can therefore cause mutations that was not supposed to be. This may be a non problem at all. Maybe it’s just the curse of OOP. But still, I feel anxious when I think about it.

Another maintenance issue that I see is the use of structs itself. I’m so used to using reference types that I may fail to remember that structs are value types. When you pass a class type to a mutating function, the default expectation is that the passed instance would be changed. This is not the case for structs. Structs are value types so they will be copied when passed to a function. The struct instance that you passed to a mutating function will not change unless the parameters in such function has the “ref” qualifier which means to pass by reference. So if you forgot to add “ref” to your functions, you may have a buggy code. This is just one struct issue. How many more will I discover?

struct TestStruct { public int x; public TestStruct(int x) { this.x = x; } } private void Mutate(TestStruct instance) { instance.x += 5; } TestStruct test = new TestStruct(1); Mutate(test); Debug.Log(test.x); // Does it display 1 or 6?

Another problem that I foresee is I may not be able to reuse OOP based libraries effectively. I have lots of utility classes that I have developed throughout my career. They’re good because they are battle tested. Most Asset Store products are OOP based. Most open source/free libraries are OOP based. I may not be able to use them when using ECS since the library features that will be put inside components will need to be a struct to maintain cache coherence. These classes will have to be ported to conform to the data oriented way. It’s either you break cache coherence or rewrite libraries so they can be used in ECS way. If you break cache coherence just so you can use a library, by how much and how often can you do so? If you do it enough, you’re contradicting the reason why you are using ECS in the first place. If you’re not going to exploit cache coherence, you might as well use Composition. It’s a thing to balance and I don’t have the right answers.

The programming language itself is a problem. Since C# has OOP features and if you’re so used to OOP, the temptation is always there to use OOP solutions. If Unity is going to introduce ECS, I’m wishing that they’re going to introduce a new language for it. Something that does not allow classes or encapsulation. Go or Rust would be good fit. This way, developers would be forced to think in ECS and there’s no way for them to revert to OOP because the language does not allow it. It would not be strange for Unity to do this because they initially started with 3 supported languages. Keep C# but a more appropriate language should be introduced for their ECS.

Conclusion

These are my thoughts on ECS. It feels good to just let them out. I hope someone could shed some light. I hope someone can debunk my fears.

Even with these concerns, I’m still going to dive in to Unity’s ECS when it comes out. It’s a new world to explore. It’s a whole new paradigm. It will take some time to get the hang of. The OOP solutions that I know will need to have translations to ECS.