为 LightGame 提供素材转换工具和转换后的头文件

This commit is contained in:
SepComet 2026-06-11 09:28:20 +08:00
parent e00fc1799d
commit fda8b120d3
13 changed files with 3589 additions and 159 deletions

View File

@ -12,5 +12,5 @@ set(LIGHTGAME_SOURCES
)
add_executable(IMX6U-LightGame ${LIGHTGAME_SOURCES})
target_include_directories(IMX6U-LightGame PRIVATE src/engine src/systems src/levels)
target_include_directories(IMX6U-LightGame PRIVATE src/engine src/systems src/levels generated)
imx6u_configure_app_target(IMX6U-LightGame)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,6 +5,7 @@
#include "GameObject.h"
#include "Tilemap.h"
#include "Image.h"
#include "Sprite.h"
namespace LightGame
{

View File

@ -1,7 +1,19 @@
#include "LevelLoader.h"
#include "Sprite.h"
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);
}
}
void LevelLoader::load(Level& level, const LevelData& data)
{
RenderData::Tilemap tilemap;
@ -22,59 +34,65 @@ namespace LightGame
level.set_spawn_point(data.spawn_point);
const RenderData::Image* atlas = data.atlas;
const int32_t ts = data.tile_size;
const int32_t ac = data.atlas_columns;
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);
spawn.tile_x * ts,
spawn.tile_y * ts);
switch (spawn.type)
{
case GameObjectType::StaticPlatform:
obj.collider = RenderData::BoundingBox2D(
Math::Vector2Int(0, 0),
Math::Vector2Int(data.tile_size, data.tile_size));
Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = true;
break;
case GameObjectType::LightPlatform:
obj.collider = RenderData::BoundingBox2D(
Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = false;
obj.light_threshold = LightThreshold(spawn.light_min, spawn.light_max);
break;
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);
Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = true;
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));
Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts * 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));
Math::Vector2Int(0, ts / 2),
Math::Vector2Int(ts, ts));
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));
Math::Vector2Int(ts / 4, ts / 4),
Math::Vector2Int(ts * 3 / 4, ts * 3 / 4));
obj.solid = false;
break;
case GameObjectType::Trigger:
obj.collider = RenderData::BoundingBox2D(
Math::Vector2Int(0, 0),
Math::Vector2Int(data.tile_size, data.tile_size));
Math::Vector2Int(0, 0), Math::Vector2Int(ts, ts));
obj.solid = false;
obj.trigger_action = spawn.trigger_action;
obj.trigger_target_id = spawn.trigger_target;
@ -82,8 +100,7 @@ namespace LightGame
case GameObjectType::Player:
obj.collider = RenderData::BoundingBox2D(
Math::Vector2Int(4, 0),
Math::Vector2Int(12, 16));
Math::Vector2Int(8, 0), Math::Vector2Int(24, 32));
obj.solid = false;
break;

View File

@ -20,7 +20,13 @@ namespace LightGame
for (size_t i = 0; i < objects.size(); ++i)
{
const GameObject& obj = objects[i];
if (!obj.active || obj.sprite == nullptr)
if (!obj.active)
{
continue;
}
const RenderData::Sprite* sprite = obj.sprite;
if (!sprite)
{
continue;
}
@ -28,8 +34,8 @@ namespace LightGame
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())
if (screen_x + sprite->width < 0 || screen_x > camera.get_screen_width() ||
screen_y + sprite->height < 0 || screen_y > camera.get_screen_height())
{
continue;
}
@ -43,7 +49,7 @@ namespace LightGame
continue;
}
ctx.draw_sprite_ex(screen_x, screen_y, *obj.sprite, 1, obj.flip_h, false);
ctx.draw_sprite_ex(screen_x, screen_y, *sprite, 1, obj.flip_h, false);
}
}

View File

@ -32,6 +32,82 @@ namespace LightGame
}
}
bool Physics2D::is_tile_solid(const RenderData::Tilemap& tilemap, int32_t tile_x, int32_t tile_y) const
{
if (tile_x < 0 || tile_x >= tilemap.width || tile_y < 0 || tile_y >= tilemap.height)
{
return false;
}
const uint16_t tile_id = tilemap.get_tile(tile_x, tile_y);
return tile_id != 0 && tile_id != RenderData::Tilemap::EmptyTile;
}
void Physics2D::collide_tilemap(GameObject& obj, const RenderData::Tilemap& tilemap,
const RenderData::BoundingBox2D& world_collider)
{
if (tilemap.tile_w <= 0 || tilemap.tile_h <= 0)
{
return;
}
const int32_t ts = tilemap.tile_w;
const int32_t tile_left = world_collider.min.x / ts;
const int32_t tile_right = (world_collider.max.x - 1) / ts;
const int32_t tile_top = world_collider.min.y / ts;
const int32_t tile_bottom = (world_collider.max.y - 1) / ts;
for (int32_t ty = tile_top; ty <= tile_bottom; ++ty)
{
for (int32_t tx = tile_left; tx <= tile_right; ++tx)
{
if (!is_tile_solid(tilemap, tx, ty))
{
continue;
}
const RenderData::BoundingBox2D tile_box(
Math::Vector2Int(tx * ts, ty * ts),
Math::Vector2Int((tx + 1) * ts, (ty + 1) * ts));
const int32_t overlap_left = world_collider.max.x - tile_box.min.x;
const int32_t overlap_right = tile_box.max.x - world_collider.min.x;
const int32_t overlap_top = world_collider.max.y - tile_box.min.y;
const int32_t overlap_bottom = tile_box.max.y - world_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;
}
}
}
}
}
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);
@ -48,9 +124,12 @@ namespace LightGame
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();
RenderData::BoundingBox2D world_collider = obj.get_world_collider();
const RenderData::Tilemap& tilemap = level.get_tilemap();
collide_tilemap(obj, tilemap, 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];
@ -68,6 +147,7 @@ namespace LightGame
other.type == GameObjectType::ShadowPlatform);
const RenderData::BoundingBox2D other_world = other.get_world_collider();
world_collider = obj.get_world_collider();
if (!aabb_overlap(world_collider, other_world))
{
@ -90,11 +170,10 @@ namespace LightGame
}
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 overlap_left = world_collider.max.x - other_world.min.x;
const int32_t overlap_right = other_world.max.x - world_collider.min.x;
const int32_t overlap_top = world_collider.max.y - other_world.min.y;
const int32_t overlap_bottom = other_world.max.y - world_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);
@ -140,6 +219,23 @@ namespace LightGame
feet.min.y = feet.max.y;
feet.max.y = feet.max.y + 2;
const RenderData::Tilemap& tilemap = level.get_tilemap();
if (tilemap.tile_w > 0)
{
const int32_t ts = tilemap.tile_w;
const int32_t tile_left = feet.min.x / ts;
const int32_t tile_right = (feet.max.x - 1) / ts;
const int32_t tile_y = feet.min.y / ts;
for (int32_t tx = tile_left; tx <= tile_right; ++tx)
{
if (is_tile_solid(tilemap, tx, tile_y))
{
return true;
}
}
}
const auto& all_objects = level.get_all_objects();
for (size_t i = 0; i < all_objects.size(); ++i)
{
@ -158,27 +254,4 @@ namespace LightGame
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

@ -51,11 +51,7 @@ namespace LightGame
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);
bool is_tile_solid(const RenderData::Tilemap& tilemap, int32_t tile_x, int32_t tile_y) const;
void collide_tilemap(GameObject& obj, const RenderData::Tilemap& tilemap, const RenderData::BoundingBox2D& world_collider);
};
}

View File

