There are several reasons why programmers have a hard time wrapping their heads around ECS concepts. Having learned to code with an OOP centric language is one of them. Being ECS so far from the Object-Oriented design makes things more difficult for those who have already a hard time handling one single way to model the classes of the problems to solve.
Being Object-Oriented Programming so diffuse means that it is not easy to get away without using it, so like it or not, depending on the development platform, it can be necessary to mingle ECS and OOP code. However, this is not a bad thing per se, neither is a paradox as the same already happens when using OOP with a multi-paradigm language. With these languages (like c++ and c#) OOP is in fact regularly mixed with procedural data structures and concepts.

When pure ECS code cannot be used, code may appear bloated and awkward due to this forced relationship with OOP and this is why, in this article, I will show how to make this marriage work to avoid a painful divorce. After all, it’s not like ECS can solve elegantly ALL the possible programming problems. With Gamecraft, which is an ECS-centric product, we found some limitations, although none were crippling our development process (with one exception which I will mention in a moment). Admittedly, Gamecraft is a particular game with (a huge variety) of specific problems, so for other kinds of games ECS may not work out so well, although this is something that I still have to experience myself. Albeit, it’s important to note that usually not game-applications are focused on a much smaller set of classes of problems that can be more suitable to specific paradigm approaches. All my conclusions are instead based on the practical application and consequential empirical observations, to game development, that as we know, encompasses a huge variety of problem domains. Trying to force ECS to solve all these problems can be as demanding as asking pure OOP to solve all these problems.

On the other hand, I have spent a considerable amount of time investigating what the limits of the ECS-centric approach are and although some are more evident than others, I so far ended up realising that the lack of ECS based frameworks developed to handle specific responsibilities contribute to this feeling. For example, we found developing GUIs in ECS anything but frictionless. However, since the parallel between ECS and the OOP MVC pattern is evident to me, I realised that the problem is more due to the fact that an ECS based GUI Framework doesn’t exist, more than that ECS is awkward to use for GUIs.


Using Procedural Libraries

Before digging further into the issues of the weird relationship we are discussing in this article, let’s imagine a world without OOP:

ECS is closer to procedural programming than it is to the Object-Oriented programming. Hence it’s natural that ECS would work better with procedural based libraries instead of OOP libraries. Without needing to handle objects, procedural libraries are perfect companions.

In MiniExample 4: SDL, the SDL library used is a procedural library that doesn’t use objects. This makes it simpler to just call public functions and feed directly entities data. For example:

In MiniExample 4, _graphics is not correctly designed, as it should be a stateless class. However when mixing programming models, injecting dependencies in engines is allowed and better than use static stateless classes, as implementation can change for testing purposes.

Another example can be found in Gamecraft where we use a Compute shader-based rendering pipeline to render almost everything is seen in the game (except the main character and the GUI). Uploading compute buffers to the GPU results to be very ECS friendly as components arrays can be uploaded as they are.

Uploading the array of transform components to the GPU looks similar to:

in this case, GPUInstancerAPI is not injected but used as a stateless collection of function pointers, with the limits that can arise for testing.


Using ECS Libraries

Games are complex beasts. It’s not just about the high-level logic, but many low-level aspects must be developed too. These low-level aspects are usually provided in different forms, depending on the platform adopted. However what if the platform adopted is written in ECS to start with?

This is what’s happening with unity DOTS. When you view the Unity ECS framework as an engine library instead, UECS shows is true power and opens the door to ECS centric applications instead of OOP centric applications.

In Gamecraft, all the game logic is written with Svelto.ECS, but we use the UECS physic library with all the performance benefits that come with it.

Viewing UECS as a library shows a sound possible strategy to develop ECS libraries. The approach is based on the synchronization of entity data. In this case, the UECS entities would be closer to the Unity Engine more than the game layer. In our case, for example, the UECS entities are just RigidBodies.

A synchronization layer is written on purpose and provides engines to synchronise the Svelto.ECS entities with the UECS entities. A synchronization system can look like this:

In case you may wonder, performance is not an issue here. The code is vectorised by Unity Burst and executed in parallel by Unity Jobs, you will need hundreds of thousands of entities in game before such a copy strategy can start to be an issue performance-wise.

The same strategy is demonstrated in MiniExample 1: Doofuses (UECS based) where I use the package Hybrid Renderer ECS package to render the Doofuses meshes.


OOP abstraction layer

In order to understand this chapter, is important to have clear in mind that I always talk in my articles about ECS-centric applications and as discussed, ECS-centric applications may need to work with OOP libraries. As the two design approaches are almost orthogonal (I see systems executed horizontally, while objects methods called vertically), integrating an OOP library is much less intuitive and fluid than what we have seen so far. In order to solve the problem, I mainly use two approaches. The atypical and Svelto.ECS based EntityViewComponent approach is what I often mention, however, this article is about ECS in general, so I am going to present a solution that can be used with whatever ECS implementation. I call this solution OOP abstraction layer. This solution may even result superior to the EntityViewComponent approach. I have written a couple of new mini-examples to see how the OOP Layer works.

Let’s start from MiniExample 1: Doofuses with GameObjects.

This example is particularly important for unity developers. It shows how I extensively use DOTS without using UECS at all. The approach is very similar to what in Gamecraft is done for the rendering path, but in this case, I want to abstract Gameojects as actual OOP objects so that you can apply the same reasoning for instances of classes provided by other libraries. Performance is as high as the UECS version of Doofuses, with the exception of the Gameobject based rendering pipeline, which becomes the bottleneck.

Image

There is a philosophical reason behind the OOP abstraction layer as well. Coders by their nature tend to take the path of least resistance. This path is there if allowed to be there and if the framework doesn’t control where the path leads it can take to dark places where the coder will get lost.

Coders by their nature tend to take the path of least resistance.

myself

It’s called OOP abstraction layer because coders must not have direct access to the objects that the layer abstracts. If the layer would allow access to the objects directly, it wouldn’t be an abstraction layer anymore. More importantly than that, coders will 100% absolutely start to use and abuse those objects, accessing their public data and methods. This will invariably lead to coders mixing ECS and OOP, but because of the path of least resistance, a coder who is learning ECS, will stop thinking in terms of entities and revert to think in terms of objects which, in a short time, would make the coder abandon ECS and never look back.

For this reason, the OOP abstraction layer must be packaged and the objects only used internally by the few engines present in the layer that have the responsibility to access the objects for specific abstract reasons. In the Doofuses example, I use the gameobjects exclusively to set their positions. This is a straightforward case and it’s true that may become more bloated for more complicated scenarios, but the bloat will be encapsulated in the layer and hidden from the final user.

In the Doofuses Example, the packaged abstraction layer looks like:

an asmdef generates a separate assembly. The internal keyword becomes meaningful.

In this case, I am using a different strategy to create abstracted code. I could have gone with an Entity Synchronisation strategy like previously seen with the UECS approach, but instead, I am using the strategy I adopt to code complex applications with different layers of abstraction. The rule is that the abstraction layer must not only provide the way to compose its systems (as you can see in GameObjectToSveltoCompositionRoot), but it must also provide the components that specialised entities in the specialised layers must include to be processed by the abstracted layer engines. It also must provide a way to register objects if necessary through an object manager interface. In this specific case, the registration of the objects is not in ECS pure form. I resorted to mixing programming models because of lack of time (I write these demos in my spare times), but in a complex scenario, it would be definitively possible to hide the RegisterPrefab method as well. However mind that this is only for academic purposes, as I personally don’t have any problem in mixing ECS, procedural and OOP programming, as long as they are correctly abstracted and the leading one being ECS (which is the point of this article). RegisterPrefab is the only exposed functionality and it has the responsibility to convert a GameObject Prefab to an ID, so that it can be used inside ECS components. The GameObjectManager holds all the references to the prefabs registered, instances them when requested and converts IDs back to gameobjects so that the package systems can call their methods. In this specific example, I even use gameobjects in a pooled fashion way, so without a direct link between entities and gameobjects, but with gameobjects fetched from a pool linked to the Svelto group.

As you can see from the mini-example code, the engines outside the GO abstraction layer won’t ever access directly the game objects and they look almost exactly like the UECS version of the same demo, while the Synchronisation engine, encapsulated inside the GO layer, looks like:

I got to the concept of OOP abstraction layer after I analysed the limits of using the Svelto EntityViewComponent approach to wrap and abstract objects. They are two different approaches and they are both valid. Together they solve elegantly most of the problems related to OOP interaction, however, the OOP abstraction layer is a strategy that again, can be used with whatever ECS implementation, therefore more valuable to realise the true potential of the ECS-centric approach.

Another demo I coded is found in MiniExample 6: OOP Abstraction layer.

This demo has been made to show how the same problem can be approached with EntityViewComponents and the OOP abstraction layer. It shows that the EntityViewComponent approach is more compact, but way less pure than the OOP abstraction layer. In this example, I am showing how to change the parent of a gameobject, which may be a bit more interesting than setting a position.

In MoveSpheresEngine the object is actually directly accessed. This results in less code to write because the abstracted synchronisation engine is now not necessary anymore. The simplicity of the EntityViewComponent approach can lure the user to prefer it over the OOP abstraction layer approach. While the EntityViewComponent approach won’t allow mixing OOP with ECS using OOP features (is not possible to work with references or use methods), in a complex scenario will result in slower and more boilerplate-y code than using the OOP abstraction layer. When the entity data must be accessed multiple times, writing pure ECS engines result in faster code as accessing objects directly breaks the CPU cache.

At this point, a careful reader could wonder how to handle objects that react to events, so how to abstract the callbacks managed directly by the objects. In this case, I believe that a framework-agnostic solution through the OOP abstraction layer concept is still possible, while instead Svelto.ECS provides specialised tools to solve this problem through the use of DispatchOnSet and DispatchOnChange functionalities, which are not relevant to this article. The idea is that there must be a way for the object callback to delegate the reaction to specific systems that have the responsibility to act on the notification and not try to make the objects modify the entities values directly.


Conclusions

In a Multi-Paradigm scenario, an ECS-centric application can use objects. The user can decide, for example, to inject dependencies inside engines to be used outside the ECS setting. The user must be aware though that abusing this flexibility could lead to wrong paths, so to avoid a dangerous situation where too much OOP code is used across the project systems, OOP abstraction is desirable.

I, therefore, presented two ways in Svelto.ECS to abstract objects. The OOP abstraction layer strategy is important because it can be applied with any ECS framework regardless of its implementation details.

The Svelto.ECS exclusive EntityViewComponent model approach may be more convenient in specific cases, but in other could instead lead to performance degradation and boilerplate code.

As usual, for me is very important to receive your feedback about this article, Svelto.ECS and what you want me to write about in future, especially if you have specific challenges to face while developing with the ECS model.

4.3 7 votes
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Seideun
Seideun
3 months ago

I think it needs practice to really understand what this article tries to convey. When I first read it I didn’t learn much from it. It was not until I got hands dirty with a demo.that I realized I was missing something and got back to study it again.

I recommend anyone reading this article later on to build a demo as you learn, if you did not have much experience with ECS, or Svelto, in particular.

Seideun
Seideun
3 months ago

While the EntityViewComponent approach won’t allow mixing OOP with ECS using OOP features (is not possible to work with references or use methods), in a complex scenario will result in slower and more boilerplate-y code than using the OOP abstraction layer.

Too true. I was using the ViewComponent approach and soon my code became spaghetti, dried one.