feat: implemented game

main
Sara 2025-01-27 03:32:08 +01:00
parent ed3e91aead
commit 5e4f6e70b6
24 changed files with 324 additions and 38 deletions

BIN
resources/inter.ttf Normal file

Binary file not shown.

View File

@ -8,12 +8,12 @@ AssetDB::AssetDB() {
this->index_assets();
}
void AssetDB::clean() {
void AssetDB::clean(bool force) {
for(size_t i{0}; i < this->loaded.size();) {
std::string asset_name{this->loaded.at(i)};
std::shared_ptr<Asset> asset{this->assets.at(asset_name)};
// usecount=1 means the asset is unused (other than the local use and the use in this->assets)
if(asset.use_count() == 2) {
// usecount of 2 means the asset is unused (other than the local use and the use in this->assets)
if(force || asset.use_count() <= 2) {
asset->unload();
this->loaded.erase(this->loaded.begin() + i);
} else ++i; // don't iterate when the asset is unloaded

View File

@ -15,7 +15,7 @@ public:
AssetDB();
~AssetDB() = default;
template <class AssetType> std::optional<std::shared_ptr<AssetType>> get_asset(std::string const &name);
void clean();
void clean(bool force);
private:
void index_assets();
void load(std::string asset_name);
@ -25,8 +25,10 @@ template <class AssetType> std::optional<std::shared_ptr<AssetType>> AssetDB::ge
if(!this->assets.contains(name)) return std::nullopt;
std::shared_ptr<AssetType> found{std::dynamic_pointer_cast<AssetType>(this->assets.at(name))};
if(found == nullptr) return std::nullopt;
if(!found->is_loaded())
if(!found->is_loaded()) {
found->load();
this->loaded.push_back(name);
}
return std::make_optional(found);
}
}

View File

@ -1,6 +1,7 @@
#include "asset_wrapper.h"
#include "core/canvas_engine.hpp"
#include <SDL2/SDL_image.h>
#include <SDL2/SDL_log.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_ttf.h>
#include <filesystem>
@ -15,15 +16,18 @@ Texture::~Texture() {
this->unload();
}
void Texture::unload() {
SDL_DestroyTexture(this->texture);
this->texture = nullptr;
}
void Texture::load() {
this->texture = IMG_LoadTexture(CanvasEngine::get_singleton()->get_render(), this->path.c_str());
}
void Texture::unload() {
if(this->is_loaded()) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "unloading font %s", this->path.c_str());
SDL_DestroyTexture(this->texture);
this->texture = nullptr;
}
}
bool Texture::is_loaded() const {
return this->texture != nullptr;
}
@ -39,15 +43,21 @@ Font::~Font() {
}
void Font::load() {
this->font = TTF_OpenFont(this->path.c_str(), 32);
this->font = TTF_OpenFont(this->path.c_str(), this->PT_SIZE);
}
void Font::unload() {
if(this->is_loaded()) {
TTF_CloseFont(this->font);
this->font = nullptr;
}
}
bool Font::is_loaded() const {
return this->font != nullptr;
}
TTF_Font *Font::get() {
return this->font;
}
}

View File

