How we did our little Unity from scratch





Our company has its own game engine, which is used for all developed games. It provides all the important basic functionality:





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:





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.



/// @category(VSO.Basic) class SpriteComponent : public MaterialComponent { VISUAL_CLASS(MaterialComponent) public: /// @getter const std::string& GetId() const; /// @setter void SetId(const std::string& id); protected: void OnInit() override; void Draw() override; protected: /// @property Color _color = Color::WHITE; /// @property Sprite _sprite; };
      
      





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:





→ 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:





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.



All Articles