feat: documentation and cleanup pass over all objects

This commit is contained in:
Sara 2025-06-08 00:26:14 +02:00
parent 7ff9756dcc
commit 872f3f3fbf
11 changed files with 156 additions and 77 deletions

View file

@ -45,7 +45,7 @@ void Character::act() {
void Character::draw() {
if(this->health > 0) {
Render::draw(this->location, this->data->sprite);
} // else { // draw gore pile
}
}
bool Character::deal_damage(int damage) {

View file

@ -23,6 +23,7 @@ private:
void set_character(Character *character);
};
// Character's stats as a struct that's easilly made into a flyweight to share around
struct CharacterData {
int health{1};
int damage{1};

View file

@ -12,16 +12,20 @@ InputHandler::~InputHandler() {
Input::remove_handler(this);
}
KeyEventHandler::KeyEventHandler(std::set<SDL_Scancode> code, Listener listener, bool allow_repeat)
: scancode{code}
KeyEventHandler::KeyEventHandler(std::set<SDL_Scancode> codes, Listener listener, bool allow_repeat)
: scancodes{codes}
, listener{listener}
, allow_repeat{allow_repeat}
{}
void KeyEventHandler::process_event(SDL_Event event) {
// check if this event is an event related to keys
if((event.type == SDL_KEYDOWN || event.type == SDL_KEYUP)
// check if it is an allowed event (either non-repeated or repetition is allowed)
&& (event.key.repeat == 0 || this->allow_repeat)
&& this->scancode.contains(event.key.keysym.scancode)) {
// check if it is one of our keys
&& this->scancodes.contains(event.key.keysym.scancode)) {
// trigger event, notifying it if the key was pressed or released
this->listener(event.type == SDL_KEYDOWN);
}
}

View file

@ -7,25 +7,35 @@
#include <set>
namespace rogue {
// Interface representing a handler for SDL os events that can be used to handle input
// Uses RAII to register and deregister with the static input service
struct InputHandler {
InputHandler();
virtual ~InputHandler();
virtual void process_event(SDL_Event event) = 0;
};
struct KeyEventHandler : public InputHandler {
typedef std::function<void(bool down)> Listener;
KeyEventHandler(std::set<SDL_Scancode> code, Listener listener, bool allow_repeat = true);
// implementation of the InputHandler interface that filters for SDL_KeyEvents that represent a given key or keys being pressed
//
class KeyEventHandler : public InputHandler {
public:
typedef std::function<void(bool down)> Listener; // defines the interface required for listening for key events
// from this constructor until the object is destroyed, these settings remain the same, if you need to modify them, recreate the object
KeyEventHandler(std::set<SDL_Scancode> codes, Listener listener, bool allow_repeat = true);
// handle events passed in from the OS through SDL
virtual void process_event(SDL_Event event) override;
std::set<SDL_Scancode> scancode{};
Listener listener{};
bool allow_repeat{false};
private:
std::set<SDL_Scancode> scancodes{}; // scancodes to listen for
Listener listener{}; // function to notify when event is triggered
bool allow_repeat{false}; // whether or not to allow key repeat (multiple down events before an up on the same key)
};
class Input {
public:
// register an input handler, registered handlers will be notified when Input::process_event is called
static void add_handler(InputHandler *handler);
static void remove_handler(InputHandler *handler);
// notify registered InputHandlers of an event
static void process_event(SDL_Event event);
private:
static std::set<InputHandler*> handlers;

View file

@ -94,7 +94,7 @@ void Render::provide_render_data(RenderData &data) {
Render::data = &data;
}
void Render::clear(Tile camera_center, unsigned fov) {
void Render::start_frame(Tile camera_center, unsigned fov) {
assert_render_initialized();
Render::camera_width = fov;
SDL_SetRenderDrawColor(Render::data->renderer, 0u, 0u, 0u, 255u);
@ -126,7 +126,7 @@ void Render::draw(Tile world_space_tile, Sprite sprite) {
void Render::draw_menu(Sprite menu) {
assert_render_initialized();
Render::clear({0, 0});
Render::start_frame({0, 0});
SDL_Rect const rect {
.x = window_size.x/2 - window_size.y/2,
.y = 0,
@ -148,7 +148,7 @@ void Render::draw_healthbar(int current, int max, SDL_Rect area, int border) {
SDL_RenderFillRect(Render::data->renderer, &area);
}
void Render::present() {
void Render::finish_frame() {
assert_render_initialized();
SDL_RenderPresent(Render::data->renderer);
}

View file

@ -11,6 +11,8 @@
namespace rogue {
// Contains all data required to render the game world
// IMPORTANT: store this in stack memory within the scope that encompasses the game loop (probably main(..)).
// When this is destroyed, rendering becomes impossible until a new one is provided to Render
class RenderData {
friend class RenderDataSetup;
friend class Render;
@ -23,45 +25,65 @@ private: RenderData() = default;
};
// Global interface for rendering
// Serves as a service locator for render data with a simplified interface
class Render {
private:
friend class RenderData;
// called by render data when it's constructor is called, de-initializes Renderer
// once this is called rendering is no longer possible
static void clear_render_data();
public:
// provides the render data struct to the renderer, enabling it's use
static void provide_render_data(RenderData &data);
static void clear(Tile camera_center, unsigned fov = 10);
// clear the screen and set the camera location for the next frame
static void start_frame(Tile camera_center, unsigned fov = 10);
// draw a sprite to the screen using it's sprite index
static void draw(Tile world_space_tile, Sprite sprite);
// draw a menu image that fills screen
static void draw_menu(Sprite menu);
// draw a healthbar
static void draw_healthbar(int current, int max, SDL_Rect area={0,0,500,50}, int border=10);
static void present();
// finish the frame and swap the buffers
static void finish_frame();
private:
// the tile at the center of the camera, set at the start of a frame
static Tile camera_center;
// number of tiles to render horizontally
static unsigned camera_width;
// the world offset to apply to sprites when drawing them to put them relative to the camera
static Tile camera_offset;
static SDL_Point window_size;
static int tile_screen_width;
static int half_tile_screen_width;
static RenderData *data;
static SDL_Point window_size; // size of the window, computed at the start of the frame
static int tile_screen_width; // width of a tile on the screen in pixels
static int half_tile_screen_width; // half of 'tile_screen_width', pre-computed at frame start
static RenderData *data; // render data provided to the renderer, rendering is only possible while this is set
};
// Render configuration, builder for RenderData
class RenderDataSetup {
public:
RenderDataSetup() = default;
// window has to be set up before anything else
RenderDataSetup &with_window(char const *name, Uint32 flags);
// renderer has to be set up before images can be loaded
RenderDataSetup &with_renderer(Uint32 flags);
// set the resource path, required to load resources
RenderDataSetup &with_resource_path(std::filesystem::path base_path);
// load a sprite and register it
RenderDataSetup &with_sprite(std::filesystem::path sprite_path);
// load a menu image and register it
RenderDataSetup &with_menu(std::filesystem::path sprite_path);
// construct render-data that can be provided to Render
RenderData build();
private:
// helper function for loading SDL textures from filepaths
SDL_Texture *load_texture(std::filesystem::path file_path);
private:
unsigned fov{10};
// the base path to load images from
std::optional<std::filesystem::path> resource_base_path;
SDL_Window *window{nullptr};
SDL_Renderer *renderer{nullptr};
SDL_Window *window{nullptr}; // window created for the application
SDL_Renderer *renderer{nullptr}; // renderer created from the window
// images loaded from resource_path
std::vector<SDL_Texture*> sprites{};
std::vector<SDL_Texture*> menus{};
};

View file

@ -3,23 +3,29 @@
#include "SDL2/SDL_rect.h"
//
// commonly reused typedefs and macros
//
// bitmask of the four cardinal directions
// would be an enum, but C++ typechecking doesn't like that, so this results in more readable code overall
typedef unsigned short Directions;
#define NORTH 0x1u
#define EAST (0x1u << 1)
#define SOUTH (0x1u << 2)
#define WEST (0x1u << 3)
// simple shorthand for wrapping limiting a number by wrapping
// useful for generating random numbers within a range
#define WRAP(M_x, M_min, M_max) (((M_x - M_min) % (M_max - M_min)) + M_min)
#define casel(M_switch, M_case)\
case M_case:\
M_switch##_case_##M_case
// typedefs that help with differentiating between chunk coordinates and tile coordinates
typedef SDL_Point Tile;
typedef SDL_Point Chunk;
// represents a sprite index
typedef unsigned Sprite;
// define a comparison function for SDL_Point to make code slightly more readable
static inline bool operator==(SDL_Point const &lhs, SDL_Point const &rhs) {
return lhs.x == rhs.x && lhs.y == rhs.y;
}

View file

@ -56,14 +56,14 @@ void World::act_if_requested() {
void World::act() {
this->player.act();
this->boss.act();
for(Character &character : this->characters) {
character.act();
}
this->boss.act();
}
void World::render() {
Render::clear(this->player.location, 20);
Render::start_frame(this->player.location, 20);
for(size_t i{0}; i < this->rooms.size(); ++i) {
this->rooms[i].draw({
.x = int(i % this->shear * this->chunk_size),

View file

@ -27,53 +27,81 @@ struct Room {
bool tile_is_wall(Tile local_tile) const;
};
struct World {
// represents the current state of the game environment and actors
class World {
friend class WorldGenerator;
public:
World();
~World() = default;
void act_if_requested();
void act();
void render();
Room &get_room(Chunk chunk);
Character *get_player();
Character *get_boss();
Chunk get_chunk(Tile tile);
TileData query_tile(Tile tile);
void on_input(bool);
void act_if_requested(); // will call 'act' when 'turn_requested' is set
void render(); // render the world, includes ui, environment and characters
Room &get_room(Chunk chunk); // returns the room at the given chunk
Character *get_player(); // returns the player character instance
Character *get_boss(); // returns the boss character instance
Chunk get_chunk(Tile tile); // returns the chunk at the given world-space tile
TileData query_tile(Tile tile); // returns data related to the given world-space tile
void on_input(bool pressed); // registered with the 'direction_pressed' key event handler
private:
int chunk_size{1};
unsigned shear{0};
bool turn_requested{false};
Character player{};
Character boss{};
std::vector<Room> rooms{};
std::vector<Character> characters{};
KeyEventHandler direction_pressed;
void act();
private:
int chunk_size{1}; // size of chunks
unsigned shear{0}; // number of chunks before the next row (width of the map in chunks)
bool turn_requested{false}; // flag for requesting a turn, consumed by 'act_if_requested' set by 'on_input'
// this flag exists (instead of just calling 'act' from 'on_input' to avoid moving the world
// before the player's input is registered by the PlayerCharacterLogic
Character player{}; // player character, should be instantiated by world generator
Character boss{}; // boss character, should be instantiated by world generator
std::vector<Room> rooms{}; // all of the rooms in the map in order, this is a 2d array that wraps at 'shear'
std::vector<Character> characters{}; // all of the characters in the map, including dead ones
KeyEventHandler direction_pressed; // triggered when the player presses any direction, calls 'on_input'
};
// Builder/factory for configuring and generating game environments and enemies
class WorldGenerator {
public:
WorldGenerator() = default;
// set the chunk size, this also limits the size of rooms,
// chunks are always squares with a size of side_length * side_length
WorldGenerator &with_chunk_size(unsigned side_length);
// set the size of the world,
// the world is always a square with a size of side_length * side_length
WorldGenerator &with_world_size(unsigned side_length);
// set the minimum and maximum side length for rooms,
// rooms are generated with random sizes for both width and height
WorldGenerator &with_room_size(unsigned min_side_length, unsigned max_side_length);
// set reusable factory object for the player
WorldGenerator &with_player(Character::Spawner spawner);
// add an enemy that can be spawned in rooms,
// pass in a factory object and the inverse of the frequency
// (think of it as "1 in every N rooms will have this enemy")
WorldGenerator &with_enemy(Character::Spawner spawner, int inv_frequency);
// set the factory object used to spawn the boss
WorldGenerator &with_boss(Character::Spawner spawner);
// set the chance that a room will have a connection in inverse frequency
// keep in mind this is rolled per room pair that isn't already connected, so 2 times per room
WorldGenerator &with_connecting_chance(unsigned num);
// generates a new world using the current configuration, requires all settings to be set
std::unique_ptr<World> generate();
private:
// set up for Recursive Backtracking Maze Generation
void generate_world(std::unique_ptr<World> &world, Chunk start);
// Recursive Backtracking Maze Generation
void generate_room(std::unique_ptr<World> &world, Chunk const &last);
// spawn enemies in a generated room
// generates a boss enemy for the first room it's called on.
void add_enemies(std::unique_ptr<World> &world, Chunk const &chunk, Room &room);
// add the player to a room (does not have to be generated, as the player always spawns at the center)
void setup_player(std::unique_ptr<World> &world, Chunk spawn_chunk);
private:
// Factory objects for enemies
Character::Spawner player_spawner{};
Character::Spawner boss_spawner{};
// factory object, inverse frequency
std::vector<std::pair<Character::Spawner, int>> enemy_spawners{};
// sizes
int chunk_side_length{0}, world_side_length{0};
int max_room_size{0}, min_room_size{0};
unsigned connecting_chance{1};
// set by 'generate_enemies' when the boss is generated
bool boss_generated{false};
};
}

View file

@ -10,18 +10,25 @@ Tile CritteLogic::move() {
if(this->is_aware_of_player) {
// cache query_tile call for repeated use
std::function<TileData(Tile)> query_tile{std::bind(&World::query_tile, this->character->world, std::placeholders::_1)};
// this could be a few quick vector math operations, but i'm minimizing the C++ features i'm using with SDL structs
// difference between the player's location and ours
Tile difference{
player->location.x - location.x,
player->location.y - location.y
};
// absolute difference, useful for comparing them
Tile abs{std::abs(difference.x), std::abs(difference.y)};
Tile direction{
// signs of the difference
Tile signs{
difference.x == 0 ? 0 : difference.x / std::abs(difference.x),
difference.y == 0 ? 0 : difference.y / std::abs(difference.y)
};
Tile x_motion{location.x + direction.x, location.y};
Tile y_motion{location.x, location.y + direction.y};
if(abs.x > abs.y) {
// theoretical motion along x
Tile x_motion{location.x + signs.x, location.y};
// theoretical motion along y
Tile y_motion{location.x, location.y + signs.y};
// choose either x or y motion depending on which is possible and faster
if(abs.x >= abs.y) {
if(!query_tile(x_motion).is_wall)
return x_motion;
else if(!query_tile(y_motion).is_wall) {

View file

@ -37,17 +37,23 @@ CharacterData const critte_data{
int main(int argc, char *argv[]) {
// Game state, simple way of managing which menu/screen the player is currently on
GameState game_state{GameState::MainMenu};
// World generator, can be used to generate new rooms when the player restarts the game
WorldGenerator generator{WorldGenerator()
.with_world_size(6)
// world generation
.with_chunk_size(8)
.with_room_size(5, 8)
.with_connecting_chance(5)
// setup character spawners/character factories
.with_player(Character::make_factory<PlayerCharacterLogic>(player_data))
// enemies and boss all use the same "AI" with different data
.with_enemy(Character::make_factory<CritteLogic>(critte_data), 2)
.with_enemy(Character::make_factory<CritteLogic>(critte_data), 3)
.with_enemy(Character::make_factory<CritteLogic>(critte_data), 10)
.with_boss(Character::make_factory<CritteLogic>(boss_data))};
// renderdata, passed to Render but kept here so that RAII will call the destructor when main pops off the stack
RenderData data{RenderDataSetup()
// create window and renderer, allowing for texture creation
.with_window("roguelike", SDL_WINDOW_ALLOW_HIGHDPI | SDL_WINDOW_FULLSCREEN)
@ -65,45 +71,37 @@ int main(int argc, char *argv[]) {
.with_menu("escape.png")
.build()
};
// again, provided to the renderer here, but ownership remains local
Render::provide_render_data(data);
// use <functional> features to capture game_state and listen for input within main()
// the victory and death screens only allow jumping back with return/enter
KeyEventHandler return_to_main_input{{SDL_SCANCODE_RETURN},
[&game_state](bool down) {
if(!down) return;
switch(game_state) {
case GameState::Game: // do nothing, let the input be handled by the world/characters
case GameState::MainMenu: // main menu starts with direction input not enter
break;
case GameState::GameOver:
game_state = GameState::MainMenu;
break;
case GameState::Escape:
game_state = GameState::MainMenu;
break;
if(!down) {
return;
} else if(game_state == GameState::GameOver || game_state == GameState::Escape) {
game_state = GameState::MainMenu;
}
},
false
};
// The game is started by pressing one of the input directions as a soft tutorial
KeyEventHandler start_game_input{{
SDL_SCANCODE_H, SDL_SCANCODE_J, SDL_SCANCODE_K, SDL_SCANCODE_L,
SDL_SCANCODE_LEFT, SDL_SCANCODE_RIGHT, SDL_SCANCODE_UP, SDL_SCANCODE_DOWN
},
[&game_state](bool down) {
if(!down) return;
switch(game_state) {
case GameState::Game:
case GameState::GameOver:
case GameState::Escape:
break;
case GameState::MainMenu:
game_state = GameState::Game;
break;
if(!down) {
return;
} else if(game_state == GameState::MainMenu) {
game_state = GameState::Game;
}
},
false
};
Render::provide_render_data(data);
// create an empty world, filled in when the game is started by the player
std::unique_ptr<World> world{nullptr};
SDL_Event evt;
// main loop, continues until broken out of
for(;;) {
// DOOM-style menus
switch(game_state) {
@ -115,6 +113,7 @@ int main(int argc, char *argv[]) {
if(world == nullptr) {
world.reset(generator.generate().release());
}
// tick characters (if requested) and render
world->act_if_requested();
world->render();
// game end conditions (player or boss death)
@ -133,14 +132,16 @@ int main(int argc, char *argv[]) {
Render::draw_menu(2);
break;
}
Render::present();
Render::finish_frame();
// receive input
SDL_Event evt;
if(SDL_WaitEvent(&evt)) {
if(evt.type == SDL_QUIT) {
goto break_main_loop;
if(evt.type == SDL_QUIT) { // shortcirquit for quitting when requested
break; // quit by breaking out of the main loop
} else {
Input::process_event(evt);
}
}
} break_main_loop:
}
return 0;
}