From 872f3f3fbf35e1aa3121fe825e6667d207c842c5 Mon Sep 17 00:00:00 2001 From: Sara Date: Sun, 8 Jun 2025 00:26:14 +0200 Subject: [PATCH] feat: documentation and cleanup pass over all objects --- src/core/character.cpp | 2 +- src/core/character.h | 1 + src/core/input.cpp | 10 +++++-- src/core/input.h | 22 ++++++++++---- src/core/renderer.cpp | 6 ++-- src/core/renderer.h | 40 +++++++++++++++++++------ src/core/roguedefs.h | 14 ++++++--- src/core/world.cpp | 4 +-- src/core/world.h | 66 ++++++++++++++++++++++++++++++------------ src/enemies.cpp | 15 +++++++--- src/main.cpp | 53 ++++++++++++++++----------------- 11 files changed, 156 insertions(+), 77 deletions(-) diff --git a/src/core/character.cpp b/src/core/character.cpp index 7ad35c9..f04c7e9 100644 --- a/src/core/character.cpp +++ b/src/core/character.cpp @@ -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) { diff --git a/src/core/character.h b/src/core/character.h index 50fb9bf..ddfca85 100644 --- a/src/core/character.h +++ b/src/core/character.h @@ -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}; diff --git a/src/core/input.cpp b/src/core/input.cpp index 90b67d5..16ead51 100644 --- a/src/core/input.cpp +++ b/src/core/input.cpp @@ -12,16 +12,20 @@ InputHandler::~InputHandler() { Input::remove_handler(this); } -KeyEventHandler::KeyEventHandler(std::set code, Listener listener, bool allow_repeat) -: scancode{code} +KeyEventHandler::KeyEventHandler(std::set 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); } } diff --git a/src/core/input.h b/src/core/input.h index e295d51..e5c4fdb 100644 --- a/src/core/input.h +++ b/src/core/input.h @@ -7,25 +7,35 @@ #include 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 Listener; - KeyEventHandler(std::set 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 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 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 scancode{}; - Listener listener{}; - bool allow_repeat{false}; +private: + std::set 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 handlers; diff --git a/src/core/renderer.cpp b/src/core/renderer.cpp index 2940ac1..0a9fb8b 100644 --- a/src/core/renderer.cpp +++ b/src/core/renderer.cpp @@ -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); } diff --git a/src/core/renderer.h b/src/core/renderer.h index c745ed2..4713f23 100644 --- a/src/core/renderer.h +++ b/src/core/renderer.h @@ -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 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 sprites{}; std::vector menus{}; }; diff --git a/src/core/roguedefs.h b/src/core/roguedefs.h index 336df9e..db530a1 100644 --- a/src/core/roguedefs.h +++ b/src/core/roguedefs.h @@ -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; } diff --git a/src/core/world.cpp b/src/core/world.cpp index e95e570..cd916f7 100644 --- a/src/core/world.cpp +++ b/src/core/world.cpp @@ -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), diff --git a/src/core/world.h b/src/core/world.h index 85f4001..ae40af4 100644 --- a/src/core/world.h +++ b/src/core/world.h @@ -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 rooms{}; - std::vector 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 rooms{}; // all of the rooms in the map in order, this is a 2d array that wraps at 'shear' + std::vector 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 generate(); private: + // set up for Recursive Backtracking Maze Generation void generate_world(std::unique_ptr &world, Chunk start); + // Recursive Backtracking Maze Generation void generate_room(std::unique_ptr &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, 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, Chunk spawn_chunk); private: + // Factory objects for enemies Character::Spawner player_spawner{}; Character::Spawner boss_spawner{}; + // factory object, inverse frequency std::vector> 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}; }; } diff --git a/src/enemies.cpp b/src/enemies.cpp index 46137af..32a38c6 100644 --- a/src/enemies.cpp +++ b/src/enemies.cpp @@ -10,18 +10,25 @@ Tile CritteLogic::move() { if(this->is_aware_of_player) { // cache query_tile call for repeated use std::function 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) { diff --git a/src/main.cpp b/src/main.cpp index e490aa1..7532849 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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(player_data)) + // enemies and boss all use the same "AI" with different data .with_enemy(Character::make_factory(critte_data), 2) .with_enemy(Character::make_factory(critte_data), 3) .with_enemy(Character::make_factory(critte_data), 10) .with_boss(Character::make_factory(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 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{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; }