merge remote and local changes

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