I recently released my first game
BYTEPATH and it seemed to me that it would be useful to write down my thoughts about what I learned in the process of its creation. I will divide these lessons into “soft” and “hard”: by soft, I mean ideas related to software development, hard ones are more technical aspects of programming. In addition, I will talk about why I want to write my own engine.
Soft lessons
I will inform for the sake of context that I started making my own games about 5-6 years ago and I have 3 “serious” projects that I worked on before the release of the first game. Two of these projects are dead and completely failed, and the last one I temporarily suspended to work on BYTEPATH.
Here are the gif-animations from these projects. The first two projects failed for various reasons, but from a programming point of view they failed (at least, as I see it) because I tried too often to be too smart and generalized too much in advance. Most soft lessons are connected with this failure, so it was important to say so.
Premature Compilation
So far the most important lesson I learned from this game is that if there is a behavior that repeats for several types of entities, then it is better to copy-paste it by default, rather than performing abstraction / generalization too early.
')
In practice, this is very difficult to achieve. We, programmers, are accustomed to notice repetitions and strive to get rid of them as soon as possible, but I noticed that usually this impulse creates problems more often than it solves. The main problem he creates is that generalizations are often wrong, and when a generalization is wrong, it binds the structure of the code to itself, and this is much more difficult to correct and change than in the absence of generalization.
Consider an example of an entity that performs
ABC
actions. At first, we encode
ABC
directly in essence, because there is no reason to do otherwise. But when it comes to another entity that performs
ABD
, we analyze everything and think “let's take
AB
out of these two essentials, and then each of them will process only
C
and
D
independently,” which seems quite logical, because we abstract
AB
and will be able to reuse them in other places. If new entities use
AB
in the same way they are defined, this is not a problem. Suppose we have
ABE
,
ABF
and so on ...
But sooner or later (and usually it happens earlier) an entity appears that requires
AB*
, almost similar to
AB
, but with a small and incompatible difference. Then we can either change
AB
with
AB*
in mind, or create a completely new part that will contain
AB*
behavior. If we repeat this exercise several times, then in the first case we will come to a very complicated
AB
with various switches and flags for different behaviors, and in the second case we will return to the first cell of the field, because all slightly different versions of
AB
will still contain a bunch duplicate code.
At the core of this problem is the fact that every time we add something new or change the behavior of something old, we must do it taking into account the existing structures. To change something, we must always think “will it be in
AB
or in
AB*
?”, And this seemingly simple question is the source of all the problems. This is because we are trying to insert something into the existing structure, and not just add something new and make it work. It is impossible to overestimate the difference in simply doing what is needed and by taking into account the existing code.
Therefore, I realized that at first it was much easier to choose copy-pasting code by default. In the example shown above, we have
ABC
, and to add
ABD
we simply copy
ABC
and delete part
C
, replacing it with
D
The same applies to
ABE
with
ABF
, and when we need to add
AB*
, we simply copy-paste
AB
again and replace it with
AB*
. When we add something new to this scheme, it is enough for us to simply copy the code from a place where a similar action is already being performed, and to change it without worrying about how it will fit into the already existing code. It turned out that this method is much better to implement and leads to fewer problems, although it looks counterintuitive.
Most tips don't fit single developers.
There is a mismatch of contexts between most of the tips for programmers from the Internet and what I actually have to do as a single developer. The reason for this is as follows: first, most programmers work in a team with other people, so advice is usually given with this assumption; secondly, most of the software created by people must exist for a very long time, but this does not apply to an indie game. This means that most of the programmer's advice is practically useless for the solo development of indie games, and because of this I can do a lot of things that are impossible for other people.
For example, I can use global values, because very often they are useful, and as long as I can hold them in my head, they do not pose a problem (for more on this, see
part 10 of the BYTEPATH tutorial ). Also, I can not comment too much on my code, because I keep most of it in my head, because the code base is not very large. I can create scripts that work only on my machine, because no one will need to build the game, that is, the complexity of this step can be greatly reduced and I will not need special tools to do the work. I can have huge functions and classes, and since I create them from scratch and know exactly how they work, their huge volume is not a problem. And I can do all this because, as it turns out, most of the problems associated with them are manifested only in teams working on software with a long lifespan.
From working on this project, I learned that nothing really bad happened when I did all these “bad” things. Somewhere on the border of consciousness, I always remembered that to create indie games I didn’t need super-quality code, given the fact that many developers created great games using very bad code writing practices:
casenpai: I am horrified by what you look like in the 864 case code.
Toby Fox (author of Undertale): I do not know how to program, lol.And this project was supposed to confirm this opinion to me. It is worth noting - this does not mean that you can relax and write trash code. In the context of developing indie games, this means that most likely it is worth fighting this impulse of most programmers, with the almost autistic need to do everything right and clean, because it is the enemy that slows down your work.
ECS
The Entity Component System pattern is a good, real example of the controversy that has been said in the previous two sections. After reading most of the articles, it becomes clear that indie developers consider inheritance a bad practice, and that we can use components, creating entities from the Lego constructor, and that they make it much easier to use reusable behavior, and literally everything in creating a game becomes easier.
By definition, the desire of programmers to ECS speaks about premature generalization, because if we view things as Lego bricks and think about how we can assemble new things from them, then we think in terms of reusable pieces that can be combined in some useful way. in a way. And for the reasons I have listed in the section on preliminary synthesis, I think this is COMPLETELY WRONG! This exact scientific graph explains my position well:

