This commit is contained in:
HP 2026-06-07 12:34:07 +08:00
parent b093796e61
commit c177114bf3
123 changed files with 932 additions and 1008 deletions

View File

@ -6,96 +6,50 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
<<<<<<< HEAD
set(ENGINE_SOURCES
src/Asset/ObjLoader.cpp
src/Asset/SpriteAssetLoader.cpp
src/Core/DepthBuffer.cpp
src/Core/FrameBuffer.cpp
src/Core/Renderer.cpp
game/src/app/TomGameApp.cpp
game/src/components/SpriteAnimator.cpp
game/src/audio/VoiceEffect.cpp
game/src/audio/VoicePlayer.cpp
game/src/audio/VoiceRecorder.cpp
game/src/hardware/AudioInput.cpp
game/src/hardware/AudioOutput.cpp
game/src/hardware/ButtonInput.cpp
game/src/systems/AnimationSystem.cpp
src/Rasterizer/Rasterizer.cpp
src/Rasterizer/SpriteRasterizer.cpp
src/Rasterizer/TriangleRasterizer.cpp
src/Scene/Camera.cpp
src/Shading/BlinnPhongShader.cpp
=======
set(SOURCES
src/Apps/Demo/main.cpp
src/Gfx/Asset/ObjLoader.cpp
src/Gfx/Core/DepthBuffer.cpp
src/Gfx/Core/FrameBuffer.cpp
src/Gfx/Core/Renderer.cpp
src/Gfx/Draw2D/DrawContext.cpp
src/Gfx/Rasterizer/Rasterizer.cpp
src/Gfx/Rasterizer/TriangleRasterizer.cpp
src/Gfx/Scene/Camera.cpp
src/Gfx/Shading/BlinnPhongShader.cpp
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5
set(CORE_SOURCES
src/Core/Asset/ObjLoader.cpp
src/Core/Asset/SpriteAssetLoader.cpp
src/Core/Core/DepthBuffer.cpp
src/Core/Core/FrameBuffer.cpp
src/Core/Core/Renderer.cpp
src/Core/Draw2D/DrawContext.cpp
src/Core/Rasterizer/Rasterizer.cpp
src/Core/Rasterizer/TriangleRasterizer.cpp
src/Core/Scene/Camera.cpp
src/Core/Shading/BlinnPhongShader.cpp
)
set(ENGINE_INCLUDE_DIRS
src/Platform
src/Asset
src/Core
src/Math
src/Rasterizer
src/RenderData
src/Scene
src/Shading
game/src/app
game/src/audio
game/src/components
game/src/hardware
game/src/systems
set(CORE_INCLUDE_DIRS
src/Core/Platform
src/Core/Asset
src/Core/Core
src/Core/Draw2D
src/Core/Math
src/Core/Rasterizer
src/Core/RenderData
src/Core/Scene
src/Core/Shading
assets/font
assets/sprite
)
set(SOURCES
src/main.cpp
${ENGINE_SOURCES}
src/Apps/Game/Main.cpp
${CORE_SOURCES}
)
if(USE_FRAMEBUFFER)
list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
list(APPEND SOURCES src/Core/Platform/FBDisplay.cpp)
else()
list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp)
list(APPEND SOURCES src/Core/Platform/SDLDisplay.cpp)
endif()
add_executable(IMX6U-Game ${SOURCES})
<<<<<<< HEAD
target_include_directories(IMX6U-Game PRIVATE ${ENGINE_INCLUDE_DIRS})
=======
target_include_directories(IMX6U-Game PRIVATE
src/Gfx/Platform
src/Gfx/Asset
src/Gfx/Core
src/Gfx/Draw2D
src/Gfx/Math
src/Gfx/Rasterizer
src/Gfx/RenderData
src/Gfx/Scene
src/Gfx/Shading
assets/font
assets/sprite
)
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5
target_include_directories(IMX6U-Game PRIVATE ${CORE_INCLUDE_DIRS})
if(USE_FRAMEBUFFER)
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)
else()
target_include_directories(IMX6U-Game PRIVATE
libs/Win/SDL2/include
)
if(WIN32)
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64")
@ -109,6 +63,7 @@ else()
set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll")
endif()
target_include_directories(IMX6U-Game PRIVATE libs/Win/SDL2/include)
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR})
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2)
@ -119,14 +74,9 @@ else()
)
else()
find_package(SDL2 REQUIRED)
find_package(SDL2_image QUIET)
target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2)
endif()
find_package(SDL2_image QUIET)
endif()
if(UNIX)
target_link_libraries(IMX6U-Game PRIVATE asound)
endif()
if(MSVC)
@ -135,46 +85,16 @@ if(MSVC)
endif()
if(NOT USE_FRAMEBUFFER)
set(SPRITE_ANIMATION_TEST_SOURCES
game/tests/manual/SpriteAnimationTest.cpp
${ENGINE_SOURCES}
src/Platform/SDLDisplay.cpp
)
if(WIN32)
add_executable(SpriteAnimationTest ${SPRITE_ANIMATION_TEST_SOURCES})
target_include_directories(SpriteAnimationTest PRIVATE
${ENGINE_INCLUDE_DIRS}
libs/Win/SDL2/include
)
target_link_directories(SpriteAnimationTest PRIVATE ${SDL2_LIB_DIR})
target_link_libraries(SpriteAnimationTest PRIVATE SDL2main SDL2)
add_custom_command(TARGET SpriteAnimationTest POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${SDL2_DLL}"
"$<TARGET_FILE_DIR:SpriteAnimationTest>"
)
else()
add_executable(SpriteAnimationTest ${SPRITE_ANIMATION_TEST_SOURCES})
target_include_directories(SpriteAnimationTest PRIVATE ${ENGINE_INCLUDE_DIRS})
target_link_libraries(SpriteAnimationTest PRIVATE SDL2::SDL2 asound)
endif()
if(MSVC AND TARGET SpriteAnimationTest)
target_compile_options(SpriteAnimationTest PRIVATE /utf-8 /W3)
endif()
set(SPRITE_ASSET_TOOL_SOURCES
game/tools/asset_pipeline/SpriteAssetTool.cpp
src/Asset/SpriteAssetLoader.cpp
src/Apps/Game/tools/asset_pipeline/SpriteAssetTool.cpp
src/Core/Asset/SpriteAssetLoader.cpp
)
if(WIN32)
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
target_include_directories(SpriteAssetTool PRIVATE
src/Asset
src/RenderData
src/Core/Asset
src/Core/RenderData
libs/Win/SDL2/include
libs/Win/SDL_image/include
)
@ -194,8 +114,8 @@ if(NOT USE_FRAMEBUFFER)
elseif(SDL2_image_FOUND)
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
target_include_directories(SpriteAssetTool PRIVATE
src/Asset
src/RenderData
src/Core/Asset
src/Core/RenderData
)
target_link_libraries(SpriteAssetTool PRIVATE SDL2::SDL2 SDL2_image::SDL2_image)
else()
@ -206,9 +126,10 @@ if(NOT USE_FRAMEBUFFER)
add_custom_target(ConvertTomSprites
COMMAND $<TARGET_FILE:SpriteAssetTool>
--batch
"${CMAKE_CURRENT_SOURCE_DIR}/game/assets/raw"
"${CMAKE_CURRENT_SOURCE_DIR}/game/assets/sprites"
"src/Apps/Game/assets/raw"
"src/Apps/Game/assets/sprites"
--preset tom-800x480
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
DEPENDS SpriteAssetTool
COMMENT "Converting PNG assets to board-ready .sprite files"
VERBATIM

View File

@ -33,13 +33,13 @@ IMX6U 运行时性能预算较紧,后续开发必须遵守 `docs/DEVELOPMENT_G
## 应用层与图形库拆分
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_GFX_ARCHITECTURE.md`
项目后续按“两个游戏 + 一个启动器 + 一套底层图形库”组织,详细边界见 `docs/APP_AND_CORE_ARCHITECTURE.md`
- `Gfx` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
- `Core` / 底层图形库:只提供 framebuffer、SDL2/fb0 平台适配、独立时间源、基础绘制能力例如点、线、矩形、四边形、sprite、bitmap font、tilemap。
- `Apps/Launcher`:负责游戏选择、全局设置和启动流程。
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。
- 依赖方向必须保持 `Apps -> Shared -> Core -> Platform`,底层图形库不能反向依赖具体游戏。
初期推荐单进程多应用模式Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
@ -194,7 +194,7 @@ assets/font/font_atlas.h
## 显示后端架构
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层:
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Core` 的平台适配层:
```
┌──────────────────────────────────────────────┐
@ -202,7 +202,7 @@ assets/font/font_atlas.h
├──────────────────────────────────────────────┤
│ SharedUI、存档、配置、资源索引 │
├──────────────────────────────────────────────┤
GfxDrawContext、FrameBuffer、Draw2D、Math │
CoreDrawContext、FrameBuffer、Draw2D、Math │
├──────────────────────────────────────────────┤
│ Platform::IDisplay │
│ - SDLDisplay : SDL2 显示/输入适配 │
@ -230,7 +230,7 @@ IMX6U-Game/
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
├─ src/
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ ├─ Core/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
│ │ ├─ Core/ # FrameBuffer、DepthBuffer
│ │ ├─ Math/ # 向量、矩阵、数学工具
@ -245,7 +245,7 @@ IMX6U-Game/
│ └─ test_fb.cpp # 独立 fb 测试(最小示例)
├─ docs/
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
│ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层
│ ├─ APP_AND_CORE_ARCHITECTURE.md # 应用层与图形库分层
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
├─ CMakeLists.txt
└─ README.md
@ -284,8 +284,8 @@ IMX6U-Game/
- 双平台显示后端SDL2 / Framebuffer
- 离线资源转换工具PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
- Core 目录规范化,代码收敛到 `src/Core/`
- `Core::DrawContext` 统一绘制入口,封装现有绘制能力
- C++11 兼容代码
- CMake 跨平台构建
@ -293,7 +293,7 @@ IMX6U-Game/
1. FrameBuffer 性能优化(`memset` 清屏、去掉 `at()`、定点数/NEON
2. 应用层拆分Launcher / GameA / GameB / Shared和统一 `IApp` 主循环
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
5. 纹理贴图、OBJ 模型加载与完整光照
## 说明

View File

@ -0,0 +1,135 @@
# 应用层与 Core 分层设计
本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。
## 目录边界
```text
src/
Core/ # 底层库:渲染、数学、资源、平台适配
Asset/ # 离线资源格式加载
Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
Draw2D/ # Core::DrawContext
Math/ # Vector、Matrix、MathUtil
Platform/ # IDisplay、SDLDisplay、FBDisplay、ITimeSource
Rasterizer/ # 线段和三角形光栅化
RenderData/ # Color、Image、Triangle、Tilemap 等数据结构
Scene/ # Camera、Transform、Mesh、Model
Shading/ # Shader 相关代码
Apps/
Demo/ # Core 能力演示
Game/ # Tom 游戏
```
`src/Core/Core` 这个二级目录保留的是原底层库里的核心运行时对象。它和顶层 `src/Core` 名称重复,但含义不同:
- `src/Core`:整个底层库。
- `src/Core/Core`:底层库内部的 framebuffer、depthbuffer、timer 等核心对象。
后续如果觉得重复命名影响阅读,可以再把 `src/Core/Core` 单独改成 `Runtime/``Buffer/`,但这次先只做旧底层库名称到 `Core` 的一致性修复。
## 依赖方向
允许:
```text
Apps -> Shared -> Core -> Platform
Apps -> Core
```
禁止:
```text
Core -> Apps
Core -> Shared
GameA -> GameB
GameB -> GameA
Platform -> Game
```
Core 层不应该知道具体游戏规则、场景流程、角色状态机、关卡数据或游戏专属资源路径。
## Core 职责
Core 只提供底层能力:
- 管理 `FrameBuffer`、`DepthBuffer` 和绘制上下文。
- 提供基础绘制接口line、triangle、sprite、sprite region、tilemap、bitmap font。
- 提供基础数学、颜色、图片、三角形、tilemap 等数据结构。
- 封装 SDL2 / framebuffer 显示提交。
- 提供独立时间源 `Platform::ITimeSource`
- 使用离线转换后的运行时资源,不在热路径解码 PNG/TTF。
Core 不做:
- 不实现具体游戏规则。
- 不直接读取某个游戏专属资源目录。
- 不在核心绘制接口暴露 SDL2 类型。
- 不在每帧热路径中执行图片、字体解码或文件 IO。
## 应用职责
`src/Apps/*` 负责具体应用流程:
- 创建具体游戏或 Demo 的主循环。
- 加载应用自己的资源。
- 调用 `Core::DrawContext` 绘制画面。
- 根据平台输入更新游戏状态。
当前主程序位于:
```text
src/Apps/Game/Main.cpp
```
它默认启动 Tom 游戏视觉入口。
## DrawContext
`Core::DrawContext` 是当前统一绘制入口,位于:
```text
src/Core/Draw2D/DrawContext.h
src/Core/Draw2D/DrawContext.cpp
```
它封装:
- `Core::FrameBuffer`
- `Core::DepthBuffer`
- `Rasterizer::Rasterizer`
- `Rasterizer::TriangleRasterizer`
对外提供:
```cpp
Core::DrawContext ctx(width, height);
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(x, y, image);
ctx.draw_text(font, x, y, color, "text");
ctx.present(display);
```
## 显示后端
显示层通过 `Platform::IDisplay` 抽象:
```text
Platform::IDisplay
SDLDisplay # PC / SDL2 调试后端
FBDisplay # Linux /dev/fb0 后端
```
CMake 通过 `USE_FRAMEBUFFER` 选择实现:
```cmake
-DUSE_FRAMEBUFFER=OFF # 默认,使用 SDLDisplay
-DUSE_FRAMEBUFFER=ON # 使用 FBDisplay
```
## 后续建议
1. 保持 `Core` 不依赖 `Apps`
2. 新增游戏逻辑放在 `src/Apps/Game` 或新的 `src/Apps/*`
3. 新增底层绘制、数学、资源格式、平台显示能力放在 `src/Core`
4. 如果继续清理命名,优先处理 `src/Core/Core` 这个重复目录名。

View File

@ -1,259 +0,0 @@
# 应用层与图形库分层设计
本项目后续包含三个应用层目标:两个游戏和一个启动器;同时还需要沉淀一套可复用的 IMX6U 轻量图形库。为了避免后期耦合和性能返工,必须明确区分“应用层”和“底层库”。
## 1. 推荐总体结构
推荐拆成四个逻辑层:
```text
IMX6U-Game/
├─ src/
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口(✅ 已实现)
│ │ ├─ Core/ # FrameBuffer、DepthBuffer✅ 已实现)
│ │ ├─ Math/ # 向量、矩阵、数学工具(✅ 已实现)
│ │ ├─ Rasterizer/ # 线段、三角形光栅化(✅ 已实现)
│ │ ├─ RenderData/ # Color、Triangle 等数据结构(✅ 已实现)
│ │ ├─ Scene/ # Camera、Transform✅ 已实现)
│ │ ├─ Shading/ # 着色器(预留)
│ │ ├─ Platform/ # SDL2 / fb0 显示适配与独立时间源(✅ 已实现)
│ │ └─ Asset/ # 资源加载(✅ 已实现)
│ ├─ Apps/
│ │ ├─ Demo/ # 当前 3D 立方体 demo✅ 已实现)
│ │ ├─ Launcher/ # 启动器应用(待实现)
│ │ ├─ GameA/ # 第一个游戏(待实现)
│ │ └─ GameB/ # 第二个游戏(待实现)
│ └─ Shared/ # 可选:应用层共享但不属于 Gfx 的东西
│ ├─ Save/ # 存档格式、配置读写
│ ├─ UI/ # 启动器和游戏共用 UI 组件
│ └─ Assets/ # 应用层资源索引、资源命名约定
├─ assets/
│ ├─ font/ # 共享像素字体源文件与生成的 font_atlas
│ ├─ sprite/ # 当前 demo/test sprite 源文件与生成头文件
│ ├─ launcher/
│ ├─ game_a/
│ ├─ game_b/
│ └─ shared/
├─ tools/
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
└─ docs/
```
~~当前项目已有 `Core/Math/Platform/Rasterizer/RenderData/Scene` 等目录,短期不必一次性搬迁;但新增代码应按上面的边界收敛。等功能稳定后,再把现有底层代码整体移动到 `src/Gfx/`。~~
**已完成**:底层代码已整体迁移到 `src/Gfx/`,包括 Core、Math、Rasterizer、RenderData、Scene、Shading、Platform、Asset 和新增的 Draw2D。Demo 入口位于 `src/Apps/Demo/`
## 2. 四个层级的职责
### 2.1 Gfx底层图形库
职责:
- 管理 framebuffer、depthbuffer、渲染上下文。
- 提供基础绘制接口点、线、矩形、四边形、三角形、sprite、SpriteRegion、tilemap、简单文本等。
- 提供颜色、矩形、定点数、纹理、裁剪区域等基础数据结构。
- 封装 SDL2 / framebuffer 显示提交、输入轮询,并通过独立 `ITimeSource` 提供单调整数毫秒时间。
- 使用离线转换后的紧凑资源数据(例如 PNG 转头文件、bitmap font atlas运行时不直接依赖 PNG/TTF 解码。
- 只关心“怎么画得快、怎么提交到屏幕”,不关心具体游戏规则。
禁止:
- 不依赖 `GameA`、`GameB`、`Launcher`。
- 不包含具体关卡、角色、菜单流程、游戏状态机。
- 不直接读取某个游戏专属资源路径。
- 不在核心绘制接口中暴露 SDL2 类型。
- 不在每帧热路径中执行图片/字体解码;资源转换应在构建前或工具阶段完成。
### 2.2 Apps/GameA 与 Apps/GameB两个游戏
职责:
- 各自维护自己的游戏规则、状态机、关卡、实体、碰撞、计分、胜负逻辑。
- 通过 Gfx 提供的接口绘制画面。
- 通过平台输入快照读取按键/触摸状态,而不是直接调用 SDL。
- 管理自己的资源索引和场景切换。
禁止:
- 游戏之间互相 include 对方代码。
- 游戏直接操作 SDL renderer / texture / window。
- 游戏直接依赖 framebuffer 内存布局,除非是明确标注的性能特例。
### 2.3 Apps/Launcher启动器
职责:
- 展示游戏列表、图标、说明、版本信息。
- 选择并启动 GameA 或 GameB。
- 管理全局设置,例如音量、亮度、输入校准、语言等。
- 可以复用 Shared/UI但不承载具体游戏逻辑。
启动方式有两种可选方案:
1. **单进程多应用模式**Launcher、GameA、GameB 编译成一个可执行文件,内部切换当前 App。
2. **多进程模式**Launcher 是独立程序,选择后启动另一个游戏可执行文件。
对 IMX6U 初期开发,推荐先用 **单进程多应用模式**,原因:
- 构建、部署、调试更简单。
- SDL2 初始化、资源缓存、输入状态可以共用。
- 避免频繁退出/启动进程带来的黑屏、资源重载和状态恢复问题。
等项目成熟后,如果每个游戏体积较大,或者需要独立更新,再考虑多进程拆分。
### 2.4 Shared应用层共享模块
Shared 只放“应用层共享,但不属于底层图形库”的内容,例如:
- UI 控件:按钮、列表、九宫格面板、菜单焦点管理。
- 存档/设置:配置文件、最高分、解锁状态。
- 资源清单:多个应用共用的字体、图标、音效索引。
Shared 可以依赖 GfxGfx 不能依赖 Shared。
## 3. 依赖方向
必须保持单向依赖:
```text
Apps/GameA ─┐
Apps/GameB ─┼─> Shared ─> Gfx ─> Platform(SDL2/fb0/ITimeSource)
Launcher ─┘
Apps/GameA ─┐
Apps/GameB ─┼──────────> Gfx
Launcher ─┘
```
禁止反向依赖:
```text
Gfx -> Apps 禁止
Gfx -> Shared 禁止
GameA -> GameB 禁止
GameB -> GameA 禁止
Platform -> Game 禁止
```
## 4. 应用统一接口
推荐为 Launcher、GameA、GameB 提供统一应用接口,例如:
```cpp
class IApp
{
public:
virtual ~IApp() {}
virtual void on_enter() = 0;
virtual void on_exit() = 0;
virtual void update(uint32_t fixed_delta_ms) = 0;
virtual void render(Gfx::DrawContext& ctx) = 0;
virtual AppId next_app() const = 0;
};
```
主循环只认识 `IApp`
```text
poll input -> update current app -> render current app -> present framebuffer
```
这样三个应用层共用同一个主循环、同一套 SDL2 初始化、framebuffer 提交流程和独立时间源。
注意:接口可以先保留虚函数,因为它只在每帧应用级调用,不在像素/顶点热路径中调用。像 `draw_rect`、`draw_quad`、`set_pixel_fast` 这类热路径函数不要虚化。
## 5. 底层图形库优先提供的 API
Gfx 初期建议先做 2D 基础能力,不要一开始就做复杂引擎:
```cpp
namespace Gfx
{
struct RectI { int32_t x, y, w, h; };
struct PointI { int32_t x, y; };
struct Color32 { uint32_t rgba; };
class DrawContext
{
public:
void clear(Color32 color);
void draw_pixel(int32_t x, int32_t y, Color32 color);
void draw_line(PointI a, PointI b, Color32 color);
void draw_rect(RectI rect, Color32 color);
void fill_rect(RectI rect, Color32 color);
void draw_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
void fill_quad(PointI p0, PointI p1, PointI p2, PointI p3, Color32 color);
void draw_sprite(int32_t x, int32_t y, const RenderData::Image& image);
void draw_sprite_region(int32_t x, int32_t y,
const RenderData::SpriteRegion& region);
void draw_text(const RenderData::BitmapFont& font, int32_t x, int32_t y,
RenderData::Color color, const char* text);
void draw_tilemap(const RenderData::Tilemap& tilemap,
int32_t screen_x, int32_t screen_y,
int32_t viewport_w, int32_t viewport_h,
int32_t camera_x, int32_t camera_y);
};
}
```
后续再扩展:
- `set_clip_rect`
- `set_camera_2d`
- `batch sprite/tile`
## 6. 命名建议
为了避免“项目名、库名、游戏名”混乱,建议:
- 项目仓库仍叫 `IMX6U-Game`
- 底层图形库命名为 `Gfx``MiniGfx`
- 三个应用用明确名字:`Launcher`、`GameA`、`GameB`,后续再替换成真实游戏名。
- CMake target 可以是:
- `imx6u_gfx` 静态库
- `imx6u_launcher`
- `imx6u_game_a`
- `imx6u_game_b`
- 或初期单可执行文件 `imx6u_suite`
## 7. 推荐演进顺序
1. ~~先抽出统一 `IApp``AppManager`,让当前 demo 成为一个 app。~~
2. ~~把 SDL2 初始化、输入、present 固定在平台层,应用层不直接碰 SDL。~~
3. ~~建立 `Gfx::DrawContext`,先封装 clear、pixel、line、rect、quad。~~ **已完成**`Gfx::DrawContext` 封装了 clear、draw_line、draw_triangle、draw_sprite、draw_sprite_region、draw_text、draw_tilemap、present
4. ~~底层代码迁移到 `src/Gfx/`Demo 入口迁移到 `src/Apps/Demo/`。~~ **已完成**
5. 新增 Launcher app只做最小菜单和应用切换。
6. 新增 GameA/GameB 空壳,验证三应用切换。
7. 再逐步把现有 3D demo 或 2D 游戏逻辑迁入对应 Game 目录。
8. 最后重构 CMake`imx6u_gfx` + 应用 target 拆分。
## 8. 性能注意事项
- 应用切换不应重复销毁/创建 SDL window、renderer、texture。
- 三个应用共用 framebuffer、输入状态和 `Platform::ITimeSource` 时间源Display 不承担计时职责。
- 每个应用可以有自己的资源缓存,但必须有上限和释放策略。
- Launcher 不应常驻消耗大量纹理/音频资源;进入游戏后可释放非必要启动器资源。
- Gfx 的绘制函数要保持小而直接,优先内联和连续内存写入。
- UI 控件层可以面向对象;像素/quad/sprite/tile 绘制层不要过度抽象。
## 9. 资源转换约定
- 运行时资源优先使用离线转换后的简单数组,不在 IMX6U 运行时解码 PNG/TTF。
- `tools/png_to_header.py` 将 PNG 转为 `uint32_t` RGBA 数组,适用于 sprite、小图标、测试纹理等。
- `tools/gen_font_atlas.py` 将共享像素字体转为 ASCII bitmap atlas并输出同名 PNG 预览和 C++ 头文件。
- 生成头文件、源 PNG/TTF 和转换脚本应一起纳入仓库,保证资源可追溯、可再生成。
- 生成数据目前面向简单直接的调试/小型游戏资源;后续如果资源体积增长,应再评估 1-bit/8-bit mask、RLE 或自定义资源包格式。
## 10. SpriteRegion 与 Tilemap 约定
- `RenderData::SpriteRegion` 只描述某张 atlas 中的子区域,不拥有像素数据;它通过 `const Image* atlas` 引用源图。
- `DrawContext::draw_sprite_ex` 是底层 sprite 绘制入口负责源区域检查、目标屏幕裁剪、scale 和 flip`draw_sprite_region` 系列只是对 atlas 子区域的语义包装。
- `RenderData::Tilemap` 使用 `uint16_t` tile id 保存地图网格,`Tilemap::EmptyTile` (`0xFFFF`) 表示空 tile。
- `Tilemap` 当前只支持一个 atlas、固定 tile 宽高和固定 `atlas_columns`tile id 通过 `tile_id % atlas_columns` / `tile_id / atlas_columns` 映射到 atlas 中的源区域。
- `DrawContext::draw_tilemap` 的裁剪分两层:
- tilemap 层按 tile 计算需要尝试绘制的可见范围;
- sprite 层按像素裁剪每个 tile支持 camera 像素级滚动时显示半个 tile。
- 带 `viewport_w` / `viewport_h``draw_tilemap` 重载用于子视口绘制,`screen_x` / `screen_y` 是视口左上角;旧重载默认视口延伸到 framebuffer 右下角。
- 当前实现优先保证清晰语义和可验证行为;后续性能优化可增加 tile region 查表、不透明 tile 行拷贝、chunk/dirty rect 或专用 tile 快路径。

View File

@ -2,7 +2,7 @@
本文档用于记录当前项目已经采用的坐标系、矩阵、相机和屏幕空间约定,避免后续开发时在符号、方向和乘法顺序上产生混乱。
> 文档分工坐标、矩阵、深度等数学语义记录在本文档IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_GFX_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
> 文档分工坐标、矩阵、深度等数学语义记录在本文档IMX6U 运行时性能红线记录在 `../docs/DEVELOPMENT_GUIDELINES.md`;两个游戏、启动器和底层图形库的分层边界记录在 `../docs/APP_AND_CORE_ARCHITECTURE.md`。新增或修改热路径、应用层边界、显示后端代码时,应同步参考对应文档。
## 1. 通用约定

View File

@ -9,7 +9,7 @@
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/RenderData` 中是否已有可复用能力。
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Core/Core`、`src/Core/Math`、`src/Core/RenderData` 中是否已有可复用能力。
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
## 2. 数值计算规范
@ -143,7 +143,7 @@
后续如果继续推进性能优化,优先建立这些基础设施:
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`。
1. 统一定点数类型与转换工具,集中放在 `src/Core/Math/`。
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
@ -163,8 +163,8 @@
### 12.1 SDL2 边界
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`
- SDL2 类型和调用只允许出现在 `src/Core/Platform/` 以及明确的平台适配层中。
- `src/Core/Core`、`src/Core/Math`、`src/Core/Rasterizer`、`src/Core/RenderData`、`src/Core/Scene`、`src/Core/Draw2D` 不应直接包含 `SDL.h`
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep不直接依赖 float 秒数。

View File

@ -1,116 +0,0 @@
# 游戏目录结构
这个目录用于编写新的游戏。仓库根目录下的 `src/` 继续作为软渲染器、数学库、光栅化和平台显示层;`game/` 只放游戏自己的代码、资源、脚本和数据。
## 目录结构
```text
game/
README.md
assets/
sprites/ 运行时使用的 2D 精灵图片。
textures/ 3D 材质或其他渲染用途的纹理。
tilesets/ 瓦片图片和瓦片集元数据。
fonts/ 位图字体或字体图集。
audio/
music/ 背景音乐。
sfx/ 短音效。
models/ 运行时使用的 3D 模型文件。
materials/ 模型或精灵使用的材质定义。
ui/ UI 面板、图标、光标、HUD 资源。
raw/ 原始素材文件,不建议运行时直接加载。
scripts/
gameplay/ 预留给运行时玩法脚本。
ui/ 预留给运行时 UI 流程脚本。
tools/ 游戏开发期间使用的小型辅助脚本。
src/
app/ 游戏启动、场景切换、主循环衔接代码。
scenes/ 启动、菜单、游戏、暂停、结算等场景。
systems/ 输入、渲染、碰撞、动画、AI、音频等系统。
entities/ 实体定义和实体工厂。
components/ 系统使用的小型数据组件。
ui/ HUD 和菜单代码。
data/
config/ 分辨率、控制、难度、平台选项等配置。
balance/ 速度、生命、伤害、分数等数值表。
localization/ 多语言文本表。
tools/
asset_pipeline/ 资产转换、压缩、打包相关工具。
level_editor/ 可选的关卡编辑工具。
docs/
design/ 玩法规则、操作方式、关卡设计说明。
technical/ 运行时约束和接入说明。
tests/
unit/ 纯逻辑单元测试。
fixtures/ 测试用的小型数据文件。
```
## 边界约定
- 仓库根目录的 `src/` 是渲染器和引擎基础层。
- `game/src/` 可以依赖根目录 `src/`,但根目录 `src/` 不应该反向依赖 `game/`
- `game/assets/raw/` 只放可编辑源素材,运行时应加载转换后的资源。
- `game/assets/sprites/` 放离线转换后的 `.sprite` 文件PC 测试和 IMX6U 运行时都从这里读取。
- `game/scripts/gameplay/` 预留给运行时脚本;构建、转换、检查类脚本放在 `game/scripts/tools/``game/tools/asset_pipeline/`
- 平台相关逻辑尽量集中在 `game/src/app/``game/data/config/`,不要散落到各个玩法模块里。
## 精灵资源转换
板端运行时不直接解码 PNG。开发时把 PNG 放在 `game/assets/raw/`,再用 PC 端工具转换为 `game/assets/sprites/*.sprite`
`.sprite` 是项目自定义的简单二进制格式:
```text
offset size 内容
0 4 magic: "SPRT"
4 4 version: 1, little-endian uint32
8 4 width, little-endian uint32
12 4 height, little-endian uint32
16 4 format: 1 表示 RGBA8888
20 * width * height 个 RGBA8888 像素,每像素 little-endian uint32
```
像素颜色沿用渲染器约定:`0xRRGGBBAA`,最低 8 位是 alpha。
常用命令:
```bash
# 构建 PC 端工具
cmake -B build-win .
cmake --build build-win --config Release --target SpriteAssetTool
# 批量转换当前汤姆猫素材,输出适配 800x480 的板端资源
cmake --build build-win --config Release --target ConvertTomSprites
# 单张转换,保持原尺寸
./build-win/Release/SpriteAssetTool.exe game/assets/raw/ui-record.png game/assets/sprites/ui-record.sprite
# 单张转换并等比缩放到最大范围
./build-win/Release/SpriteAssetTool.exe game/assets/raw/Tom-stand.png game/assets/sprites/Tom-stand.sprite --fit 560 360
```
Linux PC 构建工具需要 `libsdl2-dev``libsdl2-image-dev`。ARM framebuffer 构建不会编译 `SpriteAssetTool`,只编译 `.sprite` 加载器。
启动汤姆猫资源链路测试:
```bash
./build-win/Release/IMX6U-Game.exe --tom
```
部署到板端时,把可执行文件和转换后的 `game/assets/sprites/` 一起拷贝,保持相对路径即可;也可以放到 `/tmp/game/assets/sprites/``/usr/local/share/imx6u-game/sprites/`
## 建议优先创建的文件
真正开始写游戏逻辑时,建议先从这些文件开始:
```text
game/src/app/GameApp.h
game/src/app/GameApp.cpp
game/src/scenes/BootScene.h
game/src/scenes/BootScene.cpp
game/src/scenes/PlayScene.h
game/src/scenes/PlayScene.cpp
game/data/config/game.json
```
后续可以把现有的 `src/main.cpp` 改成薄启动器:只负责创建显示后端、初始化缓冲区和渲染器,然后把更新和绘制流程交给 `GameApp`

View File

@ -1,381 +1,138 @@
#include <iostream>
#include <array>
#include <cstdint>
<<<<<<< HEAD:src/main.cpp
#include <fstream>
#include <string>
#include <vector>
=======
#include <cstdio>
#include <cstring>
#include <thread>
#include <algorithm>
#include <chrono>
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include "Vector2.h"
#include "Vector3.h"
#include "Vector4.h"
#include "Matrix4x4.h"
#include "MathUtil.h"
#include "Color.h"
#include "Triangle.h"
#include "Camera.h"
#include <cstdint>
#include <cstdlib>
#include "Timer.h"
#include "Vertex.h"
<<<<<<< HEAD:src/main.cpp
#include "DepthBuffer.h"
#include "SpriteAssetLoader.h"
#include "SpriteRasterizer.h"
#include "Image.h"
#include "TomGameApp.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "ButtonInput.h"
=======
#include "DrawContext.h"
#include "test_sprite.h"
#include "font_atlas.h"
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include <cstring>
#include <cstdio>
#include <iostream>
#include <thread>
#include "BitmapFont.h"
#include "Color.h"
#include "Display.h"
#include "DrawContext.h"
#include "Image.h"
#include "TimeSource.h"
#include "Timer.h"
#include "font_atlas.h"
#include "test_sprite.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#endif
const int32_t width = 800;
const int32_t height = 600;
const int32_t tomWidth = 800;
const int32_t tomHeight = 480;
struct ProjectedVertex
namespace
{
Math::Vector3 screen;
bool visible = false;
};
const int32_t DemoWidth = 800;
const int32_t DemoHeight = 600;
struct CubeFace
{
std::array<int, 4> vertices;
};
struct CubeTriangle
{
std::array<int, 3> vertices;
};
static ProjectedVertex ProjectToScreen(
const Math::Vector3 &vertex,
const Math::Matrix4x4 &mvp,
const Math::Matrix4x4 &viewport)
{
using namespace Math;
const Vector4 clip = mvp * Vector4::Point(vertex);
if (std::abs(clip.w) < 1e-5f)
struct ProgramOptions
{
return {};
}
uint32_t target_fps;
bool show_help;
const float invW = 1.0f / clip.w;
const float ndcX = clip.x * invW;
const float ndcY = clip.y * invW;
const float ndcZ = clip.z * invW;
if (ndcX < -1.0f || ndcX > 1.0f || ndcY < -1.0f || ndcY > 1.0f || ndcZ < -1.0f || ndcZ > 1.0f)
{
return {};
}
const Vector4 screen = viewport * Vector4(ndcX, ndcY, ndcZ, 1.0f);
ProjectedVertex result;
result.screen = Vector3(screen.x, screen.y, screen.z);
result.visible = true;
return result;
}
static bool IsFaceVisible(const CubeFace &face, const std::array<Math::Vector3, 8> &viewSpaceVertices)
{
using namespace Math;
const Vector3 &v0 = viewSpaceVertices[face.vertices[0]];
const Vector3 &v1 = viewSpaceVertices[face.vertices[1]];
const Vector3 &v2 = viewSpaceVertices[face.vertices[2]];
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
const Vector3 faceCenter =
(viewSpaceVertices[face.vertices[0]] +
viewSpaceVertices[face.vertices[1]] +
viewSpaceVertices[face.vertices[2]] +
viewSpaceVertices[face.vertices[3]]) /
4.0f;
return faceNormal.dot(faceCenter) > 0.0f;
}
static bool IsTriangleVisible(const CubeTriangle &triangle, const std::array<Math::Vector3, 8> &viewSpaceVertices)
{
using namespace Math;
const Vector3 &v0 = viewSpaceVertices[triangle.vertices[0]];
const Vector3 &v1 = viewSpaceVertices[triangle.vertices[1]];
const Vector3 &v2 = viewSpaceVertices[triangle.vertices[2]];
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
const Vector3 faceCenter = (v0 + v1 + v2) / 3.0f;
return faceNormal.dot(faceCenter) > 0.0f;
}
<<<<<<< HEAD:src/main.cpp
static bool HasArg(int argc, char* argv[], const std::string& expected)
{
for (int i = 1; i < argc; ++i)
{
if (argv[i] != nullptr && expected == argv[i])
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
return true;
}
}
return false;
}
static std::string FindSpriteAssetPath(const std::string& fileName)
{
const char* roots[] = {
"game/assets/sprites/",
"../game/assets/sprites/",
"../../game/assets/sprites/",
"../../../game/assets/sprites/",
"/tmp/game/assets/sprites/",
"/usr/local/share/imx6u-game/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
static Platform::IDisplay* CreateDisplay()
{
const std::string path = std::string(roots[i]) + fileName;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("game/assets/sprites/") + fileName;
}
static bool LoadSpriteAsset(const std::string& fileName, RenderData::Image& image)
{
const std::string path = FindSpriteAssetPath(fileName);
if (!Asset::SpriteAssetLoader::Load(path, image))
{
std::cerr << "Load sprite asset failed: " << path << std::endl;
return false;
}
return true;
}
static int RunTomGame()
{
#ifdef USE_FRAMEBUFFER
Platform::IDisplay *display = new Platform::FBDisplay();
return new Platform::FBDisplay();
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
return new Platform::SDLDisplay();
#endif
if (!display->init(tomWidth, tomHeight))
{
delete display;
return -1;
}
Core::FrameBuffer frameBuffer(tomWidth, tomHeight);
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
RenderData::Image background;
RenderData::Image tomStand;
RenderData::Image tomListen;
RenderData::Image tomSay1;
RenderData::Image tomSay2;
RenderData::Image tomSay3;
RenderData::Image tomSay4;
RenderData::Image button;
if (!LoadSpriteAsset("background.sprite", background) ||
!LoadSpriteAsset("Tom-stand.sprite", tomStand) ||
!LoadSpriteAsset("Tom-listhen.sprite", tomListen) ||
!LoadSpriteAsset("Tom-say1.sprite", tomSay1) ||
!LoadSpriteAsset("Tom-say2.sprite", tomSay2) ||
!LoadSpriteAsset("Tom-say3.sprite", tomSay3) ||
!LoadSpriteAsset("Tom-say4.sprite", tomSay4) ||
!LoadSpriteAsset("ui-record.sprite", button))
static void PrintUsage(const char* program_name)
{
std::cerr << "Run ConvertTomSprites before starting --tom." << std::endl;
display->shutdown();
delete display;
return -1;
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
Game::AudioInput audioInput;
Game::AudioOutput audioOutput;
Game::ButtonInput buttonInput;
#if defined(__linux__)
buttonInput.init();
#endif
Game::TomGameAssets assets;
assets.background = &background;
assets.tomStand = &tomStand;
assets.tomListen = &tomListen;
assets.tomSay1 = &tomSay1;
assets.tomSay2 = &tomSay2;
assets.tomSay3 = &tomSay3;
assets.tomSay4 = &tomSay4;
assets.button = &button;
Game::TomGameApp app(&frameBuffer, &spriteRasterizer, &audioInput, &audioOutput, &buttonInput);
app.set_assets(assets);
if (!app.is_ready())
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
{
std::cerr << "TomGameApp assets are incomplete." << std::endl;
display->shutdown();
delete display;
return -1;
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
const char* fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
bool isRunning = true;
uint32_t lastTime = display->get_time_ms();
while (isRunning)
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
{
display->poll_events(isRunning);
const uint32_t currentTime = display->get_time_ms();
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
lastTime = currentTime;
app.update(deltaTime);
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
app.draw();
display->present(&frameBuffer);
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));
}
}
buttonInput.shutdown();
audioInput.shutdown();
audioOutput.shutdown();
display->shutdown();
delete display;
return 0;
=======
static void PrintUsage(const char *program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
struct ProgramOptions
int main(int argc, char* argv[])
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
{
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char *arg = argv[i];
const char *fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
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));
}
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
}
int main(int argc, char *argv[])
{
<<<<<<< HEAD:src/main.cpp
if (HasArg(argc, argv, "--tom"))
{
return RunTomGame();
}
=======
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#ifdef USE_FRAMEBUFFER
Platform::IDisplay *display = new Platform::FBDisplay();
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
#endif
if (!display->init(width, height))
Platform::IDisplay* display = CreateDisplay();
if (!display->init(DemoWidth, DemoHeight))
{
delete display;
return -1;
}
Gfx::DrawContext ctx(width, height);
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
Core::DrawContext ctx(DemoWidth, DemoHeight);
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
RenderData::BitmapFont font;
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
@ -384,164 +141,56 @@ int main(int argc, char *argv[])
font.columns = font_columns;
font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = {
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
const std::array<Math::Vector3, 8> cubeVertices = {
Math::Vector3(-0.5f, -0.5f, -0.5f),
Math::Vector3(0.5f, -0.5f, -0.5f),
Math::Vector3(0.5f, 0.5f, -0.5f),
Math::Vector3(-0.5f, 0.5f, -0.5f),
Math::Vector3(-0.5f, -0.5f, 0.5f),
Math::Vector3(0.5f, -0.5f, 0.5f),
Math::Vector3(0.5f, 0.5f, 0.5f),
Math::Vector3(-0.5f, 0.5f, 0.5f)};
const std::array<CubeFace, 6> cubeFaces = {
CubeFace{{0, 3, 2, 1}},
CubeFace{{4, 5, 6, 7}},
CubeFace{{0, 4, 7, 3}},
CubeFace{{1, 2, 6, 5}},
CubeFace{{0, 1, 5, 4}},
CubeFace{{3, 7, 6, 2}}};
const std::array<CubeTriangle, 12> cubeTriangles = {
CubeTriangle{{0, 3, 2}}, CubeTriangle{{0, 2, 1}},
CubeTriangle{{4, 5, 6}}, CubeTriangle{{4, 6, 7}},
CubeTriangle{{0, 4, 7}}, CubeTriangle{{0, 7, 3}},
CubeTriangle{{1, 2, 6}}, CubeTriangle{{1, 6, 5}},
CubeTriangle{{0, 1, 5}}, CubeTriangle{{0, 5, 4}},
CubeTriangle{{3, 7, 6}}, CubeTriangle{{3, 6, 2}}};
const RenderData::Color clearColor(18, 18, 24, 255);
const RenderData::Color cubeColor(240, 240, 240, 255);
const RenderData::Color fpsColor(0, 255, 80, 255);
const RenderData::Color fpsBg(0, 0, 0, 200);
const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
RenderData::Image sprite(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000u);
int32_t fps = 0;
int32_t frame_count = 0;
uint32_t last_fps_time = 0;
uint32_t animation_time_ms = 0;
char fps_text[32];
bool isRunning = true;
uint32_t animation_time_ms = 0;
while (isRunning)
std::cout << "Demo started. Target FPS: " << timer.target_fps() << std::endl;
bool is_running = true;
while (is_running)
{
timer.begin_frame(time_source.get_time_ms());
const uint32_t fixed_delta_ms = timer.fixed_delta_ms();
animation_time_ms += fixed_delta_ms;
animation_time_ms += timer.fixed_delta_ms();
display->poll_events(isRunning);
ctx.clear(clearColor);
const float animation_time = static_cast<float>(animation_time_ms) / 1000.0f;
const Math::Matrix4x4 model =
Math::MathUtil::get_rotation_matrix_y(animation_time) *
Math::MathUtil::get_rotation_matrix_x(static_cast<float>(animation_time_ms * 6u) / 10000.0f);
const Math::Matrix4x4 view = camera.get_view_matrix();
const Math::Matrix4x4 modelView = view * model;
const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio);
const Math::Matrix4x4 viewport = camera.get_viewport_matrix(static_cast<float>(width), static_cast<float>(height));
const Math::Matrix4x4 mvp = projection * modelView;
std::array<Math::Vector3, 8> viewSpaceVertices;
std::array<ProjectedVertex, 8> projectedVertices;
for (size_t i = 0; i < cubeVertices.size(); ++i)
bool should_quit = false;
display->poll_events(should_quit);
if (should_quit)
{
viewSpaceVertices[i] = (modelView * Math::Vector4::Point(cubeVertices[i])).to_vector3();
projectedVertices[i] = ProjectToScreen(cubeVertices[i], mvp, viewport);
is_running = false;
}
std::array<bool, 6> visibleFaces = {};
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
visibleFaces[faceIndex] = IsFaceVisible(cubeFaces[faceIndex], viewSpaceVertices);
}
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.fill_rect(48, 72, 704, 360, RenderData::Color(42, 48, 60, 255));
ctx.fill_rect(64, 88, 672, 328, RenderData::Color(24, 28, 36, 255));
std::array<RenderData::Triangle, 12> drawTriangles;
size_t drawCommandCount = 0;
for (const CubeTriangle &cubeTriangle : cubeTriangles)
{
if (!IsTriangleVisible(cubeTriangle, viewSpaceVertices))
{
continue;
}
const int32_t travel = std::max(1, DemoWidth - sprite.width - 128);
const int32_t sprite_x = 64 + static_cast<int32_t>((animation_time_ms / 8u) % static_cast<uint32_t>(travel));
const int32_t sprite_y = 220;
ctx.draw_sprite(sprite_x, sprite_y, sprite);
ctx.draw_text(font, 64, 96, RenderData::Color(240, 240, 240, 255), "GFX DEMO");
const ProjectedVertex &v0 = projectedVertices[cubeTriangle.vertices[0]];
const ProjectedVertex &v1 = projectedVertices[cubeTriangle.vertices[1]];
const ProjectedVertex &v2 = projectedVertices[cubeTriangle.vertices[2]];
if (!v0.visible || !v1.visible || !v2.visible)
{
continue;
}
drawTriangles[drawCommandCount++] =
RenderData::Triangle(
Scene::Vertex(v0.screen),
Scene::Vertex(v1.screen),
Scene::Vertex(v2.screen));
}
for (size_t i = 0; i < drawCommandCount; ++i)
{
ctx.draw_triangle(drawTriangles[i], cubeColor);
}
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
if (!visibleFaces[faceIndex])
{
continue;
}
const CubeFace &face = cubeFaces[faceIndex];
for (size_t edgeOffset = 0; edgeOffset < face.vertices.size(); ++edgeOffset)
{
const int startIndex = face.vertices[edgeOffset];
const int endIndex = face.vertices[(edgeOffset + 1) % face.vertices.size()];
const ProjectedVertex &start = projectedVertices[startIndex];
const ProjectedVertex &end = projectedVertices[endIndex];
if (!start.visible || !end.visible)
{
continue;
}
ctx.draw_line(
Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(),
Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(),
clearColor);
}
}
// sprite 测试
ctx.draw_sprite(10, 10, sprite_img);
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(animation_time_ms / 20u) % 32, 0);
// FPS 计数
++frame_count;
const uint32_t now = time_source.get_time_ms();
if (now - last_fps_time >= 1000)
if (now - last_fps_time >= 1000u)
{
fps = frame_count;
frame_count = 0;
last_fps_time = now;
}
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
ctx.draw_text(
font,
4,
4,
RenderData::Color(0, 255, 80, 255),
RenderData::Color(0, 0, 0, 200),
fps_text);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);