@ -1,54 +1,51 @@
#pragma once
#include "LevelData.h"
#include "tile_atlas.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 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,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,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, 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 },
{ 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,
nullptr,
0,
&kAtlas,
tile_atlas_columns,
spawns,
8,
Math::Vector2Int(2 * kTileSize, 11 * kTileSize),
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
0,
0,
kMapWidth * kTileSize,

View File

@ -1,64 +1,61 @@
#pragma once
#include "LevelData.h"
#include "tile_atlas.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 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 5=light_platform_off 6=light_platform_on
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,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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, 11, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Player, 2, 5, 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::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::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::Collectible, 6, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 16, 1, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Trigger, 48, 12, 0, 0, TriggerAction::LevelComplete, 0 },
{ GameObjectType::Trigger, 24, 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,
nullptr,
0,
&kAtlas,
tile_atlas_columns,
spawns,
15,
Math::Vector2Int(2 * kTileSize, 11 * kTileSize),
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
0,
0,
kMapWidth * kTileSize,

View File

@ -1,66 +1,64 @@
#pragma once
#include "LevelData.h"
#include "tile_atlas.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 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
// Objects: LightPlatform, ShadowPlatform, Door via spawns
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,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
0,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,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,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, 11, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Player, 2, 5, 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::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::Door, 30, 10, 1500, 2500, TriggerAction::None, 0 },
{ GameObjectType::Door, 15, 4, 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::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::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::Collectible, 6, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 10, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Collectible, 19, 2, 0, 0, TriggerAction::None, 0 },
{ GameObjectType::Trigger, 48, 12, 0, 0, TriggerAction::LevelComplete, 0 },
{ GameObjectType::Trigger, 24, 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,
nullptr,
0,
&kAtlas,
tile_atlas_columns,
spawns,
15,
Math::Vector2Int(2 * kTileSize, 11 * kTileSize),
Math::Vector2Int(2 * kTileSize, 5 * kTileSize),
0,
0,
kMapWidth * kTileSize,

123
tools/pack_sprite_atlas.py Normal file
View File

@ -0,0 +1,123 @@
"""
将多个独立 sprite PNG JSON 配置打包为 sprite atlas生成 C++ header
用法:
python tools/pack_sprite_atlas.py config.json output.h
JSON 格式:
{
"atlas_width": 512,
"atlas_height": 512,
"sprites": [
{ "name": "player_idle_0", "file": "player_idle_0.png" },
{ "name": "player_idle_1", "file": "player_idle_1.png" },
...
]
}
- atlas_width / atlas_height: 输出 atlas 尺寸
- sprites: 按顺序排列的 sprite 列表
- 每个 sprite 保持原始尺寸从左到右从上到下排列
- 生成的 header 包含每个 sprite 的子矩形信息
"""
import json
import os
import sys
from PIL import Image
def pack(config_path: str, output_path: str):
config_dir = os.path.dirname(os.path.abspath(config_path))
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
atlas_w = cfg["atlas_width"]
atlas_h = cfg["atlas_height"]
sprites = cfg["sprites"]
atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
regions = []
cursor_x = 0
cursor_y = 0
row_height = 0
for s in sprites:
png_path = os.path.join(config_dir, s["file"])
img = Image.open(png_path).convert("RGBA")
w, h = img.size
if cursor_x + w > atlas_w:
cursor_x = 0
cursor_y += row_height
row_height = 0
if cursor_y + h > atlas_h:
print(f"错误: atlas 空间不足sprite '{s['name']}' 放不下")
sys.exit(1)
atlas.paste(img, (cursor_x, cursor_y))
regions.append({
"name": s["name"],
"x": cursor_x,
"y": cursor_y,
"w": w,
"h": h,
})
cursor_x += w
row_height = max(row_height, h)
var_name = os.path.splitext(os.path.basename(output_path))[0]
pixels = list(atlas.getdata())
packed = []
for r, g, b, a in pixels:
packed.append(
((r >> 3) << 11)
| ((g >> 3) << 6)
| ((b >> 3) << 1)
| (1 if a else 0)
)
with open(output_path, "w", encoding="utf-8") as f:
f.write("// Auto-generated by tools/pack_sprite_atlas.py\n")
f.write(f"#pragma once\n#include <cstdint>\n#include \"Image.h\"\n#include \"Sprite.h\"\n\n")
f.write(f"namespace {var_name} {{\n\n")
f.write(f"static const int32_t atlas_width = {atlas_w};\n")
f.write(f"static const int32_t atlas_height = {atlas_h};\n\n")
f.write(f"static const uint16_t atlas_pixels[] = {{\n")
for i in range(0, len(packed), 16):
chunk = packed[i : i + 16]
line = ", ".join(f"0x{p:04X}" for p in chunk)
f.write(f" {line},\n")
f.write("};\n\n")
f.write(f"static const RenderData::Image atlas_image(atlas_pixels, atlas_width, atlas_height);\n\n")
for r in regions:
f.write(f"static const RenderData::Sprite spr_{r['name']}("
f"&atlas_image, {r['x']}, {r['y']}, {r['w']}, {r['h']});\n")
f.write(f"\n}} // namespace {var_name}\n")
print(f"已生成: {output_path}")
print(f" atlas: {atlas_w}x{atlas_h}, {len(regions)} sprites")
for r in regions:
print(f" {r['name']}: ({r['x']},{r['y']}) {r['w']}x{r['h']}")
def main():
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} config.json output.h")
sys.exit(1)
pack(sys.argv[1], sys.argv[2])
if __name__ == "__main__":
main()