As you see, the principle of “yolo-coding” that I advocate is at first much simpler and gradually becomes more difficult: the complexity of the project increases and the yolo-technology begins to demonstrate its problems. On the other hand, ECS is much more difficult at first - you have to create components, and this is by definition more difficult than creating just working elements. But over time, the utility of ECS becomes more and more obvious and at some point it wins over yolo coding. My point is that in the context of the majority of indie games, the moment in which ECS becomes the best investment is never coming.
Speaking of the inconsistency of context: if this article is popular, then some AAA developer will appear in the comments and say something like, “I’ve been working in this industry for 20 years, and this silly person is a complete BODY !!! ECS is very useful, I have already released several AAA games that have earned millions of dollars, which have been played by billions of people around the world !!! Stop carrying this nonsense !!! "
And although this AAA developer will be right that ECS is useful for him, this is not always true for other indie developers: due to the mismatch of contexts, these two groups solve very different tasks.
Anyway, it seems to me that I, as I could, conveyed my point of view. Does this mean that ECS users are stupid or stupid? Not. I think that if you are already used to using ECS ​​and it works for you, you can use it without thinking. But I believe that indie developers in general should be more critical of such solutions and their shortcomings. I think Jonathan Blow’s thought is very appropriate (in no way do I think he would agree with me regarding ECS):
Avoid splitting behavior into multiple objects.
One of the patterns that I didn’t seem to have avoided is the splitting of one behavior into several objects. In BYTEPATH, this was mainly manifested in the way I created the “Console” room, but in Frogfaller (the game I did earlier) this is more obvious:
This object consists of the main body of a jellyfish, from the individual legs of a jellyfish and a logical object, connecting everything together and coordinating the behavior of the body and legs. This is a very clumsy way to encode such an entity, because the behavior is divided into three different types of objects and their coordination becomes very difficult, but when I have to encode an entity in a similar way (and there are many complex entities in the game), I naturally choose this solution. .
One of the reasons why I choose this separation by default is that each physical object must be contained in the code in one object, that is, when I want to create a new physical object, I also need to create a new instance of the object. In fact, this is not a strict rule or restriction that is mandatory for execution, it is just very convenient for me because of the way
I designed the architecture of my physics API .
In fact, I have been thinking about how to solve this problem for a long time, but I still could not find a good solution. Simple coding in just one object looks terrible, because you need to perform coordination between various physical objects, but the separation of physical objects into regular objects with their subsequent coordination also seems unacceptable and incorrect. I do not know how other people solve this problem, so I am waiting for your advice!
Hard lessons
Their context is that I wrote my game on Lua and with
LĂ–VE . I wrote 0 lines of code in C and C ++, everything was written in Lua. Therefore, many of these lessons are related to Lua itself, although most of them apply to other languages.
nil
90% of bugs received from players are associated with access to
nil
variables. I did not track the statistics of which types of access are more / less frequent, but most often they are associated with the death of an object when another object stores a link to this dead object and tries to do something with it. I think this refers to the category of "term life" problems.
The solution to this problem in each case is usually implemented very simply, it is enough to check whether the object exists and only after that perform actions with it:
if self.other_object then doThing(self.other_object) end
However, the problem of coding in this way is that I refer to another object by reinsuring too much, and since Lua is an interpreted language, such rare bugs occur with branches of code. But I can’t come up with any other way to solve this problem, and since it is a serious source of bugs, it seems to me that it is right to have a strategy for correct processing them.
In the future, I am thinking of never referring from one object to another directly, but instead refer to them through their id. In such a situation, when I want to do something with another object, I first have to get it by its id, and then do something with it:
local other_object = getObjectByID(self.other_id) if other_object then doThing(other_object) end
The advantage of this approach is that it forces me to receive an object every time I want to do something with it. In addition, I will never do anything like this:
self.other_object = getObjectByID(self.other_id)
This means that I never keep a permanent link to another object in the current one, that is, errors cannot happen due to the death of another object. This does not seem to me a very desirable decision, because every time I want to do something, it adds a lot of unnecessary. Languages ​​like MoonScript help a little with this, because there you can do something like this:
if object = getObjectByID(self.other_id) doThing(object)
But since I will not use MoonScript, it seems to me that I will have to accept this.
Greater control over memory allocation
Although I will not argue that garbage collection is bad, especially considering that I am going to use Lua for my next games, I still really dislike some of its aspects. In C-like languages, the occurrence of a leak is annoying, but in them we can usually approximately understand where it occurs. However, in languages ​​like Lua, the garbage collector is like a black box. You can look into it to get hints about what is happening, but this is not the ideal way to work. When you have a leak in Lua, it turns out to be a much bigger problem than in C. This is complemented by the fact that I use the C ++ codebase that I don’t own, namely the LÖVE codebase. I don’t know how the developers set up memory allocation for their part, so from the Lua side it’s much harder for me to achieve predictable memory behavior.
It is worth noting that in terms of the speed of problems with the Lua garbage collector, I do not have. You can control it in such a way that it works with certain restrictions (for example, so that it does not start up for n ms), so there are no problems with this. The only problem is that you can tell him not to run for more than n ms, and he will not be able to collect all the garbage that you generated per frame. Therefore, maximum control over the amount of allocated memory is desirable. There is a very good article on this topic:
http://bitsquid.blogspot.com.br/2011/08/fixing-memory-issues-in-lua.html , and I will tell you more about it when I get to the engine in this article .
Timers, input and camera
These are three areas in which I am very pleased with the solutions I have received. For these common tasks, I wrote three libraries:
All of them have APIs that seem very intuitive to me and make my life very easy. While the most useful for me was Timer, because it allows me to implement all sorts of solutions in a simple way:
timer:after(2, function() self.dead = true end)
This code kills the current object (self) after 2 seconds. Also this library makes it very convenient to implement tween transitions:
timer:tween(2, self, {alpha = 0}, 'in-out-cubic', function() self.dead = true end)
This line allows you to smoothly change (tween) the
alpha
attribute of an object to 0 for 2 seconds using tween
in-out-cubic
mode, and then destroy the object. This allows you to create the effect of gradual dissolution and disappearance. You can also use it to make objects flicker on impact:
timer:every(0.05, function() self.visible = not self.visible, 10)
This code 10 times every 0.05 seconds switches the value of
self.visible
between true and false. This means that it creates a flicker of 0.5 seconds. As you can see, the library can be used almost infinitely. This was made possible by the way Lua works with its anonymous functions.
Other libraries have an equally trivial API, which is powerful and useful. The camera library is the only one that turned out to be too low-level, but this can be improved in the future. Its meaning is to be able to realize something similar to what is shown in this video:
But in the end I created something like an intermediate layer between the very basics of the camera module and what is shown in the video. Since I wanted the library to be used by people using LĂ–VE, I had to make fewer assumptions about which types of attributes might be available. That is, some of the features shown in the video cannot be realized. In the future, when I will create my own engine, I will be able to admit everything I want about my game objects, that is, I will be able to implement the correct version of the library that can do everything shown in this video!
Rooms and Areas
(Room) (Area). — «» «». , . — , . Area «» (spaces). Area Room ( , , Area
addGameObject
,
queryGameObjectsInCircle
, ..):
Area = Class() function Area:new() self.game_objects = {} end function Area:update(dt)
Room = Class() function Room:new() self.area = Area() end function Room:update(dt) self.area:update(dt) end
, , . , - , , , Area .
, Area, . BYTEPATH . , Room Area, , .
snake_case camelCase
snake_case camelCase . snake_case , /, - CamelCase. : camelCase . snake_case , .
Engine
, , . LÖVE — , , . , Steamworks, HTTPS, Chipmunk, C/C++, Linux , , .
, , C/C++ . C, , , Lua , . , , .
, LÖVE Unity. , — Throne of Lies:
«», ( , ) . , , , . . /r/gamedev
. , , :
, . , , , . , . , , , . -, ; , . Unity , . : . , , , . . , . . Unity , - , .
, Async, Vulkan, FB standalone , , ( FB ..), UI , , , , Scroll Rect' , ( , Unity).
, … , . , UI . Play, Stop, , , .
… (collab) , — , , , . . collab gitlab CE . . — 2-3 ( , ), Unity . -… Play, 2 . 2-3 . 10 .
… Unity , , — . , , Unity, .
… UNET? . , . , , - , , , . , , . , Photon , , . , . . , … … . : , , .
. , : 3D, Unreal. . Unity-, . , - Unity. , Unity .
, , Unity, , , , . , - Unity, . , , Unity:
:
:
Unity, , , , . : Unity Unity .
, , , Unity: . , LÖVE — , -. , . xblade724 , Unity. .
/, . , : Unreal, Epic , Fortnite; Monogame, ; GameMaker, YoYo Games .
, , . , ? - 5% , , , ?
, , , , , , . Unreal , 2D-, Unreal — , Monogame , C#, GameMaker , . — .
, , :
C/Lua
C/Lua ( , ): . Lua - C, , , Lua . , C. , Lua, , , , , , .
. — C. , , , Lua. , . Stingray Engine:
, , .
, , C/Lua , . - Lua, , , LĂ–VE. , .
, Steamworks, Twitch, Discord , API, , , C/C++, . , LĂ–VE, , , .
Unity Unreal, , , , , - , .
C/C++ , .
, Unity Unreal : . , , , , . , , , , , .
, — . Bloons TD5, - Steam 10 . . , Steam — , . , C, , , SDL Emscripten, .
,
. . // ( - , ), , . , , (, ) .
. , . , , , , , . , .
, , . , .
, , , , . Lua
, 10 10 . , Lua , C.
, — . , / LÖVE, Lua ( Linux) — . Lua LÖVE , , , , , . , LÖVE ( ), .
, - , , . , , , , , - . . , .
, — , , ( , , )!