670 lines
23 KiB
C++
670 lines
23 KiB
C++
/* ✨ +------------------------------------------------------+
|
|
____/ \____ ✨/| Open source game framework licensed to freely use, |
|
|
✨\ / / | copy, and modify. Created for 🌠dank.game🌠 |
|
|
+--\ . . /--+ | |
|
|
| ~/ ︶ \👍| | 🌐 https://open.shampoo.ooo/shampoo/spacebox |
|
|
| ~~~🌊~~~~🌊~ | +------------------------------------------------------+
|
|
| SPACE 🪐🅱 OX | /
|
|
| 🌊 ~ ~~~~ ~~ |/
|
|
+-------------*/
|
|
|
|
#include "Game.hpp"
|
|
|
|
Game::Game(std::initializer_list<std::string> configuration_merge)
|
|
{
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
|
|
/* Changing directory is necessary for loading external resources in a macOS app bundle. This may be useful on other platforms
|
|
* too, so it may make more sense to be configurable. */
|
|
#if defined(__MACOS__)
|
|
fs::current_path(SDL_GetBasePath());
|
|
#endif
|
|
|
|
/* Set custom log function that prints to stdout/stderr and to file if enabled. Temporarily set to DEBUG priority before loading
|
|
* user setting. */
|
|
SDL_LogSetOutputFunction(&Game::sdl_log_override, this);
|
|
SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_DEBUG);
|
|
|
|
/* Merge user configuration into the existing configuration and turn on auto refresh if it is enabled by the configuration. Merge
|
|
* configuration file paths passed into the contructor as well. */
|
|
_configuration.merge(user_config_path);
|
|
#ifdef __ANDROID__
|
|
_configuration.merge(configuration()("configuration", "android config path").get<std::string>());
|
|
#elif defined(__EMSCRIPTEN__)
|
|
_configuration.merge(configuration()("configuration", "wasm config path").get<std::string>());
|
|
#endif
|
|
for (const std::string& extra_configuration_path : configuration_merge)
|
|
{
|
|
_configuration.merge(extra_configuration_path);
|
|
}
|
|
if (configuration()("configuration", "auto refresh"))
|
|
{
|
|
_configuration.enable_auto_refresh(user_config_path);
|
|
}
|
|
|
|
/* Set the appropriate priority level for the default log category. Change it to VERBOSE if it is requested. Otherwise,
|
|
* change it to INFO if the user does not want debug statements, or leave it at DEBUG if not. */
|
|
if (_configuration("log", "verbose to stdout"))
|
|
{
|
|
SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_VERBOSE);
|
|
}
|
|
else if (!configuration()["log"]["debug-to-file"] && !configuration()["log"]["debug-to-stdout"])
|
|
{
|
|
SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, SDL_LOG_PRIORITY_INFO);
|
|
}
|
|
|
|
/* If recording is enabled by configuration, activate it. */
|
|
if (configuration()("recording", "enabled"))
|
|
{
|
|
recorder.animation.play();
|
|
}
|
|
|
|
/* Log the current working directory as seen by std::filesystem */
|
|
std::ostringstream log_message;
|
|
log_message << "Current path as seen by std::filesystem is " << fs::current_path();
|
|
sb::Log::log(log_message);
|
|
|
|
/* Load the key mappings into the input manager object */
|
|
input.load_key_map();
|
|
|
|
/* Log Android storage paths as determined by SDL */
|
|
#if defined(__ANDROID__) || defined(ANDROID)
|
|
log_message = std::ostringstream();
|
|
log_message << "Using Android SDK version " << SDL_GetAndroidSDKVersion() << std::endl;
|
|
log_message << "SDL_AndroidGetInternalStoragePath() is " << SDL_AndroidGetInternalStoragePath() << std::endl;
|
|
log_message << "SDL_AndroidGetExternalStorageState() is " << SDL_AndroidGetExternalStorageState() <<
|
|
" (1=read, 2=write, 3=r/w)" << std::endl;
|
|
log_message << "SDL_AndroidGetExternalStoragePath() is " << SDL_AndroidGetExternalStoragePath();
|
|
sb::Log::log(log_message);
|
|
#endif
|
|
|
|
/* Pretty print config JSON to debug log */
|
|
log_message = std::ostringstream();
|
|
log_message << std::setw(4) << configuration()() << std::endl;
|
|
sb::Log::log(log_message, sb::Log::DEBUG);
|
|
|
|
/* Tell SDL which render driver you will be requesting (not sure whether this only affects SDL_ GL */
|
|
SDL_SetHint(SDL_HINT_RENDER_DRIVER, configuration()["display"]["render driver"].get<std::string>().c_str());
|
|
|
|
/* Initialize the buffer of frame lengths which will be used to calculate and display FPS */
|
|
frame_length_history.reserve(5000);
|
|
|
|
/* Subscribe to events */
|
|
_delegate.subscribe(&Game::handle_quit_event, this, SDL_QUIT);
|
|
_delegate.subscribe(&Game::respond, this);
|
|
|
|
/* Needed for displaying fullscreen correctly on Linux (?) Also might need SDL_VIDEO_CENTERED (?) */
|
|
std::string fullscreen_env_assigment = "SDL_VIDEO_X11_LEGACY_FULLSCREEN=0";
|
|
putenv(const_cast<char*>(fullscreen_env_assigment.c_str()));
|
|
|
|
/* log compiled and linked SDL versions */
|
|
SDL_version version;
|
|
log_message = std::ostringstream();
|
|
SDL_VERSION(&version);
|
|
log_message << "compiled against SDL " << static_cast<int>(version.major) << "." << static_cast<int>(version.minor) <<
|
|
"." << static_cast<int>(version.patch) << std::endl;
|
|
SDL_GetVersion(&version);
|
|
log_message << "linked to SDL " << static_cast<int>(version.major) << "." << static_cast<int>(version.minor) << "." <<
|
|
static_cast<int>(version.patch);
|
|
sb::Log::log(log_message);
|
|
/* allows use of our own main function (?) see SDL_SetMainReady.html */
|
|
SDL_SetMainReady();
|
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER) < 0)
|
|
{
|
|
sb::Log::sdl_error("SDL could not initialize");
|
|
flag_to_end();
|
|
}
|
|
log_message = std::ostringstream();
|
|
|
|
/* Android and macOS do not use GLEW */
|
|
#if !defined(__ANDROID__) && !defined(ANDROID) && !defined(__MACOS__)
|
|
log_message << "GLEW " << glewGetString(GLEW_VERSION);
|
|
#endif
|
|
|
|
sb::Log::log(log_message.str());
|
|
glm::ivec2 window_size = _configuration("display", "dimensions").get<glm::ivec2>();
|
|
|
|
/* Set GL context attributes before creating a window (see SDL_GLattr.html). Don't ask Emscripten or Android for a specific GL
|
|
* context version. */
|
|
#if !defined(__EMSCRIPTEN__) && !defined(__ANDROID__) && !defined(ANDROID)
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, _configuration("gl", "major version"));
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, _configuration("gl", "minor version"));
|
|
#endif
|
|
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, _configuration("gl", "depth size"));
|
|
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, _configuration("gl", "red size"));
|
|
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, _configuration("gl", "green size"));
|
|
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, _configuration("gl", "blue size"));
|
|
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, _configuration("gl", "share with current context"));
|
|
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, _configuration("gl", "double buffer"));
|
|
|
|
/* Use compatibility mode on Windows because it seemed to be required on one version of Windows tested */
|
|
#if defined(__MINGW32__)
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY);
|
|
#elif defined(__MACOS__)
|
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
|
#endif
|
|
|
|
/* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */
|
|
Uint32 flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE;
|
|
if (_configuration("display", "fullscreen"))
|
|
{
|
|
flags |= SDL_WINDOW_FULLSCREEN;
|
|
}
|
|
_window = SDL_CreateWindow(_configuration("display", "title").get_ref<const std::string&>().c_str(), SDL_WINDOWPOS_CENTERED,
|
|
SDL_WINDOWPOS_CENTERED, window_size.x, window_size.y, flags);
|
|
if (_window == nullptr)
|
|
{
|
|
sb::Log::sdl_error("Could not create window");
|
|
flag_to_end();
|
|
}
|
|
|
|
/* Create a single GL context for the game. This is restrictive since it forces the game to use only one context, so it should
|
|
* be improved in the future. */
|
|
if ((glcontext = SDL_GL_CreateContext(window())) == nullptr)
|
|
{
|
|
sb::Log::sdl_error("Could not get GL context");
|
|
flag_to_end();
|
|
}
|
|
|
|
/* Clear window to black */
|
|
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
|
glClear(GL_COLOR_BUFFER_BIT);
|
|
SDL_GL_SwapWindow(_window);
|
|
|
|
/* Try setting vsync */
|
|
bool vsync_enabled = configuration()("display", "vsync").get<bool>();
|
|
if (SDL_GL_SetSwapInterval(static_cast<int>(vsync_enabled)) == 0)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Set vsync to " << vsync_enabled;
|
|
sb::Log::log(message);
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("Setting vysnc is not supported");
|
|
}
|
|
|
|
/* Initialize GLEW for GL function discovery on all platforms except Android and macOS */
|
|
#if !defined(__ANDROID__) && !defined(ANDROID) && !defined(__MACOS__)
|
|
GLenum error = glewInit();
|
|
if (error != GLEW_OK)
|
|
{
|
|
std::ostringstream message;
|
|
message << "GLEW could not initialize " << glewGetErrorString(error);
|
|
sb::Log::log(message, sb::Log::ERR);
|
|
}
|
|
#endif
|
|
|
|
/* Log the GL profile that was chosen */
|
|
log_gl_properties();
|
|
log_display_mode();
|
|
|
|
/* Show or hide cursor depending on the configuration */
|
|
SDL_ShowCursor(_configuration("display", "show cursor"));
|
|
|
|
/* Initialize SDL TTF font library */
|
|
if (TTF_Init() < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not initialize SDL ttf");
|
|
flag_to_end();
|
|
}
|
|
else
|
|
{
|
|
SDL_Log("initialized SDL ttf %d.%d.%d", SDL_TTF_MAJOR_VERSION, SDL_TTF_MINOR_VERSION, SDL_TTF_PATCHLEVEL);
|
|
}
|
|
|
|
/* Try to load the default font path. The font will be freed when the Game object is destroyed. */
|
|
_font = font(configuration()("display", "default font path").get<std::string>(), configuration()("display", "default font size"));
|
|
|
|
/* Initialize SDL mixer with OGG support (in addition to WAV) */
|
|
if (Mix_Init(MIX_INIT_OGG) == 0)
|
|
{
|
|
sb::Log::sdl_error("Could not initialize SDL mixer");
|
|
}
|
|
else
|
|
{
|
|
SDL_Log("initialized SDL mixer %d.%d.%d", SDL_MIXER_MAJOR_VERSION, SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL);
|
|
|
|
/* Log the available audio devices found on the system */
|
|
const int audio_device_count = SDL_GetNumAudioDevices(SDL_TRUE);
|
|
for (int ii = 0; ii < audio_device_count; ii++)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Found audio capture device " << ii << ": " << SDL_GetAudioDeviceName(ii, SDL_TRUE);
|
|
sb::Log::log(message);
|
|
}
|
|
|
|
/* Open the audio device chosen automatically by SDL mixer */
|
|
if (Mix_OpenAudio(_configuration("audio", "frequency"), MIX_DEFAULT_FORMAT, MIX_DEFAULT_CHANNELS,
|
|
_configuration("audio", "chunk size")) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not set up audio");
|
|
}
|
|
else
|
|
{
|
|
SDL_Log("Using audio driver: %s", SDL_GetCurrentAudioDriver());
|
|
}
|
|
}
|
|
|
|
#if SDL_BYTEORDER == SDL_BIG_ENDIAN
|
|
sb::Log::log("big endian");
|
|
#else
|
|
sb::Log::log("little endian");
|
|
#endif
|
|
last_frame_timestamp = SDL_GetTicks();
|
|
}
|
|
|
|
void Game::sdl_log_override(void* userdata, int category, SDL_LogPriority priority, const char* message)
|
|
{
|
|
Game* game = static_cast<Game*>(userdata);
|
|
std::ostream& out = (priority > SDL_LOG_PRIORITY_WARN) ? std::cerr : std::cout;
|
|
|
|
/* Print to stdout/stderr if priority is higher than debug or debug statements are enabled */
|
|
if (priority > SDL_LOG_PRIORITY_DEBUG || game->configuration()["log"]["debug-to-stdout"])
|
|
{
|
|
out << message << std::endl;
|
|
|
|
/* If printing to stdout, print to Android log as well */
|
|
#if defined(__ANDROID__) || defined(ANDROID)
|
|
__android_log_print(ANDROID_LOG_VERBOSE, game->configuration()["log"]["short-name"].get_ref<const std::string&>().c_str(),
|
|
"%s", message);
|
|
#endif
|
|
|
|
}
|
|
|
|
/* Create a date + time timestamp */
|
|
std::ostringstream timestamp;
|
|
std::time_t now = std::time(nullptr);
|
|
timestamp << std::put_time(std::localtime(&now), "%F %T ");
|
|
|
|
/* Handle writing to log file */
|
|
if (game->configuration()["log"]["enabled"])
|
|
{
|
|
fs::path path = game->configuration()("log", "output-directory").get<std::string>();
|
|
if (!fs::exists(path))
|
|
{
|
|
fs::create_directories(path);
|
|
}
|
|
|
|
/* Prepend timestamp to the message */
|
|
std::stringstream stamped_message;
|
|
stamped_message << timestamp.str() << message;
|
|
|
|
/* If debug is enabled, append message to debug log file */
|
|
if (game->configuration()["log"]["debug-to-file"])
|
|
{
|
|
fs::path debug_path = path / game->configuration()("log", "debug-file-name").get<std::string>();
|
|
std::ofstream debug_stream(debug_path, std::ios_base::app);
|
|
debug_stream << stamped_message.str() << std::endl;
|
|
}
|
|
|
|
/* Only append messages to the info log that are higher than debug priority */
|
|
if (priority > SDL_LOG_PRIORITY_DEBUG)
|
|
{
|
|
fs::path info_path = path / game->configuration()("log", "info-file-name").get<std::string>();
|
|
std::ofstream info_stream(info_path, std::ios_base::app);
|
|
info_stream << stamped_message.str() << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
void Game::print_frame_length_history()
|
|
{
|
|
for (float& frame_length : frame_length_history)
|
|
{
|
|
std::cout << frame_length << ", ";
|
|
}
|
|
std::cout << std::endl;
|
|
}
|
|
|
|
/* Create, compile and return the ID of a GL shader from the GLSL code at path */
|
|
GLuint Game::load_shader(const fs::path& path, GLenum type) const
|
|
{
|
|
GLuint shader = glCreateShader(type);
|
|
std::ostringstream message;
|
|
std::string contents = sb::file_to_string(path);
|
|
const char *c_str = contents.c_str();
|
|
glShaderSource(shader, 1, &c_str, 0);
|
|
glCompileShader(shader);
|
|
GLint is_compiled;
|
|
glGetShaderiv(shader, GL_COMPILE_STATUS, &is_compiled);
|
|
if (is_compiled == GL_TRUE)
|
|
{
|
|
message << "compiled shader at " << path;
|
|
sb::Log::log(message);
|
|
return shader;
|
|
}
|
|
else
|
|
{
|
|
/* log error by allocating a string to copy the GL error message buffer into */
|
|
std::string error_info;
|
|
GLint max_length;
|
|
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &max_length);
|
|
error_info.resize(max_length, 0);
|
|
glGetShaderInfoLog(shader, error_info.size(), nullptr, error_info.data());
|
|
message << "failed to compile " << path << ": " << error_info;
|
|
sb::Log::log(message, sb::Log::Level::ERR);
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
bool Game::link_shader(GLuint program) const
|
|
{
|
|
glLinkProgram(program);
|
|
int is_linked;
|
|
glGetProgramiv(program, GL_LINK_STATUS, (int *) &is_linked);
|
|
std::ostringstream message;
|
|
if (is_linked == GL_TRUE)
|
|
{
|
|
message << "linked shader program " << program;
|
|
sb::Log::log(message);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
/* log error by allocating a string to copy the GL error message buffer into */
|
|
std::string error_info;
|
|
GLint max_length;
|
|
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &max_length);
|
|
error_info.resize(max_length, 0);
|
|
glGetProgramInfoLog(program, error_info.size(), nullptr, error_info.data());
|
|
message << "failed linking shader program " << program << ": " << error_info;
|
|
sb::Log::log(message, sb::Log::Level::ERR);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void Game::log_display_mode() const
|
|
{
|
|
SDL_DisplayMode current;
|
|
std::ostringstream message;
|
|
for (int ii = 0; ii < SDL_GetNumVideoDisplays(); ii++)
|
|
{
|
|
int mode = SDL_GetCurrentDisplayMode(ii, ¤t);
|
|
if (mode != 0)
|
|
{
|
|
message << "Could not get display mode for video display #" << ii;
|
|
sb::Log::sdl_error(message.str());
|
|
}
|
|
else
|
|
{
|
|
if (ii > 0)
|
|
{
|
|
message << ", ";
|
|
}
|
|
message << "Display #" << ii << ": display mode is " << current.w << "x" << current.h << "px @ " <<
|
|
current.refresh_rate << "hz " << SDL_GetPixelFormatName(current.format);
|
|
}
|
|
}
|
|
sb::Log::log(message);
|
|
}
|
|
|
|
void Game::log_gl_properties() const
|
|
{
|
|
std::ostringstream message;
|
|
|
|
message << "OpenGL " << glGetString(GL_VERSION) << ", renderer " << glGetString(GL_RENDERER) << ", shading language " <<
|
|
glGetString(GL_SHADING_LANGUAGE_VERSION) << ", vendor " << glGetString(GL_VENDOR);
|
|
|
|
int attribute;
|
|
int status = SDL_GL_GetAttribute(SDL_GL_RED_SIZE, &attribute);
|
|
if (!status) {
|
|
message << ", SDL_GL_RED_SIZE: requested " << _configuration("gl", "red size") << ", got " << attribute;
|
|
} else {
|
|
sb::Log::sdl_error("Failed to get SDL_GL_RED_SIZE");
|
|
}
|
|
|
|
status = SDL_GL_GetAttribute(SDL_GL_GREEN_SIZE, &attribute);
|
|
if (!status) {
|
|
message << ", SDL_GL_GREEN_SIZE: requested " << _configuration("gl", "green size") << ", got " << attribute;
|
|
} else {
|
|
sb::Log::sdl_error("Failed to get SDL_GL_GREEN_SIZE");
|
|
}
|
|
|
|
status = SDL_GL_GetAttribute(SDL_GL_BLUE_SIZE, &attribute);
|
|
if (!status) {
|
|
message << ", SDL_GL_BLUE_SIZE: requested " << _configuration("gl", "blue size") << ", got " << attribute;
|
|
} else {
|
|
sb::Log::sdl_error("Failed to get SDL_GL_BLUE_SIZE");
|
|
}
|
|
|
|
status = SDL_GL_GetAttribute(SDL_GL_DEPTH_SIZE, &attribute);
|
|
if (!status) {
|
|
message << ", SDL_GL_DEPTH_SIZE: requested " << _configuration("gl", "depth size") << ", got " << attribute;
|
|
} else {
|
|
sb::Log::sdl_error("Failed to get SDL_GL_DEPTH_SIZE");
|
|
}
|
|
|
|
sb::Log::log(message);
|
|
|
|
/* This block fails on macOS */
|
|
#if !defined(__MACOS__)
|
|
std::ostringstream debug_message;
|
|
debug_message << "OpenGL extensions: " << glGetString(GL_EXTENSIONS);
|
|
sb::Log::log(message, sb::Log::DEBUG);
|
|
#endif
|
|
}
|
|
|
|
void Game::log_surface_format(SDL_Surface* surface, std::string preface)
|
|
{
|
|
SDL_PixelFormat* format = surface->format;
|
|
std::string pixel_format = SDL_GetPixelFormatName(format->format);
|
|
SDL_Log("%s bpp: %i mask: %i %i %i %i format: %s", preface.c_str(),
|
|
format->BytesPerPixel, format->Rmask, format->Gmask, format->Bmask,
|
|
format->Amask, pixel_format.c_str());
|
|
}
|
|
|
|
const Configuration& Game::configuration() const
|
|
{
|
|
return _configuration;
|
|
}
|
|
|
|
Configuration& Game::configuration()
|
|
{
|
|
return _configuration;
|
|
}
|
|
|
|
const sb::Delegate& sb::Game::delegate() const
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
sb::Delegate& sb::Game::delegate()
|
|
{
|
|
return _delegate;
|
|
}
|
|
|
|
const SDL_Window* Game::window() const
|
|
{
|
|
return _window;
|
|
}
|
|
|
|
SDL_Window* Game::window()
|
|
{
|
|
return _window;
|
|
}
|
|
|
|
const Input& Game::get_input() const
|
|
{
|
|
return input;
|
|
}
|
|
|
|
Input& Game::get_input()
|
|
{
|
|
return input;
|
|
}
|
|
|
|
std::shared_ptr<TTF_Font> sb::Game::font() const
|
|
{
|
|
return _font;
|
|
}
|
|
|
|
std::shared_ptr<TTF_Font> sb::Game::font(const fs::path& path, int size) const
|
|
{
|
|
std::shared_ptr<TTF_Font> font = std::shared_ptr<TTF_Font>(TTF_OpenFont(path.string().c_str(), size), TTF_CloseFont);
|
|
if (font.get() == nullptr)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not load " << path;
|
|
sb::Log::log(message, sb::Log::ERR);
|
|
if (path != configuration()("display", "default font path").get<std::string>())
|
|
{
|
|
return this->font();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Loaded " << path;
|
|
sb::Log::log(message);
|
|
}
|
|
return font;
|
|
}
|
|
|
|
void Game::run()
|
|
{
|
|
/* Any events received before this discarded. This prevents input from being entered before the game starts. Input is also suspended
|
|
* for a moment in case something is input as the screen is appearing. */
|
|
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
|
|
suppress_input_temporarily();
|
|
|
|
/* In Emscripten builds, use the browser's requestAnimationFrame to call each frame until the game is quit. */
|
|
#if defined(__EMSCRIPTEN__)
|
|
SDL_Log("using emscripten main loop");
|
|
emscripten_set_main_loop_arg(&loop, this, -1, true);
|
|
|
|
/* Other builds request a frame repeatedly until the game quits, delaying for the configured amount of milliseconds between each
|
|
* request. */
|
|
#else
|
|
SDL_Log("using standard main loop");
|
|
while (!done)
|
|
{
|
|
frame(SDL_GetTicks());
|
|
SDL_Delay(configuration()("display", "sdl delay"));
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void Game::respond(SDL_Event& event)
|
|
{
|
|
if (_delegate.compare(event, "screenshot"))
|
|
{
|
|
recorder.capture_screen();
|
|
}
|
|
else if (_delegate.compare(event, "record"))
|
|
{
|
|
recorder.toggle();
|
|
}
|
|
else if (_delegate.compare(event, "save current stash"))
|
|
{
|
|
recorder.grab_stash();
|
|
}
|
|
else if (_delegate.compare(event, "log video memory size"))
|
|
{
|
|
recorder.log_video_memory_size();
|
|
}
|
|
}
|
|
|
|
void Game::frame(float timestamp)
|
|
{
|
|
/* Max framerate is the maximum number of frames to display per second. It is unlimited if the value is -1. */
|
|
int max_framerate = configuration()("display", "max framerate").get<int>();
|
|
|
|
/* If max framerate is unlimited, always build a frame. Otherwise, check if enough milliseconds have passed since the last frame. */
|
|
if (max_framerate == -1 || timestamp - last_frame_timestamp >= 1000.0f / max_framerate)
|
|
{
|
|
/* Amount of time passed since the last timestamp was recorded in milliseconds. */
|
|
last_frame_length = timestamp - last_frame_timestamp;
|
|
|
|
/* Constrain the size of the frame length history and add the milliseconds between this frame and the last. */
|
|
if (frame_length_history.size() == 5000)
|
|
{
|
|
frame_length_history.pop_back();
|
|
}
|
|
frame_length_history.insert(frame_length_history.begin(), last_frame_length);
|
|
|
|
/* Save the timestamp */
|
|
last_frame_timestamp = timestamp;
|
|
|
|
/* Update game state and render frame (update and draw should be separated into different functions in a future revision to
|
|
* allow for different timing intervals for each) */
|
|
float timestamp_seconds = timestamp / 1000.0f;
|
|
recorder.update(timestamp_seconds);
|
|
_delegate.dispatch();
|
|
input.unsuppress_animation.update(timestamp_seconds);
|
|
update(timestamp_seconds);
|
|
_configuration.update(timestamp_seconds);
|
|
|
|
/* Update frame count per second for verbose log */
|
|
frame_count_this_second++;
|
|
if (timestamp - last_frame_count_timestamp >= 1000)
|
|
{
|
|
current_frames_per_second = frame_count_this_second;
|
|
last_frame_count_timestamp = timestamp;
|
|
frame_count_this_second = 0;
|
|
std::ostringstream message;
|
|
message << "Counted " << current_frames_per_second << " frames last second";
|
|
sb::Log::log(message, sb::Log::VERBOSE);
|
|
}
|
|
}
|
|
}
|
|
|
|
#if defined(__EMSCRIPTEN__)
|
|
|
|
void loop(void* context)
|
|
{
|
|
Game* game = static_cast<Game*>(context);
|
|
game->frame(emscripten_performance_now());
|
|
if (game->done)
|
|
{
|
|
emscripten_cancel_main_loop();
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
void Game::flag_to_end()
|
|
{
|
|
done = true;
|
|
}
|
|
|
|
void Game::handle_quit_event(SDL_Event &event)
|
|
{
|
|
if (event.type == SDL_QUIT)
|
|
{
|
|
flag_to_end();
|
|
}
|
|
}
|
|
|
|
void Game::quit()
|
|
{
|
|
/* Unsubscribe all delegate object's subscribers. */
|
|
_delegate.unsubscribe(&display);
|
|
_delegate.unsubscribe(&input);
|
|
_delegate.unsubscribe(this);
|
|
|
|
if (glcontext != nullptr)
|
|
{
|
|
SDL_GL_DeleteContext(glcontext);
|
|
}
|
|
if (_window != nullptr)
|
|
{
|
|
SDL_DestroyWindow(_window);
|
|
}
|
|
Mix_CloseAudio();
|
|
Mix_Quit();
|
|
SDL_Quit();
|
|
}
|
|
|
|
Game::~Game()
|
|
{
|
|
/* Delete font before quitting the TTF library so the font's shared pointer will successfully call its delete
|
|
* function in the TTF library. */
|
|
_font.reset();
|
|
if (TTF_WasInit())
|
|
{
|
|
TTF_Quit();
|
|
}
|
|
}
|