Skip to content

Add loading screen #158

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 114 additions & 23 deletions src/application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
#include "helper/message_box.hpp"
#include "helper/sleep.hpp"
#include "input/input.hpp"
#include "manager/music_manager.hpp"
#include "scenes/loading_screen/loading_screen.hpp"
#include "scenes/scene.hpp"
#include "ui/layout.hpp"

#include <chrono>
#include <fmt/chrono.h>
#include <future>
#include <memory>
#include <ranges>
#include <stdexcept>
Expand Down Expand Up @@ -35,9 +40,6 @@ Application::Application(std::shared_ptr<Window>&& window, const std::vector<std
m_window{ std::move(window) },
m_renderer{ *m_window, m_command_line_arguments.target_fps.has_value() ? Renderer::VSync::Disabled
: Renderer::VSync::Enabled },
m_music_manager{ this, num_audio_channels },
m_input_manager{ std::make_shared<input::InputManager>(m_window) },
m_settings_manager{ this },
m_target_framerate{ m_command_line_arguments.target_fps } {
initialize();
} catch (const helper::GeneralError& general_error) {
Expand Down Expand Up @@ -156,7 +158,7 @@ void Application::handle_event(const SDL_Event& event) {

// this global event handlers (atm only one) are special cases, they receive all inputs if they are not handled by the scenes explicitly

if (m_music_manager.handle_event(m_input_manager, event)) {
if (m_music_manager->handle_event(m_input_manager, event)) {
return;
}
}
Expand Down Expand Up @@ -249,32 +251,121 @@ void Application::render() const {

void Application::initialize() {

load_resources();
push_scene(scenes::create_scene(*this, SceneId::MainMenu, ui::FullScreenLayout{ *m_window }));
auto loading_screen = scenes::LoadingScreen{ this };

const auto start_time = SDL_GetTicks64();

const std::future<void> load_everything = std::async(std::launch::async, [this] {
this->m_music_manager = std::make_unique<MusicManager>(this, num_audio_channels);

this->m_input_manager = std::make_shared<input::InputManager>(this->m_window);

this->m_settings_manager = std::make_unique<SettingsManager>(this);

this->m_font_manager = std::make_unique<FontManager>();

this->load_resources();

#ifdef DEBUG_BUILD
m_fps_text = std::make_unique<ui::Label>(
this, "FPS: ?", font_manager().get(FontId::Default), Color::white(),
std::pair<double, double>{ 0.95, 0.95 },
ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center },
ui::RelativeLayout{ window(), 0.0, 0.0, 0.1, 0.05 }, false
);
m_fps_text = std::make_unique<ui::Label>(
this, "FPS: ?", font_manager().get(FontId::Default), Color::white(),
std::pair<double, double>{ 0.95, 0.95 },
ui::Alignment{ ui::AlignmentHorizontal::Middle, ui::AlignmentVertical::Center },
ui::RelativeLayout{ window(), 0.0, 0.0, 0.1, 0.05 }, false
);
#endif

#if defined(_HAVE_DISCORD_SDK)
if (m_settings_manager.settings().discord) {
auto discord_instance = DiscordInstance::initialize();
if (not discord_instance.has_value()) {
spdlog::warn(
"Error initializing the discord instance, it might not be running: {}", discord_instance.error()
);
} else {
m_discord_instance = std::move(discord_instance.value());
m_discord_instance->after_setup();
if (m_settings_manager->settings().discord) {
auto discord_instance = DiscordInstance::initialize();
if (not discord_instance.has_value()) {
spdlog::warn(
"Error initializing the discord instance, it might not be running: {}", discord_instance.error()
);
} else {
m_discord_instance = std::move(discord_instance.value());
m_discord_instance->after_setup();
}
}
}

#endif
});


using namespace std::chrono_literals;

const auto sleep_time = m_target_framerate.has_value() ? std::chrono::duration_cast<std::chrono::nanoseconds>(1s)
/ m_target_framerate.value()
: 0s;
auto start_execution_time = std::chrono::steady_clock::now();


bool finished_loading = false;

// this is a duplicate of below in some cases, but it's just for the loading screen and can't be factored out easily
// this also only uses a subset of all things, the real event loop uses, so that nothing breaks while doing multithreading
// the only things usable are: (since NOT accessed (writing) via the loading thread and already initialized):
// - m_command_line_arguments
// - m_window
// - m_renderer
// - m_target_framerate

while ((not finished_loading) and m_is_running
#if defined(__CONSOLE__)
and console::inMainLoop()
#endif
) {

// we can't use the normal event loop, so we have to do it manually
SDL_Event event;
while (SDL_PollEvent(&event) != 0) {
if (event.type == SDL_QUIT) {
m_is_running = false;
}
}

loading_screen.update();
// this service_provider only guarantees the renderer + the window to be accessible without race conditions
loading_screen.render(*this);

// present and wait (depending if vsync is on or not, this has to be done manually)
m_renderer.present();

if (m_target_framerate.has_value()) {

const auto now = std::chrono::steady_clock::now();
const auto runtime = (now - start_execution_time);
if (runtime < sleep_time) {
//TODO(totto): use SDL_DelayNS in sdl >= 3.0
helper::sleep_nanoseconds(sleep_time - runtime);
start_execution_time = std::chrono::steady_clock::now();
} else {
start_execution_time = now;
}
}
// end waiting

// wait until is faster, since it just compares two time_points instead of getting now() and than adding the wait-for argument
finished_loading =
load_everything.wait_until(std::chrono::system_clock::time_point::min()) == std::future_status::ready;
}


const auto duration = std::chrono::milliseconds(SDL_GetTicks64() - start_time);

// we can reach this via SDL_QUIT or (not console::inMainLoop())
if (not finished_loading) {

spdlog::debug("Aborted loading after {}", duration);

// just exit immediately, without cleaning up, since than we would have to cancel the loading thread somehow, which is way rto complicated, let the OS clean up our mess we create her xD
std::exit(0);
}


spdlog::debug("Took {} to load", duration);

push_scene(scenes::create_scene(*this, SceneId::MainMenu, ui::FullScreenLayout{ *m_window }));
}

void Application::load_resources() {
Expand All @@ -292,7 +383,7 @@ void Application::load_resources() {
};
for (const auto& [font_id, path] : fonts) {
const auto font_path = utils::get_assets_folder() / "fonts" / path;
m_font_manager.load(font_id, font_path, fonts_size);
m_font_manager->load(font_id, font_path, fonts_size);
}
}

Expand Down
38 changes: 23 additions & 15 deletions src/application.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@ struct Application final : public EventListener, public ServiceProvider {
private:
static constexpr auto num_audio_channels = u8{ 2 };

bool m_is_running{ true };
CommandLineArguments m_command_line_arguments;
std::shared_ptr<Window> m_window;
Renderer m_renderer;
bool m_is_running{ true };
MusicManager m_music_manager;
std::shared_ptr<input::InputManager> m_input_manager;
SettingsManager m_settings_manager;
FontManager m_font_manager;
helper::optional<u32> m_target_framerate;

// these fields are initalized asynchronously in a separate thread
std::unique_ptr<MusicManager> m_music_manager;
std::shared_ptr<input::InputManager> m_input_manager;
std::unique_ptr<SettingsManager> m_settings_manager;
std::unique_ptr<FontManager> m_font_manager;


#ifdef DEBUG_BUILD
std::unique_ptr<ui::Label> m_fps_text{ nullptr };
Expand Down Expand Up @@ -74,29 +76,35 @@ struct Application final : public EventListener, public ServiceProvider {
}

FontManager& font_manager() override {
return m_font_manager;
return *m_font_manager;
}
const FontManager& font_manager() const override {
return m_font_manager;

[[nodiscard]] const FontManager& font_manager() const override {
return *m_font_manager;
}

CommandLineArguments& command_line_arguments() override {
return m_command_line_arguments;
}
const CommandLineArguments& command_line_arguments() const override {

[[nodiscard]] const CommandLineArguments& command_line_arguments() const override {
return m_command_line_arguments;
}

SettingsManager& settings_manager() override {
return m_settings_manager;
return *m_settings_manager;
}
const SettingsManager& settings_manager() const override {
return m_settings_manager;

[[nodiscard]] const SettingsManager& settings_manager() const override {
return *m_settings_manager;
}

MusicManager& music_manager() override {
return m_music_manager;
return *m_music_manager;
}
const MusicManager& music_manager() const override {
return m_music_manager;

[[nodiscard]] const MusicManager& music_manager() const override {
return *m_music_manager;
}

[[nodiscard]] const Renderer& renderer() const override {
Expand Down
2 changes: 1 addition & 1 deletion src/helper/clock_source.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace {
[[nodiscard]] double elapsed_time() {
return static_cast<double>(SDL_GetTicks()) / 1000.0;
return static_cast<double>(SDL_GetTicks64()) / 1000.0;
}

} // namespace
Expand Down
126 changes: 126 additions & 0 deletions src/scenes/loading_screen/loading_screen.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "loading_screen.hpp"
#include "game/graphic_helpers.hpp"
#include "game/tetromino_type.hpp"
#include "graphics/point.hpp"
#include "graphics/rect.hpp"
#include "graphics/renderer.hpp"
#include "graphics/window.hpp"
#include "helper/platform.hpp"
#include "manager/service_provider.hpp"
#include "scenes/logo/logo.hpp"
#include "ui/layout.hpp"

#include <numbers>

scenes::LoadingScreen::LoadingScreen(ServiceProvider* service_provider)
: m_segments{
{ Mino{ Mino::GridPoint{ 0, 0 }, helper::TetrominoType::J }, 1.0 },
{ Mino{ Mino::GridPoint{ 1, 0 }, helper::TetrominoType::L }, 1.0 },
{ Mino{ Mino::GridPoint{ 2, 0 }, helper::TetrominoType::I }, 1.0 },
{ Mino{ Mino::GridPoint{ 2, 1 }, helper::TetrominoType::O }, 1.0 },
{ Mino{ Mino::GridPoint{ 2, 2 }, helper::TetrominoType::S }, 1.0 },
{ Mino{ Mino::GridPoint{ 1, 2 }, helper::TetrominoType::T }, 1.0 },
{ Mino{ Mino::GridPoint{ 0, 2 }, helper::TetrominoType::I }, 1.0 },
{ Mino{ Mino::GridPoint{ 0, 1 }, helper::TetrominoType::Z }, 1.0 },
},m_logo{logo::get_logo(service_provider)} {

const auto [total_x_tiles, total_y_tiles] = utils::get_orientation() == utils::Orientation::Landscape
? std::pair<u32, u32>{ 17, 9 }
: std::pair<u32, u32>{ 9, 17 };

constexpr auto loading_segments_size = 3;

const auto& window = service_provider->window();

const auto layout = window.size();

const u32 tile_size_x = layout.x / total_x_tiles;
const u32 tile_size_y = layout.y / total_y_tiles;

m_tile_size = std::min(tile_size_y, tile_size_x);

const shapes::UPoint grid_start_offset = { (total_x_tiles - loading_segments_size) / 2,
(total_y_tiles - loading_segments_size) / 2 };

m_start_offset = grid_start_offset * m_tile_size;

constexpr const auto logo_width_percentage = 0.8;

constexpr const auto start_x = (1.0 - logo_width_percentage) / 2.0;

const auto window_ratio = static_cast<double>(layout.x) / static_cast<double>(layout.y);

const auto logo_ratio = static_cast<double>(logo::height) / static_cast<double>(logo::width) * window_ratio;

const auto logo_height_percentage = logo_width_percentage * logo_ratio;

m_logo_rect = ui::RelativeLayout(window, start_x, 0.05, logo_width_percentage, logo_height_percentage).get_rect();
}

namespace {
[[nodiscard]] double elapsed_time() {
return static_cast<double>(SDL_GetTicks64()) / 1000.0;
}
} // namespace


void scenes::LoadingScreen::update() {

constexpr const auto speed = std::numbers::pi_v<double> * 1.0;
constexpr const auto amplitude = 1.1;
constexpr const auto scale_offset = 1.3;

const auto length = m_segments.size();
const auto length_d = static_cast<double>(length);

const auto time = elapsed_time();

for (size_t i = 0; i < length; ++i) {

auto& segment = m_segments.at(i);

auto& scale = std::get<1>(segment);

const auto offset = std::numbers::pi_v<double> * 2.0 * static_cast<double>(length - i - 1) / length_d;

scale = std::min(amplitude * std::sin(time * speed + offset) + scale_offset, 1.0);
}
//
}

void scenes::LoadingScreen::render(const ServiceProvider& service_provider) const {

service_provider.renderer().draw_rect_filled(service_provider.window().screen_rect(), Color::black());

service_provider.renderer().draw_texture(m_logo, m_logo_rect);

constexpr const auto scale_threshold = 0.25;

for (const auto& [mino, scale] : m_segments) {
if (scale >= scale_threshold) {
const auto original_scale =
static_cast<double>(m_tile_size) / static_cast<double>(grid::original_tile_size);


const auto tile_size = static_cast<u32>(static_cast<double>(m_tile_size) * scale);

helper::graphics::render_mino(
mino, service_provider, MinoTransparency::Solid, original_scale,
[this, tile_size](const Mino::GridPoint& point) -> auto {
return this->to_screen_coords(point, tile_size);
},
{ tile_size, tile_size }
);
}

//TODO(Totto): render text here, but than we need to load the fonts before this, not in the loading thread (not that they take that long)
}
}


[[nodiscard]] shapes::UPoint scenes::LoadingScreen::to_screen_coords(const Mino::GridPoint& point, u32 tile_size)
const {
const auto start_edge = m_start_offset + point.cast<u32>() * m_tile_size;
const auto inner_offset = m_tile_size - tile_size / 2;
return start_edge + shapes::UPoint{ inner_offset, inner_offset };
}
Loading
Loading