ohsp

Prototype for a game with dual thruster controls.
git clone git://git.amin.space/ohsp.git
Log | Files | Refs | LICENSE

commit 81dd4840837c968fa43530d4890c5183765692c1
parent 66a5e8ef34618f8b4bf1e30a89947a281cbd0af1
Author: amin <dev@aminmesbah.com>
Date:   Fri,  3 Nov 2017 07:43:16 +0000

Add dynamic reloading of game code on linux

Inspired and informed by Casey Muratori's method in the wonderful
Handmade Hero [1]. The the game is compiled into a shared object that is
dynamically loaded by the platform layer.

There is a way to do this with dlls on windows as well.

I had to temporarily remove the `-Wpedantic` compiler flag to stop
getting the "ISO C forbids conversion of object pointer to function
pointer" error. I'm technically relying on undefined behavior, though in
practice it's fine on most modern platforms to cast from a void* to a
function pointer.

[1] https://hero.handmade.network/episode/code/day021
[2] http://nullprogram.com/blog/2014/12/23/
[3] http://chrismdp.com/2015/08/how-to-add-live-code-reload-to-your-game/

FossilOrigin-Name: a602a2c74669b4a0ed8fd76fba6efd0132461668ca90739035db5fd7b84883d6
Diffstat:
MMakefile | 27++++++++++++++++++++-------
Msrc/game.h | 4++++
Msrc/platform_sdl.c | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/platform_sdl.h | 12++++++++++++
4 files changed, 103 insertions(+), 11 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,5 +1,6 @@ CC = gcc -CFLAGS = -std=c99 -Wall -Wextra -Wpedantic -Wshadow -Wno-unused-parameter +#TODO: add -Wpedantic back +CFLAGS = -std=c99 -Wall -Wextra -Wshadow -Wno-unused-parameter LDFLAGS = $(SDL_LDFLAGS) -lm SDL_CFLAGS := $(shell sdl2-config --cflags) @@ -9,32 +10,44 @@ override CFLAGS += $(SDL_CFLAGS) SRC_FILES = platform_sdl.c game.c entity.c SRC = $(addprefix src/, $(SRC_FILES)) -EXE = ohsp +EXE_FILE = ohsp + +LIB_FILES = game.c entity.c +LIB = $(addprefix src/, $(LIB_FILES)) +LIB_NAME = game.so DBGDIR = debug -DBGEXE = $(DBGDIR)/$(EXE) +DBGEXE = $(DBGDIR)/$(EXE_FILE) +DBGLIB = $(DBGDIR)/$(LIB_NAME) DBGCFLAGS = -g -Og -Werror RELDIR = release -RELEXE = $(RELDIR)/$(EXE) +RELEXE = $(RELDIR)/$(EXE_FILE) +RELLIB = $(RELDIR)/$(LIB_NAME) RELCFLAGS = -O2 -Os -.PHONY: all clean compile_debug compile_release debug memcheck prep run todo +.PHONY: all clean compile_debug compile_release debug lib_debug lib_release memcheck prep run todo all: compile_debug compile_release clean: rm -f $(RELDIR)/* $(DBGDIR)/* -compile_debug: prep +compile_debug: prep lib_debug $(CC) $(CFLAGS) $(DBGCFLAGS) $(SRC) -o $(DBGEXE) $(LDFLAGS) -compile_release: prep +compile_release: prep lib_release $(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS) debug: compile_debug gdb $(DBGEXE) +lib_debug: + $(CC) $(CFLAGS) -fpic -shared $(DBGCFLAGS) $(LIB) -o $(DBGLIB) $(LDFLAGS) + +lib_release: + $(CC) $(CFLAGS) -fpic -shared $(RELCFLAGS) $(LIB) -o $(RELLIB) $(LDFLAGS) + memcheck: compile_debug valgrind --track-origins=yes ./$(DBGEXE) diff --git a/src/game.h b/src/game.h @@ -63,6 +63,10 @@ struct OffscreenBuffer unsigned int pitch; }; +// TODO: Consider using the #define trick from HMH +typedef void (game_update_t)(struct GameState*, struct GameControllerInput, int, int); +typedef void (game_render_t)(struct OffscreenBuffer*, float, struct GameState*); + void game_init(struct GameState *game_state, int field_width, int field_height); void game_update(struct GameState *game_state, struct GameControllerInput game_input, int field_width, int field_height); void game_render(struct OffscreenBuffer *buffer, float dt, struct GameState *game_state); diff --git a/src/platform_sdl.c b/src/platform_sdl.c @@ -1,7 +1,5 @@ #include "platform_sdl.h" -#include "game.h" - #include <stdbool.h> #include <stdio.h> #include <time.h> @@ -15,6 +13,56 @@ extern bool PAUSED; static struct SDLOffscreenBuffer global_back_buffer; +void sdl_unload_game_code(struct SDLGameCode *game_code) +{ + if (!game_code) + { + printf("Invalid pointer *game_code\n"); + return; + } + + if (game_code->game_code_library) + { + SDL_UnloadObject(game_code->game_code_library); + game_code->game_code_library = 0; + } + game_code->is_valid = false; + // TODO: see if we need to set these function pointers to stubs (hmh021) + game_code->game_update = 0; + game_code->game_render = 0; +} + + +// TODO: Add temp dll in case loading fails +struct SDLGameCode sdl_load_game_code(char *source_lib_path) +{ + struct SDLGameCode game_code; + game_code.is_valid = false; + game_code.game_code_library = 0; + game_code.game_update = 0; + game_code.game_render = 0; + + game_code.game_code_library = SDL_LoadObject(source_lib_path); + if (game_code.game_code_library) + { + // NOTE: The C standard (as of C99) distinguishes function pointers + // from object pointers (`void *` is an object pointer). Technically it + // is undefined behavior to cast between these two pointer types. In + // practice, it works on most modern platforms. + game_code.game_update = (game_update_t *) SDL_LoadFunction(game_code.game_code_library, "game_update"); + game_code.game_render = (game_render_t *) SDL_LoadFunction(game_code.game_code_library, "game_render"); + game_code.is_valid = (game_code.game_update && game_code.game_render); + } + + if (!game_code.is_valid) + { + printf("Game code is not valid: %s\n", SDL_GetError()); + } + + return game_code; +} + + struct SDLWindowDimension sdl_get_window_dimension(SDL_Window *window) { struct SDLWindowDimension result; @@ -248,6 +296,10 @@ int main(int argc, char *argv[]) controller_input.right_stick_x = 0; controller_input.right_stick_y = 0; + // TODO: Make this not hard-coded + struct SDLGameCode game_code = sdl_load_game_code("./release/game.so"); + int counter = 0; + while (running) { uint64_t current_ms = (SDL_GetPerformanceCounter() * SECOND) / SDL_GetPerformanceFrequency(); @@ -256,6 +308,17 @@ int main(int argc, char *argv[]) lag += elapsed_ms; //printf("%" PRIu64 ", %" PRIu64 ", %f\n", elapsed_ms, lag, MS_PER_UPDATE); + if (counter < 500) + { + ++counter; + } + else + { + sdl_unload_game_code(&game_code); + game_code = sdl_load_game_code("./release/game.so"); + counter = 0; + } + SDL_Event event; while (SDL_PollEvent(&event)) { @@ -281,14 +344,14 @@ int main(int argc, char *argv[]) { while (lag >= MS_PER_UPDATE) { - game_update(&game_state, controller_input, buffer.width, buffer.height); + game_code.game_update(&game_state, controller_input, buffer.width, buffer.height); //printf("\t%" PRIu64 ", %f\n", lag, MS_PER_UPDATE); lag -= MS_PER_UPDATE; } } clear_screen(&global_back_buffer, COLOR_BACKGROUND); - game_render(&buffer, lag/SECOND, &game_state); + game_code.game_render(&buffer, lag/SECOND, &game_state); sdl_update_window(renderer, &global_back_buffer); if (elapsed_ms <= MS_PER_FRAME) { diff --git a/src/platform_sdl.h b/src/platform_sdl.h @@ -1,6 +1,9 @@ #ifndef PLATFORM_SDL_H #include "SDL.h" +#include "game.h" + +#include <stdbool.h> #define MAX_CONTROLLERS 4 #define DEADZONE_THRESHOLD 9000 @@ -21,5 +24,14 @@ struct SDLWindowDimension int height; }; +struct SDLGameCode +{ + bool is_valid; + void *game_code_library; + time_t last_write_time; + game_update_t *game_update; + game_render_t *game_render; +}; + #define PLATFORM_SDL_H #endif