@ -32,6 +32,8 @@ public:
class Font : public Asset {
TTF_Font *font{nullptr};
public:
static const int PT_SIZE{64};
public:
Font(std::filesystem::path const &path);
~Font();

View File

@ -1,6 +1,8 @@
#include "canvas_engine.hpp"
#include "core/level.hpp"
#include <SDL2/SDL_pixels.h>
#include <SDL2/SDL_timer.h>
#include <SDL2/SDL_ttf.h>
#include <SDL2/SDL_video.h>
#include <cassert>
#include <SDL2/SDL_error.h>
@ -22,18 +24,29 @@ CanvasEngine *CanvasEngine::singleton_instance{nullptr};
CanvasEngine::CanvasEngine() {
if(SDL_Init(SDL_INIT_EVERYTHING) != 0) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to initialize SDL, SDL error: %s", SDL_GetError());
this->deinit_handled = true;
return;
}
if(IMG_Init(IMG_INIT_PNG | IMG_INIT_JXL) == 0) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to initialize SDL_image, error: %s", IMG_GetError());
SDL_Quit();
this->deinit_handled = true;
return;
}
if(TTF_Init() == -1) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to initialize SDL_ttf, error: %s", TTF_GetError());
IMG_Quit();
SDL_Quit();
this->deinit_handled = true;
return;
}
this->window = SDL_CreateWindow(PROJECTNAME, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 1000, 800, SDL_WINDOW_RESIZABLE | SDL_WINDOW_FULLSCREEN_DESKTOP);
if(this->window == nullptr) {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to create window, SDL error: %s", SDL_GetError());
IMG_Quit();
TTF_Quit();
SDL_Quit();
this->deinit_handled = true;
return;
}
this->render = SDL_CreateRenderer(this->window, -1, SDL_RENDERER_ACCELERATED);
@ -41,7 +54,9 @@ CanvasEngine::CanvasEngine() {
SDL_LogError(SDL_LOG_CATEGORY_ERROR, "Failed to initialize renderer, SDL error: %s", SDL_GetError());
SDL_DestroyWindow(this->window);
IMG_Quit();
TTF_Quit();
SDL_Quit();
this->deinit_handled = true;
return;
}
this->render_target = SDL_CreateTexture(this->render, SDL_PIXELFORMAT_ABGR8888, SDL_TEXTUREACCESS_TARGET, 1920, 1080);
@ -50,12 +65,22 @@ CanvasEngine::CanvasEngine() {
}
CanvasEngine::~CanvasEngine() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "CANVAS: Shutting down");
this->level.reset();
this->assets.clean(true);
if(!this->deinit_handled) {
SDL_DestroyRenderer(this->render);
SDL_DestroyWindow(this->window);
IMG_Quit();
TTF_Quit();
SDL_Quit();
}
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "CANVAS: Done shutting down");
}
void CanvasEngine::run(std::unique_ptr<Level> &level) {
if(!stay_open)
return;
assert(CanvasEngine::singleton_instance == nullptr && "Engine singleton instance already assigned, starting another instance is invalid");
// register as singleton
CanvasEngine::singleton_instance = this;
@ -82,6 +107,10 @@ void CanvasEngine::run(std::unique_ptr<Level> &level) {
} else {
SDL_Delay(2);
}
if(this->next_level) {
this->level = std::move(this->next_level);
this->level->instantiate();
}
}
assert(CanvasEngine::singleton_instance == this && "Engine singleton instance changed while game was running");
CanvasEngine::singleton_instance = nullptr;
@ -95,6 +124,10 @@ void CanvasEngine::set_target_delta_time(double target) {
this->target_delta_time = target;
}
void CanvasEngine::change_level(std::unique_ptr<Level> &level) {
this->next_level = std::move(level);
}
AssetDB &CanvasEngine::get_assets() {
return this->assets;
}

View File

@ -25,11 +25,13 @@ private:
CollisionWorld collision_world{};
InputMap input_map{}; //!< map of inputs to input callback objects
std::unique_ptr<Level> level;
std::unique_ptr<Level> next_level;
Uint64 last_frame_start_time{}; //!< time at start of last frame
Uint64 frame_start_time{}; //!< time at start of this frame
double delta_time{0.f}; //!< measured delta time
double target_delta_time{}; //!< delta time target
bool stay_open{false}; //!< application loop will continue so long as this is true
bool deinit_handled{false};
public:
CanvasEngine();
~CanvasEngine();
@ -41,6 +43,7 @@ public:
void run(std::unique_ptr<Level> &level);
void request_close();
void set_target_delta_time(double target);
void change_level(std::unique_ptr<Level> &level);
AssetDB &get_assets();
CollisionWorld &get_collision_world();
InputMap &get_input_map();

View File

@ -5,14 +5,22 @@
#include <cmath>
namespace ce {
Level::~Level() {
this->deinstantiate();
}
void Level::instantiate() {
std::unique_ptr<Node> constructed{this->construct()};
constructed->set_is_inside_tree(true);
this->root = std::move(constructed);
this->root->set_is_inside_tree(true);
this->root->set_level(this);
this->root->propagate_added();
}
void Level::deinstantiate() {
if(this->root) {
this->root->propagate_removed();
this->root.reset();
}
}
void Level::propagate_tick(double delta_time) {
@ -24,20 +32,17 @@ void Level::propagate_draw(SDL_Renderer *render) {
int w, h;
SDL_Window *window{SDL_RenderGetWindow(render)};
SDL_GetWindowSize(window, &w, &h);
ce::Transform const screen_transform{
.position = ce::Vecf::ZERO,
Transform const screen_transform{
.position = Vecf::ZERO,
.rotation = 0.f,
.scale = ce::Vecf::ONE * float(w),
.scale = Vecf::ONE * float(w),
};
float const ratio{float(h)/float(w)};
this->root->propagate_draw(render, Transform().translated({
0.5f / this->view_transform.scale.x,
0.5f / this->view_transform.scale.y * ratio
}) * this->view_transform * screen_transform);
this->root->propagate_draw_ui(render, Transform().translated({
0.5f / this->view_transform.scale.x,
0.5f / this->view_transform.scale.y * ratio
}) * screen_transform);
}) * view_transform * screen_transform);
this->root->propagate_draw_ui(render, screen_transform);
}
Node *Level::get_root() {

View File

@ -14,7 +14,7 @@ protected:
.scale = {1.f/10.f, 1.f/10.f},
};
public:
virtual ~Level() = default;
virtual ~Level();
void instantiate();
virtual Node::OwnedPtr construct() = 0;
void deinstantiate();

