搭建 LightGame 游戏框架
This commit is contained in:
parent
189072ba4e
commit
aecd5a46ed
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "IMX6U-Game",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
#include "Camera2D.h"
|
||||
#include <algorithm>
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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)
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
#include "GameStateManager.h"
|
||||
#include "DrawContext.h"
|
||||
#include "BitmapFont.h"
|
||||
#include "ButtonInput.h"
|
||||
#include <cstdio>
|
||||
|
||||
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<int32_t>(level) * (w - 2)) / max_level : 0;
|
||||
if (fill_w > 0)
|
||||
{
|
||||
const uint8_t r = static_cast<uint8_t>(255 * (max_level - level) / max_level);
|
||||
const uint8_t g = static_cast<uint8_t>(255 * level / max_level);
|
||||
ctx.fill_rect(x + 1, y + 1, fill_w, h - 2, RenderData::Color(r, g, 0, 255));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
#include "Level.h"
|
||||
#include <algorithm>
|
||||
|
||||
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<GameObject*> Level::get_objects_by_type(GameObjectType type)
|
||||
{
|
||||
std::vector<GameObject*> 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<const GameObject*> Level::get_objects_by_type(GameObjectType type) const
|
||||
{
|
||||
std::vector<const GameObject*> 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<GameObject*> Level::query_region(const RenderData::BoundingBox2D& region)
|
||||
{
|
||||
std::vector<GameObject*> 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<const GameObject*> Level::query_region(const RenderData::BoundingBox2D& region) const
|
||||
{
|
||||
std::vector<const GameObject*> 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_;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#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<GameObject> objects_;
|
||||
std::vector<Checkpoint> 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<GameObject*> get_objects_by_type(GameObjectType type);
|
||||
std::vector<const GameObject*> get_objects_by_type(GameObjectType type) const;
|
||||
|
||||
std::vector<GameObject*> query_region(const RenderData::BoundingBox2D& region);
|
||||
std::vector<const GameObject*> 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<Checkpoint>& get_checkpoints() const { return checkpoints_; }
|
||||
Math::Vector2Int get_active_checkpoint() const;
|
||||
|
||||
std::vector<GameObject>& get_all_objects() { return objects_; }
|
||||
const std::vector<GameObject>& get_all_objects() const { return objects_; }
|
||||
size_t object_count() const { return objects_.size(); }
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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;
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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&>(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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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();
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
#include "Physics2D.h"
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
|
||||
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<int32_t>(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<int32_t>(dt_ms);
|
||||
const int32_t dy = obj.velocity.y * static_cast<int32_t>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
#include "PlayerController.h"
|
||||
#include "ButtonInput.h"
|
||||
#include "PointerInput.h"
|
||||
#include <algorithm>
|
||||
|
||||
#ifndef USE_FRAMEBUFFER
|
||||
#include <SDL.h>
|
||||
#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<int32_t>(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<int32_t>(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<int32_t>(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<int32_t>(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<uint32_t>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#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;
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#include "LightEffectSystem.h"
|
||||
|
||||
namespace LightGame
|
||||
{
|
||||
void LightEffectSystem::update(std::vector<GameObject>& 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "GameObject.h"
|
||||
|
||||
namespace LightGame
|
||||
{
|
||||
class LightEffectSystem
|
||||
{
|
||||
public:
|
||||
void update(std::vector<GameObject>& 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);
|
||||
};
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
#include "LinuxPhotoSensor.h"
|
||||
#include <iostream>
|
||||
|
||||
#if defined(__linux__)
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#include <dirent.h>
|
||||
#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<uint16_t>(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<uint16_t>(value);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void LinuxPhotoSensor::shutdown()
|
||||
{
|
||||
opened_ = false;
|
||||
level_ = 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -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_; }
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
#include "SdlPhotoSensor.h"
|
||||
#include <SDL.h>
|
||||
#include <iostream>
|
||||
#include <algorithm>
|
||||
|
||||
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<uint32_t>(level_) + step_;
|
||||
level_ = static_cast<uint16_t>(std::min(next, static_cast<uint32_t>(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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -0,0 +1,439 @@
|
|||
#include "GameObject.h"
|
||||
#include "Level.h"
|
||||
#include "Physics2D.h"
|
||||
#include "LightEffectSystem.h"
|
||||
#include "LevelLoader.h"
|
||||
#include "LevelData.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <iostream>
|
||||
|
||||
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<Checkpoint&>(cps[0]).activated = true;
|
||||
|
||||
respawn = level.get_active_checkpoint();
|
||||
assert(respawn.x == 200);
|
||||
assert(respawn.y == 300);
|
||||
|
||||
const_cast<Checkpoint&>(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<GameObject> 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<GameObject> 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<GameObject> 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<GameObject> 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;
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
#include "FrameBuffer.h"
|
||||
#include "Image.h"
|
||||
#include "SpriteAssetLoader.h"
|
||||
#include "SdlPhotoSensor.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue