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 as ECS reasonings are often at the antipodes from the OOP ones.
Being Object-Oriented Programming so popular 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 mix 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 used 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 relationship work and avoid painful conflicts. 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 the notable exception of implementing GUI without a dedicated ECS based framework). 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. Game development, as we know, encompasses a huge variety of problem domains and forcing ECS to solve all these problems can be as demanding as trying to solve the same problems with OOP only.

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 ended up realising that the lack of ECS based frameworks developed to handle specific responsibilities contribute to the disorienting feeling of using ECS for the first time. For example, we found developing GUIs in ECS anything but frictionless. However, since the parallel between ECS and the OOP MVC pattern is obvious 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 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 is a procedural library that doesn’t use objects. This makes it simpler to call public functions to feed directly entities data. For example:

DrawBox is a procedural function that draws boxes using, in this case, data that comes directly from entities.

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.

An old working example of this can actually be found among my GitHub repositories as well and I discuss it in this article:


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. Once the user sees DOTS ECS as an engine library instead of a game framework, they will realise its true power that will open 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 DOTS ECS physic library with all the performance benefits that come with it.

DOTS ECS shows a sound strategy to develop ECS based libraries that don’t need to share the same framework used to develop the game. This approach is based on the synchronization of entity data between the game and the libraries. In this case, the DOTS entities would be closer to the Unity Engine more than the game layer. In Gamecraft, for example, the DOTS entities are just Havok Rigidbodies.

A synchronization layer is written on purpose and provides engines to synchronise the Svelto.ECS entities with the DOTS entities and vice versa. A synchronization system/engine 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 are 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 mentioned in my past articled, 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 the OOP abstraction layer. This solution may even result superior to the EntityViewComponent approach in specific cases. 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 DOTS ECS 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. The same reasoning can be applied to any object provided by other libraries. The miniexample shows Performance is as high as the DOTS ECS 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 the OOP abstraction layer because coders must not have direct access to the objects that the layer abstracts. If the layer would allow direct access to the objects, 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 inside ECS systems. This will invariably lead 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 black-boxed and the objects only used internally by the few engines present in the layer that have the responsibility to access the objects for specific abstracting reasons. In the Doofuses example, I use the game objects 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 c# these black-boxed layer can easily and intuitively be package in separate assemblies, so that the rule to use objects internally will be enforced by the compiler.

The Resource Manager

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

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. The abstraction layer must also provide a way to register objects, if necessary, through an object manager interface that I usually call Resource Manager. In this specific case, the registration of the objects is not in ECS pure form. I resorted to mixing programming paradigms 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 ECS centric. 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 (which cannot hold references to objects). The GameObjectManager (our resource manager) holds all the references to the prefabs registered, create new instances when requested and converts IDs back to gameobjects.

As you can see from the mini-example code, the engines outside the GO abstraction layer won’t ever access game objects directly and they look almost exactly like the DOTS ECS version of the same demo, while the Synchronisation engine, encapsulated inside the Gameobject abstraction 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. using Abstraction Layers and EntityViewComponents 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 game object, which may be a bit more interesting than setting a position.

In MoveSpheresEngine the object is accessed through the EntityViewComponent interface. This results in less code to write because the 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 using OOP features directly, using them in a complex scenario will result in slower and more boilerplate-y code than using the OOP abstraction layer.

Since usually entity data must be accessed multiple times, writing pure ECS engines and then synchronizing the component values once with the underlying objects, results in faster code as accessing objects directly (through the EntityViewCompnent) 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.


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 scenario. 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.4 8 votes
Article Rating
Subscribe
Notify of
guest
2 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Seideun
Seideun
5 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
5 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.