Benjamin Frohlund
Programmer

Unicorn Engine

Unicorn Engine is a c++ game engine with level editor, and is made solely by myself and its currently under development.

The engine offers simple features to get start, some of these features are:

  • ECS (Entity-Component-System)

  • C++ Reflection System

  • 3D Rendering

  • FBX Loading/Rendering

  • 2D Sprite Rendering

  • Level Editor

  • Serialization (under development)

  • Custom Allocator For Components

Everything in the engine is made inhouse except for these 3rd party libraries

Components, Reflection, Allocation

I had two main reasons for choosing a ECS system for my game engine.

The first reason was that I had previously worked with component based systems like Unitys component system and I wanted to try something new.

The second reason was I wanted to create a engine where the Editor and "Game" didn't know about eachother, in other words no dependencies between them.

Too achieve the second reason, I had to make it so the user can create components and systems in the game project while still being able to display them in the editor without any includes or dependencies between them.

Too make this possible I created a simple reflection system that the user can register their components to. I can later use this information to display the varibles in the editor.

Using the reflection system the user can register the components type and its members using the macros. The macros will then read the information and set the TypeInfo and MemberInfo accordingly.

Because we dont want to include any files from the game project, we also can't create a instance of the component in the engine or editor. We solve this by having a std::function that returns a vector of chars in the same size as the component type.


This makes it possible to use the reflection system to read where each member is and use that offset to give the vector data. The reason why we dont return for example a void pointer is because we dont want to allocate memory outside of the ECS. The ECS should allocate and store all data related to components itself, so that all components of the same type are stored contiguous in memory.

Saying that the component allocator is a true allocator might be a bit of a reach, but it does give all components of the same type a place to live contiguous in memory.

The allocator has two functions, allocate and deallocate. In allocate we check if there is any spots in the current memory block that are not used, if so then just put the component there. If there isn't a spot open then we give it a spot after the last element in the block.

In deallocate we subtract to pointers to get the distance in bytes between them, we then divide by the component size which gives us the index where the component currently is. We add that index to the freelist where other components later can put themselfs.

Now using the reflection data and the allocated component, we can use it to display the varibles in the editor without any dependencies to the game project.

In the editor we get all components on a entity, using this information we pass in the the current member of the component we are looping aswell as a void pointer to the adress where the allocator has stored the component.

We then check the type of the member, and as in the first picture, if it's of type int then we copy from the component to get the current value. We then display this value and if it's changed we copy the changed value back to the component.

This also works with vectors

Entities And Systems

In my implementation of an ECS, a entity is just an id nothing more nothing less. Just like a system is just a function, I have no fancy classes the user needs to inherit from. All the user does is make a function and register it in the ecs world.

With systems there are ofcourse some rules the user need to follow for it to work.

One rule is that the system needs to be registered together with the components that it want to run through.

The second rule is that the function must take in a ecsWorld and a Entity as the first two arguments. This makes it possible for all systems to access the world where we can use the entity to get other components that we did not register.

In the above picture we have an example of a render system. We make a lambda where we pass in the components we want to process in the system. We also call on the MeshRenderSystem function where we do the actual work.

We then register the system/lambda to the ecs system manager, here in this function we pass in all components as template arguments, aswell as the lambda, a name for the system and when the system should run. In this case we run the system during OnRender.

We the use a internal System struct that holds some data for the ecs world that it needs to know about each system.

Then when we want to run the system we simple get the bitmask of the current entity, this bitmask is a uint32_t and it holds information about which components a entity has. The same goes for the system signature where we store what components the system wants.

Using this we can get the correct entities and pass them to the system for processing.

ECS World

The world is a container for everything that is in the scene.

The world class handles storing systems, entities and components. It also handles the creation of entities aswell as giving the user functions to interact with the entity, such as adding/removing components, check if an entity has a component and so on.

The world also holds pointers to systems that the system manager holds. The reason for doing it this way is so we can create and register a system to a specific world much easier. So in the editor you will be able to add and remove systems during runtime from the world as the user sees fits.

The heaviest operation in the ecs world class is the AddComponent function.

We begin with removing the entity from its current bitmask, which is used for faster lookup.

We then get the type information on the component from the reflection system. Then we allocate some memory for the component based on the type information. This returns us a void pointer where we can do placement new, to construct the component at the new memory location.

By doing the placement new, we can handle STL containers like std::string and std::vector. But we can also handle pointers in the components.

After all this we calculate the new bitmask and return the new component incase the user wants to do changes to it after its creation.

Just The Beginning

The ECS is ofcource just a part of what the engine can currently do, and the engine is still in full development with alot of missing features.

If you have some extra time go visit my Github page, where you can download and test the engine yourself.