View File

@ -19,6 +19,7 @@ void Node::add_child(Node::OwnedPtr &child) {
this->children.push_back({child->get_name(), std::move(child)});
added->parent = this;
added->set_is_inside_tree(this->inside_tree);
added->set_level(this->level);
this->child_added.invoke(added);
added->propagate_added();
}

View File

@ -22,14 +22,12 @@ void Sprite::_draw(SDL_Renderer *render, ce::Transform const &view_transform) {
}
int w, h;
SDL_QueryTexture(this->texture->get(), NULL, NULL, &w, &h);
Transform transform{this->get_global_transform() * view_transform};
assert(transform.scale.x != 0 && transform.scale.y != 0); // !!!
ce::Vecf size{this->get_size()};
size.scale(view_transform.scale);
Transform const transform{this->get_global_transform() * view_transform};
ce::Vecf const size{this->get_size().scaled(view_transform.scale)};
assert(size.x != 0.f && size.y != 0.f);
//float fw(w), fh(h);
SDL_Rect src{.x=0, .y=0, .w=w, .h=h};
SDL_FRect dst{.x=transform.position.x - size.x/2.f, .y=transform.position.y - size.y/2.f, .w=size.x, .h=size.y};
SDL_Rect const src{.x=0, .y=0, .w=w, .h=h};
SDL_FRect const dst{.x=transform.position.x - size.x/2.f, .y=transform.position.y - size.y/2.f, .w=size.x, .h=size.y};
SDL_RenderCopyExF(render, this->texture->get(),
&src, &dst,
transform.rotation * 57.2958f,NULL,

69
src/core/ui_text.cpp Normal file
View File

@ -0,0 +1,69 @@
#include "ui_text.hpp"
#include "core/assets/asset_db.hpp"
#include "core/canvas_engine.hpp"
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_ttf.h>
#include <cassert>
namespace ce {
UiText::UiText(std::string const &name, std::string text, std::string font, SDL_Color color)
: Node2D(name)
, font{}
, text{text}
, color{color} {
std::optional<std::shared_ptr<Font>> font_asset{CanvasEngine::get_singleton()->get_assets().get_asset<Font>(font)};
if(font_asset.has_value())
this->font = font_asset.value();
}
UiText::~UiText() {
if(this->cached)
SDL_DestroyTexture(this->cached);
}
void UiText::_draw_ui(SDL_Renderer *render, Transform const &ui_transform) {
if(dirty) {
this->render();
}
if(this->cached == nullptr) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "No texture assigned");
this->set_visible(false);
return;
}
int w, h; SDL_QueryTexture(this->cached, NULL, NULL, &w, &h);
Transform const transform{this->get_global_transform() * ui_transform};
Vecf const size{Vecf{float(w) / float(h), 1.f}.scaled(transform.scale)};
assert(size.x != 0.f && size.y != 0.f);
SDL_Rect const src{.x=0, .y=0, .w=w, .h=h};
SDL_FRect const dst{.x=transform.position.x, .y=transform.position.y, .w=size.x, .h=size.y};
SDL_RenderCopyExF(render, this->cached,
&src, &dst,
transform.rotation * 57.2958f,NULL,
SDL_FLIP_NONE);
}
void UiText::set_text(std::string text) {
this->text = text;
this->render();
}
std::string const &UiText::get_text() const {
return this->text;
}
void UiText::render() {
if(this->cached != nullptr)
SDL_DestroyTexture(this->cached);
SDL_Renderer *render{CanvasEngine::get_singleton()->get_render()};
if(render == nullptr) {
this->dirty = true; // can't render right now, defer for later
return;
}
SDL_Surface *surf{TTF_RenderText_Blended(this->font->get(), this->text.c_str(), this->color)};
assert(surf != nullptr && "Failed to render text");
this->cached = SDL_CreateTextureFromSurface(render, surf);
assert(this->cached != nullptr && "Failed to pass rendered text to the GPU");
SDL_FreeSurface(surf);
this->dirty = false;
}
}

