spacebox/src/Game.cpp

707 lines
22 KiB
C++

#include "Game.hpp"
Game::Game()
{
/* Set the appropriate priority level for the default log category so either info level messages
* and higher are enabled or debug level messages are enabled, depending on the global configuration */
SDL_LogPriority default_log_category_priority;
if (configuration()["log"]["debug-to-file"] || configuration()["log"]["debug-to-stdout"])
{
default_log_category_priority = SDL_LOG_PRIORITY_DEBUG;
}
else
{
default_log_category_priority = SDL_LOG_PRIORITY_INFO;
}
SDL_LogSetPriority(sb::Log::DEFAULT_CATEGORY, default_log_category_priority);
/* set custom log function that prints to stdout/stderr and to file if enabled */
SDL_LogSetOutputFunction(&Game::sdl_log_override, this);
/* pretty print config to debug log */
std::ostringstream log_message;
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 when calling SDL_CreateRenderer */
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 FPS */
frame_length_history.reserve(5000);
set_framerate(configuration()["display"]["framerate"]);
delegate.subscribe(&Game::handle_quit_event, this, SDL_QUIT);
/* 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_AUDIO) < 0)
{
sb::Log::sdl_error("SDL could not initialize");
flag_to_end();
}
log_message = std::ostringstream();
log_message << "GLEW " << glewGetString(GLEW_VERSION);
sb::Log::log(log_message.str());
glm::ivec2 window_size = configuration()["display"]["dimensions"].get<glm::ivec2>();
/* Set these before creating a window (see SDL_GLattr.html) */
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
SDL_GL_SetAttribute(SDL_GL_SHARE_WITH_CURRENT_CONTEXT, 1);
/* Create a window with dimensions set in the config, centered, and flagged to be usable in OpenGL context */
_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, SDL_WINDOW_OPENGL);
if (_window == nullptr)
{
sb::Log::sdl_error("Could not create window");
flag_to_end();
}
/* Create an SDL renderer for clearing the screen to black and for logging renderer properties. Destroy renderer when finished.
* Skip this in emscripten because it causes a mouse event bug. */
#ifndef __EMSCRIPTEN__
if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr)
{
sb::Log::sdl_error("Could not create renderer");
flag_to_end();
}
else
{
int w, h;
/* log the renderer resolution */
SDL_GetRendererOutputSize(renderer, &w, &h);
log_message = std::ostringstream();
log_message << "renderer output size is " << w << "x" << h;
sb::Log::log(log_message);
/* clear screen to black */
SDL_SetRenderTarget(renderer, nullptr);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
SDL_RenderPresent(renderer);
SDL_RenderFlush(renderer);
SDL_DestroyRenderer(renderer);
}
#endif
SDL_ShowCursor(configuration()["display"]["show-cursor"]);
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);
}
if ((bp_mono_font = TTF_OpenFont("BPmono.ttf", 14)) == nullptr)
{
sb::Log::log("Could not load BPmono.ttf", sb::Log::ERROR);
}
if (Mix_Init(MIX_INIT_OGG) == 0)
{
sb::Log::sdl_error("Could not initialize SDL mixer");
// flag_to_end();
}
else
{
SDL_Log("initialized SDL mixer %d.%d.%d", SDL_MIXER_MAJOR_VERSION,
SDL_MIXER_MINOR_VERSION, SDL_MIXER_PATCHLEVEL);
}
// if (Mix_OpenAudio(MIX_DEFAULT_FREQUENCY, MIX_DEFAULT_FORMAT,
// MIX_DEFAULT_CHANNELS, 1024) < 0)
if (Mix_OpenAudio(11025, AUDIO_U8, MIX_DEFAULT_CHANNELS, 2048) < 0)
{
sb::Log::sdl_error("Could not set up audio");
}
SDL_Log("Using audio driver: %s", SDL_GetCurrentAudioDriver());
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);
}
audio.load_sfx();
audio.load_bgm();
#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::load_sdl_context()
{
if (glcontext != nullptr)
{
SDL_GL_DeleteContext(glcontext);
glcontext = nullptr;
}
SDL_RendererInfo renderer_info;
int render_driver_count = SDL_GetNumRenderDrivers();
SDL_Log("Render drivers:");
for (int ii = 0; ii < render_driver_count; ii++)
{
SDL_GetRenderDriverInfo(ii, &renderer_info);
log_renderer_info(renderer_info);
}
if ((renderer = SDL_CreateRenderer(_window, -1, SDL_RENDERER_TARGETTEXTURE | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC)) == nullptr)
{
sb::Log::sdl_error("Could not create renderer");
flag_to_end();
}
else
{
SDL_Log("Current renderer:");
SDL_GetRendererInfo(renderer, &renderer_info);
log_renderer_info(renderer_info);
SDL_Log("Renderer supports the use of render targets? %d", SDL_RenderTargetSupported(renderer));
}
is_gl_context = false;
log_display_mode();
}
void Game::load_gl_context()
{
if (renderer != nullptr)
{
SDL_DestroyRenderer(renderer);
renderer = nullptr;
}
if ((glcontext = SDL_GL_CreateContext(window())) == nullptr)
{
sb::Log::sdl_error("Could not get GL context");
flag_to_end();
}
/* try enabling vsync */
if (SDL_GL_SetSwapInterval(1) == 0)
{
sb::Log::log("enabled vsync");
}
else
{
sb::Log::log("vsync not supported");
}
GLenum error = glewInit();
std::ostringstream message;
if (error != GLEW_OK)
{
message << "GLEW could not initialize " << glewGetErrorString(error);
sb::Log::log(message, sb::Log::ERROR);
}
message << "OpenGL " << glGetString(GL_VERSION) << ", renderer " << glGetString(GL_RENDERER) << ", shading language " <<
glGetString(GL_SHADING_LANGUAGE_VERSION);
sb::Log::log(message);
is_gl_context = true;
log_display_mode();
}
/* Overrides SDL's default log function to log a message to stdout/stderr and, if log is enabled in the
* global configuration, to a file. Debug level statements may be suppressed, printed to stdout, or printed to
* both stdout and file, depending on the global configuration. */
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;
}
/* handle writing to log file */
if (game->configuration()["log"]["enabled"])
{
fs::path path = game->configuration()["log"]["output-directory"];
if (!fs::exists(path))
{
fs::create_directories(path);
}
/* prepend a timestamp to the message */
std::time_t now = std::time(nullptr);
std::stringstream stamped_message;
stamped_message << std::put_time(std::localtime(&now), "%F %T ") << 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"];
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"];
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::fstream file = std::fstream(path);
std::ostringstream message;
std::string contents = sb::file_to_string(path);
glShaderSource(shader, 1, reinterpret_cast<const GLchar**>(&contents), 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::ERROR);
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::ERROR);
return false;
}
}
void Game::log_renderer_info(SDL_RendererInfo& info)
{
std::ostringstream message;
message << "renderer name: " << info.name << ", flags: " << info.flags << ", texture formats: " <<
info.num_texture_formats << ", max texture w: " << info.max_texture_width << ", max texture h: " <<
info.max_texture_height;
sb::Log::log(message);
}
/* Write resolution, monitor refresh rate, and pixel format to the log. Code taken from SDL_GetCurrentDisplayMode.html
* on the SDL wiki */
void Game::log_display_mode()
{
SDL_DisplayMode current;
std::ostringstream message;
for (int ii = 0; ii < SDL_GetNumVideoDisplays(); ii++)
{
int mode = SDL_GetCurrentDisplayMode(ii, &current);
if (mode != 0)
{
message << "Could not get display mode for video display #" << ii;
sb::Log::sdl_error(message.str());
}
else
{
message << "Display #" << ii << ": display mode is " << current.w << "x" << current.h << "px @ " <<
current.refresh_rate << "hz " << get_pixel_format_string(current.format);
sb::Log::log(message);
}
}
}
void Game::log_surface_format(SDL_Surface* surface, std::string preface)
{
SDL_PixelFormat* format = surface->format;
std::string pixel_format = get_pixel_format_string(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());
}
std::string Game::get_pixel_format_string(Uint32 format)
{
std::string pixel_format;
if (format == SDL_PIXELFORMAT_UNKNOWN)
{
pixel_format = "SDL_PIXELFORMAT_UNKNOWN";
}
else if (format == SDL_PIXELFORMAT_INDEX1LSB)
{
pixel_format = "SDL_PIXELFORMAT_INDEX1LSB";
}
else if (format == SDL_PIXELFORMAT_INDEX1MSB)
{
pixel_format = "SDL_PIXELFORMAT_INDEX1MSB";
}
else if (format == SDL_PIXELFORMAT_INDEX4LSB)
{
pixel_format = "SDL_PIXELFORMAT_INDEX4LSB";
}
else if (format == SDL_PIXELFORMAT_INDEX4MSB)
{
pixel_format = "SDL_PIXELFORMAT_INDEX4MSB";
}
else if (format == SDL_PIXELFORMAT_INDEX8)
{
pixel_format = "SDL_PIXELFORMAT_INDEX8";
}
else if (format == SDL_PIXELFORMAT_RGB332)
{
pixel_format = "SDL_PIXELFORMAT_RGB332";
}
else if (format == SDL_PIXELFORMAT_RGB444)
{
pixel_format = "SDL_PIXELFORMAT_RGB444";
}
else if (format == SDL_PIXELFORMAT_RGB555)
{
pixel_format = "SDL_PIXELFORMAT_RGB555";
}
else if (format == SDL_PIXELFORMAT_BGR555)
{
pixel_format = "SDL_PIXELFORMAT_BGR555";
}
else if (format == SDL_PIXELFORMAT_ARGB4444)
{
pixel_format = "SDL_PIXELFORMAT_ARGB4444";
}
else if (format == SDL_PIXELFORMAT_RGBA4444)
{
pixel_format = "SDL_PIXELFORMAT_RGBA4444";
}
else if (format == SDL_PIXELFORMAT_ABGR4444)
{
pixel_format = "SDL_PIXELFORMAT_ABGR4444";
}
else if (format == SDL_PIXELFORMAT_BGRA4444)
{
pixel_format = "SDL_PIXELFORMAT_BGRA4444";
}
else if (format == SDL_PIXELFORMAT_ARGB1555)
{
pixel_format = "SDL_PIXELFORMAT_ARGB1555";
}
else if (format == SDL_PIXELFORMAT_RGBA5551)
{
pixel_format = "SDL_PIXELFORMAT_RGBA5551";
}
else if (format == SDL_PIXELFORMAT_ABGR1555)
{
pixel_format = "SDL_PIXELFORMAT_ABGR1555";
}
else if (format == SDL_PIXELFORMAT_BGRA5551)
{
pixel_format = "SDL_PIXELFORMAT_BGRA5551";
}
else if (format == SDL_PIXELFORMAT_RGB565)
{
pixel_format = "SDL_PIXELFORMAT_RGB565";
}
else if (format == SDL_PIXELFORMAT_BGR565)
{
pixel_format = "SDL_PIXELFORMAT_BGR565";
}
else if (format == SDL_PIXELFORMAT_RGB24)
{
pixel_format = "SDL_PIXELFORMAT_RGB24";
}
else if (format == SDL_PIXELFORMAT_BGR24)
{
pixel_format = "SDL_PIXELFORMAT_BGR24";
}
else if (format == SDL_PIXELFORMAT_RGB888)
{
pixel_format = "SDL_PIXELFORMAT_RGB888";
}
else if (format == SDL_PIXELFORMAT_RGBX8888)
{
pixel_format = "SDL_PIXELFORMAT_RGBX8888";
}
else if (format == SDL_PIXELFORMAT_BGR888)
{
pixel_format = "SDL_PIXELFORMAT_BGR888";
}
else if (format == SDL_PIXELFORMAT_BGRX8888)
{
pixel_format = "SDL_PIXELFORMAT_BGRX8888";
}
else if (format == SDL_PIXELFORMAT_ARGB8888)
{
pixel_format = "SDL_PIXELFORMAT_ARGB8888";
}
else if (format == SDL_PIXELFORMAT_RGBA8888)
{
pixel_format = "SDL_PIXELFORMAT_RGBA8888";
}
else if (format == SDL_PIXELFORMAT_ABGR8888)
{
pixel_format = "SDL_PIXELFORMAT_ABGR8888";
}
else if (format == SDL_PIXELFORMAT_BGRA8888)
{
pixel_format = "SDL_PIXELFORMAT_BGRA8888";
}
else if (format == SDL_PIXELFORMAT_ARGB2101010)
{
pixel_format = "SDL_PIXELFORMAT_ARGB2101010";
}
return pixel_format;
}
const nlohmann::json& Game::configuration() const
{
return _configuration.config;
}
nlohmann::json& Game::configuration()
{
return _configuration.config;
}
const SDL_Window* Game::window() const
{
return _window;
}
SDL_Window* Game::window()
{
return _window;
}
const SDL_Renderer* Game::get_renderer() const
{
return renderer;
}
SDL_Renderer* Game::get_renderer()
{
return renderer;
}
const Input& Game::get_input() const
{
return input;
}
Input& Game::get_input()
{
return input;
}
Audio& Game::get_audio()
{
return audio;
}
void Game::run()
{
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
suppress_input_temporarily();
#if defined(__EMSCRIPTEN__)
SDL_Log("using emscripten main loop");
emscripten_set_main_loop_arg(&loop, this, -1, true);
#else
SDL_Log("using standard main loop");
while (!done)
{
frame(SDL_GetTicks());
SDL_Delay(8);
}
#endif
}
void Game::frame(float ticks)
{
if (ticks - last_frame_timestamp + frame_time_overflow >= frame_length)
{
last_frame_length = ticks - last_frame_timestamp;
if (frame_length_history.size() == 5000)
{
frame_length_history.pop_back();
}
frame_length_history.insert(frame_length_history.begin(), last_frame_length);
frame_time_overflow = last_frame_length + frame_time_overflow - frame_length;
last_frame_timestamp = ticks;
if (last_frame_length < 1000)
{
recorder.update();
delegate.dispatch();
audio.update();
input.unsuppress_animation.update();
update();
framerate_indicator.update();
_configuration.update();
if (!is_gl_context)
{
SDL_SetRenderTarget(renderer, nullptr);
SDL_RenderPresent(renderer);
}
}
// if (frame_time_overflow > frame_length)
// {
// SDL_Log("%i frame(s) dropped", ((int) (frame_time_overflow / frame_length)));
// frame_time_overflow = 0;
// }
frame_count_this_second++;
if (ticks - last_frame_count_timestamp >= 1000)
{
framerate_indicator.refresh();
last_frame_count_timestamp = ticks;
frame_count_this_second = 0;
}
}
// std::cout << std::endl;
}
#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;
}
/* Set the length of a frame in seconds by passing the framerate, the amount of frames to display
* per second */
void Game::set_framerate(int framerate)
{
if (framerate < 1)
{
framerate = 1;
}
frame_length = 1000.0 / framerate;
}
/* Return the length of a frame in seconds */
float Game::get_frame_length() const
{
return frame_length;
}
void Game::handle_quit_event(SDL_Event &event)
{
if (event.type == SDL_QUIT)
{
flag_to_end();
}
}
void Game::quit()
{
if (glcontext != nullptr)
{
SDL_GL_DeleteContext(glcontext);
}
if (renderer != nullptr)
{
SDL_DestroyRenderer(renderer);
}
if (_window != nullptr)
{
SDL_DestroyWindow(_window);
}
if (TTF_WasInit())
{
TTF_CloseFont(bp_mono_font);
TTF_Quit();
}
Mix_CloseAudio();
Mix_Quit();
SDL_Quit();
}
Game::~Game()
{
get_delegate().unsubscribe(this);
}
FramerateIndicator::FramerateIndicator(Node* parent) : Sprite(parent)
{
get_delegate().subscribe(&FramerateIndicator::respond, this);
hide();
}
void FramerateIndicator::respond(SDL_Event& event)
{
if (get_delegate().compare(event, "toggle-framerate"))
{
toggle_hidden();
}
}
SDL_Surface* FramerateIndicator::get_surface()
{
std::string padded = sb::pad(get_root()->frame_count_this_second, 2);
SDL_Surface* shaded = TTF_RenderText_Shaded(
get_root()->bp_mono_font, padded.c_str(), {0, 0, 0, 255}, {255, 255, 255, 255});
if (!shaded)
{
sb::Log::sdl_error("Could not create text");
}
return shaded;
}
void FramerateIndicator::refresh()
{
if (!is_hidden() && get_root()->bp_mono_font != nullptr)
{
unload();
SDL_Surface* surface = get_surface();
SDL_Texture* texture = SDL_CreateTextureFromSurface(get_root()->get_renderer(), surface);
add_frames(texture);
SDL_FreeSurface(surface);
set_ne(get_display().window_box().ne());
}
}