[Back to blog]You can get this blog via IPFS: ipfs get /ipns/itjac.me/blog.pdf

[Link] tags:     C  response  gamejam  
08 June 2019

Introduction and the entity system


This post was prompted by https://samsai.online/post/linux-game-jam-2019-experiences/ where Samsai, from the https://gamingonlinux.com community and Twitch, https://twitch.tv/thesamsai writes about their experiences developing "Cursed Pl@tformer", https://samsai.itch.io/cursed-platformer for the Linux Game Jam 2019 event. I do love decenteralised content, and I feel compelled to engage in decenteralised engagement - by writing a response.
In said post, Samsai details the entity system they developed for this game, and the result is a very object-oriented design, where function pointers are explicit, rather than implicit in the vtable, because of their use of C, which does not come with vtable support. I do not want to discourage that method, and I do not claim that what I write about next is a better way, I just want to talk about a different way of approaching the same problem.
In this case, the function pointer points to the logic update function of the entity. This way, all entities of a game level can be iterated through, and for each, their respective update function can be called. In their code, they call this function the "processFunc" and I think now is a good time to actually show said entity code,
struct Entity {
struct Level* level;
int width;
char hidden;
char disabled;
char sprite;
char state;
double x, y, x_vel, y_vel;
void* internal_data;
void (*processFunc)(struct Entity* entity, double delta);
} ;
It is quite the marvel.
Let's talk about the different method. Replace the "processFunc" member with an enum member, which has as its enumerations the possible logic processes the game knows, so for example LOGIC_NONE, LOGIC_PLATFORM, LOGIC_BAT, LOGIC_INFO, and so on. Then on level initialisation, an array of pointers to entities, would be allocated for each possible enumeration, and populated by each entity respectively to the logic enumeration they have assigned to themselves. Now on each game loop iteration, the "bat" logic function would process every bat in the level, and the "platform" logic function would process every platform, and so on. In such a system, an entity can belong to multiple logic arrays, for example a bat could kill but also give you a message.
Just for some C education, let's talk about the "internal_data" member. While it is a perfectly valid way to solve the problem, I want to mention "unions" (no, not just because I'm a leftist). Unions in C allow you to define memory for several different types, and the actual memory allocated will be equal to the largest member of the union. For non-pointer members of the union, you get the necessary memory allocated already when the "entity" struct is allocated, skipping one pointer indirection. This is not just a performance consideration, but pointers also complicate the mental models programmers have to keep when writing code. Also unions provide type checking. In this code, the "internal_data" member, as a union, could look something like,
union {
char *message;
struct Entity *block; // Block to remove when the button is pressed
struct {
// p0 is left-most position it travels, and p1 the right-most
int p0, p1;
// This would be for controlling the speed of the platform
float last_move_time, delta_move_time;
} platform_info;
// and so on...
} internal_data;
Either method is fine, and up to those who intend to finish the product. I will note here that in the actual code from Gitlab, dated 07/June/2019, the "internal_data" member is gone.

So why is this even interesting enough to think about writing about? I will ignore the possible argument for performance, because I do not have the energy to do an actual analysis, but most importantly, it is benign at this level of game development. It is the prevailing narrative that object-oriented code design is good, and I spent a lot of years in that mindset, and when I was convinced that it wasn't good for me, I found myself completely lost on how to not code in that way. This alternative method I'm explaining is intuative to me now, but it was not at all back then. And I would like to point out that the code we are discussing, is the result of someone moving from object-oriented programming languages, to C, and the result is a very object oriented approach. All frames of mind are difficult to escape.

Memory management


Samsai writes about their issues with a memory bug that was difficult to debug. In this case we are actually talking about an infamous kind of bug that is being debated amongst C programmers; whether data should always be initialized to 0 or not. I'm ambivalent on the subject. I did encounter these issues often enough at the start, but I have learned since and I don't get them anymore, and if the language assumes it should set the memory to 0, then extra instructions have to be used to do so. A language could assume it should always initialise to 0, but let programmers tell the compiler to not do it in a scope. These changes are never going to happen for C, but it is a worthwhile thought for new languages.
Either way, this wouldn't have been an issue with the memory management code I'm about to explain. This is not to say that there would be no issues.
The idea is that when the game is executed, we allocate once enough memory from the OS for the entire runtime of the game. The way this usually works in practice, is that we allocate more than enough, and on PC grow it if we notice we need more. Then when we release the game, we analyse how much memory our game actually uses, and set the allocation to fit that. On consoles you would know how much memory is available, so you would allocate that amount, or a bit less to have some to spare in case of emergency. Luckily, it turns out that most games, at least at what I've been experimenting with, only require a simple and very fast linear memory manager. You start by defining what we will call a memory arena,
#include <stdint.h>

struct memory_arena {
uint8_t *base;
uintmax_t used, max, watermark;
} ;
Then we create a "memory_arena" for all the memory of the game,
#include <stdlib.h>
#include <string.h>
#define KB(x) (x * 1024)
#define MB(x) (KB(x) * 1024)
struct memory_arena all_memory;

