Writing a game engine in your first year: easy! (Almost)

Hello! My name is Gleb Maryin, I'm in my first year of undergraduate "Applied Mathematics and Computer Science" at the St. Petersburg HSE. In the second semester, all freshmen in our program make team projects in C ++. My teammates and I decided to write a game engine. 

Read what we get under the cat.


There are three of us in the team: me, Alexei Luchinin and Ilya Onofriychuk. None of us are experts in game development, much less in creating game engines. This is the first big project for us: before it, we did only homework and laboratory work, so it is unlikely that professionals in the field of computer graphics will find new information for themselves here. We will be glad if our ideas help those who also want to create their own engine. But this topic is complex and multifaceted, and the article in no way claims to be complete specialized literature.

Everyone else who is interested in learning about our implementation - enjoy reading!

Graphic arts


First window, mouse and keyboard


To create windows, handle mouse and keyboard input, we chose the SDL2 library. It was a random choice, but so far we have not regretted it. 

It was important at the very first stage to write a convenient wrapper over the library so that you could create a window with a couple of lines, do manipulations with it like moving the cursor and entering full-screen mode and handle events: keystrokes, cursor movements. The task was not difficult: we quickly made a program that can close and open a window, and when you click on RMB, display “Hello, World!”. 

Then the main game cycle appeared:

Event ev;
bool running = true;
while (running):
	ev = pullEvent();
	for handler in handlers[ev.type]:
		handler.handleEvent(ev);

Each event handlers attached - handlerseg handlers[QUIT] = {QuitHandler()}. Their task is to handle the corresponding event. QuitHandlerIn the example, it will expose running = false, thereby stopping the game.

Hello world


For drawing in the engine we use OpenGL. The first Hello Worldone, as I think, in many projects, was a white square on a black background: 

glBegin(GL_QUADS);
glVertex2f(-1.0f, 1.0f);
glVertex2f(1.0f, 1.0f);
glVertex2f(1.0f, -1.0f);
glVertex2f(-1.0f, -1.0f);
glEnd();


Then we learned how to draw a two-dimensional polygon and carried out the figures in a separate class GraphicalObject2d, which can rotate with glRotate, move with glTranslateand stretch with glScale. We set the color in four channels using glColor4f(r, g, b, a).

With this functionality, you can already make a beautiful fountain of squares. Create a class ParticleSystemthat has an array of objects. Each iteration of the main loop, the particle system updates the old squares and collects some new ones that it starts in a random direction:



Camera


The next step was to write a camera that could move and look in different directions. To understand how to solve this problem, we needed knowledge from linear algebra. If this is not very interesting for you, you can skip the section, see the gif, and read on .

We want to draw a vertex in the coordinates of the screen, knowing its coordinates relative to the center of the object to which it belongs.

  1. First, we need to find its coordinates relative to the center of the world in which the object is located.
  2. Then, knowing the coordinates and location of the camera, find the position of the vertex in the base of the camera.
  3. Then project the vertex onto the plane of the screen. 

As you can see, there are three stages. Multiplication by three matrices corresponds to them. We called these matrices Model, Viewand Projection.

Let's start by obtaining the coordinates of the object in the basis of the world. Three transformations can be done with an object: scale, rotate, and move. All these operations are specified by multiplying the original vector (coordinates in the basis of the object) by the corresponding matrices. Then the matrix Modelwill look like this: 

Model = Translate * Scale * Rotate. 

Further, knowing the position of the camera, we want to determine the coordinates in its basis: multiply the previously obtained coordinates by the matrix View. In C ++, this is conveniently calculated using the function:


glm::mat4 View = glm::lookAt(cameraPosition, objectPosition, up);

Literally: look at objectPositionfrom a position cameraPosition, and the upward direction is “up”. Why is this direction necessary? Imagine taking a picture of a kettle. You point the camera at him and place the kettle in the frame. At this point, you can say exactly where the frame is at the top (most likely where the kettle has a lid). The program cannot figure out for us how to arrange the frame, and that is why the “up” vector must be specified.

We got the coordinates in the basis of the camera, it remains to project the obtained coordinates on the plane of the camera. The matrix is ​​engaged in this Projection, which creates the effect of reducing the object when it is removed from us.

To get the coordinates of the vertex on the screen, you need to multiply the vector by the matrix at least five times. All matrices have a size of 4 by 4, so you have to do quite a few multiplication operations. We do not want to load processor cores with a lot of simple tasks. For this, a video card that has the necessary resources is better. So, you need to write a shader: a small instruction for a video card. OpenGL has a special GLSL shader language, similar to C, that will help us do this. Let's not go into the details of writing a shader, it’s better to finally look at what happened:


Explanation: There are ten squares that are a short distance behind each other. On the right side of them is a player who rotates and moves the camera. 

Physics


What is a game without physics? To handle the physical interaction, we decided to use the Box2d library and created a class WorldObject2dthat inherited from GraphicalObject2d. Unfortunately, Box2d did not work out of the box, so the brave Ilya wrote a wrapper for b2Body and all the physical connections that are in this library.


Until that moment, we thought to make the graphics in the engine absolutely two-dimensional, and for lighting, if we decide to add it, use the raycasting technique. But we had on hand a wonderful camera that can display objects in all three dimensions. Therefore, we added thickness to all two-dimensional objects - why not? In addition, in the future, this will allow you to make quite beautiful lighting that will leave shadows from thick objects.