301
src/Apps/Game/Main.cpp Normal file
View File

@ -0,0 +1,301 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include "Color.h"
#include "Display.h"
#include "DrawContext.h"
#include "Image.h"
#include "SpriteAssetLoader.h"
#include "TimeSource.h"
#include "Timer.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#endif
namespace
{
const int32_t ScreenWidth = 800;
const int32_t ScreenHeight = 480;
struct ProgramOptions
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
struct SpriteImage
{
std::vector<uint32_t> pixels;
RenderData::Image image;
bool is_valid() const
{
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
};
static Platform::IDisplay* CreateDisplay()
{
#ifdef USE_FRAMEBUFFER
return new Platform::FBDisplay();
#else
return new Platform::SDLDisplay();
#endif
}
static void PrintUsage(const char* program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
{
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
const char* fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
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));
}
}
static std::string FindSpriteAssetPath(const std::string& file_name)
{
const char* roots[] = {
"src/Apps/Game/assets/sprites/",
"../src/Apps/Game/assets/sprites/",
"../../src/Apps/Game/assets/sprites/",
"../../../src/Apps/Game/assets/sprites/",
"/usr/local/share/imx6u-game/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
{
const std::string path = std::string(roots[i]) + file_name;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("src/Apps/Game/assets/sprites/") + file_name;
}
static void EnableAlphaColorKey(SpriteImage& sprite)
{
for (size_t i = 0; i < sprite.pixels.size(); ++i)
{
if ((sprite.pixels[i] & 0xFFu) == 0u)
{
sprite.pixels[i] = 0x00000000u;
}
}
sprite.image = RenderData::Image(
sprite.pixels.data(),
sprite.image.width,
sprite.image.height,
0x00000000u);
}
static bool LoadSpriteAsset(const std::string& file_name, SpriteImage& sprite, bool use_alpha_key)
{
const std::string path = FindSpriteAssetPath(file_name);
if (!Asset::SpriteAssetLoader::Load(path, sprite.pixels, sprite.image))
{
std::cerr << "Load sprite asset failed: " << path << std::endl;
return false;
}
if (use_alpha_key)
{
EnableAlphaColorKey(sprite);
}
return sprite.is_valid();
}
static const RenderData::Image& SelectTomFrame(
uint32_t animation_time_ms,
const SpriteImage& stand,
const SpriteImage& listen,
const SpriteImage* const* speaking_frames,
size_t speaking_frame_count)
{
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
if (phase < 2u)
{
return stand.image;
}
if (phase < 3u)
{
return listen.image;
}
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
return speaking_frames[frame_index]->image;
}
}
int main(int argc, char* argv[])
{
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Platform::IDisplay* display = CreateDisplay();
if (!display->init(ScreenWidth, ScreenHeight))
{
delete display;
return -1;
}
SpriteImage background;
SpriteImage tom_stand;
SpriteImage tom_listen;
SpriteImage tom_say1;
SpriteImage tom_say2;
SpriteImage tom_say3;
SpriteImage tom_say4;
SpriteImage record_button;
if (!LoadSpriteAsset("background.sprite", background, false) ||
!LoadSpriteAsset("Tom-stand.sprite", tom_stand, true) ||
!LoadSpriteAsset("Tom-listhen.sprite", tom_listen, true) ||
!LoadSpriteAsset("Tom-say1.sprite", tom_say1, true) ||
!LoadSpriteAsset("Tom-say2.sprite", tom_say2, true) ||
!LoadSpriteAsset("Tom-say3.sprite", tom_say3, true) ||
!LoadSpriteAsset("Tom-say4.sprite", tom_say4, true) ||
!LoadSpriteAsset("ui-record.sprite", record_button, true))
{
std::cerr << "Missing Tom sprite assets. Run the ConvertTomSprites target first.\n";
display->shutdown();
delete display;
return -1;
}
const SpriteImage* speaking_frames[] = {
&tom_say1,
&tom_say2,
&tom_say3,
&tom_say4,
&tom_say3,
&tom_say2
};
const size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
std::cout << "Tom game started. Target FPS: " << timer.target_fps() << std::endl;
bool is_running = true;
uint32_t animation_time_ms = 0;
while (is_running)
{
timer.begin_frame(time_source.get_time_ms());
animation_time_ms += timer.fixed_delta_ms();
bool should_quit = false;
display->poll_events(should_quit);
if (should_quit)
{
is_running = false;
}
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(0, 0, background.image);
const RenderData::Image& tom = SelectTomFrame(
animation_time_ms,
tom_stand,
tom_listen,
speaking_frames,
speaking_frame_count);
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
ctx.draw_sprite(tom_x, tom_y, tom);
const int32_t button_x = (ScreenWidth - record_button.image.width) / 2;
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
ctx.draw_sprite(button_x, button_y, record_button.image);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);
}
display->shutdown();
delete display;
return 0;
}

