Our company has its own game engine, which is used for all developed games. It provides all the important basic functionality:
- rendering
- work with SDK;
- work with the operating system;
- with network and resources.
However, it lacked what Unity is so valued for - a convenient system for organizing scenes and game objects, as well as editors for them.
Here I want to tell how we implemented all these amenities and what we came to.
What is now
Now we have some semblance of a component system in Unity with all the important subsystems and editors. However, since we proceeded from the needs of our specific projects, there are quite significant differences.
We have visual objects that are stored in scenes. These objects consist of nodes that are organized in a hierarchy and each node can have a number of entities, such as:
- Transform - transformation of the node;
- Component - is engaged in rendering and there can be only one or not at all. Components are sprite, mesh, particle and other entities that can display. The closest equivalent to Unity is Renderer;
- Behavior - responsible for the behavior, and there may be several. This is a direct analogue of MonoBehaviour in Unity, any logic is written in them;
- Sorting is an entity that is responsible for the order in which nodes in a scene are displayed. Since our system should have been easy to integrate into already running games, with the existing and diverse logic for displaying objects, it was necessary to be able to integrate new entities into old ones. So sorting allows you to transfer control over the display order to the external code.
As in Unity, programmers create their component, behavior or sorting. To do this, just write a class, redefine the necessary events (Update, OnStart, etc.) and mark the necessary fields in a special way. In UnrealEngine, this is done with macros, and we decided to use tags in the comments.
Further in the class, taking into account the tags, all the code will be generated, which is necessary for saving and loading data, for editors to work, to support cloning and other small functions.
Automatic serialization and generation of editors is supported not only for entities that are stored in a visual object, but also for any class. To do this, it is enough to inherit it from the special Serializable class and mark the necessary properties with tags. And if you want instances of the class to be full assets (analogous to ScriptableObject from Unity), then the class should be inherited from the Asset class.
As a result, the library provides an opportunity to quickly develop new functionality. And now part of the work on developing the game, for example, creating effects, layout UI, design of game scenes, can be transferred to specialists who can cope with it better than programmers.
Main blocks
Code Generation
For the operation of many systems, you need to write quite a lot of routine code, which is necessary due to the lack of reflection in C ++ (
reflection - the ability to access information about types in the program code). Therefore, we generate most of this technical code.
A generator is a set of python scripts that parse header files and generate the necessary code based on them. For flexible generation settings, special tags are used in the comments.
We can generate code for the following subsystems:
- Serialization - used to save / load data from disk or when transmitting over a network. Will be considered in more detail later.
- Bindings for the reflection library - used to automatically display the editor to the data. Will be discussed in the chapter on the editor.
- Code for cloning entities - used to clone entities in the editor and in the game.
- Code for our lightweight runtime reflection.
→ An example of the generated code for one class can be
found here.
Parsing c ++
Almost all solutions to parsing header files led to parsing code with clang. But after the experiments, it became clear that the speed of such a solution did not suit us at all. Moreover, the power that clang provided was not necessary for us.
Therefore, another solution was found:
CppHeaderParser . This is a python single-file library that can read header files. It is very primitive, does not follow #include, skips macros, does not parse characters, and works very quickly.
We use it to this day, however, we had to make a fair amount of edits in order to fix bugs and expand our capabilities, in particular, support for innovations from C ++ 17 was added.
We wanted to avoid misunderstandings related to the uncertainty of the status of code generation. Therefore, it was decided that the generation should occur completely automatically. We use CMake, in which the generation starts at each compilation (we were not able to configure the generation to start only when the dependencies change). So that this does not take much time and does not annoy, we store a cache with the result of parsing all files and directory contents. As a result, the idle start of code generation takes only a few seconds.
Code generator
With generation, everything is simpler. There are a lot of libraries for generating anything from a template. We chose
Templite + , since it is very small, has the necessary functionality and works properly.
There were two approaches to generation. The first version contained many conditions, checks, and other code, so the templates themselves were minimal, and most of the logic and the text produced was in python code. It was convenient, because in python code is more convenient to write than in templates, and you could easily screw up arbitrarily tricky logic. However, this was also terrible, because the python code mixed with a huge number of lines of C ++ code was inconvenient to read or write. Used python-generators simplified the situation, but did not eliminate the problem as a whole.
Therefore, the current version of the generation is based on templates, and python code simply prepares the necessary data and now it looks much better.
Serialization
For serialization, various libraries were considered: protobuf, FlexBuffers, cereal, etc.
Libraries with code generation (Protobuf, FlatBuffers and others) did not fit, because we have handwritten structures and there is no way to integrate the generated structures into user code. And to double the number of classes just for serialization is too wasteful.
The
cereal library seemed to be the best candidate - nice syntax, clear implementation, it is convenient to generate serialization code. However, its binary format did not suit us, as did the format of most other libraries. Important format requirements were independence from hardware (data should be read regardless of byte order and bit depth) and the binary format should be convenient for writing from python.
Writing a binary file from python was important, since we wanted to have a platform-independent and project-independent universal script that would convert data from a text view to a binary one. Therefore, we wrote a script that turned out to be a very convenient serialization tool.
The main idea was taken from cereal, it is based on basic archives for reading and writing data. From them, different heirs are created that implement the record in different formats: xml, json, binary. And serialization code is generated by classes and uses these archives to write data.
Editor
We use the ImGui library for editors, on which we wrote all the main editor windows: scene contents, file and asset viewer, asset inspector, animation editor, etc.
The main code of the editor is written by hand, but for viewing and editing the properties of specific classes, we use the rttr library, the binning generated for it and the generalized inspector code that can work with rttr.
Reflection Library - rttr
To organize reflection in C ++, the rttr library was chosen. It does not require intervention in the classes themselves, has a convenient and understandable API, has support for collections and wrappers over types (such as smart pointers) with the ability to register your wrappers and allows you to do whatever is necessary (create types, iterate over class members, change properties, call methods, etc.).
It also allows you to work with pointers, as with regular fields, and uses the null object pattern, which greatly simplifies working with it.
The minus of the library is it is bulky and not very fast, so we use it only for editors. In the game code for working with the parameters of objects, for example, for an animation system, we use the simplest reflection library of our own production.
The rttr library requires writing a binding with the declaration of all methods and properties of the class. This binding is generated from python code for all classes that need editing support. And due to the fact that metadata can be added to rttr for any entity, the code generator can set different settings for class members: tooltips, parameters of acceptable value boundaries for numeric fields, a special inspector for the field, etc. These metadata are used in the inspector to display the editing interface .
→ An example code for declaring a class in rttr can be
found here
Inspector
The code of the editors themselves very rarely works with rttr directly. The most commonly used layer is that the object is able to draw an ImGui inspector for it. This is handwritten code that works with data from rttr and draws ImGui controls for it.
To customize the display of the data editing interface, the metadata specified during registration in rttr is used. We support all primitive types, collections, it is possible to create objects stored by value and by pointer. If a class member is a pointer to a base class, then you can select a specific descendant during creation.
The inspectors' code also assumes the support of canceling operations - when changing values, a command is created to change the data, which can then be rolled back.
So far, we do not have a system for determining atomic changes with the ability to view and save them. This means that we do not have support for saving the changed properties of the object to the scene and applying these changes after loading the prefab. And also there is no automatic creation of animated tracks when changing the properties of an object.
Windows and editors
At the moment, many different subsystems and editors have been created on the basis of our editors, code generation, and asset creation systems:
- The system of game interfaces provides a flexible and convenient layout and includes all the necessary interface elements. A system of visual scripting of window behavior was made for her.
- The system for switching the state of animations is similar to the state editor in animations in Unity, but it differs somewhat by the principle of operation and has wider application.
- The designer of quests and events allows you to flexibly customize game events, quests and tutorials, almost without the participation of programmers.
When developing all these subsystems and editors, we looked closely at
Unity ,
Unreal Engine and tried to take the best from them. And some of these subsystems are made on the side of game projects.
To summarize
In conclusion, I would like to describe how the development was carried out. The first working version was made and integrated into some game projects by a couple of people in just two months. It has not yet had code generation, and the abundance of editors that is now. At the same time, it was a working version, with which the movement forward began. This is not to say that at that time it corresponded to the main vector of the engine’s development, everything rested on the enthusiasm of several people and a clear understanding of the necessity and correctness of what we did.
All subsequent development was carried out very actively and evolutionarily, step by step, but always taking into account the interests of game projects. At the moment, more than ten people are working on the development of “our small Unity” and the development of a new version is no longer as fast and fast as it was at the very beginning.
Nevertheless, we have achieved great results in just a couple of years and are not going to stop. I wish you to move forward to what you think is right and important for yourself and for the company as a whole.