Making a game for Playdate with Pure C. Chapter 1
Once upon a time (a year ago actually) I purchased one non typical handheld game console called Playdate.
It is tiny, yellow and has a crank which can be cranked. Oh, one more thing: it has a monochrome screen. Not black-n-white - black-n-white can display shades of grey.
But monochrome screen can display only black and white. Well, not exactly white, but something that isn't black.
Playdate console is a unique mix of primitivism and modern tech. At first sight one can think 'holy crap, who plays with this thing?'. But I can tell you for sure that Playdate already has 800+ games. So unlike million unknown handheld consoles Playdate has a really active community.
What type of game I want to make? Primitive puzzles - no. I'd like to make some kind of action like GTA with cars, physics and shooting. So I need a car! Of course the game is going to be 2D. If we have a car it means it has to be movable. And without silly top-down view like in GTA2. I want a perspective view instead and I want it to be real fancy! If we don't have 3D and to control our car we have a D-Pad it means we need a car in 8 different directions. I asked my artist to draw it and I received this:
"Please wait! Say something about game setting!" you say. The setting is pretty simple - we are rolling on our SUF armed with a machine gun in the middle of nowhere in Australian desert. And like good real animal fans we shoot emus and kangaroos. For the sake of fairness I'll clarify that animals are trying to kill us. So the game is a survivor just like 'Vampire Survivors'
Considering that Playdate has only two colors we have to work hard to make it look and feel like we are in a desert. That's why the first thing to concentrate on is requisite which surrounds us: cactuses, small sand mounds and tumbleweeds (fun fact: 'tumbleweed' in Russian is spelled literally like 'roll a field over').
Oh I forgot to mention one small but important detail: screen size is 400 x 240 pixels. I mean it is really small. It means the amount of visible objects has to be not huge but they still have to make us feeling that we are located in Australian desert.
Let's get to the code.
Officially one can use Lua or C only for development for Playdate. I don't like Lua just like other script languages (personally I like C++ the most in gamedev) - it means 'say hello to pure C!'. Well I am not a big fan of C, but it is way better than Lua. What about absence of object oriented programming? Well we will simulate objects and make it look right as much as possible cause I have 10+ years of OOP (Swift, C++, C#) which definitely formatted my brain to object oriented thinking.
The very first step of making a game is main.c file which doesn't have int main function but it has Playdate system callback
int eventHandler(PlaydateAPI* playdate, PDSystemEvent event, uint32_t arg)
This function is the only item in the layer between Playdate OS and my code. It is being called on every event. The first argument PlaydateAPI* playdate is a pointer to API of operating system of Playdate. PlaydateAPI is a cute structure which consists of substructures which store function pointers to different things like drawing, reading-writing files, showing FPS etc. The second argument of this function is
The third argument arg is useless for us currently. Let's go on.
The whole game code can be put right inside main.c file but I don't want to do it. And the reason is not because it is wrong - I really don't care what is right and what is wrong in my personal projects. I confess that sometimes I write real bullshit but I just accept myself and I am feeling ok, namasté. I want to move game logic code into a different class/module to make it comfortable and more supportable in future for me. Please note that I am not a fan of crazy clear module dividing just like Java-devs - I am trying to keep the balance. So the whole game logic finally will be put into a file named Game.
Game is imitating a class like in OOP. Game instance is created in Playdate game init event and it will be destroyed in the according game terminate event. Wait but where this instance is going to be stored? In static memory!
As I said the instance is being created inside init event (line 21). Next we call GameSetup function (line 22) which performs one time actions required after game initialization. It is a great place to argue about the necessity of this function cause somebody may say that all that stuff can be placed inside GameCreate call as well. Let's leave this opportunity to argue to people who love arguing. Back to the code. Next I bind GameUpdate function call to game tick. I cannot do it straight cause functions signatures differ: GameUpdate is int GameUpdate(Game *game) but Playdate's update callback has to be int function(void *userData). It is not a big deal - I create a function called rawUpdate which is passed as update callback and it calls my GameUpdate function. Easy-peasy.
You made it here, good work! Let me put a spoiler and show you what I had at my first iteration of game development
Let's go back to the past. Pure C looks like a well designed language until you need to work with dynamic arrays and dynamic strings. It turned out that I need two arguments if I need to pass an array to a function: pointer to data and array size. It is not good from design perspective. Or I can avoid dynamic arrays and store all objects in static arrays. They may seem easier cause we don't need to manage memory manually. But static arrays have to have length limit known in compilation time. And it is an annoying disadvantage. Let me describe why. Let's say I store cactuses inside static array. I must provide its maximum length. Let's say I put it as 100 - I can store up to 100 cactuses during game. Good? Yes until I need 101-st cactus. I lost in that point of storyline.
Fun fact: several years ago we heard about GTA3 and GTA Vice City source leaks. And I did review of this code. Dynamic objects of the city (vehicles, pedestrians and pickups (armor, hidden packages, rampages, weapons, money)) are stored in static arrays. And pickups array has limit of 512 objects. It means that if you make a mess with cops, dead bodies with a huge amount of dropped weapons and money and if you reach the limit of 512 pickups located in the game scene simultaneously once 513-rd pickup appears you'll have one of previous pickups disappearing at once even though its timeout is not fired yet.
This is exactly what I don't want to have in my game. I want to have a real dynamic array like I have in different modern programming languages. And I want to keep one array as one entity in my code. Also I would like to store array's inner stuff inside incapsulated just like we do in OOP. I want to use it just like I use std::vector in C++, Array in Swift and List in C#. Unfortunately pure C doesn't have anything similar. And I am going to create it by myself!
Let's go straight to the code:
Header file has a forward declaration of Array structure which does't exist and API for creating, interacting and destroying arrays. Important note: we use C and it means we have no destructors like we have in C++/C# or deinit-functions like we have in Swift. Those functions are callbacks which are called automatically when variable goes out of its scope and it allows destroying inner dynamic data automatically. What does it mean? It means we have to call our destructor-like functions manually. So every ArrayCreate call must have according ArrayDestroy call somewhere else. What if we forget putting according ArrayDestroy calls? Right: a memory leak 🙃. You know I am feeling myself as a software developer who went downshifting. But anyway it was my grown-up decision. And I am going to keep my responsibility on my shoulders steadfastly (do you guys speak like this in English?)
Now let me show you the implementation:
Here we have a structure called ArrayImpl. Inside ArrayCreate function we create one ArrayImpl instance not Array (which actually doesn't exist, do you remember?) but resulting pointer is casted and returned as a pointer to Array. Why? You know I honestly found this technique in SQLite source code. This is the way to divide interface and implementation and make implementation private even though there is no private keyword in this programming language. The logic is simple: API accepts Array pointer but we work with it as ArrayImpl pointer which points to real array's contents. Client doesn't know about ArrayImpl cause it is not accessible with includes. Profit!
Alright but what ArrayImpl consists of? Previous screenshot shows that it has more than two fields. Well let me describe.
- int itemSize - it is single item size. Item is a type which is stored in the array. We need single item size to know how many bytes we want to allocated for a new object during appending new object into array (just like push_back in std::vector in C++, append in Swift's Array and Add in C#'s List). But you may say 'Objection! We don't have such an argument in different languages like C++, why do we need this here?'. Good question, thank you. But the case is that we do have this argument in std::vector in C++ but it is not an obvious argument, it is a part of class'es type: when we write std::vector<T> we have T as a template parameter. Template parameter is available within all functions of the class and we can call sizeof(T) whenever we want. Same in Swift but it is done with generics which are simplified templates, C# is the same. In pure C we have no templates at all. Just like air on the Moon. It would be nice to have it but we don't. We have macros in pure C but I hate macros cause they can convert well designed code into non-readable mess. That's why we have int itemSize which expects to hold result of sizeof operator with the type which is expected to be stored in array instance.
- realloc function pointer. It may look redundant and actually it is but the fact is that PlaydateAPI(remember this structure from main.c file?) has its own realloc pointer which is used in code examples instead of standard library's one. So I decided to follow this pattern. Yes I believe that this function pointer calls standard realloc but anyway - having realloc function pointer injected also gives us a great opportunity to mock this call and cover it with unit tests. Actually I wrote 0 unit tests for Array but one day I am going to cover it with unit tests for 100%, I promise.
- void *data - it is real data pointer. Why void? Cause our array can store anything including basic types, custom structures and unions. To make a universal storage we must use some kind of universal object type like object in C# and AnyObject in Swift. What do we have in pure C? Ahaha we have void pointer. Actually C coders know that void pointer is usually used for pointing to arbitrary data in different contexts. Amount of allocated bytes pointed by data must be greater or equal size of array * itemSize. If array is empty data equals NULL.
- int capacity is capacity of data. It is equal to amount of objects that can fit in data allocated and pointed to by data pointer.
- int size is real amount of objects stored in array. What's the difference between this and capacity? Same like std::vector in C++ has: sometimes capacity may differ from size. E.g. we have 4 objects in our array, next we erase one object and have 3 objects left. Reallocating data from 4 objects to 3 will be redundant - we can just set size to 3 but capacity stays 4. It is faster - allocation may take time especially when amount of objects is huge. Also it is useful if we are going to insert object next cause we already have enough capacity to fit additional object without memory reallocation. Of course if we decide to add more objects sooner or later memory reallocation will happen, it cannot be avoided universally. But those cases have to be optimized by Array customer not Array itself.
Now you know what Array has inside. Let me remind you that I want to have something similar to std::vector from C++ or Array from Swift in pure C. And now it is time to show API's inner kitchen.
- ArrayCreate (exists in the previous screenshot) - builder function which creates an Array instance. It accepts itemSize and realloc function pointer. ArrayCreate's goal is allocating memory for ArrayImpl instance, assigning all the fields in it (if we skip any field we may find trash values there) and returning resulting pointer cases to Array not ArrayImpl. Amount of stored objects is always 0. If you seek a way of creating array from literal like you do in
// С++
auto myArray = {1, 2, 3};
// C#
var myArray = new int[]{ 1, 2, 3 };
// Swift
let myArray = [1, 2, 3]
// or even Objective-C
NSArray *myArray = @[@1, @2, @3];
you will never find it in pure C cause it is just a syntax sugar over several dedicated operations: array creation and array filling.
2) ArrayClear - function which removes all array's content.
It doesn't destroy the array itself but drops any stored objects if exist. Naming is the same as std::vector::clear in C++. I was thinking between Swift'y removeAll and C++ clear and ended up with clearcause I got used to it more. Function body takes the argument and casts it to ArrayImpl pointer. Next if obtained struct has data not equal to NULL then we drop the data and set capacity and size to zero. If the array is empty we do nothing with memory
3) ArrayGetSize - the easiest function here. It returns size of array.
Here we just cast the pointer and return its size field
4) ArrayGetObjectAt - object obtaining. In 'regular' programming languages we have operator[] but here we have pure C and any action is a function.
This function returns address of requested object at index. Array can store any type (but single array is designed to have the same type of objects) and that's why we return a void pointer. Customer has to cast received pointer to corresponding type: if array contains ints one has to cast the result to intpointer, if float - to float pointer, if custom structures or unions - I bet you know the answer. Of course it is pretty easy to make a mistake: put a wrong type during casting on a customer side. How is it fixed in different languages? C++ has templated types: std::vector<T> has T which is a template argument. And if we create a vector of int's (T is int) it means that operator[] returns int as well and nothing more. Same in Swift but the feature is called generics not templates. Same in C#. All these languages check the types during compilation. But in pure C we cannot afford this cause we are down-shifters. If indexargument is out of bounds: less than zero or greater or equal to array size then we return NULLpointer. In other languages I mentioned we receive an exception in this case but pure C has no exceptions! Anyway I don't like code with exceptions cause exception is goto operator in a different form. goto is bad - at least we were trying to get rid of it 20 years ago! That's why we return NULL. If index is valid we perform some cute pointer arithmetics and return correct address.
5) ArrayGetMutableObjectAt - same as previous but returning non-const data pointer instead of const one. Why is it so important to be moved to a dedicated function?
Our programming language is not high leveled but it has full constness as a feature. Constness is a very underestimated feature - it makes the code a lot more readable. I got used to marking variables as constants instead during development in Swift. Later when I worked in C++ projects I also tried adding const everywhere I can and sometimes I received indignant comments like 'why the hell you put those unnecessary consts here🗿'. And the fact is that const keyword is mostly ignored by regular C programmers and I don't agree with that. Try adding const wherever you can (except function arguments) and you'll notice that your code's readability improved
6) ArrayPushBack - push one object in the end of array. Same as std::vector::push_back in C++, Array.append in Swift and List.Add in C#.
This function has the fanciest logic. Object is passed as a const void pointer. First we check whether object may fit already in array's capacity. If it does then we just copying its itemSize bytes starting from its pointer. If it doesn't fit then we make memory reallocation to increase the capacity of our array. And next perform copying anyway. Please note that any object we pass no matter it is a basic type or a struct has to be passed as a pointer. So the client code may look like this ArrayPushBack(myArray, &myValue) (note the ampersand symbol). It is not very comfortable API but it allows us storing anything in such kind of arrays without templates and generics
7) ArrayDestroy - the latest array's function for today. It destroys provided array - removes it from memory like it never existed. Every array sooner or later appears here where his life ends. This is Valhalla for arrays. Every array dreams of getting there in the end of his life. Arrays who don't get there float in leaked memory like restless ghosts.
Function's body is pretty clear: first we call ArrayClear to be sure that the array has no contents. Next we drop array itself from memory.
Conclusion
Let me confess: I took the style from CoreFoundation Apple's library which imitates objects with pure C. BTW Objective-C looks a lot similar to CoreFoundation and Swift was created from Objective-C. And it is not the only way to imitate OOP with pure C - I also used GTK+ in past but I did not like its way cause there are a lot of macros spread all over the lib. And I don't like macros
===
My name is Eugene. I am a software developer from Almaty, Kazakhstan. This post is my manual translation from Russian to English. The game is already published here https://fnc12.itch.io/australian-safari. To be continued...
Get Australian Safari
Australian Safari
Survival for Playdate
More posts
- Making a game for Playdate with Pure C. Chapter 4Jun 15, 2024
- Making a game for Playdate with Pure C. Chapter 3Apr 18, 2024
- Making a game for Playdate with Pure C. Chapter 2Feb 08, 2024
Comments
Log in with itch.io to leave a comment.
The second chapter is here
https://fnc12.itch.io/australian-safari/devlog/678770/making-a-game-for-playdate-with-pure-c-chapter-2