74
src/Apps/Game/README.md Normal file
View File

@ -0,0 +1,74 @@
# Tom Game
`src/Apps/Game` 是 Tom 游戏应用目录。仓库根目录下的 `src/Core` 继续作为渲染、数学、光栅化和平台显示层Game 目录只放游戏入口、游戏逻辑、素材和素材转换工具。
## 当前入口
主构建目标 `IMX6U-Game` 现在从下面的文件启动:
```text
src/Apps/Game/Main.cpp
```
运行后默认启动 Tom不再需要 `--tom` 参数。可选参数只保留帧率:
```bash
IMX6U-Game.exe --fps 30
IMX6U-Game.exe --fps=60
```
## 素材目录
```text
src/Apps/Game/assets/raw/ PNG 源素材
src/Apps/Game/assets/sprites/ 转换后的 .sprite 运行时素材
src/Apps/Game/tools/ 素材转换等开发工具
src/Apps/Game/src/ 旧版 TomGameApp、音频、动画等游戏模块
src/Apps/Game/tests/ 手工测试或后续测试代码
```
板端运行时不直接解码 PNG。开发时把 PNG 放在 `src/Apps/Game/assets/raw/`,再转换为 `src/Apps/Game/assets/sprites/*.sprite`
## 素材转换
`.sprite` 是项目自定义的 RGBA8888 二进制格式:
```text
offset size content
0 4 magic: "SPRT"
4 4 version: 1, little-endian uint32
8 4 width, little-endian uint32
12 4 height, little-endian uint32
16 4 format: 1 means RGBA8888
20 * width * height pixels, little-endian uint32, 0xRRGGBBAA
```
常用命令:
```bash
cmake --build build-win --config Release --target SpriteAssetTool
cmake --build build-win --config Release --target ConvertTomSprites
```
单张转换示例:
```bash
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/ui-record.png src/Apps/Game/assets/sprites/ui-record.sprite
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/Tom-stand.png src/Apps/Game/assets/sprites/Tom-stand.sprite --fit 560 360
```
## 运行
Windows PC
```bash
./build-win/Release/IMX6U-Game.exe
```
Linux framebuffer / IMX6U
```bash
./IMX6U-Game --fps 30
```
部署到板端时,把可执行文件和 `src/Apps/Game/assets/sprites/` 一起拷贝,并保持相对路径;也可以把 `.sprite` 放到 `/usr/local/share/imx6u-game/sprites/`

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 2.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View File

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 2.5 MiB

