a-game

2D platformer written from scratch.
git clone git://git.amin.space/a-game.git
Log | Files | Refs | README | LICENSE

commit be373a7d2000d67d16196cd02b27e577828c334a
parent 39b705d57e67d22b6cb1c30e777803f0020b2e55
Author: amin <dev@aminmesbah.com>
Date:   Fri,  1 Jun 2018 05:12:47 +0000

Hotload game code

FossilOrigin-Name: d41312085f5ec71e9749997d5c485106ae9060750798a1f49d118ec241c57eff
Diffstat:
MMakefile | 46+++++++++++++++++++++++++++++-----------------
Msrc/game.h | 2++
Msrc/platform_linux.c | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Asrc/platform_linux.h | 27+++++++++++++++++++++++++++
4 files changed, 167 insertions(+), 22 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,45 +1,57 @@ CC = clang CFLAGS = -std=c99 -Ilib -Wall -Wextra -Wshadow -Wswitch-enum -Wno-unused-parameter -Wno-missing-braces -LDFLAGS = -ldl -lglfw -lGL -lGLEW -lGLU -lm -lpthread -lX11 -lXi -lXrandr +LDFLAGS = -ldl -lglfw -lGL -lGLEW -lm SRC_FILES = game.c glmth.c platform_linux.c shader.c SRC = $(addprefix src/, $(SRC_FILES)) EXE_FILE = a_game +LIB_FILES = game.c glmth.c shader.c +LIB = $(addprefix src/, $(LIB_FILES)) +LIB_NAME = game.so + DBGDIR = build/debug DBGEXE = $(DBGDIR)/$(EXE_FILE) DBGCFLAGS = -g -Og -Werror RELDIR = build/release RELEXE = $(RELDIR)/$(EXE_FILE) -RELCFLAGS = -O2 -Os +RELLIB = $(RELDIR)/$(LIB_NAME) +RELLIBTMP = $(RELLIB).tmp +RELCFLAGS = -DPLATFORM_HOTLOAD_GAME_CODE -O2 -Os -.PHONY: all clean debug debug_dir gdb memcheck memcheck release release_dir run todo +.PHONY: default all build_debug build_lib build_release clean dir_debug dir_release gdb memcheck run todo -all: debug release +default: build_release -clean: - rm -f $(RELDIR)/* $(DBGDIR)/* +all: build_debug build_release -debug: debug_dir +build_debug: dir_debug $(CC) $(CFLAGS) $(DBGCFLAGS) $(SRC) -o $(DBGEXE) $(LDFLAGS) -debug_dir: - @mkdir -p $(DBGDIR) +build_lib: + $(CC) $(CFLAGS) -fpic -shared $(RELCFLAGS) $(LIB) -o $(RELLIBTMP) $(LDFLAGS) + mv $(RELLIBTMP) $(RELLIB) -gdb: debug - gdb $(DBGEXE) +build_release: dir_release build_lib + $(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS) -memcheck: debug - valgrind --track-origins=yes ./$(DBGEXE) +clean: + rm -f $(RELDIR)/* $(DBGDIR)/* -release: release_dir - $(CC) $(CFLAGS) $(RELCFLAGS) $(SRC) -o $(RELEXE) $(LDFLAGS) +dir_debug: + @mkdir -p $(DBGDIR) -release_dir: +dir_release: @mkdir -p $(RELDIR) -run: release +gdb: build_debug + gdb $(DBGEXE) + +memcheck: build_debug + valgrind --track-origins=yes ./$(DBGEXE) + +run: build_release ./$(RELEXE) todo: diff --git a/src/game.h b/src/game.h @@ -11,6 +11,8 @@ struct GameState struct Shader triangle_shader; }; +typedef void (game_update_and_render_func)(struct GameState *game_state); + void game_init(struct GameState *game_state); void game_update_and_render(struct GameState *game_state); void game_deinit(struct GameState *game_state); diff --git a/src/platform_linux.c b/src/platform_linux.c @@ -1,16 +1,23 @@ +#include "platform_linux.h" + +#include <inttypes.h> +#include <stdint.h> #include <stdio.h> +#include <time.h> + +#include <dlfcn.h> +#include <sys/stat.h> #define GLEW_STATC #include <GL/glew.h> #include <GLFW/glfw3.h> -#include "game.h" - -#define SCR_WIDTH 800 -#define SCR_HEIGHT 600 void error_callback(int error, const char* description); void framebuffer_size_callback(GLFWwindow* window, int width, int height); +time_t file_get_modified_time(char *file_path); +void unload_game_code(struct GameCode *game_code); +struct GameCode load_game_code(char *source_lib_path); int main(void) @@ -28,7 +35,7 @@ int main(void) glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); - GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "A Game", NULL, NULL); + GLFWwindow* window = glfwCreateWindow(PLATFORM_SCR_WIDTH, PLATFORM_SCR_HEIGHT, "A Game", NULL, NULL); if (!window) { fprintf(stderr, "GLFW window creation failed\n"); @@ -51,9 +58,38 @@ int main(void) struct GameState game_state = {0}; game_init(&game_state); +#ifdef PLATFORM_HOTLOAD_GAME_CODE + struct GameCode game_code = load_game_code(PLATFORM_GAME_LIB_PATH); +#endif + + uint64_t lag = 0; + uint64_t previous_ms = (glfwGetTimerValue() * PLATFORM_SECOND) / glfwGetTimerFrequency(); + while (!glfwWindowShouldClose(window)) { + uint64_t current_ms = (glfwGetTimerValue() * PLATFORM_SECOND) / glfwGetTimerFrequency(); + uint64_t elapsed_ms = current_ms - previous_ms; + previous_ms = current_ms; + lag += elapsed_ms; + //printf("%" PRIu64 ", %" PRIu64 ", %f\n", elapsed_ms, lag, PLATFORM_MS_PER_UPDATE); + +#ifdef PLATFORM_HOTLOAD_GAME_CODE + time_t last_write_time = file_get_modified_time(PLATFORM_GAME_LIB_PATH); + bool game_code_has_changed = last_write_time && (last_write_time != game_code.last_write_time); + if (game_code_has_changed) + { + unload_game_code(&game_code); + game_code = load_game_code(PLATFORM_GAME_LIB_PATH); + if (!game_code.is_valid) + { + // TODO: fall back to backup? + } + } + game_code.game_update_and_render(&game_state); +#else game_update_and_render(&game_state); +#endif // PLATFORM_HOTLOAD_GAME_CODE + glfwSwapBuffers(window); glfwPollEvents(); } @@ -76,3 +112,71 @@ void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } + + +time_t file_get_modified_time(char *file_path) +{ + time_t mtime = 0; + struct stat file_status = {0}; + if (stat(file_path, &file_status) == 0) + { + //printf("File: %s last modified: %s\n", file_path, ctime(&file_status.st_mtime)); + mtime = file_status.st_mtime; + } + else + { + fprintf(stderr, "ERROR: Failed to stat file: %s\n", file_path); + } + return mtime; +} + + +void unload_game_code(struct GameCode *game_code) +{ + if (!game_code) + { + fprintf(stderr, "ERROR: Invalid pointer *game_code\n"); + return; + } + + if (game_code->game_code_library) + { + dlclose(game_code->game_code_library); + game_code->game_code_library = 0; + } + game_code->is_valid = false; + game_code->game_update_and_render = 0; +} + + +// TODO: Add backup dll in case loading fails +struct GameCode load_game_code(char *source_lib_path) +{ + struct GameCode game_code = {0}; + + game_code.last_write_time = file_get_modified_time(source_lib_path); + if (game_code.last_write_time) + { + game_code.game_code_library = dlopen(source_lib_path, RTLD_LAZY); + 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. + // + // In this case, we are protected by POSIX, which specifies that + // function and data pointers must be the same size. We will only ever + // be using dlsym on POSIX-compliant platforms. + game_code.game_update_and_render = (game_update_and_render_func *) dlsym(game_code.game_code_library, "game_update_and_render"); + game_code.is_valid = (game_code.game_update_and_render); + } + } + + if (!game_code.is_valid) + { + fprintf(stderr, "ERROR: Game code is not valid: %s\n", dlerror()); + } + + return game_code; +} diff --git a/src/platform_linux.h b/src/platform_linux.h @@ -0,0 +1,27 @@ +#ifndef PLATFORM_LINUX_H +#define PLATFORM_LINUX_H + +#include <stdbool.h> +#include <time.h> + +#include "game.h" + +#define PLATFORM_GAME_LIB_PATH "./build/release/game.so" +#define PLATFORM_SCR_WIDTH 800 +#define PLATFORM_SCR_HEIGHT 600 + +#define PLATFORM_SECOND 1000.0f +#define PLATFORM_FPS 60 +#define PLATFORM_MS_PER_FRAME (PLATFORM_SECOND / PLATFORM_FPS) +#define PLATFORM_UPDATES_PER_SECOND 120 +#define PLATFORM_MS_PER_UPDATE (PLATFORM_SECOND / PLATFORM_UPDATES_PER_SECOND) + +struct GameCode +{ + bool is_valid; + void *game_code_library; + time_t last_write_time; + game_update_and_render_func *game_update_and_render; +}; + +#endif