110
tools/pack_tile_atlas.py Normal file
View File

@ -0,0 +1,110 @@
"""
将多个独立 tile PNG JSON 配置打包为 tile atlas生成 C++ header
用法:
python tools/pack_tile_atlas.py config.json output.h
JSON 格式:
{
"tile_size": 32,
"columns": 4,
"tiles": [
{ "id": 0, "file": "empty.png" },
{ "id": 1, "file": "ground_top.png" },
...
]
}
- tile_size: 每个 tile 的像素尺寸正方形
- columns: atlas 每行放几个 tile
- tiles: tile_id 排序的列表id 0 开始连续
- file: 相对于 config.json 所在目录的 PNG 路径
- 未指定的 tile_id 自动填充透明
"""
import json
import os
import sys
import math
from PIL import Image
def pack(config_path: str, output_path: str):
config_dir = os.path.dirname(os.path.abspath(config_path))
with open(config_path, "r", encoding="utf-8") as f:
cfg = json.load(f)
tile_size = cfg["tile_size"]
columns = cfg["columns"]
tiles = cfg["tiles"]
max_id = max(t["id"] for t in tiles)
tile_count = max_id + 1
rows = (tile_count + columns - 1) // columns
atlas_w = columns * tile_size
atlas_h = rows * tile_size
atlas = Image.new("RGBA", (atlas_w, atlas_h), (0, 0, 0, 0))
tile_map = {t["id"]: t for t in tiles}
for tid in range(tile_count):
col = tid % columns
row = tid // columns
x = col * tile_size
y = row * tile_size
if tid in tile_map:
png_path = os.path.join(config_dir, tile_map[tid]["file"])
img = Image.open(png_path).convert("RGBA")
if img.size != (tile_size, tile_size):
img = img.resize((tile_size, tile_size), Image.NEAREST)
atlas.paste(img, (x, y))
var_name = os.path.splitext(os.path.basename(output_path))[0]
pixels = list(atlas.getdata())
packed = []
for r, g, b, a in pixels:
packed.append(
((r >> 3) << 11)
| ((g >> 3) << 6)
| ((b >> 3) << 1)
| (1 if a else 0)
)
with open(output_path, "w", encoding="utf-8") as f:
f.write("// Auto-generated by tools/pack_tile_atlas.py\n")
f.write(f"#pragma once\n#include <cstdint>\n#include \"Image.h\"\n\n")
f.write(f"static const int32_t {var_name}_width = {atlas_w};\n")
f.write(f"static const int32_t {var_name}_height = {atlas_h};\n")
f.write(f"static const int32_t {var_name}_tile_size = {tile_size};\n")
f.write(f"static const int32_t {var_name}_columns = {columns};\n\n")
f.write(f"static const uint16_t {var_name}_pixels[] = {{\n")
for i in range(0, len(packed), 16):
chunk = packed[i : i + 16]
line = ", ".join(f"0x{p:04X}" for p in chunk)
f.write(f" {line},\n")
f.write("};\n\n")
f.write(f"static const RenderData::Image {var_name}_image({var_name}_pixels, {var_name}_width, {var_name}_height);\n")
print(f"已生成: {output_path}")
print(f" atlas: {atlas_w}x{atlas_h}, {tile_count} tiles ({columns}x{rows})")
print(f" var: {var_name}")
def main():
if len(sys.argv) < 3:
print(f"用法: {sys.argv[0]} config.json output.h")
sys.exit(1)
pack(sys.argv[1], sys.argv[2])
if __name__ == "__main__":
main()