302 lines
6.9 KiB
C++
302 lines
6.9 KiB
C++
#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;
|
|
}
|