搭建 LightGame 游戏框架

This commit is contained in:
SepComet 2026-06-09 12:25:01 +08:00
parent 189072ba4e
commit aecd5a46ed
35 changed files with 3164 additions and 0 deletions

View File

@ -31,6 +31,7 @@ set(CORE_SOURCES
if(USE_FRAMEBUFFER) if(USE_FRAMEBUFFER)
list(APPEND CORE_SOURCES list(APPEND CORE_SOURCES
src/Core/Platform/FBDisplay.cpp src/Core/Platform/FBDisplay.cpp
src/Core/Platform/LinuxPhotoSensor.cpp
) )
else() else()
list(APPEND CORE_SOURCES list(APPEND CORE_SOURCES
@ -39,6 +40,7 @@ else()
src/Core/Platform/SdlAudioOutput.cpp src/Core/Platform/SdlAudioOutput.cpp
src/Core/Platform/SdlKeyboardButtonInput.cpp src/Core/Platform/SdlKeyboardButtonInput.cpp
src/Core/Platform/SdlPointerInput.cpp src/Core/Platform/SdlPointerInput.cpp
src/Core/Platform/SdlPhotoSensor.cpp
) )
endif() endif()
@ -135,6 +137,7 @@ endfunction()
add_subdirectory(src/Apps/Game) add_subdirectory(src/Apps/Game)
add_subdirectory(src/Apps/Demo) add_subdirectory(src/Apps/Demo)
add_subdirectory(src/Apps/LightGame)
if(BUILD_TESTING AND NOT USE_FRAMEBUFFER) if(BUILD_TESTING AND NOT USE_FRAMEBUFFER)
add_executable(render_pipeline_tests 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_link_libraries(render_pipeline_tests PRIVATE imx6u_core)
target_include_directories(render_pipeline_tests PRIVATE ${CORE_INCLUDE_DIRS}) 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) if(CMAKE_CONFIGURATION_TYPES)
foreach(config ${CMAKE_CONFIGURATION_TYPES}) foreach(config ${CMAKE_CONFIGURATION_TYPES})
string(TOUPPER "${config}" config_upper) string(TOUPPER "${config}" config_upper)
set_target_properties(render_pipeline_tests PROPERTIES set_target_properties(render_pipeline_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY_${config_upper} "${CMAKE_BINARY_DIR}/${config}" 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() endforeach()
else() else()
set_target_properties(render_pipeline_tests PROPERTIES set_target_properties(render_pipeline_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
) )
set_target_properties(game_engine_tests PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
)
endif() endif()
if(WIN32) if(WIN32)
@ -166,7 +189,9 @@ if(BUILD_TESTING AND NOT USE_FRAMEBUFFER)
if(MSVC) if(MSVC)
target_compile_options(render_pipeline_tests PRIVATE /utf-8 /W3) target_compile_options(render_pipeline_tests PRIVATE /utf-8 /W3)
target_compile_options(game_engine_tests PRIVATE /utf-8 /W3)
endif() endif()
add_test(NAME render_pipeline_tests COMMAND render_pipeline_tests) add_test(NAME render_pipeline_tests COMMAND render_pipeline_tests)
add_test(NAME game_engine_tests COMMAND game_engine_tests)
endif() endif()

6
package-lock.json generated Normal file
View File

@ -0,0 +1,6 @@
{
"name": "IMX6U-Game",
"lockfileVersion": 3,
"requires": true,
"packages": {}
}

View File

@ -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)

View File

@ -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;
}
}

View File

@ -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_; }
};
}

View File

@ -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)
);
}
};
}

View File

@ -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));
}
}
}

View File

@ -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);
};
}

View File

@ -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_;
}
}

View File

@ -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(); }
};
}

View File

@ -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;
};
}

View File

@ -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);
}
}
}

View File

@ -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);
};
}

View File

@ -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);
}
}
}

View File

@ -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);
};
}

View File

@ -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++;
}
}
}
}

View File

@ -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();
};
}

View File

@ -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;
}
}

View File

@ -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);
};
}

View File

@ -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;
}
}

View File

@ -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);
};
}

View File

@ -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
};
}
}

View File

@ -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
};
}
}

View File

@ -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
};
}
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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);
};
}

View File

@ -5,11 +5,13 @@
#include "AlsaAudioOutput.h" #include "AlsaAudioOutput.h"
#include "EvdevButtonInput.h" #include "EvdevButtonInput.h"
#include "EvdevTouchInput.h" #include "EvdevTouchInput.h"
#include "LinuxPhotoSensor.h"
#else #else
#include "SdlAudioInput.h" #include "SdlAudioInput.h"
#include "SdlAudioOutput.h" #include "SdlAudioOutput.h"
#include "SdlKeyboardButtonInput.h" #include "SdlKeyboardButtonInput.h"
#include "SdlPointerInput.h" #include "SdlPointerInput.h"
#include "SdlPhotoSensor.h"
#endif #endif
namespace Platform namespace Platform
@ -19,10 +21,12 @@ namespace Platform
typedef AlsaAudioOutput DefaultAudioOutput; typedef AlsaAudioOutput DefaultAudioOutput;
typedef EvdevButtonInput DefaultButtonInput; typedef EvdevButtonInput DefaultButtonInput;
typedef EvdevTouchInput DefaultPointerInput; typedef EvdevTouchInput DefaultPointerInput;
typedef LinuxPhotoSensor DefaultPhotoSensor;
#else #else
typedef SdlAudioInput DefaultAudioInput; typedef SdlAudioInput DefaultAudioInput;
typedef SdlAudioOutput DefaultAudioOutput; typedef SdlAudioOutput DefaultAudioOutput;
typedef SdlKeyboardButtonInput DefaultButtonInput; typedef SdlKeyboardButtonInput DefaultButtonInput;
typedef SdlPointerInput DefaultPointerInput; typedef SdlPointerInput DefaultPointerInput;
typedef SdlPhotoSensor DefaultPhotoSensor;
#endif #endif
} }

View File

@ -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;
};
}

View File

@ -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;
}
}

View File

@ -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_; }
};
}

View File

@ -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;
}
}

View File

@ -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);
};
}

439
tests/game_engine_tests.cpp Normal file
View File

@ -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;
}

View File

@ -3,6 +3,7 @@
#include "FrameBuffer.h" #include "FrameBuffer.h"
#include "Image.h" #include "Image.h"
#include "SpriteAssetLoader.h" #include "SpriteAssetLoader.h"
#include "SdlPhotoSensor.h"
#include <cassert> #include <cassert>
#include <cstdio> #include <cstdio>
@ -232,6 +233,32 @@ namespace
assert(pixels[2] == 0x0000u); assert(pixels[2] == 0x0000u);
assert(pixels[3] == 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() int main()
@ -245,6 +272,7 @@ int main()
TestDrawTextUsesBitMaskAndColor(); TestDrawTextUsesBitMaskAndColor();
TestDrawTextColorChangesPerCallAndBackgroundFill(); TestDrawTextColorChangesPerCallAndBackgroundFill();
TestDrawTextClipsBitMaskToScreen(); TestDrawTextClipsBitMaskToScreen();
TestSdlPhotoSensorClampAndRange();
std::cout << "render_pipeline_tests: PASS\n"; std::cout << "render_pipeline_tests: PASS\n";
return 0; return 0;
} }