feat: documentation and cleanup pass over all objects
This commit is contained in:
parent
7ff9756dcc
commit
872f3f3fbf
|
@ -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) {
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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{};
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
49
src/main.cpp
49
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<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:
|
||||
if(!down) {
|
||||
return;
|
||||
} else if(game_state == GameState::GameOver || game_state == GameState::Escape) {
|
||||
game_state = GameState::MainMenu;
|
||||
break;
|
||||
case GameState::Escape:
|
||||
game_state = GameState::MainMenu;
|
||||
break;
|
||||
}
|
||||
},
|
||||
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:
|
||||
if(!down) {
|
||||
return;
|
||||
} else if(game_state == GameState::MainMenu) {
|
||||
game_state = GameState::Game;
|
||||
break;
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue