Hold state in a struct
This program looks simple: a square moves, and a click reverses it. The change is in the code. It has no global variables. All its data is in one struct, and each function gets a pointer to that struct.
The full program
/* A program with no global variables. A click changes the speed direction. */
#define SDL_MAIN_USE_CALLBACKS
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 220.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */
/* All the data of the program, in one place. */
typedef struct {
SDL_Window *window;
SDL_Renderer *renderer;
Uint64 last_ticks;
SDL_FRect square;
float direction; /* 1 is right, and -1 is left. */
} App;
static void update(App *app, float dt)
{
app->square.x += app->direction * SPEED * dt;
if (app->square.x < 0.0f) {
app->square.x = 0.0f;
app->direction = 1.0f;
} else if (app->square.x + app->square.w > WINDOW_WIDTH) {
app->square.x = WINDOW_WIDTH - app->square.w;
app->direction = -1.0f;
}
}
static void draw(const App *app)
{
SDL_SetRenderDrawColor(app->renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(app->renderer);
SDL_SetRenderDrawColor(app->renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRect(app->renderer, &app->square);
SDL_RenderPresent(app->renderer);
}
SDL_AppResult SDL_AppInit(void **appstate, int argc, char *argv[])
{
App *app = SDL_calloc(1, sizeof(App));
if (app == NULL) {
return SDL_APP_FAILURE;
}
*appstate = app; /* SDL gives this pointer to the other functions. */
if (!SDL_Init(SDL_INIT_VIDEO)) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
if (!SDL_CreateWindowAndRenderer("Hold state in a struct", WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&app->window, &app->renderer)) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
return SDL_APP_FAILURE;
}
SDL_SetRenderVSync(app->renderer, 1);
app->square = (SDL_FRect){ 40.0f, 216.0f, 48.0f, 48.0f };
app->direction = 1.0f;
app->last_ticks = SDL_GetTicksNS();
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event)
{
App *app = appstate;
if (event->type == SDL_EVENT_QUIT) {
return SDL_APP_SUCCESS;
}
if (event->type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
app->direction = -app->direction;
}
return SDL_APP_CONTINUE;
}
SDL_AppResult SDL_AppIterate(void *appstate)
{
App *app = appstate;
Uint64 now = SDL_GetTicksNS();
float dt = (float)(now - app->last_ticks) / (float)SDL_NS_PER_SECOND;
app->last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
update(app, dt);
draw(app);
return SDL_APP_CONTINUE;
}
void SDL_AppQuit(void *appstate, SDL_AppResult result)
{
SDL_free(appstate); /* SDL destroys the window and the renderer. */
}/* A program with no global variables. A click changes the speed direction. */
#include <SDL.h>
#include <stdbool.h>
#ifdef __EMSCRIPTEN__
#include <emscripten.h>
#endif
#define WINDOW_WIDTH 640
#define WINDOW_HEIGHT 480
#define SPEED 220.0f /* pixels per second */
#define MAX_DT 0.05f /* seconds */
/* All the data of the program, in one place. */
typedef struct {
SDL_Window *window;
SDL_Renderer *renderer;
Uint64 last_ticks;
SDL_FRect square;
float direction; /* 1 is right, and -1 is left. */
bool running;
} App;
static void update(App *app, float dt)
{
app->square.x += app->direction * SPEED * dt;
if (app->square.x < 0.0f) {
app->square.x = 0.0f;
app->direction = 1.0f;
} else if (app->square.x + app->square.w > WINDOW_WIDTH) {
app->square.x = WINDOW_WIDTH - app->square.w;
app->direction = -1.0f;
}
}
static void draw(const App *app)
{
SDL_SetRenderDrawColor(app->renderer, 24, 26, 32, SDL_ALPHA_OPAQUE);
SDL_RenderClear(app->renderer);
SDL_SetRenderDrawColor(app->renderer, 255, 157, 92, SDL_ALPHA_OPAQUE);
SDL_RenderFillRectF(app->renderer, &app->square);
SDL_RenderPresent(app->renderer);
}
/* The browser and the loop both call this with a void pointer. */
static void frame(void *data)
{
App *app = data;
SDL_Event event;
Uint64 now = SDL_GetPerformanceCounter();
float dt = (float)(now - app->last_ticks) / (float)SDL_GetPerformanceFrequency();
app->last_ticks = now;
if (dt > MAX_DT) {
dt = MAX_DT;
}
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
app->running = false;
}
if (event.type == SDL_MOUSEBUTTONDOWN) {
app->direction = -app->direction;
}
}
update(app, dt);
draw(app);
}
int main(int argc, char *argv[])
{
App *app = SDL_calloc(1, sizeof(App));
if (app == NULL) {
return 1;
}
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
SDL_Log("SDL_Init failed: %s", SDL_GetError());
SDL_free(app);
return 1;
}
if (SDL_CreateWindowAndRenderer(WINDOW_WIDTH, WINDOW_HEIGHT, 0,
&app->window, &app->renderer) != 0) {
SDL_Log("SDL_CreateWindowAndRenderer failed: %s", SDL_GetError());
SDL_Quit();
SDL_free(app);
return 1;
}
SDL_SetWindowTitle(app->window, "Hold state in a struct");
SDL_RenderSetVSync(app->renderer, 1);
app->square = (SDL_FRect){ 40.0f, 216.0f, 48.0f, 48.0f };
app->direction = 1.0f;
app->running = true;
app->last_ticks = SDL_GetPerformanceCounter();
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(frame, app, 0, 1);
#else
while (app->running) {
frame(app);
}
#endif
SDL_DestroyRenderer(app->renderer);
SDL_DestroyWindow(app->window);
SDL_Quit();
SDL_free(app);
return 0;
}Why move away from globals
The earlier lessons used global variables to keep the code short. This works for a small program, but it becomes a problem as a program grows.
Any function can change a global, so you cannot see what a function touches from its name and its arguments. Compare these two lines:
void update(float dt);
void update(App *app, float dt);The second line tells you what the function uses. It also lets you have two sets of data, which is what you need for a second player or a saved game. And a const pointer tells the reader that a function only looks:
static void draw(const App *app)Put the data in one struct
typedef struct {
SDL_Window *window;
SDL_Renderer *renderer;
Uint64 last_ticks;
SDL_FRect square;Everything that the program must remember between frames goes here. Values that only one function needs stay as local variables in that function.
Make the struct
App *app = SDL_calloc(1, sizeof(App));SDL_calloc allocates memory and fills it with zeros. So each pointer starts as NULL, and each number starts as 0. This is safer than SDL_malloc, which leaves random values in the memory.
Get the struct to each function
SDL3 has a place for this pointer. SDL_AppInit gets the address of a pointer, and the program stores its struct there:
*appstate = app; /* SDL gives this pointer to the other functions. */SDL then passes that pointer to each callback. The type is void *, a pointer to any type, so each function converts it first:
App *app = appstate;This is why the callbacks had an appstate argument in all the earlier lessons.
In SDL2, main owns the struct, and it passes the pointer to frame. On the desktop, this is a normal call:
while (app->running) {
frame(app);
}The browser version needs a different function, which takes the pointer as an extra argument:
emscripten_set_main_loop_arg(frame, app, 0, 1);The browser calls frame with a void *, a pointer to any type. So frame takes a void * argument, and it converts that first:
App *app = data;C converts a void * to another pointer type for you, with no cast.
Pass the struct down
update(app, dt);
draw(app);Each function that needs the data gets the pointer. With a pointer, you write app->square.x in place of square.x. The code is a little longer, and it is clearer about where the data lives.
Free the struct
SDL_free(appstate); /* SDL destroys the window and the renderer. */SDL calls SDL_AppQuit with the same pointer, so the program frees the struct there.
SDL_Quit();
SDL_free(app);The struct holds the window and the renderer, so it is freed last.
Try this
- Convert Pong to one
Appstruct. Add the ball, the paddles, and the score. - Add a second square to
Appwith its own speed. - Make one more struct for the game data, inside
App. Thenupdateneeds only that part.
What you learned
- Globals hide what a function uses, and a pointer argument shows it.
- One struct can hold all the data that lasts between frames.
SDL_callocgives you memory that starts at zero.- A
void *carries a pointer through code that does not know its type.