Compare commits
6 Commits
d92b890528
...
964a11a215
| Author | SHA1 | Date |
|---|---|---|
|
|
964a11a215 | |
|
|
0defa04eef | |
|
|
352a1233e6 | |
|
|
c177114bf3 | |
|
|
b093796e61 | |
|
|
3beae16917 |
133
CMakeLists.txt
|
|
@ -10,49 +10,50 @@ endif()
|
||||||
|
|
||||||
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
|
option(USE_FRAMEBUFFER "Use Linux framebuffer instead of SDL2" OFF)
|
||||||
|
|
||||||
set(SOURCES
|
set(CORE_SOURCES
|
||||||
src/Apps/Demo/main.cpp
|
src/Core/Asset/ObjLoader.cpp
|
||||||
src/Gfx/Asset/ObjLoader.cpp
|
src/Core/Asset/SpriteAssetLoader.cpp
|
||||||
src/Gfx/Core/DepthBuffer.cpp
|
src/Core/Core/DepthBuffer.cpp
|
||||||
src/Gfx/Core/FrameBuffer.cpp
|
src/Core/Core/FrameBuffer.cpp
|
||||||
src/Gfx/Core/Renderer.cpp
|
src/Core/Core/Renderer.cpp
|
||||||
src/Gfx/Draw2D/DrawContext.cpp
|
src/Core/Draw2D/DrawContext.cpp
|
||||||
src/Gfx/Rasterizer/Rasterizer.cpp
|
src/Core/Rasterizer/Rasterizer.cpp
|
||||||
src/Gfx/Rasterizer/TriangleRasterizer.cpp
|
src/Core/Rasterizer/TriangleRasterizer.cpp
|
||||||
src/Gfx/Scene/Camera.cpp
|
src/Core/Scene/Camera.cpp
|
||||||
src/Gfx/Shading/BlinnPhongShader.cpp
|
src/Core/Shading/BlinnPhongShader.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
if(USE_FRAMEBUFFER)
|
set(CORE_INCLUDE_DIRS
|
||||||
list(APPEND SOURCES src/Gfx/Platform/FBDisplay.cpp)
|
src/Core/Platform
|
||||||
else()
|
src/Core/Asset
|
||||||
list(APPEND SOURCES src/Gfx/Platform/SDLDisplay.cpp)
|
src/Core/Core
|
||||||
endif()
|
src/Core/Draw2D
|
||||||
|
src/Core/Math
|
||||||
add_executable(IMX6U-Game ${SOURCES})
|
src/Core/Rasterizer
|
||||||
|
src/Core/RenderData
|
||||||
target_include_directories(IMX6U-Game PRIVATE
|
src/Core/Scene
|
||||||
src/Gfx/Platform
|
src/Core/Shading
|
||||||
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/font
|
||||||
assets/sprite
|
assets/sprite
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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)
|
if(USE_FRAMEBUFFER)
|
||||||
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)
|
target_compile_definitions(IMX6U-Game PRIVATE USE_FRAMEBUFFER)
|
||||||
else()
|
else()
|
||||||
target_include_directories(IMX6U-Game PRIVATE
|
|
||||||
libs/Win/SDL2/include
|
|
||||||
libs/Win/SDL_image/include
|
|
||||||
)
|
|
||||||
|
|
||||||
if(WIN32)
|
if(WIN32)
|
||||||
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
|
||||||
set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64")
|
set(SDL2_LIB_DIR "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL2/lib/x64")
|
||||||
|
|
@ -66,21 +67,18 @@ else()
|
||||||
set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll")
|
set(SDL2_IMAGE_DLL "${CMAKE_CURRENT_SOURCE_DIR}/libs/Win/SDL_image/lib/x86/SDL2_image.dll")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
|
target_include_directories(IMX6U-Game PRIVATE libs/Win/SDL2/include)
|
||||||
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2 SDL2_image)
|
target_link_directories(IMX6U-Game PRIVATE ${SDL2_LIB_DIR})
|
||||||
|
target_link_libraries(IMX6U-Game PRIVATE SDL2main SDL2)
|
||||||
|
|
||||||
add_custom_command(TARGET IMX6U-Game POST_BUILD
|
add_custom_command(TARGET IMX6U-Game POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
"${SDL2_DLL}"
|
"${SDL2_DLL}"
|
||||||
"$<TARGET_FILE_DIR:IMX6U-Game>"
|
"$<TARGET_FILE_DIR:IMX6U-Game>"
|
||||||
)
|
)
|
||||||
add_custom_command(TARGET IMX6U-Game POST_BUILD
|
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
|
||||||
"${SDL2_IMAGE_DLL}"
|
|
||||||
"$<TARGET_FILE_DIR:IMX6U-Game>"
|
|
||||||
)
|
|
||||||
else()
|
else()
|
||||||
find_package(SDL2 REQUIRED)
|
find_package(SDL2 REQUIRED)
|
||||||
|
find_package(SDL2_image QUIET)
|
||||||
target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2)
|
target_link_libraries(IMX6U-Game PRIVATE SDL2::SDL2)
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
@ -89,3 +87,56 @@ if(MSVC)
|
||||||
target_compile_options(IMX6U-Game PRIVATE /utf-8 /W3)
|
target_compile_options(IMX6U-Game PRIVATE /utf-8 /W3)
|
||||||
set_property(TARGET IMX6U-Game PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
|
set_property(TARGET IMX6U-Game PROPERTY INTERPROCEDURAL_OPTIMIZATION_RELEASE TRUE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
if(NOT USE_FRAMEBUFFER)
|
||||||
|
set(SPRITE_ASSET_TOOL_SOURCES
|
||||||
|
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/Core/Asset
|
||||||
|
src/Core/RenderData
|
||||||
|
libs/Win/SDL2/include
|
||||||
|
libs/Win/SDL_image/include
|
||||||
|
)
|
||||||
|
target_link_directories(SpriteAssetTool PRIVATE ${SDL2_LIB_DIR} ${SDL2_IMAGE_LIB_DIR})
|
||||||
|
target_link_libraries(SpriteAssetTool PRIVATE SDL2main SDL2 SDL2_image)
|
||||||
|
|
||||||
|
add_custom_command(TARGET SpriteAssetTool POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${SDL2_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:SpriteAssetTool>"
|
||||||
|
)
|
||||||
|
add_custom_command(TARGET SpriteAssetTool POST_BUILD
|
||||||
|
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||||
|
"${SDL2_IMAGE_DLL}"
|
||||||
|
"$<TARGET_FILE_DIR:SpriteAssetTool>"
|
||||||
|
)
|
||||||
|
elseif(SDL2_image_FOUND)
|
||||||
|
add_executable(SpriteAssetTool ${SPRITE_ASSET_TOOL_SOURCES})
|
||||||
|
target_include_directories(SpriteAssetTool PRIVATE
|
||||||
|
src/Core/Asset
|
||||||
|
src/Core/RenderData
|
||||||
|
)
|
||||||
|
target_link_libraries(SpriteAssetTool PRIVATE SDL2::SDL2 SDL2_image::SDL2_image)
|
||||||
|
else()
|
||||||
|
message(STATUS "SpriteAssetTool disabled: SDL2_image was not found")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(TARGET SpriteAssetTool)
|
||||||
|
add_custom_target(ConvertTomSprites
|
||||||
|
COMMAND $<TARGET_FILE:SpriteAssetTool>
|
||||||
|
--batch
|
||||||
|
"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
|
||||||
|
)
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
|
||||||
18
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/Launcher`:负责游戏选择、全局设置和启动流程。
|
||||||
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
|
- `Apps/GameA`、`Apps/GameB`:分别维护自己的游戏规则、状态、资源和渲染调用。
|
||||||
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
|
- `Shared`:只放应用层共享但不属于底层图形库的 UI、存档、配置、资源索引等。
|
||||||
- 依赖方向必须保持 `Apps -> Shared -> Gfx -> Platform`,底层图形库不能反向依赖具体游戏。
|
- 依赖方向必须保持 `Apps -> Shared -> Core -> Platform`,底层图形库不能反向依赖具体游戏。
|
||||||
|
|
||||||
初期推荐单进程多应用模式:Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
|
初期推荐单进程多应用模式:Launcher、GameA、GameB 共用同一个 SDL2 初始化、framebuffer 和主循环,通过统一 `IApp` 接口切换当前应用。
|
||||||
|
|
||||||
|
|
@ -196,7 +196,7 @@ assets/font/font_atlas.h
|
||||||
|
|
||||||
## 显示后端架构
|
## 显示后端架构
|
||||||
|
|
||||||
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Gfx` 的平台适配层:
|
显示层通过抽象接口 `Platform::IDisplay` 与渲染逻辑解耦。后续应用层和图形库拆分后,`Platform` 会收敛到 `Core` 的平台适配层:
|
||||||
|
|
||||||
```
|
```
|
||||||
┌──────────────────────────────────────────────┐
|
┌──────────────────────────────────────────────┐
|
||||||
|
|
@ -204,7 +204,7 @@ assets/font/font_atlas.h
|
||||||
├──────────────────────────────────────────────┤
|
├──────────────────────────────────────────────┤
|
||||||
│ Shared:UI、存档、配置、资源索引 │
|
│ Shared:UI、存档、配置、资源索引 │
|
||||||
├──────────────────────────────────────────────┤
|
├──────────────────────────────────────────────┤
|
||||||
│ Gfx:DrawContext、FrameBuffer、Draw2D、Math │
|
│ Core:DrawContext、FrameBuffer、Draw2D、Math │
|
||||||
├──────────────────────────────────────────────┤
|
├──────────────────────────────────────────────┤
|
||||||
│ Platform::IDisplay │
|
│ Platform::IDisplay │
|
||||||
│ - SDLDisplay : SDL2 显示/输入适配 │
|
│ - SDLDisplay : SDL2 显示/输入适配 │
|
||||||
|
|
@ -232,7 +232,7 @@ IMX6U-Game/
|
||||||
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
│ ├─ gen_font_atlas.py # TTF -> bitmap font atlas/header
|
||||||
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
│ └─ png_to_header.py # PNG -> uint32_t RGBA header
|
||||||
├─ src/
|
├─ src/
|
||||||
│ ├─ Gfx/ # 底层图形库:可复用、无具体游戏规则
|
│ ├─ Core/ # 底层图形库:可复用、无具体游戏规则
|
||||||
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
|
│ │ ├─ Draw2D/ # DrawContext 统一绘制入口
|
||||||
│ │ ├─ Core/ # FrameBuffer、DepthBuffer
|
│ │ ├─ Core/ # FrameBuffer、DepthBuffer
|
||||||
│ │ ├─ Math/ # 向量、矩阵、数学工具
|
│ │ ├─ Math/ # 向量、矩阵、数学工具
|
||||||
|
|
@ -247,7 +247,7 @@ IMX6U-Game/
|
||||||
│ └─ test_fb.cpp # 独立 fb 测试(最小示例)
|
│ └─ test_fb.cpp # 独立 fb 测试(最小示例)
|
||||||
├─ docs/
|
├─ docs/
|
||||||
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
|
│ ├─ DEVELOPMENT_GUIDELINES.md # IMX6U 性能红线
|
||||||
│ ├─ APP_AND_GFX_ARCHITECTURE.md # 应用层与图形库分层
|
│ ├─ APP_AND_CORE_ARCHITECTURE.md # 应用层与图形库分层
|
||||||
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
|
│ └─ CONVENTIONS.md # 坐标系、矩阵、深度等数学约定
|
||||||
├─ CMakeLists.txt
|
├─ CMakeLists.txt
|
||||||
└─ README.md
|
└─ README.md
|
||||||
|
|
@ -301,10 +301,8 @@ IMX6U-Game/
|
||||||
- 双平台显示后端(SDL2 / Framebuffer)
|
- 双平台显示后端(SDL2 / Framebuffer)
|
||||||
- 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
|
- 离线资源转换工具:PNG sprite -> C++ 头文件,像素字体 -> bitmap atlas/header
|
||||||
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
|
- 基础 2D sprite、SpriteRegion、bitmap font 文本绘制和 tilemap 视口绘制,当前 demo 显示 FPS 文本、测试 sprite 和小型滚动 tilemap
|
||||||
- 当前板端性能 demo 已临时移除旋转正方体,只保留 2D sprite/tilemap/FPS 与 `Frame/Present` 耗时显示,用于测量 framebuffer 提交瓶颈
|
|
||||||
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
|
- Gfx 目录规范化,代码收敛到 `src/Gfx/`
|
||||||
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
|
- `Gfx::DrawContext` 统一绘制入口,封装现有绘制能力
|
||||||
- `DrawContext::clear_color()` 支持只清颜色缓冲,避免 2D-only demo 每帧无意义清 depth buffer
|
|
||||||
- C++11 兼容代码
|
- C++11 兼容代码
|
||||||
- CMake 跨平台构建
|
- CMake 跨平台构建
|
||||||
|
|
||||||
|
|
@ -312,7 +310,7 @@ IMX6U-Game/
|
||||||
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON)
|
1. FrameBuffer / FBDisplay 性能优化(目标像素格式 backbuffer、dirty rect、专用 tile/sprite 快路径、NEON)
|
||||||
2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环
|
2. 应用层拆分(Launcher / GameA / GameB / Shared)和统一 `IApp` 主循环
|
||||||
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
|
3. SDL2 输入抽象(键盘/触摸/按键状态快照)
|
||||||
4. Gfx 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
|
4. Core 基础 2D 绘制接口(矩形、四边形、继续完善 Sprite/Text/Tilemap 的批处理和专用快路径)
|
||||||
5. 纹理贴图、OBJ 模型加载与完整光照
|
5. 纹理贴图、OBJ 模型加载与完整光照
|
||||||
|
|
||||||
## 说明
|
## 说明
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,89 @@
|
||||||
|
# This is the CMakeCache file.
|
||||||
|
# For build in directory: e:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||||
|
# It was generated by CMake: C:/Program Files/CMake/bin/cmake.exe
|
||||||
|
# You can edit this file to change values found and used by cmake.
|
||||||
|
# If you do not want to change any of the values, simply exit the editor.
|
||||||
|
# If you do want to change a value, simply edit, save, and exit the editor.
|
||||||
|
# The syntax for the file is as follows:
|
||||||
|
# KEY:TYPE=VALUE
|
||||||
|
# KEY is the name of a variable in the cache.
|
||||||
|
# TYPE is a hint to GUIs for the type of VALUE, DO NOT EDIT TYPE!.
|
||||||
|
# VALUE is the current value for the KEY.
|
||||||
|
|
||||||
|
########################
|
||||||
|
# EXTERNAL cache entries
|
||||||
|
########################
|
||||||
|
|
||||||
|
//Semicolon separated list of supported configuration types, only
|
||||||
|
// supports Debug, Release, MinSizeRel, and RelWithDebInfo, anything
|
||||||
|
// else will be ignored.
|
||||||
|
CMAKE_CONFIGURATION_TYPES:STRING=Debug;Release;MinSizeRel;RelWithDebInfo
|
||||||
|
|
||||||
|
//Value Computed by CMake.
|
||||||
|
CMAKE_FIND_PACKAGE_REDIRECTS_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check/CMakeFiles/pkgRedirects
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
CMAKE_PROJECT_COMPAT_VERSION:STATIC=
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
CMAKE_PROJECT_DESCRIPTION:STATIC=
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
CMAKE_PROJECT_HOMEPAGE_URL:STATIC=
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
CMAKE_PROJECT_NAME:STATIC=IMX6U-Game
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
CMAKE_PROJECT_SPDX_LICENSE:STATIC=
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
IMX6U-Game_BINARY_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
IMX6U-Game_IS_TOP_LEVEL:STATIC=ON
|
||||||
|
|
||||||
|
//Value Computed by CMake
|
||||||
|
IMX6U-Game_SOURCE_DIR:STATIC=E:/嵌入式实验/实训/IMX6U-Game
|
||||||
|
|
||||||
|
|
||||||
|
########################
|
||||||
|
# INTERNAL cache entries
|
||||||
|
########################
|
||||||
|
|
||||||
|
//This is the directory where this CMakeCache.txt was created
|
||||||
|
CMAKE_CACHEFILE_DIR:INTERNAL=e:/嵌入式实验/实训/IMX6U-Game/build-check
|
||||||
|
//Major version of cmake used to create the current loaded cache
|
||||||
|
CMAKE_CACHE_MAJOR_VERSION:INTERNAL=4
|
||||||
|
//Minor version of cmake used to create the current loaded cache
|
||||||
|
CMAKE_CACHE_MINOR_VERSION:INTERNAL=3
|
||||||
|
//Patch version of cmake used to create the current loaded cache
|
||||||
|
CMAKE_CACHE_PATCH_VERSION:INTERNAL=3
|
||||||
|
//Path to CMake executable.
|
||||||
|
CMAKE_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cmake.exe
|
||||||
|
//Path to cpack program executable.
|
||||||
|
CMAKE_CPACK_COMMAND:INTERNAL=C:/Program Files/CMake/bin/cpack.exe
|
||||||
|
//Path to ctest program executable.
|
||||||
|
CMAKE_CTEST_COMMAND:INTERNAL=C:/Program Files/CMake/bin/ctest.exe
|
||||||
|
//Name of external makefile project generator.
|
||||||
|
CMAKE_EXTRA_GENERATOR:INTERNAL=
|
||||||
|
//Name of generator.
|
||||||
|
CMAKE_GENERATOR:INTERNAL=Visual Studio 17 2022
|
||||||
|
//Generator instance identifier.
|
||||||
|
CMAKE_GENERATOR_INSTANCE:INTERNAL=C:/Program Files/Microsoft Visual Studio/2022/Community
|
||||||
|
//Name of generator platform.
|
||||||
|
CMAKE_GENERATOR_PLATFORM:INTERNAL=
|
||||||
|
//Name of generator toolset.
|
||||||
|
CMAKE_GENERATOR_TOOLSET:INTERNAL=
|
||||||
|
//Source directory with the top level CMakeLists.txt file for this
|
||||||
|
// project
|
||||||
|
CMAKE_HOME_DIRECTORY:INTERNAL=E:/嵌入式实验/实训/IMX6U-Game
|
||||||
|
//Name of CMakeLists files to read
|
||||||
|
CMAKE_LIST_FILE_NAME:INTERNAL=CMakeLists.txt
|
||||||
|
//number of local generators
|
||||||
|
CMAKE_NUMBER_OF_MAKEFILES:INTERNAL=1
|
||||||
|
//Platform information initialized
|
||||||
|
CMAKE_PLATFORM_INFO_INITIALIZED:INTERNAL=1
|
||||||
|
//Path to CMake installation.
|
||||||
|
CMAKE_ROOT:INTERNAL=C:/Program Files/CMake/share/cmake-4.3
|
||||||
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
# 应用层与 Core 分层设计
|
||||||
|
|
||||||
|
本文记录当前项目的代码分层。`src/Core` 是可复用底层库,`src/Apps` 放具体应用和游戏。
|
||||||
|
|
||||||
|
## 目录边界
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/
|
||||||
|
Core/ # 底层库:渲染、数学、资源、平台适配
|
||||||
|
Asset/ # 离线资源格式加载
|
||||||
|
Core/ # FrameBuffer、DepthBuffer、Renderer、Timer
|
||||||
|
Draw2D/ # Core::DrawContext
|
||||||
|
Math/ # Vector、Matrix、MathUtil
|
||||||
|
Platform/ # 显示、时间、音频、按键等平台接口和后端
|
||||||
|
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 后端
|
||||||
|
```
|
||||||
|
|
||||||
|
音频输入通过 `Platform::IAudioInput` 抽象:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform::IAudioInput
|
||||||
|
SdlAudioInput # PC / SDL2 麦克风后端
|
||||||
|
AlsaAudioInput # Linux ALSA 录音后端
|
||||||
|
```
|
||||||
|
|
||||||
|
音频输出通过 `Platform::IAudioOutput` 抽象:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform::IAudioOutput
|
||||||
|
SdlAudioOutput # PC / SDL2 扬声器后端
|
||||||
|
AlsaAudioOutput # Linux ALSA 播放后端
|
||||||
|
```
|
||||||
|
|
||||||
|
按键输入通过 `Platform::IButtonInput` 抽象:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Platform::IButtonInput
|
||||||
|
SdlKeyboardButtonInput # PC / SDL2 键盘后端,默认空格键
|
||||||
|
EvdevButtonInput # Linux evdev 按键后端
|
||||||
|
```
|
||||||
|
|
||||||
|
游戏代码只能依赖这些 `I*` 接口。ALSA、evdev、SDL2、`/dev/fb0` 等平台细节只能出现在 `src/Core/Platform` 或明确的平台适配代码中。
|
||||||
|
如果只需要当前构建平台的默认后端,可以使用 `Platform::DefaultAudioInput`、`Platform::DefaultAudioOutput` 和 `Platform::DefaultButtonInput`。
|
||||||
|
|
||||||
|
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` 这个重复目录名。
|
||||||
|
|
@ -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. 通用约定
|
## 1. 通用约定
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
|
- **先按 IMX6U 运行时成本设计,再按 PC 调试便利包装。** Windows/Linux SDL 版本只是验证与调试入口,不代表最终性能预算。
|
||||||
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
|
- **热路径默认禁止隐式高成本操作。** 每帧、每对象、每顶点、每像素级代码必须避免动态分配、浮点、虚函数链、复杂 STL 算法和异常控制流。
|
||||||
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
|
- **核心逻辑保持 C++11 兼容。** 不引入需要新工具链或重型运行时支持的语言/库特性。
|
||||||
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/RenderData` 中是否已有可复用能力。
|
- **优先复用已有类型与缓冲。** 新增抽象前先确认 `src/Core/Core`、`src/Core/Math`、`src/Core/RenderData` 中是否已有可复用能力。
|
||||||
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
|
- **性能相关例外必须写明边界。** 如果必须违反本文红线,需要在代码附近注释说明原因、调用频率、数据规模和替代方案。
|
||||||
|
|
||||||
## 2. 数值计算规范
|
## 2. 数值计算规范
|
||||||
|
|
@ -164,7 +164,7 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
|
||||||
|
|
||||||
后续如果继续推进性能优化,优先建立这些基础设施:
|
后续如果继续推进性能优化,优先建立这些基础设施:
|
||||||
|
|
||||||
1. 统一定点数类型与转换工具,集中放在 `src/Gfx/Math/`。
|
1. 统一定点数类型与转换工具,集中放在 `src/Core/Math/`。
|
||||||
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
|
2. 帧级临时缓冲/工作区,集中管理可复用数组和 scratch memory。
|
||||||
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
|
3. 渲染数据的运行时紧凑格式,区分“加载期模型数据”和“运行时渲染数据”。
|
||||||
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
|
4. ARM release 配置下的性能开关,关闭日志、调试绘制和昂贵检查。
|
||||||
|
|
@ -184,8 +184,8 @@ Framebuffer 后端是 IMX6U 上最容易误判性能的路径。`FBDisplay::pres
|
||||||
|
|
||||||
### 12.1 SDL2 边界
|
### 12.1 SDL2 边界
|
||||||
|
|
||||||
- SDL2 类型和调用只允许出现在 `src/Gfx/Platform/` 以及明确的平台适配层中。
|
- SDL2 类型和调用只允许出现在 `src/Core/Platform/` 以及明确的平台适配层中。
|
||||||
- `src/Gfx/Core`、`src/Gfx/Math`、`src/Gfx/Rasterizer`、`src/Gfx/RenderData`、`src/Gfx/Scene`、`src/Gfx/Draw2D` 不应直接包含 `SDL.h`。
|
- `src/Core/Core`、`src/Core/Math`、`src/Core/Rasterizer`、`src/Core/RenderData`、`src/Core/Scene`、`src/Core/Draw2D` 不应直接包含 `SDL.h`。
|
||||||
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
|
- 游戏逻辑不直接处理 `SDL_Event`,应转换为项目自己的输入状态结构。
|
||||||
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep,不直接依赖 float 秒数。
|
- 时间源不挂在 `IDisplay` 上;核心逻辑从 `Platform::ITimeSource` 读取单调整数毫秒,并使用 `Core::Timer` 生成整数 tick / fixed timestep,不直接依赖 float 秒数。
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,22 @@
|
||||||
#include <iostream>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdio>
|
#include <cstdlib>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <iostream>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include "Color.h"
|
#include "Color.h"
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include "Timer.h"
|
#include "Timer.h"
|
||||||
#include "DrawContext.h"
|
#include "DrawContext.h"
|
||||||
#include "test_sprite.h"
|
#include "Image.h"
|
||||||
#include "font_atlas.h"
|
|
||||||
|
|
||||||
#include "Display.h"
|
|
||||||
#include "TimeSource.h"
|
#include "TimeSource.h"
|
||||||
|
#include "Timer.h"
|
||||||
|
#include "font_atlas.h"
|
||||||
|
#include "test_sprite.h"
|
||||||
|
|
||||||
#ifdef USE_FRAMEBUFFER
|
#ifdef USE_FRAMEBUFFER
|
||||||
#include "FBDisplay.h"
|
#include "FBDisplay.h"
|
||||||
#else
|
#else
|
||||||
|
|
@ -42,6 +45,22 @@ struct ProgramOptions
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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[])
|
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
ProgramOptions options;
|
ProgramOptions options;
|
||||||
|
|
@ -98,6 +117,7 @@ static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::IT
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
|
|
@ -107,23 +127,17 @@ int main(int argc, char *argv[])
|
||||||
PrintUsage(argv[0]);
|
PrintUsage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
Core::Timer timer(options.target_fps);
|
|
||||||
Platform::SteadyTimeSource time_source;
|
|
||||||
|
|
||||||
#ifdef USE_FRAMEBUFFER
|
Platform::IDisplay* display = CreateDisplay();
|
||||||
Platform::IDisplay *display = new Platform::FBDisplay();
|
if (!display->init(DemoWidth, DemoHeight))
|
||||||
#else
|
|
||||||
Platform::IDisplay *display = new Platform::SDLDisplay();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (!display->init(width, height))
|
|
||||||
{
|
{
|
||||||
delete display;
|
delete display;
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
Gfx::DrawContext ctx(width, height);
|
Core::DrawContext ctx(DemoWidth, DemoHeight);
|
||||||
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
|
Core::Timer timer(options.target_fps);
|
||||||
|
Platform::SteadyTimeSource time_source;
|
||||||
|
|
||||||
RenderData::BitmapFont font;
|
RenderData::BitmapFont font;
|
||||||
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
|
||||||
|
|
@ -149,6 +163,7 @@ int main(int argc, char *argv[])
|
||||||
int32_t fps = 0;
|
int32_t fps = 0;
|
||||||
int32_t frame_count = 0;
|
int32_t frame_count = 0;
|
||||||
uint32_t last_fps_time = 0;
|
uint32_t last_fps_time = 0;
|
||||||
|
uint32_t animation_time_ms = 0;
|
||||||
char fps_text[32];
|
char fps_text[32];
|
||||||
char perf_text[64];
|
char perf_text[64];
|
||||||
uint32_t last_frame_ms = 0;
|
uint32_t last_frame_ms = 0;
|
||||||
|
|
@ -173,12 +188,13 @@ int main(int argc, char *argv[])
|
||||||
// FPS 计数
|
// FPS 计数
|
||||||
++frame_count;
|
++frame_count;
|
||||||
const uint32_t now = time_source.get_time_ms();
|
const uint32_t now = time_source.get_time_ms();
|
||||||
if (now - last_fps_time >= 1000)
|
if (now - last_fps_time >= 1000u)
|
||||||
{
|
{
|
||||||
fps = frame_count;
|
fps = frame_count;
|
||||||
frame_count = 0;
|
frame_count = 0;
|
||||||
last_fps_time = now;
|
last_fps_time = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
|
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, fpsColor, fpsBg, fps_text);
|
||||||
std::snprintf(perf_text, sizeof(perf_text), "Frame:%ums Present:%ums", last_frame_ms, last_present_ms);
|
std::snprintf(perf_text, sizeof(perf_text), "Frame:%ums Present:%ums", last_frame_ms, last_present_ms);
|
||||||
|
|
|
||||||
|
|
@ -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 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.7 MiB |
|
After Width: | Height: | Size: 2.2 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 2.5 MiB |
|
After Width: | Height: | Size: 2.6 MiB |
|
After Width: | Height: | Size: 2.4 MiB |
|
After Width: | Height: | Size: 5.5 MiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 89 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
#include "TomGameApp.h"
|
||||||
|
#include "../audio/VoiceEffect.h"
|
||||||
|
#include "AudioInput.h"
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
#include "ButtonInput.h"
|
||||||
|
#include "FrameBuffer.h"
|
||||||
|
#include "SpriteRasterizer.h"
|
||||||
|
#include "Image.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
TomGameApp::TomGameApp(
|
||||||
|
Core::FrameBuffer* frameBuffer,
|
||||||
|
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||||
|
Platform::IAudioInput* audioInput,
|
||||||
|
Platform::IAudioOutput* audioOutput,
|
||||||
|
Platform::IButtonInput* buttonInput) :
|
||||||
|
frameBuffer(frameBuffer),
|
||||||
|
spriteRasterizer(spriteRasterizer),
|
||||||
|
audioInput(audioInput),
|
||||||
|
audioOutput(audioOutput),
|
||||||
|
buttonInput(buttonInput),
|
||||||
|
state(TomGameState::Idle),
|
||||||
|
screenWidth(frameBuffer ? frameBuffer->get_width() : 0),
|
||||||
|
screenHeight(frameBuffer ? frameBuffer->get_height() : 0),
|
||||||
|
tomX(0),
|
||||||
|
tomY(0),
|
||||||
|
buttonX(0),
|
||||||
|
buttonY(0),
|
||||||
|
audioSampleRate(16000),
|
||||||
|
audioChannels(1),
|
||||||
|
pitchFactor(1.45f),
|
||||||
|
outputGain(1.15f)
|
||||||
|
{
|
||||||
|
recorder.configure(audioSampleRate, audioChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::set_assets(const TomGameAssets& assets)
|
||||||
|
{
|
||||||
|
this->assets = assets;
|
||||||
|
|
||||||
|
speakingFrames.clear();
|
||||||
|
if (assets.tomSay1 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay1));
|
||||||
|
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||||
|
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||||
|
if (assets.tomSay4 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay4));
|
||||||
|
if (assets.tomSay3 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay3));
|
||||||
|
if (assets.tomSay2 != nullptr) speakingFrames.push_back(RenderData::Sprite(assets.tomSay2));
|
||||||
|
|
||||||
|
speakingAnimator.set_frames(speakingFrames);
|
||||||
|
speakingAnimator.set_frame_time(0.08f);
|
||||||
|
speakingAnimator.set_loop(true);
|
||||||
|
speakingAnimator.stop();
|
||||||
|
|
||||||
|
calculate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::configure_audio(uint32_t sampleRate, uint32_t channels)
|
||||||
|
{
|
||||||
|
if (sampleRate > 0)
|
||||||
|
{
|
||||||
|
audioSampleRate = sampleRate;
|
||||||
|
}
|
||||||
|
if (channels > 0)
|
||||||
|
{
|
||||||
|
audioChannels = channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
recorder.configure(audioSampleRate, audioChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::update(float deltaTime, bool buttonPressed)
|
||||||
|
{
|
||||||
|
bool trigger = buttonPressed;
|
||||||
|
if (buttonInput != nullptr)
|
||||||
|
{
|
||||||
|
buttonInput->update();
|
||||||
|
trigger = trigger || buttonInput->was_pressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (state)
|
||||||
|
{
|
||||||
|
case TomGameState::Idle:
|
||||||
|
if (trigger)
|
||||||
|
{
|
||||||
|
start_recording();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TomGameState::Recording:
|
||||||
|
if (audioInput != nullptr)
|
||||||
|
{
|
||||||
|
recorder.update(*audioInput, 512);
|
||||||
|
}
|
||||||
|
if (trigger || recorder.is_finished())
|
||||||
|
{
|
||||||
|
finish_recording();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TomGameState::Processing:
|
||||||
|
process_voice();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case TomGameState::Speaking:
|
||||||
|
speakingAnimator.update(deltaTime);
|
||||||
|
if (audioOutput == nullptr)
|
||||||
|
{
|
||||||
|
back_to_idle();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.update(*audioOutput, 1024);
|
||||||
|
if (player.is_finished())
|
||||||
|
{
|
||||||
|
back_to_idle();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::draw()
|
||||||
|
{
|
||||||
|
if (spriteRasterizer == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets.background != nullptr)
|
||||||
|
{
|
||||||
|
spriteRasterizer->DrawImage(*assets.background, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||||
|
if (tomSprite.is_valid())
|
||||||
|
{
|
||||||
|
spriteRasterizer->DrawSprite(tomSprite, tomX, tomY);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assets.button != nullptr)
|
||||||
|
{
|
||||||
|
spriteRasterizer->DrawImage(*assets.button, buttonX, buttonY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomGameApp::is_ready() const
|
||||||
|
{
|
||||||
|
return frameBuffer != nullptr &&
|
||||||
|
spriteRasterizer != nullptr &&
|
||||||
|
assets.background != nullptr &&
|
||||||
|
assets.tomStand != nullptr &&
|
||||||
|
assets.tomListen != nullptr &&
|
||||||
|
assets.button != nullptr &&
|
||||||
|
speakingFrames.size() == 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool TomGameApp::is_button_hit(int32_t x, int32_t y) const
|
||||||
|
{
|
||||||
|
if (assets.button == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return x >= buttonX &&
|
||||||
|
x < buttonX + assets.button->get_width() &&
|
||||||
|
y >= buttonY &&
|
||||||
|
y < buttonY + assets.button->get_height();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::calculate_layout()
|
||||||
|
{
|
||||||
|
screenWidth = frameBuffer ? frameBuffer->get_width() : 0;
|
||||||
|
screenHeight = frameBuffer ? frameBuffer->get_height() : 0;
|
||||||
|
|
||||||
|
if (assets.button != nullptr)
|
||||||
|
{
|
||||||
|
buttonX = (screenWidth - assets.button->get_width()) / 2;
|
||||||
|
buttonY = screenHeight - assets.button->get_height() - 16;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderData::Sprite tomSprite = get_current_tom_sprite();
|
||||||
|
if (tomSprite.is_valid())
|
||||||
|
{
|
||||||
|
tomX = (screenWidth - tomSprite.width) / 2;
|
||||||
|
tomY = screenHeight - tomSprite.height - 72;
|
||||||
|
if (tomY < 0)
|
||||||
|
{
|
||||||
|
tomY = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::start_recording()
|
||||||
|
{
|
||||||
|
if (audioInput != nullptr && !audioInput->is_open())
|
||||||
|
{
|
||||||
|
audioInput->init("default", audioSampleRate, audioChannels);
|
||||||
|
}
|
||||||
|
|
||||||
|
player.stop();
|
||||||
|
recorder.configure(audioSampleRate, audioChannels);
|
||||||
|
recorder.start();
|
||||||
|
state = TomGameState::Recording;
|
||||||
|
calculate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::finish_recording()
|
||||||
|
{
|
||||||
|
if (recorder.is_recording())
|
||||||
|
{
|
||||||
|
recorder.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
state = TomGameState::Processing;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::process_voice()
|
||||||
|
{
|
||||||
|
std::vector<int16_t> samples = recorder.get_samples();
|
||||||
|
samples = VoiceEffect::trim_silence(samples, 0.02f, audioChannels);
|
||||||
|
samples = VoiceEffect::pitch_up(samples, pitchFactor, audioChannels);
|
||||||
|
samples = VoiceEffect::amplify(samples, outputGain);
|
||||||
|
|
||||||
|
start_speaking(samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::start_speaking(const std::vector<int16_t>& samples)
|
||||||
|
{
|
||||||
|
if (samples.empty())
|
||||||
|
{
|
||||||
|
back_to_idle();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
player.set_voice(samples, audioSampleRate, audioChannels);
|
||||||
|
player.play();
|
||||||
|
|
||||||
|
speakingAnimator.reset();
|
||||||
|
speakingAnimator.play();
|
||||||
|
state = TomGameState::Speaking;
|
||||||
|
calculate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::back_to_idle()
|
||||||
|
{
|
||||||
|
player.stop();
|
||||||
|
speakingAnimator.stop();
|
||||||
|
state = TomGameState::Idle;
|
||||||
|
calculate_layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TomGameApp::draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y)
|
||||||
|
{
|
||||||
|
if (spriteRasterizer == nullptr || !sprite.is_valid())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
spriteRasterizer->DrawSprite(sprite, centerX - sprite.width / 2, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderData::Sprite TomGameApp::get_current_tom_sprite() const
|
||||||
|
{
|
||||||
|
if (state == TomGameState::Recording && assets.tomListen != nullptr)
|
||||||
|
{
|
||||||
|
return RenderData::Sprite(assets.tomListen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state == TomGameState::Speaking)
|
||||||
|
{
|
||||||
|
const RenderData::Sprite* sprite = speakingAnimator.get_current_sprite();
|
||||||
|
if (sprite != nullptr)
|
||||||
|
{
|
||||||
|
return *sprite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return RenderData::Sprite(assets.tomStand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,120 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "../audio/VoicePlayer.h"
|
||||||
|
#include "../audio/VoiceRecorder.h"
|
||||||
|
#include "../components/SpriteAnimator.h"
|
||||||
|
#include "Sprite.h"
|
||||||
|
|
||||||
|
namespace Core
|
||||||
|
{
|
||||||
|
class FrameBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Rasterizer
|
||||||
|
{
|
||||||
|
class SpriteRasterizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace RenderData
|
||||||
|
{
|
||||||
|
class Image;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IAudioInput;
|
||||||
|
class IAudioOutput;
|
||||||
|
class IButtonInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
enum class TomGameState
|
||||||
|
{
|
||||||
|
Idle,
|
||||||
|
Recording,
|
||||||
|
Processing,
|
||||||
|
Speaking
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TomGameAssets
|
||||||
|
{
|
||||||
|
const RenderData::Image* background;
|
||||||
|
const RenderData::Image* tomStand;
|
||||||
|
const RenderData::Image* tomListen;
|
||||||
|
const RenderData::Image* tomSay1;
|
||||||
|
const RenderData::Image* tomSay2;
|
||||||
|
const RenderData::Image* tomSay3;
|
||||||
|
const RenderData::Image* tomSay4;
|
||||||
|
const RenderData::Image* button;
|
||||||
|
|
||||||
|
TomGameAssets() :
|
||||||
|
background(nullptr),
|
||||||
|
tomStand(nullptr),
|
||||||
|
tomListen(nullptr),
|
||||||
|
tomSay1(nullptr),
|
||||||
|
tomSay2(nullptr),
|
||||||
|
tomSay3(nullptr),
|
||||||
|
tomSay4(nullptr),
|
||||||
|
button(nullptr)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
class TomGameApp
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Core::FrameBuffer* frameBuffer;
|
||||||
|
Rasterizer::SpriteRasterizer* spriteRasterizer;
|
||||||
|
Platform::IAudioInput* audioInput;
|
||||||
|
Platform::IAudioOutput* audioOutput;
|
||||||
|
Platform::IButtonInput* buttonInput;
|
||||||
|
|
||||||
|
TomGameAssets assets;
|
||||||
|
TomGameState state;
|
||||||
|
VoiceRecorder recorder;
|
||||||
|
VoicePlayer player;
|
||||||
|
SpriteAnimator speakingAnimator;
|
||||||
|
std::vector<RenderData::Sprite> speakingFrames;
|
||||||
|
|
||||||
|
int32_t screenWidth;
|
||||||
|
int32_t screenHeight;
|
||||||
|
int32_t tomX;
|
||||||
|
int32_t tomY;
|
||||||
|
int32_t buttonX;
|
||||||
|
int32_t buttonY;
|
||||||
|
|
||||||
|
uint32_t audioSampleRate;
|
||||||
|
uint32_t audioChannels;
|
||||||
|
float pitchFactor;
|
||||||
|
float outputGain;
|
||||||
|
|
||||||
|
void calculate_layout();
|
||||||
|
void start_recording();
|
||||||
|
void finish_recording();
|
||||||
|
void process_voice();
|
||||||
|
void start_speaking(const std::vector<int16_t>& samples);
|
||||||
|
void back_to_idle();
|
||||||
|
void draw_sprite_centered(const RenderData::Sprite& sprite, int32_t centerX, int32_t y);
|
||||||
|
const RenderData::Sprite get_current_tom_sprite() const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
TomGameApp(
|
||||||
|
Core::FrameBuffer* frameBuffer,
|
||||||
|
Rasterizer::SpriteRasterizer* spriteRasterizer,
|
||||||
|
Platform::IAudioInput* audioInput,
|
||||||
|
Platform::IAudioOutput* audioOutput,
|
||||||
|
Platform::IButtonInput* buttonInput = nullptr);
|
||||||
|
|
||||||
|
void set_assets(const TomGameAssets& assets);
|
||||||
|
void configure_audio(uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||||
|
void update(float deltaTime, bool buttonPressed = false);
|
||||||
|
void draw();
|
||||||
|
|
||||||
|
bool is_ready() const;
|
||||||
|
bool is_button_hit(int32_t x, int32_t y) const;
|
||||||
|
TomGameState get_state() const { return state; }
|
||||||
|
float get_record_volume() const { return recorder.get_last_volume(); }
|
||||||
|
float get_play_progress() const { return player.get_progress(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
#include "VoiceEffect.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
std::vector<int16_t> VoiceEffect::pitch_up(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float factor,
|
||||||
|
uint32_t channels)
|
||||||
|
{
|
||||||
|
return change_speed(samples, factor, channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> VoiceEffect::change_speed(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float speed,
|
||||||
|
uint32_t channels)
|
||||||
|
{
|
||||||
|
if (samples.empty() || speed <= 0.0f || channels == 0)
|
||||||
|
{
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t frameCount = samples.size() / channels;
|
||||||
|
if (frameCount == 0)
|
||||||
|
{
|
||||||
|
return std::vector<int16_t>();
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t outputFrameCount = std::max<size_t>(1, static_cast<size_t>(frameCount / speed));
|
||||||
|
std::vector<int16_t> output(outputFrameCount * channels, 0);
|
||||||
|
|
||||||
|
for (size_t outFrame = 0; outFrame < outputFrameCount; ++outFrame)
|
||||||
|
{
|
||||||
|
const float sourceFrame = static_cast<float>(outFrame) * speed;
|
||||||
|
const size_t source0 = std::min(static_cast<size_t>(sourceFrame), frameCount - 1);
|
||||||
|
const size_t source1 = std::min(source0 + 1, frameCount - 1);
|
||||||
|
const float t = sourceFrame - static_cast<float>(source0);
|
||||||
|
|
||||||
|
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||||
|
{
|
||||||
|
const int16_t a = samples[source0 * channels + channel];
|
||||||
|
const int16_t b = samples[source1 * channels + channel];
|
||||||
|
const float mixed = static_cast<float>(a) + (static_cast<float>(b) - static_cast<float>(a)) * t;
|
||||||
|
output[outFrame * channels + channel] = clamp_to_sample(mixed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> VoiceEffect::amplify(const std::vector<int16_t>& samples, float gain)
|
||||||
|
{
|
||||||
|
if (samples.empty() || gain == 1.0f)
|
||||||
|
{
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> output(samples.size(), 0);
|
||||||
|
for (size_t i = 0; i < samples.size(); ++i)
|
||||||
|
{
|
||||||
|
output[i] = clamp_to_sample(static_cast<float>(samples[i]) * gain);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> VoiceEffect::trim_silence(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float threshold,
|
||||||
|
uint32_t channels)
|
||||||
|
{
|
||||||
|
if (samples.empty() || channels == 0)
|
||||||
|
{
|
||||||
|
return samples;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t frameCount = samples.size() / channels;
|
||||||
|
size_t startFrame = 0;
|
||||||
|
size_t endFrame = frameCount;
|
||||||
|
|
||||||
|
while (startFrame < frameCount)
|
||||||
|
{
|
||||||
|
bool loud = false;
|
||||||
|
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||||
|
{
|
||||||
|
if (sample_abs(samples[startFrame * channels + channel]) >= threshold)
|
||||||
|
{
|
||||||
|
loud = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loud)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++startFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (endFrame > startFrame)
|
||||||
|
{
|
||||||
|
bool loud = false;
|
||||||
|
const size_t frame = endFrame - 1;
|
||||||
|
for (uint32_t channel = 0; channel < channels; ++channel)
|
||||||
|
{
|
||||||
|
if (sample_abs(samples[frame * channels + channel]) >= threshold)
|
||||||
|
{
|
||||||
|
loud = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loud)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
--endFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t startSample = startFrame * channels;
|
||||||
|
const size_t endSample = endFrame * channels;
|
||||||
|
return std::vector<int16_t>(samples.begin() + startSample, samples.begin() + endSample);
|
||||||
|
}
|
||||||
|
|
||||||
|
int16_t VoiceEffect::clamp_to_sample(float value)
|
||||||
|
{
|
||||||
|
if (value > 32767.0f)
|
||||||
|
{
|
||||||
|
return 32767;
|
||||||
|
}
|
||||||
|
if (value < -32768.0f)
|
||||||
|
{
|
||||||
|
return -32768;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<int16_t>(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoiceEffect::sample_abs(int16_t sample)
|
||||||
|
{
|
||||||
|
return std::abs(static_cast<float>(sample) / 32768.0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class VoiceEffect
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static std::vector<int16_t> pitch_up(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float factor = 1.45f,
|
||||||
|
uint32_t channels = 1);
|
||||||
|
|
||||||
|
static std::vector<int16_t> change_speed(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float speed,
|
||||||
|
uint32_t channels = 1);
|
||||||
|
|
||||||
|
static std::vector<int16_t> amplify(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float gain);
|
||||||
|
|
||||||
|
static std::vector<int16_t> trim_silence(
|
||||||
|
const std::vector<int16_t>& samples,
|
||||||
|
float threshold = 0.02f,
|
||||||
|
uint32_t channels = 1);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static int16_t clamp_to_sample(float value);
|
||||||
|
static float sample_abs(int16_t sample);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,154 @@
|
||||||
|
#include "VoicePlayer.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
VoicePlayer::VoicePlayer() :
|
||||||
|
playPosition(0),
|
||||||
|
sampleRate(16000),
|
||||||
|
channels(1),
|
||||||
|
playing(false),
|
||||||
|
finished(false)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void VoicePlayer::set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate, uint32_t channels)
|
||||||
|
{
|
||||||
|
this->samples = samples;
|
||||||
|
this->sampleRate = sampleRate > 0 ? sampleRate : 16000;
|
||||||
|
this->channels = channels > 0 ? channels : 1;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoicePlayer::play()
|
||||||
|
{
|
||||||
|
if (samples.empty())
|
||||||
|
{
|
||||||
|
playing = false;
|
||||||
|
finished = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (playPosition >= samples.size())
|
||||||
|
{
|
||||||
|
playPosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
playing = true;
|
||||||
|
finished = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoicePlayer::stop()
|
||||||
|
{
|
||||||
|
playing = false;
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoicePlayer::reset()
|
||||||
|
{
|
||||||
|
playPosition = 0;
|
||||||
|
playing = false;
|
||||||
|
finished = samples.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoicePlayer::clear()
|
||||||
|
{
|
||||||
|
samples.clear();
|
||||||
|
playPosition = 0;
|
||||||
|
playing = false;
|
||||||
|
finished = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int VoicePlayer::update(Platform::IAudioOutput& audioOutput, int maxSamplesPerUpdate)
|
||||||
|
{
|
||||||
|
if (!playing || samples.empty())
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audioOutput.is_open() ||
|
||||||
|
audioOutput.get_sample_rate() != sampleRate ||
|
||||||
|
audioOutput.get_channels() != channels)
|
||||||
|
{
|
||||||
|
if (!audioOutput.init(audioOutput.get_device_name(), sampleRate, channels))
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t remaining = get_remaining_samples();
|
||||||
|
if (remaining == 0)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int requestCount = get_aligned_sample_count(maxSamplesPerUpdate);
|
||||||
|
if (requestCount <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(requestCount), remaining));
|
||||||
|
requestCount = get_aligned_sample_count(requestCount);
|
||||||
|
if (requestCount <= 0)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int written = audioOutput.write_samples(samples.data() + playPosition, requestCount);
|
||||||
|
if (written <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
playPosition += static_cast<size_t>(written);
|
||||||
|
if (playPosition >= samples.size())
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return written;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t VoicePlayer::get_remaining_samples() const
|
||||||
|
{
|
||||||
|
if (playPosition >= samples.size())
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return samples.size() - playPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoicePlayer::get_duration_seconds() const
|
||||||
|
{
|
||||||
|
if (sampleRate == 0 || channels == 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoicePlayer::get_progress() const
|
||||||
|
{
|
||||||
|
if (samples.empty())
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(playPosition) / static_cast<float>(samples.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
int VoicePlayer::get_aligned_sample_count(int sampleCount) const
|
||||||
|
{
|
||||||
|
if (sampleCount <= 0 || channels == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return sampleCount - (sampleCount % static_cast<int>(channels));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class VoicePlayer
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<int16_t> samples;
|
||||||
|
size_t playPosition;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint32_t channels;
|
||||||
|
bool playing;
|
||||||
|
bool finished;
|
||||||
|
|
||||||
|
int get_aligned_sample_count(int sampleCount) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VoicePlayer();
|
||||||
|
|
||||||
|
void set_voice(const std::vector<int16_t>& samples, uint32_t sampleRate = 16000, uint32_t channels = 1);
|
||||||
|
void play();
|
||||||
|
void stop();
|
||||||
|
void reset();
|
||||||
|
void clear();
|
||||||
|
int update(Platform::IAudioOutput& audioOutput, int maxSamplesPerUpdate = 1024);
|
||||||
|
|
||||||
|
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||||
|
size_t get_sample_count() const { return samples.size(); }
|
||||||
|
size_t get_play_position() const { return playPosition; }
|
||||||
|
size_t get_remaining_samples() const;
|
||||||
|
uint32_t get_sample_rate() const { return sampleRate; }
|
||||||
|
uint32_t get_channels() const { return channels; }
|
||||||
|
float get_duration_seconds() const;
|
||||||
|
float get_progress() const;
|
||||||
|
|
||||||
|
bool is_playing() const { return playing; }
|
||||||
|
bool is_finished() const { return finished; }
|
||||||
|
bool has_voice() const { return !samples.empty(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,170 @@
|
||||||
|
#include "VoiceRecorder.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
VoiceRecorder::VoiceRecorder() :
|
||||||
|
sampleRate(16000),
|
||||||
|
channels(1),
|
||||||
|
maxDurationSeconds(5.0f),
|
||||||
|
silenceThreshold(0.03f),
|
||||||
|
silenceDurationSeconds(0.8f),
|
||||||
|
minRecordSeconds(0.3f),
|
||||||
|
lastVolume(0.0f),
|
||||||
|
silentSampleCount(0),
|
||||||
|
recording(false),
|
||||||
|
finished(false),
|
||||||
|
finishedBySilence(false)
|
||||||
|
{}
|
||||||
|
|
||||||
|
void VoiceRecorder::configure(
|
||||||
|
uint32_t sampleRate,
|
||||||
|
uint32_t channels,
|
||||||
|
float maxDurationSeconds,
|
||||||
|
float silenceThreshold,
|
||||||
|
float silenceDurationSeconds,
|
||||||
|
float minRecordSeconds)
|
||||||
|
{
|
||||||
|
if (sampleRate > 0)
|
||||||
|
{
|
||||||
|
this->sampleRate = sampleRate;
|
||||||
|
}
|
||||||
|
if (channels > 0)
|
||||||
|
{
|
||||||
|
this->channels = channels;
|
||||||
|
}
|
||||||
|
if (maxDurationSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
this->maxDurationSeconds = maxDurationSeconds;
|
||||||
|
}
|
||||||
|
if (silenceThreshold >= 0.0f)
|
||||||
|
{
|
||||||
|
this->silenceThreshold = silenceThreshold;
|
||||||
|
}
|
||||||
|
if (silenceDurationSeconds > 0.0f)
|
||||||
|
{
|
||||||
|
this->silenceDurationSeconds = silenceDurationSeconds;
|
||||||
|
}
|
||||||
|
if (minRecordSeconds >= 0.0f)
|
||||||
|
{
|
||||||
|
this->minRecordSeconds = minRecordSeconds;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceRecorder::start()
|
||||||
|
{
|
||||||
|
clear();
|
||||||
|
recording = true;
|
||||||
|
finished = false;
|
||||||
|
finishedBySilence = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceRecorder::stop()
|
||||||
|
{
|
||||||
|
recording = false;
|
||||||
|
finished = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void VoiceRecorder::clear()
|
||||||
|
{
|
||||||
|
samples.clear();
|
||||||
|
lastVolume = 0.0f;
|
||||||
|
silentSampleCount = 0;
|
||||||
|
recording = false;
|
||||||
|
finished = false;
|
||||||
|
finishedBySilence = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int VoiceRecorder::update(Platform::IAudioInput& audioInput, int sampleCount)
|
||||||
|
{
|
||||||
|
if (!recording || sampleCount <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t maxSampleCount = get_max_sample_count();
|
||||||
|
if (samples.size() >= maxSampleCount)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t remaining = maxSampleCount - samples.size();
|
||||||
|
const int requestCount = static_cast<int>(std::min<size_t>(static_cast<size_t>(sampleCount), remaining));
|
||||||
|
std::vector<int16_t> buffer(static_cast<size_t>(requestCount), 0);
|
||||||
|
|
||||||
|
const int readCount = audioInput.read_samples(buffer.data(), requestCount);
|
||||||
|
if (readCount <= 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastVolume = calculate_volume(buffer.data(), readCount);
|
||||||
|
samples.insert(samples.end(), buffer.begin(), buffer.begin() + readCount);
|
||||||
|
|
||||||
|
if (samples.size() >= get_min_sample_count() && lastVolume < silenceThreshold)
|
||||||
|
{
|
||||||
|
silentSampleCount += static_cast<size_t>(readCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
silentSampleCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (silentSampleCount >= get_silence_sample_limit())
|
||||||
|
{
|
||||||
|
finishedBySilence = true;
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
else if (samples.size() >= maxSampleCount)
|
||||||
|
{
|
||||||
|
stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
return readCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoiceRecorder::get_duration_seconds() const
|
||||||
|
{
|
||||||
|
if (sampleRate == 0 || channels == 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(samples.size()) / static_cast<float>(sampleRate * channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t VoiceRecorder::get_max_sample_count() const
|
||||||
|
{
|
||||||
|
return static_cast<size_t>(sampleRate * channels * maxDurationSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t VoiceRecorder::get_silence_sample_limit() const
|
||||||
|
{
|
||||||
|
return static_cast<size_t>(sampleRate * channels * silenceDurationSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t VoiceRecorder::get_min_sample_count() const
|
||||||
|
{
|
||||||
|
return static_cast<size_t>(sampleRate * channels * minRecordSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
float VoiceRecorder::calculate_volume(const int16_t* buffer, int sampleCount) const
|
||||||
|
{
|
||||||
|
if (buffer == nullptr || sampleCount <= 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int i = 0; i < sampleCount; ++i)
|
||||||
|
{
|
||||||
|
const double value = static_cast<double>(buffer[i]) / 32768.0;
|
||||||
|
sum += value * value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(std::sqrt(sum / sampleCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "AudioInput.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class VoiceRecorder
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<int16_t> samples;
|
||||||
|
uint32_t sampleRate;
|
||||||
|
uint32_t channels;
|
||||||
|
float maxDurationSeconds;
|
||||||
|
float silenceThreshold;
|
||||||
|
float silenceDurationSeconds;
|
||||||
|
float minRecordSeconds;
|
||||||
|
float lastVolume;
|
||||||
|
size_t silentSampleCount;
|
||||||
|
bool recording;
|
||||||
|
bool finished;
|
||||||
|
bool finishedBySilence;
|
||||||
|
|
||||||
|
size_t get_max_sample_count() const;
|
||||||
|
size_t get_silence_sample_limit() const;
|
||||||
|
size_t get_min_sample_count() const;
|
||||||
|
float calculate_volume(const int16_t* buffer, int sampleCount) const;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VoiceRecorder();
|
||||||
|
|
||||||
|
void configure(
|
||||||
|
uint32_t sampleRate,
|
||||||
|
uint32_t channels,
|
||||||
|
float maxDurationSeconds = 5.0f,
|
||||||
|
float silenceThreshold = 0.03f,
|
||||||
|
float silenceDurationSeconds = 0.8f,
|
||||||
|
float minRecordSeconds = 0.3f);
|
||||||
|
|
||||||
|
void start();
|
||||||
|
void stop();
|
||||||
|
void clear();
|
||||||
|
int update(Platform::IAudioInput& audioInput, int sampleCount = 512);
|
||||||
|
|
||||||
|
const std::vector<int16_t>& get_samples() const { return samples; }
|
||||||
|
size_t get_sample_count() const { return samples.size(); }
|
||||||
|
uint32_t get_sample_rate() const { return sampleRate; }
|
||||||
|
uint32_t get_channels() const { return channels; }
|
||||||
|
float get_duration_seconds() const;
|
||||||
|
float get_last_volume() const { return lastVolume; }
|
||||||
|
|
||||||
|
bool is_recording() const { return recording; }
|
||||||
|
bool is_finished() const { return finished; }
|
||||||
|
bool is_finished_by_silence() const { return finishedBySilence; }
|
||||||
|
bool has_samples() const { return !samples.empty(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
#include "SpriteAnimator.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
SpriteAnimator::SpriteAnimator() :
|
||||||
|
currentFrame(0),
|
||||||
|
frameTime(0.1f),
|
||||||
|
timer(0.0f),
|
||||||
|
loop(true),
|
||||||
|
playing(true)
|
||||||
|
{}
|
||||||
|
|
||||||
|
SpriteAnimator::SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop) :
|
||||||
|
frames(frames),
|
||||||
|
currentFrame(0),
|
||||||
|
frameTime(frameTime > 0.0f ? frameTime : 0.1f),
|
||||||
|
timer(0.0f),
|
||||||
|
loop(loop),
|
||||||
|
playing(!frames.empty())
|
||||||
|
{}
|
||||||
|
|
||||||
|
void SpriteAnimator::set_frames(const std::vector<RenderData::Sprite>& frames)
|
||||||
|
{
|
||||||
|
this->frames = frames;
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::set_frame_time(float frameTime)
|
||||||
|
{
|
||||||
|
if (frameTime > 0.0f)
|
||||||
|
{
|
||||||
|
this->frameTime = frameTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::set_loop(bool loop)
|
||||||
|
{
|
||||||
|
this->loop = loop;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::play()
|
||||||
|
{
|
||||||
|
playing = !frames.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::stop()
|
||||||
|
{
|
||||||
|
playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::reset()
|
||||||
|
{
|
||||||
|
currentFrame = 0;
|
||||||
|
timer = 0.0f;
|
||||||
|
playing = !frames.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SpriteAnimator::update(float deltaTime)
|
||||||
|
{
|
||||||
|
if (!playing || frames.empty() || frameTime <= 0.0f)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
timer += deltaTime;
|
||||||
|
while (timer >= frameTime)
|
||||||
|
{
|
||||||
|
timer -= frameTime;
|
||||||
|
|
||||||
|
if (currentFrame + 1 < frames.size())
|
||||||
|
{
|
||||||
|
++currentFrame;
|
||||||
|
}
|
||||||
|
else if (loop)
|
||||||
|
{
|
||||||
|
currentFrame = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
playing = false;
|
||||||
|
currentFrame = frames.size() - 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderData::Sprite* SpriteAnimator::get_current_sprite() const
|
||||||
|
{
|
||||||
|
if (frames.empty() || currentFrame >= frames.size())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &frames[currentFrame];
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SpriteAnimator::is_finished() const
|
||||||
|
{
|
||||||
|
return !loop && !playing && !frames.empty() && currentFrame == frames.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <vector>
|
||||||
|
#include "Sprite.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
class SpriteAnimator
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<RenderData::Sprite> frames;
|
||||||
|
size_t currentFrame;
|
||||||
|
float frameTime;
|
||||||
|
float timer;
|
||||||
|
bool loop;
|
||||||
|
bool playing;
|
||||||
|
|
||||||
|
public:
|
||||||
|
SpriteAnimator();
|
||||||
|
SpriteAnimator(const std::vector<RenderData::Sprite>& frames, float frameTime, bool loop = true);
|
||||||
|
|
||||||
|
void set_frames(const std::vector<RenderData::Sprite>& frames);
|
||||||
|
void set_frame_time(float frameTime);
|
||||||
|
void set_loop(bool loop);
|
||||||
|
|
||||||
|
void play();
|
||||||
|
void stop();
|
||||||
|
void reset();
|
||||||
|
void update(float deltaTime);
|
||||||
|
|
||||||
|
const RenderData::Sprite* get_current_sprite() const;
|
||||||
|
size_t get_current_frame() const { return currentFrame; }
|
||||||
|
size_t get_frame_count() const { return frames.size(); }
|
||||||
|
bool is_playing() const { return playing; }
|
||||||
|
bool is_finished() const;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
#include "AnimationSystem.h"
|
||||||
|
#include "SpriteRasterizer.h"
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
size_t AnimationSystem::add(SpriteAnimator* animator, int32_t x, int32_t y)
|
||||||
|
{
|
||||||
|
objects.push_back(AnimationObject(animator, x, y));
|
||||||
|
return objects.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::remove(size_t index)
|
||||||
|
{
|
||||||
|
if (index >= objects.size())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
objects.erase(objects.begin() + index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::clear()
|
||||||
|
{
|
||||||
|
objects.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::update(float deltaTime)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < objects.size(); ++i)
|
||||||
|
{
|
||||||
|
if (objects[i].animator != nullptr)
|
||||||
|
{
|
||||||
|
objects[i].animator->update(deltaTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < objects.size(); ++i)
|
||||||
|
{
|
||||||
|
const AnimationObject& object = objects[i];
|
||||||
|
if (!object.visible || object.animator == nullptr)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const RenderData::Sprite* sprite = object.animator->get_current_sprite();
|
||||||
|
if (sprite != nullptr)
|
||||||
|
{
|
||||||
|
spriteRasterizer.DrawSprite(*sprite, object.x, object.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::set_position(size_t index, int32_t x, int32_t y)
|
||||||
|
{
|
||||||
|
AnimationObject* object = get_object(index);
|
||||||
|
if (object == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object->x = x;
|
||||||
|
object->y = y;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AnimationSystem::set_visible(size_t index, bool visible)
|
||||||
|
{
|
||||||
|
AnimationObject* object = get_object(index);
|
||||||
|
if (object == nullptr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
object->visible = visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimationObject* AnimationSystem::get_object(size_t index)
|
||||||
|
{
|
||||||
|
if (index >= objects.size())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &objects[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const AnimationObject* AnimationSystem::get_object(size_t index) const
|
||||||
|
{
|
||||||
|
if (index >= objects.size())
|
||||||
|
{
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return &objects[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
#pragma once
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <vector>
|
||||||
|
#include "../components/SpriteAnimator.h"
|
||||||
|
|
||||||
|
namespace Rasterizer
|
||||||
|
{
|
||||||
|
class SpriteRasterizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Game
|
||||||
|
{
|
||||||
|
struct AnimationObject
|
||||||
|
{
|
||||||
|
SpriteAnimator* animator;
|
||||||
|
int32_t x;
|
||||||
|
int32_t y;
|
||||||
|
bool visible;
|
||||||
|
|
||||||
|
AnimationObject() : animator(nullptr), x(0), y(0), visible(true) {}
|
||||||
|
AnimationObject(SpriteAnimator* animator, int32_t x, int32_t y) :
|
||||||
|
animator(animator),
|
||||||
|
x(x),
|
||||||
|
y(y),
|
||||||
|
visible(true)
|
||||||
|
{}
|
||||||
|
};
|
||||||
|
|
||||||
|
class AnimationSystem
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::vector<AnimationObject> objects;
|
||||||
|
|
||||||
|
public:
|
||||||
|
size_t add(SpriteAnimator* animator, int32_t x, int32_t y);
|
||||||
|
void remove(size_t index);
|
||||||
|
void clear();
|
||||||
|
|
||||||
|
void update(float deltaTime);
|
||||||
|
void draw(Rasterizer::SpriteRasterizer& spriteRasterizer) const;
|
||||||
|
|
||||||
|
void set_position(size_t index, int32_t x, int32_t y);
|
||||||
|
void set_visible(size_t index, bool visible);
|
||||||
|
|
||||||
|
AnimationObject* get_object(size_t index);
|
||||||
|
const AnimationObject* get_object(size_t index) const;
|
||||||
|
size_t count() const { return objects.size(); }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,234 @@
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "FrameBuffer.h"
|
||||||
|
#include "SDLDisplay.h"
|
||||||
|
#include "SpriteAssetLoader.h"
|
||||||
|
#include "SpriteRasterizer.h"
|
||||||
|
#include "Image.h"
|
||||||
|
#include "Sprite.h"
|
||||||
|
#include "SpriteAnimator.h"
|
||||||
|
#include "AnimationSystem.h"
|
||||||
|
|
||||||
|
static std::string FindAssetPath(const std::string& fileName)
|
||||||
|
{
|
||||||
|
const char* roots[] = {
|
||||||
|
"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)
|
||||||
|
{
|
||||||
|
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("src/Apps/Game/assets/sprites/") + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadSpriteImage(const std::string& fileName, RenderData::Image& image)
|
||||||
|
{
|
||||||
|
const std::string path = FindAssetPath(fileName);
|
||||||
|
if (!Asset::SpriteAssetLoader::Load(path, image))
|
||||||
|
{
|
||||||
|
std::cerr << "Load sprite failed: " << path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RenderData::Image ResizeImageNearest(const RenderData::Image& source, int32_t width, int32_t height)
|
||||||
|
{
|
||||||
|
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
return RenderData::Image();
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderData::Image result(width, height);
|
||||||
|
for (int32_t y = 0; y < height; ++y)
|
||||||
|
{
|
||||||
|
const int32_t sourceY = y * source.get_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
static RenderData::Image ResizeImageToFit(const RenderData::Image& source, int32_t maxWidth, int32_t maxHeight)
|
||||||
|
{
|
||||||
|
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||||
|
{
|
||||||
|
return RenderData::Image();
|
||||||
|
}
|
||||||
|
|
||||||
|
const float scaleX = static_cast<float>(maxWidth) / source.get_width();
|
||||||
|
const float scaleY = static_cast<float>(maxHeight) / source.get_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));
|
||||||
|
return ResizeImageNearest(source, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadFrames(
|
||||||
|
const std::vector<std::string>& fileNames,
|
||||||
|
std::vector<RenderData::Image>& images,
|
||||||
|
std::vector<RenderData::Sprite>& frames,
|
||||||
|
int32_t maxFrameWidth,
|
||||||
|
int32_t maxFrameHeight)
|
||||||
|
{
|
||||||
|
images.clear();
|
||||||
|
frames.clear();
|
||||||
|
images.reserve(fileNames.size());
|
||||||
|
|
||||||
|
for (size_t i = 0; i < fileNames.size(); ++i)
|
||||||
|
{
|
||||||
|
RenderData::Image image;
|
||||||
|
if (!LoadSpriteImage(fileNames[i], image))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
images.push_back(ResizeImageToFit(image, maxFrameWidth, maxFrameHeight));
|
||||||
|
}
|
||||||
|
|
||||||
|
frames.reserve(images.size());
|
||||||
|
for (size_t i = 0; i < images.size(); ++i)
|
||||||
|
{
|
||||||
|
frames.push_back(RenderData::Sprite(&images[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
return !frames.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
(void)argc;
|
||||||
|
(void)argv;
|
||||||
|
|
||||||
|
const int32_t width = 800;
|
||||||
|
const int32_t height = 480;
|
||||||
|
|
||||||
|
Platform::SDLDisplay display;
|
||||||
|
if (!display.init(width, height))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderData::Image originalBackground;
|
||||||
|
if (!LoadSpriteImage("background.sprite", originalBackground))
|
||||||
|
{
|
||||||
|
display.shutdown();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
RenderData::Image background = ResizeImageNearest(originalBackground, width, height);
|
||||||
|
|
||||||
|
std::vector<std::string> frameFiles;
|
||||||
|
frameFiles.push_back("Tom-stand.sprite");
|
||||||
|
frameFiles.push_back("Tom-listhen.sprite");
|
||||||
|
frameFiles.push_back("Tom-openmouse1.sprite");
|
||||||
|
frameFiles.push_back("Tom-openmouse2.sprite");
|
||||||
|
frameFiles.push_back("Tom-say1.sprite");
|
||||||
|
frameFiles.push_back("Tom-say2.sprite");
|
||||||
|
frameFiles.push_back("Tom-say3.sprite");
|
||||||
|
frameFiles.push_back("Tom-say4.sprite");
|
||||||
|
|
||||||
|
std::vector<RenderData::Image> frameImages;
|
||||||
|
std::vector<RenderData::Sprite> frames;
|
||||||
|
if (!LoadFrames(frameFiles, frameImages, frames, width * 7 / 10, height * 9 / 10))
|
||||||
|
{
|
||||||
|
display.shutdown();
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Core::FrameBuffer frameBuffer(width, height);
|
||||||
|
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
|
||||||
|
|
||||||
|
Game::SpriteAnimator tomAnimator(frames, 0.12f, true);
|
||||||
|
Game::AnimationSystem animationSystem;
|
||||||
|
|
||||||
|
const RenderData::Sprite* firstFrame = tomAnimator.get_current_sprite();
|
||||||
|
const int32_t tomX = firstFrame ? (width - firstFrame->width) / 2 : 0;
|
||||||
|
const int32_t tomY = firstFrame ? height - firstFrame->height - 20 : 0;
|
||||||
|
animationSystem.add(&tomAnimator, tomX, tomY);
|
||||||
|
|
||||||
|
std::cout << "Sprite animation test" << std::endl;
|
||||||
|
std::cout << "Space: play/pause, R: reset, Esc: quit" << std::endl;
|
||||||
|
std::cout << "Background: " << originalBackground.get_width() << "x" << originalBackground.get_height()
|
||||||
|
<< " -> " << background.get_width() << "x" << background.get_height() << std::endl;
|
||||||
|
std::cout << "Tom frame 0: " << frameImages[0].get_width() << "x" << frameImages[0].get_height() << std::endl;
|
||||||
|
|
||||||
|
bool shouldQuit = false;
|
||||||
|
uint32_t lastTime = display.get_time_ms();
|
||||||
|
while (!shouldQuit)
|
||||||
|
{
|
||||||
|
SDL_Event event;
|
||||||
|
while (SDL_PollEvent(&event))
|
||||||
|
{
|
||||||
|
if (event.type == SDL_QUIT)
|
||||||
|
{
|
||||||
|
shouldQuit = true;
|
||||||
|
}
|
||||||
|
else if (event.type == SDL_KEYDOWN)
|
||||||
|
{
|
||||||
|
if (event.key.repeat != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.key.keysym.sym == SDLK_ESCAPE)
|
||||||
|
{
|
||||||
|
shouldQuit = true;
|
||||||
|
}
|
||||||
|
else if (event.key.keysym.sym == SDLK_SPACE)
|
||||||
|
{
|
||||||
|
if (tomAnimator.is_playing())
|
||||||
|
{
|
||||||
|
tomAnimator.stop();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tomAnimator.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.key.keysym.sym == SDLK_r)
|
||||||
|
{
|
||||||
|
tomAnimator.reset();
|
||||||
|
tomAnimator.play();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t currentTime = display.get_time_ms();
|
||||||
|
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
|
||||||
|
lastTime = currentTime;
|
||||||
|
|
||||||
|
animationSystem.update(deltaTime);
|
||||||
|
|
||||||
|
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
|
||||||
|
spriteRasterizer.DrawImage(background, 0, 0);
|
||||||
|
animationSystem.draw(spriteRasterizer);
|
||||||
|
display.present(&frameBuffer);
|
||||||
|
|
||||||
|
SDL_Delay(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
display.shutdown();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
|
||||||
|
|
@ -0,0 +1,601 @@
|
||||||
|
#include <SDL.h>
|
||||||
|
#include <SDL_image.h>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cctype>
|
||||||
|
#include <climits>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <iostream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include "Image.h"
|
||||||
|
#include "SpriteAssetLoader.h"
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
#define WIN32_LEAN_AND_MEAN
|
||||||
|
#define NOMINMAX
|
||||||
|
#include <windows.h>
|
||||||
|
#else
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
enum class TransformMode
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
Resize,
|
||||||
|
Fit,
|
||||||
|
Tom800x480Preset
|
||||||
|
};
|
||||||
|
|
||||||
|
struct TransformOptions
|
||||||
|
{
|
||||||
|
TransformMode mode;
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
|
||||||
|
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 == '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string ToLower(std::string text)
|
||||||
|
{
|
||||||
|
std::transform(text.begin(), text.end(), text.begin(), [](char value) {
|
||||||
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(value)));
|
||||||
|
});
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string JoinPath(const std::string& directory, const std::string& fileName)
|
||||||
|
{
|
||||||
|
if (directory.empty())
|
||||||
|
{
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsPathSeparator(directory[directory.size() - 1]))
|
||||||
|
{
|
||||||
|
return directory + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
return directory + "/" + fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string GetFileName(const std::string& path)
|
||||||
|
{
|
||||||
|
const size_t slash = path.find_last_of("/\\");
|
||||||
|
if (slash == std::string::npos)
|
||||||
|
{
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.substr(slash + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string GetParentPath(const std::string& path)
|
||||||
|
{
|
||||||
|
const size_t slash = path.find_last_of("/\\");
|
||||||
|
if (slash == std::string::npos)
|
||||||
|
{
|
||||||
|
return std::string();
|
||||||
|
}
|
||||||
|
if (slash == 0)
|
||||||
|
{
|
||||||
|
return path.substr(0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.substr(0, slash);
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::string ChangeExtensionToSprite(const std::string& fileName)
|
||||||
|
{
|
||||||
|
const size_t slash = fileName.find_last_of("/\\");
|
||||||
|
const size_t dot = fileName.find_last_of('.');
|
||||||
|
if (dot != std::string::npos && (slash == std::string::npos || dot > slash))
|
||||||
|
{
|
||||||
|
return fileName.substr(0, dot) + Asset::SpriteAssetLoader::GetFileExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileName + Asset::SpriteAssetLoader::GetFileExtension();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool HasPngExtension(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string lower = ToLower(path);
|
||||||
|
return lower.size() >= 4 && lower.substr(lower.size() - 4) == ".png";
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParsePositiveInt(const char* text, int32_t& value)
|
||||||
|
{
|
||||||
|
if (text == nullptr || text[0] == '\0')
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
errno = 0;
|
||||||
|
char* end = nullptr;
|
||||||
|
const long parsed = std::strtol(text, &end, 10);
|
||||||
|
if (errno != 0 || end == text || *end != '\0' || parsed <= 0 || parsed > INT_MAX)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = static_cast<int32_t>(parsed);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool DirectoryExists(const std::string& path)
|
||||||
|
{
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const DWORD attrs = GetFileAttributesA(path.c_str());
|
||||||
|
return attrs != INVALID_FILE_ATTRIBUTES && (attrs & FILE_ATTRIBUTE_DIRECTORY) != 0;
|
||||||
|
#else
|
||||||
|
struct stat info;
|
||||||
|
return stat(path.c_str(), &info) == 0 && S_ISDIR(info.st_mode);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool CreateSingleDirectory(const std::string& path)
|
||||||
|
{
|
||||||
|
if (path.empty() || DirectoryExists(path))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
if (CreateDirectoryA(path.c_str(), nullptr) != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return GetLastError() == ERROR_ALREADY_EXISTS && DirectoryExists(path);
|
||||||
|
#else
|
||||||
|
if (mkdir(path.c_str(), 0755) == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return errno == EEXIST && DirectoryExists(path);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EnsureDirectoryTree(const std::string& path)
|
||||||
|
{
|
||||||
|
if (path.empty())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string normalized = path;
|
||||||
|
while (!normalized.empty() && IsPathSeparator(normalized[normalized.size() - 1]))
|
||||||
|
{
|
||||||
|
normalized.erase(normalized.size() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalized.empty())
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t start = 0;
|
||||||
|
if (normalized.size() >= 2 && normalized[1] == ':')
|
||||||
|
{
|
||||||
|
start = 2;
|
||||||
|
}
|
||||||
|
if (start < normalized.size() && IsPathSeparator(normalized[start]))
|
||||||
|
{
|
||||||
|
++start;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = start; i <= normalized.size(); ++i)
|
||||||
|
{
|
||||||
|
if (i != normalized.size() && !IsPathSeparator(normalized[i]))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string part = normalized.substr(0, i);
|
||||||
|
if (part.empty() || (part.size() == 2 && part[1] == ':'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CreateSingleDirectory(part))
|
||||||
|
{
|
||||||
|
std::cerr << "Create directory failed: " << part << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DirectoryExists(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool EnsureParentDirectory(const std::string& path)
|
||||||
|
{
|
||||||
|
return EnsureDirectoryTree(GetParentPath(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ListPngFiles(const std::string& directory, std::vector<std::string>& fileNames)
|
||||||
|
{
|
||||||
|
fileNames.clear();
|
||||||
|
|
||||||
|
#if defined(_WIN32)
|
||||||
|
const std::string searchPath = JoinPath(directory, "*");
|
||||||
|
WIN32_FIND_DATAA data;
|
||||||
|
HANDLE handle = FindFirstFileA(searchPath.c_str(), &data);
|
||||||
|
if (handle == INVALID_HANDLE_VALUE)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
if ((data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0 &&
|
||||||
|
HasPngExtension(data.cFileName))
|
||||||
|
{
|
||||||
|
fileNames.push_back(data.cFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (FindNextFileA(handle, &data) != 0);
|
||||||
|
|
||||||
|
FindClose(handle);
|
||||||
|
#else
|
||||||
|
DIR* dir = opendir(directory.c_str());
|
||||||
|
if (dir == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
dirent* entry = nullptr;
|
||||||
|
while ((entry = readdir(dir)) != nullptr)
|
||||||
|
{
|
||||||
|
const std::string name = entry->d_name;
|
||||||
|
if (name == "." || name == ".." || !HasPngExtension(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
fileNames.push_back(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::sort(fileNames.begin(), fileNames.end(), [](const std::string& left, const std::string& right) {
|
||||||
|
return ToLower(left) < ToLower(right);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool LoadPngImage(const std::string& path, LoadedImage& image)
|
||||||
|
{
|
||||||
|
SDL_Surface* loadedSurface = IMG_Load(path.c_str());
|
||||||
|
if (loadedSurface == nullptr)
|
||||||
|
{
|
||||||
|
std::cerr << "IMG_Load failed: " << path << " : " << IMG_GetError() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_Surface* rgbaSurface = SDL_ConvertSurfaceFormat(loadedSurface, SDL_PIXELFORMAT_RGBA8888, 0);
|
||||||
|
SDL_FreeSurface(loadedSurface);
|
||||||
|
if (rgbaSurface == nullptr)
|
||||||
|
{
|
||||||
|
std::cerr << "SDL_ConvertSurfaceFormat failed: " << SDL_GetError() << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
SDL_FreeSurface(rgbaSurface);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int32_t y = 0; y < rgbaSurface->h; ++y)
|
||||||
|
{
|
||||||
|
const uint8_t* srcRow = static_cast<const uint8_t*>(rgbaSurface->pixels) + y * rgbaSurface->pitch;
|
||||||
|
const uint32_t* srcPixels = reinterpret_cast<const uint32_t*>(srcRow);
|
||||||
|
for (int32_t x = 0; x < rgbaSurface->w; ++x)
|
||||||
|
{
|
||||||
|
pixels[static_cast<size_t>(y) * rgbaSurface->w + x] = srcPixels[x];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SDL_UnlockSurface(rgbaSurface);
|
||||||
|
|
||||||
|
image.assign(rgbaSurface->w, rgbaSurface->h, pixels);
|
||||||
|
SDL_FreeSurface(rgbaSurface);
|
||||||
|
return image.is_valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
static LoadedImage ResizeImageNearest(const LoadedImage& source, int32_t width, int32_t height)
|
||||||
|
{
|
||||||
|
if (!source.is_valid() || width <= 0 || height <= 0)
|
||||||
|
{
|
||||||
|
return LoadedImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.image.height / height;
|
||||||
|
for (int32_t x = 0; x < width; ++x)
|
||||||
|
{
|
||||||
|
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 LoadedImage ResizeImageToFit(const LoadedImage& source, int32_t maxWidth, int32_t maxHeight)
|
||||||
|
{
|
||||||
|
if (!source.is_valid() || maxWidth <= 0 || maxHeight <= 0)
|
||||||
|
{
|
||||||
|
return LoadedImage();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.image.width * scale));
|
||||||
|
const int32_t height = std::max(1, static_cast<int32_t>(source.image.height * scale));
|
||||||
|
return ResizeImageNearest(source, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
static TransformOptions ResolveTransformForInput(
|
||||||
|
const std::string& inputPath,
|
||||||
|
const TransformOptions& requested)
|
||||||
|
{
|
||||||
|
if (requested.mode != TransformMode::Tom800x480Preset)
|
||||||
|
{
|
||||||
|
return requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformOptions resolved;
|
||||||
|
const std::string fileName = ToLower(GetFileName(inputPath));
|
||||||
|
if (fileName == "background.png")
|
||||||
|
{
|
||||||
|
resolved.mode = TransformMode::Resize;
|
||||||
|
resolved.width = 800;
|
||||||
|
resolved.height = 480;
|
||||||
|
}
|
||||||
|
else if (fileName.find("tom-") == 0)
|
||||||
|
{
|
||||||
|
resolved.mode = TransformMode::Fit;
|
||||||
|
resolved.width = 560;
|
||||||
|
resolved.height = 360;
|
||||||
|
}
|
||||||
|
else if (fileName.find("ui-") == 0)
|
||||||
|
{
|
||||||
|
resolved.mode = TransformMode::Fit;
|
||||||
|
resolved.width = 72;
|
||||||
|
resolved.height = 72;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ApplyTransform(const std::string& inputPath, const TransformOptions& options, LoadedImage& image)
|
||||||
|
{
|
||||||
|
const TransformOptions resolved = ResolveTransformForInput(inputPath, options);
|
||||||
|
switch (resolved.mode)
|
||||||
|
{
|
||||||
|
case TransformMode::None:
|
||||||
|
return image.is_valid();
|
||||||
|
|
||||||
|
case TransformMode::Resize:
|
||||||
|
image = ResizeImageNearest(image, resolved.width, resolved.height);
|
||||||
|
return image.is_valid();
|
||||||
|
|
||||||
|
case TransformMode::Fit:
|
||||||
|
image = ResizeImageToFit(image, resolved.width, resolved.height);
|
||||||
|
return image.is_valid();
|
||||||
|
|
||||||
|
case TransformMode::Tom800x480Preset:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ConvertFile(
|
||||||
|
const std::string& inputPath,
|
||||||
|
const std::string& outputPath,
|
||||||
|
const TransformOptions& transformOptions)
|
||||||
|
{
|
||||||
|
LoadedImage image;
|
||||||
|
if (!LoadPngImage(inputPath, image))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ApplyTransform(inputPath, transformOptions, image))
|
||||||
|
{
|
||||||
|
std::cerr << "Image transform failed: " << inputPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureParentDirectory(outputPath))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Asset::SpriteAssetLoader::Save(outputPath, image.image))
|
||||||
|
{
|
||||||
|
std::cerr << "Save failed: " << outputPath << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << inputPath << " -> " << outputPath
|
||||||
|
<< " (" << image.image.width << "x" << image.image.height << ")" << std::endl;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ConvertBatch(
|
||||||
|
const std::string& inputDirectory,
|
||||||
|
const std::string& outputDirectory,
|
||||||
|
const TransformOptions& transformOptions)
|
||||||
|
{
|
||||||
|
std::vector<std::string> files;
|
||||||
|
if (!ListPngFiles(inputDirectory, files) || files.empty())
|
||||||
|
{
|
||||||
|
std::cerr << "No PNG files found: " << inputDirectory << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureDirectoryTree(outputDirectory))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t successCount = 0;
|
||||||
|
for (size_t i = 0; i < files.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string inputPath = JoinPath(inputDirectory, files[i]);
|
||||||
|
const std::string outputPath = JoinPath(outputDirectory, ChangeExtensionToSprite(files[i]));
|
||||||
|
if (ConvertFile(inputPath, outputPath, transformOptions))
|
||||||
|
{
|
||||||
|
++successCount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "Converted " << successCount << " / " << files.size() << " PNG files." << std::endl;
|
||||||
|
return successCount == files.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool ParseTransformOptions(int argc, char* argv[], int startIndex, TransformOptions& options)
|
||||||
|
{
|
||||||
|
if (startIndex >= argc)
|
||||||
|
{
|
||||||
|
options = TransformOptions();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string mode = argv[startIndex];
|
||||||
|
if (mode == "--resize" || mode == "--fit")
|
||||||
|
{
|
||||||
|
if (startIndex + 2 >= argc)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t width = 0;
|
||||||
|
int32_t height = 0;
|
||||||
|
if (!ParsePositiveInt(argv[startIndex + 1], width) ||
|
||||||
|
!ParsePositiveInt(argv[startIndex + 2], height) ||
|
||||||
|
startIndex + 3 != argc)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.mode = mode == "--resize" ? TransformMode::Resize : TransformMode::Fit;
|
||||||
|
options.width = width;
|
||||||
|
options.height = height;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "--preset")
|
||||||
|
{
|
||||||
|
if (startIndex + 2 != argc)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string presetName = argv[startIndex + 1];
|
||||||
|
if (presetName != "tom-800x480")
|
||||||
|
{
|
||||||
|
std::cerr << "Unknown preset: " << presetName << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.mode = TransformMode::Tom800x480Preset;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void PrintUsage()
|
||||||
|
{
|
||||||
|
std::cout << "Usage:" << std::endl;
|
||||||
|
std::cout << " SpriteAssetTool input.png output.sprite [--resize width height]" << std::endl;
|
||||||
|
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 src/Apps/Game/assets/raw src/Apps/Game/assets/sprites --preset tom-800x480" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[])
|
||||||
|
{
|
||||||
|
if (argc < 3)
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int imageFlags = IMG_INIT_PNG;
|
||||||
|
if ((IMG_Init(imageFlags) & imageFlags) != imageFlags)
|
||||||
|
{
|
||||||
|
std::cerr << "IMG_Init failed: " << IMG_GetError() << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TransformOptions transformOptions;
|
||||||
|
bool ok = false;
|
||||||
|
const std::string command = argv[1];
|
||||||
|
if (command == "--batch")
|
||||||
|
{
|
||||||
|
if (argc < 4 || !ParseTransformOptions(argc, argv, 4, transformOptions))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
IMG_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = ConvertBatch(argv[2], argv[3], transformOptions);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ParseTransformOptions(argc, argv, 3, transformOptions))
|
||||||
|
{
|
||||||
|
PrintUsage();
|
||||||
|
IMG_Quit();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = ConvertFile(argv[1], argv[2], transformOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
IMG_Quit();
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
#include "SpriteAssetLoader.h"
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <limits>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void WritePixels(std::ofstream& file, const RenderData::Image& image)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < image.total_pixels(); ++i)
|
||||||
|
{
|
||||||
|
WriteU32LE(file, image.data()[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Asset
|
||||||
|
{
|
||||||
|
bool SpriteAssetLoader::Load(const std::string& path, 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> pixels(pixelCount, 0);
|
||||||
|
if (!ReadPixels(file, pixels))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
char trailingByte = 0;
|
||||||
|
file.read(&trailingByte, 1);
|
||||||
|
if (file.gcount() != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
image = RenderData::Image(
|
||||||
|
static_cast<int32_t>(width),
|
||||||
|
static_cast<int32_t>(height),
|
||||||
|
pixels);
|
||||||
|
return image.is_valid();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
|
||||||
|
{
|
||||||
|
if (!image.is_valid())
|
||||||
|
{
|
||||||
|
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.get_width()));
|
||||||
|
WriteU32LE(file, static_cast<uint32_t>(image.get_height()));
|
||||||
|
WriteU32LE(file, SpriteFormatRgba8888);
|
||||||
|
WritePixels(file, image);
|
||||||
|
return file.good();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
#pragma once
|
||||||
|
#include <string>
|
||||||
|
#include "Image.h"
|
||||||
|
|
||||||
|
namespace Asset
|
||||||
|
{
|
||||||
|
class SpriteAssetLoader
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
static const char* GetFileExtension() { return ".sprite"; }
|
||||||
|
static bool Load(const std::string& path, RenderData::Image& image);
|
||||||
|
static bool Save(const std::string& path, const RenderData::Image& image);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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 "Display.h"
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
namespace Gfx
|
namespace Core
|
||||||
{
|
{
|
||||||
DrawContext::DrawContext(int32_t width, int32_t height)
|
DrawContext::DrawContext(int32_t width, int32_t height)
|
||||||
{
|
{
|
||||||
|
|
@ -25,7 +25,7 @@ namespace Platform
|
||||||
class IDisplay;
|
class IDisplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Gfx
|
namespace Core
|
||||||
{
|
{
|
||||||
class DrawContext
|
class DrawContext
|
||||||
{
|
{
|
||||||
|
|
@ -0,0 +1,141 @@
|
||||||
|
#include "AlsaAudioInput.h"
|
||||||
|
#include <cmath>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
AlsaAudioInput::AlsaAudioInput()
|
||||||
|
: device_name_("default"),
|
||||||
|
sample_rate_(16000),
|
||||||
|
channels_(1),
|
||||||
|
opened_(false)
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
,
|
||||||
|
handle_(nullptr)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AlsaAudioInput::~AlsaAudioInput()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlsaAudioInput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
if (sample_rate == 0 || channels == 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioInput invalid params." << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
device_name_ = device_name;
|
||||||
|
sample_rate_ = sample_rate;
|
||||||
|
channels_ = channels;
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_CAPTURE, 0);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioInput open failed: " << snd_strerror(result) << std::endl;
|
||||||
|
handle_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = snd_pcm_set_params(
|
||||||
|
handle_,
|
||||||
|
SND_PCM_FORMAT_S16_LE,
|
||||||
|
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||||
|
channels_,
|
||||||
|
sample_rate_,
|
||||||
|
1,
|
||||||
|
50000);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioInput set params failed: " << snd_strerror(result) << std::endl;
|
||||||
|
shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opened_ = true;
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
std::cerr << "AlsaAudioInput backend is unavailable. Enable ALSA development libraries." << std::endl;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int AlsaAudioInput::read_samples(int16_t* buffer, int sample_count)
|
||||||
|
{
|
||||||
|
if (buffer == nullptr || sample_count <= 0 || !opened_)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / channels_);
|
||||||
|
int result = snd_pcm_readi(handle_, buffer, frames);
|
||||||
|
if (result == -EPIPE)
|
||||||
|
{
|
||||||
|
snd_pcm_prepare(handle_);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
result = snd_pcm_recover(handle_, result, 0);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioInput read failed: " << snd_strerror(result) << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result * static_cast<int>(channels_);
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
float AlsaAudioInput::read_volume(int sample_count)
|
||||||
|
{
|
||||||
|
if (sample_count <= 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> samples(static_cast<size_t>(sample_count), 0);
|
||||||
|
const int count = read_samples(samples.data(), sample_count);
|
||||||
|
if (count <= 0)
|
||||||
|
{
|
||||||
|
return 0.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
double sum = 0.0;
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
{
|
||||||
|
const double value = static_cast<double>(samples[i]) / 32768.0;
|
||||||
|
sum += value * value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return static_cast<float>(std::sqrt(sum / count));
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlsaAudioInput::shutdown()
|
||||||
|
{
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
if (handle_ != nullptr)
|
||||||
|
{
|
||||||
|
snd_pcm_close(handle_);
|
||||||
|
handle_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
opened_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AudioInput.h"
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
typedef struct _snd_pcm snd_pcm_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class AlsaAudioInput : public IAudioInput
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::string device_name_;
|
||||||
|
uint32_t sample_rate_;
|
||||||
|
uint32_t channels_;
|
||||||
|
bool opened_;
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
snd_pcm_t* handle_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
AlsaAudioInput();
|
||||||
|
~AlsaAudioInput();
|
||||||
|
|
||||||
|
bool init(
|
||||||
|
const std::string& device_name = "default",
|
||||||
|
uint32_t sample_rate = 16000,
|
||||||
|
uint32_t channels = 1) override;
|
||||||
|
int read_samples(int16_t* buffer, int sample_count) override;
|
||||||
|
float read_volume(int sample_count = 512) override;
|
||||||
|
void shutdown() override;
|
||||||
|
|
||||||
|
bool is_open() const override { return opened_; }
|
||||||
|
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||||
|
uint32_t get_channels() const override { return channels_; }
|
||||||
|
const std::string& get_device_name() const override { return device_name_; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,204 @@
|
||||||
|
#include "AlsaAudioOutput.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
#include <alsa/asoundlib.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
struct WavHeader
|
||||||
|
{
|
||||||
|
char riff[4];
|
||||||
|
uint32_t file_size;
|
||||||
|
char wave[4];
|
||||||
|
char fmt[4];
|
||||||
|
uint32_t fmt_size;
|
||||||
|
uint16_t audio_format;
|
||||||
|
uint16_t channels;
|
||||||
|
uint32_t sample_rate;
|
||||||
|
uint32_t byte_rate;
|
||||||
|
uint16_t block_align;
|
||||||
|
uint16_t bits_per_sample;
|
||||||
|
char data[4];
|
||||||
|
uint32_t data_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
static bool IsSupportedWav(const WavHeader& header)
|
||||||
|
{
|
||||||
|
return std::memcmp(header.riff, "RIFF", 4) == 0 &&
|
||||||
|
std::memcmp(header.wave, "WAVE", 4) == 0 &&
|
||||||
|
std::memcmp(header.fmt, "fmt ", 4) == 0 &&
|
||||||
|
std::memcmp(header.data, "data", 4) == 0 &&
|
||||||
|
header.audio_format == 1 &&
|
||||||
|
header.bits_per_sample == 16 &&
|
||||||
|
header.fmt_size == 16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
AlsaAudioOutput::AlsaAudioOutput()
|
||||||
|
: device_name_("default"),
|
||||||
|
sample_rate_(16000),
|
||||||
|
channels_(1),
|
||||||
|
opened_(false)
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
,
|
||||||
|
handle_(nullptr)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
AlsaAudioOutput::~AlsaAudioOutput()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlsaAudioOutput::init(const std::string& device_name, uint32_t sample_rate, uint32_t channels)
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
if (sample_rate == 0 || channels == 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioOutput invalid params." << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
device_name_ = device_name;
|
||||||
|
sample_rate_ = sample_rate;
|
||||||
|
channels_ = channels;
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
int result = snd_pcm_open(&handle_, device_name_.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioOutput open failed: " << snd_strerror(result) << std::endl;
|
||||||
|
handle_ = nullptr;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = snd_pcm_set_params(
|
||||||
|
handle_,
|
||||||
|
SND_PCM_FORMAT_S16_LE,
|
||||||
|
SND_PCM_ACCESS_RW_INTERLEAVED,
|
||||||
|
channels_,
|
||||||
|
sample_rate_,
|
||||||
|
1,
|
||||||
|
50000);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioOutput set params failed: " << snd_strerror(result) << std::endl;
|
||||||
|
shutdown();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opened_ = true;
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
std::cerr << "AlsaAudioOutput backend is unavailable. Enable ALSA development libraries." << std::endl;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int AlsaAudioOutput::write_samples(const int16_t* samples, int sample_count)
|
||||||
|
{
|
||||||
|
if (samples == nullptr || sample_count <= 0 || !opened_)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sample_count / channels_);
|
||||||
|
int result = snd_pcm_writei(handle_, samples, frames);
|
||||||
|
if (result == -EPIPE)
|
||||||
|
{
|
||||||
|
snd_pcm_prepare(handle_);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
result = snd_pcm_recover(handle_, result, 0);
|
||||||
|
if (result < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "AlsaAudioOutput write failed: " << snd_strerror(result) << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result * static_cast<int>(channels_);
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
bool AlsaAudioOutput::play_wav(const std::string& path)
|
||||||
|
{
|
||||||
|
std::ifstream file(path.c_str(), std::ios::binary);
|
||||||
|
if (!file.good())
|
||||||
|
{
|
||||||
|
std::cerr << "Open wav failed: " << path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
WavHeader header;
|
||||||
|
file.read(reinterpret_cast<char*>(&header), sizeof(header));
|
||||||
|
if (!file.good() || !IsSupportedWav(header))
|
||||||
|
{
|
||||||
|
std::cerr << "Unsupported wav: " << path << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opened_ || sample_rate_ != header.sample_rate || channels_ != header.channels)
|
||||||
|
{
|
||||||
|
if (!init(device_name_, header.sample_rate, header.channels))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<int16_t> samples(header.data_size / sizeof(int16_t), 0);
|
||||||
|
file.read(reinterpret_cast<char*>(samples.data()), header.data_size);
|
||||||
|
if (!file.good())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t offset = 0;
|
||||||
|
while (offset < samples.size())
|
||||||
|
{
|
||||||
|
const int count = static_cast<int>(std::min<size_t>(4096, samples.size() - offset));
|
||||||
|
const int written = write_samples(samples.data() + offset, count);
|
||||||
|
if (written <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += static_cast<size_t>(written);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
if (handle_ != nullptr)
|
||||||
|
{
|
||||||
|
snd_pcm_drain(handle_);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void AlsaAudioOutput::shutdown()
|
||||||
|
{
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
if (handle_ != nullptr)
|
||||||
|
{
|
||||||
|
snd_pcm_close(handle_);
|
||||||
|
handle_ = nullptr;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
opened_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "AudioOutput.h"
|
||||||
|
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
typedef struct _snd_pcm snd_pcm_t;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class AlsaAudioOutput : public IAudioOutput
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
std::string device_name_;
|
||||||
|
uint32_t sample_rate_;
|
||||||
|
uint32_t channels_;
|
||||||
|
bool opened_;
|
||||||
|
#if defined(__linux__) && defined(PLATFORM_HAS_ALSA)
|
||||||
|
snd_pcm_t* handle_;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public:
|
||||||
|
AlsaAudioOutput();
|
||||||
|
~AlsaAudioOutput();
|
||||||
|
|
||||||
|
bool init(
|
||||||
|
const std::string& device_name = "default",
|
||||||
|
uint32_t sample_rate = 16000,
|
||||||
|
uint32_t channels = 1) override;
|
||||||
|
int write_samples(const int16_t* samples, int sample_count) override;
|
||||||
|
bool play_wav(const std::string& path) override;
|
||||||
|
void shutdown() override;
|
||||||
|
|
||||||
|
bool is_open() const override { return opened_; }
|
||||||
|
uint32_t get_sample_rate() const override { return sample_rate_; }
|
||||||
|
uint32_t get_channels() const override { return channels_; }
|
||||||
|
const std::string& get_device_name() const override { return device_name_; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IAudioInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IAudioInput() {}
|
||||||
|
|
||||||
|
virtual bool init(
|
||||||
|
const std::string& device_name = "default",
|
||||||
|
uint32_t sample_rate = 16000,
|
||||||
|
uint32_t channels = 1) = 0;
|
||||||
|
virtual int read_samples(int16_t* buffer, int sample_count) = 0;
|
||||||
|
virtual float read_volume(int sample_count = 512) = 0;
|
||||||
|
virtual void shutdown() = 0;
|
||||||
|
|
||||||
|
virtual bool is_open() const = 0;
|
||||||
|
virtual uint32_t get_sample_rate() const = 0;
|
||||||
|
virtual uint32_t get_channels() const = 0;
|
||||||
|
virtual const std::string& get_device_name() const = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IAudioOutput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IAudioOutput() {}
|
||||||
|
|
||||||
|
virtual bool init(
|
||||||
|
const std::string& device_name = "default",
|
||||||
|
uint32_t sample_rate = 16000,
|
||||||
|
uint32_t channels = 1) = 0;
|
||||||
|
virtual int write_samples(const int16_t* samples, int sample_count) = 0;
|
||||||
|
virtual bool play_wav(const std::string& path) = 0;
|
||||||
|
virtual void shutdown() = 0;
|
||||||
|
|
||||||
|
virtual bool is_open() const = 0;
|
||||||
|
virtual uint32_t get_sample_rate() const = 0;
|
||||||
|
virtual uint32_t get_channels() const = 0;
|
||||||
|
virtual const std::string& get_device_name() const = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
class IButtonInput
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual ~IButtonInput() {}
|
||||||
|
|
||||||
|
virtual bool init(const std::string& device_path = "", int key_code = 0) = 0;
|
||||||
|
virtual void update() = 0;
|
||||||
|
virtual void shutdown() = 0;
|
||||||
|
|
||||||
|
virtual bool is_open() const = 0;
|
||||||
|
virtual bool is_down() const = 0;
|
||||||
|
virtual bool was_pressed() const = 0;
|
||||||
|
virtual bool was_released() const = 0;
|
||||||
|
virtual const std::string& get_device_path() const = 0;
|
||||||
|
virtual int get_key_code() const = 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#ifdef USE_FRAMEBUFFER
|
||||||
|
#include "AlsaAudioInput.h"
|
||||||
|
#include "AlsaAudioOutput.h"
|
||||||
|
#include "EvdevButtonInput.h"
|
||||||
|
#else
|
||||||
|
#include "SdlAudioInput.h"
|
||||||
|
#include "SdlAudioOutput.h"
|
||||||
|
#include "SdlKeyboardButtonInput.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
#ifdef USE_FRAMEBUFFER
|
||||||
|
typedef AlsaAudioInput DefaultAudioInput;
|
||||||
|
typedef AlsaAudioOutput DefaultAudioOutput;
|
||||||
|
typedef EvdevButtonInput DefaultButtonInput;
|
||||||
|
#else
|
||||||
|
typedef SdlAudioInput DefaultAudioInput;
|
||||||
|
typedef SdlAudioOutput DefaultAudioOutput;
|
||||||
|
typedef SdlKeyboardButtonInput DefaultButtonInput;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
#include "EvdevButtonInput.h"
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <linux/input.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Platform
|
||||||
|
{
|
||||||
|
EvdevButtonInput::EvdevButtonInput()
|
||||||
|
: device_path_("/dev/input/event0"),
|
||||||
|
key_code_(28),
|
||||||
|
opened_(false),
|
||||||
|
down_(false),
|
||||||
|
pressed_(false),
|
||||||
|
released_(false)
|
||||||
|
#if defined(__linux__)
|
||||||
|
,
|
||||||
|
fd_(-1)
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
EvdevButtonInput::~EvdevButtonInput()
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EvdevButtonInput::init(const std::string& device_path, int key_code)
|
||||||
|
{
|
||||||
|
shutdown();
|
||||||
|
|
||||||
|
device_path_ = device_path.empty() ? "/dev/input/event0" : device_path;
|
||||||
|
key_code_ = key_code == 0 ? 28 : key_code;
|
||||||
|
down_ = false;
|
||||||
|
pressed_ = false;
|
||||||
|
released_ = false;
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
fd_ = open(device_path_.c_str(), O_RDONLY | O_NONBLOCK);
|
||||||
|
if (fd_ < 0)
|
||||||
|
{
|
||||||
|
std::cerr << "EvdevButtonInput open failed: " << device_path_ << std::endl;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
opened_ = true;
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
std::cerr << "EvdevButtonInput is only implemented on Linux evdev." << std::endl;
|
||||||
|
return false;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void EvdevButtonInput::update()
|
||||||
|
{
|
||||||
|
pressed_ = false;
|
||||||
|
released_ = false;
|
||||||
|
|
||||||
|
if (!opened_)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(__linux__)
|
||||||
|
input_event event;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
const ssize_t bytes = read(fd_, &event, sizeof(event));
|
||||||
|
if (bytes == static_cast<ssize_t>(sizeof(event)))
|
||||||
|
{
|
||||||
|
if (event.type != EV_KEY || event.code != key_code_)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.value == 1)
|
||||||
|
{
|
||||||
|
if (!down_)
|
||||||
|
{
|
||||||
|
pressed_ = true;
|
||||||
|
}
|
||||||
|
down_ = true;
|
||||||
|
}
|
||||||
|
else if (event.value == 0)
|
||||||
|
{
|
||||||
|
if (down_)
|
||||||
|
{
|
||||||
|
released_ = true;
|
||||||
|
}
|
||||||
|
down_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (errno != EAGAIN && errno != EWOULDBLOCK)
|
||||||
|
{
|
||||||
|
std::cerr << "EvdevButtonInput read failed." << std::endl;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void EvdevButtonInput::shutdown()
|
||||||
|
{
|
||||||
|
#if defined(__linux__)
|
||||||
|
if (fd_ >= 0)
|
||||||
|
{
|
||||||
|
close(fd_);
|
||||||
|
fd_ = -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
opened_ = false;
|
||||||
|
down_ = false;
|
||||||
|
pressed_ = false;
|
||||||
|
released_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||