This commit is contained in:
HP
2026-06-07 12:34:07 +08:00
parent b093796e61
commit c177114bf3
123 changed files with 932 additions and 1008 deletions
+115 -466
View File
@@ -1,381 +1,138 @@
#include <iostream>
#include <array>
#include <cstdint>
<<<<<<< HEAD:src/main.cpp
#include <fstream>
#include <string>
#include <vector>
=======
#include <cstdio>
#include <cstring>
#include <thread>
#include <algorithm>
#include <chrono>
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include "Vector2.h"
#include "Vector3.h"
#include "Vector4.h"
#include "Matrix4x4.h"
#include "MathUtil.h"
#include "Color.h"
#include "Triangle.h"
#include "Camera.h"
#include <cstdint>
#include <cstdlib>
#include "Timer.h"
#include "Vertex.h"
<<<<<<< HEAD:src/main.cpp
#include "DepthBuffer.h"
#include "SpriteAssetLoader.h"
#include "SpriteRasterizer.h"
#include "Image.h"
#include "TomGameApp.h"
#include "AudioInput.h"
#include "AudioOutput.h"
#include "ButtonInput.h"
=======
#include "DrawContext.h"
#include "test_sprite.h"
#include "font_atlas.h"
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#include <cstring>
#include <cstdio>
#include <iostream>
#include <thread>
#include "BitmapFont.h"
#include "Color.h"
#include "Display.h"
#include "DrawContext.h"
#include "Image.h"
#include "TimeSource.h"
#include "Timer.h"
#include "font_atlas.h"
#include "test_sprite.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#endif
const int32_t width = 800;
const int32_t height = 600;
const int32_t tomWidth = 800;
const int32_t tomHeight = 480;
struct ProjectedVertex
namespace
{
Math::Vector3 screen;
bool visible = false;
};
const int32_t DemoWidth = 800;
const int32_t DemoHeight = 600;
struct CubeFace
{
std::array<int, 4> vertices;
};
struct CubeTriangle
{
std::array<int, 3> vertices;
};
static ProjectedVertex ProjectToScreen(
const Math::Vector3 &vertex,
const Math::Matrix4x4 &mvp,
const Math::Matrix4x4 &viewport)
{
using namespace Math;
const Vector4 clip = mvp * Vector4::Point(vertex);
if (std::abs(clip.w) < 1e-5f)
struct ProgramOptions
{
return {};
}
uint32_t target_fps;
bool show_help;
const float invW = 1.0f / clip.w;
const float ndcX = clip.x * invW;
const float ndcY = clip.y * invW;
const float ndcZ = clip.z * invW;
if (ndcX < -1.0f || ndcX > 1.0f || ndcY < -1.0f || ndcY > 1.0f || ndcZ < -1.0f || ndcZ > 1.0f)
{
return {};
}
const Vector4 screen = viewport * Vector4(ndcX, ndcY, ndcZ, 1.0f);
ProjectedVertex result;
result.screen = Vector3(screen.x, screen.y, screen.z);
result.visible = true;
return result;
}
static bool IsFaceVisible(const CubeFace &face, const std::array<Math::Vector3, 8> &viewSpaceVertices)
{
using namespace Math;
const Vector3 &v0 = viewSpaceVertices[face.vertices[0]];
const Vector3 &v1 = viewSpaceVertices[face.vertices[1]];
const Vector3 &v2 = viewSpaceVertices[face.vertices[2]];
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
const Vector3 faceCenter =
(viewSpaceVertices[face.vertices[0]] +
viewSpaceVertices[face.vertices[1]] +
viewSpaceVertices[face.vertices[2]] +
viewSpaceVertices[face.vertices[3]]) /
4.0f;
return faceNormal.dot(faceCenter) > 0.0f;
}
static bool IsTriangleVisible(const CubeTriangle &triangle, const std::array<Math::Vector3, 8> &viewSpaceVertices)
{
using namespace Math;
const Vector3 &v0 = viewSpaceVertices[triangle.vertices[0]];
const Vector3 &v1 = viewSpaceVertices[triangle.vertices[1]];
const Vector3 &v2 = viewSpaceVertices[triangle.vertices[2]];
const Vector3 faceNormal = (v1 - v0).cross(v2 - v0);
const Vector3 faceCenter = (v0 + v1 + v2) / 3.0f;
return faceNormal.dot(faceCenter) > 0.0f;
}
<<<<<<< HEAD:src/main.cpp
static bool HasArg(int argc, char* argv[], const std::string& expected)
{
for (int i = 1; i < argc; ++i)
{
if (argv[i] != nullptr && expected == argv[i])
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
return true;
}
}
return false;
}
static std::string FindSpriteAssetPath(const std::string& fileName)
{
const char* roots[] = {
"game/assets/sprites/",
"../game/assets/sprites/",
"../../game/assets/sprites/",
"../../../game/assets/sprites/",
"/tmp/game/assets/sprites/",
"/usr/local/share/imx6u-game/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
static Platform::IDisplay* CreateDisplay()
{
const std::string path = std::string(roots[i]) + fileName;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("game/assets/sprites/") + fileName;
}
static bool LoadSpriteAsset(const std::string& fileName, RenderData::Image& image)
{
const std::string path = FindSpriteAssetPath(fileName);
if (!Asset::SpriteAssetLoader::Load(path, image))
{
std::cerr << "Load sprite asset failed: " << path << std::endl;
return false;
}
return true;
}
static int RunTomGame()
{
#ifdef USE_FRAMEBUFFER
Platform::IDisplay *display = new Platform::FBDisplay();
return new Platform::FBDisplay();
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
return new Platform::SDLDisplay();
#endif
if (!display->init(tomWidth, tomHeight))
{
delete display;
return -1;
}
Core::FrameBuffer frameBuffer(tomWidth, tomHeight);
Rasterizer::SpriteRasterizer spriteRasterizer(&frameBuffer);
RenderData::Image background;
RenderData::Image tomStand;
RenderData::Image tomListen;
RenderData::Image tomSay1;
RenderData::Image tomSay2;
RenderData::Image tomSay3;
RenderData::Image tomSay4;
RenderData::Image button;
if (!LoadSpriteAsset("background.sprite", background) ||
!LoadSpriteAsset("Tom-stand.sprite", tomStand) ||
!LoadSpriteAsset("Tom-listhen.sprite", tomListen) ||
!LoadSpriteAsset("Tom-say1.sprite", tomSay1) ||
!LoadSpriteAsset("Tom-say2.sprite", tomSay2) ||
!LoadSpriteAsset("Tom-say3.sprite", tomSay3) ||
!LoadSpriteAsset("Tom-say4.sprite", tomSay4) ||
!LoadSpriteAsset("ui-record.sprite", button))
static void PrintUsage(const char* program_name)
{
std::cerr << "Run ConvertTomSprites before starting --tom." << std::endl;
display->shutdown();
delete display;
return -1;
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
Game::AudioInput audioInput;
Game::AudioOutput audioOutput;
Game::ButtonInput buttonInput;
#if defined(__linux__)
buttonInput.init();
#endif
Game::TomGameAssets assets;
assets.background = &background;
assets.tomStand = &tomStand;
assets.tomListen = &tomListen;
assets.tomSay1 = &tomSay1;
assets.tomSay2 = &tomSay2;
assets.tomSay3 = &tomSay3;
assets.tomSay4 = &tomSay4;
assets.button = &button;
Game::TomGameApp app(&frameBuffer, &spriteRasterizer, &audioInput, &audioOutput, &buttonInput);
app.set_assets(assets);
if (!app.is_ready())
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
{
std::cerr << "TomGameApp assets are incomplete." << std::endl;
display->shutdown();
delete display;
return -1;
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
const char* fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
bool isRunning = true;
uint32_t lastTime = display->get_time_ms();
while (isRunning)
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
{
display->poll_events(isRunning);
const uint32_t currentTime = display->get_time_ms();
const float deltaTime = static_cast<float>(currentTime - lastTime) * 0.001f;
lastTime = currentTime;
app.update(deltaTime);
frameBuffer.clear(RenderData::Color(18, 18, 24, 255));
app.draw();
display->present(&frameBuffer);
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
if (sleep_ms > 0u)
{
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
}
buttonInput.shutdown();
audioInput.shutdown();
audioOutput.shutdown();
display->shutdown();
delete display;
return 0;
=======
static void PrintUsage(const char *program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
struct ProgramOptions
int main(int argc, char* argv[])
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
static ProgramOptions ParseProgramOptions(int argc, char *argv[])
{
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char *arg = argv[i];
const char *fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
static void SleepRemainingFrameTime(const Core::Timer &timer, const Platform::ITimeSource &time_source)
{
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
if (sleep_ms > 0u)
{
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
}
int main(int argc, char *argv[])
{
<<<<<<< HEAD:src/main.cpp
if (HasArg(argc, argv, "--tom"))
{
return RunTomGame();
}
=======
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
>>>>>>> 777ff96602c52c86f03d612bb4213de746f580a5:src/Apps/Demo/main.cpp
#ifdef USE_FRAMEBUFFER
Platform::IDisplay *display = new Platform::FBDisplay();
#else
Platform::IDisplay *display = new Platform::SDLDisplay();
#endif
if (!display->init(width, height))
Platform::IDisplay* display = CreateDisplay();
if (!display->init(DemoWidth, DemoHeight))
{
delete display;
return -1;
}
Gfx::DrawContext ctx(width, height);
std::cout << "Target FPS: " << timer.target_fps() << std::endl;
Core::DrawContext ctx(DemoWidth, DemoHeight);
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
RenderData::BitmapFont font;
font.atlas = RenderData::Image(font_atlas_pixels, font_atlas_width, font_atlas_height);
@@ -384,164 +141,56 @@ int main(int argc, char *argv[])
font.columns = font_columns;
font.first_char = font_first_char;
RenderData::Image sprite_img(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000);
RenderData::SpriteRegion sprite_region(&sprite_img, 0, 0, sprite_img.width, sprite_img.height);
const std::array<uint16_t, 8 * 4> tileIds = {
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0,
0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile,
RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0, RenderData::Tilemap::EmptyTile, 0};
RenderData::Tilemap testTilemap(tileIds.data(), 8, 4, &sprite_img, sprite_img.width, sprite_img.height, 1);
Scene::Camera camera;
camera.transform.position = Math::Vector3(0.0f, 0.0f, 3.0f);
camera.transform.rotation = Math::Vector3(0.0f, 3.1415926535f, 0.0f);
const std::array<Math::Vector3, 8> cubeVertices = {
Math::Vector3(-0.5f, -0.5f, -0.5f),
Math::Vector3(0.5f, -0.5f, -0.5f),
Math::Vector3(0.5f, 0.5f, -0.5f),
Math::Vector3(-0.5f, 0.5f, -0.5f),
Math::Vector3(-0.5f, -0.5f, 0.5f),
Math::Vector3(0.5f, -0.5f, 0.5f),
Math::Vector3(0.5f, 0.5f, 0.5f),
Math::Vector3(-0.5f, 0.5f, 0.5f)};
const std::array<CubeFace, 6> cubeFaces = {
CubeFace{{0, 3, 2, 1}},
CubeFace{{4, 5, 6, 7}},
CubeFace{{0, 4, 7, 3}},
CubeFace{{1, 2, 6, 5}},
CubeFace{{0, 1, 5, 4}},
CubeFace{{3, 7, 6, 2}}};
const std::array<CubeTriangle, 12> cubeTriangles = {
CubeTriangle{{0, 3, 2}}, CubeTriangle{{0, 2, 1}},
CubeTriangle{{4, 5, 6}}, CubeTriangle{{4, 6, 7}},
CubeTriangle{{0, 4, 7}}, CubeTriangle{{0, 7, 3}},
CubeTriangle{{1, 2, 6}}, CubeTriangle{{1, 6, 5}},
CubeTriangle{{0, 1, 5}}, CubeTriangle{{0, 5, 4}},
CubeTriangle{{3, 7, 6}}, CubeTriangle{{3, 6, 2}}};
const RenderData::Color clearColor(18, 18, 24, 255);
const RenderData::Color cubeColor(240, 240, 240, 255);
const RenderData::Color fpsColor(0, 255, 80, 255);
const RenderData::Color fpsBg(0, 0, 0, 200);
const float aspectRatio = static_cast<float>(width) / static_cast<float>(height);
RenderData::Image sprite(test_sprite_pixels, test_sprite_width, test_sprite_height, 0x00000000u);
int32_t fps = 0;
int32_t frame_count = 0;
uint32_t last_fps_time = 0;
uint32_t animation_time_ms = 0;
char fps_text[32];
bool isRunning = true;
uint32_t animation_time_ms = 0;
while (isRunning)
std::cout << "Demo started. Target FPS: " << timer.target_fps() << std::endl;
bool is_running = true;
while (is_running)
{
timer.begin_frame(time_source.get_time_ms());
const uint32_t fixed_delta_ms = timer.fixed_delta_ms();
animation_time_ms += fixed_delta_ms;
animation_time_ms += timer.fixed_delta_ms();
display->poll_events(isRunning);
ctx.clear(clearColor);
const float animation_time = static_cast<float>(animation_time_ms) / 1000.0f;
const Math::Matrix4x4 model =
Math::MathUtil::get_rotation_matrix_y(animation_time) *
Math::MathUtil::get_rotation_matrix_x(static_cast<float>(animation_time_ms * 6u) / 10000.0f);
const Math::Matrix4x4 view = camera.get_view_matrix();
const Math::Matrix4x4 modelView = view * model;
const Math::Matrix4x4 projection = camera.get_perspective_projection_matrix(aspectRatio);
const Math::Matrix4x4 viewport = camera.get_viewport_matrix(static_cast<float>(width), static_cast<float>(height));
const Math::Matrix4x4 mvp = projection * modelView;
std::array<Math::Vector3, 8> viewSpaceVertices;
std::array<ProjectedVertex, 8> projectedVertices;
for (size_t i = 0; i < cubeVertices.size(); ++i)
bool should_quit = false;
display->poll_events(should_quit);
if (should_quit)
{
viewSpaceVertices[i] = (modelView * Math::Vector4::Point(cubeVertices[i])).to_vector3();
projectedVertices[i] = ProjectToScreen(cubeVertices[i], mvp, viewport);
is_running = false;
}
std::array<bool, 6> visibleFaces = {};
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
visibleFaces[faceIndex] = IsFaceVisible(cubeFaces[faceIndex], viewSpaceVertices);
}
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.fill_rect(48, 72, 704, 360, RenderData::Color(42, 48, 60, 255));
ctx.fill_rect(64, 88, 672, 328, RenderData::Color(24, 28, 36, 255));
std::array<RenderData::Triangle, 12> drawTriangles;
size_t drawCommandCount = 0;
for (const CubeTriangle &cubeTriangle : cubeTriangles)
{
if (!IsTriangleVisible(cubeTriangle, viewSpaceVertices))
{
continue;
}
const int32_t travel = std::max(1, DemoWidth - sprite.width - 128);
const int32_t sprite_x = 64 + static_cast<int32_t>((animation_time_ms / 8u) % static_cast<uint32_t>(travel));
const int32_t sprite_y = 220;
ctx.draw_sprite(sprite_x, sprite_y, sprite);
ctx.draw_text(font, 64, 96, RenderData::Color(240, 240, 240, 255), "GFX DEMO");
const ProjectedVertex &v0 = projectedVertices[cubeTriangle.vertices[0]];
const ProjectedVertex &v1 = projectedVertices[cubeTriangle.vertices[1]];
const ProjectedVertex &v2 = projectedVertices[cubeTriangle.vertices[2]];
if (!v0.visible || !v1.visible || !v2.visible)
{
continue;
}
drawTriangles[drawCommandCount++] =
RenderData::Triangle(
Scene::Vertex(v0.screen),
Scene::Vertex(v1.screen),
Scene::Vertex(v2.screen));
}
for (size_t i = 0; i < drawCommandCount; ++i)
{
ctx.draw_triangle(drawTriangles[i], cubeColor);
}
for (size_t faceIndex = 0; faceIndex < cubeFaces.size(); ++faceIndex)
{
if (!visibleFaces[faceIndex])
{
continue;
}
const CubeFace &face = cubeFaces[faceIndex];
for (size_t edgeOffset = 0; edgeOffset < face.vertices.size(); ++edgeOffset)
{
const int startIndex = face.vertices[edgeOffset];
const int endIndex = face.vertices[(edgeOffset + 1) % face.vertices.size()];
const ProjectedVertex &start = projectedVertices[startIndex];
const ProjectedVertex &end = projectedVertices[endIndex];
if (!start.visible || !end.visible)
{
continue;
}
ctx.draw_line(
Math::Vector2(start.screen.x, start.screen.y).to_vector2Int(),
Math::Vector2(end.screen.x, end.screen.y).to_vector2Int(),
clearColor);
}
}
// sprite 测试
ctx.draw_sprite(10, 10, sprite_img);
ctx.draw_sprite_region_ex(30, 10, sprite_region, 2, false, false);
ctx.draw_sprite_region_ex(10, 30, sprite_region, 3, true, false);
ctx.draw_tilemap(testTilemap, 650, 500, 96, 48, static_cast<int32_t>(animation_time_ms / 20u) % 32, 0);
// FPS 计数
++frame_count;
const uint32_t now = time_source.get_time_ms();
if (now - last_fps_time >= 1000)
if (now - last_fps_time >= 1000u)
{
fps = frame_count;
frame_count = 0;
last_fps_time = now;
}
std::snprintf(fps_text, sizeof(fps_text), "FPS: %d", fps);
ctx.draw_text(font, 4, 4, fpsColor, fpsBg, fps_text);
ctx.draw_text(
font,
4,
4,
RenderData::Color(0, 255, 80, 255),
RenderData::Color(0, 0, 0, 200),
fps_text);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);
+301
View File
@@ -0,0 +1,301 @@
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <iostream>
#include <string>
#include <thread>
#include <vector>
#include "Color.h"
#include "Display.h"
#include "DrawContext.h"
#include "Image.h"
#include "SpriteAssetLoader.h"
#include "TimeSource.h"
#include "Timer.h"
#ifdef USE_FRAMEBUFFER
#include "FBDisplay.h"
#else
#include "SDLDisplay.h"
#endif
namespace
{
const int32_t ScreenWidth = 800;
const int32_t ScreenHeight = 480;
struct ProgramOptions
{
uint32_t target_fps;
bool show_help;
ProgramOptions()
: target_fps(Core::Timer::DefaultFps),
show_help(false)
{
}
};
struct SpriteImage
{
std::vector<uint32_t> pixels;
RenderData::Image image;
bool is_valid() const
{
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
};
static Platform::IDisplay* CreateDisplay()
{
#ifdef USE_FRAMEBUFFER
return new Platform::FBDisplay();
#else
return new Platform::SDLDisplay();
#endif
}
static void PrintUsage(const char* program_name)
{
std::cout
<< "Usage: " << program_name << " [--fps 30|45|60]\n"
<< " " << program_name << " [--fps=30|45|60]\n";
}
static ProgramOptions ParseProgramOptions(int argc, char* argv[])
{
ProgramOptions options;
for (int i = 1; i < argc; ++i)
{
const char* arg = argv[i];
const char* fps_value = nullptr;
if (std::strcmp(arg, "--help") == 0 || std::strcmp(arg, "-h") == 0)
{
options.show_help = true;
return options;
}
else if (std::strcmp(arg, "--fps") == 0)
{
if (i + 1 < argc)
{
fps_value = argv[++i];
}
else
{
std::cerr << "Missing value for --fps, falling back to 30. Supported: 30, 45, 60.\n";
}
}
else if (std::strncmp(arg, "--fps=", 6) == 0)
{
fps_value = arg + 6;
}
if (fps_value != nullptr)
{
const uint32_t parsed_fps = static_cast<uint32_t>(std::strtoul(fps_value, nullptr, 10));
if (Core::Timer::is_supported_fps(parsed_fps))
{
options.target_fps = parsed_fps;
}
else
{
std::cerr << "Unsupported FPS '" << fps_value << "', falling back to 30. Supported: 30, 45, 60.\n";
options.target_fps = Core::Timer::DefaultFps;
}
}
}
return options;
}
static void SleepRemainingFrameTime(const Core::Timer& timer, const Platform::ITimeSource& time_source)
{
const uint32_t sleep_ms = timer.remaining_frame_ms(time_source.get_time_ms());
if (sleep_ms > 0u)
{
std::this_thread::sleep_for(std::chrono::milliseconds(sleep_ms));
}
}
static std::string FindSpriteAssetPath(const std::string& file_name)
{
const char* roots[] = {
"src/Apps/Game/assets/sprites/",
"../src/Apps/Game/assets/sprites/",
"../../src/Apps/Game/assets/sprites/",
"../../../src/Apps/Game/assets/sprites/",
"/usr/local/share/imx6u-game/sprites/"
};
for (size_t i = 0; i < sizeof(roots) / sizeof(roots[0]); ++i)
{
const std::string path = std::string(roots[i]) + file_name;
std::ifstream file(path.c_str(), std::ios::binary);
if (file.good())
{
return path;
}
}
return std::string("src/Apps/Game/assets/sprites/") + file_name;
}
static void EnableAlphaColorKey(SpriteImage& sprite)
{
for (size_t i = 0; i < sprite.pixels.size(); ++i)
{
if ((sprite.pixels[i] & 0xFFu) == 0u)
{
sprite.pixels[i] = 0x00000000u;
}
}
sprite.image = RenderData::Image(
sprite.pixels.data(),
sprite.image.width,
sprite.image.height,
0x00000000u);
}
static bool LoadSpriteAsset(const std::string& file_name, SpriteImage& sprite, bool use_alpha_key)
{
const std::string path = FindSpriteAssetPath(file_name);
if (!Asset::SpriteAssetLoader::Load(path, sprite.pixels, sprite.image))
{
std::cerr << "Load sprite asset failed: " << path << std::endl;
return false;
}
if (use_alpha_key)
{
EnableAlphaColorKey(sprite);
}
return sprite.is_valid();
}
static const RenderData::Image& SelectTomFrame(
uint32_t animation_time_ms,
const SpriteImage& stand,
const SpriteImage& listen,
const SpriteImage* const* speaking_frames,
size_t speaking_frame_count)
{
const uint32_t phase = (animation_time_ms / 1000u) % 6u;
if (phase < 2u)
{
return stand.image;
}
if (phase < 3u)
{
return listen.image;
}
const size_t frame_index = (animation_time_ms / 120u) % speaking_frame_count;
return speaking_frames[frame_index]->image;
}
}
int main(int argc, char* argv[])
{
const ProgramOptions options = ParseProgramOptions(argc, argv);
if (options.show_help)
{
PrintUsage(argv[0]);
return 0;
}
Platform::IDisplay* display = CreateDisplay();
if (!display->init(ScreenWidth, ScreenHeight))
{
delete display;
return -1;
}
SpriteImage background;
SpriteImage tom_stand;
SpriteImage tom_listen;
SpriteImage tom_say1;
SpriteImage tom_say2;
SpriteImage tom_say3;
SpriteImage tom_say4;
SpriteImage record_button;
if (!LoadSpriteAsset("background.sprite", background, false) ||
!LoadSpriteAsset("Tom-stand.sprite", tom_stand, true) ||
!LoadSpriteAsset("Tom-listhen.sprite", tom_listen, true) ||
!LoadSpriteAsset("Tom-say1.sprite", tom_say1, true) ||
!LoadSpriteAsset("Tom-say2.sprite", tom_say2, true) ||
!LoadSpriteAsset("Tom-say3.sprite", tom_say3, true) ||
!LoadSpriteAsset("Tom-say4.sprite", tom_say4, true) ||
!LoadSpriteAsset("ui-record.sprite", record_button, true))
{
std::cerr << "Missing Tom sprite assets. Run the ConvertTomSprites target first.\n";
display->shutdown();
delete display;
return -1;
}
const SpriteImage* speaking_frames[] = {
&tom_say1,
&tom_say2,
&tom_say3,
&tom_say4,
&tom_say3,
&tom_say2
};
const size_t speaking_frame_count = sizeof(speaking_frames) / sizeof(speaking_frames[0]);
Core::DrawContext ctx(ScreenWidth, ScreenHeight);
Core::Timer timer(options.target_fps);
Platform::SteadyTimeSource time_source;
std::cout << "Tom game started. Target FPS: " << timer.target_fps() << std::endl;
bool is_running = true;
uint32_t animation_time_ms = 0;
while (is_running)
{
timer.begin_frame(time_source.get_time_ms());
animation_time_ms += timer.fixed_delta_ms();
bool should_quit = false;
display->poll_events(should_quit);
if (should_quit)
{
is_running = false;
}
ctx.clear(RenderData::Color(18, 18, 24, 255));
ctx.draw_sprite(0, 0, background.image);
const RenderData::Image& tom = SelectTomFrame(
animation_time_ms,
tom_stand,
tom_listen,
speaking_frames,
speaking_frame_count);
const int32_t tom_x = (ScreenWidth - tom.width) / 2;
const int32_t tom_y = std::max(0, ScreenHeight - tom.height - 72);
ctx.draw_sprite(tom_x, tom_y, tom);
const int32_t button_x = (ScreenWidth - record_button.image.width) / 2;
const int32_t button_y = ScreenHeight - record_button.image.height - 16;
ctx.draw_sprite(button_x, button_y, record_button.image);
ctx.present(display);
SleepRemainingFrameTime(timer, time_source);
}
display->shutdown();
delete display;
return 0;
}
+74
View File
@@ -0,0 +1,74 @@
# Tom Game
`src/Apps/Game` 是 Tom 游戏应用目录。仓库根目录下的 `src/Core` 继续作为渲染、数学、光栅化和平台显示层;Game 目录只放游戏入口、游戏逻辑、素材和素材转换工具。
## 当前入口
主构建目标 `IMX6U-Game` 现在从下面的文件启动:
```text
src/Apps/Game/Main.cpp
```
运行后默认启动 Tom,不再需要 `--tom` 参数。可选参数只保留帧率:
```bash
IMX6U-Game.exe --fps 30
IMX6U-Game.exe --fps=60
```
## 素材目录
```text
src/Apps/Game/assets/raw/ PNG 源素材
src/Apps/Game/assets/sprites/ 转换后的 .sprite 运行时素材
src/Apps/Game/tools/ 素材转换等开发工具
src/Apps/Game/src/ 旧版 TomGameApp、音频、动画等游戏模块
src/Apps/Game/tests/ 手工测试或后续测试代码
```
板端运行时不直接解码 PNG。开发时把 PNG 放在 `src/Apps/Game/assets/raw/`,再转换为 `src/Apps/Game/assets/sprites/*.sprite`。
## 素材转换
`.sprite` 是项目自定义的 RGBA8888 二进制格式:
```text
offset size content
0 4 magic: "SPRT"
4 4 version: 1, little-endian uint32
8 4 width, little-endian uint32
12 4 height, little-endian uint32
16 4 format: 1 means RGBA8888
20 * width * height pixels, little-endian uint32, 0xRRGGBBAA
```
常用命令:
```bash
cmake --build build-win --config Release --target SpriteAssetTool
cmake --build build-win --config Release --target ConvertTomSprites
```
单张转换示例:
```bash
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/ui-record.png src/Apps/Game/assets/sprites/ui-record.sprite
./build-win/Release/SpriteAssetTool.exe src/Apps/Game/assets/raw/Tom-stand.png src/Apps/Game/assets/sprites/Tom-stand.sprite --fit 560 360
```
## 运行
Windows PC:
```bash
./build-win/Release/IMX6U-Game.exe
```
Linux framebuffer / IMX6U:
```bash
./IMX6U-Game --fps 30
```
部署到板端时,把可执行文件和 `src/Apps/Game/assets/sprites/` 一起拷贝,并保持相对路径;也可以把 `.sprite` 放到 `/usr/local/share/imx6u-game/sprites/`。
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

+1
View File
@@ -0,0 +1 @@
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -0,0 +1 @@
+280
View File
@@ -0,0 +1,280 @@
#include "TomGameApp.h"
#include "../audio/VoiceEffect.h"
#include "../hardware/AudioInput.h"
#include "../hardware/AudioOutput.h"
#include "../hardware/ButtonInput.h"
#include "FrameBuffer.h"
#include "SpriteRasterizer.h"
#include "Image.h"
namespace Game
{
TomGameApp::TomGameApp(
Core::FrameBuffer* frameBuffer,
Rasterizer::SpriteRasterizer* spriteRasterizer,
AudioInput* audioInput,
AudioOutput* audioOutput,
ButtonInput* 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);
}
}
+117
View File
@@ -0,0 +1,117 @@
#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 Game
{
class AudioInput;
class AudioOutput;
class ButtonInput;
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;
AudioInput* audioInput;
AudioOutput* audioOutput;
ButtonInput* 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,
AudioInput* audioInput,
AudioOutput* audioOutput,
ButtonInput* 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(); }
};
}
+145
View File
@@ -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);
}
}
+33
View File
@@ -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);
};
}
+155
View File
@@ -0,0 +1,155 @@
#include "VoicePlayer.h"
#include "../hardware/AudioOutput.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(AudioOutput& 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));
}
}
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Game
{
class AudioOutput;
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(AudioOutput& 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(); }
};
}
+171
View File
@@ -0,0 +1,171 @@
#include "VoiceRecorder.h"
#include "../hardware/AudioInput.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(AudioInput& 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));
}
}
+59
View File
@@ -0,0 +1,59 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
namespace Game
{
class AudioInput;
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(AudioInput& 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(); }
};
}
+1
View File
@@ -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;
};
}
+140
View File
@@ -0,0 +1,140 @@
#include "AudioInput.h"
#include <cmath>
#include <cstdlib>
#include <iostream>
#if defined(__linux__)
#include <alsa/asoundlib.h>
#endif
namespace Game
{
AudioInput::AudioInput() :
deviceName("default"),
sampleRate(16000),
channels(1),
opened(false)
#if defined(__linux__)
,
handle(nullptr)
#endif
{}
AudioInput::~AudioInput()
{
shutdown();
}
bool AudioInput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
{
shutdown();
if (sampleRate == 0 || channels == 0)
{
std::cerr << "AudioInput invalid params." << std::endl;
return false;
}
this->deviceName = deviceName;
this->sampleRate = sampleRate;
this->channels = channels;
#if defined(__linux__)
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_CAPTURE, 0);
if (result < 0)
{
std::cerr << "AudioInput 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,
sampleRate,
1,
50000);
if (result < 0)
{
std::cerr << "AudioInput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened = true;
return true;
#else
std::cerr << "AudioInput is only implemented on Linux ALSA." << std::endl;
return false;
#endif
}
int AudioInput::read_samples(int16_t* buffer, int sampleCount)
{
if (buffer == nullptr || sampleCount <= 0 || !opened)
{
return 0;
}
#if defined(__linux__)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / 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 << "AudioInput read failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels);
#else
return 0;
#endif
}
float AudioInput::read_volume(int sampleCount)
{
if (sampleCount <= 0)
{
return 0.0f;
}
std::vector<int16_t> samples(static_cast<size_t>(sampleCount), 0);
const int count = read_samples(samples.data(), sampleCount);
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 AudioInput::shutdown()
{
#if defined(__linux__)
if (handle != nullptr)
{
snd_pcm_close(handle);
handle = nullptr;
}
#endif
opened = false;
}
}
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <cstdint>
#include <string>
#include <vector>
#if defined(__linux__)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Game
{
class AudioInput
{
private:
std::string deviceName;
uint32_t sampleRate;
uint32_t channels;
bool opened;
#if defined(__linux__)
snd_pcm_t* handle;
#endif
public:
AudioInput();
~AudioInput();
bool init(
const std::string& deviceName = "default",
uint32_t sampleRate = 16000,
uint32_t channels = 1);
int read_samples(int16_t* buffer, int sampleCount);
float read_volume(int sampleCount = 512);
void shutdown();
bool is_open() const { return opened; }
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
const std::string& get_device_name() const { return deviceName; }
};
}
+203
View File
@@ -0,0 +1,203 @@
#include "AudioOutput.h"
#include <algorithm>
#include <cstring>
#include <fstream>
#include <iostream>
#include <vector>
#if defined(__linux__)
#include <alsa/asoundlib.h>
#endif
namespace
{
struct WavHeader
{
char riff[4];
uint32_t fileSize;
char wave[4];
char fmt[4];
uint32_t fmtSize;
uint16_t audioFormat;
uint16_t channels;
uint32_t sampleRate;
uint32_t byteRate;
uint16_t blockAlign;
uint16_t bitsPerSample;
char data[4];
uint32_t dataSize;
};
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.audioFormat == 1 &&
header.bitsPerSample == 16 &&
header.fmtSize == 16;
}
}
namespace Game
{
AudioOutput::AudioOutput() :
deviceName("default"),
sampleRate(16000),
channels(1),
opened(false)
#if defined(__linux__)
,
handle(nullptr)
#endif
{}
AudioOutput::~AudioOutput()
{
shutdown();
}
bool AudioOutput::init(const std::string& deviceName, uint32_t sampleRate, uint32_t channels)
{
shutdown();
if (sampleRate == 0 || channels == 0)
{
std::cerr << "AudioOutput invalid params." << std::endl;
return false;
}
this->deviceName = deviceName;
this->sampleRate = sampleRate;
this->channels = channels;
#if defined(__linux__)
int result = snd_pcm_open(&handle, deviceName.c_str(), SND_PCM_STREAM_PLAYBACK, 0);
if (result < 0)
{
std::cerr << "AudioOutput 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,
sampleRate,
1,
50000);
if (result < 0)
{
std::cerr << "AudioOutput set params failed: " << snd_strerror(result) << std::endl;
shutdown();
return false;
}
opened = true;
return true;
#else
std::cerr << "AudioOutput is only implemented on Linux ALSA." << std::endl;
return false;
#endif
}
int AudioOutput::write_samples(const int16_t* samples, int sampleCount)
{
if (samples == nullptr || sampleCount <= 0 || !opened)
{
return 0;
}
#if defined(__linux__)
const snd_pcm_uframes_t frames = static_cast<snd_pcm_uframes_t>(sampleCount / 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 << "AudioOutput write failed: " << snd_strerror(result) << std::endl;
return 0;
}
}
return result * static_cast<int>(channels);
#else
return 0;
#endif
}
bool AudioOutput::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 || sampleRate != header.sampleRate || channels != header.channels)
{
if (!init(deviceName, header.sampleRate, header.channels))
{
return false;
}
}
std::vector<int16_t> samples(header.dataSize / sizeof(int16_t), 0);
file.read(reinterpret_cast<char*>(samples.data()), header.dataSize);
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__)
if (handle != nullptr)
{
snd_pcm_drain(handle);
}
#endif
return true;
}
void AudioOutput::shutdown()
{
#if defined(__linux__)
if (handle != nullptr)
{
snd_pcm_close(handle);
handle = nullptr;
}
#endif
opened = false;
}
}
+39
View File
@@ -0,0 +1,39 @@
#pragma once
#include <cstdint>
#include <string>
#if defined(__linux__)
typedef struct _snd_pcm snd_pcm_t;
#endif
namespace Game
{
class AudioOutput
{
private:
std::string deviceName;
uint32_t sampleRate;
uint32_t channels;
bool opened;
#if defined(__linux__)
snd_pcm_t* handle;
#endif
public:
AudioOutput();
~AudioOutput();
bool init(
const std::string& deviceName = "default",
uint32_t sampleRate = 16000,
uint32_t channels = 1);
int write_samples(const int16_t* samples, int sampleCount);
bool play_wav(const std::string& path);
void shutdown();
bool is_open() const { return opened; }
uint32_t get_sample_rate() const { return sampleRate; }
uint32_t get_channels() const { return channels; }
const std::string& get_device_name() const { return deviceName; }
};
}
+122
View File
@@ -0,0 +1,122 @@
#include "ButtonInput.h"
#include <iostream>
#if defined(__linux__)
#include <errno.h>
#include <fcntl.h>
#include <linux/input.h>
#include <unistd.h>
#endif
namespace Game
{
ButtonInput::ButtonInput() :
devicePath("/dev/input/event0"),
keyCode(28),
opened(false),
down(false),
pressed(false),
released(false)
#if defined(__linux__)
,
fd(-1)
#endif
{}
ButtonInput::~ButtonInput()
{
shutdown();
}
bool ButtonInput::init(const std::string& devicePath, int keyCode)
{
shutdown();
this->devicePath = devicePath;
this->keyCode = keyCode;
down = false;
pressed = false;
released = false;
#if defined(__linux__)
fd = open(devicePath.c_str(), O_RDONLY | O_NONBLOCK);
if (fd < 0)
{
std::cerr << "ButtonInput open failed: " << devicePath << std::endl;
return false;
}
opened = true;
return true;
#else
std::cerr << "ButtonInput is only implemented on Linux evdev." << std::endl;
return false;
#endif
}
void ButtonInput::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 != keyCode)
{
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 << "ButtonInput read failed." << std::endl;
}
break;
}
}
#endif
}
void ButtonInput::shutdown()
{
#if defined(__linux__)
if (fd >= 0)
{
close(fd);
fd = -1;
}
#endif
opened = false;
down = false;
pressed = false;
released = false;
}
}
+35
View File
@@ -0,0 +1,35 @@
#pragma once
#include <cstdint>
#include <string>
namespace Game
{
class ButtonInput
{
private:
std::string devicePath;
int keyCode;
bool opened;
bool down;
bool pressed;
bool released;
#if defined(__linux__)
int fd;
#endif
public:
ButtonInput();
~ButtonInput();
bool init(const std::string& devicePath = "/dev/input/event0", int keyCode = 28);
void update();
void shutdown();
bool is_open() const { return opened; }
bool is_down() const { return down; }
bool was_pressed() const { return pressed; }
bool was_released() const { return released; }
const std::string& get_device_path() const { return devicePath; }
int get_key_code() const { return keyCode; }
};
}
+1
View File
@@ -0,0 +1 @@
+1
View File
@@ -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(); }
};
}
+1
View File
@@ -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;
}
+1
View File
@@ -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;
}
+182
View File
@@ -0,0 +1,182 @@
#include "SpriteAssetLoader.h"
#include <cstdint>
#include <fstream>
#include <limits>
namespace
{
static const char SpriteMagic[4] = { 'S', 'P', 'R', 'T' };
static const size_t SpriteHeaderSize = 20;
static const uint32_t SpriteVersion = 1;
static const uint32_t SpriteFormatRgba8888 = 1;
static const uint64_t MaxSpritePixels = 8192ull * 8192ull;
static bool ReadExact(std::ifstream& file, char* data, size_t size)
{
file.read(data, static_cast<std::streamsize>(size));
return static_cast<size_t>(file.gcount()) == size;
}
static bool ReadU32LE(const std::vector<uint8_t>& bytes, size_t offset, uint32_t& value)
{
if (offset + 4 > bytes.size())
{
return false;
}
value =
static_cast<uint32_t>(bytes[offset]) |
(static_cast<uint32_t>(bytes[offset + 1]) << 8) |
(static_cast<uint32_t>(bytes[offset + 2]) << 16) |
(static_cast<uint32_t>(bytes[offset + 3]) << 24);
return true;
}
static void WriteU32LE(std::ofstream& file, uint32_t value)
{
const char bytes[4] = {
static_cast<char>(value & 0xFF),
static_cast<char>((value >> 8) & 0xFF),
static_cast<char>((value >> 16) & 0xFF),
static_cast<char>((value >> 24) & 0xFF)
};
file.write(bytes, sizeof(bytes));
}
static bool CheckPixelCount(uint32_t width, uint32_t height, size_t& pixelCount)
{
if (width == 0 || height == 0)
{
return false;
}
const uint64_t total = static_cast<uint64_t>(width) * static_cast<uint64_t>(height);
if (total > MaxSpritePixels ||
total > static_cast<uint64_t>(std::numeric_limits<size_t>::max() / sizeof(uint32_t)))
{
return false;
}
pixelCount = static_cast<size_t>(total);
return true;
}
static bool ReadPixels(std::ifstream& file, std::vector<uint32_t>& pixels)
{
std::vector<uint8_t> bytes(pixels.size() * sizeof(uint32_t), 0);
if (!ReadExact(file, reinterpret_cast<char*>(bytes.data()), bytes.size()))
{
return false;
}
for (size_t i = 0; i < pixels.size(); ++i)
{
uint32_t value = 0;
if (!ReadU32LE(bytes, i * sizeof(uint32_t), value))
{
return false;
}
pixels[i] = value;
}
return true;
}
}
namespace Asset
{
bool SpriteAssetLoader::Load(
const std::string& path,
std::vector<uint32_t>& pixels,
RenderData::Image& image)
{
std::ifstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
std::vector<uint8_t> header(SpriteHeaderSize, 0);
if (!ReadExact(file, reinterpret_cast<char*>(header.data()), header.size()))
{
return false;
}
if (header[0] != SpriteMagic[0] ||
header[1] != SpriteMagic[1] ||
header[2] != SpriteMagic[2] ||
header[3] != SpriteMagic[3])
{
return false;
}
uint32_t version = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t format = 0;
if (!ReadU32LE(header, 4, version) ||
!ReadU32LE(header, 8, width) ||
!ReadU32LE(header, 12, height) ||
!ReadU32LE(header, 16, format))
{
return false;
}
size_t pixelCount = 0;
if (version != SpriteVersion ||
format != SpriteFormatRgba8888 ||
!CheckPixelCount(width, height, pixelCount))
{
return false;
}
std::vector<uint32_t> loadedPixels(pixelCount, 0);
if (!ReadPixels(file, loadedPixels))
{
return false;
}
char trailingByte = 0;
file.read(&trailingByte, 1);
if (file.gcount() != 0)
{
return false;
}
pixels.swap(loadedPixels);
image = RenderData::Image(pixels.data(), static_cast<int32_t>(width), static_cast<int32_t>(height));
return image.pixels != nullptr && image.width > 0 && image.height > 0;
}
bool SpriteAssetLoader::Save(const std::string& path, const RenderData::Image& image)
{
if (image.pixels == nullptr || image.width <= 0 || image.height <= 0)
{
return false;
}
size_t pixelCount = 0;
if (!CheckPixelCount(static_cast<uint32_t>(image.width), static_cast<uint32_t>(image.height), pixelCount))
{
return false;
}
std::ofstream file(path.c_str(), std::ios::binary);
if (!file.good())
{
return false;
}
file.write(SpriteMagic, sizeof(SpriteMagic));
WriteU32LE(file, SpriteVersion);
WriteU32LE(file, static_cast<uint32_t>(image.width));
WriteU32LE(file, static_cast<uint32_t>(image.height));
WriteU32LE(file, SpriteFormatRgba8888);
for (size_t i = 0; i < pixelCount; ++i)
{
WriteU32LE(file, image.pixels[i]);
}
return file.good();
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <string>
#include <vector>
#include "Image.h"
namespace Asset
{
class SpriteAssetLoader
{
public:
static const char* GetFileExtension() { return ".sprite"; }
static bool Load(const std::string& path, std::vector<uint32_t>& pixels, RenderData::Image& image);
static bool Save(const std::string& path, const RenderData::Image& image);
};
}
View File
@@ -6,7 +6,7 @@
#include "Display.h"
#include <algorithm>
namespace Gfx
namespace Core
{
DrawContext::DrawContext(int32_t width, int32_t height)
{
@@ -25,7 +25,7 @@ namespace Platform
class IDisplay;
}
namespace Gfx
namespace Core
{
class DrawContext
{

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