int main(int argc, char *argv[]) {
all_memory.max = MB(10);
all_memory.base = (uint8_t *)malloc(all_memory.max);
all_memory.used = all_memory.watermark = 0;
memset(all_memory.base, 0, all_memory.max); // All the memory is now zeroed

return EXIT_SUCCESS;
}
Now using this "memory_arena" as linear memory is easy,
uint8_t *memory_arena_use(struct memory_arena *mem, uintmax_t amount) {
uint8_t *rv = mem->base + mem->used;
if (mem->max < mem->used + amount) {
// Do error handling
return NULL;
}

mem->watermark = mem->used += amount;
return rv;
}
Due to how I code my games, I usually make two child memory arenas from the "all memory" one - one for the "engine" and one for the "game". But the details of why are too long of a tangent, so I'll rather explain another cool option that is possible with this method. Make two child memory arenas from the "all memory" one, one called "persistent" memory, and one "transient". The difference is that the persistent one is used for data that should persist across frames, like the entities of a level, and then the transient/temporary one is scrapped at the start of each frame, which with a linear memory manager, just means setting the "used" member to zero. This is also where the "watermark" member comes in, it lets you analyse how much memory has been used at maximum, which lets you make decisions about how much to allocate. Then you can use the transient arena for things like string manipulation before printing and not worry about how much memory use is accumulating. Or parsing the map files, because the textual data is only temporarily needed, it gets translated into other data.
Another benefit of using a method like this, is that you can now easily give accurate information on how much RAM your game requires. Also you can assume that your memory is initialised to zero, because we do that manually, which relates to the bug they were having. Now I want to note that there's some weirdness about how the Linux kernel doesn't actually give you all the memory you ask for, but I don't know the details well enough.

Game timing


I just want to make a quick mention of timing in games. I'll make it quick, because it does require some criticism of the code in Cursed Pl@tformer, and I feel especially uncomfortable with criticising code in a game jam project.
The timing code in Cursed Pl@tformer makes no sense to me. The game loop first runs code that does not depend on time, it then sleeps for however much the difference is between delta and 1 / 60, and after this it runs code that depends on time. Delta is calculated from (last - now) just before the sleep, but "last" is calculated from just before the game loop on the first frame, and then from just after the sleep on consecutive frames. When delta is less than 1/60, the code sleeps and then adds that remainder to delta. I do not think this frame timing was thought about deeply, which is fine, but I'll argue for how I do frame timing.
First, "variable time step" makes no sense to me. When I first started learning game development, a form of it that I was taught is to just use the delta time from the previous frame. I think I have since encountered variations of it, where there were accumulators involved, and the math behind this might be more correct than I know, but I doubt it. It is clear that with such a time step, you are moving things an incorrect amount, if the current frame doesn't take the exact same amount of time as the previous one, which probably never happens. With a fixed time step however the amount of time the logic takes does not matter, as long as it isn't longer than the expected frame time, because the rest of the time the process sleeps or waits on vsync. One thing that worries me about if I am correct on this, is that there are AAA titles that run smoother without vsync, which means they either do use a variable time step, or a fixed time step with a smaller delta time than the refresh rate. If it is infact variable time step, then I don't know what to make of the world.
The way I reason about frame timing is,
int target_frame_time = 16; // This pseudo code assumes these timings are in ms
// I'm actually not sure if this would be more correct with 16.0f / 1000.0f. Need to think about this more
float delta_time = 1.0f / 60.0f;
while (!quit) { // Game loop
int start_time = some_time_function();
// process input
update(delta_time);
render();
int end_time = some_time_function();
int frame_time = end_time - start_time;
int difference = target_frame_time - frame_time;
if (difference < 0) {
// This means that the frame took longer than 16 ms
// so we can decide on how to handle this.
// Maybe switch down to 30 fps
} else if (difference > 0) {
sleep(difference);
}
}
The "frame time" is actually how much time it took to process everything this frame, then we sleep until we match the target frame time.

Conclusion


Before I even started putting finger to key, I knew I wanted to come packing with some creative positivity, and use the game tech they made to make something of my own. The result was a non challenging list of maps that can be entered in my version of the game, by jumping onto the house, in the very first level of the game. I chose to make them a walk in the park, because I am currently in a state in life where platformer challenges are far from my preferred diet, which is why I never got past the first bat in the original game.
Using their tech was a lot of fun! I got nostalgic feelings from when I wrote Lua for Garry's Mod, or otherwise found other people's games, and learned their innards. And the simplicity, due to the constraints, made it effortless to get started, and by the nature of ASCII art, I could make my maps, and when I finished up the last one, I went back through the other ones, because I had gotten a bit better at it. I am completely convinced at this point that a limited number of options, are what embolden my creative output at the skill level that I am at, and I intend to figure out how to utilize that more.
I also wanted to do some programming with their tech, and ended up adding two new colors and a new entity type. I won't spoil them here, but they are all used in my maps. I also did fix up the frame timing stuff, to a similar result as described above.
My maps are amateurish, but amateurish creativity is a joy and don't let the system tell you to buy yourself happiness instead.

The original project is on Gitlab, so I registered an account and created a fork there, which can be found here - https://gitlab.com/Smilex1/linux-game-jam-2019-game