Introduction
Presentation
This book will teach you everything you need to know about building video games
with the libgote
. I'll try to add information about the various aspects of the
library and how you can link them together to make a complete video game.
Refer to the API documentation for an in-depth detail of all the functions provided by the library.
Building from source
Cloning the repository
Using SSH
Make sure your SSH key is linked to your GitHub account.
git clone git@github.com:nasso/libgote.git
Using HTTPS
git clone https://github.com/nasso/libgote.git
Building libgote
Just navigate in the root directory of the repository and call make:
cd libgote
make
If you want to build it with debug flag (-g3
), add DEBUG=1
to the make
command:
make DEBUG=1
Generating the documentation
The library documentation is generated using Doxygen. A online version is available here.
First, make sure the ./target
directory exists at the root of the repository.
It is automatically created when building the library, but you can also use
mkdir
.
Concepts
This chapter will give you an overview of the different concepts you need to be
familiar with to use libgote
.
Some of those concepts aren't specific to libgote
and you will easily find
more resource online about them: entity-component-system (ECS), game states and
game assets.
State
Basics
As described in this article, a state (or game-state) represents a "mode" our game is in. For instance, we could separate an RPG in different game states:
- A "local map" state where the player can move around a terrain
- A "world map" state where a zoomed-out map of the world is displayed
- A "combat" state for fights!
- A "pause menu" state displaying the pause menu of the game
- ...
Essentially, every state has its own start
, stop
and update
functions.
Usage
To create a state with the libgote
, use calloc
to allocate a
gt_state_t
structure and set the callback functions you care about:
gt_state_t *create_combat_state(void)
{
gt_state_t *state = my_calloc(1, sizeof(gt_state_t));
state->update = &combat_state_update;
return (state);
}
Of course, update
isn't the only callback available to game states. Here is
a list of the most commonly used:
- on_start - called when the state starts.
- on_stop - called when the state stops.
- update - called on every iteration of the game loop while the state is active.
- destroy - called when the state is destroyed, use it to free the
self
field.
The libgote
will safely ignore any callback set to NULL
. This is why you
should use calloc
to allocate the gt_state_t
structure (calloc
ensures
all the allocated bytes are set to NULL
or 0
).
The self
field allows you to store arbitrary data associated with your state.
Its value is passed as an argument to all the callbacks. In most cases, you will
use it to keep a reference to data you want to clean-up when the state is
stopped:
/* Structure holding the data associated with the state */
struct combat_state {
player_t *player;
};
/* Create the player when the state starts */
static void combat_state_on_start(void *ptr, gt_world_t *world)
{
struct combat_state *self = ptr;
self->player = player_create(world);
}
/* Remove the player when the state stops */
static void combat_state_on_stop(void *ptr, gt_world_t *world)
{
struct combat_state *self = ptr;
player_remove(self->player, world);
}
/* Create the state, its data structure, and set the callbacks */
gt_state_t *create_combat_state(void)
{
gt_state_t *state = my_calloc(1, sizeof(gt_state_t));
state->self = my_calloc(1, sizeof(struct combat_state));
state->destroy = &my_free;
state->on_start = &combat_state_on_start;
state->on_stop = &combat_state_on_stop;
return (state);
}
Make sure not to forget to set state->destroy
to an appropriate freeing
function! If you don't do it, memory WILL leak!
State machine
Game states are usually managed by something called a state machine. It is the one responsible for calling the right function at every iteration of the game loop. Only one state can be active at a time!
The state machine is often implemented as a stack, where states can be pushed and popped. The state at the top of the stack is the active state.
Transitions
A change in the state machine's stack is called a state transition. There
are 4 different kinds of transitions in the libgote
:
- Push transitions - a state is pushed on top of the stack: it becomes the active state.
- Pop transitions - the state on top of the stack is removed (the state below it, if any, becomes active).
- Switch transitions - the state on top of the stack is replaced with another state.
- Quit transitions - all the states are removed from the stack, killing the the state machine.
As soon as the state stack becomes empty, the state machine is killed and the game is effectively terminated.
Additionnal callbacks
In the libgote
, states have actually 3 extra callbacks I didn't mention yet.
You might need them in some special cases:
- on_pause - called when a state is pushed on top of the current one.
- on_resume - called when the state above gets popped.
- shadow_update - same as
update
, but is called on ALL the states in the stack.
World and Resource
World
All the data global to your game is stored in what is called a "World". That includes, for instance, all the entities of your game. Entities are covered in their own chapter.
The world really is just a hash table mapping keys (character strings) to resources.
Resource
What is a resource? Anything you want! The only requirement is that it must be a pointer. Other than that, you can have anything you want except for function pointers, because of the C language specification forbidding it. If you really need to store a function pointer in your World, wrap them in a heap-allocated structure and it will be fine!
Usage in the libgote
Creating a new world
You will rarely ever need to create or destroy a world directly (libgote
does
it for you when you create a gt_app_t
). Though, here's how you
would do it:
// create a world this way:
gt_world_t *world = gt_world_create();
// destroy it when you're done!
gt_world_destroy(world);
Resource insertion
When inserting a resource in the world, you can optionally specify a destructor to be called when the resource is removed or the world destroyed:
list_t *cool_stuff = list_from(3, "furries", "undertale", "you");
// the destroyer must be explicitly cast because C cannot implicitly cast
// function pointers, even when they are compatible.
gt_world_insert(world, "cool_stuff", data, (void (*)(void*)) &list_destroy);
For resources which do not need to be destroyed (i.e. static value), just set
the destroyer to NULL
:
gt_world_insert(world, "some_string", "Hey! This is a resource!", NULL);
Resource retrieval
Getting a resource from the world is straightforward:
const char *my_resource = gt_world_get(world, "some_string");
my_printf("%s", my_resource); // "Hey! This is a resource!"
Resource removal
Removing a resource from the world is, also, straightforward:
gt_world_remove(world, "some_string");
Note that gt_world_remove
does NOT return anything! The resource is
destroyed as soon as you remove it from the world.
Entity and Resource
Entity
An entity represents any object of your game. Examples of entities include the player, NPCs, an item, a button or a map. An entity doesn't hold any data (other than its existence)! Essentially, entities can be thought of as elements in a big array containing all the objects that make up a scene of your game:
player | villager | sword | play_button |
---|
Component
A component is what gives properties to an entity. Without components, entities are... Well, useless! They don't do anything! They don't even have a position, a color, a sprite... All this information comes from components.
At any given point, an entity either has or doesn't have a component. It cannot have, say, the same component twice. Keeping the example from above, it's as if each component type added one new line to the following table:
entity | |
---|---|
Component A | value 1 |
Component B | value 2 |
Component C | value 3 |
Entities are not required to have a value for all component types. For example, we could say that the player only needs a position and a sprite:
player | |
---|---|
Position | (3, 4) |
Sprite | player.png |
Color | - |
The player doesn't have a "color", so the value for the "color" component line is just left empty.
Adding an entity adds a column to the table!
player | villager | sword | play_button | |
---|---|---|---|---|
Position | (3, 4) | (17, 12) | (-4, 1) | (40, 160) |
Sprite | player.png | villager.png | sword.png | - |
Color | - | - | - | #88FF00 |
Even if only one entity requires a "color" component the line for the "color" component spans all the table. That can create "holes" in the table, but it's okay because there are ways to eliminate that problem we will see very shortly.
Usage with the libgote
Creating a component class
Say you want to create a new kind of component. For this example, we will create a "position" component class.
The first step is to declare a new gt_component_class_t
global constant.
Usually, you will want to declare it in a header file with the extern
keyword:
extern const gt_component_class_t POSITION_COMP;
And then, define it in your source file:
const gt_component_class_t POSITION_COMP = {
.name = "position_component",
.destroyer = &my_free,
};
There are two important fields here:
name
- a static, unique character string that will be used as the key for the storage resource in the world. It must be unique and must not be used as the key for any other resource in the world.destroyer
- a pointer to the function to be called when the component data is to be destroyed.
Speaking about the component data: it can be anything you want! It is common to
have a constructor for your component data (declared in the same header file as
the gt_component_class_t
it corresponds to):
typedef struct {
f64_t x;
f64_t y;
} position_comp_t;
position_comp_t *position_component(f64_t x, f64_t y);
An example implementation could be:
position_comp_t *position_component(f64_t x, f64_t y)
{
position_comp_t *self = my_calloc(1, sizeof(position_comp_t));
self->x = x;
self->y = y;
return (self);
}
Registering components
Components are stored in "storages" kept as a Resource in the World. Think of a storage as a simple array (that's basically what they are anyway). Keeping the table example from earlier, each component "storage" corresponds to a line of the table. That means there's one storage per component type.
player | villager | sword | play_button | |
---|---|---|---|---|
Position | (3, 4) | (17, 12) | (-4, 1) | (40, 160) |
Sprite | player.png | villager.png | sword.png | - |
Color | - | - | - | #88FF00 |
In this example, there are 3 storages: one for Position
components, one for
Sprite
components and another for Color
components. Each line is a storage.
There are two implementations of storage in the libgote
:
gt_vec_storage
- literally just a vector: better when most entities will have this component (there's very few "holes").gt_map_storage
- uses a hash map: better when few entities will have this component (there are many, big "holes"). Not implemented as of writing this. Do not use.
As a component storage is just a resource in the world, you add them just like any other resource:
// create the storage (here it's just a vector/array!)
gt_storage_t *strg = gt_vec_storage();
gt_world_insert(
world,
POSITION_COMP.name,
strg,
(void (*)(void*)) >_storage_destroy
);
Though, having to do that for every component type would be kinda annoying. This
is why the gt_world_register
function exists! It takes care of the last
argument (the destroyer):
gt_storage_t *strg = gt_vec_storage();
gt_world_register(world, POSITION_COMP.name, strg);
But there's even better than this! The gt_world_register_component
function!
gt_world_register_component(world, &POSITION_COMP, >_vec_storage);
Here, all you have to give is a pointer to the gt_component_class_t
and a
pointer to the constructor for the storage implementation to use. It does the
exact same thing as the code snippets above!
Creating an entity
You cannot create an entity from nothing (there's no "gt_entity_create
"
function). The reason for this is that an entity only makes sense when it is
part of a world, with the appropriate storages available.
You can create entities with the gt_world_create_entity
function:
The arguments are:
- The world
- How many components to initialize the entity with
- For each initial component:
- A pointer to the
gt_component_class_t
- The component data
- A pointer to the
For example, to create an entity using our position_comp_t
:
gt_entity_t *ent = gt_world_create_entity(world, 1,
&POSITION_COMP, position_component(2.0, 6.0)
);
Or with more components you've created (don't forget to change the component count!):
gt_entity_t *player = gt_world_create_entity(world, 3,
&POSITION_COMP, position_component(2.0, 6.0),
&SPRITE_COMP, sprite_component("player.png"),
&INVENTORY_COMP, inventory_component()
);
Of course, if you want to create an entity without any component:
gt_entity_t *ent = gt_world_create_entity(world, 0);
Removing the entity
When you're done using an entity, you have to manually remove it from the world! States do not know about the entities you've created, so they are not removed automatically!
To remove entities, use the gt_world_remove_entity
function:
/* Remove the player entity when the state stops */
static void combat_state_on_stop(void *ptr, gt_world_t *world)
{
struct combat_state *self = ptr;
gt_world_remove_entity(world, self->player);
}