添加 LightGame 关卡编辑器并修复死亡扣命、光感门阈值与渲染问题

- 新增 LevelEditor:ImGui 面板支持 tile 绘制、对象放置、选择、擦除、网格/碰撞体可视化、关卡导出为头文件
- 修复 LevelRenderer::draw 中 draw_tilemap 错误传入负 screen 坐标导致摄像机移动后 tile 消失
- 修复 Preview Light 滑块用 reinterpret_cast 将 uint16_t 强转为 int* 写入 4 字节覆盖相邻 bool 成员
- 修复编辑器放置光感门缺少 light_threshold 默认值导致不响应亮度变化
- 统一 LightPlatform[3192,4095] ShadowPlatform[0,1023] Door[1023,3192] 阈值到关卡数据与编辑器默认值
- 修复 SDL_StopTextInput 导致 ImGui InputText 无法输入及键盘状态异常
- 修复死亡状态每帧扣命导致一次死亡直接 GameOver
- 集成 ImGui (SDL2 Renderer 后端) 到构建系统
This commit is contained in:
2026-06-11 19:30:04 +08:00
parent d49aef8c0f
commit 05d7d9783e
41 changed files with 62887 additions and 338 deletions
+2 -1
View File
@@ -8,9 +8,10 @@ set(LIGHTGAME_SOURCES
src/engine/GameStateManager.cpp
src/engine/LightGameApp.cpp
src/systems/LightEffectSystem.cpp
src/editor/LevelEditor.cpp
src/main.cpp
)
add_executable(IMX6U-LightGame ${LIGHTGAME_SOURCES})
target_include_directories(IMX6U-LightGame PRIVATE src/engine src/systems src/levels generated)
target_include_directories(IMX6U-LightGame PRIVATE src/engine src/systems src/levels src/editor generated)
imx6u_configure_app_target(IMX6U-LightGame)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,709 @@
#include "LevelEditor.h"
#include "DrawContext.h"
#include "BitmapFont.h"
#include "LevelLoader.h"
#include "imgui.h"
#include "tile_atlas.h"
#include <algorithm>
#include <cstring>
#include <cstdio>
#include <iostream>
#include <sstream>
namespace LightGame
{
namespace
{
static RenderData::Sprite MakeTileSprite(const RenderData::Image* atlas, int32_t tile_id,
int32_t tile_size, int32_t atlas_columns)
{
const int32_t col = tile_id % atlas_columns;
const int32_t row = tile_id / atlas_columns;
return RenderData::Sprite(atlas, col * tile_size, row * tile_size, tile_size, tile_size);
}
static const RenderData::Sprite spr_platform = MakeTileSprite(&tile_atlas_image, 3, 32, 4);
static const RenderData::Sprite spr_spike = MakeTileSprite(&tile_atlas_image, 4, 32, 4);
static const RenderData::Sprite spr_light_platform = MakeTileSprite(&tile_atlas_image, 6, 32, 4);
static const RenderData::Sprite spr_shadow_platform = MakeTileSprite(&tile_atlas_image, 8, 32, 4);
static const RenderData::Sprite spr_door = MakeTileSprite(&tile_atlas_image, 9, 32, 4);
static const RenderData::Sprite spr_coin = MakeTileSprite(&tile_atlas_image, 11, 32, 4);
static const RenderData::Sprite spr_checkpoint = MakeTileSprite(&tile_atlas_image, 13, 32, 4);
static const char* ObjTypeToString(GameObjectType type)
{
switch (type)
{
case GameObjectType::Player: return "Player";
case GameObjectType::StaticPlatform: return "StaticPlatform";
case GameObjectType::LightPlatform: return "LightPlatform";
case GameObjectType::ShadowPlatform: return "ShadowPlatform";
case GameObjectType::Hazard: return "Hazard";
case GameObjectType::Collectible: return "Collectible";
case GameObjectType::Trigger: return "Trigger";
case GameObjectType::Door: return "Door";
}
return "StaticPlatform";
}
static const char* TriggerActionToString(TriggerAction action)
{
switch (action)
{
case TriggerAction::None: return "None";
case TriggerAction::LevelComplete: return "LevelComplete";
case TriggerAction::Checkpoint: return "Checkpoint";
case TriggerAction::Death: return "Death";
}
return "None";
}
}
LevelEditor::LevelEditor(int32_t screen_w, int32_t screen_h)
: screen_width_(screen_w),
screen_height_(screen_h),
active_(false),
should_quit_(false),
current_tool_(Tool::TileBrush),
selected_tile_id_(1),
selected_object_type_(GameObjectType::StaticPlatform),
selected_object_id_(0),
preview_light_level_(2048),
show_grid_(true),
show_colliders_(false),
map_width_(0),
map_height_(0),
tile_size_(32),
mouse_was_down_(false),
last_tile_x_(-1),
last_tile_y_(-1),
pending_load_level_(-1)
{
camera_.configure(screen_width_, screen_height_);
std::memset(export_name_, 0, sizeof(export_name_));
export_open_ = false;
}
void LevelEditor::set_active(bool active)
{
active_ = active;
if (active)
{
camera_.configure(screen_width_, screen_height_);
}
}
void LevelEditor::load_from_data(const LevelData& data)
{
level_ = Level();
LevelLoader::load(level_, data);
map_width_ = data.map_width;
map_height_ = data.map_height;
tile_size_ = data.tile_size;
camera_.set_level_bounds(LevelBounds(0, 0, map_width_ * tile_size_, map_height_ * tile_size_));
camera_.snap_to(level_.get_spawn_point());
selected_object_id_ = 0;
}
void LevelEditor::new_level(int32_t width, int32_t height, int32_t tile_size)
{
level_ = Level();
map_width_ = width;
map_height_ = height;
tile_size_ = tile_size;
const int32_t total = width * height;
std::vector<uint16_t> empty_tiles(static_cast<size_t>(total), 0);
RenderData::Tilemap tilemap;
tilemap.tiles = empty_tiles.data();
tilemap.width = width;
tilemap.height = height;
tilemap.tile_w = tile_size;
tilemap.tile_h = tile_size;
tilemap.atlas = &tile_atlas_image;
tilemap.atlas_columns = tile_atlas_columns;
level_.set_tilemap(tilemap);
level_.init_tile_buffer(empty_tiles.data(), total);
level_.set_bounds(LevelBounds(0, 0, width * tile_size, height * tile_size));
level_.set_spawn_point(Math::Vector2Int(tile_size * 2, tile_size * 2));
camera_.set_level_bounds(LevelBounds(0, 0, width * tile_size, height * tile_size));
camera_.snap_to(level_.get_spawn_point());
selected_object_id_ = 0;
}
void LevelEditor::update(uint32_t dt_ms, Platform::IKeyboardState* keyboard, Platform::IPointerInput* pointer)
{
if (!active_) return;
update_camera(dt_ms, keyboard, pointer);
handle_mouse(pointer);
}
void LevelEditor::update_camera(uint32_t dt_ms, Platform::IKeyboardState* keyboard, Platform::IPointerInput* pointer)
{
(void)dt_ms;
const int32_t speed = 8;
if (keyboard)
{
if (keyboard->is_key_down(Platform::KEY_A))
camera_.set_position(camera_.get_x() - speed, camera_.get_y());
if (keyboard->is_key_down(Platform::KEY_D))
camera_.set_position(camera_.get_x() + speed, camera_.get_y());
if (keyboard->is_key_down(Platform::KEY_W))
camera_.set_position(camera_.get_x(), camera_.get_y() - speed);
if (keyboard->is_key_down(Platform::KEY_S))
camera_.set_position(camera_.get_x(), camera_.get_y() + speed);
}
if (pointer && pointer->is_open())
{
// 中键拖拽可后续扩展
(void)pointer;
}
}
void LevelEditor::handle_mouse(Platform::IPointerInput* pointer)
{
if (!pointer || !pointer->is_open()) return;
if (ImGui::GetIO().WantCaptureMouse) return;
const bool is_down = pointer->is_down();
const bool was_pressed = pointer->was_pressed();
const int32_t px = pointer->get_x();
const int32_t py = pointer->get_y();
const int32_t world_x = px + camera_.get_x();
const int32_t world_y = py + camera_.get_y();
const int32_t tile_x = world_to_tile_x(world_x);
const int32_t tile_y = world_to_tile_y(world_y);
if (current_tool_ == Tool::TileBrush || current_tool_ == Tool::Eraser)
{
if (is_down && is_valid_tile(tile_x, tile_y))
{
const uint16_t tile_id = (current_tool_ == Tool::Eraser) ? 0 : static_cast<uint16_t>(selected_tile_id_);
paint_tile(tile_x, tile_y, tile_id);
}
}
else if (current_tool_ == Tool::ObjectPlace)
{
if (was_pressed && is_valid_tile(tile_x, tile_y))
{
place_object(tile_x, tile_y);
}
}
else if (current_tool_ == Tool::Select)
{
if (was_pressed)
{
select_at(world_x, world_y);
}
}
}
void LevelEditor::paint_tile(int32_t tile_x, int32_t tile_y, uint16_t tile_id)
{
uint16_t* tiles = level_.get_mutable_tiles();
if (!tiles) return;
const int32_t idx = tile_y * map_width_ + tile_x;
tiles[idx] = tile_id;
uint16_t* orig = const_cast<uint16_t*>(level_.get_original_tiles());
if (orig) orig[idx] = tile_id;
}
void LevelEditor::place_object(int32_t tile_x, int32_t tile_y)
{
const int32_t ts = tile_size_;
GameObject obj;
obj.type = selected_object_type_;
obj.position = Math::Vector2Int(tile_x * ts, tile_y * ts);
obj.active = true;
switch (selected_object_type_)
{
case GameObjectType::Player:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(8, 0), Math::Vector2Int(24, 32));
obj.solid = false;
level_.set_spawn_point(obj.position);
break;
case GameObjectType::StaticPlatform:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = true;
obj.sprite = &spr_platform;
break;
case GameObjectType::LightPlatform:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = false;
obj.light_threshold = LightThreshold(3192, 4095);
obj.sprite = &spr_light_platform;
break;
case GameObjectType::ShadowPlatform:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = true;
obj.light_threshold = LightThreshold(0, 1023);
obj.sprite = &spr_shadow_platform;
break;
case GameObjectType::Door:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = true;
obj.light_threshold = LightThreshold(1023, 3192);
obj.sprite = &spr_door;
break;
case GameObjectType::Hazard:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, ts / 2), Math::Vector2Int(ts, ts));
obj.solid = false;
obj.sprite = &spr_spike;
break;
case GameObjectType::Collectible:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(ts / 4, ts / 4), Math::Vector2Int(ts * 3 / 4, ts * 3 / 4));
obj.solid = false;
obj.sprite = &spr_coin;
break;
case GameObjectType::Trigger:
obj.collider = RenderData::BoundingBox2D(Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = false;
obj.trigger_action = TriggerAction::Checkpoint;
obj.sprite = &spr_checkpoint;
level_.add_checkpoint(Checkpoint(obj.position.x, obj.position.y));
break;
default:
break;
}
level_.add_object(obj);
}
void LevelEditor::select_at(int32_t world_x, int32_t world_y)
{
selected_object_id_ = 0;
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 box = obj.get_world_collider();
if (world_x >= box.min.x && world_x < box.max.x &&
world_y >= box.min.y && world_y < box.max.y)
{
selected_object_id_ = obj.id;
return;
}
}
}
void LevelEditor::draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font)
{
if (!active_) return;
ctx.clear_color(RenderData::Color(20, 20, 40, 255));
light_system_.update(level_.get_all_objects(), preview_light_level_);
light_system_.update_tilemap(level_, preview_light_level_);
renderer_.draw(ctx, level_, camera_, light_system_, preview_light_level_);
if (show_grid_) draw_grid(ctx);
draw_spawn_point(ctx);
if (selected_object_id_ != 0) draw_selection_highlight(ctx);
if (show_colliders_) renderer_.draw_debug(ctx, level_, camera_, font);
}
void LevelEditor::draw_ui()
{
if (!active_) return;
draw_ui_main_menu();
draw_ui_toolbar();
draw_ui_properties();
draw_ui_status_bar();
draw_ui_export_dialog();
}
void LevelEditor::draw_grid(Core::DrawContext& ctx)
{
const int32_t ts = tile_size_;
const int32_t cam_x = camera_.get_x();
const int32_t cam_y = camera_.get_y();
const int32_t start_x = (cam_x / ts) * ts;
const int32_t start_y = (cam_y / ts) * ts;
const int32_t end_x = cam_x + screen_width_ + ts;
const int32_t end_y = cam_y + screen_height_ + ts;
const RenderData::Color grid_color(80, 80, 80, 128);
for (int32_t x = start_x; x < end_x; x += ts)
{
const int32_t sx = x - cam_x;
ctx.draw_line(Math::Vector2Int(sx, 0), Math::Vector2Int(sx, screen_height_), grid_color);
}
for (int32_t y = start_y; y < end_y; y += ts)
{
const int32_t sy = y - cam_y;
ctx.draw_line(Math::Vector2Int(0, sy), Math::Vector2Int(screen_width_, sy), grid_color);
}
}
void LevelEditor::draw_spawn_point(Core::DrawContext& ctx)
{
const Math::Vector2Int sp = level_.get_spawn_point();
const int32_t sx = sp.x - camera_.get_x();
const int32_t sy = sp.y - camera_.get_y();
if (sx < -16 || sx > screen_width_ + 16 || sy < -16 || sy > screen_height_ + 16) return;
const RenderData::Color color(0, 255, 0, 255);
ctx.draw_line(Math::Vector2Int(sx - 8, sy - 8), Math::Vector2Int(sx + 8, sy + 8), color);
ctx.draw_line(Math::Vector2Int(sx + 8, sy - 8), Math::Vector2Int(sx - 8, sy + 8), color);
}
void LevelEditor::draw_selection_highlight(Core::DrawContext& ctx)
{
GameObject* obj = level_.get_object(selected_object_id_);
if (!obj || !obj->active) return;
const RenderData::BoundingBox2D box = obj->get_world_collider();
const int32_t sx = box.min.x - camera_.get_x();
const int32_t sy = box.min.y - camera_.get_y();
const int32_t w = box.max.x - box.min.x;
const int32_t h = box.max.y - box.min.y;
if (sx + w < 0 || sx > screen_width_ || sy + h < 0 || sy > screen_height_) return;
const RenderData::Color color(255, 255, 0, 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);
}
void LevelEditor::draw_ui_main_menu()
{
if (ImGui::BeginMainMenuBar())
{
if (ImGui::BeginMenu("File"))
{
if (ImGui::MenuItem("New Level", "Ctrl+N"))
{
new_level(25, 8, 32);
}
if (ImGui::BeginMenu("Load Level"))
{
if (ImGui::MenuItem("Level 1")) pending_load_level_ = 0;
if (ImGui::MenuItem("Level 2")) pending_load_level_ = 1;
if (ImGui::MenuItem("Level 3")) pending_load_level_ = 2;
ImGui::EndMenu();
}
if (ImGui::MenuItem("Export to Header..."))
{
export_open_ = true;
}
if (ImGui::MenuItem("Close Editor", "Esc"))
{
should_quit_ = true;
}
ImGui::EndMenu();
}
if (ImGui::BeginMenu("View"))
{
ImGui::MenuItem("Show Grid", nullptr, &show_grid_);
ImGui::MenuItem("Show Colliders", nullptr, &show_colliders_);
ImGui::EndMenu();
}
ImGui::EndMainMenuBar();
}
}
void LevelEditor::draw_ui_toolbar()
{
ImGui::SetNextWindowPos(ImVec2(0, 20));
ImGui::SetNextWindowSize(ImVec2(140, static_cast<float>(screen_height_ - 20)));
ImGui::Begin("Tools", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar);
ImGui::Text("Tool");
if (ImGui::RadioButton("Tile Brush", current_tool_ == Tool::TileBrush))
current_tool_ = Tool::TileBrush;
if (ImGui::RadioButton("Object Place", current_tool_ == Tool::ObjectPlace))
current_tool_ = Tool::ObjectPlace;
if (ImGui::RadioButton("Select", current_tool_ == Tool::Select))
current_tool_ = Tool::Select;
if (ImGui::RadioButton("Eraser", current_tool_ == Tool::Eraser))
current_tool_ = Tool::Eraser;
ImGui::Separator();
if (current_tool_ == Tool::TileBrush)
{
ImGui::Text("Tile Type");
const char* tile_names[] = { "Empty", "Ground Top", "Ground Fill", "Platform", "Spike" };
for (int i = 0; i < 5; ++i)
{
if (ImGui::RadioButton(tile_names[i], selected_tile_id_ == i))
selected_tile_id_ = i;
}
}
else if (current_tool_ == Tool::ObjectPlace)
{
ImGui::Text("Object Type");
struct ObjEntry { GameObjectType type; const char* name; };
static const ObjEntry entries[] = {
{ GameObjectType::Player, "Player" },
{ GameObjectType::StaticPlatform, "Platform" },
{ GameObjectType::LightPlatform, "Light Platform" },
{ GameObjectType::ShadowPlatform, "Shadow Platform" },
{ GameObjectType::Door, "Door" },
{ GameObjectType::Hazard, "Hazard" },
{ GameObjectType::Collectible, "Collectible" },
{ GameObjectType::Trigger, "Trigger" },
};
for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); ++i)
{
if (ImGui::RadioButton(entries[i].name, selected_object_type_ == entries[i].type))
selected_object_type_ = entries[i].type;
}
}
else if (current_tool_ == Tool::Select)
{
ImGui::Text("Click to select");
if (selected_object_id_ != 0)
{
if (ImGui::Button("Deselect"))
selected_object_id_ = 0;
}
}
else if (current_tool_ == Tool::Eraser)
{
ImGui::Text("Click to erase tiles");
}
ImGui::Separator();
ImGui::Text("Preview Light");
{
int light_val = static_cast<int>(preview_light_level_);
if (ImGui::SliderInt("##light", &light_val, 0, 4095))
preview_light_level_ = static_cast<uint16_t>(light_val);
}
ImGui::End();
}
void LevelEditor::draw_ui_properties()
{
if (selected_object_id_ == 0) return;
GameObject* obj = level_.get_object(selected_object_id_);
if (!obj) return;
ImGui::SetNextWindowPos(ImVec2(static_cast<float>(screen_width_ - 220), 20));
ImGui::SetNextWindowSize(ImVec2(220, 300));
ImGui::Begin("Properties", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse);
ImGui::Text("Type: %s", ObjTypeToString(obj->type));
ImGui::Text("ID: %u", obj->id);
ImGui::InputInt("Pos X", &obj->position.x);
ImGui::InputInt("Pos Y", &obj->position.y);
if (obj->type == GameObjectType::LightPlatform ||
obj->type == GameObjectType::ShadowPlatform ||
obj->type == GameObjectType::Door)
{
int min_l = obj->light_threshold.min_level;
int max_l = obj->light_threshold.max_level;
ImGui::SliderInt("Light Min", &min_l, 0, 4095);
ImGui::SliderInt("Light Max", &max_l, 0, 4095);
obj->light_threshold.min_level = static_cast<uint16_t>(min_l);
obj->light_threshold.max_level = static_cast<uint16_t>(max_l);
}
if (obj->type == GameObjectType::Trigger)
{
const char* actions[] = { "None", "LevelComplete", "Checkpoint", "Death" };
int action_idx = static_cast<int>(obj->trigger_action);
if (ImGui::Combo("Action", &action_idx, actions, 4))
{
obj->trigger_action = static_cast<TriggerAction>(action_idx);
}
}
if (ImGui::Button("Delete", ImVec2(-1, 0)))
{
level_.remove_object(selected_object_id_);
selected_object_id_ = 0;
}
ImGui::End();
}
void LevelEditor::draw_ui_status_bar()
{
ImGui::SetNextWindowPos(ImVec2(140, static_cast<float>(screen_height_ - 24)));
ImGui::SetNextWindowSize(ImVec2(static_cast<float>(screen_width_ - 140), 24));
ImGui::Begin("Status", nullptr,
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize |
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoTitleBar |
ImGuiWindowFlags_NoScrollbar);
ImGui::Text("Map: %dx%d | Tile: %d | Objects: %zu | Light: %d",
map_width_, map_height_, tile_size_,
level_.object_count(), preview_light_level_);
ImGui::End();
}
void LevelEditor::draw_ui_export_dialog()
{
if (!export_open_) return;
ImGui::SetNextWindowPos(ImVec2(static_cast<float>(screen_width_) * 0.5f - 200, static_cast<float>(screen_height_) * 0.5f - 100), ImGuiCond_Appearing);
ImGui::SetNextWindowSize(ImVec2(400, 200));
if (ImGui::Begin("Export Level", &export_open_))
{
ImGui::InputText("Level Name", export_name_, sizeof(export_name_));
if (ImGui::Button("Export", ImVec2(120, 0)))
{
if (std::strlen(export_name_) > 0)
{
const std::string header = export_to_header(export_name_);
const std::string filename = std::string(export_name_) + "Data.h";
std::cout << "[Editor] Export: name='" << export_name_ << "'" << std::endl;
std::cout << "[Editor] Export: filename='" << filename << "'" << std::endl;
std::cout << "[Editor] Export: header_size=" << header.size() << std::endl;
FILE* f = nullptr;
errno_t err = fopen_s(&f, filename.c_str(), "w");
if (err == 0 && f)
{
size_t written = std::fwrite(header.c_str(), 1, header.size(), f);
std::fclose(f);
std::cout << "[Editor] Export: written=" << written << " bytes, OK" << std::endl;
}
else
{
std::cerr << "[Editor] Export: fopen_s failed, err=" << err << std::endl;
}
export_open_ = false;
}
else
{
std::cout << "[Editor] Export: name empty, skipped" << std::endl;
}
}
ImGui::SameLine();
if (ImGui::Button("Cancel", ImVec2(120, 0)))
{
export_open_ = false;
}
}
ImGui::End();
}
int32_t LevelEditor::world_to_tile_x(int32_t world_x) const
{
return world_x / tile_size_;
}
int32_t LevelEditor::world_to_tile_y(int32_t world_y) const
{
return world_y / tile_size_;
}
bool LevelEditor::is_valid_tile(int32_t tx, int32_t ty) const
{
return tx >= 0 && tx < map_width_ && ty >= 0 && ty < map_height_;
}
RenderData::Sprite LevelEditor::MakeObjSprite(int32_t tile_id)
{
const int32_t col = tile_id % tile_atlas_columns;
const int32_t row = tile_id / tile_atlas_columns;
return RenderData::Sprite(&tile_atlas_image, col * 32, row * 32, 32, 32);
}
std::string LevelEditor::export_to_header(const std::string& level_name) const
{
std::ostringstream oss;
oss << "#pragma once\n\n";
oss << "#include \"LevelData.h\"\n";
oss << "#include \"tile_atlas.h\"\n\n";
oss << "namespace LightGame\n";
oss << "{\n";
oss << " namespace " << level_name << "\n";
oss << " {\n";
oss << " static const int32_t kMapWidth = " << map_width_ << ";\n";
oss << " static const int32_t kMapHeight = " << map_height_ << ";\n";
oss << " static const int32_t kTileSize = " << tile_size_ << ";\n\n";
oss << " static const uint16_t tiles[] = {\n";
const uint16_t* tiles = level_.get_original_tiles();
if (tiles)
{
for (int32_t y = 0; y < map_height_; ++y)
{
oss << " ";
for (int32_t x = 0; x < map_width_; ++x)
{
oss << tiles[y * map_width_ + x];
if (x < map_width_ - 1 || y < map_height_ - 1) oss << ",";
}
oss << "\n";
}
}
oss << " };\n\n";
const auto& objects = level_.get_all_objects();
int32_t spawn_count = 0;
for (size_t i = 0; i < objects.size(); ++i)
if (objects[i].active) ++spawn_count;
if (spawn_count > 0)
{
oss << " static const ObjectSpawn spawns[] = {\n";
for (size_t i = 0; i < objects.size(); ++i)
{
const GameObject& obj = objects[i];
if (!obj.active) continue;
const int32_t tx = obj.position.x / tile_size_;
const int32_t ty = obj.position.y / tile_size_;
oss << " { GameObjectType::" << ObjTypeToString(obj.type)
<< ", " << tx << ", " << ty << ", "
<< obj.light_threshold.min_level << ", "
<< obj.light_threshold.max_level << ", ";
oss << "TriggerAction::" << TriggerActionToString(obj.trigger_action) << ", "
<< obj.trigger_target_id << " },\n";
}
oss << " };\n\n";
}
oss << " static const RenderData::Image kAtlas(tile_atlas_pixels, tile_atlas_width, tile_atlas_height);\n\n";
oss << " static const LevelData data = {\n";
oss << " tiles,\n";
oss << " kMapWidth,\n";
oss << " kMapHeight,\n";
oss << " kTileSize,\n";
oss << " &kAtlas,\n";
oss << " tile_atlas_columns,\n";
if (spawn_count > 0)
oss << " spawns,\n";
else
oss << " nullptr,\n";
oss << " " << spawn_count << ",\n";
oss << " nullptr,\n";
oss << " 0,\n";
const Math::Vector2Int sp = level_.get_spawn_point();
oss << " Math::Vector2Int(" << sp.x << ", " << sp.y << "),\n";
const LevelBounds b = level_.get_bounds();
oss << " " << b.min_x << ",\n";
oss << " " << b.min_y << ",\n";
oss << " " << b.max_x << ",\n";
oss << " " << b.max_y << "\n";
oss << " };\n";
oss << " }\n";
oss << "}\n";
return oss.str();
}
}
@@ -0,0 +1,99 @@
#pragma once
#include <cstdint>
#include <string>
#include "Level.h"
#include "Camera2D.h"
#include "LevelRenderer.h"
#include "LightEffectSystem.h"
#include "LevelData.h"
#include "IKeyboardState.h"
#include "PointerInput.h"
#include "Sprite.h"
namespace Core { class DrawContext; }
namespace RenderData { struct BitmapFont; }
namespace LightGame
{
class LevelEditor
{
public:
enum class Tool { TileBrush, ObjectPlace, Select, Eraser };
LevelEditor(int32_t screen_w, int32_t screen_h);
void load_from_data(const LevelData& data);
void new_level(int32_t width, int32_t height, int32_t tile_size);
void update(uint32_t dt_ms, Platform::IKeyboardState* keyboard, Platform::IPointerInput* pointer);
void draw(Core::DrawContext& ctx, const RenderData::BitmapFont& font);
void draw_ui();
bool is_active() const { return active_; }
void set_active(bool active);
bool should_quit() const { return should_quit_; }
void set_pending_load(int32_t level_index) { pending_load_level_ = level_index; }
int32_t get_pending_load() const { return pending_load_level_; }
void clear_pending_load() { pending_load_level_ = -1; }
std::string export_to_header(const std::string& level_name) const;
private:
void update_camera(uint32_t dt_ms, Platform::IKeyboardState* keyboard, Platform::IPointerInput* pointer);
void handle_mouse(Platform::IPointerInput* pointer);
void paint_tile(int32_t tile_x, int32_t tile_y, uint16_t tile_id);
void place_object(int32_t tile_x, int32_t tile_y);
void select_at(int32_t world_x, int32_t world_y);
void erase_at(int32_t world_x, int32_t world_y);
void draw_grid(Core::DrawContext& ctx);
void draw_spawn_point(Core::DrawContext& ctx);
void draw_selection_highlight(Core::DrawContext& ctx);
void draw_ui_main_menu();
void draw_ui_toolbar();
void draw_ui_properties();
void draw_ui_status_bar();
void draw_ui_export_dialog();
int32_t world_to_tile_x(int32_t world_x) const;
int32_t world_to_tile_y(int32_t world_y) const;
bool is_valid_tile(int32_t tx, int32_t ty) const;
static RenderData::Sprite MakeObjSprite(int32_t tile_id);
int32_t screen_width_;
int32_t screen_height_;
Level level_;
Camera2D camera_;
LevelRenderer renderer_;
LightEffectSystem light_system_;
bool active_;
bool should_quit_;
Tool current_tool_;
int32_t selected_tile_id_;
GameObjectType selected_object_type_;
uint32_t selected_object_id_;
uint16_t preview_light_level_;
bool show_grid_;
bool show_colliders_;
int32_t map_width_;
int32_t map_height_;
int32_t tile_size_;
bool mouse_was_down_;
int32_t last_tile_x_;
int32_t last_tile_y_;
int32_t pending_load_level_;
mutable char export_name_[64];
mutable bool export_open_;
};
}
+1
View File
@@ -27,6 +27,7 @@ namespace LightGame
void follow(const Math::Vector2Int& target);
void snap_to(const Math::Vector2Int& target);
void set_position(int32_t x, int32_t y) { position_.x = x; position_.y = y; }
Math::Vector2Int get_position() const { return position_; }
int32_t get_x() const { return position_.x; }
int32_t get_y() const { return position_.y; }
@@ -11,7 +11,7 @@ namespace LightGame
if (tilemap.tiles != nullptr && tilemap.atlas != nullptr)
{
ctx.draw_tilemap(tilemap,
-camera.get_x(), -camera.get_y(),
0, 0,
camera.get_screen_width(), camera.get_screen_height(),
camera.get_x(), camera.get_y());
}
+13 -4
View File
@@ -50,7 +50,8 @@ namespace LightGame
manual_light_level_(2048),
light_step_(256),
has_manual_override_(false),
debug_mode_(false)
debug_mode_(false),
death_processed_(false)
{
camera_.configure(screen_width, screen_height);
camera_.set_dead_zone(60, 40);
@@ -234,12 +235,20 @@ namespace LightGame
if (player_controller_.is_dead())
{
state_manager_.get_hud().lives--;
if (state_manager_.get_hud().lives <= 0)
if (!death_processed_)
{
state_manager_.set_state(GameState::GameOver);
state_manager_.get_hud().lives--;
death_processed_ = true;
if (state_manager_.get_hud().lives <= 0)
{
state_manager_.set_state(GameState::GameOver);
}
}
}
else
{
death_processed_ = false;
}
}
check_triggers();
@@ -57,6 +57,7 @@ namespace LightGame
bool has_manual_override_;
bool debug_mode_;
bool death_processed_;
public:
LightGameApp(
+70 -57
View File
@@ -1,57 +1,70 @@
#pragma once
#include "LevelData.h"
#include "tile_atlas.h"
namespace LightGame
{
namespace Level1
{
static const int32_t kMapWidth = 25;
static const int32_t kMapHeight = 8;
static const int32_t kTileSize = 32;
// Tile IDs: 0=empty 1=ground_top 2=ground_fill 3=platform 4=spike
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,
1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,
};
static const ObjectSpawn spawns[] = {
{ GameObjectType::Player, 2, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 5, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 4, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 18, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 12, 7, 0, 0, TriggerAction::Death, 0 },
{ GameObjectType::Hazard, 13, 7, 0, 0, TriggerAction::Death, 0 },
{ GameObjectType::Hazard, 14, 7, 0, 0, TriggerAction::Death, 0 },
{ GameObjectType::Trigger, 23, 5, 0, 0, TriggerAction::LevelComplete, 0 },
};
static const RenderData::Image kAtlas(tile_atlas_pixels, tile_atlas_width, tile_atlas_height);
static const LevelData data = {
tiles,
kMapWidth,
kMapHeight,
kTileSize,
&kAtlas,
tile_atlas_columns,
spawns,
8,
nullptr,
0,
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
0,
0,
kMapWidth * kTileSize,
kMapHeight * kTileSize
};
}
}
#pragma once
#include "LevelData.h"
#include "tile_atlas.h"
namespace LightGame
{
namespace Level1
{
static const int32_t kMapWidth = 25;
static const int32_t kMapHeight = 8;
static const int32_t kTileSize = 32;
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,
1,1,1,1,0,0,0,0,1,1,0,0,0,0,0,0,1,1,0,0,0,0,0,1,1,
2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2
};
static const ObjectSpawn spawns[] = {
{ GameObjectType::Player, 2, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 5, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 4, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 18, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 12, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 13, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 14, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Trigger, 23, 5, 0, 0, TriggerAction::LevelComplete, 0 },
{ GameObjectType::Hazard, 4, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 6, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 5, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 7, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 10, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 11, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 15, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 18, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 19, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 20, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 20, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 21, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 21, 6, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Hazard, 22, 6, 0, 0, TriggerAction::None, 0 },
};
static const RenderData::Image kAtlas(tile_atlas_pixels, tile_atlas_width, tile_atlas_height);
static const LevelData data = {
tiles,
kMapWidth,
kMapHeight,
kTileSize,
&kAtlas,
tile_atlas_columns,
spawns,
22,
nullptr,
0,
Math::Vector2Int(64, 160),
0,
0,
800,
256
};
}
}
+10 -10
View File
@@ -26,16 +26,16 @@ namespace LightGame
static const ObjectSpawn spawns[] = {
{ GameObjectType::Player, 2, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 4, 4, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 6, 3, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 8, 2, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 10, 3, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 12, 4, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 14, 3, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 16, 2, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 18, 3, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 20, 4, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 22, 5, 1500, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 4, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 6, 3, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 8, 2, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 10, 3, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 12, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 14, 3, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 16, 2, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 18, 3, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 20, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 22, 5, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 6, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 2, 0, 0, TriggerAction::None, 0 },
+10 -10
View File
@@ -27,18 +27,18 @@ namespace LightGame
static const ObjectSpawn spawns[] = {
{ GameObjectType::Player, 2, 5, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 4, 4, 2000, 0, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 6, 3, 0, 1000, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 8, 4, 2000, 0, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 10, 3, 0, 1000, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 12, 4, 2000, 0, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 4, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 6, 3, 0, 1023, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 8, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 10, 3, 0, 1023, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 12, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::Door, 15, 4, 1500, 2500, TriggerAction::None, 0 },
{ GameObjectType::Door, 15, 4, 1023, 3192, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 17, 4, 2000, 0, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 19, 3, 0, 1000, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 21, 4, 2000, 0, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 22, 5, 0, 1000, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 17, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 19, 3, 0, 1023, TriggerAction::None, 0 },
{ GameObjectType::LightPlatform, 21, 4, 3192, 4095, TriggerAction::None, 0 },
{ GameObjectType::ShadowPlatform, 22, 5, 0, 1023, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 6, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 2, 0, 0, TriggerAction::None, 0 },
+84 -2
View File
@@ -8,15 +8,23 @@
#include "DefaultHardware.h"
#include "Display.h"
#include "DrawContext.h"
#include "FrameBuffer.h"
#include "TimeSource.h"
#include "Timer.h"
#include "LightGameApp.h"
#include "LevelEditor.h"
#include "font_atlas.h"
#include "Level1Data.h"
#include "Level2Data.h"
#include "Level3Data.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#include "imgui.h"
#include "backends/imgui_impl_sdl2.h"
#include "backends/imgui_impl_sdlrenderer2.h"
#endif
namespace
@@ -91,6 +99,17 @@ int main(int argc, char* argv[])
&pointerInput,
&photoSensor);
LightGame::LevelEditor editor(ScreenWidth, ScreenHeight);
#ifndef USE_FRAMEBUFFER
Platform::SDLDisplay* sdl_display = static_cast<Platform::SDLDisplay*>(display);
IMGUI_CHECKVERSION();
ImGui::CreateContext();
ImGui::StyleColorsDark();
ImGui_ImplSDL2_InitForSDLRenderer(sdl_display->get_window(), sdl_display->get_renderer());
ImGui_ImplSDLRenderer2_Init(sdl_display->get_renderer());
#endif
bool debug_mode = false;
for (int i = 1; i < argc; ++i)
{
@@ -109,7 +128,28 @@ int main(int argc, char* argv[])
timer.begin_frame(time_source.get_time_ms());
bool should_quit = false;
#ifdef USE_FRAMEBUFFER
display->poll_events(should_quit);
#else
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL2_ProcessEvent(&event);
if (event.type == SDL_QUIT)
{
should_quit = true;
}
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_ESCAPE)
{
should_quit = true;
}
if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_F1)
{
editor.set_active(!editor.is_active());
}
}
#endif
if (should_quit)
{
is_running = false;
@@ -118,15 +158,57 @@ int main(int argc, char* argv[])
buttonInput.update();
pointerInput.update();
app.update(timer.fixed_delta_ms());
app.draw(ctx, font);
if (editor.is_active())
{
if (editor.get_pending_load() >= 0)
{
int32_t idx = editor.get_pending_load();
if (idx == 0) editor.load_from_data(LightGame::Level1::data);
else if (idx == 1) editor.load_from_data(LightGame::Level2::data);
else if (idx == 2) editor.load_from_data(LightGame::Level3::data);
editor.clear_pending_load();
}
editor.update(timer.fixed_delta_ms(), &keyboardState, &pointerInput);
editor.draw(ctx, font);
}
else
{
app.update(timer.fixed_delta_ms());
app.draw(ctx, font);
}
#ifdef USE_FRAMEBUFFER
ctx.present(display);
#else
const Core::FrameBuffer* fb = ctx.get_frame_buffer();
SDL_UpdateTexture(sdl_display->get_texture(), nullptr, fb->get_buffer(), ScreenWidth * 2);
SDL_RenderClear(sdl_display->get_renderer());
SDL_RenderCopy(sdl_display->get_renderer(), sdl_display->get_texture(), nullptr, nullptr);
ImGui_ImplSDLRenderer2_NewFrame();
ImGui_ImplSDL2_NewFrame();
ImGui::NewFrame();
if (editor.is_active())
{
editor.draw_ui();
}
ImGui::Render();
ImGui_ImplSDLRenderer2_RenderDrawData(ImGui::GetDrawData(), sdl_display->get_renderer());
SDL_RenderPresent(sdl_display->get_renderer());
#endif
SleepRemainingFrameTime(timer, time_source);
}
photoSensor.shutdown();
pointerInput.shutdown();
buttonInput.shutdown();
#ifndef USE_FRAMEBUFFER
ImGui_ImplSDLRenderer2_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
#endif
display->shutdown();
delete display;
return 0;
+2
View File
@@ -82,5 +82,7 @@ namespace Core
const RenderData::Color& color, const RenderData::Color& bg_color, const char* text);
void present(Platform::IDisplay* display);
const Core::FrameBuffer* get_frame_buffer() const { return frameBuffer; }
};
}
+1 -1
View File
@@ -57,7 +57,7 @@ namespace Platform
return false;
}
SDL_StopTextInput();
SDL_StartTextInput();
return true;
}
+4
View File
@@ -18,5 +18,9 @@ namespace Platform
void present(const Core::FrameBuffer* framebuffer) override;
void poll_events(bool& should_quit) override;
void shutdown() override;
SDL_Window* get_window() const { return window; }
SDL_Renderer* get_renderer() const { return renderer; }
SDL_Texture* get_texture() const { return texture; }
};
}