Lighting appeared in between cases. To create it, it was necessary to write the appropriate instructions for drawing each pixel - a fragment shader.



Textures


To download the images, we used the DevIL library. Each GraphicalObject2dbecame fit one instance of the class GraphicalPolygon- the front part of the object - and GraphicalEdge- side part. On each you can stretch your texture. First result:


Everything that’s required from the graphics is ready: drawing, one light source and texture. Graphics - that's it for now.

State machine, setting the behavior of objects


Every object, whatever it may be — a state in the state machine, graphic or physical — must be “ticking”, that is, each iteration of the game loop is updated.

Objects that can update are inherited from the Behavior class we created. It has functions onStart, onActive, onStopthat allow you to override the behavior of the heir at startup, during life and at the end of its activity. Now we need to create a supreme object Activitythat calls these functions from all objects. The loop function that does this is as follows:

void loop():
    onAwake();
    awake = true;
    while (awake):
        onStart();
        running = true
        while (running):
            onActive();
        onStop();
    onDestroy();

For now running == true, someone can call a function pause()that does running = false. If someone calls kill(), then awake, and runningturn to false, and activity to a complete stop.

Problem: we want to pause a group of objects, for example, a system of particles and particles inside it. In the current state, you need to manually call onPausefor each object, which is not very convenient.

Solution: everyone Behaviorwill have an array subBehaviorsthat he will update, that is:

void onStart():
	onStart() 		//     
	for sb in subBehaviors:
		sb.onStart()	//       Behavior
void onActive():
	onActive()
	for sb in subBehaviors:
		sb.onActive()

And so on, for each function.

But not every behavior can be set in this way. For example, if an enemy is walking on the platform, the enemy, then he most likely has different states: he is standing idle_stay, he is walking on the platform without noticing us idle_walk, and at any moment he can notice us and go into attack state attack. I also want to conveniently set the conditions for the transition between states, for example:

bool isTransitionActivated(): 		//  idle_walk->attack
	return canSee(enemy);

The desired pattern is a state machine. We also made her the heir Behavior, since on each tick it is necessary to check whether the time has come to switch the state. This is useful not only for objects in the game. For example, Levelthis is a state Level Switcher, and transitions inside the controller machine are conditions for switching levels in the game.

The state has three stages: it has begun, it is ticking, it has stopped. You can add some actions to each stage, for example, attach a texture to an object, apply an impulse to it, set the speed, and so on.

Conservation


Creating a level in the editor, I want to be able to save it, and the game itself should be able to load the level from the saved data. Therefore, all objects that need to be saved are inherited from the class NamedStoredObject. It stores a string with the name, class name and has a function dump()that dumps data about an object into a string.  

To make a save, it remains to simply override dump()for each object. Loading is a constructor from a string containing all the information about the object. Download is complete when such a constructor is made for each object. 

In fact, the game and the editor are almost the same class, only in the game the level is loaded in read mode, and in the editor in record mode. The engine uses the rapidjson library to write and read objects from json.

GUI


At some point, the question arose before us: let the graphics, the state machine, and all the rest be written. How can a user write a game using this? 

In the original version, he would have to inherit from Game2dand override onActive, and create objects in the fields of the class. But during the creation, he cannot see what he is creating, and he would also need to compile his program and link to our library. Horror! There would be pluses - one could ask such complex behaviors that one could have imagined: for example, move the block of land as much as the player’s lives, and do so provided that Uranus is in the constellation Taurus and the euro does not exceed 40 rubles. However, we still decided to make a graphical interface.

In the graphical interface, the number of actions that can be performed with an object will be limited: flip through the animation slide, apply force, set a certain speed, and so on. The same situation with transitions in the state machine. In large engines, the problem of a limited number of actions is solved by linking the current program with another - for example, Unity and Godot use binding with C #. Already from this script you can do anything: and see in which constellation Uranus, and what is the current euro exchange rate. We do not have such functionality at the moment, but our plans include connecting the engine with Python 3.

To implement the graphical interface, we decided to use Dear ImGui, because it is very small (compared to the well-known Qt) and writing on it is very simple. ImGui - the paradigm of creating a graphical interface. In it, every iteration of the main loop, all widgets and windows are redrawn only if necessary. On the one hand, this reduces the amount of memory consumed, but on the other hand, it most likely takes longer than one execution of the complex function of creating and saving the necessary information for subsequent drawing. It remains only to implement the interfaces for creating and editing.

Here's what the graphical interface looks like at the time of the article’s release:


Level editor


State Machine Editor

Conclusion


We have created only the basis on which you can hang something more interesting. In other words, there is room for growth: you can implement shadow rendering, the ability to create more than one light source, you can connect the engine with the Python 3 interpreter to write scripts for the game. I would like to refine the interface: make it more beautiful, add more different objects, support for hot keys ...

There is still a lot of work, but we are happy with what we have at the moment. 

During the creation of the project, we got a lot of diverse experience: working with graphics, creating graphical interfaces, working with json files, wrappers of numerous C libraries. And also the experience of writing the first big project in a team. We hope that we were able to tell about it as interesting as it was interesting to deal with it :)

Link to the gihab project: github.com/Glebanister/ample

All Articles