awesome!!!
This commit is contained in:
31
src/config.h
Normal file
31
src/config.h
Normal file
@ -0,0 +1,31 @@
|
||||
#ifndef CONFIG_H
|
||||
#define CONFIG_H
|
||||
|
||||
/*
|
||||
* this file is for configuration values which are to be set at
|
||||
* compile time. generally speaking, it's for things that would be unwise to
|
||||
* change mid-development without considering the work it might take to
|
||||
* adapt the game logic.
|
||||
*
|
||||
* if you're looking for distribution-related definitions like
|
||||
* APP_NAME, you should know that they are set from CMake.
|
||||
*/
|
||||
|
||||
|
||||
#define TICKS_PER_SECOND 60
|
||||
#define FIXED_DELTA_TIME (1.0 / TICKS_PER_SECOND)
|
||||
|
||||
#define RENDER_BASE_WIDTH 640
|
||||
#define RENDER_BASE_HEIGHT 360
|
||||
|
||||
#define TEXTURE_ATLAS_SIZE 2048
|
||||
#define TEXTURE_ATLAS_BIT_DEPTH 32
|
||||
#define TEXTURE_ATLAS_FORMAT SDL_PIXELFORMAT_RGBA32
|
||||
|
||||
#define NUM_KEYBIND_SLOTS 8
|
||||
|
||||
/* 1024 * 1024 */
|
||||
/* #define UMKA_STACK_SIZE 1048576 */
|
||||
|
||||
|
||||
#endif
|
3
src/context.c
Normal file
3
src/context.c
Normal file
@ -0,0 +1,3 @@
|
||||
#include "context.h"
|
||||
|
||||
t_ctx ctx;
|
68
src/context.h
Normal file
68
src/context.h
Normal file
@ -0,0 +1,68 @@
|
||||
#ifndef CONTEXT_H
|
||||
#define CONTEXT_H
|
||||
|
||||
|
||||
#include "config.h"
|
||||
#include "private/rendering.h"
|
||||
#include "textures.h"
|
||||
#include "input.h"
|
||||
#include "game_api.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
typedef struct context {
|
||||
/* the program's actual argc and argv */
|
||||
int argc;
|
||||
char **argv;
|
||||
|
||||
struct texture_cache texture_cache;
|
||||
struct input_state input;
|
||||
|
||||
struct sprite_primitive *render_queue_sprites;
|
||||
struct rect_primitive *render_queue_rectangles;
|
||||
struct circle_primitive *render_queue_circles;
|
||||
|
||||
struct audio_channel *audio_channels;
|
||||
|
||||
struct circle_radius_cache_item *circle_radius_hash;
|
||||
|
||||
/* main loop machinery */
|
||||
int64_t clocks_per_second;
|
||||
int64_t prev_frame_time;
|
||||
int64_t desired_frametime; /* how long one tick should be */
|
||||
int64_t frame_accumulator;
|
||||
int64_t delta_averager_residual;
|
||||
int64_t time_averager[4];
|
||||
uint64_t tick_count;
|
||||
uint64_t step_count;
|
||||
|
||||
/* set just once on startup */
|
||||
uint64_t random_seed;
|
||||
|
||||
/* this should be a multiple of TICKS_PER_SECOND */
|
||||
/* use it to simulate low framerate (e.g. at 60 tps, set to 2 for 30 fps) */
|
||||
/* it can be changed at runtime; any resulting logic anomalies are bugs */
|
||||
unsigned int update_multiplicity;
|
||||
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Window *window;
|
||||
uint32_t window_id;
|
||||
int window_w;
|
||||
int window_h;
|
||||
|
||||
bool was_successful;
|
||||
|
||||
/* you may read from and write to these from game code */
|
||||
void *udata;
|
||||
bool debug;
|
||||
bool is_running;
|
||||
bool resync_flag;
|
||||
} t_ctx;
|
||||
|
||||
extern t_ctx ctx;
|
||||
|
||||
#endif
|
70
src/game/game.c
Normal file
70
src/game/game.c
Normal file
@ -0,0 +1,70 @@
|
||||
#include "game.h"
|
||||
#include "../textures.h"
|
||||
#include "../game_api.h"
|
||||
#include "state.h"
|
||||
#include "scenes/scene.h"
|
||||
#include "scenes/title.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <malloc.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
void game_step(int64_t delta) {
|
||||
(void)delta;
|
||||
}
|
||||
|
||||
|
||||
void game_tick(void) {
|
||||
if (ctx.tick_count == 0) {
|
||||
ctx.udata = ccalloc(1, sizeof (struct state));
|
||||
struct state *state = ctx.udata;
|
||||
state->ctx = &ctx;
|
||||
state->scene = title_scene(state);
|
||||
|
||||
input_add_action(&ctx.input, "debug_dump_atlases");
|
||||
input_bind_action_scancode(&ctx.input, "debug_dump_atlases", SDL_SCANCODE_HOME);
|
||||
|
||||
input_add_action(&ctx.input, "debug_toggle");
|
||||
input_bind_action_scancode(&ctx.input, "debug_toggle", SDL_SCANCODE_BACKSPACE);
|
||||
|
||||
input_add_action(&ctx.input, "player_left");
|
||||
input_bind_action_scancode(&ctx.input, "player_left", SDL_SCANCODE_LEFT);
|
||||
|
||||
input_add_action(&ctx.input, "player_right");
|
||||
input_bind_action_scancode(&ctx.input, "player_right", SDL_SCANCODE_RIGHT);
|
||||
|
||||
input_add_action(&ctx.input, "player_jump");
|
||||
input_bind_action_scancode(&ctx.input, "player_jump", SDL_SCANCODE_X);
|
||||
|
||||
input_add_action(&ctx.input, "ui_accept");
|
||||
input_bind_action_scancode(&ctx.input, "ui_accept", SDL_SCANCODE_RETURN);
|
||||
}
|
||||
|
||||
struct state *state = ctx.udata;
|
||||
|
||||
if (input_is_action_just_pressed(&ctx.input, "debug_dump_atlases")) {
|
||||
textures_dump_atlases(&ctx.texture_cache);
|
||||
}
|
||||
|
||||
if (input_is_action_just_pressed(&ctx.input, "debug_toggle")) {
|
||||
ctx.debug = !ctx.debug;
|
||||
}
|
||||
|
||||
state->scene->tick(state);
|
||||
|
||||
/* there's a scene switch pending, we can do it now that the tick is done */
|
||||
if (state->next_scene != NULL) {
|
||||
state->scene->end(state);
|
||||
state->scene = state->next_scene;
|
||||
state->is_scene_switching = false;
|
||||
state->next_scene = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void game_end(void) {
|
||||
struct state *state = ctx.udata;
|
||||
state->scene->end(state);
|
||||
free(state);
|
||||
}
|
15
src/game/game.h
Normal file
15
src/game/game.h
Normal file
@ -0,0 +1,15 @@
|
||||
#ifndef GAME_H
|
||||
#define GAME_H
|
||||
|
||||
|
||||
#include "../game_api.h"
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
void game_step(int64_t delta);
|
||||
void game_tick(void);
|
||||
void game_end(void);
|
||||
|
||||
|
||||
#endif
|
303
src/game/player.c
Normal file
303
src/game/player.c
Normal file
@ -0,0 +1,303 @@
|
||||
#include "player.h"
|
||||
#include "world.h"
|
||||
#include "../game_api.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <tgmath.h>
|
||||
|
||||
|
||||
static void update_timers(struct player *player) {
|
||||
tick_timer(&player->jump_air_timer);
|
||||
tick_timer(&player->jump_coyote_timer);
|
||||
tick_timer(&player->jump_buffer_timer);
|
||||
}
|
||||
|
||||
|
||||
static void input_move(struct input_state *input, struct player *player) {
|
||||
/* apply horizontal damping when the player stops moving */
|
||||
/* in other words, make it decelerate to a standstill */
|
||||
if (!input_is_action_pressed(input, "player_left") &&
|
||||
!input_is_action_pressed(input, "player_right"))
|
||||
{
|
||||
player->dx *= player->horizontal_damping;
|
||||
}
|
||||
|
||||
int input_dir = 0;
|
||||
if (input_is_action_pressed(input, "player_left"))
|
||||
input_dir = -1;
|
||||
if (input_is_action_pressed(input, "player_right"))
|
||||
input_dir = 1;
|
||||
if (input_is_action_pressed(input, "player_left") &&
|
||||
input_is_action_pressed(input, "player_right"))
|
||||
input_dir = 0;
|
||||
|
||||
player->dx += (float)input_dir * player->run_horizontal_speed;
|
||||
player->dx = SDL_clamp(player->dx, -player->max_dx, player->max_dx);
|
||||
|
||||
if (fabs(player->dx) < player->run_horizontal_speed) {
|
||||
player->dx = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void jump(struct player *player) {
|
||||
player->jump_coyote_timer = 0;
|
||||
player->jump_buffer_timer = 0;
|
||||
player->dy = player->jump_force_initial;
|
||||
player->action = PLAYER_ACTION_JUMP;
|
||||
player->jump_air_timer = player->jump_air_ticks;
|
||||
}
|
||||
|
||||
|
||||
static void input_jump(struct input_state *input, struct player *player) {
|
||||
player->current_gravity_multiplier = player->jump_default_multiplier;
|
||||
|
||||
if (input_is_action_just_pressed(input, "player_jump")) {
|
||||
player->jump_air_timer = 0;
|
||||
player->jump_buffer_timer = player->jump_buffer_ticks;
|
||||
|
||||
if (player->action == PLAYER_ACTION_GROUND || player->jump_coyote_timer > 0) {
|
||||
jump(player);
|
||||
}
|
||||
}
|
||||
|
||||
if (input_is_action_pressed(input, "player_jump")) {
|
||||
if (player->action != PLAYER_ACTION_GROUND && player->jump_air_timer > 0) {
|
||||
player->current_gravity_multiplier = player->jump_boosted_multiplier;
|
||||
player->dy += player->jump_force_increase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void update_collider_x(struct player *player) {
|
||||
player->collider_x.w = player->rect.w;
|
||||
player->collider_x.h = player->rect.h - 8;
|
||||
player->collider_x.x = player->rect.x;
|
||||
player->collider_x.y = player->rect.y + ((player->rect.h - player->collider_x.h) / 2);
|
||||
}
|
||||
|
||||
|
||||
static void update_collider_y(struct player *player) {
|
||||
player->collider_y.w = player->rect.w;
|
||||
player->collider_y.h = player->rect.h;
|
||||
player->collider_y.x = player->rect.x + ((player->rect.w - player->collider_y.w) / 2);
|
||||
player->collider_y.y = player->rect.y;
|
||||
}
|
||||
|
||||
|
||||
static void apply_gravity(struct player *player, float gravity) {
|
||||
player->dy -= gravity * player->current_gravity_multiplier;
|
||||
player->dy = fmax(player->dy, -player->terminal_velocity);
|
||||
|
||||
if (player->dy < 0) {
|
||||
/* just started falling */
|
||||
if (player->action == PLAYER_ACTION_GROUND) {
|
||||
player->jump_coyote_timer = player->jump_coyote_ticks;
|
||||
}
|
||||
|
||||
player->action = PLAYER_ACTION_FALL;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* returns whether or not a correction was applied */
|
||||
static bool corner_correct(struct player *player, t_frect collision) {
|
||||
/*
|
||||
* somewhat of a hack here. we only want to do corner correction
|
||||
* if the corner in question really is the corner of a "platform,"
|
||||
* and not simply the edge of a single tile in a row of many
|
||||
* tiles, as that would briefly clip the player into the ceiling,
|
||||
* halting movement. the dumbest way i could think of to fix this
|
||||
* is to simply ensure that there's no tile to possibly clip into
|
||||
* before correcting, by checking if there's a tile right above
|
||||
* the center of the player
|
||||
*/
|
||||
if (world_is_tile_at(player->world,
|
||||
player->rect.x + (player->rect.w / 2),
|
||||
player->rect.y - 1))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
|
||||
float collision_center_x = collision.x + (collision.w / 2);
|
||||
float abs_difference = fabs(player_center_x - collision_center_x);
|
||||
|
||||
/* we're good, no correction needed */
|
||||
if (abs_difference < player->jump_corner_correction_offset)
|
||||
return false;
|
||||
|
||||
/* collision was on player's right side */
|
||||
if (player_center_x < collision_center_x) {
|
||||
player->rect.x -= collision.w / 2;
|
||||
}
|
||||
/* collision was on player's left side */
|
||||
else if (player_center_x > collision_center_x) {
|
||||
player->rect.x += collision.w / 2;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
static void calc_collisions_x(struct player *player) {
|
||||
t_frect collision;
|
||||
bool is_colliding = world_find_intersect_frect(player->world, player->collider_x, &collision);
|
||||
if (!is_colliding) return;
|
||||
|
||||
float player_center_x = player->collider_x.x + (player->collider_x.w / 2);
|
||||
float collision_center_x = collision.x + (collision.w / 2);
|
||||
|
||||
enum collision_direction { COLLISION_LEFT = -1, COLLISION_RIGHT = 1 };
|
||||
enum collision_direction dir_x =
|
||||
player_center_x > collision_center_x ? COLLISION_LEFT : COLLISION_RIGHT;
|
||||
|
||||
player->rect.x -= collision.w * (float)dir_x;
|
||||
player->dx = 0;
|
||||
}
|
||||
|
||||
|
||||
static void calc_collisions_y(struct player *player) {
|
||||
t_frect collision;
|
||||
bool is_colliding = world_find_intersect_frect(player->world, player->collider_y, &collision);
|
||||
if (!is_colliding) return;
|
||||
|
||||
float player_center_y = player->collider_y.y + (player->collider_y.h / 2);
|
||||
float collision_center_y = collision.y + (collision.h / 2);
|
||||
|
||||
enum collision_direction { COLLISION_ABOVE = -1, COLLISION_BELOW = 1 };
|
||||
enum collision_direction dir_y =
|
||||
player_center_y > collision_center_y ? COLLISION_ABOVE : COLLISION_BELOW;
|
||||
|
||||
/* before the resolution */
|
||||
if (dir_y == COLLISION_ABOVE) {
|
||||
if (corner_correct(player, collision)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* resolution */
|
||||
player->rect.y -= collision.h * (float)dir_y;
|
||||
player->dy = 0;
|
||||
|
||||
/* after the resolution */
|
||||
if (dir_y == COLLISION_BELOW) {
|
||||
/* bandaid fix for float precision-related jittering */
|
||||
player->rect.y = ceilf(player->rect.y);
|
||||
|
||||
player->action = PLAYER_ACTION_GROUND;
|
||||
|
||||
if (player->jump_buffer_timer > 0) {
|
||||
jump(player);
|
||||
apply_gravity(player, player->world->gravity);
|
||||
}
|
||||
} else if (dir_y == COLLISION_ABOVE) {
|
||||
player->jump_air_timer = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct player *player_create(struct world *world) {
|
||||
struct player *player = cmalloc(sizeof *player);
|
||||
|
||||
*player = (struct player) {
|
||||
.world = world,
|
||||
|
||||
.sprite_w = 48,
|
||||
.sprite_h = 48,
|
||||
|
||||
.rect = (t_frect) {
|
||||
.x = 92,
|
||||
.y = 200,
|
||||
.w = 16,
|
||||
.h = 32,
|
||||
},
|
||||
|
||||
.action = PLAYER_ACTION_GROUND,
|
||||
|
||||
.collider_thickness = 42,
|
||||
.max_dx = 8,
|
||||
.run_horizontal_speed = 0.5f,
|
||||
.horizontal_damping = 0.9f,
|
||||
.current_gravity_multiplier = 1,
|
||||
.terminal_velocity = 30,
|
||||
|
||||
.jump_force_initial = 10,
|
||||
.jump_force_increase = 1,
|
||||
.jump_air_ticks = 18,
|
||||
.jump_coyote_ticks = 8,
|
||||
.jump_buffer_ticks = 8,
|
||||
|
||||
.jump_default_multiplier = 1.4f,
|
||||
.jump_boosted_multiplier = 1,
|
||||
|
||||
.jump_corner_correction_offset = 16.0f,
|
||||
};
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
|
||||
static void drawdef(struct player *player) {
|
||||
push_sprite("/assets/player/baron-walk.png",
|
||||
(t_frect) {
|
||||
.x = player->rect.x + ((player->rect.w - player->sprite_w) / 2),
|
||||
.y = player->rect.y - 8,
|
||||
.w = player->sprite_w,
|
||||
.h = player->sprite_h,
|
||||
});
|
||||
|
||||
push_circle((t_fvec2) { 256, 128 },
|
||||
24,
|
||||
(t_color) { 255, 0, 0, 255 });
|
||||
}
|
||||
|
||||
|
||||
static void drawdef_debug(struct player *player) {
|
||||
if (!ctx.debug)
|
||||
return;
|
||||
|
||||
/* const int info_separation = 24; */
|
||||
/* const struct RectPrimitive info_theme = { */
|
||||
/* .x = 8, */
|
||||
/* .r = 0, .g = 0, .b = 0, .a = 255, */
|
||||
/* }; */
|
||||
|
||||
push_rectangle(player->collider_x,
|
||||
(t_color){ 0, 0, 255, 128 });
|
||||
|
||||
push_rectangle(player->collider_y,
|
||||
(t_color){ 0, 0, 255, 128 });
|
||||
}
|
||||
|
||||
|
||||
void player_destroy(struct player *player) {
|
||||
free(player);
|
||||
}
|
||||
|
||||
|
||||
void player_calc(struct player *player) {
|
||||
update_timers(player);
|
||||
|
||||
input_move(&ctx.input, player);
|
||||
input_jump(&ctx.input, player);
|
||||
|
||||
player->rect.x += player->dx;
|
||||
update_collider_x(player);
|
||||
calc_collisions_x(player);
|
||||
|
||||
apply_gravity(player, player->world->gravity);
|
||||
player->rect.y -= player->dy;
|
||||
update_collider_y(player);
|
||||
calc_collisions_y(player);
|
||||
|
||||
update_collider_x(player);
|
||||
update_collider_y(player);
|
||||
|
||||
drawdef(player);
|
||||
drawdef_debug(player);
|
||||
}
|
66
src/game/player.h
Normal file
66
src/game/player.h
Normal file
@ -0,0 +1,66 @@
|
||||
#ifndef PLAYER_H
|
||||
#define PLAYER_H
|
||||
|
||||
|
||||
#include "../game_api.h"
|
||||
|
||||
|
||||
struct world;
|
||||
|
||||
|
||||
enum player_action {
|
||||
PLAYER_ACTION_GROUND,
|
||||
PLAYER_ACTION_FALL,
|
||||
PLAYER_ACTION_JUMP,
|
||||
};
|
||||
|
||||
|
||||
struct player {
|
||||
struct world *world;
|
||||
|
||||
/* visual */
|
||||
float sprite_w;
|
||||
float sprite_h;
|
||||
|
||||
/* body */
|
||||
t_frect rect;
|
||||
|
||||
/* state */
|
||||
enum player_action action;
|
||||
|
||||
/* physics */
|
||||
t_frect collider_x;
|
||||
t_frect collider_y;
|
||||
int collider_thickness;
|
||||
float dx;
|
||||
float dy;
|
||||
float max_dx;
|
||||
float run_horizontal_speed;
|
||||
float horizontal_damping;
|
||||
float current_gravity_multiplier;
|
||||
float terminal_velocity;
|
||||
|
||||
/* jumping */
|
||||
float jump_force_initial;
|
||||
float jump_force_increase; /* aka "jump power", increment of dy per tick */
|
||||
int jump_air_ticks;
|
||||
int jump_air_timer;
|
||||
int jump_coyote_ticks;
|
||||
int jump_coyote_timer;
|
||||
int jump_buffer_ticks;
|
||||
int jump_buffer_timer;
|
||||
|
||||
/* gravity multipliers */
|
||||
float jump_default_multiplier;
|
||||
float jump_boosted_multiplier;
|
||||
|
||||
float jump_corner_correction_offset; /* from center */
|
||||
};
|
||||
|
||||
|
||||
struct player *player_create(struct world *world);
|
||||
void player_destroy(struct player *player);
|
||||
void player_calc(struct player *player);
|
||||
|
||||
|
||||
#endif
|
32
src/game/scenes/ingame.c
Normal file
32
src/game/scenes/ingame.c
Normal file
@ -0,0 +1,32 @@
|
||||
#include "ingame.h"
|
||||
#include "title.h"
|
||||
#include "scene.h"
|
||||
|
||||
|
||||
static void ingame_tick(struct state *state) {
|
||||
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
|
||||
|
||||
world_drawdef(scn->world);
|
||||
player_calc(scn->player);
|
||||
}
|
||||
|
||||
|
||||
static void ingame_end(struct state *state) {
|
||||
struct scene_ingame *scn = (struct scene_ingame *)state->scene;
|
||||
player_destroy(scn->player);
|
||||
world_destroy(scn->world);
|
||||
}
|
||||
|
||||
|
||||
struct scene *ingame_scene(struct state *state) {
|
||||
(void)state;
|
||||
|
||||
struct scene_ingame *new_scene = ccalloc(1, sizeof *new_scene);
|
||||
new_scene->base.tick = ingame_tick;
|
||||
new_scene->base.end = ingame_end;
|
||||
|
||||
new_scene->world = world_create();
|
||||
new_scene->player = player_create(new_scene->world);
|
||||
|
||||
return (struct scene *)new_scene;
|
||||
}
|
23
src/game/scenes/ingame.h
Normal file
23
src/game/scenes/ingame.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef INGAME_H
|
||||
#define INGAME_H
|
||||
|
||||
|
||||
#include "../../game_api.h"
|
||||
#include "../state.h"
|
||||
#include "scene.h"
|
||||
#include "../player.h"
|
||||
#include "../world.h"
|
||||
|
||||
|
||||
struct scene_ingame {
|
||||
struct scene base;
|
||||
|
||||
struct world *world;
|
||||
struct player *player;
|
||||
};
|
||||
|
||||
|
||||
struct scene *ingame_scene(struct state *state);
|
||||
|
||||
|
||||
#endif
|
8
src/game/scenes/scene.c
Normal file
8
src/game/scenes/scene.c
Normal file
@ -0,0 +1,8 @@
|
||||
#include "scene.h"
|
||||
#include "../state.h"
|
||||
|
||||
|
||||
void switch_to(struct state *state, struct scene *(*scene_func)(struct state *)) {
|
||||
state->next_scene = scene_func(state);
|
||||
state->is_scene_switching = true;
|
||||
}
|
16
src/game/scenes/scene.h
Normal file
16
src/game/scenes/scene.h
Normal file
@ -0,0 +1,16 @@
|
||||
#ifndef SCENE_H
|
||||
#define SCENE_H
|
||||
|
||||
|
||||
struct state;
|
||||
struct scene {
|
||||
char *id;
|
||||
void (*tick)(struct state *);
|
||||
void (*end)(struct state *);
|
||||
};
|
||||
|
||||
|
||||
void switch_to(struct state *state, struct scene *(*scene_func)(struct state *));
|
||||
|
||||
|
||||
#endif
|
41
src/game/scenes/title.c
Normal file
41
src/game/scenes/title.c
Normal file
@ -0,0 +1,41 @@
|
||||
#include "title.h"
|
||||
#include "ingame.h"
|
||||
#include "../world.h"
|
||||
#include "../player.h"
|
||||
#include "../../game_api.h"
|
||||
|
||||
|
||||
static void title_tick(struct state *state) {
|
||||
struct scene_title *scn = (struct scene_title *)state->scene;
|
||||
(void)scn;
|
||||
|
||||
if (input_is_action_just_pressed(&state->ctx->input, "ui_accept")) {
|
||||
switch_to(state, ingame_scene);
|
||||
}
|
||||
|
||||
|
||||
push_sprite("/assets/title.png", (t_frect) {
|
||||
(RENDER_BASE_WIDTH / 2) - (320 / 2), 64, 320, 128
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static void title_end(struct state *state) {
|
||||
struct scene_title *scn = (struct scene_title *)state->scene;
|
||||
player_destroy(scn->player);
|
||||
world_destroy(scn->world);
|
||||
}
|
||||
|
||||
|
||||
struct scene *title_scene(struct state *state) {
|
||||
(void)state;
|
||||
|
||||
struct scene_title *new_scene = ccalloc(1, sizeof *new_scene);
|
||||
new_scene->base.tick = title_tick;
|
||||
new_scene->base.end = title_end;
|
||||
|
||||
new_scene->world = world_create();
|
||||
new_scene->player = player_create(new_scene->world);
|
||||
|
||||
return (struct scene *)new_scene;
|
||||
}
|
23
src/game/scenes/title.h
Normal file
23
src/game/scenes/title.h
Normal file
@ -0,0 +1,23 @@
|
||||
#ifndef TITLE_H
|
||||
#define TITLE_H
|
||||
|
||||
|
||||
#include "../../game_api.h"
|
||||
#include "../state.h"
|
||||
#include "scene.h"
|
||||
#include "../player.h"
|
||||
#include "../world.h"
|
||||
|
||||
|
||||
struct scene_title {
|
||||
struct scene base;
|
||||
|
||||
struct world *world;
|
||||
struct player *player;
|
||||
};
|
||||
|
||||
|
||||
struct scene *title_scene(struct state *state);
|
||||
|
||||
|
||||
#endif
|
16
src/game/state.h
Normal file
16
src/game/state.h
Normal file
@ -0,0 +1,16 @@
|
||||
#ifndef STATE_H
|
||||
#define STATE_H
|
||||
|
||||
|
||||
#include "../game_api.h"
|
||||
|
||||
|
||||
struct state {
|
||||
t_ctx *ctx;
|
||||
struct scene *scene;
|
||||
struct scene *next_scene;
|
||||
bool is_scene_switching;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
187
src/game/world.c
Normal file
187
src/game/world.c
Normal file
@ -0,0 +1,187 @@
|
||||
#include "world.h"
|
||||
#include "../game_api.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <tgmath.h>
|
||||
|
||||
|
||||
static void update_tiles(struct world *world) {
|
||||
for (size_t row = 0; row < world->tilemap_height; ++row) {
|
||||
for (size_t col = 0; col < world->tilemap_width; ++col) {
|
||||
world->tiles[(row * world->tilemap_width) + col] = (struct tile) {
|
||||
.rect = (t_rect) {
|
||||
.x = (int)col * world->tile_size,
|
||||
.y = (int)row * world->tile_size,
|
||||
.w = world->tile_size,
|
||||
.h = world->tile_size,
|
||||
},
|
||||
.type = world->tilemap[row][col],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static t_vec2 to_grid_location(struct world *world, float x, float y) {
|
||||
return (t_vec2) {
|
||||
.x = (int)floor(x / (float)world->tile_size),
|
||||
.y = (int)floor(y / (float)world->tile_size),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static void drawdef_debug(struct world *world) {
|
||||
if (!ctx.debug) return;
|
||||
|
||||
for (size_t i = 0; i < world->tilemap_height * world->tilemap_width; ++i) {
|
||||
if (world->tiles[i].type == TILE_TYPE_VOID) continue;
|
||||
|
||||
push_rectangle(to_frect(world->tiles[i].rect),
|
||||
(t_color) { 255, 0, 255, 128 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct world *world_create(void) {
|
||||
struct world *world = cmalloc(sizeof *world);
|
||||
|
||||
*world = (struct world) {
|
||||
.tiles = NULL,
|
||||
.tile_size = 42,
|
||||
.tilemap_width = 20,
|
||||
.tilemap_height = 12,
|
||||
.gravity = 1,
|
||||
};
|
||||
|
||||
/* create the tilemap */
|
||||
/* it simply stores what's in each tile as a 2d array */
|
||||
/* on its own, it's entirely unrelated to drawing or logic */
|
||||
world->tilemap = cmalloc(sizeof *world->tilemap * world->tilemap_height);
|
||||
for (size_t i = 0; i < world->tilemap_height; ++i) {
|
||||
world->tilemap[i] = cmalloc(sizeof **world->tilemap * world->tilemap_width);
|
||||
|
||||
for (size_t j = 0; j < world->tilemap_width; ++j) {
|
||||
world->tilemap[i][j] = TILE_TYPE_VOID;
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 12; ++i) {
|
||||
world->tilemap[world->tilemap_height-2][2+i] = TILE_TYPE_SOLID;
|
||||
}
|
||||
world->tilemap[world->tilemap_height-3][8] = TILE_TYPE_SOLID;
|
||||
world->tilemap[world->tilemap_height-4][8] = TILE_TYPE_SOLID;
|
||||
world->tilemap[world->tilemap_height-5][10] = TILE_TYPE_SOLID;
|
||||
world->tilemap[world->tilemap_height-6][10] = TILE_TYPE_SOLID;
|
||||
for (size_t i = 0; i < 7; ++i) {
|
||||
world->tilemap[world->tilemap_height-6][12+i] = TILE_TYPE_SOLID;
|
||||
}
|
||||
|
||||
/* the tiles array contains data meant to be used by other logic */
|
||||
/* most importantly, it is used to draw the tiles */
|
||||
const size_t tile_count = world->tilemap_height * world->tilemap_width;
|
||||
world->tiles = cmalloc(sizeof *world->tiles * tile_count);
|
||||
update_tiles(world);
|
||||
|
||||
return world;
|
||||
}
|
||||
|
||||
|
||||
void world_destroy(struct world *world) {
|
||||
free(world->tiles);
|
||||
|
||||
for (size_t i = 0; i < world->tilemap_height; ++i) {
|
||||
free(world->tilemap[i]);
|
||||
}
|
||||
free(world->tilemap);
|
||||
|
||||
free(world);
|
||||
}
|
||||
|
||||
|
||||
void world_drawdef(struct world *world) {
|
||||
for (size_t i = 0; i < world->tilemap_height * world->tilemap_width; ++i) {
|
||||
if (world->tiles[i].type == TILE_TYPE_VOID)
|
||||
continue;
|
||||
|
||||
push_sprite("/assets/white.png", to_frect(world->tiles[i].rect));
|
||||
}
|
||||
|
||||
drawdef_debug(world);
|
||||
}
|
||||
|
||||
|
||||
bool world_find_intersect_frect(struct world *world, t_frect rect, t_frect *intersection) {
|
||||
bool is_intersecting = false;
|
||||
|
||||
const size_t tile_count = world->tilemap_height * world->tilemap_width;
|
||||
for (size_t i = 0; i < tile_count; ++i) {
|
||||
if (world->tiles[i].type == TILE_TYPE_VOID)
|
||||
continue;
|
||||
|
||||
t_frect tile_frect = {
|
||||
.x = (float)(world->tiles[i].rect.x),
|
||||
.y = (float)(world->tiles[i].rect.y),
|
||||
.w = (float)(world->tiles[i].rect.w),
|
||||
.h = (float)(world->tiles[i].rect.h),
|
||||
};
|
||||
|
||||
if (intersection == NULL) {
|
||||
t_frect temp;
|
||||
is_intersecting = intersect_frect(&rect, &tile_frect, &temp);
|
||||
} else {
|
||||
is_intersecting = intersect_frect(&rect, &tile_frect, intersection);
|
||||
}
|
||||
|
||||
if (is_intersecting)
|
||||
break;
|
||||
}
|
||||
|
||||
return is_intersecting;
|
||||
}
|
||||
|
||||
|
||||
bool world_find_intersect_rect(struct world *world, t_rect rect, t_rect *intersection) {
|
||||
bool is_intersecting = false;
|
||||
|
||||
const size_t tile_count = world->tilemap_height * world->tilemap_width;
|
||||
for (size_t i = 0; i < tile_count; ++i) {
|
||||
if (world->tiles[i].type == TILE_TYPE_VOID)
|
||||
continue;
|
||||
|
||||
t_rect *tile_rect = &world->tiles[i].rect;
|
||||
|
||||
if (intersection == NULL) {
|
||||
t_rect temp;
|
||||
is_intersecting = intersect_rect(&rect, tile_rect, &temp);
|
||||
} else {
|
||||
is_intersecting = intersect_rect(&rect, tile_rect, intersection);
|
||||
}
|
||||
|
||||
if (is_intersecting)
|
||||
break;
|
||||
}
|
||||
|
||||
return is_intersecting;
|
||||
}
|
||||
|
||||
|
||||
bool world_is_tile_at(struct world *world, float x, float y) {
|
||||
t_vec2 position_in_grid = to_grid_location(world, x, y);
|
||||
return world->tilemap[position_in_grid.y][position_in_grid.x] != TILE_TYPE_VOID;
|
||||
}
|
||||
|
||||
|
||||
void world_place_tile(struct world *world, float x, float y) {
|
||||
t_vec2 position_in_grid = to_grid_location(world, x, y);
|
||||
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_SOLID;
|
||||
update_tiles(world);
|
||||
}
|
||||
|
||||
|
||||
void world_remove_tile(struct world *world, float x, float y) {
|
||||
t_vec2 position_in_grid = to_grid_location(world, x, y);
|
||||
world->tilemap[position_in_grid.y][position_in_grid.x] = TILE_TYPE_VOID;
|
||||
update_tiles(world);
|
||||
}
|
44
src/game/world.h
Normal file
44
src/game/world.h
Normal file
@ -0,0 +1,44 @@
|
||||
#ifndef WORLD_H
|
||||
#define WORLD_H
|
||||
|
||||
#include "../game_api.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
enum tile_type {
|
||||
TILE_TYPE_VOID,
|
||||
TILE_TYPE_SOLID,
|
||||
};
|
||||
|
||||
|
||||
struct tile {
|
||||
t_rect rect;
|
||||
enum tile_type type;
|
||||
};
|
||||
|
||||
|
||||
struct world {
|
||||
enum tile_type **tilemap;
|
||||
struct tile *tiles;
|
||||
|
||||
int tile_size;
|
||||
unsigned int tilemap_width;
|
||||
unsigned int tilemap_height;
|
||||
size_t tile_nonvoid_count;
|
||||
float gravity;
|
||||
};
|
||||
|
||||
|
||||
struct world *world_create(void);
|
||||
void world_destroy(struct world *world);
|
||||
void world_drawdef(struct world *world);
|
||||
bool world_find_intersect_frect(struct world *world, t_frect rect, t_frect *intersection);
|
||||
bool world_find_intersect_rect(struct world *world, t_rect rect, t_rect *intersection);
|
||||
bool world_is_tile_at(struct world *world, float x, float y);
|
||||
void world_place_tile(struct world *world, float x, float y);
|
||||
void world_remove_tile(struct world *world, float x, float y);
|
||||
|
||||
|
||||
#endif
|
13
src/game_api.h
Normal file
13
src/game_api.h
Normal file
@ -0,0 +1,13 @@
|
||||
/* include this header in game code to get the usable parts of the engine */
|
||||
#ifndef GAME_API_H
|
||||
#define GAME_API_H
|
||||
|
||||
|
||||
#include "context.h"
|
||||
#include "rendering.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
|
||||
#endif
|
296
src/input.c
Normal file
296
src/input.c
Normal file
@ -0,0 +1,296 @@
|
||||
#include "input.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stb_ds.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
|
||||
static void update_action_pressed_state(struct input_state *input, struct action *action) {
|
||||
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
|
||||
switch (action->bindings[i].source) {
|
||||
case BUTTON_SOURCE_NOT_SET:
|
||||
break;
|
||||
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
|
||||
/* not pressed */
|
||||
if (input->keyboard_state[action->bindings[i].code.scancode] == 0) {
|
||||
action->just_changed = action->is_pressed;
|
||||
action->is_pressed = false;
|
||||
}
|
||||
/* pressed */
|
||||
else {
|
||||
action->just_changed = !action->is_pressed;
|
||||
action->is_pressed = true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case BUTTON_SOURCE_MOUSE:
|
||||
/* not pressed */
|
||||
if ((input->mouse_state & action->bindings[i].code.mouse_button) == 0) {
|
||||
action->just_changed = action->is_pressed;
|
||||
action->is_pressed = false;
|
||||
}
|
||||
/* pressed */
|
||||
else {
|
||||
action->just_changed = !action->is_pressed;
|
||||
action->is_pressed = true;
|
||||
|
||||
/*
|
||||
* SDL_RenderWindowToLogical will turn window mouse
|
||||
* coords into a position inside the logical render
|
||||
* area. this has to be done to get an accurate point
|
||||
* that can actually be used in game logic
|
||||
*/
|
||||
SDL_RenderWindowToLogical(input->renderer,
|
||||
input->mouse_window_position.x,
|
||||
input->mouse_window_position.y,
|
||||
&action->position.x,
|
||||
&action->position.y);
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void input_bind_code_to_action(struct input_state *input,
|
||||
char *action_name,
|
||||
enum button_source source,
|
||||
union button_code code)
|
||||
{
|
||||
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
|
||||
if (action_item == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return;
|
||||
}
|
||||
struct action *action = &action_item->value;
|
||||
|
||||
/* check every binding to make sure this code isn't already bound */
|
||||
for (size_t i = 0; i < SDL_arraysize(action->bindings); ++i) {
|
||||
struct button *binding = &action->bindings[i];
|
||||
|
||||
if (binding->source != source)
|
||||
break;
|
||||
|
||||
bool is_already_bound = false;
|
||||
switch (binding->source) {
|
||||
case BUTTON_SOURCE_NOT_SET:
|
||||
break;
|
||||
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
|
||||
is_already_bound = binding->code.scancode == code.scancode;
|
||||
break;
|
||||
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
|
||||
is_already_bound = binding->code.keycode == code.keycode;
|
||||
break;
|
||||
case BUTTON_SOURCE_GAMEPAD:
|
||||
is_already_bound = binding->code.gamepad_button == code.gamepad_button;
|
||||
break;
|
||||
case BUTTON_SOURCE_MOUSE:
|
||||
is_already_bound = binding->code.mouse_button == code.mouse_button;
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_already_bound) {
|
||||
log_warn("(%s) Code already bound to action \"%s\".", __func__, action_name);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* if we're at max bindings, forget the first element and shift the rest */
|
||||
if (action->num_bindings == SDL_arraysize(action->bindings)) {
|
||||
--action->num_bindings;
|
||||
size_t shifted_size = (sizeof action->bindings) - (sizeof action->bindings[0]);
|
||||
memmove(action->bindings, action->bindings + 1, shifted_size);
|
||||
}
|
||||
|
||||
action->bindings[action->num_bindings++] = (struct button) {
|
||||
.source = source,
|
||||
.code = code,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static void input_unbind_code_from_action(struct input_state *input,
|
||||
char *action_name,
|
||||
enum button_source source,
|
||||
union button_code code)
|
||||
{
|
||||
struct action_hash_item *action_item = shgetp_null(input->action_hash, action_name);
|
||||
if (action_item == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return;
|
||||
}
|
||||
struct action *action = &action_item->value;
|
||||
|
||||
/* check every binding to make sure this code is bound */
|
||||
size_t index = 0;
|
||||
bool is_bound = false;
|
||||
for (index = 0; index < SDL_arraysize(action->bindings); ++index) {
|
||||
struct button *binding = &action->bindings[index];
|
||||
|
||||
if (binding->source != source)
|
||||
continue;
|
||||
|
||||
switch (binding->source) {
|
||||
case BUTTON_SOURCE_NOT_SET:
|
||||
break;
|
||||
case BUTTON_SOURCE_KEYBOARD_PHYSICAL:
|
||||
is_bound = binding->code.scancode == code.scancode;
|
||||
break;
|
||||
case BUTTON_SOURCE_KEYBOARD_CHARACTER:
|
||||
is_bound = binding->code.keycode == code.keycode;
|
||||
break;
|
||||
case BUTTON_SOURCE_GAMEPAD:
|
||||
is_bound = binding->code.gamepad_button == code.gamepad_button;
|
||||
break;
|
||||
case BUTTON_SOURCE_MOUSE:
|
||||
is_bound = binding->code.mouse_button == code.mouse_button;
|
||||
break;
|
||||
}
|
||||
|
||||
if (is_bound)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!is_bound) {
|
||||
log_warn("(%s) Code is not bound to action \"%s\".", __func__, action_name);
|
||||
return;
|
||||
}
|
||||
|
||||
/* remove the element to unbind and shift the rest so there isn't a gap */
|
||||
if (action->num_bindings == SDL_arraysize(action->bindings)) {
|
||||
size_t elements_after_index = action->num_bindings - index;
|
||||
size_t shifted_size = elements_after_index * (sizeof action->bindings[0]);
|
||||
memmove(action->bindings + index, action->bindings + index + 1, shifted_size);
|
||||
--action->num_bindings;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void input_state_init(struct input_state *input, SDL_Renderer *renderer) {
|
||||
sh_new_strdup(input->action_hash);
|
||||
input->renderer = renderer;
|
||||
}
|
||||
|
||||
|
||||
void input_state_deinit(struct input_state *input) {
|
||||
shfree(input->action_hash);
|
||||
}
|
||||
|
||||
|
||||
void input_state_update(struct input_state *input) {
|
||||
input->keyboard_state = SDL_GetKeyboardState(NULL);
|
||||
input->mouse_state = SDL_GetMouseState(&input->mouse_window_position.x,
|
||||
&input->mouse_window_position.y);
|
||||
|
||||
for (size_t i = 0; i < shlenu(input->action_hash); ++i) {
|
||||
struct action *action = &input->action_hash[i].value;
|
||||
update_action_pressed_state(input, action);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void input_bind_action_scancode(struct input_state *input,
|
||||
char *action_name,
|
||||
SDL_Scancode scancode)
|
||||
{
|
||||
input_bind_code_to_action(input,
|
||||
action_name,
|
||||
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
|
||||
(union button_code) { .scancode = scancode });
|
||||
}
|
||||
|
||||
|
||||
void input_unbind_action_scancode(struct input_state *input,
|
||||
char *action_name,
|
||||
SDL_Scancode scancode)
|
||||
{
|
||||
input_unbind_code_from_action(input,
|
||||
action_name,
|
||||
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
|
||||
(union button_code) { .scancode = scancode });
|
||||
}
|
||||
|
||||
|
||||
void input_bind_action_mouse(struct input_state *input,
|
||||
char *action_name,
|
||||
uint8_t mouse_button)
|
||||
{
|
||||
input_bind_code_to_action(input,
|
||||
action_name,
|
||||
BUTTON_SOURCE_MOUSE,
|
||||
(union button_code) { .mouse_button = mouse_button});
|
||||
}
|
||||
|
||||
|
||||
void input_unbind_action_mouse(struct input_state *input,
|
||||
char *action_name,
|
||||
uint8_t mouse_button)
|
||||
{
|
||||
input_unbind_code_from_action(input,
|
||||
action_name,
|
||||
BUTTON_SOURCE_MOUSE,
|
||||
(union button_code) { .mouse_button = mouse_button});
|
||||
}
|
||||
|
||||
|
||||
void input_add_action(struct input_state *input, char *action_name) {
|
||||
if (shgeti(input->action_hash, action_name) >= 0) {
|
||||
log_warn("(%s) Action \"%s\" is already registered.", __func__, action_name);
|
||||
return;
|
||||
}
|
||||
|
||||
shput(input->action_hash, action_name, (struct action) { 0 });
|
||||
}
|
||||
|
||||
|
||||
void input_delete_action(struct input_state *input, char *action_name) {
|
||||
if (shdel(input->action_hash, action_name) == 0)
|
||||
log_warn("(%s) Action \"%s\" is not registered.", __func__, action_name);
|
||||
}
|
||||
|
||||
|
||||
bool input_is_action_pressed(struct input_state *input, char *action_name) {
|
||||
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
|
||||
if (action == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return false;
|
||||
}
|
||||
return action->value.is_pressed;
|
||||
}
|
||||
|
||||
|
||||
bool input_is_action_just_pressed(struct input_state *input, char *action_name) {
|
||||
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
|
||||
if (action == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return false;
|
||||
}
|
||||
return action->value.is_pressed && action->value.just_changed;
|
||||
}
|
||||
|
||||
|
||||
bool input_is_action_just_released(struct input_state *input, char *action_name) {
|
||||
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
|
||||
if (action == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return false;
|
||||
}
|
||||
return !action->value.is_pressed && action->value.just_changed;
|
||||
}
|
||||
|
||||
|
||||
t_fvec2 input_get_action_position(struct input_state *input, char *action_name) {
|
||||
struct action_hash_item *action = shgetp_null(input->action_hash, action_name);
|
||||
if (action == NULL) {
|
||||
log_warn("(%s) Action \"%s\" does not exist.", __func__, action_name);
|
||||
return (t_fvec2) { 0 };
|
||||
}
|
||||
|
||||
return action->value.position;
|
||||
}
|
92
src/input.h
Normal file
92
src/input.h
Normal file
@ -0,0 +1,92 @@
|
||||
#ifndef INPUT_H
|
||||
#define INPUT_H
|
||||
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
enum button_source {
|
||||
BUTTON_SOURCE_NOT_SET,
|
||||
BUTTON_SOURCE_KEYBOARD_PHYSICAL,
|
||||
BUTTON_SOURCE_KEYBOARD_CHARACTER,
|
||||
BUTTON_SOURCE_GAMEPAD,
|
||||
BUTTON_SOURCE_MOUSE,
|
||||
};
|
||||
|
||||
|
||||
union button_code {
|
||||
SDL_Scancode scancode;
|
||||
SDL_Keycode keycode;
|
||||
SDL_GameControllerButton gamepad_button;
|
||||
uint8_t mouse_button; /* SDL_BUTTON_ enum */
|
||||
};
|
||||
|
||||
|
||||
/* an input to which an action is bound */
|
||||
/* it is not limited to literal buttons */
|
||||
struct button {
|
||||
enum button_source source;
|
||||
union button_code code;
|
||||
};
|
||||
|
||||
|
||||
/* represents the collective state of a group of buttons */
|
||||
/* that is, changes in the states of any of the bound buttons will affect it */
|
||||
struct action {
|
||||
size_t num_bindings;
|
||||
struct button bindings[NUM_KEYBIND_SLOTS];
|
||||
t_fvec2 position; /* set if applicable */
|
||||
bool is_pressed;
|
||||
bool just_changed;
|
||||
};
|
||||
|
||||
|
||||
struct action_hash_item {
|
||||
char *key;
|
||||
struct action value;
|
||||
};
|
||||
|
||||
|
||||
struct input_state {
|
||||
struct action_hash_item *action_hash;
|
||||
SDL_Renderer *renderer; /* some input relates to the screen in some way */
|
||||
const uint8_t *keyboard_state; /* array of booleans indexed by scancode */
|
||||
uint32_t mouse_state; /* SDL mouse button bitmask */
|
||||
t_vec2 mouse_window_position;
|
||||
enum button_source last_active_source;
|
||||
bool is_anything_just_pressed;
|
||||
};
|
||||
|
||||
|
||||
void input_state_init(struct input_state *input, SDL_Renderer *renderer);
|
||||
void input_state_deinit(struct input_state *input);
|
||||
void input_state_update(struct input_state *input);
|
||||
|
||||
void input_bind_action_scancode(struct input_state *input,
|
||||
char *action_name,
|
||||
SDL_Scancode scancode);
|
||||
void input_unbind_action_scancode(struct input_state *input,
|
||||
char *action_name,
|
||||
SDL_Scancode scancode);
|
||||
void input_bind_action_mouse(struct input_state *input,
|
||||
char *action_name,
|
||||
uint8_t mouse_button);
|
||||
void input_unbind_action_mouse(struct input_state *input,
|
||||
char *action_name,
|
||||
uint8_t mouse_button);
|
||||
|
||||
void input_add_action(struct input_state *input, char *action_name);
|
||||
void input_delete_action(struct input_state *input, char *action_name);
|
||||
|
||||
bool input_is_action_pressed(struct input_state *input, char *action_name);
|
||||
bool input_is_action_just_pressed(struct input_state *input, char *action_name);
|
||||
bool input_is_action_just_released(struct input_state *input, char *action_name);
|
||||
t_fvec2 input_get_action_position(struct input_state *input, char *action_name);
|
||||
|
||||
|
||||
#endif
|
306
src/main.c
Normal file
306
src/main.c
Normal file
@ -0,0 +1,306 @@
|
||||
#include "context.h"
|
||||
#include "rendering.h"
|
||||
#include "input.h"
|
||||
#include "util.h"
|
||||
#include "textures.h"
|
||||
#include "game/game.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_image.h>
|
||||
#include <SDL2/SDL_ttf.h>
|
||||
#include <physfs.h>
|
||||
#include <stb_ds.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdio.h>
|
||||
#include <inttypes.h>
|
||||
#include <tgmath.h>
|
||||
#include <limits.h>
|
||||
#include <math.h>
|
||||
|
||||
|
||||
static void poll_events(void) {
|
||||
SDL_Event e;
|
||||
|
||||
while (SDL_PollEvent(&e)) {
|
||||
switch (e.type) {
|
||||
case SDL_QUIT:
|
||||
ctx.is_running = false;
|
||||
break;
|
||||
|
||||
case SDL_WINDOWEVENT:
|
||||
if (e.window.windowID != ctx.window_id)
|
||||
break;
|
||||
|
||||
switch (e.window.event) {
|
||||
case SDL_WINDOWEVENT_RESIZED:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void main_loop(void) {
|
||||
/*
|
||||
if (!ctx.is_running) {
|
||||
end(ctx);
|
||||
clean_up(ctx);
|
||||
emscripten_cancel_main_loop();
|
||||
}
|
||||
*/
|
||||
|
||||
/* frame timer */
|
||||
int64_t current_frame_time = SDL_GetPerformanceCounter();
|
||||
int64_t delta_time = current_frame_time - ctx.prev_frame_time;
|
||||
ctx.prev_frame_time = current_frame_time;
|
||||
|
||||
/* handle unexpected timer anomalies (overflow, extra slow frames, etc) */
|
||||
if (delta_time > ctx.desired_frametime * 8) { /* ignore extra-slow frames */
|
||||
delta_time = ctx.desired_frametime;
|
||||
}
|
||||
delta_time = MAX(0, delta_time);
|
||||
|
||||
/* vsync time snapping */
|
||||
/* get the refresh rate of the current display (it might not always be the same one) */
|
||||
int display_framerate = 60; /* a reasonable guess */
|
||||
SDL_DisplayMode current_display_mode;
|
||||
int current_display_index = SDL_GetWindowDisplayIndex(ctx.window);
|
||||
if (SDL_GetCurrentDisplayMode(current_display_index, ¤t_display_mode) == 0)
|
||||
display_framerate = current_display_mode.refresh_rate;
|
||||
|
||||
int64_t snap_hz = display_framerate;
|
||||
if (snap_hz <= 0)
|
||||
snap_hz = 60;
|
||||
|
||||
/* these are to snap delta time to vsync values if it's close enough */
|
||||
int64_t vsync_maxerror = (int64_t)((double)ctx.clocks_per_second * 0.0002);
|
||||
size_t snap_frequency_count = 8;
|
||||
for (size_t i = 0; i < snap_frequency_count; ++i) {
|
||||
int64_t frequency = (ctx.clocks_per_second / snap_hz) * (i+1);
|
||||
|
||||
if (llabs(delta_time - frequency) < vsync_maxerror) {
|
||||
delta_time = frequency;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* delta time averaging */
|
||||
size_t time_averager_count = SDL_arraysize(ctx.time_averager);
|
||||
|
||||
for (size_t i = 0; i < time_averager_count - 1; ++i) {
|
||||
ctx.time_averager[i] = ctx.time_averager[i+1];
|
||||
}
|
||||
ctx.time_averager[time_averager_count - 1] = delta_time;
|
||||
int64_t averager_sum = 0;
|
||||
for (size_t i = 0; i < time_averager_count; ++i) {
|
||||
averager_sum += ctx.time_averager[i];
|
||||
}
|
||||
delta_time = averager_sum / time_averager_count;
|
||||
|
||||
ctx.delta_averager_residual += averager_sum % time_averager_count;
|
||||
delta_time += ctx.delta_averager_residual / time_averager_count;
|
||||
ctx.delta_averager_residual %= time_averager_count;
|
||||
|
||||
/* add to the accumulator */
|
||||
ctx.frame_accumulator += delta_time;
|
||||
|
||||
/* spiral of death protection */
|
||||
if (ctx.frame_accumulator > ctx.desired_frametime * 8) {
|
||||
ctx.resync_flag = true;
|
||||
}
|
||||
|
||||
/* timer resync if requested */
|
||||
if (ctx.resync_flag) {
|
||||
ctx.frame_accumulator = 0;
|
||||
delta_time = ctx.desired_frametime;
|
||||
ctx.resync_flag = false;
|
||||
}
|
||||
|
||||
/* finally, let's get to work */
|
||||
poll_events();
|
||||
input_state_update(&ctx.input);
|
||||
|
||||
/* do we _always_ have to be dry? */
|
||||
if (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
|
||||
while (ctx.frame_accumulator >= ctx.desired_frametime * ctx.update_multiplicity) {
|
||||
for (size_t i = 0; i < ctx.update_multiplicity; ++i) {
|
||||
render_queue_clear();
|
||||
|
||||
game_tick();
|
||||
|
||||
ctx.frame_accumulator -= ctx.desired_frametime;
|
||||
ctx.tick_count = (ctx.tick_count % ULLONG_MAX) + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
render_queue_clear();
|
||||
}
|
||||
|
||||
/* the "step" will always run as long as there is time to spare. */
|
||||
/* without interpolation, this is preferable to fixed ticks if the additional */
|
||||
/* delta time calculations (like in physics code) are acceptable */
|
||||
game_step(delta_time);
|
||||
ctx.step_count = (ctx.step_count % ULLONG_MAX) + 1;
|
||||
|
||||
render();
|
||||
}
|
||||
|
||||
|
||||
static bool initialize(void) {
|
||||
if (SDL_Init(SDL_INIT_EVERYTHING) == -1) {
|
||||
CRY_SDL("SDL initialization failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* init got far enough to create a window */
|
||||
ctx.window = SDL_CreateWindow("emerald",
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
SDL_WINDOWPOS_CENTERED,
|
||||
RENDER_BASE_WIDTH,
|
||||
RENDER_BASE_HEIGHT,
|
||||
SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_RESIZABLE);
|
||||
if (ctx.window == NULL) {
|
||||
CRY_SDL("Window creation failed.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* might need this to have multiple windows */
|
||||
ctx.window_id = SDL_GetWindowID(ctx.window);
|
||||
|
||||
/* now that we have a window, we know a renderer can be created */
|
||||
ctx.renderer = SDL_CreateRenderer(ctx.window, -1, SDL_RENDERER_PRESENTVSYNC);
|
||||
|
||||
/* SDL_SetHint(SDL_HINT_RENDER_LOGICAL_SIZE_MODE, "overscan"); */
|
||||
/* SDL_RenderSetIntegerScale(ctx.renderer, SDL_TRUE); */
|
||||
SDL_RenderSetLogicalSize(ctx.renderer, RENDER_BASE_WIDTH, RENDER_BASE_HEIGHT);
|
||||
SDL_GetRendererOutputSize(ctx.renderer, &ctx.window_w, &ctx.window_h);
|
||||
|
||||
/* images */
|
||||
if (IMG_Init(IMG_INIT_PNG) == 0) {
|
||||
CRY_SDL("SDL_image initialization failed.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* filesystem time */
|
||||
/* TODO: ANDROID: see the warning in physicsfs PHYSFS_init docs/header */
|
||||
if (!PHYSFS_init(ctx.argv[0]) ||
|
||||
!PHYSFS_setSaneConfig(ORGANIZATION_NAME, APP_NAME, PACKAGE_EXTENSION, false, true))
|
||||
{
|
||||
CRY_PHYSFS("Filesystem initialization failed.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* you could change this at runtime if you wanted */
|
||||
ctx.update_multiplicity = 1;
|
||||
|
||||
/* debug mode _defaults_ to being enabled on debug builds. */
|
||||
/* you should be able to enable it at runtime on any build */
|
||||
#ifndef NDEBUG
|
||||
ctx.debug = true;
|
||||
#else
|
||||
ctx.debug = false;
|
||||
#endif
|
||||
|
||||
/* random seeding */
|
||||
/* SDL_GetPerformanceCounter returns some platform-dependent number. */
|
||||
/* it should vary between game instances. i checked! random enough for me. */
|
||||
ctx.random_seed = SDL_GetPerformanceCounter();
|
||||
srand((unsigned int)ctx.random_seed);
|
||||
stbds_rand_seed(ctx.random_seed);
|
||||
|
||||
/* main loop machinery */
|
||||
ctx.is_running = true;
|
||||
ctx.resync_flag = true;
|
||||
ctx.clocks_per_second = SDL_GetPerformanceFrequency();
|
||||
ctx.prev_frame_time = SDL_GetPerformanceCounter();
|
||||
ctx.desired_frametime = ctx.clocks_per_second / TICKS_PER_SECOND;
|
||||
ctx.frame_accumulator = 0;
|
||||
ctx.tick_count = 0;
|
||||
ctx.step_count = 0;
|
||||
|
||||
/* delta time averaging */
|
||||
ctx.delta_averager_residual = 0;
|
||||
for (size_t i = 0; i < SDL_arraysize(ctx.time_averager); ++i) {
|
||||
ctx.time_averager[i] = ctx.desired_frametime;
|
||||
}
|
||||
|
||||
/* rendering */
|
||||
/* these are dynamic arrays and will be allocated lazily by stb_ds */
|
||||
ctx.render_queue_sprites = NULL;
|
||||
ctx.render_queue_rectangles = NULL;
|
||||
ctx.render_queue_circles = NULL;
|
||||
ctx.circle_radius_hash = NULL;
|
||||
|
||||
textures_cache_init(&ctx.texture_cache, ctx.window);
|
||||
if (TTF_Init() < 0) {
|
||||
CRY_SDL("SDL_ttf initialization failed.");
|
||||
goto fail;
|
||||
}
|
||||
|
||||
/* input */
|
||||
input_state_init(&ctx.input, ctx.renderer);
|
||||
|
||||
/* scripting */
|
||||
/*
|
||||
if (!scripting_init(ctx)) {
|
||||
goto fail;
|
||||
}
|
||||
*/
|
||||
|
||||
return true;
|
||||
|
||||
fail:
|
||||
SDL_Quit();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/* will not be called on an abnormal exit */
|
||||
static void clean_up(void) {
|
||||
/*
|
||||
scripting_deinit(ctx);
|
||||
*/
|
||||
|
||||
input_state_deinit(&ctx.input);
|
||||
|
||||
arrfree(ctx.render_queue_sprites);
|
||||
arrfree(ctx.render_queue_rectangles);
|
||||
textures_cache_deinit(&ctx.texture_cache);
|
||||
|
||||
PHYSFS_deinit();
|
||||
TTF_Quit();
|
||||
IMG_Quit();
|
||||
SDL_Quit();
|
||||
}
|
||||
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
ctx.argc = argc;
|
||||
ctx.argv = argv;
|
||||
|
||||
|
||||
if (!initialize())
|
||||
return EXIT_FAILURE;
|
||||
|
||||
for (int i = 1; i < (argc - 1); ++i) {
|
||||
if (strcmp(argv[i], "--data-dir") == 0) {
|
||||
if (!PHYSFS_mount(argv[i+1], NULL, true)) {
|
||||
CRY_PHYSFS("Data dir mount override failed.");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.was_successful = true;
|
||||
while (ctx.is_running)
|
||||
main_loop();
|
||||
|
||||
game_end();
|
||||
clean_up();
|
||||
|
||||
return ctx.was_successful ? EXIT_SUCCESS : EXIT_FAILURE;
|
||||
}
|
34
src/private/rendering.h
Normal file
34
src/private/rendering.h
Normal file
@ -0,0 +1,34 @@
|
||||
#ifndef PRIVATE_RENDERING_H
|
||||
#define PRIVATE_RENDERING_H
|
||||
|
||||
#include "../rendering.h"
|
||||
#include "../util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
struct sprite_primitive {
|
||||
t_frect rect;
|
||||
t_color color;
|
||||
double rotation;
|
||||
char *path;
|
||||
SDL_BlendMode blend_mode;
|
||||
int atlas_index;
|
||||
int layer;
|
||||
bool flip_x;
|
||||
bool flip_y;
|
||||
};
|
||||
|
||||
struct rect_primitive {
|
||||
t_frect rect;
|
||||
t_color color;
|
||||
};
|
||||
|
||||
struct circle_primitive {
|
||||
float radius;
|
||||
t_color color;
|
||||
t_fvec2 position;
|
||||
};
|
||||
|
||||
#endif
|
367
src/rendering.c
Normal file
367
src/rendering.c
Normal file
@ -0,0 +1,367 @@
|
||||
#include "private/rendering.h"
|
||||
#include "context.h"
|
||||
#include "config.h"
|
||||
#include "textures.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stb_ds.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <tgmath.h>
|
||||
|
||||
|
||||
void render_queue_clear(void) {
|
||||
/* since i don't intend to free the queues, */
|
||||
/* it's faster and simpler to just "start over" */
|
||||
/* and start overwriting the existing data */
|
||||
arrsetlen(ctx.render_queue_sprites, 0);
|
||||
arrsetlen(ctx.render_queue_rectangles, 0);
|
||||
arrsetlen(ctx.render_queue_circles, 0);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* an implementation note:
|
||||
* try to avoid doing expensive work in the push functions,
|
||||
* because they will be called multiple times in the main loop
|
||||
* before anything is really rendered
|
||||
*/
|
||||
|
||||
/* sprite */
|
||||
void push_sprite(char *path, t_frect rect) {
|
||||
textures_load(&ctx.texture_cache, path);
|
||||
|
||||
struct sprite_primitive sprite = {
|
||||
.rect = rect,
|
||||
.color = (t_color) { 255, 255, 255, 255 },
|
||||
.path = path,
|
||||
.rotation = 0.0,
|
||||
.blend_mode = SDL_BLENDMODE_BLEND,
|
||||
.atlas_index =
|
||||
textures_get_atlas_index(&ctx.texture_cache, path),
|
||||
.layer = 0,
|
||||
.flip_x = false,
|
||||
.flip_y = false,
|
||||
};
|
||||
|
||||
arrput(ctx.render_queue_sprites, sprite);
|
||||
}
|
||||
|
||||
|
||||
void push_sprite_ex(t_frect rect, t_push_sprite_args args) {
|
||||
textures_load(&ctx.texture_cache, args.path);
|
||||
|
||||
struct sprite_primitive sprite = {
|
||||
.rect = rect,
|
||||
.color = args.color,
|
||||
.path = args.path,
|
||||
.rotation = args.rotation,
|
||||
.blend_mode = args.blend_mode,
|
||||
.atlas_index =
|
||||
textures_get_atlas_index(&ctx.texture_cache, args.path),
|
||||
.layer = args.layer,
|
||||
.flip_x = args.flip_x,
|
||||
.flip_y = args.flip_y,
|
||||
};
|
||||
|
||||
arrput(ctx.render_queue_sprites, sprite);
|
||||
}
|
||||
|
||||
|
||||
/* rectangle */
|
||||
void push_rectangle(t_frect rect, t_color color) {
|
||||
struct rect_primitive rectangle = {
|
||||
.rect = rect,
|
||||
.color = color,
|
||||
};
|
||||
|
||||
arrput(ctx.render_queue_rectangles, rectangle);
|
||||
}
|
||||
|
||||
|
||||
/* circle */
|
||||
void push_circle(t_fvec2 position, float radius, t_color color) {
|
||||
struct circle_primitive circle = {
|
||||
.radius = radius,
|
||||
.color = color,
|
||||
.position = position,
|
||||
};
|
||||
|
||||
arrput(ctx.render_queue_circles, circle);
|
||||
}
|
||||
|
||||
|
||||
/* rendering */
|
||||
static void render_background(void) {
|
||||
SDL_SetRenderDrawColor(ctx.renderer, 230, 230, 230, 255);
|
||||
SDL_Rect rect = {
|
||||
0,
|
||||
0,
|
||||
ctx.window_w,
|
||||
ctx.window_h
|
||||
};
|
||||
SDL_RenderFillRect(ctx.renderer, &rect);
|
||||
|
||||
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
|
||||
}
|
||||
|
||||
|
||||
/* compare functions for the sort in render_sprites */
|
||||
static int cmp_atlases(const void *a, const void *b) {
|
||||
int index_a = ((const struct sprite_primitive *)a)->atlas_index;
|
||||
int index_b = ((const struct sprite_primitive *)b)->atlas_index;
|
||||
|
||||
return (index_a > index_b) - (index_a < index_b);
|
||||
}
|
||||
|
||||
|
||||
static int cmp_blend_modes(const void *a, const void *b) {
|
||||
SDL_BlendMode mode_a = ((const struct sprite_primitive *)a)->blend_mode;
|
||||
SDL_BlendMode mode_b = ((const struct sprite_primitive *)b)->blend_mode;
|
||||
|
||||
return (mode_a > mode_b) - (mode_a < mode_b);
|
||||
}
|
||||
|
||||
|
||||
static int cmp_colors(const void *a, const void *b) {
|
||||
t_color color_a = ((const struct sprite_primitive *)a)->color;
|
||||
t_color color_b = ((const struct sprite_primitive *)b)->color;
|
||||
|
||||
/* check reds */
|
||||
if (color_a.r < color_b.r)
|
||||
return -1;
|
||||
else if (color_a.r > color_b.r)
|
||||
return 1;
|
||||
|
||||
/* reds were equal, check greens */
|
||||
else if (color_a.g < color_b.g)
|
||||
return -1;
|
||||
else if (color_a.g > color_b.g)
|
||||
return 1;
|
||||
|
||||
/* greens were equal, check blues */
|
||||
else if (color_a.b < color_b.b)
|
||||
return -1;
|
||||
else if (color_a.b > color_b.b)
|
||||
return 1;
|
||||
|
||||
/* blues were equal, check alphas */
|
||||
else if (color_a.a < color_b.a)
|
||||
return -1;
|
||||
else if (color_a.a > color_b.a)
|
||||
return 1;
|
||||
|
||||
/* entirely equal */
|
||||
else
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
static int cmp_layers(const void *a, const void *b) {
|
||||
int layer_a = ((const struct sprite_primitive *)a)->layer;
|
||||
int layer_b = ((const struct sprite_primitive *)b)->layer;
|
||||
|
||||
return (layer_a > layer_b) - (layer_a < layer_b);
|
||||
}
|
||||
|
||||
|
||||
/* necessary to allow the renderer to draw in batches in the best case */
|
||||
static void sort_sprites(struct sprite_primitive *sprites) {
|
||||
size_t sprites_len = arrlenu(sprites);
|
||||
qsort(sprites, sprites_len, sizeof *sprites, cmp_atlases);
|
||||
qsort(sprites, sprites_len, sizeof *sprites, cmp_blend_modes);
|
||||
qsort(sprites, sprites_len, sizeof *sprites, cmp_colors);
|
||||
qsort(sprites, sprites_len, sizeof *sprites, cmp_layers);
|
||||
}
|
||||
|
||||
|
||||
static void render_sprite(struct sprite_primitive *sprite) {
|
||||
SDL_Rect srcrect_value = { 0 };
|
||||
SDL_Rect *srcrect = &srcrect_value;
|
||||
SDL_Texture *texture = NULL;
|
||||
|
||||
/* loner */
|
||||
if (sprite->atlas_index == -1) {
|
||||
srcrect = NULL;
|
||||
texture = textures_get_loner(&ctx.texture_cache, sprite->path);
|
||||
} else {
|
||||
*srcrect = textures_get_srcrect(&ctx.texture_cache, sprite->path);
|
||||
texture = textures_get_atlas(&ctx.texture_cache, sprite->atlas_index);
|
||||
}
|
||||
|
||||
SDL_RendererFlip flip = SDL_FLIP_NONE;
|
||||
if (sprite->flip_x)
|
||||
flip |= SDL_FLIP_HORIZONTAL;
|
||||
if (sprite->flip_y)
|
||||
flip |= SDL_FLIP_VERTICAL;
|
||||
|
||||
SDL_SetTextureColorMod(texture,
|
||||
sprite->color.r,
|
||||
sprite->color.g,
|
||||
sprite->color.b);
|
||||
SDL_SetTextureAlphaMod(texture, sprite->color.a);
|
||||
SDL_SetTextureBlendMode(texture, sprite->blend_mode);
|
||||
|
||||
SDL_Rect rect = {
|
||||
(int)sprite->rect.x,
|
||||
(int)sprite->rect.y,
|
||||
(int)sprite->rect.w,
|
||||
(int)sprite->rect.h
|
||||
};
|
||||
|
||||
SDL_RenderCopyEx(ctx.renderer,
|
||||
texture,
|
||||
srcrect,
|
||||
&rect,
|
||||
sprite->rotation,
|
||||
NULL,
|
||||
flip);
|
||||
}
|
||||
|
||||
|
||||
static void render_rectangle(struct rect_primitive *rectangle) {
|
||||
SDL_SetRenderDrawColor(ctx.renderer,
|
||||
rectangle->color.r,
|
||||
rectangle->color.g,
|
||||
rectangle->color.b,
|
||||
rectangle->color.a);
|
||||
|
||||
SDL_Rect rect = {
|
||||
(int)rectangle->rect.x,
|
||||
(int)rectangle->rect.y,
|
||||
(int)rectangle->rect.w,
|
||||
(int)rectangle->rect.h
|
||||
};
|
||||
|
||||
SDL_RenderFillRect(ctx.renderer, &rect);
|
||||
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
|
||||
}
|
||||
|
||||
|
||||
/* vertices_out and indices_out MUST BE FREED */
|
||||
static void create_circle_geometry(t_fvec2 position,
|
||||
t_color color,
|
||||
float radius,
|
||||
size_t num_vertices,
|
||||
SDL_Vertex **vertices_out,
|
||||
int **indices_out)
|
||||
{
|
||||
SDL_Vertex *vertices = cmalloc(sizeof *vertices * (num_vertices + 1));
|
||||
int *indices = cmalloc(sizeof *indices * (num_vertices * 3));
|
||||
|
||||
/* the angle (in radians) to rotate by on each iteration */
|
||||
float seg_rotation_angle = (360.0f / (float)num_vertices) * ((float)M_PI / 180);
|
||||
|
||||
vertices[0].position.x = (float)position.x;
|
||||
vertices[0].position.y = (float)position.y;
|
||||
vertices[0].color.r = color.r;
|
||||
vertices[0].color.g = color.g;
|
||||
vertices[0].color.b = color.b;
|
||||
vertices[0].color.a = color.a;
|
||||
vertices[0].tex_coord = (SDL_FPoint){ 0, 0 };
|
||||
|
||||
/* this point will rotate around the center */
|
||||
float start_x = 0.0f - radius;
|
||||
float start_y = 0.0f;
|
||||
|
||||
for (size_t i = 1; i < num_vertices + 1; ++i) {
|
||||
float final_seg_rotation_angle = (float)i * seg_rotation_angle;
|
||||
|
||||
vertices[i].position.x =
|
||||
cos(final_seg_rotation_angle) * start_x -
|
||||
sin(final_seg_rotation_angle) * start_y;
|
||||
vertices[i].position.y =
|
||||
cos(final_seg_rotation_angle) * start_y +
|
||||
sin(final_seg_rotation_angle) * start_x;
|
||||
|
||||
vertices[i].position.x += position.x;
|
||||
vertices[i].position.y += position.y;
|
||||
|
||||
vertices[i].color.r = color.r;
|
||||
vertices[i].color.g = color.g;
|
||||
vertices[i].color.b = color.b;
|
||||
vertices[i].color.a = color.a;
|
||||
|
||||
vertices[i].tex_coord = (SDL_FPoint){ 0, 0 };
|
||||
|
||||
|
||||
size_t triangle_offset = 3 * (i - 1);
|
||||
|
||||
/* center point index */
|
||||
indices[triangle_offset] = 0;
|
||||
/* generated point index */
|
||||
indices[triangle_offset + 1] = (int)i;
|
||||
|
||||
size_t index = (i + 1) % num_vertices;
|
||||
if (index == 0)
|
||||
index = num_vertices;
|
||||
indices[triangle_offset + 2] = (int)index;
|
||||
}
|
||||
|
||||
*vertices_out = vertices;
|
||||
*indices_out = indices;
|
||||
}
|
||||
|
||||
|
||||
static void render_circle(struct circle_primitive *circle) {
|
||||
SDL_Vertex *vertices = NULL;
|
||||
int *indices = NULL;
|
||||
int num_vertices = (int)circle->radius;
|
||||
|
||||
create_circle_geometry(circle->position,
|
||||
circle->color,
|
||||
circle->radius,
|
||||
num_vertices,
|
||||
&vertices,
|
||||
&indices);
|
||||
|
||||
SDL_RenderGeometry(ctx.renderer, NULL,
|
||||
vertices,
|
||||
num_vertices + 1, /* vertices + center vertex */
|
||||
indices,
|
||||
num_vertices * 3);
|
||||
|
||||
free(vertices);
|
||||
free(indices);
|
||||
}
|
||||
|
||||
|
||||
static void render_sprites(void) {
|
||||
if (ctx.texture_cache.is_dirty)
|
||||
textures_update_current_atlas(&ctx.texture_cache);
|
||||
|
||||
sort_sprites(ctx.render_queue_sprites);
|
||||
|
||||
for (size_t i = 0; i < arrlenu(ctx.render_queue_sprites); ++i) {
|
||||
render_sprite(&ctx.render_queue_sprites[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void render_rectangles(void) {
|
||||
for (size_t i = 0; i < arrlenu(ctx.render_queue_rectangles); ++i) {
|
||||
render_rectangle(&ctx.render_queue_rectangles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void render_circles(void) {
|
||||
for (size_t i = 0; i < arrlenu(ctx.render_queue_circles); ++i) {
|
||||
render_circle(&ctx.render_queue_circles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void render(void) {
|
||||
SDL_SetRenderDrawBlendMode(ctx.renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(ctx.renderer, 0, 0, 0, 255);
|
||||
SDL_RenderClear(ctx.renderer);
|
||||
SDL_SetRenderDrawColor(ctx.renderer, 255, 255, 255, 255);
|
||||
|
||||
render_background();
|
||||
render_sprites();
|
||||
render_rectangles();
|
||||
render_circles();
|
||||
|
||||
SDL_RenderPresent(ctx.renderer);
|
||||
}
|
41
src/rendering.h
Normal file
41
src/rendering.h
Normal file
@ -0,0 +1,41 @@
|
||||
#ifndef RENDERING_H
|
||||
#define RENDERING_H
|
||||
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct push_sprite_args {
|
||||
char *path;
|
||||
int layer;
|
||||
t_color color;
|
||||
double rotation;
|
||||
SDL_BlendMode blend_mode;
|
||||
bool flip_x;
|
||||
bool flip_y;
|
||||
} t_push_sprite_args;
|
||||
|
||||
/* clears all render queues */
|
||||
void render_queue_clear(void);
|
||||
|
||||
/* pushes a sprite onto the sprite render queue */
|
||||
/* this is a simplified version of push_sprite_ex for the most common case. */
|
||||
/* it assumes you want no color modulation, no rotation, no flip, layer 0, and alpha blending */
|
||||
void push_sprite(char *path, t_frect rect);
|
||||
|
||||
/* pushes a sprite onto the sprite render queue */
|
||||
/* note that if args is zeroed out, the color will be transparent black */
|
||||
void push_sprite_ex(t_frect rect, t_push_sprite_args args);
|
||||
|
||||
/* pushes a filled rectangle onto the rectangle render queue */
|
||||
void push_rectangle(t_frect rect, t_color color);
|
||||
|
||||
/* pushes a filled circle onto the circle render queue */
|
||||
void push_circle(t_fvec2 position, float radius, t_color color);
|
||||
|
||||
/* renders the background, then the primitives in all render queues */
|
||||
void render(void);
|
||||
|
||||
#endif
|
139
src/scripting.c
Normal file
139
src/scripting.c
Normal file
@ -0,0 +1,139 @@
|
||||
#include "scripting.h"
|
||||
#include "util.h"
|
||||
#include "context.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <umka_api.h>
|
||||
#include <physfs.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
|
||||
static void msgbox(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_INFORMATION,
|
||||
params[1].ptrVal, params[0].ptrVal, NULL);
|
||||
}
|
||||
|
||||
static void umka_log_info(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
log_info(params[0].ptrVal);
|
||||
}
|
||||
|
||||
static void umka_log_critical(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
log_critical(params[0].ptrVal);
|
||||
}
|
||||
|
||||
static void umka_log_warn(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
log_warn(params[0].ptrVal);
|
||||
}
|
||||
|
||||
static void is_action_pressed(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
struct state *state = params[1].ptrVal;
|
||||
t_ctx *ctx = state->hidden_ptr;
|
||||
|
||||
bool value = input_is_action_pressed(&ctx->input, params[0].ptrVal);
|
||||
result->uintVal = value;
|
||||
}
|
||||
|
||||
static void is_action_just_pressed(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
struct state *state = params[1].ptrVal;
|
||||
t_ctx *ctx = state->hidden_ptr;
|
||||
|
||||
bool value = input_is_action_just_pressed(&ctx->input, params[0].ptrVal);
|
||||
result->uintVal = value;
|
||||
}
|
||||
|
||||
static void is_action_just_released(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
struct state *state = params[1].ptrVal;
|
||||
t_ctx *ctx = state->hidden_ptr;
|
||||
|
||||
bool value = input_is_action_just_released(&ctx->input, params[0].ptrVal);
|
||||
result->uintVal = value;
|
||||
}
|
||||
|
||||
static void get_action_position(UmkaStackSlot *params, UmkaStackSlot *result) {
|
||||
struct state *state = params[2].ptrVal;
|
||||
t_ctx *ctx = state->hidden_ptr;
|
||||
|
||||
t_fvec2 *position = params[0].ptrVal;
|
||||
*position = input_get_action_position(&ctx->input, params[1].ptrVal);
|
||||
|
||||
// the result is in a hidden result pointer allocated by Umka
|
||||
result->ptrVal = params[0].ptrVal;
|
||||
}
|
||||
|
||||
static void register_api(void *umka) {
|
||||
umkaAddFunc(umka, "msgbox", msgbox);
|
||||
umkaAddFunc(umka, "logInfo", umka_log_info);
|
||||
umkaAddFunc(umka, "logCritical", umka_log_critical);
|
||||
umkaAddFunc(umka, "logWarn", umka_log_warn);
|
||||
umkaAddFunc(umka, "cImplIsActionPressed", is_action_pressed);
|
||||
umkaAddFunc(umka, "cImplIsActionJustPressed", is_action_just_pressed);
|
||||
umkaAddFunc(umka, "cImplIsActionJustReleased", is_action_just_released);
|
||||
umkaAddFunc(umka, "getActionPosition", get_action_position);
|
||||
}
|
||||
|
||||
bool scripting_init(t_ctx *ctx) {
|
||||
if (!PHYSFS_exists("/scripts/main.um")) {
|
||||
CRY("Failed to initialize scripting", "Could not find a main.um (we need it)");
|
||||
return false;
|
||||
}
|
||||
|
||||
ctx->umka = umkaAlloc();
|
||||
|
||||
char *main_script = file_to_str("/scripts/main.um");
|
||||
bool umka_ok = umkaInit(ctx->umka,
|
||||
"main.um",
|
||||
main_script,
|
||||
UMKA_STACK_SIZE,
|
||||
NULL,
|
||||
ctx->argc,
|
||||
ctx->argv,
|
||||
false,
|
||||
false,
|
||||
NULL);
|
||||
free(main_script);
|
||||
|
||||
if (!umka_ok) {
|
||||
CRY("Failed to initialize scripting", "Unknown Umka error");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* all Umka files are compiled even if they're never used */
|
||||
char **dir_file_names = PHYSFS_enumerateFiles("/scripts");
|
||||
for (char **i = dir_file_names; *i != NULL; ++i) {
|
||||
char *file_name = *i;
|
||||
|
||||
if (!strends(file_name, ".um"))
|
||||
continue;
|
||||
|
||||
/* got this one already */
|
||||
if (strcmp(file_name, "main.um") == 0)
|
||||
continue;
|
||||
|
||||
/* need to figure out the actual path (as opposed to the lone file name) */
|
||||
const char *path_prefix = "/scripts/";
|
||||
size_t path_size = snprintf(NULL, 0, "%s%s", path_prefix, file_name) + 1;
|
||||
char *path = cmalloc(path_size);
|
||||
snprintf(path, path_size, "%s%s", path_prefix, file_name);
|
||||
|
||||
char *contents = file_to_str(path);
|
||||
umkaAddModule(ctx->umka, file_name, contents);
|
||||
|
||||
free(path);
|
||||
free(contents);
|
||||
}
|
||||
PHYSFS_freeList(dir_file_names);
|
||||
|
||||
register_api(ctx->umka);
|
||||
if (!umkaCompile(ctx->umka)) {
|
||||
cry_umka(ctx->umka);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void scripting_deinit(t_ctx *ctx) {
|
||||
umkaFree(ctx->umka);
|
||||
}
|
18
src/scripting.h
Normal file
18
src/scripting.h
Normal file
@ -0,0 +1,18 @@
|
||||
#ifndef SCRIPTING_H
|
||||
#define SCRIPTING_H
|
||||
|
||||
#include <umka_api.h>
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct context t_ctx;
|
||||
|
||||
struct state {
|
||||
t_ctx *hidden_ptr;
|
||||
uint64_t tick_count;
|
||||
};
|
||||
|
||||
bool scripting_init(void);
|
||||
|
||||
#endif
|
5
src/text.c
Normal file
5
src/text.c
Normal file
@ -0,0 +1,5 @@
|
||||
#include "text.h"
|
||||
|
||||
#include "SDL_ttf.h"
|
||||
|
||||
|
29
src/text.h
Normal file
29
src/text.h
Normal file
@ -0,0 +1,29 @@
|
||||
#ifndef TEXT_H
|
||||
#define TEXT_H
|
||||
|
||||
|
||||
#include "util.h"
|
||||
|
||||
#include "SDL.h"
|
||||
#include "SDL_ttf.h"
|
||||
|
||||
|
||||
struct text {
|
||||
char *text;
|
||||
t_color color;
|
||||
int ptsize;
|
||||
};
|
||||
|
||||
|
||||
struct text_cache_item {
|
||||
char *key;
|
||||
struct text *value;
|
||||
};
|
||||
|
||||
|
||||
struct text_cache {
|
||||
struct text_cache_item *hash;
|
||||
};
|
||||
|
||||
|
||||
#endif
|
357
src/textures.c
Normal file
357
src/textures.c
Normal file
@ -0,0 +1,357 @@
|
||||
#include "textures.h"
|
||||
#include "config.h"
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <SDL2/SDL_image.h>
|
||||
#include <physfs.h>
|
||||
#include <physfsrwops.h>
|
||||
#include <stb_ds.h>
|
||||
#include <stb_rect_pack.h>
|
||||
|
||||
#include <limits.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
|
||||
|
||||
static SDL_Surface *image_to_surface(char *path) {
|
||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||
if (handle == NULL)
|
||||
goto fail;
|
||||
|
||||
SDL_Surface *result = IMG_Load_RW(handle, true);
|
||||
if (result == NULL)
|
||||
goto fail;
|
||||
|
||||
SDL_SetSurfaceBlendMode(result, SDL_BLENDMODE_NONE);
|
||||
SDL_SetSurfaceRLE(result, true);
|
||||
|
||||
return result;
|
||||
|
||||
fail:
|
||||
CRY(path, "Failed to load image. Aborting...");
|
||||
die_abruptly();
|
||||
}
|
||||
|
||||
|
||||
/* adds a new, blank atlas surface to the cache */
|
||||
static void add_new_atlas(struct texture_cache *cache) {
|
||||
SDL_PixelFormat *native_format =
|
||||
SDL_AllocFormat(SDL_GetWindowPixelFormat(cache->window));
|
||||
|
||||
/* the window format won't have an alpha channel, so we figure this out */
|
||||
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
|
||||
uint32_t a_mask = 0x000000FF;
|
||||
#else
|
||||
uint32_t a_mask = 0xFF000000;
|
||||
#endif
|
||||
|
||||
SDL_Surface *new_atlas = SDL_CreateRGBSurface(0,
|
||||
TEXTURE_ATLAS_SIZE,
|
||||
TEXTURE_ATLAS_SIZE,
|
||||
TEXTURE_ATLAS_BIT_DEPTH,
|
||||
native_format->Rmask,
|
||||
native_format->Gmask,
|
||||
native_format->Bmask,
|
||||
a_mask);
|
||||
SDL_FreeFormat(native_format);
|
||||
|
||||
SDL_SetSurfaceRLE(new_atlas, true);
|
||||
arrput(cache->atlas_surfaces, new_atlas);
|
||||
|
||||
SDL_Texture *new_atlas_texture =
|
||||
SDL_CreateTextureFromSurface(cache->renderer, new_atlas);
|
||||
arrput(cache->atlas_textures, new_atlas_texture);
|
||||
}
|
||||
|
||||
|
||||
static void recreate_current_atlas_texture(struct texture_cache *cache) {
|
||||
/* TODO: figure out if SDL_UpdateTexture alone is faster than blitting */
|
||||
SDL_Surface *atlas_surface = cache->atlas_surfaces[cache->atlas_index];
|
||||
|
||||
/* clear */
|
||||
SDL_FillRect(atlas_surface, NULL, 0);
|
||||
|
||||
/* blit the texture surfaces onto the atlas */
|
||||
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
|
||||
if (cache->hash[i].value.atlas_index != cache->atlas_index)
|
||||
continue;
|
||||
|
||||
SDL_BlitSurface(cache->hash[i].value.data,
|
||||
NULL,
|
||||
atlas_surface,
|
||||
&cache->hash[i].value.srcrect);
|
||||
}
|
||||
|
||||
/* texturize it! */
|
||||
SDL_LockSurface(atlas_surface);
|
||||
SDL_UpdateTexture(cache->atlas_textures[cache->atlas_index],
|
||||
NULL,
|
||||
atlas_surface->pixels,
|
||||
atlas_surface->pitch);
|
||||
SDL_UnlockSurface(atlas_surface);
|
||||
}
|
||||
|
||||
|
||||
/* uses the textures currently in the cache to create an array of stbrp_rects */
|
||||
static stbrp_rect *create_rects_from_cache(struct texture_cache *cache) {
|
||||
stbrp_rect *rects = NULL;
|
||||
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
|
||||
SDL_Surface *surface_data = cache->hash[i].value.data;
|
||||
stbrp_rect new_rect = {
|
||||
.w = surface_data->w,
|
||||
.h = surface_data->h,
|
||||
};
|
||||
|
||||
arrput(rects, new_rect);
|
||||
}
|
||||
return rects;
|
||||
}
|
||||
|
||||
|
||||
/* returns an array which contains a _copy_ of every unpacked rect in rects. */
|
||||
/* each of these copies will have their original index in rects saved in */
|
||||
/* their `id` field, which is an int. */
|
||||
static stbrp_rect *filter_unpacked_rects(stbrp_rect *rects) {
|
||||
stbrp_rect *unpacked_rects = NULL;
|
||||
for (size_t i = 0; i < arrlenu(rects); ++i) {
|
||||
/* already packed */
|
||||
if (rects[i].was_packed)
|
||||
continue;
|
||||
|
||||
arrput(unpacked_rects, rects[i]);
|
||||
/* stb_rect_pack mercifully gives you a free userdata int */
|
||||
/* the index is saved there so the original array can be updated later */
|
||||
unpacked_rects[arrlenu(unpacked_rects)-1].id = (int)i;
|
||||
}
|
||||
return unpacked_rects;
|
||||
}
|
||||
|
||||
|
||||
/* updates the original rects array with the data from packed_rects */
|
||||
/* returns true if all rects were packed successfully */
|
||||
static bool update_rects(struct texture_cache *cache, stbrp_rect *rects, stbrp_rect *packed_rects) {
|
||||
/* !!! do not grow either of the arrays !!! */
|
||||
/* the reallocation will try to reassign the array pointer, to no effect. */
|
||||
/* see stb_ds.h */
|
||||
bool packed_all = true;
|
||||
|
||||
for (size_t i = 0; i < arrlenu(packed_rects); ++i) {
|
||||
/* we can check if any rects failed to be packed right here */
|
||||
/* it's not ideal, but it avoids another iteration */
|
||||
if (!packed_rects[i].was_packed) {
|
||||
packed_all = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
rects[packed_rects[i].id] = packed_rects[i];
|
||||
/* while the order of the elements in the hash map is unknown to us, */
|
||||
/* their equivalents in `rects` are in that same (unknown) order, which means */
|
||||
/* we can use the index we had saved to find the original texture struct */
|
||||
cache->hash[packed_rects[i].id].value.atlas_index = cache->atlas_index;
|
||||
}
|
||||
|
||||
return packed_all;
|
||||
}
|
||||
|
||||
|
||||
/* updates the atlas location of every rect in the cache */
|
||||
static void update_texture_rects_in_atlas(struct texture_cache *cache, stbrp_rect *rects) {
|
||||
for (size_t i = 0; i < arrlenu(rects); ++i) {
|
||||
cache->hash[i].value.srcrect = (SDL_Rect) {
|
||||
.x = rects[i].x,
|
||||
.y = rects[i].y,
|
||||
.w = rects[i].w,
|
||||
.h = rects[i].h,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void textures_cache_init(struct texture_cache *cache, SDL_Window *window) {
|
||||
cache->window = window;
|
||||
cache->renderer = SDL_GetRenderer(window);
|
||||
sh_new_arena(cache->hash);
|
||||
sh_new_arena(cache->loner_hash);
|
||||
|
||||
cache->node_buffer = cmalloc(sizeof *cache->node_buffer * TEXTURE_ATLAS_SIZE);
|
||||
|
||||
add_new_atlas(cache);
|
||||
recreate_current_atlas_texture(cache);
|
||||
}
|
||||
|
||||
|
||||
void textures_cache_deinit(struct texture_cache *cache) {
|
||||
/* free atlas textures */
|
||||
for (size_t i = 0; i < arrlenu(cache->atlas_textures); ++i) {
|
||||
SDL_DestroyTexture(cache->atlas_textures[i]);
|
||||
}
|
||||
arrfree(cache->atlas_textures);
|
||||
|
||||
/* free atlas surfaces */
|
||||
for (size_t i = 0; i < arrlenu(cache->atlas_surfaces); ++i) {
|
||||
SDL_FreeSurface(cache->atlas_surfaces[i]);
|
||||
}
|
||||
arrfree(cache->atlas_surfaces);
|
||||
|
||||
/* free cache hashes */
|
||||
for (size_t i = 0; i < shlenu(cache->hash); ++i) {
|
||||
SDL_FreeSurface(cache->hash[i].value.data);
|
||||
}
|
||||
shfree(cache->hash);
|
||||
|
||||
for (size_t i = 0; i < shlenu(cache->loner_hash); ++i) {
|
||||
SDL_FreeSurface(cache->loner_hash[i].value.data);
|
||||
}
|
||||
shfree(cache->loner_hash);
|
||||
|
||||
free(cache->node_buffer);
|
||||
}
|
||||
|
||||
|
||||
void textures_dump_atlases(struct texture_cache *cache) {
|
||||
PHYSFS_mkdir("/dump");
|
||||
|
||||
const char string_template[] = "/dump/atlas%zd.png";
|
||||
char buf[2048]; /* larger than will ever be necessary */
|
||||
|
||||
size_t i = 0;
|
||||
for (; i < arrlenu(cache->atlas_surfaces); ++i) {
|
||||
snprintf(buf, sizeof buf, string_template, i);
|
||||
|
||||
SDL_RWops *handle = PHYSFSRWOPS_openWrite(buf);
|
||||
|
||||
if (handle == NULL) {
|
||||
CRY("Texture atlas dump failed.", "File could not be opened");
|
||||
return;
|
||||
}
|
||||
|
||||
IMG_SavePNG_RW(cache->atlas_surfaces[i], handle, true);
|
||||
log_info("Dumped atlas %s", buf);
|
||||
}
|
||||
|
||||
size_t num_loners = shlenu(cache->loner_hash);
|
||||
log_info("%zd atlases dumped. %zd loners left undumped.", i, num_loners);
|
||||
}
|
||||
|
||||
|
||||
void textures_load(struct texture_cache *cache, char *path) {
|
||||
/* no need to do anything if it was loaded already */
|
||||
if (shgeti(cache->hash, path) >= 0 || shgeti(cache->loner_hash, path) >= 0)
|
||||
return;
|
||||
|
||||
SDL_Surface *surface = image_to_surface(path);
|
||||
struct texture new_texture;
|
||||
new_texture.data = surface;
|
||||
|
||||
/* it's a "loner texture," it doesn't fit in an atlas so it's not in one */
|
||||
if (surface->w > TEXTURE_ATLAS_SIZE || surface->h > TEXTURE_ATLAS_SIZE) {
|
||||
new_texture.loner_data = SDL_CreateTextureFromSurface(cache->renderer, surface);
|
||||
new_texture.atlas_index = -1;
|
||||
shput(cache->loner_hash, path, new_texture);
|
||||
} else {
|
||||
new_texture.atlas_index = cache->atlas_index;
|
||||
shput(cache->hash, path, new_texture);
|
||||
cache->is_dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void textures_update_current_atlas(struct texture_cache *cache) {
|
||||
/* this function makes a lot more sense if you read stb_rect_pack.h */
|
||||
stbrp_context pack_ctx; /* target info */
|
||||
stbrp_init_target(&pack_ctx,
|
||||
TEXTURE_ATLAS_SIZE,
|
||||
TEXTURE_ATLAS_SIZE,
|
||||
cache->node_buffer,
|
||||
TEXTURE_ATLAS_SIZE);
|
||||
|
||||
stbrp_rect *rects = create_rects_from_cache(cache);
|
||||
|
||||
/* we have to keep packing, and creating atlases if necessary, */
|
||||
/* until all rects have been packed. */
|
||||
/* ideally, this will not iterate more than once. */
|
||||
bool textures_remaining = true;
|
||||
while (textures_remaining) {
|
||||
stbrp_rect *rects_to_pack = filter_unpacked_rects(rects);
|
||||
stbrp_pack_rects(&pack_ctx, rects_to_pack, (int)arrlen(rects_to_pack));
|
||||
|
||||
textures_remaining = !update_rects(cache, rects, rects_to_pack);
|
||||
arrfree(rects_to_pack); /* got what we needed */
|
||||
|
||||
/* some textures couldn't be packed */
|
||||
if (textures_remaining) {
|
||||
update_texture_rects_in_atlas(cache, rects);
|
||||
recreate_current_atlas_texture(cache);
|
||||
|
||||
/* need a new atlas for next time */
|
||||
add_new_atlas(cache);
|
||||
++cache->atlas_index;
|
||||
}
|
||||
};
|
||||
|
||||
update_texture_rects_in_atlas(cache, rects);
|
||||
recreate_current_atlas_texture(cache);
|
||||
|
||||
cache->is_dirty = false;
|
||||
|
||||
arrfree(rects);
|
||||
}
|
||||
|
||||
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
|
||||
if (texture == NULL) {
|
||||
CRY("Texture lookup failed.",
|
||||
"Tried to get texture that isn't loaded.");
|
||||
return (SDL_Rect){ 0, 0, 0, 0 };
|
||||
}
|
||||
return texture->value.srcrect;
|
||||
}
|
||||
|
||||
|
||||
int textures_get_atlas_index(struct texture_cache *cache, char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->hash, path);
|
||||
|
||||
/* it might be a loner texture */
|
||||
if (texture == NULL) {
|
||||
texture = shgetp_null(cache->loner_hash, path);
|
||||
|
||||
/* never mind it's just not there at all */
|
||||
if (texture == NULL) {
|
||||
CRY("Texture atlas index lookup failed.",
|
||||
"Tried to get atlas index of texture that isn't loaded.");
|
||||
return INT_MIN;
|
||||
}
|
||||
}
|
||||
|
||||
return texture->value.atlas_index;
|
||||
}
|
||||
|
||||
|
||||
SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index) {
|
||||
/* out of bounds */
|
||||
if (arrlen(cache->atlas_textures) < index + 1 || index < 0)
|
||||
return NULL;
|
||||
|
||||
return cache->atlas_textures[index];
|
||||
}
|
||||
|
||||
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path) {
|
||||
struct texture_cache_item *texture = shgetp_null(cache->loner_hash, path);
|
||||
|
||||
if (texture == NULL) {
|
||||
CRY("Loner texture lookup failed.",
|
||||
"Tried to get texture that isn't loaded.");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
return texture->value.loner_data;
|
||||
}
|
||||
|
||||
|
||||
size_t textures_get_num_atlases(struct texture_cache *cache) {
|
||||
return cache->atlas_index + 1;
|
||||
}
|
78
src/textures.h
Normal file
78
src/textures.h
Normal file
@ -0,0 +1,78 @@
|
||||
#ifndef TEXTURES_H
|
||||
#define TEXTURES_H
|
||||
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <stb_rect_pack.h>
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
|
||||
struct texture {
|
||||
SDL_Rect srcrect; /* position in atlas */
|
||||
SDL_Surface *data; /* original image data */
|
||||
SDL_Texture *loner_data; /* loner textures store their data directly */
|
||||
int atlas_index; /* which atlas the texture is in */
|
||||
int8_t layer;
|
||||
};
|
||||
|
||||
|
||||
struct texture_cache_item {
|
||||
char *key;
|
||||
struct texture value;
|
||||
};
|
||||
|
||||
|
||||
/* use the public API to create and manipulate instances of this structure */
|
||||
struct texture_cache {
|
||||
/* from context */
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
|
||||
struct texture_cache_item *hash;
|
||||
struct texture_cache_item *loner_hash;
|
||||
|
||||
stbrp_node *node_buffer; /* used internally by stb_rect_pack */
|
||||
|
||||
SDL_Surface **atlas_surfaces;
|
||||
SDL_Texture **atlas_textures;
|
||||
int atlas_index;
|
||||
|
||||
bool is_dirty; /* current atlas needs to be recreated */
|
||||
};
|
||||
|
||||
|
||||
void textures_cache_init(struct texture_cache *cache, SDL_Window *window);
|
||||
void textures_cache_deinit(struct texture_cache *cache);
|
||||
|
||||
/* for debugging */
|
||||
void textures_dump_atlases(struct texture_cache *cache);
|
||||
|
||||
/* loads an image if it isn't in the cache, otherwise a no-op. */
|
||||
/* can be called from anywhere at any time after init, useful if you want to */
|
||||
/* preload textures you know will definitely be used */
|
||||
void textures_load(struct texture_cache *cache, char *path);
|
||||
|
||||
/* repacks the current texture atlas based on the texture cache */
|
||||
void textures_update_current_atlas(struct texture_cache *cache);
|
||||
|
||||
/* returns a rect in a texture cache atlas based on a path, for drawing */
|
||||
/* if the texture is not found, returns a zero-filled rect (so check w or h) */
|
||||
SDL_Rect textures_get_srcrect(struct texture_cache *cache, char *path);
|
||||
|
||||
/* returns which atlas the texture in the path is in, starting from 0 */
|
||||
/* if the texture is not found, returns INT_MIN */
|
||||
int textures_get_atlas_index(struct texture_cache *cache, char *path);
|
||||
|
||||
/* returns a pointer to the atlas at `index` */
|
||||
/* if the index is out of bounds, returns NULL. */
|
||||
/* you can get the index via texture_get_atlas_index */
|
||||
SDL_Texture *textures_get_atlas(struct texture_cache *cache, int index);
|
||||
|
||||
SDL_Texture *textures_get_loner(struct texture_cache *cache, char *path);
|
||||
|
||||
/* returns the number of atlases in the cache */
|
||||
size_t textures_get_num_atlases(struct texture_cache *cache);
|
||||
|
||||
|
||||
#endif
|
191
src/util.c
Normal file
191
src/util.c
Normal file
@ -0,0 +1,191 @@
|
||||
#include "util.h"
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <physfsrwops.h>
|
||||
#define STB_DS_IMPLEMENTATION
|
||||
#define STBDS_ASSERT SDL_assert
|
||||
#define STBDS_REALLOC(c,p,s) crealloc(p, s)
|
||||
#define STBDS_FREE(c,p) free(p)
|
||||
#include <stb_ds.h>
|
||||
#define STB_RECT_PACK_IMPLEMENTATION
|
||||
#define STBRP_ASSERT SDL_assert
|
||||
#include <stb_rect_pack.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include <stdnoreturn.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
|
||||
|
||||
void cry_impl(const char *file, const int line, const char *title, const char *text) {
|
||||
SDL_LogCritical(SDL_LOG_CATEGORY_APPLICATION,
|
||||
"TEARS AT %s:%d: %s ... %s", file, line, title, text);
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR, title, text, NULL);
|
||||
}
|
||||
|
||||
|
||||
static void log_impl(const char *restrict format, va_list args, SDL_LogPriority priority) {
|
||||
SDL_LogMessageV(SDL_LOG_CATEGORY_APPLICATION,
|
||||
priority,
|
||||
format,
|
||||
args);
|
||||
}
|
||||
|
||||
|
||||
void log_info(const char *restrict format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log_impl(format, args, SDL_LOG_PRIORITY_INFO);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
|
||||
void log_critical(const char *restrict format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log_impl(format, args, SDL_LOG_PRIORITY_CRITICAL);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
|
||||
void log_warn(const char *restrict format, ...) {
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
log_impl(format, args, SDL_LOG_PRIORITY_WARN);
|
||||
va_end(args);
|
||||
}
|
||||
|
||||
|
||||
noreturn static void alloc_failure_death(void) {
|
||||
log_critical("Allocation failure. Aborting NOW.");
|
||||
SDL_ShowSimpleMessageBox(SDL_MESSAGEBOX_ERROR,
|
||||
"MEMORY ALLOCATION FAILURE.",
|
||||
"FAILED TO ALLOCATE MEMORY. "
|
||||
"YOU MIGHT BE UNLUCKY. "
|
||||
"THE GAME WILL EXIT NOW.",
|
||||
NULL);
|
||||
|
||||
die_abruptly();
|
||||
}
|
||||
|
||||
|
||||
noreturn void die_abruptly(void) {
|
||||
/* a zombie window will linger if we don't at least try to quit SDL */
|
||||
SDL_Quit();
|
||||
abort();
|
||||
}
|
||||
|
||||
|
||||
void *cmalloc(size_t size) {
|
||||
void *ptr = malloc(size);
|
||||
if (ptr == NULL)
|
||||
alloc_failure_death();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
|
||||
void *crealloc(void *ptr, size_t size) {
|
||||
void *out = realloc(ptr, size);
|
||||
if (out == NULL)
|
||||
alloc_failure_death();
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
void *ccalloc(size_t num, size_t size) {
|
||||
void *ptr = calloc(num, size);
|
||||
if (ptr == NULL)
|
||||
alloc_failure_death();
|
||||
return ptr;
|
||||
}
|
||||
|
||||
|
||||
int64_t file_to_bytes(char *path, unsigned char **buf_out) {
|
||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||
|
||||
if (handle == NULL) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
|
||||
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
|
||||
|
||||
*buf_out = cmalloc(data_size);
|
||||
SDL_RWread(handle, *buf_out, sizeof **buf_out, data_size / sizeof **buf_out);
|
||||
SDL_RWclose(handle); /* we got all we needed from the stream */
|
||||
|
||||
return data_size;
|
||||
}
|
||||
|
||||
|
||||
char *file_to_str(char *path) {
|
||||
SDL_RWops *handle = PHYSFSRWOPS_openRead(path);
|
||||
|
||||
if (handle == NULL) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int64_t data_size = SDL_RWseek(handle, 0, RW_SEEK_END);
|
||||
SDL_RWseek(handle, 0, RW_SEEK_SET); /* reset offset into data */
|
||||
|
||||
char *str_out = cmalloc(data_size + 1); /* data plus final null */
|
||||
size_t len = data_size / sizeof *str_out;
|
||||
|
||||
SDL_RWread(handle, str_out, sizeof *str_out, len);
|
||||
SDL_RWclose(handle); /* we got all we needed from the stream */
|
||||
|
||||
str_out[len] = '\0';
|
||||
|
||||
return str_out;
|
||||
}
|
||||
|
||||
|
||||
bool strends(const char *str, const char *suffix) {
|
||||
size_t str_length = strlen(str);
|
||||
size_t suffix_length = strlen(suffix);
|
||||
|
||||
if (suffix_length > str_length)
|
||||
return false;
|
||||
|
||||
return memcmp((str + str_length) - suffix_length, suffix, suffix_length) == 0;
|
||||
}
|
||||
|
||||
|
||||
bool intersect_rect(const t_rect *a, const t_rect *b, t_rect *result) {
|
||||
SDL_Rect a_sdl = { a->x, a->y, a->w, a->h };
|
||||
SDL_Rect b_sdl = { b->x, b->y, b->w, b->h };
|
||||
SDL_Rect result_sdl = { 0 };
|
||||
|
||||
bool intersection = SDL_IntersectRect(&a_sdl, &b_sdl, &result_sdl);
|
||||
|
||||
*result = (t_rect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
|
||||
return intersection;
|
||||
}
|
||||
|
||||
|
||||
bool intersect_frect(const t_frect *a, const t_frect *b, t_frect *result) {
|
||||
SDL_FRect a_sdl = { a->x, a->y, a->w, a->h };
|
||||
SDL_FRect b_sdl = { b->x, b->y, b->w, b->h };
|
||||
SDL_FRect result_sdl = { 0 };
|
||||
|
||||
bool intersection = SDL_IntersectFRect(&a_sdl, &b_sdl, &result_sdl);
|
||||
|
||||
*result = (t_frect){ result_sdl.x, result_sdl.y, result_sdl.w, result_sdl.h };
|
||||
return intersection;
|
||||
}
|
||||
|
||||
|
||||
t_frect to_frect(t_rect rect) {
|
||||
return (t_frect) {
|
||||
.h = (float)rect.h,
|
||||
.w = (float)rect.w,
|
||||
.x = (float)rect.x,
|
||||
.y = (float)rect.y,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
void tick_timer(int *value) {
|
||||
*value = MAX(*value - 1, 0);
|
||||
}
|
120
src/util.h
Normal file
120
src/util.h
Normal file
@ -0,0 +1,120 @@
|
||||
#ifndef UTIL_H
|
||||
#define UTIL_H
|
||||
|
||||
|
||||
#include <SDL2/SDL.h>
|
||||
#include <physfs.h>
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include <math.h>
|
||||
#include <stdnoreturn.h>
|
||||
|
||||
|
||||
/* */
|
||||
/* GENERAL UTILITIES */
|
||||
/* */
|
||||
|
||||
void cry_impl(const char *file, const int line, const char *title, const char *text);
|
||||
#define CRY(title, text) cry_impl(__FILE__, __LINE__, title, text)
|
||||
#define CRY_SDL(title) cry_impl(__FILE__, __LINE__, title, SDL_GetError())
|
||||
#define CRY_PHYSFS(title) \
|
||||
cry_impl(__FILE__, __LINE__, title, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode()))
|
||||
|
||||
|
||||
void log_info(const char *restrict format, ...);
|
||||
void log_critical(const char *restrict format, ...);
|
||||
void log_warn(const char *restrict format, ...);
|
||||
|
||||
|
||||
/* for when there's absolutely no way to continue */
|
||||
noreturn void die_abruptly(void);
|
||||
|
||||
|
||||
/* "critical" allocation functions which will log and abort() on failure. */
|
||||
/* if it is reasonable to handle this gracefully, use the standard versions. */
|
||||
/* the stb implementations will be configured to use these */
|
||||
void *cmalloc(size_t size);
|
||||
void *crealloc(void *ptr, size_t size);
|
||||
void *ccalloc(size_t num, size_t size);
|
||||
|
||||
|
||||
/* DON'T FORGET ABOUT DOUBLE EVALUATION */
|
||||
/* YOU THINK YOU WON'T, AND THEN YOU DO */
|
||||
/* C23's typeof could fix it, so i will */
|
||||
/* leave them as macros to be replaced. */
|
||||
/* use the macro in tgmath.h for floats */
|
||||
#define MAX SDL_max
|
||||
#define MIN SDL_min
|
||||
|
||||
|
||||
/* sets buf_out to a pointer to a byte buffer which must be freed. */
|
||||
/* returns the size of this buffer. */
|
||||
int64_t file_to_bytes(char *path, unsigned char **buf_out);
|
||||
|
||||
/* returns a pointer to a string which must be freed */
|
||||
char *file_to_str(char *path);
|
||||
|
||||
|
||||
/* returns true if str ends with suffix */
|
||||
bool strends(const char *str, const char *suffix);
|
||||
|
||||
|
||||
/* */
|
||||
/* GAME LOGIC UTILITIES */
|
||||
/* */
|
||||
|
||||
/* 32-bit color data */
|
||||
typedef struct color {
|
||||
uint8_t r;
|
||||
uint8_t g;
|
||||
uint8_t b;
|
||||
uint8_t a;
|
||||
} t_color;
|
||||
|
||||
|
||||
/* a rectangle with the origin at the upper left (integer) */
|
||||
typedef struct rect {
|
||||
int x, y;
|
||||
int w, h;
|
||||
} t_rect;
|
||||
|
||||
|
||||
bool intersect_rect(const t_rect *a, const t_rect *b, t_rect *result);
|
||||
|
||||
|
||||
/* a rectangle with the origin at the upper left (floating point) */
|
||||
typedef struct frect {
|
||||
float x, y;
|
||||
float w, h;
|
||||
} t_frect;
|
||||
|
||||
|
||||
bool intersect_frect(const t_frect *a, const t_frect *b, t_frect *result);
|
||||
|
||||
t_frect to_frect(t_rect rect);
|
||||
|
||||
|
||||
/* a point in some space (integer) */
|
||||
typedef struct vec2 {
|
||||
int x, y;
|
||||
} t_vec2;
|
||||
|
||||
|
||||
/* a point in some space (floating point) */
|
||||
typedef struct fvec2 {
|
||||
float x, y;
|
||||
} t_fvec2;
|
||||
|
||||
|
||||
/* decrements an lvalue (which should be an int), stopping at 0 */
|
||||
/* meant for tick-based timers in game logic */
|
||||
/*
|
||||
* example:
|
||||
* tick_timer(&player->jump_air_timer);
|
||||
*/
|
||||
void tick_timer(int *value);
|
||||
|
||||
|
||||
#endif
|
Reference in New Issue
Block a user