The object-oriented programming techniques I used for the C code of Kuroobi
Hello everyone,
Lately I have seen a lot of people asking on what language to choose when developing for Playdate. There is a lot you can use now, the defaults are Lua and C but you can also use C++, Rust, Swift, Nim, Zig and even Typescript!
For Kuroobi I went for C (sorry if you expected a more original answer) because it is a language I am comfortable with. I already wrote a small game engine called Melice based on OpenGL. The Playdate SDK is very well done and it was straightforward to port my engine for Playdate. I also wanted to display a lot of things on screen and it seemed easier to do in C than Lua.
How Melice works
Melice is heavily influenced by object oriented programming. My everyday job involves a lot of Java code so my mind tend to find object-oriented solutions to problems. C is not an object oriented language but it is still possible to write object-like code. It was also a good exercise because it made me realise how Java and Javascript works behind the scene.
I recommend reading this StackOverflow question and this blog post about object-oriented style programming in the Linux kernel if you want to dive further.
The main idea is to use structs to represent objects, and to use function pointers inside a class struct to implement methods and handle inheritance.
MELSprite and MELSpriteClass: a parent struct for all sprites
One of the most important structs in Melice is MELSprite, which represents a generic sprite that can be displayed on the screen. A MELSprite has a reference to a MELSpriteClass, which contains pointers to the methods that are common to all sprites, such as destroy (to deinit and dealloc the sprite) or save (to write its state to the disk).
This way of writing classes is reminiscing of how Java handle classes: each object has a class
member and the class maintains a function table.
typedef struct { void (* _Nonnull destroy)(LCDSprite * _Nonnull self); void (* _Nullable collidesWithPlayer)(LCDSprite * _Nonnull self, LCDSprite * _Nonnull player); void (* _Nullable save)(MELSprite * _Nonnull self, MELOutputStream * _Nonnull outputStream); } MELSpriteClass;
typedef struct melsprite { const MELSpriteClass * _Nonnull class; MELSpriteDefinition definition; AnimationName animationName; MELAnimationDirection animationDirection; MELAnimation * _Nullable animation; MELRectangle frame; MELDirection direction; MELHitbox * _Nullable hitbox; int hitPoints; } MELSprite;
For each specific type of sprite, such as the player, the enemies, the items, etc., I create a struct that has a MELSprite as its first member, named super
.
typedef enum { PlayerCharacterKatsuo, PlayerCharacterSaki } PlayerCharacter;
typedef struct { MELSprite super; MELBoolean isDead; float speed; float maximumSpeed; float acceleration; PlayerCharacter character; } Player;
This way, I can write generic functions that takes a MELSprite* and pass any sprite using &aSpriteVariable->super
, for example:
MELSpriteSetAnimation(&self->super, AnimationNameStand);
To create a new sprite, I write constructor functions that allocates memory for the struct, initializes its fields, and assigns the appropriate class. For example, this is how I create a new player sprite:
static void destroy(LCDSprite * _Nonnull sprite); static void save(MELSprite * _Nonnull self, MELOutputStream * _Nonnull outputStream);
// The Player class implements the destroy and save methods from MELSpriteClass. static const MELSpriteClass PlayerClass = (MELSpriteClass) { .destroy = destroy, .save = save, };
LCDSprite * _Nonnull PlayerConstructor(MELSpriteDefinition * _Nonnull definition, MELSpriteInstance * _Nonnull instance) { Player *self = playdate->system->realloc(NULL, sizeof(Player)); *self = (Player) { .super = { .class = &PlayerClass, .definition = *definition, .frame = { .origin = instance->center, .size = definition->size, }, .direction = MELDirectionRight, .instance = instance, }, }; self->super.hitbox = MELSpriteHitboxAlloc(&self->super); MELSpriteSetAnimation(&self->super, AnimationNameStand); LCDSprite *sprite = playdate->sprite->newSprite(); playdate->sprite->addSprite(sprite); playdate->sprite->setUserdata(sprite, self); playdate->sprite->setZIndex(sprite, ZINDEX_PLAYER); return sprite; }
To call a common method on a sprite, I use the syntax sprite->class->method(sprite, ...)
.
// Time is up, enemy should withdraw. for (unsigned int index = 0; index < sprites.count; index++) { LCDSprite *sprite = sprites.memory[index]; MELSprite *melSprite = playdate->sprite->getUserdata(sprites.memory[index]); if (melSprite->definition.type == MELSpriteTypeEnemy && melSprite->class->withdraw) { melSprite->class->withdraw(sprite); } }
An other way to create classes: Scene
Another important struct in my game is Scene, which represents things like the game scene, the title screen, the character selection screen, etc. A Scene has a type, an init method, a dealloc method, an update method, and a list of sprites.
typedef struct scene Scene;
typedef enum { SceneTypeTitle, SceneTypeCharacterSelect, SceneTypeStory, SceneTypeGame, SceneTypeScoreEntry, } SceneType;
typedef struct scene { SceneType type; void (* _Nonnull init)(Scene * _Nonnull self); void (* _Nonnull dealloc)(Scene * _Nonnull self); int (* _Nonnull update)(void * _Nonnull self); LCDSpriteRefList sprites; } Scene;
Unlike MELSprite, I did not use a class struct for Scene, because there is at most only one instance of a Scene at the same time in memory so there is no need to have a function table.
This way of writing classes is reminiscing of how Javascript handle classes: each object has a prototype member that contains the function table and each instance can have a different implementation of each methods.
The scenes have a simple life cycle that is handled by a function named SceneMakeCurrent
:
Scene * _Nullable currentScene; void SceneMakeCurrent(Scene * _Nonnull self) { if (currentScene != NULL) { playdate->sprite->removeAllSprites(); currentScene->dealloc(currentScene); } currentScene = self; playdate->system->setUpdateCallback(self->update, self); self->init(self); }
This function is called when the game needs to switch to a different scene. It deallocates the current scene, sets the new scene as the current one, sets the update callback to the update method of the new scene, and calls the init method of the new scene.
Polymorphism: MELList
Another useful struct that I created is MELList, which represents a generic list of elements. A MELList has a pointer to a memory buffer, a count of elements, and a capacity of the buffer. I like strongly typed code and so I used macro to generate typed lists.
There are two main macros: MELListDefine(type)
and MELListImplement(type)
. The first one generates the struct and the function declarations and must be called in an header file. The second one generates the function implementations and must be called in a C file.
#define MELListDefine(type) /** List of type */ typedef struct mellist_##type { \ /** Content of the list. */ \ type * _Nullable memory; \ /** Number of elements in the list. */ \ unsigned int count; \ /** Current capacity. */ \ unsigned int capacity; \ } type##List;\ \ extern const type##List type##ListEmpty;\ type##List type##ListMake(void);\ type##List type##ListMakeWithInitialCapacity(unsigned int initialCapacity);\ void type##ListDeinit(type##List * _Nonnull self);\ void type##ListDeinitWithDeinitFunction(type##List * _Nonnull self, void (* _Nonnull deinitFunction)(type * _Nonnull));\ void type##ListEnsureCapacity(type##List * _Nonnull self, unsigned int required);\ void type##ListPush(type##List * _Nonnull self, type element);\ type type##ListPop(type##List * _Nonnull self);\ void type##ListInsert(type##List * _Nonnull self, unsigned int index, type element);\ type type##ListRemove(type##List * _Nonnull self, unsigned int index);\ type type##ListRemoveSwap(type##List * _Nonnull self, unsigned int index);
This macro-based makes it easy to create lists of any type without having to write a lot of boilerplate code, and I can use the same functions for different types of lists. For example, to create a list of a struct named Score, I use the following code:
score.h:
typedef struct { uint32_t value; char * _Nullable player; } Score; MELListDefine(Score);
score.c:
MELListImplement(Score);
Then, I can use the list functions to manipulate the list of scores:
ScoreList scores = ScoreListEmpty;
Score score1 = {500, "Amélie"}; Score score2 = {300, "Bernard"}; Score score3 = {42, "Caroline"};
ScoreListPush(&scores, score1); ScoreListPush(&scores, score2); ScoreListPush(&scores, score3);
// Get the second score Score score = scores.memory[1];
// Update the value score.value += 50; scores.memory[1] = score;
// Remove the first score ScoreListRemove(&scores, 0);
// Free the memory. It can be reused by pushing something again. ScoreListDeinit(&scores);
Conclusion
And that's a wrap for now. I hope you enjoyed reading about some of the technical aspects of Kuroobi, such as the choice of the programming language and the structuring of the code. I would love talking about debugging an other time since it is a big part of C development as well.
Feel free to leave me a comment or contact me if you want to know more about this devlog, I am always open to discussion.
Get Kuroobi (Playdate)
Kuroobi (Playdate)
Pilot a flying robot fighting with martial arts to defeat a mad scientist and save Japan in 3 minutes.
Status | Released |
Author | Rapcal |
Genre | Action |
Tags | Arcade, High Score, Playdate, Shoot 'Em Up |
More posts
- Version 1.1.0Feb 29, 2024
- Game ManualDec 12, 2023
- GameplayOct 14, 2023
- First concept and game lengthOct 07, 2023
Leave a comment
Log in with itch.io to leave a comment.