27
src/core/ui_text.hpp Normal file
View File

@ -0,0 +1,27 @@
#ifndef CORE_UI_TEXT_HPP
#define CORE_UI_TEXT_HPP
#include "core/assets/asset_wrapper.h"
#include "core/math/transform.hpp"
#include "core/node2d.hpp"
namespace ce {
class UiText : public Node2D {
std::shared_ptr<Font> font{nullptr};
std::string text{};
SDL_Color color{255, 255, 255, 255};
SDL_Texture *cached{nullptr};
bool dirty{true};
public:
UiText(std::string const &name, std::string text, std::string font, SDL_Color color);
~UiText();
virtual void _draw_ui(SDL_Renderer *render, Transform const &view_transform) override;
void set_text(std::string text);
std::string const &get_text() const;
protected:
void render();
};
}
#endif // !CORE_UI_TEXT_HPP

15
src/end_screen.cpp Normal file
View File

@ -0,0 +1,15 @@
#include "end_screen.hpp"
#include "core/math/transform.hpp"
#include "core/node2d.hpp"
#include "core/ui_text.hpp"
#include <format>
EndScreen::EndScreen(unsigned score)
: score{score} {}
ce::Node::OwnedPtr EndScreen::construct() {
ce::Node::OwnedPtr root{new ce::Node2D("root")};
root->create_child<ce::UiText>("score", std::format("Score: {}", this->score), "inter", SDL_Color{255, 255, 255, 255})
->set_global_transform(ce::Transform().translated({0.3f, 0.3f}).scaled({0.1f, 0.1f}));
return std::move(root);
}

13
src/end_screen.hpp Normal file
View File

@ -0,0 +1,13 @@
#ifndef END_SCREEN_HPP
#define END_SCREEN_HPP
#include "core/level.hpp"
class EndScreen : public ce::Level {
unsigned score;
public:
EndScreen(unsigned score);
virtual ce::Node::OwnedPtr construct() override;
};
#endif // !END_SCREEN_HPP

View File

@ -1,9 +1,16 @@
#include "level_1.hpp"
#include "core/canvas_engine.hpp"
#include "core/level.hpp"
#include "core/node.hpp"
#include "core/node2d.hpp"
#include "core/ui_text.hpp"
#include "end_screen.hpp"
#include "life_display.hpp"
#include "player.hpp"
#include "score_display.hpp"
#include "scrolling_ground.hpp"
#include "truck.hpp"
#include "spawner.hpp"
#include <SDL2/SDL_log.h>
ce::Node::OwnedPtr Level1::construct() {
ce::Node::OwnedPtr root{new ce::Node2D("root")};
@ -13,6 +20,23 @@ ce::Node::OwnedPtr Level1::construct() {
.scale = ce::Vecf::ONE
});
root->create_child<Player>();
root->create_child<Truck>(true);
root->create_child<Spawner>();
root->create_child<ScoreDisplay>()
->set_global_transform(ce::Transform().scaled({.05f, .05f}).translated({0.01f, 0.f}));
root->create_child<LifeDisplay>()
->set_global_transform(ce::Transform().scaled({.05f, .05f}).translated({.9f, 0.f}));
return std::move(root);
}
void Level1::add_score() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "add_score");
this->score_added.invoke(++this->score);
}
void Level1::lose_life() {
this->life_lost.invoke(--this->lives);
if(this->lives == 0) {
std::unique_ptr<ce::Level> next{ce::Level::make<EndScreen>(this->score)};
ce::CanvasEngine::get_singleton()->change_level(next);
}
}

View File

@ -2,8 +2,17 @@
#define LEVEL_1_HPP
#include "core/level.hpp"
#include "core/signal.hpp"
class Level1 : public ce::Level {
public:
unsigned score{0};
unsigned lives{3};
ce::Signal<unsigned> score_added{};
ce::Signal<unsigned> life_lost{};
public:
void add_score();
void lose_life();
virtual ce::Node::OwnedPtr construct() override;
};

19
src/life_display.cpp Normal file
View File