After

Width:  |  Height:  |  Size: 2.5 MiB

View File

Before

Width:  |  Height:  |  Size: 2.6 MiB

After

Width:  |  Height:  |  Size: 2.6 MiB

View File

Before

Width:  |  Height:  |  Size: 2.4 MiB

After

Width:  |  Height:  |  Size: 2.4 MiB

View File

Before

Width:  |  Height:  |  Size: 5.5 MiB

After

Width:  |  Height:  |  Size: 5.5 MiB

View File

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 88 KiB

View File

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 89 KiB

View File

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

View File

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 80 KiB

View File

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -3,9 +3,9 @@
#include "../hardware/AudioInput.h"
#include "../hardware/AudioOutput.h"
#include "../hardware/ButtonInput.h"
#include "../../../src/Core/FrameBuffer.h"
#include "../../../src/Rasterizer/SpriteRasterizer.h"
#include "../../../src/RenderData/Image.h"
#include "FrameBuffer.h"
#include "SpriteRasterizer.h"
#include "Image.h"
namespace Game
{

View File

@ -4,7 +4,7 @@
#include "../audio/VoicePlayer.h"
#include "../audio/VoiceRecorder.h"
#include "../components/SpriteAnimator.h"
#include "../../../src/RenderData/Sprite.h"
#include "Sprite.h"
namespace Core
{

View File

@ -1,7 +1,7 @@
#pragma once
#include <cstddef>
#include <vector>
#include "../../../src/RenderData/Sprite.h"
#include "Sprite.h"
namespace Game
{

View File

@ -1,5 +1,5 @@
#include "AnimationSystem.h"
#include "../../../src/Rasterizer/SpriteRasterizer.h"
#include "SpriteRasterizer.h"
namespace Game
{

View File

@ -18,10 +18,10 @@
static std::string FindAssetPath(const std::string& fileName)
{
const char* roots[] = {
"game/assets/sprites/",
"../game/assets/sprites/",
"../../game/assets/sprites/",
"../../../game/assets/sprites/"
"src/Apps/Game/assets/sprites/",
"../src/Apps/Game/assets/sprites/",
"../../src/Apps/Game/assets/sprites/",
"../../../src/Apps/Game/assets/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
@ -34,7 +34,7 @@ static std::string FindAssetPath(const std::string& fileName)
}
}
return std::string("game/assets/sprites/") + fileName;
return std::string("src/Apps/Game/assets/sprites/") + fileName;
}
static bool LoadSpriteImage(const std::string& fileName, RenderData::Image& image)

View File

@ -41,6 +41,23 @@ namespace
TransformOptions() : mode(TransformMode::None), width(0), height(0) {}
};
struct LoadedImage
{
std::vector<uint32_t> pixels;
RenderData::Image image;
void assign(int32_t width, int32_t height, const std::vector<uint32_t>& sourcePixels)
{
pixels = sourcePixels;
image = RenderData::Image(pixels.empty() ? nullptr : pixels.data(), width, height);
}
bool is_valid() const
{
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
};
static bool IsPathSeparator(char value)
{
return value == '/' || value == '\\';
@ -274,7 +291,7 @@ namespace
}
}
static bool LoadPngImage(const std::string& path, RenderData::Image& image)
static bool LoadPngImage(const std::string& path, LoadedImage& image)
{
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
if (loadedSurface == nullptr)
@ -291,7 +308,7 @@ static bool LoadPngImage(const std::string& path, RenderData::Image& image)
return false;
}
std::vector<uint32_t> pixels(static_cast<size_t>(rgbaSurface->w) * rgbaSurface->h, 0);
std::vector<uint32_t> pixels(static_cast<size_t>(rgbaSurface->w) * static_cast<size_t>(rgbaSurface->h), 0);
if (SDL_LockSurface(rgbaSurface) != 0)
{
std::cerr << "SDL_LockSurface failed: " << SDL_GetError() << std::endl;
@ -310,45 +327,48 @@ static bool LoadPngImage(const std::string& path, RenderData::Image& image)
}
SDL_UnlockSurface(rgbaSurface);
image = RenderData::Image(rgbaSurface->w, rgbaSurface->h, pixels);
image.assign(rgbaSurface->w, rgbaSurface->h, pixels);
SDL_FreeSurface(rgbaSurface);
return image.is_valid();
}
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
static LoadedImage ResizeImageNearest(const LoadedImage& source, int32_t width, int32_t height)
{
if (!source.is_valid() || width <= 0 || height <= 0)
{
return RenderData::Image();
return LoadedImage();
}
RenderData::Image result(width, height);
std::vector<uint32_t> pixels(static_cast<size_t>(width) * static_cast<size_t>(height), 0);
for (int32_t y = 0; y < height; ++y)
{
const int32_t sourceY = y * source.get_height() / height;
const int32_t sourceY = y * source.image.height / height;
for (int32_t x = 0; x < width; ++x)
{
const int32_t sourceX = x * source.get_width() / width;
result.set_pixel(x, y, source.get_pixel_fast(sourceX, sourceY));
const int32_t sourceX = x * source.image.width / width;
pixels[static_cast<size_t>(y) * static_cast<size_t>(width) + static_cast<size_t>(x)] =
source.image.pixels[static_cast<size_t>(sourceY) * static_cast<size_t>(source.image.width) + static_cast<size_t>(sourceX)];
}
}
LoadedImage result;
result.assign(width, height, pixels);
return result;
}
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
static LoadedImage ResizeImageToFit(const LoadedImage& source, int32_t maxWidth, int32_t maxHeight)
{
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
{
return RenderData::Image();
return LoadedImage();
}
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
const float scaleY = static_cast<float>(maxHeight) / source.get_height();
const float scaleX = static_cast<float>(maxWidth) / source.image.width;
const float scaleY = static_cast<float>(maxHeight) / source.image.height;
const float scale = std::min(scaleX, scaleY);
const int32_t width = std::max(1, static_cast<int32_t>(source.get_width() * scale));
const int32_t height = std::max(1, static_cast<int32_t>(source.get_height() * scale));
const int32_t width = std::max(1, static_cast<int32_t>(source.image.width * scale));
const int32_t height = std::max(1, static_cast<int32_t>(source.image.height * scale));
return ResizeImageNearest(source, width, height);
}
@ -385,7 +405,7 @@ static TransformOptions ResolveTransformForInput(
return resolved;
}
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, RenderData::Image& image)
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, LoadedImage& image)
{
const TransformOptions resolved = ResolveTransformForInput(inputPath, options);
switch (resolved.mode)
@ -413,7 +433,7 @@ static bool ConvertFile(
const std::string& outputPath,
const TransformOptions& transformOptions)
{
RenderData::Image image;
LoadedImage image;
if (!LoadPngImage(inputPath, image))
{
return false;
@ -430,14 +450,14 @@ static bool ConvertFile(
return false;
}
if (!Asset::SpriteAssetLoader::Save(outputPath, image))
if (!Asset::SpriteAssetLoader::Save(outputPath, image.image))
{
std::cerr << "Save failed: " << outputPath << std::endl;
return false;
}
std::cout << inputPath << " -> " << outputPath
<< " (" << image.get_width() << "x" << image.get_height() << ")" << std::endl;
<< " (" << image.image.width << "x" << image.image.height << ")" << std::endl;
return true;
}
@ -532,7 +552,7 @@ static void PrintUsage()
std::cout << " SpriteAssetTool input.png output.sprite [--fit maxWidth maxHeight]" << std::endl;
std::cout << " SpriteAssetTool --batch inputDir outputDir [--resize width height]" << std::endl;
std::cout << " SpriteAssetTool --batch inputDir outputDir [--fit maxWidth maxHeight]" << std::endl;
std::cout << " SpriteAssetTool --batch game/assets/raw game/assets/sprites --preset tom-800x480" << std::endl;
std::cout << " SpriteAssetTool --batch src/Apps/Game/assets/raw src/Apps/Game/assets/sprites --preset tom-800x480" << std::endl;
}
int main(int argc, char* argv[])

View File

@ -0,0 +1,182 @@
#include "SpriteAssetLoader.h"
#include <cstdint>
#include <fstream>
#include <limits>
namespace
{
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
static const size_t SpriteHeaderSize = 20;
static const uint32_t SpriteVersion = 1;
static const uint32_t SpriteFormatRgba8888 = 1;
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
static bool ReadExact(std::ifstream& file, char* data, size_t size)
{
file.read(data, static_cast<std::streamsize>(size));
return static_cast<size_t>(file.gcount()) == size;
}
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
{
if (offset + 4 > bytes.size())
{
return false;
}
value =
static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
return true;
}
static void WriteU32LE(std::ofstream& file, uint32_t value)
{
const char bytes[4] = {
static_cast<char>(value & 0xFF),
static_cast<char>((value >> 8) & 0xFF),
static_cast<char>((value >> 16) & 0xFF),
static_cast<char>((value >> 24) & 0xFF)
};
file.write(bytes, sizeof(bytes));
}
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
{
if (width == 0 || height == 0)
{
return false;
}
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
if (total > MaxSpritePixels ||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
{
return false;
}
pixelCount = static_cast<size_t>(total);
return true;
}
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
{
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
{
return false;
}
for (size_t i = 0; i < pixels.size(); ++i)
{
uint32_t value = 0;
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
{
return false;
}
pixels[i] = value;
}
return true;
}
}
namespace Asset
{
bool SpriteAssetLoader::Load(
const std::string& path,
std::vector<uint32_t>& pixels,
RenderData::Image& image)
{
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
std::vector<uint8_t> header(SpriteHeaderSize, 0);
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
{
return false;
}
if (header[0] != SpriteMagic[0] ||
header[1] != SpriteMagic[1] ||
header[2] != SpriteMagic[2] ||
header[3] != SpriteMagic[3])
{
return false;
}
uint32_t version = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t format = 0;
if (!ReadU32LE(header, 4, version) ||
!ReadU32LE(header, 8, width) ||
!ReadU32LE(header, 12, height) ||
!ReadU32LE(header, 16, format))
{
return false;
}
size_t pixelCount = 0;
if (version != SpriteVersion ||
format != SpriteFormatRgba8888 ||
!CheckPixelCount(width, height, pixelCount))
{
return false;
}
std::vector<uint32_t> loadedPixels(pixelCount, 0);
if (!ReadPixels(file, loadedPixels))
{
return false;
}
char trailingByte = 0;
file.read(&trailingByte, 1);
if (file.gcount() != 0)
{
return false;
}
pixels.swap(loadedPixels);
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height));
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
{
if (image.pixels == nullptr || image.width <= 0 || image.height <= 0)
{
return false;
}
size_t pixelCount = 0;
if (!CheckPixelCount(static_cast<uint32_t>(image.width), static_cast<uint32_t>(image.height), pixelCount))
{
return false;
}
std::ofstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
file.write(SpriteMagic, sizeof(SpriteMagic));
WriteU32LE(file, SpriteVersion);
WriteU32LE(file, static_cast<uint32_t>(image.width));
WriteU32LE(file, static_cast<uint32_t>(image.height));
WriteU32LE(file, SpriteFormatRgba8888);
for (size_t i = 0; i < pixelCount; ++i)
{
WriteU32LE(file, image.pixels[i]);
}
return file.good();
}
}

View File

@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <vector>
#include "Image.h"
namespace Asset
{
class SpriteAssetLoader
{
public:
static const char* GetFileExtension() { return ".sprite"; }
static bool Load(const std::string& path, std::vector<uint32_t>& pixels, RenderData::Image& image);
static bool Save(const std::string& path, const RenderData::Image& image);
};
}

View File

@ -6,7 +6,7 @@
#include "Display.h"
#include <algorithm>
namespace Gfx
namespace Core
{
DrawContext::DrawContext(int32_t width, int32_t height)
{

View File

@ -25,7 +25,7 @@ namespace Platform
class IDisplay;
}
namespace Gfx
namespace Core
{
class DrawContext
{

Some files were not shown because too many files have changed in this diff Show More