diff options
author | Vito Caputo <vcaputo@pengaru.com> | 2020-04-18 20:29:15 -0700 |
---|---|---|
committer | Vito Caputo <vcaputo@pengaru.com> | 2020-04-19 03:27:39 -0700 |
commit | b4911c2abfa513e91c82a712ef277f7e6209b502 (patch) | |
tree | 4ee41f6fc432d15147f0bf7e18b032d01efe2231 |
*: initial commit
This is largely ripped out of Eon to try give a reusable
scaffolding for accelerating SDL-based simple game development.
I expect there to be future commits adding more configurability
like a means of influencing which flags are passed to SDL_Init(),
e.g. if you have no need for joystick support, don't pass in that
flag, and libplay won't pass it to SDL_Init() and do the joystick
opening/mapping dance, etc.
-rw-r--r-- | Makefile.am | 1 | ||||
-rw-r--r-- | README | 10 | ||||
-rwxr-xr-x | bootstrap | 5 | ||||
-rw-r--r-- | configure.ac | 26 | ||||
-rw-r--r-- | src/Makefile.am | 2 | ||||
-rw-r--r-- | src/macros.h | 34 | ||||
-rw-r--r-- | src/play.c | 384 | ||||
-rw-r--r-- | src/play.h | 69 |
8 files changed, 531 insertions, 0 deletions
diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..af437a6 --- /dev/null +++ b/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = src @@ -0,0 +1,10 @@ +libplay is thin veneer over SDL2 and SDL2_Mixer targeting simple game +development. + +It gives a simple API for music playback with asynchronous next song +queueing, rudimentary game context switching with event routing, and some +basic timers built around SDL_Ticks. + +Note that this library doesn't do graceful error handling, since it +targets game development errors are treated as fatal and simply exit +with something printed to stderr. diff --git a/bootstrap b/bootstrap new file mode 100755 index 0000000..99f6f06 --- /dev/null +++ b/bootstrap @@ -0,0 +1,5 @@ +#!/bin/sh + +aclocal \ +&& automake --gnu --add-missing \ +&& autoconf diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..22cabbd --- /dev/null +++ b/configure.ac @@ -0,0 +1,26 @@ +AC_INIT([libplay], [1.0], [vcaputo@pengaru.com]) +AM_INIT_AUTOMAKE([-Wall -Werror foreign]) +AC_PROG_CC +AM_PROG_CC_C_O +AM_PROG_AR +AC_PROG_RANLIB +AM_SILENT_RULES([yes]) + +CFLAGS="$CFLAGS -Wall" + +dnl Check for SDL2 +PKG_CHECK_MODULES(SDL2, sdl2) +CFLAGS="$CFLAGS $SDL2_CFLAGS" +LIBS="$LIBS $SDL2_LIBS" + +dnl Check for SDL2_mixer +PKG_CHECK_MODULES(SDL2_MIXER, SDL2_mixer) +LIBS="$SDL2_MIXER_LIBS $LIBS" +CFLAGS="$SDL2_MIXER_CFLAGS $CFLAGS" + +AC_CONFIG_FILES([ + Makefile + src/Makefile +]) + +AC_OUTPUT diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..aff4573 --- /dev/null +++ b/src/Makefile.am @@ -0,0 +1,2 @@ +noinst_LIBRARIES = libplay.a +libplay_a_SOURCES = play.c play.h diff --git a/src/macros.h b/src/macros.h new file mode 100644 index 0000000..cee2464 --- /dev/null +++ b/src/macros.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _MACROS_H +#define _MACROS_H + +#include <stdio.h> +#include <stdlib.h> + +#define fatal_if(_cond, _fmt, ...) \ + if (_cond) { \ + fprintf(stderr, "Fatal error: " _fmt "\n", ##__VA_ARGS__); \ + exit(EXIT_FAILURE); \ + } + +#define warn_if(_cond, _fmt, ...) \ + if (_cond) { \ + fprintf(stderr, "Warning: " _fmt "\n", ##__VA_ARGS__); \ + } + +#endif diff --git a/src/play.c b/src/play.c new file mode 100644 index 0000000..ca51c2e --- /dev/null +++ b/src/play.c @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <SDL.h> +#include <SDL_mixer.h> +#include <assert.h> +#include <stdarg.h> +#include <stdint.h> +#include <stdlib.h> /* exit, atexit */ + +#include "macros.h" +#include "play.h" + +/* libplay: SDL2 and SDL2_Mixer glued together a bit */ + +typedef struct play_t { + Mix_Music *music; + char *music_file, *queued_music_file; + unsigned music_flags, queued_music_flags; + Uint32 tick_offsets[PLAY_TICKS_CNT]; + Uint32 ticks_paused_at; + unsigned ticks_paused:1; + unsigned update_needed:1; + const play_ops_t **ops; + int n_ops; + int context; + void *contexts[]; +} play_t; + + +void play_music_set(play_t *play, unsigned flags, const char *fmt, ...) +{ + int fade = (flags & PLAY_MUSIC_FLAG_FADEIN); + int optional = (flags & PLAY_MUSIC_FLAG_OPTIONAL); + int queue = (flags & PLAY_MUSIC_FLAG_QUEUE); + int idempotent = (flags & PLAY_MUSIC_FLAG_IDEMPOTENT); + char file[256] = {}; + Mix_Music *new; + va_list ap; + + va_start(ap, fmt); + vsnprintf(file, sizeof(file), fmt, ap); + va_end(ap); + + assert(!file[sizeof(file) - 1]); + + if (queue && play->music) { + free(play->queued_music_file); + play->queued_music_file = strdup(file); + play->queued_music_flags = (flags & ~PLAY_MUSIC_FLAG_QUEUE); + + return; + } + + if (idempotent && play->music && !strcmp(file, play->music_file)) + return; + + new = Mix_LoadMUS(file); + if (!new && optional) + return; + + fatal_if(!new, + "Unable to load music \"%s\", reason: \"%s\"", file, Mix_GetError()); + + if (play->music) { + if (play->queued_music_file) { + free(play->queued_music_file); + play->queued_music_file = NULL; + play->queued_music_flags = 0; + } + + Mix_HaltMusic(); + Mix_FreeMusic(play->music); + free(play->music_file); + play->music = NULL; + play->music_file = NULL; + play->music_flags = 0; + } + + play->music = new; + fatal_if(!(play->music_file = strdup(file)), + "Unable to dup \"%s\"", file); + play->music_flags = flags; + + Mix_VolumeMusic(100); /* XXX: just hard-coding music volume for now */ + if (fade) { + fatal_if(Mix_FadeInMusic(play->music, 0, 1000), + "Unable to play music"); + } else { + fatal_if(Mix_PlayMusic(play->music, 0), + "Unable to play music"); + } + + Mix_ResumeMusic(); // XXX KLUDGE ALERT: on windows paused music doesn't seem to resume + // automatically via the above playmusic/fadeinmusic stuff. +} + + +/* XXX KLUDGE ALERT: this is necessary because Mix_GetMusicHookData() doesn't + * work unless we hook the mixing. The postmix register doesn't seem to set + * the pointer returned by GetMusicHookData() + * So we use a global, stow the pointer for music_finished() to access :(. + */ +static play_t *__play_ptr_for_music_fixme; + +static void music_finished(void) +{ + play_t *play = __play_ptr_for_music_fixme; + + if (play->queued_music_file) { + char *file = play->queued_music_file; + unsigned flags = play->queued_music_flags; + + play->queued_music_file = NULL; + play->queued_music_flags = 0; + + play_music_set(play, flags, "%s", file); + + free(file); + } + + if (!Mix_PlayingMusic() && (play->music_flags & PLAY_MUSIC_FLAG_LOOP)) + fatal_if(Mix_PlayMusic(play->music, 0), + "Unable to loop music"); +} + + +void play_music_pause(play_t *play) +{ + Mix_PauseMusic(); +} + + +void play_music_resume(play_t *play) +{ + Mix_ResumeMusic(); +} + + +void play_context_enter(play_t *play, int context) +{ + assert(play); + assert(context >= 0 && context < play->n_ops); + + if (play->context == context) + return; + + if (play->ops[play->context]->leave) + play->ops[play->context]->leave(play, play->contexts[play->context]); + + play->context = context; + + if (play->ops[play->context]->enter) + play->ops[play->context]->enter(play, play->contexts[play->context]); + + /* whenever a context switch occurs, this flag triggers immediately calling of update() hooks. */ + play->update_needed = 1; +} + + +/* return context pointer of specified context, + * a negative context requests the current context. + */ +void * play_context(play_t *play, int context) +{ + assert(play); + + if (context < 0) + context = play->context; + + assert(context < play->n_ops); + + return play->contexts[context]; +} + + +/* unpause ticks if necessary, see play_ticks_pause() */ +static inline void ticks_active(play_t *play) +{ + Uint32 delta; + + if (!play->ticks_paused) + return; + + delta = SDL_GetTicks() - play->ticks_paused_at; + + for (int i = 0; i < PLAY_TICKS_CNT; i++) + play->tick_offsets[i] += delta; + + play->ticks_paused = 0; +} + + +/* return ticks counter */ +unsigned play_ticks(play_t *play, play_ticks_t timer) +{ + assert(play); + assert(timer < PLAY_TICKS_CNT); + + ticks_active(play); + + return SDL_GetTicks() - play->tick_offsets[timer]; +} + + +/* reset ticks counter to begin counting from now */ +void play_ticks_reset(play_t *play, play_ticks_t timer) +{ + assert(play); + assert(timer < PLAY_TICKS_CNT); + + ticks_active(play); + + play->tick_offsets[timer] = SDL_GetTicks(); +} + + +/* "pause" timers - this basically just notes the current time, + * and on any subsequent timer use the difference is added to + * all timer offsets as an implicit "resume" before discarding + * the paused state. + */ +void play_ticks_pause(play_t *play) +{ + if (play->ticks_paused) + return; + + play->ticks_paused = 1; + play->ticks_paused_at = SDL_GetTicks(); +} + + +/* returns true if specified duration elapsed, resets timer if so. */ +int play_ticks_elapsed(play_t *play, play_ticks_t timer, unsigned duration) +{ + assert(play); + assert(timer < PLAY_TICKS_CNT); + + if (play_ticks(play, timer) >= duration) { + play_ticks_reset(play, timer); + return 1; + } + + return 0; +} + + +void play_quit(play_t *play) +{ + SDL_Event ev = { .type = SDL_QUIT }; + + SDL_PushEvent(&ev); +} + + +/* This initializes a bunch of SDL stuff and calls all the ops.init() hooks + * serially, in-order, before returning a handle to it all. + * Note this doesn't create an SDL window or OpenGL context, as that's all + * quite game-specific so it's left for presumably the first init hook. + */ +play_t * play_startup(int argc, char *argv[], const play_ops_t *ops[]) +{ + size_t n_ops; + play_t *play; + + assert(ops); + + for (n_ops = 0; ops[n_ops]; n_ops++); + assert(n_ops > 0); + + play = calloc(1, sizeof(play_t) + sizeof(void *) * n_ops); + fatal_if(!play, + "Unable to allocate play_t"); + + fatal_if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_AUDIO|SDL_INIT_GAMECONTROLLER) != 0, + "Unable to initialize SDL"); + + fatal_if(atexit(SDL_Quit), + "Unable to set exit handler"); + + fatal_if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) == -1, + "Unable to open audio"); + + __play_ptr_for_music_fixme = play; + Mix_HookMusicFinished(music_finished); + Mix_AllocateChannels(32); + + /* Just in case the user has dropped a game controller mapping */ + SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt"); + + /* I don't want to get too crazy over here with controller handling, just open all + * the attached controllers at startup and get on with it. + */ + for (int i = 0; i < SDL_NumJoysticks(); ++i) { + SDL_GameController *controller; + char guid[64] = {}; + + SDL_JoystickGetGUIDString(SDL_JoystickGetDeviceGUID(i), guid, sizeof(guid)); + + if (!SDL_IsGameController(i)) { + fprintf(stderr, + "Ignoring unrecognized joystick \"%s\",\n" + "add a \"gamecontrollerdb.txt\" to use it.\n", + guid); + continue; + } + + controller = SDL_GameControllerOpen(i); + warn_if(!controller, + "Could not open game controller \"%s\": %s", + guid, SDL_GetError()); + + /* XXX: For now I'm just opening all of them and losing their handles, + * it seems benign enough, the events will be delivered just the same. + * If the controller handle is needed for things then it gets more tricky.. + */ + } + + for (size_t i = 0; i < n_ops; i++) { + play->n_ops++; + if (ops[i]->init) + play->contexts[i] = ops[i]->init(play, argc, argv); + } + play->ops = ops; + + return play; +} + + +int play_shutdown(play_t *play) +{ +#if 0 + /* XXX: for now just rely on atexit(SDL_Quit()) doing the SDL stuff, + * and don't bother cleaning up anything play-specific, we're exiting. + */ + /* TODO: cleanup all the contexts */ + stage_free(play->contexts[play->context].stage); + SDL_DestroyWindow(play->window); + Mix_CloseAudio(); +#endif + + return EXIT_SUCCESS; +} + + +void play_run(play_t *play) +{ + for (;;) { + SDL_Event ev; + + do { + play->update_needed = 0; + + if (play->ops[play->context]->update) + play->ops[play->context]->update(play, play->contexts[play->context]); + + /* see comment in play_context_enter() for why play->update_needed exists */ + } while (play->update_needed); + + if (play->ops[play->context]->render) + play->ops[play->context]->render(play, play->contexts[play->context]); + + while (!play->update_needed && SDL_PollEvent(&ev)) { + if (ev.type == SDL_APP_TERMINATING || ev.type == SDL_QUIT) + return; + + if (play->ops[play->context]->dispatch) + play->ops[play->context]->dispatch(play, play->contexts[play->context], &ev); + } + } +} diff --git a/src/play.h b/src/play.h new file mode 100644 index 0000000..0f1d2da --- /dev/null +++ b/src/play.h @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2020 - Vito Caputo - <vcaputo@pengaru.com> + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3 as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef _PLAY_H +#define _PLAY_H + +#include <SDL.h> + +typedef enum play_ticks_t { + PLAY_TICKS_TIMER0, + PLAY_TICKS_TIMER1, + PLAY_TICKS_TIMER2, + PLAY_TICKS_TIMER3, + PLAY_TICKS_TIMER4, + PLAY_TICKS_TIMER5, + PLAY_TICKS_TIMER6, + PLAY_TICKS_TIMER7, + PLAY_TICKS_TIMER8, + PLAY_TICKS_TIMER9, + PLAY_TICKS_CNT +} play_ticks_t; + +#define PLAY_MUSIC_FLAG_LOOP (1L) +#define PLAY_MUSIC_FLAG_FADEIN (1L << 1) +#define PLAY_MUSIC_FLAG_OPTIONAL (1L << 2) +#define PLAY_MUSIC_FLAG_QUEUE (1L << 3) +#define PLAY_MUSIC_FLAG_IDEMPOTENT (1L << 4) + +typedef struct play_t play_t; + +typedef struct play_ops_t { + void * (*init)(play_t *play, int argc, char *argv[]); + void (*destroy)(play_t *play, void *context); + void (*enter)(play_t *play, void *context); + void (*leave)(play_t *play, void *context); + void (*update)(play_t *play, void *context); + void (*render)(play_t *play, void *context); + void (*dispatch)(play_t *play, void *context, SDL_Event *event); +} play_ops_t; + +play_t * play_startup(int argc, char *argv[], const play_ops_t *ops[]); +int play_shutdown(play_t *play); +void play_run(play_t *play); + +void play_music_set(play_t *play, unsigned flags, const char *fmt, ...); +void play_music_pause(play_t *play); +void play_music_resume(play_t *play); +void play_context_enter(play_t *play, int context); +void * play_context(play_t *play, int context); +unsigned play_ticks(play_t *play, play_ticks_t timer); +void play_ticks_reset(play_t *play, play_ticks_t timer); +void play_ticks_pause(play_t *play); +int play_ticks_elapsed(play_t *play, play_ticks_t timer, unsigned duration); +void play_quit(play_t *play); + +#endif |