@ -0,0 +1,19 @@
#include "life_display.hpp"
#include "core/ui_text.hpp"
#include "level_1.hpp"
#include <format>
LifeDisplay::LifeDisplay()
: ce::UiText("score_display", "0", "inter", {255, 255, 255, 255}) {}
void LifeDisplay::_added() {
if(Level1 *level{dynamic_cast<Level1*>(this->get_level())}) {
level->life_lost.connect(ce::Callable<void, unsigned>::make(this, &LifeDisplay::_lives_changed));
this->_lives_changed(level->lives);
}
}
void LifeDisplay::_lives_changed(unsigned value) {
this->set_text(std::format("{}", value));
this->render();
}

13
src/life_display.hpp Normal file
View File

@ -0,0 +1,13 @@
#ifndef LIFE_DISPLAY_HPP
#define LIFE_DISPLAY_HPP
#include "core/ui_text.hpp"
class LifeDisplay : public ce::UiText {
public:
LifeDisplay();
virtual void _added() override;
void _lives_changed(unsigned new_value);
};
#endif // !LIFE_DISPLAY_HPP

View File

@ -1,4 +1,5 @@
#include "core/canvas_engine.hpp"
#include "core/level.hpp"
#include "level_1.hpp"
#include <SDL2/SDL_log.h>
@ -6,6 +7,6 @@ ce::CanvasEngine engine{};
int main(int argc [[maybe_unused]], char* argv [[maybe_unused]][]) {
SDL_LogSetAllPriority(SDL_LOG_PRIORITY_VERBOSE);
std::unique_ptr<ce::Level> level{std::make_unique<Level1>()};
std::unique_ptr<ce::Level> level{ce::Level::make<Level1>()};
engine.run(level);
}

View File

@ -1,4 +1,5 @@
#include "player.hpp"
#include "level_1.hpp"
#include "truck.hpp"
#include "core/callable.hpp"
#include "core/canvas_engine.hpp"
@ -72,6 +73,10 @@ void Player::_input_vertical_movement(ce::InputValue value) {
void Player::_on_overlap_enter(ce::CollisionShape *, ce::CollidableNode *other, ce::CollisionShape *shape) {
if(this->invincibility > 0.f)
return;
if(Truck *truck{dynamic_cast<Truck*>(other)})
if(Truck *truck{dynamic_cast<Truck*>(other)}) {
this->invincibility = 2.f;
if(Level1 *level{dynamic_cast<Level1*>(this->get_level())}) {
level->lose_life();
}
}
}

18
src/score_display.cpp Normal file
View File

@ -0,0 +1,18 @@
#include "score_display.hpp"
#include "core/ui_text.hpp"
#include "level_1.hpp"
#include <format>
ScoreDisplay::ScoreDisplay()
: ce::UiText("score_display", "0", "inter", {255, 255, 255, 255}) {}
void ScoreDisplay::_added() {
if(Level1 *level{dynamic_cast<Level1*>(this->get_level())}) {
level->score_added.connect(ce::Callable<void, unsigned>::make(this, &ScoreDisplay::_score_changed));
}
}
void ScoreDisplay::_score_changed(unsigned value) {
this->set_text(std::format("{}", value));
this->render();
}

13
src/score_display.hpp Normal file
View File

@ -0,0 +1,13 @@
#ifndef SCORE_DISPLAY_HPP
#define SCORE_DISPLAY_HPP
#include "core/ui_text.hpp"
class ScoreDisplay : public ce::UiText {
public:
ScoreDisplay();
virtual void _added() override;
void _score_changed(unsigned new_value);
};
#endif // !SCORE_DISPLAY_HPP

View File

@ -2,6 +2,7 @@
#include "core/math/transform.hpp"
#include "core/sprite.hpp"
#include "core/collision_shape.hpp"
#include "level_1.hpp"
#include <algorithm>
#include <cmath>
#include <SDL2/SDL_log.h>
@ -29,6 +30,11 @@ void Truck::_tick(double delta) {
trans.position.x = std::clamp(trans.position.x, -LIMITS.x, LIMITS.x);
trans.position.y = std::max(trans.position.y, -LIMITS.y);
this->set_global_transform(trans);
if(transform.position.y > 4.5f)
if(transform.position.y > 4.5f) {
this->flag_for_deletion();
if(Level1 *level{dynamic_cast<Level1*>(this->get_level())}) {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "adding score");
level->add_score();
}
}
}