111
171
CMakeLists.txt
|
|
@ -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(SOURCES
|
||||
src/main.cpp
|
||||
${ENGINE_SOURCES}
|
||||
)
|
||||
|
||||
if(USE_FRAMEBUFFER)
|
||||
list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
|
||||
else()
|
||||
list(APPEND SOURCES src/Gfx/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
|
||||
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
|
||||
)
|
||||
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5
|
||||
|
||||
set(SOURCES
|
||||
src/Apps/Game/Main.cpp
|
||||
${CORE_SOURCES}
|
||||
)
|
||||
|
||||
if(USE_FRAMEBUFFER)
|
||||
list(APPEND SOURCES src/Core/Platform/FBDisplay.cpp)
|
||||
else()
|
||||
list(APPEND SOURCES src/Core/Platform/SDLDisplay.cpp)
|
||||
endif()
|
||||
|
||||
add_executable(IMX6U-Game ${SOURCES})
|
||||
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
|
||||
|
|
|
|||
20
README.md
|
|
@ -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
|
|||
├──────────────────────────────────────────────┤
|
||||
│ Shared:UI、存档、配置、资源索引 │
|
||||
├──────────────────────────────────────────────┤
|
||||
│ Gfx:DrawContext、FrameBuffer、Draw2D、Math │
|
||||
│ Core:DrawContext、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 模型加载与完整光照
|
||||
|
||||
## 说明
|
||||
|
|
|
|||
|
|
@ -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` 这个重复目录名。
|
||||
|
|
@ -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 可以依赖 Gfx;Gfx 不能依赖 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 快路径。
|
||||
|
|
@ -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. 通用约定
|
||||
|
||||
|
|
|
|||
|
|
@ -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 秒数。
|
||||
|
||||
|
|
|
|||
116
game/README.md
|
|
@ -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`。
|
||||
|
|
@ -1,281 +1,35 @@
|
|||
#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 {};
|
||||
}
|
||||
|
||||
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])
|
||||
{
|
||||
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)
|
||||
{
|
||||
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();
|
||||
#else
|
||||
Platform::IDisplay *display = 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))
|
||||
{
|
||||
std::cerr << "Run ConvertTomSprites before starting --tom." << std::endl;
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
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())
|
||||
{
|
||||
std::cerr << "TomGameApp assets are incomplete." << std::endl;
|
||||
display->shutdown();
|
||||
delete display;
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool isRunning = true;
|
||||
uint32_t lastTime = display->get_time_ms();
|
||||
while (isRunning)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
uint32_t target_fps;
|
||||
bool show_help;
|
||||
|
||||
|
|
@ -284,16 +38,32 @@ struct ProgramOptions
|
|||
show_help(false)
|
||||
{
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
||||
{
|
||||
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;
|
||||
const char* arg = argv[i];
|
||||
const char* fps_value = nullptr;
|
||||
|
||||
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
|
||||
{
|
||||
|
|
@ -332,50 +102,37 @@ static ProgramOptions ParseProgramOptions(int argc, char *argv[])
|
|||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
|
||||
{
|
||||
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[])
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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/`。
|
||||
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 2.2 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 2.5 MiB After Width: | Height: | Size: 2.5 MiB |
|
Before Width: | Height: | Size: 2.6 MiB After Width: | Height: | Size: 2.6 MiB |
|
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 5.5 MiB After Width: | Height: | Size: 5.5 MiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
|
|
@ -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
|
||||
{
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
#pragma once
|
||||
#include <cstddef>
|
||||
#include <vector>
|
||||
#include "../../../src/RenderData/Sprite.h"
|
||||
#include "Sprite.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
#include "AnimationSystem.h"
|
||||
#include "../../../src/Rasterizer/SpriteRasterizer.h"
|
||||
#include "SpriteRasterizer.h"
|
||||
|
||||
namespace Game
|
||||
{
|
||||
|
|
@ -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)
|
||||
|
|
@ -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[])
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
#include "Display.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace Gfx
|
||||
namespace Core
|
||||
{
|
||||
DrawContext::DrawContext(int32_t width, int32_t height)
|
||||
{
|
||||
|
|
@ -25,7 +25,7 @@ namespace Platform
|
|||
class IDisplay;
|
||||
}
|
||||
|
||||
namespace Gfx
|
||||
namespace Core
|
||||
{
|
||||
class DrawContext
|
||||
{
|
||||