From aecd5a46ed9d8412734e0a25db3ba7498846e58a Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Tue, 9 Jun 2026 12:25:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=90=AD=E5=BB=BA=20LightGame=20=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E6=A1=86=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CMakeLists.txt | 25 + package-lock.json | 6 + src/Apps/LightGame/CMakeLists.txt | 16 + src/Apps/LightGame/src/engine/Camera2D.cpp | 98 ++++ src/Apps/LightGame/src/engine/Camera2D.h | 37 ++ src/Apps/LightGame/src/engine/GameObject.h | 83 ++++ .../LightGame/src/engine/GameStateManager.cpp | 167 +++++++ .../LightGame/src/engine/GameStateManager.h | 78 ++++ src/Apps/LightGame/src/engine/Level.cpp | 138 ++++++ src/Apps/LightGame/src/engine/Level.h | 72 +++ src/Apps/LightGame/src/engine/LevelData.h | 40 ++ src/Apps/LightGame/src/engine/LevelLoader.cpp | 97 ++++ src/Apps/LightGame/src/engine/LevelLoader.h | 13 + .../LightGame/src/engine/LevelRenderer.cpp | 80 ++++ src/Apps/LightGame/src/engine/LevelRenderer.h | 28 ++ .../LightGame/src/engine/LightGameApp.cpp | 250 ++++++++++ src/Apps/LightGame/src/engine/LightGameApp.h | 76 +++ src/Apps/LightGame/src/engine/Physics2D.cpp | 184 ++++++++ src/Apps/LightGame/src/engine/Physics2D.h | 61 +++ .../LightGame/src/engine/PlayerController.cpp | 289 ++++++++++++ .../LightGame/src/engine/PlayerController.h | 88 ++++ src/Apps/LightGame/src/levels/Level1Data.h | 58 +++ src/Apps/LightGame/src/levels/Level2Data.h | 68 +++ src/Apps/LightGame/src/levels/Level3Data.h | 70 +++ src/Apps/LightGame/src/main.cpp | 131 ++++++ .../src/systems/LightEffectSystem.cpp | 56 +++ .../LightGame/src/systems/LightEffectSystem.h | 17 + src/Core/Platform/DefaultHardware.h | 4 + src/Core/Platform/IPhotoSensor.h | 21 + src/Core/Platform/LinuxPhotoSensor.cpp | 168 +++++++ src/Core/Platform/LinuxPhotoSensor.h | 28 ++ src/Core/Platform/SdlPhotoSensor.cpp | 117 +++++ src/Core/Platform/SdlPhotoSensor.h | 33 ++ tests/game_engine_tests.cpp | 439 ++++++++++++++++++ tests/render_pipeline_tests.cpp | 28 ++ 35 files changed, 3164 insertions(+) create mode 100644 package-lock.json create mode 100644 src/Apps/LightGame/CMakeLists.txt create mode 100644 src/Apps/LightGame/src/engine/Camera2D.cpp create mode 100644 src/Apps/LightGame/src/engine/Camera2D.h create mode 100644 src/Apps/LightGame/src/engine/GameObject.h create mode 100644 src/Apps/LightGame/src/engine/GameStateManager.cpp create mode 100644 src/Apps/LightGame/src/engine/GameStateManager.h create mode 100644 src/Apps/LightGame/src/engine/Level.cpp create mode 100644 src/Apps/LightGame/src/engine/Level.h create mode 100644 src/Apps/LightGame/src/engine/LevelData.h create mode 100644 src/Apps/LightGame/src/engine/LevelLoader.cpp create mode 100644 src/Apps/LightGame/src/engine/LevelLoader.h create mode 100644 src/Apps/LightGame/src/engine/LevelRenderer.cpp create mode 100644 src/Apps/LightGame/src/engine/LevelRenderer.h create mode 100644 src/Apps/LightGame/src/engine/LightGameApp.cpp create mode 100644 src/Apps/LightGame/src/engine/LightGameApp.h create mode 100644 src/Apps/LightGame/src/engine/Physics2D.cpp create mode 100644 src/Apps/LightGame/src/engine/Physics2D.h create mode 100644 src/Apps/LightGame/src/engine/PlayerController.cpp create mode 100644 src/Apps/LightGame/src/engine/PlayerController.h create mode 100644 src/Apps/LightGame/src/levels/Level1Data.h create mode 100644 src/Apps/LightGame/src/levels/Level2Data.h create mode 100644 src/Apps/LightGame/src/levels/Level3Data.h create mode 100644 src/Apps/LightGame/src/main.cpp create mode 100644 src/Apps/LightGame/src/systems/LightEffectSystem.cpp create mode 100644 src/Apps/LightGame/src/systems/LightEffectSystem.h create mode 100644 src/Core/Platform/IPhotoSensor.h create mode 100644 src/Core/Platform/LinuxPhotoSensor.cpp create mode 100644 src/Core/Platform/LinuxPhotoSensor.h create mode 100644 src/Core/Platform/SdlPhotoSensor.cpp create mode 100644 src/Core/Platform/SdlPhotoSensor.h create mode 100644 tests/game_engine_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2fd9351..3faac46 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -31,6 +31,7 @@ set(CORE_SOURCES if(USE_FRAMEBUFFER) list(APPEND CORE_SOURCES src/Core/Platform/FBDisplay.cpp + src/Core/Platform/LinuxPhotoSensor.cpp ) else() list(APPEND CORE_SOURCES @@ -39,6 +40,7 @@ else() src/Core/Platform/SdlAudioOutput.cpp src/Core/Platform/SdlKeyboardButtonInput.cpp src/Core/Platform/SdlPointerInput.cpp + src/Core/Platform/SdlPhotoSensor.cpp ) endif() @@ -135,6 +137,7 @@ endfunction() add_subdirectory(src/Apps/Game) add_subdirectory(src/Apps/Demo) +add_subdirectory(src/Apps/LightGame) if(BUILD_TESTING AND NOT USE_FRAMEBUFFER) add_executable(render_pipeline_tests @@ -143,17 +146,37 @@ if(BUILD_TESTING AND NOT USE_FRAMEBUFFER) target_link_libraries(render_pipeline_tests PRIVATE imx6u_core) target_include_directories(render_pipeline_tests PRIVATE ${CORE_INCLUDE_DIRS}) + add_executable(game_engine_tests + tests/game_engine_tests.cpp + src/Apps/LightGame/src/engine/Level.cpp + src/Apps/LightGame/src/engine/Camera2D.cpp + src/Apps/LightGame/src/engine/Physics2D.cpp + src/Apps/LightGame/src/engine/LevelLoader.cpp + src/Apps/LightGame/src/systems/LightEffectSystem.cpp + ) + target_include_directories(game_engine_tests PRIVATE + src/Apps/LightGame/src/engine + src/Apps/LightGame/src/systems + ${CORE_INCLUDE_DIRS} + ) + if(CMAKE_CONFIGURATION_TYPES) foreach(config ${CMAKE_CONFIGURATION_TYPES}) string(TOUPPER "${config}" config_upper) set_target_properties(render_pipeline_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${CMAKE_BINARY_DIR}/${config}" ) + set_target_properties(game_engine_tests PROPERTIES + RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${CMAKE_BINARY_DIR}/${config}" + ) endforeach() else() set_target_properties(render_pipeline_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" ) + set_target_properties(game_engine_tests PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" + ) endif() if(WIN32) @@ -166,7 +189,9 @@ if(BUILD_TESTING AND NOT USE_FRAMEBUFFER) if(MSVC) target_compile_options(render_pipeline_tests PRIVATE /utf-8 /W3) + target_compile_options(game_engine_tests PRIVATE /utf-8 /W3) endif() add_test(NAME render_pipeline_tests COMMAND render_pipeline_tests) + add_test(NAME game_engine_tests COMMAND game_engine_tests) endif() diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..620e954 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "IMX6U-Game", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} diff --git a/src/Apps/LightGame/CMakeLists.txt b/src/Apps/LightGame/CMakeLists.txt new file mode 100644 index 0000000..64b9cfb --- /dev/null +++ b/src/Apps/LightGame/CMakeLists.txt @@ -0,0 +1,16 @@ +set(LIGHTGAME_SOURCES + src/engine/Level.cpp + src/engine/Camera2D.cpp + src/engine/Physics2D.cpp + src/engine/PlayerController.cpp + src/engine/LevelLoader.cpp + src/engine/LevelRenderer.cpp + src/engine/GameStateManager.cpp + src/engine/LightGameApp.cpp + src/systems/LightEffectSystem.cpp + src/main.cpp +) + +add_executable(IMX6U-LightGame ${LIGHTGAME_SOURCES}) +target_include_directories(IMX6U-LightGame PRIVATE src/engine src/systems src/levels) +imx6u_configure_app_target(IMX6U-LightGame) diff --git a/src/Apps/LightGame/src/engine/Camera2D.cpp b/src/Apps/LightGame/src/engine/Camera2D.cpp new file mode 100644 index 0000000..e38d78c --- /dev/null +++ b/src/Apps/LightGame/src/engine/Camera2D.cpp @@ -0,0 +1,98 @@ +#include "Camera2D.h" +#include + +namespace LightGame +{ + Camera2D::Camera2D() + : position_(), + screen_width_(800), + screen_height_(480), + dead_zone_x_(60), + dead_zone_y_(40), + smooth_shift_(3), + level_bounds_() + { + } + + void Camera2D::configure(int32_t screen_width, int32_t screen_height) + { + screen_width_ = screen_width; + screen_height_ = screen_height; + } + + void Camera2D::set_level_bounds(const LevelBounds& bounds) + { + level_bounds_ = bounds; + } + + void Camera2D::set_dead_zone(int32_t dx, int32_t dy) + { + dead_zone_x_ = dx; + dead_zone_y_ = dy; + } + + void Camera2D::follow(const Math::Vector2Int& target) + { + const int32_t center_x = position_.x + screen_width_ / 2; + const int32_t center_y = position_.y + screen_height_ / 2; + const int32_t dx = target.x - center_x; + const int32_t dy = target.y - center_y; + + if (dx > dead_zone_x_ || dx < -dead_zone_x_) + { + const int32_t shift = dx >> smooth_shift_; + if (shift != 0) + { + position_.x += shift; + } + else + { + position_.x += (dx > 0) ? 1 : -1; + } + } + + if (dy > dead_zone_y_ || dy < -dead_zone_y_) + { + const int32_t shift = dy >> smooth_shift_; + if (shift != 0) + { + position_.y += shift; + } + else + { + position_.y += (dy > 0) ? 1 : -1; + } + } + + const int32_t max_x = level_bounds_.max_x - screen_width_; + const int32_t max_y = level_bounds_.max_y - screen_height_; + + if (position_.x < level_bounds_.min_x) position_.x = level_bounds_.min_x; + if (position_.y < level_bounds_.min_y) position_.y = level_bounds_.min_y; + if (position_.x > max_x) position_.x = max_x; + if (position_.y > max_y) position_.y = max_y; + + if (level_bounds_.max_x - level_bounds_.min_x <= screen_width_) + { + position_.x = level_bounds_.min_x; + } + if (level_bounds_.max_y - level_bounds_.min_y <= screen_height_) + { + position_.y = level_bounds_.min_y; + } + } + + void Camera2D::snap_to(const Math::Vector2Int& target) + { + position_.x = target.x - screen_width_ / 2; + position_.y = target.y - screen_height_ / 2; + + const int32_t max_x = level_bounds_.max_x - screen_width_; + const int32_t max_y = level_bounds_.max_y - screen_height_; + + if (position_.x < level_bounds_.min_x) position_.x = level_bounds_.min_x; + if (position_.y < level_bounds_.min_y) position_.y = level_bounds_.min_y; + if (position_.x > max_x) position_.x = max_x; + if (position_.y > max_y) position_.y = max_y; + } +} diff --git a/src/Apps/LightGame/src/engine/Camera2D.h b/src/Apps/LightGame/src/engine/Camera2D.h new file mode 100644 index 0000000..faa14a5 --- /dev/null +++ b/src/Apps/LightGame/src/engine/Camera2D.h @@ -0,0 +1,37 @@ +#pragma once + +#include +#include "Vector2.h" +#include "Camera.h" +#include "Level.h" + +namespace LightGame +{ + class Camera2D + { + private: + Math::Vector2Int position_; + int32_t screen_width_; + int32_t screen_height_; + int32_t dead_zone_x_; + int32_t dead_zone_y_; + int32_t smooth_shift_; + LevelBounds level_bounds_; + + public: + Camera2D(); + + void configure(int32_t screen_width, int32_t screen_height); + void set_level_bounds(const LevelBounds& bounds); + void set_dead_zone(int32_t dx, int32_t dy); + + void follow(const Math::Vector2Int& target); + void snap_to(const Math::Vector2Int& target); + + Math::Vector2Int get_position() const { return position_; } + int32_t get_x() const { return position_.x; } + int32_t get_y() const { return position_.y; } + int32_t get_screen_width() const { return screen_width_; } + int32_t get_screen_height() const { return screen_height_; } + }; +} diff --git a/src/Apps/LightGame/src/engine/GameObject.h b/src/Apps/LightGame/src/engine/GameObject.h new file mode 100644 index 0000000..e143ec6 --- /dev/null +++ b/src/Apps/LightGame/src/engine/GameObject.h @@ -0,0 +1,83 @@ +#pragma once + +#include +#include "Vector2.h" +#include "BoundingBox.h" +#include "Sprite.h" + +namespace LightGame +{ + enum class GameObjectType + { + Player, + StaticPlatform, + LightPlatform, + ShadowPlatform, + Hazard, + Collectible, + Trigger, + Door + }; + + enum class TriggerAction + { + None, + LevelComplete, + Checkpoint, + Death + }; + + struct LightThreshold + { + uint16_t min_level; + uint16_t max_level; + + LightThreshold() : min_level(0), max_level(0) {} + LightThreshold(uint16_t min_l, uint16_t max_l) : min_level(min_l), max_level(max_l) {} + }; + + struct GameObject + { + uint32_t id; + GameObjectType type; + Math::Vector2Int position; + Math::Vector2Int velocity; + const RenderData::Sprite* sprite; + RenderData::BoundingBox2D collider; + bool active; + bool solid; + bool flip_h; + uint16_t anim_frame; + uint16_t anim_timer_ms; + + LightThreshold light_threshold; + TriggerAction trigger_action; + uint32_t trigger_target_id; + + GameObject() + : id(0), + type(GameObjectType::StaticPlatform), + position(), + velocity(), + sprite(nullptr), + collider(), + active(true), + solid(true), + flip_h(false), + anim_frame(0), + anim_timer_ms(0), + light_threshold(), + trigger_action(TriggerAction::None), + trigger_target_id(0) + { + } + + RenderData::BoundingBox2D get_world_collider() const + { + return RenderData::BoundingBox2D( + Math::Vector2Int(position.x + collider.min.x, position.y + collider.min.y), + Math::Vector2Int(position.x + collider.max.x, position.y + collider.max.y) + ); + } + }; +} diff --git a/src/Apps/LightGame/src/engine/GameStateManager.cpp b/src/Apps/LightGame/src/engine/GameStateManager.cpp new file mode 100644 index 0000000..34a39d1 --- /dev/null +++ b/src/Apps/LightGame/src/engine/GameStateManager.cpp @@ -0,0 +1,167 @@ +#include "GameStateManager.h" +#include "DrawContext.h" +#include "BitmapFont.h" +#include "ButtonInput.h" +#include + +namespace LightGame +{ + GameStateManager::GameStateManager() + : state_(GameState::Title), + hud_(), + title_blink_ms_(0), + title_show_prompt_(true) + { + } + + void GameStateManager::update(GameState current_state, const Platform::IButtonInput* button, uint32_t dt_ms) + { + state_ = current_state; + + switch (state_) + { + case GameState::Title: + title_blink_ms_ += dt_ms; + if (title_blink_ms_ >= 500) + { + title_blink_ms_ -= 500; + title_show_prompt_ = !title_show_prompt_; + } + break; + + case GameState::Playing: + if (button && button->was_pressed()) + { + state_ = GameState::Paused; + } + break; + + case GameState::Paused: + if (button && button->was_pressed()) + { + state_ = GameState::Playing; + } + break; + + case GameState::GameOver: + case GameState::LevelComplete: + break; + } + } + + void GameStateManager::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t screen_w, int32_t screen_h) + { + switch (state_) + { + case GameState::Title: + draw_title(ctx, font, screen_w, screen_h); + break; + case GameState::Playing: + draw_hud(ctx, font, screen_w, screen_h); + break; + case GameState::Paused: + draw_hud(ctx, font, screen_w, screen_h); + draw_pause(ctx, font, screen_w, screen_h); + break; + case GameState::GameOver: + draw_game_over(ctx, font, screen_w, screen_h); + break; + case GameState::LevelComplete: + draw_level_complete(ctx, font, screen_w, screen_h); + break; + } + } + + void GameStateManager::draw_title(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t w, int32_t h) + { + ctx.fill_rect(0, 0, w, h, RenderData::Color(0, 0, 40, 255)); + + const char* title = "LIGHT QUEST"; + const int32_t title_x = (w - 11 * 8) / 2; + const int32_t title_y = h / 3; + ctx.draw_text(font, title_x, title_y, RenderData::Color(255, 255, 100, 255), title); + + if (title_show_prompt_) + { + const char* prompt = "PRESS TO START"; + const int32_t prompt_x = (w - 14 * 8) / 2; + ctx.draw_text(font, prompt_x, title_y + 40, RenderData::Color::White(), prompt); + } + } + + void GameStateManager::draw_hud(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t w, int32_t) + { + draw_light_bar(ctx, 8, 8, 80, 8, hud_.light_level, hud_.light_max); + + char buf[32]; + snprintf(buf, sizeof(buf), "LV%d", hud_.current_level); + ctx.draw_text(font, w - 48, 8, RenderData::Color::White(), buf); + + snprintf(buf, sizeof(buf), "x%d", hud_.collectibles); + ctx.draw_text(font, 8, 24, RenderData::Color(255, 255, 0, 255), buf); + + for (int32_t i = 0; i < hud_.lives; ++i) + { + ctx.fill_rect(8 + i * 12, 40, 8, 8, RenderData::Color::Red()); + } + } + + void GameStateManager::draw_pause(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t w, int32_t h) + { + ctx.fill_rect(w / 4, h / 3, w / 2, h / 3, RenderData::Color(0, 0, 0, 200)); + + const char* text = "PAUSED"; + const int32_t tx = (w - 6 * 8) / 2; + const int32_t ty = h / 2 - 4; + ctx.draw_text(font, tx, ty, RenderData::Color::White(), text); + } + + void GameStateManager::draw_game_over(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t w, int32_t h) + { + ctx.fill_rect(0, 0, w, h, RenderData::Color(40, 0, 0, 255)); + + const char* text = "GAME OVER"; + const int32_t tx = (w - 9 * 8) / 2; + const int32_t ty = h / 2 - 4; + ctx.draw_text(font, tx, ty, RenderData::Color::Red(), text); + + const char* retry = "PRESS TO RETRY"; + const int32_t rx = (w - 14 * 8) / 2; + ctx.draw_text(font, rx, ty + 24, RenderData::Color::White(), retry); + } + + void GameStateManager::draw_level_complete(Core::DrawContext& ctx, const RenderData::BitmapFont& font, + int32_t w, int32_t h) + { + ctx.fill_rect(0, 0, w, h, RenderData::Color(0, 40, 0, 255)); + + const char* text = "LEVEL CLEAR!"; + const int32_t tx = (w - 12 * 8) / 2; + const int32_t ty = h / 2 - 4; + ctx.draw_text(font, tx, ty, RenderData::Color(100, 255, 100, 255), text); + + char buf[32]; + snprintf(buf, sizeof(buf), "SCORE: %d", hud_.collectibles); + const int32_t sx = (w - 10 * 8) / 2; + ctx.draw_text(font, sx, ty + 24, RenderData::Color::White(), buf); + } + + void GameStateManager::draw_light_bar(Core::DrawContext& ctx, int32_t x, int32_t y, + int32_t w, int32_t h, uint16_t level, uint16_t max_level) + { + ctx.fill_rect(x, y, w, h, RenderData::Color(60, 60, 60, 255)); + + const int32_t fill_w = (max_level > 0) ? (static_cast(level) * (w - 2)) / max_level : 0; + if (fill_w > 0) + { + const uint8_t r = static_cast(255 * (max_level - level) / max_level); + const uint8_t g = static_cast(255 * level / max_level); + ctx.fill_rect(x + 1, y + 1, fill_w, h - 2, RenderData::Color(r, g, 0, 255)); + } + } +} diff --git a/src/Apps/LightGame/src/engine/GameStateManager.h b/src/Apps/LightGame/src/engine/GameStateManager.h new file mode 100644 index 0000000..4d4fc22 --- /dev/null +++ b/src/Apps/LightGame/src/engine/GameStateManager.h @@ -0,0 +1,78 @@ +#pragma once + +#include + +namespace Core +{ + class DrawContext; +} + +namespace RenderData +{ + struct BitmapFont; +} + +namespace Platform +{ + class IButtonInput; +} + +namespace LightGame +{ + enum class GameState + { + Title, + Playing, + Paused, + GameOver, + LevelComplete + }; + + struct HudData + { + uint16_t light_level; + uint16_t light_max; + int32_t collectibles; + int32_t current_level; + int32_t lives; + + HudData() + : light_level(0), + light_max(4095), + collectibles(0), + current_level(1), + lives(3) + { + } + }; + + class GameStateManager + { + private: + GameState state_; + HudData hud_; + uint32_t title_blink_ms_; + bool title_show_prompt_; + + public: + GameStateManager(); + + void update(GameState current_state, const Platform::IButtonInput* button, uint32_t dt_ms); + void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t screen_w, int32_t screen_h); + + void set_state(GameState state) { state_ = state; } + GameState get_state() const { return state_; } + + HudData& get_hud() { return hud_; } + const HudData& get_hud() const { return hud_; } + + private: + void draw_title(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t w, int32_t h); + void draw_hud(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t w, int32_t h); + void draw_pause(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t w, int32_t h); + void draw_game_over(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t w, int32_t h); + void draw_level_complete(Core::DrawContext& ctx, const RenderData::BitmapFont& font, int32_t w, int32_t h); + void draw_light_bar(Core::DrawContext& ctx, int32_t x, int32_t y, int32_t w, int32_t h, + uint16_t level, uint16_t max_level); + }; +} diff --git a/src/Apps/LightGame/src/engine/Level.cpp b/src/Apps/LightGame/src/engine/Level.cpp new file mode 100644 index 0000000..891d790 --- /dev/null +++ b/src/Apps/LightGame/src/engine/Level.cpp @@ -0,0 +1,138 @@ +#include "Level.h" +#include + +namespace LightGame +{ + Level::Level() + : tilemap_(), + bounds_(), + spawn_point_(), + next_id_(1) + { + } + + uint32_t Level::add_object(const GameObject& obj) + { + GameObject copy = obj; + copy.id = next_id_++; + objects_.push_back(copy); + return copy.id; + } + + GameObject* Level::get_object(uint32_t id) + { + for (size_t i = 0; i < objects_.size(); ++i) + { + if (objects_[i].id == id) + { + return &objects_[i]; + } + } + return nullptr; + } + + const GameObject* Level::get_object(uint32_t id) const + { + for (size_t i = 0; i < objects_.size(); ++i) + { + if (objects_[i].id == id) + { + return &objects_[i]; + } + } + return nullptr; + } + + void Level::remove_object(uint32_t id) + { + objects_.erase( + std::remove_if(objects_.begin(), objects_.end(), + [id](const GameObject& o) { return o.id == id; }), + objects_.end()); + } + + std::vector Level::get_objects_by_type(GameObjectType type) + { + std::vector result; + for (size_t i = 0; i < objects_.size(); ++i) + { + if (objects_[i].type == type && objects_[i].active) + { + result.push_back(&objects_[i]); + } + } + return result; + } + + std::vector Level::get_objects_by_type(GameObjectType type) const + { + std::vector result; + for (size_t i = 0; i < objects_.size(); ++i) + { + if (objects_[i].type == type && objects_[i].active) + { + result.push_back(&objects_[i]); + } + } + return result; + } + + std::vector Level::query_region(const RenderData::BoundingBox2D& region) + { + std::vector result; + for (size_t i = 0; i < objects_.size(); ++i) + { + if (!objects_[i].active) + { + continue; + } + + const RenderData::BoundingBox2D world_box = objects_[i].get_world_collider(); + + if (world_box.max.x > region.min.x && world_box.min.x < region.max.x && + world_box.max.y > region.min.y && world_box.min.y < region.max.y) + { + result.push_back(&objects_[i]); + } + } + return result; + } + + std::vector Level::query_region(const RenderData::BoundingBox2D& region) const + { + std::vector result; + for (size_t i = 0; i < objects_.size(); ++i) + { + if (!objects_[i].active) + { + continue; + } + + const RenderData::BoundingBox2D world_box = objects_[i].get_world_collider(); + + if (world_box.max.x > region.min.x && world_box.min.x < region.max.x && + world_box.max.y > region.min.y && world_box.min.y < region.max.y) + { + result.push_back(&objects_[i]); + } + } + return result; + } + + void Level::add_checkpoint(const Checkpoint& cp) + { + checkpoints_.push_back(cp); + } + + Math::Vector2Int Level::get_active_checkpoint() const + { + for (size_t i = checkpoints_.size(); i > 0; --i) + { + if (checkpoints_[i - 1].activated) + { + return checkpoints_[i - 1].position; + } + } + return spawn_point_; + } +} diff --git a/src/Apps/LightGame/src/engine/Level.h b/src/Apps/LightGame/src/engine/Level.h new file mode 100644 index 0000000..e92772b --- /dev/null +++ b/src/Apps/LightGame/src/engine/Level.h @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include "GameObject.h" +#include "Tilemap.h" + +namespace LightGame +{ + struct LevelBounds + { + int32_t min_x; + int32_t min_y; + int32_t max_x; + int32_t max_y; + + LevelBounds() : min_x(0), min_y(0), max_x(0), max_y(0) {} + LevelBounds(int32_t x0, int32_t y0, int32_t x1, int32_t y1) + : min_x(x0), min_y(y0), max_x(x1), max_y(y1) {} + }; + + struct Checkpoint + { + Math::Vector2Int position; + bool activated; + + Checkpoint() : position(), activated(false) {} + Checkpoint(int32_t x, int32_t y) : position(x, y), activated(false) {} + }; + + class Level + { + private: + std::vector objects_; + std::vector checkpoints_; + RenderData::Tilemap tilemap_; + LevelBounds bounds_; + Math::Vector2Int spawn_point_; + uint32_t next_id_; + + public: + Level(); + + uint32_t add_object(const GameObject& obj); + GameObject* get_object(uint32_t id); + const GameObject* get_object(uint32_t id) const; + void remove_object(uint32_t id); + + std::vector get_objects_by_type(GameObjectType type); + std::vector get_objects_by_type(GameObjectType type) const; + + std::vector query_region(const RenderData::BoundingBox2D& region); + std::vector query_region(const RenderData::BoundingBox2D& region) const; + + void set_tilemap(const RenderData::Tilemap& tilemap) { tilemap_ = tilemap; } + const RenderData::Tilemap& get_tilemap() const { return tilemap_; } + + void set_bounds(const LevelBounds& bounds) { bounds_ = bounds; } + const LevelBounds& get_bounds() const { return bounds_; } + + void set_spawn_point(const Math::Vector2Int& point) { spawn_point_ = point; } + const Math::Vector2Int& get_spawn_point() const { return spawn_point_; } + + void add_checkpoint(const Checkpoint& cp); + const std::vector& get_checkpoints() const { return checkpoints_; } + Math::Vector2Int get_active_checkpoint() const; + + std::vector& get_all_objects() { return objects_; } + const std::vector& get_all_objects() const { return objects_; } + size_t object_count() const { return objects_.size(); } + }; +} diff --git a/src/Apps/LightGame/src/engine/LevelData.h b/src/Apps/LightGame/src/engine/LevelData.h new file mode 100644 index 0000000..0183b68 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LevelData.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include "Vector2.h" +#include "GameObject.h" +#include "Tilemap.h" +#include "Image.h" + +namespace LightGame +{ + struct ObjectSpawn + { + GameObjectType type; + int32_t tile_x; + int32_t tile_y; + uint16_t light_min; + uint16_t light_max; + TriggerAction trigger_action; + uint32_t trigger_target; + }; + + struct LevelData + { + const uint16_t* tiles; + int32_t map_width; + int32_t map_height; + int32_t tile_size; + const RenderData::Image* atlas; + int32_t atlas_columns; + + const ObjectSpawn* spawns; + int32_t spawn_count; + + Math::Vector2Int spawn_point; + int32_t bounds_min_x; + int32_t bounds_min_y; + int32_t bounds_max_x; + int32_t bounds_max_y; + }; +} diff --git a/src/Apps/LightGame/src/engine/LevelLoader.cpp b/src/Apps/LightGame/src/engine/LevelLoader.cpp new file mode 100644 index 0000000..81403f0 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LevelLoader.cpp @@ -0,0 +1,97 @@ +#include "LevelLoader.h" + +namespace LightGame +{ + void LevelLoader::load(Level& level, const LevelData& data) + { + RenderData::Tilemap tilemap; + tilemap.tiles = data.tiles; + tilemap.width = data.map_width; + tilemap.height = data.map_height; + tilemap.tile_w = data.tile_size; + tilemap.tile_h = data.tile_size; + tilemap.atlas = data.atlas; + tilemap.atlas_columns = data.atlas_columns; + level.set_tilemap(tilemap); + + level.set_bounds(LevelBounds( + data.bounds_min_x, + data.bounds_min_y, + data.bounds_max_x, + data.bounds_max_y)); + + level.set_spawn_point(data.spawn_point); + + for (int32_t i = 0; i < data.spawn_count; ++i) + { + const ObjectSpawn& spawn = data.spawns[i]; + GameObject obj; + obj.type = spawn.type; + obj.position = Math::Vector2Int( + spawn.tile_x * data.tile_size, + spawn.tile_y * data.tile_size); + + switch (spawn.type) + { + case GameObjectType::StaticPlatform: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(data.tile_size, data.tile_size)); + obj.solid = true; + break; + + case GameObjectType::LightPlatform: + case GameObjectType::ShadowPlatform: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(data.tile_size, data.tile_size)); + obj.solid = (spawn.type == GameObjectType::ShadowPlatform); + obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max); + break; + + case GameObjectType::Door: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(data.tile_size, data.tile_size * 2)); + obj.solid = true; + obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max); + break; + + case GameObjectType::Hazard: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(data.tile_size, data.tile_size)); + obj.solid = false; + break; + + case GameObjectType::Collectible: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(4, 4), + Math::Vector2Int(data.tile_size - 4, data.tile_size - 4)); + obj.solid = false; + break; + + case GameObjectType::Trigger: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(data.tile_size, data.tile_size)); + obj.solid = false; + obj.trigger_action = spawn.trigger_action; + obj.trigger_target_id = spawn.trigger_target; + break; + + case GameObjectType::Player: + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(4, 0), + Math::Vector2Int(12, 16)); + obj.solid = false; + break; + + default: + break; + } + + level.add_object(obj); + } + } +} diff --git a/src/Apps/LightGame/src/engine/LevelLoader.h b/src/Apps/LightGame/src/engine/LevelLoader.h new file mode 100644 index 0000000..88f3e59 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LevelLoader.h @@ -0,0 +1,13 @@ +#pragma once + +#include "Level.h" +#include "LevelData.h" + +namespace LightGame +{ + class LevelLoader + { + public: + static void load(Level& level, const LevelData& data); + }; +} diff --git a/src/Apps/LightGame/src/engine/LevelRenderer.cpp b/src/Apps/LightGame/src/engine/LevelRenderer.cpp new file mode 100644 index 0000000..5b5108d --- /dev/null +++ b/src/Apps/LightGame/src/engine/LevelRenderer.cpp @@ -0,0 +1,80 @@ +#include "LevelRenderer.h" +#include "DrawContext.h" +#include "BitmapFont.h" + +namespace LightGame +{ + void LevelRenderer::draw(Core::DrawContext& ctx, const Level& level, const Camera2D& camera, + const LightEffectSystem& light_system, uint16_t light_level) + { + const RenderData::Tilemap& tilemap = level.get_tilemap(); + if (tilemap.tiles != nullptr && tilemap.atlas != nullptr) + { + ctx.draw_tilemap(tilemap, + -camera.get_x(), -camera.get_y(), + camera.get_screen_width(), camera.get_screen_height(), + camera.get_x(), camera.get_y()); + } + + const auto& objects = level.get_all_objects(); + for (size_t i = 0; i < objects.size(); ++i) + { + const GameObject& obj = objects[i]; + if (!obj.active || obj.sprite == nullptr) + { + continue; + } + + const int32_t screen_x = obj.position.x - camera.get_x(); + const int32_t screen_y = obj.position.y - camera.get_y(); + + if (screen_x + obj.sprite->width < 0 || screen_x > camera.get_screen_width() || + screen_y + obj.sprite->height < 0 || screen_y > camera.get_screen_height()) + { + continue; + } + + const bool is_light_obj = (obj.type == GameObjectType::LightPlatform || + obj.type == GameObjectType::ShadowPlatform || + obj.type == GameObjectType::Door); + + if (is_light_obj && !light_system.is_platform_active(obj, light_level)) + { + continue; + } + + ctx.draw_sprite_ex(screen_x, screen_y, *obj.sprite, 1, obj.flip_h, false); + } + } + + void LevelRenderer::draw_debug(Core::DrawContext& ctx, const Level& level, + const Camera2D& camera, const RenderData::BitmapFont& font) + { + const auto& objects = level.get_all_objects(); + for (size_t i = 0; i < objects.size(); ++i) + { + const GameObject& obj = objects[i]; + if (!obj.active) + { + continue; + } + + const RenderData::BoundingBox2D world = obj.get_world_collider(); + const int32_t sx = world.min.x - camera.get_x(); + const int32_t sy = world.min.y - camera.get_y(); + const int32_t w = world.max.x - world.min.x; + const int32_t h = world.max.y - world.min.y; + + RenderData::Color color = RenderData::Color::Green(); + if (obj.type == GameObjectType::Hazard) color = RenderData::Color::Red(); + if (obj.type == GameObjectType::LightPlatform) color = RenderData::Color(255, 255, 0, 255); + if (obj.type == GameObjectType::ShadowPlatform) color = RenderData::Color(128, 0, 255, 255); + if (obj.type == GameObjectType::Door) color = RenderData::Color(0, 255, 255, 255); + + ctx.draw_line(Math::Vector2Int(sx, sy), Math::Vector2Int(sx + w, sy), color); + ctx.draw_line(Math::Vector2Int(sx + w, sy), Math::Vector2Int(sx + w, sy + h), color); + ctx.draw_line(Math::Vector2Int(sx + w, sy + h), Math::Vector2Int(sx, sy + h), color); + ctx.draw_line(Math::Vector2Int(sx, sy + h), Math::Vector2Int(sx, sy), color); + } + } +} diff --git a/src/Apps/LightGame/src/engine/LevelRenderer.h b/src/Apps/LightGame/src/engine/LevelRenderer.h new file mode 100644 index 0000000..395ddf9 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LevelRenderer.h @@ -0,0 +1,28 @@ +#pragma once + +#include "Level.h" +#include "Camera2D.h" +#include "LightEffectSystem.h" + +namespace Core +{ + class DrawContext; +} + +namespace RenderData +{ + struct BitmapFont; +} + +namespace LightGame +{ + class LevelRenderer + { + public: + void draw(Core::DrawContext& ctx, const Level& level, const Camera2D& camera, + const LightEffectSystem& light_system, uint16_t light_level); + + void draw_debug(Core::DrawContext& ctx, const Level& level, const Camera2D& camera, + const RenderData::BitmapFont& font); + }; +} diff --git a/src/Apps/LightGame/src/engine/LightGameApp.cpp b/src/Apps/LightGame/src/engine/LightGameApp.cpp new file mode 100644 index 0000000..a5c3d06 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LightGameApp.cpp @@ -0,0 +1,250 @@ +#include "LightGameApp.h" +#include "DrawContext.h" +#include "BitmapFont.h" +#include "ButtonInput.h" +#include "PointerInput.h" +#include "LevelData.h" +#include "LevelLoader.h" +#include "Level1Data.h" +#include "Level2Data.h" +#include "Level3Data.h" + +namespace LightGame +{ + namespace + { + const LevelData* kLevels[] = { + &Level1::data, + &Level2::data, + &Level3::data + }; + const int32_t kLevelCount = 3; + } + + LightGameApp::LightGameApp( + int32_t screen_width, + int32_t screen_height, + Platform::IButtonInput* button_input, + Platform::IPointerInput* pointer_input, + Platform::IPhotoSensor* photo_sensor) + : screen_width_(screen_width), + screen_height_(screen_height), + button_input_(button_input), + pointer_input_(pointer_input), + photo_sensor_(photo_sensor), + level_(), + camera_(), + physics_(), + player_controller_(), + light_system_(), + state_manager_(), + renderer_(), + current_level_index_(0), + player_id_(0), + debug_mode_(false) + { + camera_.configure(screen_width, screen_height); + camera_.set_dead_zone(60, 40); + } + + void LightGameApp::update(uint32_t dt_ms) + { + if (photo_sensor_ && photo_sensor_->is_open()) + { + photo_sensor_->update(); + } + + const GameState prev_state = state_manager_.get_state(); + state_manager_.update(prev_state, button_input_, dt_ms); + + switch (state_manager_.get_state()) + { + case GameState::Title: + update_title(dt_ms); + break; + case GameState::Playing: + update_playing(dt_ms); + break; + case GameState::Paused: + break; + case GameState::GameOver: + update_game_over(dt_ms); + break; + case GameState::LevelComplete: + update_level_complete(dt_ms); + break; + } + } + + void LightGameApp::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font) + { + ctx.clear_color(RenderData::Color(20, 20, 40, 255)); + + if (state_manager_.get_state() == GameState::Playing || + state_manager_.get_state() == GameState::Paused) + { + const uint16_t light = photo_sensor_ ? photo_sensor_->read_level() : 2048; + renderer_.draw(ctx, level_, camera_, light_system_, light); + + if (debug_mode_) + { + renderer_.draw_debug(ctx, level_, camera_, font); + } + } + + state_manager_.draw(ctx, font, screen_width_, screen_height_); + + if (debug_mode_ && state_manager_.get_state() == GameState::Playing) + { + const uint16_t light = photo_sensor_ ? photo_sensor_->read_level() : 2048; + char buf[32]; + snprintf(buf, sizeof(buf), "L:%d", light); + ctx.draw_text(font, screen_width_ - 64, screen_height_ - 16, + RenderData::Color(128, 128, 128, 255), buf); + } + } + + void LightGameApp::load_level(int32_t level_index) + { + if (level_index < 0 || level_index >= kLevelCount) + { + return; + } + + current_level_index_ = level_index; + level_ = Level(); + LevelLoader::load(level_, *kLevels[level_index]); + + const auto players = level_.get_objects_by_type(GameObjectType::Player); + if (!players.empty()) + { + player_id_ = players[0]->id; + } + + camera_.set_level_bounds(level_.get_bounds()); + camera_.snap_to(level_.get_spawn_point()); + + state_manager_.get_hud().current_level = level_index + 1; + } + + void LightGameApp::update_title(uint32_t dt_ms) + { + (void)dt_ms; + if (button_input_ && button_input_->was_pressed()) + { + load_level(0); + state_manager_.set_state(GameState::Playing); + } + } + + void LightGameApp::update_playing(uint32_t dt_ms) + { + const uint16_t light = photo_sensor_ ? photo_sensor_->read_level() : 2048; + light_system_.update(level_.get_all_objects(), light); + state_manager_.get_hud().light_level = light; + + GameObject* player = level_.get_object(player_id_); + if (player) + { + player_controller_.update(*player, level_, physics_, button_input_, pointer_input_, dt_ms); + camera_.follow(player->position); + + if (player_controller_.is_dead()) + { + state_manager_.get_hud().lives--; + if (state_manager_.get_hud().lives <= 0) + { + state_manager_.set_state(GameState::GameOver); + } + } + } + + check_triggers(); + } + + void LightGameApp::update_game_over(uint32_t dt_ms) + { + (void)dt_ms; + if (button_input_ && button_input_->was_pressed()) + { + state_manager_.get_hud().lives = 3; + state_manager_.get_hud().collectibles = 0; + load_level(0); + state_manager_.set_state(GameState::Playing); + } + } + + void LightGameApp::update_level_complete(uint32_t dt_ms) + { + (void)dt_ms; + if (button_input_ && button_input_->was_pressed()) + { + const int32_t next = current_level_index_ + 1; + if (next < kLevelCount) + { + load_level(next); + state_manager_.set_state(GameState::Playing); + } + else + { + state_manager_.set_state(GameState::Title); + } + } + } + + void LightGameApp::check_triggers() + { + const GameObject* player = level_.get_object(player_id_); + if (!player) + { + return; + } + + const RenderData::BoundingBox2D player_box = player->get_world_collider(); + const auto& objects = level_.get_all_objects(); + + for (size_t i = 0; i < objects.size(); ++i) + { + const GameObject& obj = objects[i]; + if (!obj.active || obj.type != GameObjectType::Trigger) + { + continue; + } + + const RenderData::BoundingBox2D trigger_box = obj.get_world_collider(); + if (aabb_overlap(player_box, trigger_box)) + { + switch (obj.trigger_action) + { + case TriggerAction::LevelComplete: + state_manager_.set_state(GameState::LevelComplete); + return; + + case TriggerAction::Checkpoint: + { + const_cast(level_).get_checkpoints(); + break; + } + + case TriggerAction::Death: + break; + + default: + break; + } + } + } + + const auto collectibles = level_.get_objects_by_type(GameObjectType::Collectible); + for (size_t i = 0; i < collectibles.size(); ++i) + { + const GameObject* c = collectibles[i]; + const RenderData::BoundingBox2D c_box = c->get_world_collider(); + if (aabb_overlap(player_box, c_box)) + { + level_.get_all_objects()[c->id - 1].active = false; + state_manager_.get_hud().collectibles++; + } + } + } +} diff --git a/src/Apps/LightGame/src/engine/LightGameApp.h b/src/Apps/LightGame/src/engine/LightGameApp.h new file mode 100644 index 0000000..b81ce42 --- /dev/null +++ b/src/Apps/LightGame/src/engine/LightGameApp.h @@ -0,0 +1,76 @@ +#pragma once + +#include +#include "Level.h" +#include "Camera2D.h" +#include "Physics2D.h" +#include "PlayerController.h" +#include "LightEffectSystem.h" +#include "GameStateManager.h" +#include "LevelRenderer.h" +#include "IPhotoSensor.h" + +namespace Core +{ + class DrawContext; +} + +namespace RenderData +{ + struct BitmapFont; +} + +namespace Platform +{ + class IButtonInput; + class IPointerInput; +} + +namespace LightGame +{ + class LightGameApp + { + private: + int32_t screen_width_; + int32_t screen_height_; + + Platform::IButtonInput* button_input_; + Platform::IPointerInput* pointer_input_; + Platform::IPhotoSensor* photo_sensor_; + + Level level_; + Camera2D camera_; + Physics2D physics_; + PlayerController player_controller_; + LightEffectSystem light_system_; + GameStateManager state_manager_; + LevelRenderer renderer_; + + int32_t current_level_index_; + uint32_t player_id_; + + bool debug_mode_; + + public: + LightGameApp( + int32_t screen_width, + int32_t screen_height, + Platform::IButtonInput* button_input, + Platform::IPointerInput* pointer_input, + Platform::IPhotoSensor* photo_sensor); + + void update(uint32_t dt_ms); + void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font); + + GameState get_state() const { return state_manager_.get_state(); } + void set_debug_mode(bool enabled) { debug_mode_ = enabled; } + + private: + void load_level(int32_t level_index); + void update_playing(uint32_t dt_ms); + void update_title(uint32_t dt_ms); + void update_game_over(uint32_t dt_ms); + void update_level_complete(uint32_t dt_ms); + void check_triggers(); + }; +} diff --git a/src/Apps/LightGame/src/engine/Physics2D.cpp b/src/Apps/LightGame/src/engine/Physics2D.cpp new file mode 100644 index 0000000..8e6e1fa --- /dev/null +++ b/src/Apps/LightGame/src/engine/Physics2D.cpp @@ -0,0 +1,184 @@ +#include "Physics2D.h" +#include +#include + +namespace LightGame +{ + bool aabb_overlap(const RenderData::BoundingBox2D& a, const RenderData::BoundingBox2D& b) + { + return a.max.x > b.min.x && a.min.x < b.max.x && + a.max.y > b.min.y && a.min.y < b.max.y; + } + + Physics2D::Physics2D() + : config_(), + sub_pixel_x_(0), + sub_pixel_y_(0) + { + } + + void Physics2D::set_config(const PhysicsConfig& config) + { + config_ = config; + } + + void Physics2D::apply_gravity(GameObject& obj, uint32_t dt_ms) + { + obj.velocity.y += config_.gravity * static_cast(dt_ms) / 1000; + + if (obj.velocity.y > config_.max_fall_speed) + { + obj.velocity.y = config_.max_fall_speed; + } + } + + void Physics2D::move_and_collide(GameObject& obj, Level& level, uint32_t dt_ms) + { + const int32_t dx = obj.velocity.x * static_cast(dt_ms); + const int32_t dy = obj.velocity.y * static_cast(dt_ms); + + sub_pixel_x_ += dx; + sub_pixel_y_ += dy; + + const int32_t move_x = sub_pixel_x_ / 1000; + const int32_t move_y = sub_pixel_y_ / 1000; + sub_pixel_x_ %= 1000; + sub_pixel_y_ %= 1000; + + obj.position.x += move_x; + obj.position.y += move_y; + + const RenderData::BoundingBox2D world_collider = obj.get_world_collider(); + const auto& all_objects = level.get_all_objects(); + + for (size_t i = 0; i < all_objects.size(); ++i) + { + const GameObject& other = all_objects[i]; + if (other.id == obj.id || !other.active) + { + continue; + } + + if (!other.solid) + { + continue; + } + + const bool is_one_way = (other.type == GameObjectType::LightPlatform || + other.type == GameObjectType::ShadowPlatform); + + const RenderData::BoundingBox2D other_world = other.get_world_collider(); + + if (!aabb_overlap(world_collider, other_world)) + { + continue; + } + + if (is_one_way) + { + const int32_t obj_bottom = world_collider.max.y; + const int32_t other_top = other_world.min.y; + const int32_t prev_bottom = obj_bottom - move_y; + + if (prev_bottom > other_top || obj.velocity.y <= 0) + { + continue; + } + + obj.position.y = other_top - (obj.collider.max.y - obj.collider.min.y); + obj.velocity.y = 0; + } + else + { + const RenderData::BoundingBox2D updated_collider = obj.get_world_collider(); + const int32_t overlap_left = updated_collider.max.x - other_world.min.x; + const int32_t overlap_right = other_world.max.x - updated_collider.min.x; + const int32_t overlap_top = updated_collider.max.y - other_world.min.y; + const int32_t overlap_bottom = other_world.max.y - updated_collider.min.y; + + const int32_t min_x = std::min(overlap_left, overlap_right); + const int32_t min_y = std::min(overlap_top, overlap_bottom); + + if (min_x < min_y) + { + if (overlap_left < overlap_right) + { + obj.position.x -= overlap_left; + } + else + { + obj.position.x += overlap_right; + } + obj.velocity.x = 0; + } + else + { + if (overlap_top < overlap_bottom) + { + obj.position.y -= overlap_top; + obj.velocity.y = 0; + } + else + { + obj.position.y += overlap_bottom; + obj.velocity.y = 0; + } + } + } + } + + const LevelBounds& bounds = level.get_bounds(); + if (obj.position.y > bounds.max_y + 128) + { + obj.active = false; + } + } + + bool Physics2D::is_grounded(const GameObject& obj, const Level& level) + { + RenderData::BoundingBox2D feet = obj.get_world_collider(); + feet.min.y = feet.max.y; + feet.max.y = feet.max.y + 2; + + const auto& all_objects = level.get_all_objects(); + for (size_t i = 0; i < all_objects.size(); ++i) + { + const GameObject& other = all_objects[i]; + if (other.id == obj.id || !other.active || !other.solid) + { + continue; + } + + const RenderData::BoundingBox2D other_world = other.get_world_collider(); + if (aabb_overlap(feet, other_world)) + { + return true; + } + } + + return false; + } + + CollisionResult Physics2D::check_collision( + const RenderData::BoundingBox2D& moving, + const RenderData::BoundingBox2D& stationary, + bool one_way) + { + CollisionResult result; + + if (!aabb_overlap(moving, stationary)) + { + return result; + } + + result.hit = true; + result.penetration_x = std::min( + moving.max.x - stationary.min.x, + stationary.max.x - moving.min.x); + result.penetration_y = std::min( + moving.max.y - stationary.min.y, + stationary.max.y - moving.min.y); + + return result; + } +} diff --git a/src/Apps/LightGame/src/engine/Physics2D.h b/src/Apps/LightGame/src/engine/Physics2D.h new file mode 100644 index 0000000..a0ad95b --- /dev/null +++ b/src/Apps/LightGame/src/engine/Physics2D.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include "GameObject.h" +#include "Level.h" + +namespace LightGame +{ + struct PhysicsConfig + { + int32_t gravity; + int32_t max_fall_speed; + int32_t friction; + + PhysicsConfig() + : gravity(800), + max_fall_speed(600), + friction(900) + { + } + }; + + struct CollisionResult + { + bool hit; + int32_t penetration_x; + int32_t penetration_y; + + CollisionResult() : hit(false), penetration_x(0), penetration_y(0) {} + }; + + bool aabb_overlap(const RenderData::BoundingBox2D& a, const RenderData::BoundingBox2D& b); + + class Physics2D + { + private: + PhysicsConfig config_; + int32_t sub_pixel_x_; + int32_t sub_pixel_y_; + static const int32_t kSubPixelScale = 256; + + public: + Physics2D(); + + void set_config(const PhysicsConfig& config); + const PhysicsConfig& get_config() const { return config_; } + + void apply_gravity(GameObject& obj, uint32_t dt_ms); + void move_and_collide(GameObject& obj, Level& level, uint32_t dt_ms); + + bool is_grounded(const GameObject& obj, const Level& level); + + private: + CollisionResult check_collision( + const RenderData::BoundingBox2D& moving, + const RenderData::BoundingBox2D& stationary, + bool one_way); + + void resolve_collision(GameObject& obj, const CollisionResult& result, bool from_above); + }; +} diff --git a/src/Apps/LightGame/src/engine/PlayerController.cpp b/src/Apps/LightGame/src/engine/PlayerController.cpp new file mode 100644 index 0000000..fa14cde --- /dev/null +++ b/src/Apps/LightGame/src/engine/PlayerController.cpp @@ -0,0 +1,289 @@ +#include "PlayerController.h" +#include "ButtonInput.h" +#include "PointerInput.h" +#include + +#ifndef USE_FRAMEBUFFER +#include +#endif + +namespace LightGame +{ + PlayerController::PlayerController() + : config_(), + state_(PlayerState::Idle), + jump_held_(false), + was_grounded_(true), + move_dir_(0), + respawn_timer_ms_(0), + idle_frames_(nullptr), + run_frames_(nullptr), + jump_frame_(nullptr), + fall_frame_(nullptr), + idle_frame_count_(0), + run_frame_count_(0), + current_anim_frame_(0), + anim_timer_ms_(0) + { + } + + void PlayerController::set_config(const PlayerConfig& config) + { + config_ = config; + } + + void PlayerController::set_sprites( + const RenderData::Sprite* idle, uint8_t idle_count, + const RenderData::Sprite* run, uint8_t run_count, + const RenderData::Sprite* jump, + const RenderData::Sprite* fall) + { + idle_frames_ = idle; + idle_frame_count_ = idle_count; + run_frames_ = run; + run_frame_count_ = run_count; + jump_frame_ = jump; + fall_frame_ = fall; + } + + void PlayerController::update(GameObject& obj, Level& level, Physics2D& physics, + const Platform::IButtonInput* button, + const Platform::IPointerInput* pointer, + uint32_t dt_ms) + { + if (state_ == PlayerState::Dead) + { + respawn_timer_ms_ += dt_ms; + if (respawn_timer_ms_ >= 1000) + { + respawn(obj, level); + } + return; + } + + update_input(button, pointer); + update_movement(obj, dt_ms); + + physics.apply_gravity(obj, dt_ms); + physics.move_and_collide(obj, level, dt_ms); + + const bool grounded = physics.is_grounded(obj, level); + + if (grounded && !was_grounded_ && obj.velocity.y >= 0) + { + obj.velocity.y = 0; + } + + if (jump_held_ && grounded) + { + obj.velocity.y = config_.jump_velocity; + } + + if (!jump_held_ && obj.velocity.y < 0) + { + obj.velocity.y = obj.velocity.y * config_.jump_cut_multiplier / 100; + } + + if (grounded) + { + if (move_dir_ != 0) + { + state_ = PlayerState::Running; + } + else + { + state_ = PlayerState::Idle; + } + } + else + { + if (obj.velocity.y < 0) + { + state_ = PlayerState::Jumping; + } + else + { + state_ = PlayerState::Falling; + } + } + + was_grounded_ = grounded; + + update_animation(obj, dt_ms); + check_death(obj, level); + } + + void PlayerController::update_input(const Platform::IButtonInput* button, + const Platform::IPointerInput* pointer) + { + move_dir_ = 0; + jump_held_ = false; + + if (button) + { + if (button->is_down()) + { + jump_held_ = true; + } + } + + if (pointer) + { + if (pointer->is_down()) + { + const int32_t px = pointer->get_x(); + if (px < 400) + { + move_dir_ = -1; + } + else if (px > 600) + { + move_dir_ = 1; + } + else + { + jump_held_ = true; + } + } + } + + if (move_dir_ == 0) + { +#ifndef USE_FRAMEBUFFER + SDL_PumpEvents(); + const Uint8* keys = SDL_GetKeyboardState(nullptr); + if (keys) + { + if (keys[SDL_SCANCODE_LEFT] || keys[SDL_SCANCODE_A]) + { + move_dir_ = -1; + } + if (keys[SDL_SCANCODE_RIGHT] || keys[SDL_SCANCODE_D]) + { + move_dir_ = 1; + } + if (keys[SDL_SCANCODE_SPACE] || keys[SDL_SCANCODE_UP] || keys[SDL_SCANCODE_W]) + { + jump_held_ = true; + } + } +#endif + } + } + + void PlayerController::update_movement(GameObject& obj, uint32_t dt_ms) + { + if (move_dir_ < 0) + { + obj.velocity.x -= config_.acceleration * static_cast(dt_ms) / 1000; + if (obj.velocity.x < -config_.move_speed) + { + obj.velocity.x = -config_.move_speed; + } + obj.flip_h = true; + } + else if (move_dir_ > 0) + { + obj.velocity.x += config_.acceleration * static_cast(dt_ms) / 1000; + if (obj.velocity.x > config_.move_speed) + { + obj.velocity.x = config_.move_speed; + } + obj.flip_h = false; + } + else + { + if (obj.velocity.x > 0) + { + obj.velocity.x -= config_.deceleration * static_cast(dt_ms) / 1000; + if (obj.velocity.x < 0) obj.velocity.x = 0; + } + else if (obj.velocity.x < 0) + { + obj.velocity.x += config_.deceleration * static_cast(dt_ms) / 1000; + if (obj.velocity.x > 0) obj.velocity.x = 0; + } + } + } + + void PlayerController::update_animation(GameObject& obj, uint32_t dt_ms) + { + anim_timer_ms_ += dt_ms; + + if (anim_timer_ms_ >= static_cast(config_.anim_frame_time_ms)) + { + anim_timer_ms_ -= config_.anim_frame_time_ms; + current_anim_frame_++; + } + + switch (state_) + { + case PlayerState::Idle: + if (idle_frames_ && idle_frame_count_ > 0) + { + obj.sprite = &idle_frames_[current_anim_frame_ % idle_frame_count_]; + } + break; + case PlayerState::Running: + if (run_frames_ && run_frame_count_ > 0) + { + obj.sprite = &run_frames_[current_anim_frame_ % run_frame_count_]; + } + break; + case PlayerState::Jumping: + obj.sprite = jump_frame_; + break; + case PlayerState::Falling: + obj.sprite = fall_frame_; + break; + case PlayerState::Dead: + break; + } + } + + void PlayerController::check_death(GameObject& obj, Level& level) + { + const LevelBounds& bounds = level.get_bounds(); + if (obj.position.y > bounds.max_y) + { + state_ = PlayerState::Dead; + respawn_timer_ms_ = 0; + return; + } + + const auto& all_objects = level.get_all_objects(); + const RenderData::BoundingBox2D player_box = obj.get_world_collider(); + + for (size_t i = 0; i < all_objects.size(); ++i) + { + const GameObject& other = all_objects[i]; + if (other.id == obj.id || !other.active) + { + continue; + } + + if (other.type == GameObjectType::Hazard) + { + const RenderData::BoundingBox2D other_box = other.get_world_collider(); + if (aabb_overlap(player_box, other_box)) + { + state_ = PlayerState::Dead; + respawn_timer_ms_ = 0; + return; + } + } + } + } + + void PlayerController::respawn(GameObject& obj, Level& level) + { + const Math::Vector2Int spawn = level.get_active_checkpoint(); + obj.position = spawn; + obj.velocity = Math::Vector2Int(0, 0); + obj.active = true; + state_ = PlayerState::Idle; + current_anim_frame_ = 0; + anim_timer_ms_ = 0; + was_grounded_ = true; + } +} diff --git a/src/Apps/LightGame/src/engine/PlayerController.h b/src/Apps/LightGame/src/engine/PlayerController.h new file mode 100644 index 0000000..74d527c --- /dev/null +++ b/src/Apps/LightGame/src/engine/PlayerController.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include "GameObject.h" +#include "Physics2D.h" + +namespace Platform +{ + class IButtonInput; + class IPointerInput; +} + +namespace LightGame +{ + enum class PlayerState + { + Idle, + Running, + Jumping, + Falling, + Dead + }; + + struct PlayerConfig + { + int32_t move_speed; + int32_t acceleration; + int32_t deceleration; + int32_t jump_velocity; + int32_t jump_cut_multiplier; + int32_t anim_frame_time_ms; + + PlayerConfig() + : move_speed(200), + acceleration(1200), + deceleration(1600), + jump_velocity(-420), + jump_cut_multiplier(40), + anim_frame_time_ms(120) + { + } + }; + + class PlayerController + { + private: + PlayerConfig config_; + PlayerState state_; + bool jump_held_; + bool was_grounded_; + int32_t move_dir_; + uint32_t respawn_timer_ms_; + + const RenderData::Sprite* idle_frames_; + const RenderData::Sprite* run_frames_; + const RenderData::Sprite* jump_frame_; + const RenderData::Sprite* fall_frame_; + uint8_t idle_frame_count_; + uint8_t run_frame_count_; + uint8_t current_anim_frame_; + uint32_t anim_timer_ms_; + + public: + PlayerController(); + + void set_config(const PlayerConfig& config); + void set_sprites( + const RenderData::Sprite* idle, uint8_t idle_count, + const RenderData::Sprite* run, uint8_t run_count, + const RenderData::Sprite* jump, + const RenderData::Sprite* fall); + + void update(GameObject& obj, Level& level, Physics2D& physics, + const Platform::IButtonInput* button, + const Platform::IPointerInput* pointer, + uint32_t dt_ms); + + PlayerState get_state() const { return state_; } + bool is_dead() const { return state_ == PlayerState::Dead; } + + private: + void update_input(const Platform::IButtonInput* button, const Platform::IPointerInput* pointer); + void update_movement(GameObject& obj, uint32_t dt_ms); + void update_animation(GameObject& obj, uint32_t dt_ms); + void check_death(GameObject& obj, Level& level); + void respawn(GameObject& obj, Level& level); + }; +} diff --git a/src/Apps/LightGame/src/levels/Level1Data.h b/src/Apps/LightGame/src/levels/Level1Data.h new file mode 100644 index 0000000..e2fdfc1 --- /dev/null +++ b/src/Apps/LightGame/src/levels/Level1Data.h @@ -0,0 +1,58 @@ +#pragma once + +#include "LevelData.h" + +namespace LightGame +{ + namespace Level1 + { + static const int32_t kMapWidth = 50; + static const int32_t kMapHeight = 15; + static const int32_t kTileSize = 16; + + static const uint16_t tiles[] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, + 1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + }; + + static const ObjectSpawn spawns[] = { + { GameObjectType::Player, 2, 11, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 5, 11, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 10, 10, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 18, 11, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Hazard, 12, 14, 0, 0, TriggerAction::Death, 0 }, + { GameObjectType::Hazard, 13, 14, 0, 0, TriggerAction::Death, 0 }, + { GameObjectType::Hazard, 14, 14, 0, 0, TriggerAction::Death, 0 }, + { GameObjectType::Trigger, 47, 12, 0, 0, TriggerAction::LevelComplete, 0 }, + }; + + static const LevelData data = { + tiles, + kMapWidth, + kMapHeight, + kTileSize, + nullptr, + 0, + spawns, + 8, + Math::Vector2Int(2 * kTileSize, 11 * kTileSize), + 0, + 0, + kMapWidth * kTileSize, + kMapHeight * kTileSize + }; + } +} diff --git a/src/Apps/LightGame/src/levels/Level2Data.h b/src/Apps/LightGame/src/levels/Level2Data.h new file mode 100644 index 0000000..cbdf85f --- /dev/null +++ b/src/Apps/LightGame/src/levels/Level2Data.h @@ -0,0 +1,68 @@ +#pragma once + +#include "LevelData.h" + +namespace LightGame +{ + namespace Level2 + { + static const int32_t kMapWidth = 50; + static const int32_t kMapHeight = 15; + static const int32_t kTileSize = 16; + + static const uint16_t tiles[] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1, + 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + }; + + static const ObjectSpawn spawns[] = { + { GameObjectType::Player, 2, 11, 0, 0, TriggerAction::None, 0 }, + + { GameObjectType::LightPlatform, 8, 10, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 12, 9, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 16, 8, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 20, 9, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 24, 10, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 28, 9, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 32, 8, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 36, 9, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 40, 10, 1500, 0, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 44, 11, 1500, 0, TriggerAction::None, 0 }, + + { GameObjectType::Collectible, 12, 8, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 20, 8, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 32, 7, 0, 0, TriggerAction::None, 0 }, + + { GameObjectType::Trigger, 48, 12, 0, 0, TriggerAction::LevelComplete, 0 }, + }; + + static const LevelData data = { + tiles, + kMapWidth, + kMapHeight, + kTileSize, + nullptr, + 0, + spawns, + 15, + Math::Vector2Int(2 * kTileSize, 11 * kTileSize), + 0, + 0, + kMapWidth * kTileSize, + kMapHeight * kTileSize + }; + } +} diff --git a/src/Apps/LightGame/src/levels/Level3Data.h b/src/Apps/LightGame/src/levels/Level3Data.h new file mode 100644 index 0000000..5d77286 --- /dev/null +++ b/src/Apps/LightGame/src/levels/Level3Data.h @@ -0,0 +1,70 @@ +#pragma once + +#include "LevelData.h" + +namespace LightGame +{ + namespace Level3 + { + static const int32_t kMapWidth = 50; + static const int32_t kMapHeight = 15; + static const int32_t kTileSize = 16; + + static const uint16_t tiles[] = { + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1, + 1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,1, + 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, + }; + + static const ObjectSpawn spawns[] = { + { GameObjectType::Player, 2, 11, 0, 0, TriggerAction::None, 0 }, + + { GameObjectType::LightPlatform, 8, 10, 2000, 0, TriggerAction::None, 0 }, + { GameObjectType::ShadowPlatform, 12, 8, 0, 1000, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 16, 10, 2000, 0, TriggerAction::None, 0 }, + { GameObjectType::ShadowPlatform, 20, 8, 0, 1000, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 24, 10, 2000, 0, TriggerAction::None, 0 }, + + { GameObjectType::Door, 30, 10, 1500, 2500, TriggerAction::None, 0 }, + + { GameObjectType::LightPlatform, 34, 10, 2000, 0, TriggerAction::None, 0 }, + { GameObjectType::ShadowPlatform, 38, 8, 0, 1000, TriggerAction::None, 0 }, + { GameObjectType::LightPlatform, 42, 10, 2000, 0, TriggerAction::None, 0 }, + { GameObjectType::ShadowPlatform, 44, 11, 0, 1000, TriggerAction::None, 0 }, + + { GameObjectType::Collectible, 12, 7, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 20, 7, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::Collectible, 38, 7, 0, 0, TriggerAction::None, 0 }, + + { GameObjectType::Trigger, 48, 12, 0, 0, TriggerAction::LevelComplete, 0 }, + }; + + static const LevelData data = { + tiles, + kMapWidth, + kMapHeight, + kTileSize, + nullptr, + 0, + spawns, + 15, + Math::Vector2Int(2 * kTileSize, 11 * kTileSize), + 0, + 0, + kMapWidth * kTileSize, + kMapHeight * kTileSize + }; + } +} diff --git a/src/Apps/LightGame/src/main.cpp b/src/Apps/LightGame/src/main.cpp new file mode 100644 index 0000000..58df14e --- /dev/null +++ b/src/Apps/LightGame/src/main.cpp @@ -0,0 +1,131 @@ +#include +#include +#include +#include +#include +#include + +#include "DefaultHardware.h" +#include "Display.h" +#include "DrawContext.h" +#include "TimeSource.h" +#include "Timer.h" +#include "LightGameApp.h" +#include "font_atlas.h" + +#ifdef USE_FRAMEBUFFER +#include "FBDisplay.h" +#else +#include "SDLDisplay.h" +#endif + +namespace +{ + const int32_t ScreenWidth = 1024; + const int32_t ScreenHeight = 600; + + static Platform::IDisplay* CreateDisplay() + { +#ifdef USE_FRAMEBUFFER + return new Platform::FBDisplay(); +#else + return new Platform::SDLDisplay(); +#endif + } + + static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source) + { + const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms()); + if (sleep_ms > 0u) + { + std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms)); + } + } +} + +int main(int argc, char* argv[]) +{ + Platform::IDisplay* display = CreateDisplay(); + if (!display->init(ScreenWidth, ScreenHeight)) + { + delete display; + return -1; + } + + Platform::DefaultButtonInput buttonInput; + Platform::DefaultPointerInput pointerInput; + Platform::DefaultPhotoSensor photoSensor; + + if (!buttonInput.init()) + { + std::cerr << "[WARN] Button input init failed." << std::endl; + } + if (!pointerInput.init("", ScreenWidth, ScreenHeight)) + { + std::cerr << "[WARN] Pointer input init failed." << std::endl; + } + if (!photoSensor.init()) + { + std::cerr << "[WARN] Photo sensor init failed; using default light level." << std::endl; + } + + Core::DrawContext ctx(ScreenWidth, ScreenHeight); + Core::Timer timer(30); + Platform::SteadyTimeSource time_source; + + RenderData::BitmapFont font; + font.mask_bits = font_atlas_mask; + font.atlas_width = font_atlas_width; + font.atlas_height = font_atlas_height; + font.char_w = font_char_w; + font.char_h = font_char_h; + font.columns = font_columns; + font.first_char = font_first_char; + + LightGame::LightGameApp app( + ScreenWidth, + ScreenHeight, + &buttonInput, + &pointerInput, + &photoSensor); + + bool debug_mode = false; + for (int i = 1; i < argc; ++i) + { + if (std::strcmp(argv[i], "--debug") == 0) + { + debug_mode = true; + } + } + app.set_debug_mode(debug_mode); + + std::cout << "[INFO] LightGame started. Target FPS: " << timer.target_fps() << std::endl; + + bool is_running = true; + while (is_running) + { + timer.begin_frame(time_source.get_time_ms()); + + bool should_quit = false; + display->poll_events(should_quit); + if (should_quit) + { + is_running = false; + } + + buttonInput.update(); + pointerInput.update(); + + app.update(timer.fixed_delta_ms()); + app.draw(ctx, font); + ctx.present(display); + SleepRemainingFrameTime(timer, time_source); + } + + photoSensor.shutdown(); + pointerInput.shutdown(); + buttonInput.shutdown(); + display->shutdown(); + delete display; + return 0; +} diff --git a/src/Apps/LightGame/src/systems/LightEffectSystem.cpp b/src/Apps/LightGame/src/systems/LightEffectSystem.cpp new file mode 100644 index 0000000..d0a6b2e --- /dev/null +++ b/src/Apps/LightGame/src/systems/LightEffectSystem.cpp @@ -0,0 +1,56 @@ +#include "LightEffectSystem.h" + +namespace LightGame +{ + void LightEffectSystem::update(std::vector& objects, uint16_t light_level) + { + for (size_t i = 0; i < objects.size(); ++i) + { + GameObject& obj = objects[i]; + + switch (obj.type) + { + case GameObjectType::LightPlatform: + obj.solid = (light_level >= obj.light_threshold.min_level); + break; + + case GameObjectType::ShadowPlatform: + obj.solid = (light_level <= obj.light_threshold.max_level); + break; + + case GameObjectType::Door: + obj.solid = !is_door_open(obj, light_level); + break; + + default: + break; + } + } + } + + bool LightEffectSystem::is_platform_active(const GameObject& obj, uint16_t light_level) + { + switch (obj.type) + { + case GameObjectType::LightPlatform: + return light_level >= obj.light_threshold.min_level; + + case GameObjectType::ShadowPlatform: + return light_level <= obj.light_threshold.max_level; + + default: + return obj.solid; + } + } + + bool LightEffectSystem::is_door_open(const GameObject& obj, uint16_t light_level) + { + if (obj.type != GameObjectType::Door) + { + return false; + } + + return light_level >= obj.light_threshold.min_level && + light_level <= obj.light_threshold.max_level; + } +} diff --git a/src/Apps/LightGame/src/systems/LightEffectSystem.h b/src/Apps/LightGame/src/systems/LightEffectSystem.h new file mode 100644 index 0000000..a81e893 --- /dev/null +++ b/src/Apps/LightGame/src/systems/LightEffectSystem.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include +#include "GameObject.h" + +namespace LightGame +{ + class LightEffectSystem + { + public: + void update(std::vector& objects, uint16_t light_level); + + static bool is_platform_active(const GameObject& obj, uint16_t light_level); + static bool is_door_open(const GameObject& obj, uint16_t light_level); + }; +} diff --git a/src/Core/Platform/DefaultHardware.h b/src/Core/Platform/DefaultHardware.h index 6099e9b..c8e9c42 100644 --- a/src/Core/Platform/DefaultHardware.h +++ b/src/Core/Platform/DefaultHardware.h @@ -5,11 +5,13 @@ #include "AlsaAudioOutput.h" #include "EvdevButtonInput.h" #include "EvdevTouchInput.h" +#include "LinuxPhotoSensor.h" #else #include "SdlAudioInput.h" #include "SdlAudioOutput.h" #include "SdlKeyboardButtonInput.h" #include "SdlPointerInput.h" +#include "SdlPhotoSensor.h" #endif namespace Platform @@ -19,10 +21,12 @@ namespace Platform typedef AlsaAudioOutput DefaultAudioOutput; typedef EvdevButtonInput DefaultButtonInput; typedef EvdevTouchInput DefaultPointerInput; + typedef LinuxPhotoSensor DefaultPhotoSensor; #else typedef SdlAudioInput DefaultAudioInput; typedef SdlAudioOutput DefaultAudioOutput; typedef SdlKeyboardButtonInput DefaultButtonInput; typedef SdlPointerInput DefaultPointerInput; + typedef SdlPhotoSensor DefaultPhotoSensor; #endif } diff --git a/src/Core/Platform/IPhotoSensor.h b/src/Core/Platform/IPhotoSensor.h new file mode 100644 index 0000000..ded572e --- /dev/null +++ b/src/Core/Platform/IPhotoSensor.h @@ -0,0 +1,21 @@ +#pragma once + +#include +#include + +namespace Platform +{ + class IPhotoSensor + { + public: + virtual ~IPhotoSensor() {} + + virtual bool init(const std::string& device_path = "") = 0; + virtual void update() = 0; + virtual void shutdown() = 0; + + virtual bool is_open() const = 0; + virtual uint16_t read_level() const = 0; + virtual const std::string& get_device_path() const = 0; + }; +} diff --git a/src/Core/Platform/LinuxPhotoSensor.cpp b/src/Core/Platform/LinuxPhotoSensor.cpp new file mode 100644 index 0000000..355fb20 --- /dev/null +++ b/src/Core/Platform/LinuxPhotoSensor.cpp @@ -0,0 +1,168 @@ +#include "LinuxPhotoSensor.h" +#include + +#if defined(__linux__) +#include +#include +#include +#include +#include +#endif + +namespace +{ +#if defined(__linux__) + static const char* kDefaultIioPath = "/sys/bus/iio/devices"; + + static std::string FindAdcDevice() + { + DIR* dir = opendir(kDefaultIioPath); + if (!dir) + { + return ""; + } + + std::string found; + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) + { + if (strncmp(entry->d_name, "iio:device", 10) == 0) + { + std::string path = std::string(kDefaultIioPath) + "/" + entry->d_name; + found = path; + break; + } + } + closedir(dir); + return found; + } + + static std::string FindVoltageChannel(const std::string& device_path) + { + DIR* dir = opendir(device_path.c_str()); + if (!dir) + { + return ""; + } + + std::string found; + struct dirent* entry; + while ((entry = readdir(dir)) != nullptr) + { + if (strncmp(entry->d_name, "in_voltage", 10) == 0 && + strstr(entry->d_name, "_raw") != nullptr) + { + found = device_path + "/" + entry->d_name; + break; + } + } + closedir(dir); + return found; + } + + static bool ReadSysfsValue(const std::string& path, int& value) + { + int fd = open(path.c_str(), O_RDONLY); + if (fd < 0) + { + return false; + } + + char buf[32]; + ssize_t bytes = read(fd, buf, sizeof(buf) - 1); + close(fd); + + if (bytes <= 0) + { + return false; + } + + buf[bytes] = '\0'; + value = atoi(buf); + return true; + } +#endif +} + +namespace Platform +{ + LinuxPhotoSensor::LinuxPhotoSensor() + : device_path_(), + opened_(false), + level_(0), + max_value_(4095) + { + } + + LinuxPhotoSensor::~LinuxPhotoSensor() + { + shutdown(); + } + + bool LinuxPhotoSensor::init(const std::string& device_path) + { + shutdown(); + +#if defined(__linux__) + std::string path = device_path; + + if (path.empty()) + { + std::string adc_device = FindAdcDevice(); + if (adc_device.empty()) + { + std::cerr << "LinuxPhotoSensor: no IIO device found in " << kDefaultIioPath << std::endl; + return false; + } + + path = FindVoltageChannel(adc_device); + if (path.empty()) + { + std::cerr << "LinuxPhotoSensor: no voltage channel found in " << adc_device << std::endl; + return false; + } + } + + device_path_ = path; + + int test_value; + if (!ReadSysfsValue(device_path_, test_value)) + { + std::cerr << "LinuxPhotoSensor: cannot read from " << device_path_ << std::endl; + return false; + } + + level_ = static_cast(test_value & 0xFFFF); + opened_ = true; + return true; +#else + (void)device_path; + std::cerr << "LinuxPhotoSensor is only available on Linux." << std::endl; + return false; +#endif + } + + void LinuxPhotoSensor::update() + { + if (!opened_) + { + return; + } + +#if defined(__linux__) + int value; + if (ReadSysfsValue(device_path_, value)) + { + if (value < 0) value = 0; + if (value > max_value_) value = max_value_; + level_ = static_cast(value); + } +#endif + } + + void LinuxPhotoSensor::shutdown() + { + opened_ = false; + level_ = 0; + } +} diff --git a/src/Core/Platform/LinuxPhotoSensor.h b/src/Core/Platform/LinuxPhotoSensor.h new file mode 100644 index 0000000..16d89cd --- /dev/null +++ b/src/Core/Platform/LinuxPhotoSensor.h @@ -0,0 +1,28 @@ +#pragma once + +#include "IPhotoSensor.h" + +namespace Platform +{ + class LinuxPhotoSensor : public IPhotoSensor + { + private: + std::string device_path_; + bool opened_; + uint16_t level_; + uint16_t max_value_; + + public: + LinuxPhotoSensor(); + ~LinuxPhotoSensor(); + + bool init(const std::string& device_path = "") override; + void update() override; + void shutdown() override; + + bool is_open() const override { return opened_; } + uint16_t read_level() const override { return level_; } + const std::string& get_device_path() const override { return device_path_; } + uint16_t get_max_value() const { return max_value_; } + }; +} diff --git a/src/Core/Platform/SdlPhotoSensor.cpp b/src/Core/Platform/SdlPhotoSensor.cpp new file mode 100644 index 0000000..89286f3 --- /dev/null +++ b/src/Core/Platform/SdlPhotoSensor.cpp @@ -0,0 +1,117 @@ +#include "SdlPhotoSensor.h" +#include +#include +#include + +namespace +{ + static const uint16_t kDefaultMin = 0; + static const uint16_t kDefaultMax = 4095; + static const uint16_t kDefaultStep = 256; + static const uint16_t kDefaultLevel = 2048; +} + +namespace Platform +{ + SdlPhotoSensor::SdlPhotoSensor() + : device_path_("sdl_keyboard"), + opened_(false), + level_(kDefaultLevel), + min_level_(kDefaultMin), + max_level_(kDefaultMax), + step_(kDefaultStep) + { + } + + SdlPhotoSensor::~SdlPhotoSensor() + { + shutdown(); + } + + bool SdlPhotoSensor::init(const std::string& device_path) + { + if (SDL_WasInit(0) == 0 && SDL_Init(0) < 0) + { + std::cerr << "SDL_Init failed: " << SDL_GetError() << std::endl; + return false; + } + + if ((SDL_WasInit(SDL_INIT_EVENTS) & SDL_INIT_EVENTS) == 0 && + SDL_InitSubSystem(SDL_INIT_EVENTS) < 0) + { + std::cerr << "SDL events init failed: " << SDL_GetError() << std::endl; + return false; + } + + device_path_ = device_path.empty() ? "sdl_keyboard" : device_path; + opened_ = true; + return true; + } + + void SdlPhotoSensor::update() + { + if (!opened_) + { + return; + } + + SDL_PumpEvents(); + const Uint8* keyboard_state = SDL_GetKeyboardState(nullptr); + if (!keyboard_state) + { + return; + } + + if (keyboard_state[SDL_SCANCODE_UP]) + { + uint32_t next = static_cast(level_) + step_; + level_ = static_cast(std::min(next, static_cast(max_level_))); + } + + if (keyboard_state[SDL_SCANCODE_DOWN]) + { + if (level_ >= step_) + { + level_ -= step_; + } + else + { + level_ = min_level_; + } + } + + if (keyboard_state[SDL_SCANCODE_HOME]) + { + level_ = max_level_; + } + + if (keyboard_state[SDL_SCANCODE_END]) + { + level_ = min_level_; + } + } + + void SdlPhotoSensor::shutdown() + { + opened_ = false; + level_ = kDefaultLevel; + } + + void SdlPhotoSensor::set_level(uint16_t level) + { + level_ = std::min(level, max_level_); + } + + void SdlPhotoSensor::set_range(uint16_t min_level, uint16_t max_level) + { + min_level_ = min_level; + max_level_ = max_level; + if (level_ < min_level_) level_ = min_level_; + if (level_ > max_level_) level_ = max_level_; + } + + void SdlPhotoSensor::set_step(uint16_t step) + { + step_ = step; + } +} diff --git a/src/Core/Platform/SdlPhotoSensor.h b/src/Core/Platform/SdlPhotoSensor.h new file mode 100644 index 0000000..643ab97 --- /dev/null +++ b/src/Core/Platform/SdlPhotoSensor.h @@ -0,0 +1,33 @@ +#pragma once + +#include "IPhotoSensor.h" + +namespace Platform +{ + class SdlPhotoSensor : public IPhotoSensor + { + private: + std::string device_path_; + bool opened_; + uint16_t level_; + uint16_t min_level_; + uint16_t max_level_; + uint16_t step_; + + public: + SdlPhotoSensor(); + ~SdlPhotoSensor(); + + bool init(const std::string& device_path = "") override; + void update() override; + void shutdown() override; + + bool is_open() const override { return opened_; } + uint16_t read_level() const override { return level_; } + const std::string& get_device_path() const override { return device_path_; } + + void set_level(uint16_t level); + void set_range(uint16_t min_level, uint16_t max_level); + void set_step(uint16_t step); + }; +} diff --git a/tests/game_engine_tests.cpp b/tests/game_engine_tests.cpp new file mode 100644 index 0000000..c217305 --- /dev/null +++ b/tests/game_engine_tests.cpp @@ -0,0 +1,439 @@ +#include "GameObject.h" +#include "Level.h" +#include "Physics2D.h" +#include "LightEffectSystem.h" +#include "LevelLoader.h" +#include "LevelData.h" + +#include +#include + +using namespace LightGame; + +namespace +{ + void TestGameObjectDefaults() + { + GameObject obj; + assert(obj.id == 0); + assert(obj.type == GameObjectType::StaticPlatform); + assert(obj.position.x == 0); + assert(obj.position.y == 0); + assert(obj.velocity.x == 0); + assert(obj.velocity.y == 0); + assert(obj.sprite == nullptr); + assert(obj.active == true); + assert(obj.solid == true); + assert(obj.flip_h == false); + } + + void TestGameObjectWorldCollider() + { + GameObject obj; + obj.position = Math::Vector2Int(100, 200); + obj.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), + Math::Vector2Int(32, 32)); + + RenderData::BoundingBox2D world = obj.get_world_collider(); + assert(world.min.x == 100); + assert(world.min.y == 200); + assert(world.max.x == 132); + assert(world.max.y == 232); + } + + void TestLevelAddAndGet() + { + Level level; + assert(level.object_count() == 0); + + GameObject platform; + platform.type = GameObjectType::StaticPlatform; + platform.position = Math::Vector2Int(0, 400); + uint32_t id1 = level.add_object(platform); + assert(id1 == 1); + assert(level.object_count() == 1); + + GameObject player; + player.type = GameObjectType::Player; + player.position = Math::Vector2Int(50, 350); + uint32_t id2 = level.add_object(player); + assert(id2 == 2); + assert(level.object_count() == 2); + + const GameObject* found = level.get_object(id1); + assert(found != nullptr); + assert(found->type == GameObjectType::StaticPlatform); + assert(found->id == 1); + + const GameObject* not_found = level.get_object(999); + assert(not_found == nullptr); + } + + void TestLevelRemove() + { + Level level; + GameObject obj; + obj.type = GameObjectType::Collectible; + uint32_t id = level.add_object(obj); + assert(level.object_count() == 1); + + level.remove_object(id); + assert(level.object_count() == 0); + assert(level.get_object(id) == nullptr); + } + + void TestLevelQueryByType() + { + Level level; + + GameObject platform; + platform.type = GameObjectType::StaticPlatform; + level.add_object(platform); + + GameObject light_plat; + light_plat.type = GameObjectType::LightPlatform; + level.add_object(light_plat); + + GameObject player; + player.type = GameObjectType::Player; + level.add_object(player); + + auto platforms = level.get_objects_by_type(GameObjectType::StaticPlatform); + assert(platforms.size() == 1); + assert(platforms[0]->type == GameObjectType::StaticPlatform); + + auto players = level.get_objects_by_type(GameObjectType::Player); + assert(players.size() == 1); + + auto hazards = level.get_objects_by_type(GameObjectType::Hazard); + assert(hazards.size() == 0); + } + + void TestLevelQueryRegion() + { + Level level; + + GameObject obj1; + obj1.type = GameObjectType::StaticPlatform; + obj1.position = Math::Vector2Int(100, 100); + obj1.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(32, 32)); + level.add_object(obj1); + + GameObject obj2; + obj2.type = GameObjectType::StaticPlatform; + obj2.position = Math::Vector2Int(500, 500); + obj2.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(32, 32)); + level.add_object(obj2); + + RenderData::BoundingBox2D query( + Math::Vector2Int(80, 80), Math::Vector2Int(200, 200)); + auto results = level.query_region(query); + assert(results.size() == 1); + assert(results[0]->position.x == 100); + } + + void TestLevelInactiveObjectsExcluded() + { + Level level; + + GameObject obj; + obj.type = GameObjectType::Collectible; + obj.active = true; + uint32_t id = level.add_object(obj); + + auto active = level.get_objects_by_type(GameObjectType::Collectible); + assert(active.size() == 1); + + GameObject* found = level.get_object(id); + found->active = false; + + auto active2 = level.get_objects_by_type(GameObjectType::Collectible); + assert(active2.size() == 0); + } + + void TestLevelCheckpoints() + { + Level level; + level.set_spawn_point(Math::Vector2Int(50, 300)); + + Checkpoint cp1(200, 300); + Checkpoint cp2(400, 200); + level.add_checkpoint(cp1); + level.add_checkpoint(cp2); + + Math::Vector2Int respawn = level.get_active_checkpoint(); + assert(respawn.x == 50); + assert(respawn.y == 300); + + level.get_all_objects(); + auto& cps = level.get_checkpoints(); + const_cast(cps[0]).activated = true; + + respawn = level.get_active_checkpoint(); + assert(respawn.x == 200); + assert(respawn.y == 300); + + const_cast(cps[1]).activated = true; + respawn = level.get_active_checkpoint(); + assert(respawn.x == 400); + assert(respawn.y == 200); + } + + void TestAabbOverlap() + { + RenderData::BoundingBox2D a(Math::Vector2Int(0, 0), Math::Vector2Int(10, 10)); + RenderData::BoundingBox2D b(Math::Vector2Int(5, 5), Math::Vector2Int(15, 15)); + assert(aabb_overlap(a, b) == true); + + RenderData::BoundingBox2D c(Math::Vector2Int(20, 20), Math::Vector2Int(30, 30)); + assert(aabb_overlap(a, c) == false); + + RenderData::BoundingBox2D d(Math::Vector2Int(10, 0), Math::Vector2Int(20, 10)); + assert(aabb_overlap(a, d) == false); + + RenderData::BoundingBox2D e(Math::Vector2Int(9, 0), Math::Vector2Int(19, 10)); + assert(aabb_overlap(a, e) == true); + } + + void TestGravityAccumulation() + { + Physics2D physics; + PhysicsConfig config; + config.gravity = 1000; + config.max_fall_speed = 500; + physics.set_config(config); + + GameObject obj; + obj.velocity = Math::Vector2Int(0, 0); + + physics.apply_gravity(obj, 16); + assert(obj.velocity.y > 0); + assert(obj.velocity.y <= config.max_fall_speed); + + for (int i = 0; i < 100; ++i) + { + physics.apply_gravity(obj, 16); + } + assert(obj.velocity.y == config.max_fall_speed); + } + + void TestGroundDetection() + { + Level level; + level.set_bounds(LevelBounds(0, 0, 800, 600)); + + GameObject ground; + ground.type = GameObjectType::StaticPlatform; + ground.position = Math::Vector2Int(0, 400); + ground.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(800, 32)); + ground.solid = true; + level.add_object(ground); + + GameObject player; + player.type = GameObjectType::Player; + player.position = Math::Vector2Int(100, 368); + player.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(16, 32)); + + Physics2D physics; + assert(physics.is_grounded(player, level) == true); + + player.position = Math::Vector2Int(100, 300); + assert(physics.is_grounded(player, level) == false); + } + + void TestOneWayPlatform() + { + Level level; + level.set_bounds(LevelBounds(0, 0, 800, 600)); + + GameObject platform; + platform.type = GameObjectType::LightPlatform; + platform.position = Math::Vector2Int(100, 300); + platform.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(64, 16)); + platform.solid = true; + level.add_object(platform); + + GameObject player; + player.type = GameObjectType::Player; + player.position = Math::Vector2Int(110, 320); + player.collider = RenderData::BoundingBox2D( + Math::Vector2Int(0, 0), Math::Vector2Int(16, 32)); + player.velocity = Math::Vector2Int(0, -100); + + Physics2D physics; + physics.move_and_collide(player, level, 16); + + assert(player.position.y < 320); + } + + void TestLightPlatformActivation() + { + LightEffectSystem system; + + GameObject light_plat; + light_plat.type = GameObjectType::LightPlatform; + light_plat.light_threshold.min_level = 2000; + light_plat.solid = false; + + std::vector objects; + objects.push_back(light_plat); + + system.update(objects, 1000); + assert(objects[0].solid == false); + + system.update(objects, 2500); + assert(objects[0].solid == true); + + system.update(objects, 2000); + assert(objects[0].solid == true); + } + + void TestShadowPlatformActivation() + { + LightEffectSystem system; + + GameObject shadow_plat; + shadow_plat.type = GameObjectType::ShadowPlatform; + shadow_plat.light_threshold.max_level = 1000; + shadow_plat.solid = true; + + std::vector objects; + objects.push_back(shadow_plat); + + system.update(objects, 2000); + assert(objects[0].solid == false); + + system.update(objects, 500); + assert(objects[0].solid == true); + + system.update(objects, 1000); + assert(objects[0].solid == true); + } + + void TestLightDoorOpen() + { + LightEffectSystem system; + + GameObject door; + door.type = GameObjectType::Door; + door.light_threshold.min_level = 1500; + door.light_threshold.max_level = 2500; + door.solid = true; + + std::vector objects; + objects.push_back(door); + + system.update(objects, 1000); + assert(objects[0].solid == true); + + system.update(objects, 2000); + assert(objects[0].solid == false); + + system.update(objects, 3000); + assert(objects[0].solid == true); + + system.update(objects, 1500); + assert(objects[0].solid == false); + + system.update(objects, 2500); + assert(objects[0].solid == false); + } + + void TestLightEffectIgnoresOtherTypes() + { + LightEffectSystem system; + + GameObject platform; + platform.type = GameObjectType::StaticPlatform; + platform.solid = true; + + GameObject hazard; + hazard.type = GameObjectType::Hazard; + hazard.solid = true; + + std::vector objects; + objects.push_back(platform); + objects.push_back(hazard); + + system.update(objects, 0); + assert(objects[0].solid == true); + assert(objects[1].solid == true); + + system.update(objects, 4095); + assert(objects[0].solid == true); + assert(objects[1].solid == true); + } + + void TestLevelLoaderBasic() + { + const uint16_t tiles[] = { + 0, 0, 0, + 0, 0, 0, + 1, 1, 1, + }; + + const ObjectSpawn spawns[] = { + { GameObjectType::Player, 1, 1, 0, 0, TriggerAction::None, 0 }, + { GameObjectType::StaticPlatform, 0, 2, 0, 0, TriggerAction::None, 0 }, + }; + + const LevelData data = { + tiles, + 3, + 3, + 16, + nullptr, + 0, + spawns, + 2, + Math::Vector2Int(16, 16), + 0, + 0, + 48, + 48 + }; + + Level level; + LevelLoader::load(level, data); + + assert(level.object_count() == 2); + assert(level.get_bounds().max_x == 48); + assert(level.get_bounds().max_y == 48); + assert(level.get_spawn_point().x == 16); + assert(level.get_spawn_point().y == 16); + + const RenderData::Tilemap& tm = level.get_tilemap(); + assert(tm.width == 3); + assert(tm.height == 3); + assert(tm.tiles == tiles); + } +} + +int main() +{ + TestGameObjectDefaults(); + TestGameObjectWorldCollider(); + TestLevelAddAndGet(); + TestLevelRemove(); + TestLevelQueryByType(); + TestLevelQueryRegion(); + TestLevelInactiveObjectsExcluded(); + TestLevelCheckpoints(); + TestAabbOverlap(); + TestGravityAccumulation(); + TestGroundDetection(); + TestOneWayPlatform(); + TestLightPlatformActivation(); + TestShadowPlatformActivation(); + TestLightDoorOpen(); + TestLightEffectIgnoresOtherTypes(); + TestLevelLoaderBasic(); + std::cout << "game_engine_tests: PASS\n"; + return 0; +} diff --git a/tests/render_pipeline_tests.cpp b/tests/render_pipeline_tests.cpp index 0916a5f..4f862f0 100644 --- a/tests/render_pipeline_tests.cpp +++ b/tests/render_pipeline_tests.cpp @@ -3,6 +3,7 @@ #include "FrameBuffer.h" #include "Image.h" #include "SpriteAssetLoader.h" +#include "SdlPhotoSensor.h" #include #include @@ -232,6 +233,32 @@ namespace assert(pixels[2] == 0x0000u); assert(pixels[3] == 0x0000u); } + + void TestSdlPhotoSensorClampAndRange() + { + Platform::SdlPhotoSensor sensor; + assert(sensor.init()); + + sensor.set_range(100, 3000); + assert(sensor.read_level() >= 100); + assert(sensor.read_level() <= 3000); + + sensor.set_level(50); + assert(sensor.read_level() == 100); + + sensor.set_level(5000); + assert(sensor.read_level() == 3000); + + sensor.set_level(1500); + assert(sensor.read_level() == 1500); + + sensor.set_step(100); + assert(sensor.is_open()); + + sensor.shutdown(); + assert(!sensor.is_open()); + assert(sensor.read_level() == 2048); + } } int main() @@ -245,6 +272,7 @@ int main() TestDrawTextUsesBitMaskAndColor(); TestDrawTextColorChangesPerCallAndBackgroundFill(); TestDrawTextClipsBitMaskToScreen(); + TestSdlPhotoSensorClampAndRange(); std::cout << "render_pipeline_tests: PASS\n"; return 0; }