- add a text plane class

- load default font as a static class variable, make it a shared pointer
- add filter option to texture storage
- move Color into sb namespace
- move Delegate object into protected
This commit is contained in:
ohsqueezy 2023-05-31 15:06:29 -04:00
parent e5aef6ffb8
commit 9ed0e9ea71
14 changed files with 292 additions and 111 deletions

View File

@ -1,30 +1,40 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| SPACE ~~~~~ | /
| ~~~~~~~ BOX |/
+-------------*/
#include "Color.hpp"
Color::Color() : Color(0, 0, 0, 255) {}
sb::Color::Color() : sb::Color(0, 0, 0, 255) {}
Color::Color(const SDL_Color& color) : Color(color.r, color.g, color.b, color.a) {};
sb::Color::Color(const SDL_Color& color) : sb::Color(color.r, color.g, color.b, color.a) {};
void Color::set_percent(const float& red, const float& green, const float& blue)
void sb::Color::percent(float red, float green, float blue)
{
r = std::round(255.0f * red);
g = std::round(255.0f * green);
b = std::round(255.0f * blue);
}
void Color::set_percent(const float& red, const float& green, const float& blue, const float& alpha)
void sb::Color::percent(float red, float green, float blue, float alpha)
{
a = std::round(255.0f * alpha);
set_percent(red, green, blue);
sb::Color::percent(red, green, blue);
}
void Color::set_hsv(const float& hue, const float& saturation, const float& value)
void sb::Color::hsv(float hue, float saturation, float value)
{
float red_percent, green_percent, blue_percent;
HSVtoRGB(red_percent, green_percent, blue_percent, hue, saturation, value);
set_percent(red_percent, green_percent, blue_percent);
sb::Color::percent(red_percent, green_percent, blue_percent);
}
float Color::get_hue() const
float sb::Color::hue() const
{
float hue, saturation, value;
float red_percent = r / 255.0f, green_percent = g / 255.0f, blue_percent = b / 255.0f;
@ -32,17 +42,17 @@ float Color::get_hue() const
return hue;
}
void Color::shift_hue(float offset)
void sb::Color::shift_hue(float offset)
{
float hue, saturation, value;
float red_percent = r / 255.0f, green_percent = g / 255.0f, blue_percent = b / 255.0f;
RGBtoHSV(red_percent, green_percent, blue_percent, hue, saturation, value);
hue = std::fmod(hue + offset, 360.0f);
HSVtoRGB(red_percent, green_percent, blue_percent, hue, saturation, value);
set_percent(red_percent, green_percent, blue_percent);
percent(red_percent, green_percent, blue_percent);
}
Color::operator std::uint32_t() const
sb::Color::operator std::uint32_t() const
{
SDL_PixelFormat* format = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA32);
std::uint32_t pixel = SDL_MapRGBA(format, r, g, b, a);
@ -50,7 +60,7 @@ Color::operator std::uint32_t() const
return pixel;
}
Color::operator std::uint16_t() const
sb::Color::operator std::uint16_t() const
{
SDL_PixelFormat* format = SDL_AllocFormat(SDL_PIXELFORMAT_RGBA4444);
std::uint16_t pixel = SDL_MapRGBA(format, r, g, b, a);
@ -58,7 +68,7 @@ Color::operator std::uint16_t() const
return pixel;
}
Color::operator std::uint8_t() const
sb::Color::operator std::uint8_t() const
{
SDL_PixelFormat* format = SDL_AllocFormat(SDL_PIXELFORMAT_RGB332);
std::uint8_t pixel = SDL_MapRGBA(format, r, g, b, a);
@ -66,22 +76,22 @@ Color::operator std::uint8_t() const
return pixel;
}
bool Color::operator==(const Color& color) const
bool sb::Color::operator==(const sb::Color& color) const
{
return r == color.r && g == color.g && b == color.b && a == color.a;
}
bool Color::operator!=(const Color& color) const
bool sb::Color::operator!=(const sb::Color& color) const
{
return !(*this == color);
}
bool Color::operator<(const Color& color) const
bool sb::Color::operator<(const sb::Color& color) const
{
return r < color.r || g < color.g || b < color.b || a < color.a;
}
std::ostream& std::operator<<(std::ostream& out, const Color& color)
std::ostream& std::operator<<(std::ostream& out, const sb::Color& color)
{
float h, s, v;
RGBtoHSV(color.r / 255.0f, color.g / 255.0f, color.b / 255.0f, h, s, v);

View File

@ -1,3 +1,13 @@
/* +------------------------------------------------------+
____/ \____ /| - Open source game framework licensed to freely use, |
\ / / | copy, modify and sell without restriction |
+--\ ^__^ /--+ | |
| ~/ \~ | | - created for <https://foam.shampoo.ooo> |
| ~~~~~~~~~~~~ | +------------------------------------------------------+
| SPACE ~~~~~ | /
| ~~~~~~~ BOX |/
+-------------*/
#pragma once
#include <cstdint>
@ -10,12 +20,14 @@
struct Color : SDL_Color
{
public:
Color();
Color(const SDL_Color&);
void set_percent(const float&, const float&, const float&);
void set_percent(const float&, const float&, const float&, const float&);
void set_hsv(const float&, const float& = 1.0f, const float& = 1.0f);
float get_hue() const;
void percent(float, float, float);
void percent(float, float, float, float);
void hsv(float, float = 1.0f, float = 1.0f);
float hue() const;
void shift_hue(float);
operator std::uint32_t() const;
operator std::uint16_t() const;
@ -24,10 +36,10 @@ struct Color : SDL_Color
bool operator!=(const Color&) const;
bool operator<(const Color&) const;
template <typename T>
Color(T red, T green, T blue, T alpha = 255)
template <typename Type>
Color(Type red, Type green, Type blue, Type alpha = 255)
{
if (std::is_floating_point<T>())
if (std::is_floating_point<Type>())
{
red = std::round(red);
green = std::round(green);
@ -49,3 +61,11 @@ namespace std
{
std::ostream& operator<<(std::ostream&, const Color&);
}
/* Add Color class to the sb namespace. This should be the default location, but Color is left in the global namespace
* for backward compatibility.
*/
namespace sb
{
using ::Color;
}

View File

@ -28,12 +28,11 @@ void Configuration::set_defaults()
{"left", "left"},
{"pause", "enter"},
{"fullscreen", {"ALT", "enter"}},
{"toggle-framerate", {"CTRL", "f"}},
{"reset", {"CTRL", "r"}}
};
config["input"] = {
{"suppress-any-key-on-mods", true},
{"system-any-key-ignore-commands", {"fullscreen", "screenshot", "toggle-framerate", "record", "quit"}},
{"system-any-key-ignore-commands", {"fullscreen", "screenshot", "record", "quit"}},
{"any-key-ignore-commands", nlohmann::json::array()},
{"default-unsuppress-delay", 700},
{"ignore-repeat-keypress", true}
@ -46,7 +45,9 @@ void Configuration::set_defaults()
{"show-cursor", false},
{"render-test-spacing", 2},
{"render driver", "opengl"},
{"fluid resize", false}
{"fluid resize", false},
{"default font path", "BPmono.ttf"},
{"default font size", 16}
};
config["audio"] = {
{"default-sfx-root", "resource/sfx"},
@ -77,12 +78,6 @@ void Configuration::set_defaults()
{"max-video-memory", 1000},
{"mp4-pixel-format", "yuv444p"}
};
config["fps-indicator"] = {
{"width", .05},
{"height", .04},
{"background", {255, 255, 255}},
{"foreground", {0, 0, 0}}
};
config["animation"] = {
{"all-frames-frameset-name", "all"}
};

View File

@ -41,6 +41,9 @@ Game::Game()
log_message << "Current path as seen by std::filesystem is " << std::filesystem::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();
@ -158,10 +161,20 @@ Game::Game()
{
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)
/* Try to load the default font path. The font will be freed when the Game object is destroyed. */
bp_mono_font = std::shared_ptr<TTF_Font>(
TTF_OpenFont(configuration()["display"]["default font path"].get<std::string>().c_str(),
configuration()["display"]["default font size"]), TTF_CloseFont);
if (bp_mono_font.get() == nullptr)
{
sb::Log::log("Could not load BPmono.ttf", sb::Log::ERROR);
}
else
{
sb::Log::log("Loaded BPmono.ttf");
}
if (Mix_Init(MIX_INIT_OGG) == 0)
{
sb::Log::sdl_error("Could not initialize SDL mixer");
@ -479,6 +492,16 @@ 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;
@ -514,6 +537,11 @@ Audio& Game::get_audio()
return audio;
}
const std::shared_ptr<TTF_Font>& sb::Game::font()
{
return sb::Game::bp_mono_font;
}
void Game::run()
{
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
@ -550,7 +578,6 @@ void Game::frame(float ticks)
audio.update();
input.unsuppress_animation.update();
update();
framerate_indicator.update();
_configuration.update();
if (!is_gl_context)
{
@ -566,12 +593,12 @@ void Game::frame(float ticks)
frame_count_this_second++;
if (ticks - last_frame_count_timestamp >= 1000)
{
std::ostringstream message;
message << "Counted " << frame_count_this_second << " frames last second";
sb::Log::log(message, sb::Log::DEBUG);
framerate_indicator.refresh();
current_frames_per_second = frame_count_this_second;
last_frame_count_timestamp = ticks;
frame_count_this_second = 0;
std::ostringstream message;
message << "Counted " << current_frames_per_second << " frames last second";
sb::Log::log(message, sb::Log::DEBUG);
}
}
}
@ -636,7 +663,6 @@ void Game::quit()
}
if (TTF_WasInit())
{
TTF_CloseFont(bp_mono_font);
TTF_Quit();
}
Mix_CloseAudio();
@ -648,42 +674,3 @@ Game::~Game()
{
_delegate.unsubscribe(this);
}
FramerateIndicator::FramerateIndicator(Node* parent) : Sprite(parent)
{
delegate().subscribe(&FramerateIndicator::respond, this);
hide();
}
void FramerateIndicator::respond(SDL_Event& event)
{
if (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());
}
}

View File

@ -52,18 +52,6 @@
#include "filesystem.hpp"
#include "extension.hpp"
class FramerateIndicator : public Sprite
{
public:
FramerateIndicator(Node*);
void respond(SDL_Event&);
SDL_Surface* get_surface();
void refresh();
};
class Game : public Node
{
@ -73,6 +61,8 @@ private:
float frame_length = 1000.0 / 60.0;
SDL_Window* _window;
inline static std::shared_ptr<TTF_Font> bp_mono_font;
inline static const std::string USER_CONFIG_PATH = "config.json";
inline static const std::string ANDROID_CONFIG_PATH = "config_android.json";
inline static const std::string WASM_CONFIG_PATH = "config_wasm.json";
@ -92,6 +82,11 @@ private:
*/
static void sdl_log_override(void* userdata, int category, SDL_LogPriority priority, const char* message);
protected:
Configuration _configuration {this};
sb::Delegate _delegate {this};
public:
/* two-state enum equivalent to a boolean that can improve readability depending on the context */
@ -108,18 +103,14 @@ public:
SDL_Renderer* renderer = nullptr;
SDL_GLContext glcontext = nullptr;
int frame_count_this_second = 0, last_frame_length;
int frame_count_this_second = 0, last_frame_length, current_frames_per_second = 0;
float frame_time_overflow = 0, last_frame_timestamp, last_frame_count_timestamp;
bool done = false, show_framerate = true, is_gl_context = true;
Configuration _configuration {this};
sb::Delegate _delegate {this};
sb::Display display {this};
Recorder recorder {this};
Input input {this};
Audio audio {this};
std::vector<float> frame_length_history;
TTF_Font* bp_mono_font = nullptr;
FramerateIndicator framerate_indicator {this};
Game();
virtual void reset() { activate(); }
@ -144,6 +135,8 @@ public:
void log_surface_format(SDL_Surface*, std::string = "surface");
const Configuration& configuration() const;
Configuration& configuration();
const sb::Delegate& delegate() const;
sb::Delegate& delegate();
const SDL_Window* window() const;
SDL_Window* window();
const SDL_Renderer* get_renderer() const;
@ -151,6 +144,7 @@ public:
const Input& get_input() const;
Input& get_input();
Audio& get_audio();
static const std::shared_ptr<TTF_Font>& font();
void run();
void frame(float);
void flag_to_end();
@ -178,6 +172,14 @@ public:
};
/* Add Game class to the sb namespace. This should be the default location, but Game is left in the global namespace
* for backward compatibility.
*/
namespace sb
{
using ::Game;
}
#if defined(__EMSCRIPTEN__)
void loop(void*);

View File

@ -12,7 +12,6 @@
Input::Input(Node *parent) : Node(parent)
{
load_key_map();
get_delegate().subscribe(&Input::respond, this, SDL_KEYDOWN);
get_delegate().subscribe(&Input::respond, this, SDL_KEYUP);
for (KeyCombination& combination : key_map)

View File

@ -122,7 +122,7 @@ sb::Texture& sb::Model::texture(const std::string& name)
else if (textures().find(name) == textures().end())
{
std::ostringstream message;
message << "No texture named " << name << " found attached to this model.";
message << "No texture named " << name << " attached to this model.";
throw std::out_of_range(message.str());
}
else

View File

@ -48,9 +48,14 @@ Configuration& Node::configuration()
return get_root()->configuration();
}
const sb::Delegate& Node::delegate() const
{
return get_root()->delegate();
}
sb::Delegate& Node::delegate()
{
return get_root()->_delegate;
return get_root()->delegate();
}
sb::Delegate& Node::get_delegate()

View File

@ -52,6 +52,11 @@ public:
const Configuration& configuration() const;
Configuration& configuration();
/*!
* @return const reference to the Delegate object
*/
const sb::Delegate& delegate() const;
/*!
* @return the Delegate object
*/

90
src/Text.cpp Normal file
View File

@ -0,0 +1,90 @@
#include "Text.hpp"
using namespace sb;
/*!
* @param content text to be displayed
*/
void Text::content(const std::string& content)
{
_content = content;
refresh();
}
/*!
* @param foreground text color
*/
void Text::foreground(const sb::Color& foreground)
{
_foreground = foreground;
refresh();
}
/*!
* @param background text background color which fills the Plane
*/
void Text::background(const sb::Color& background)
{
_background = background;
refresh();
}
/*!
* @param font shared pointer to a TTF_Font
*/
void Text::font(std::shared_ptr<TTF_Font> font)
{
_font = font;
refresh();
}
void Text::refresh()
{
/* Try getting the font pointer from this class. If that hasn't been set, try getting the default font from the Game class.
* If neither has been set, print an error without generating a texture. */
TTF_Font* font = nullptr;
if (_font.get() != nullptr)
{
font = _font.get();
}
else if (sb::Game::font().get() != nullptr)
{
font = sb::Game::font().get();
}
/* Create pixel data using the SDL image library. The pixel data surface is converted from paletted format to ARGB and flipped. */
if (font != nullptr)
{
SDL_Surface* shaded = TTF_RenderText_Shaded(font, _content.c_str(), _foreground, _background);
if (!shaded)
{
sb::Log::sdl_error("Could not create text");
}
else
{
if (!(shaded = SDL_ConvertSurfaceFormat(shaded, SDL_PIXELFORMAT_ARGB8888, 0)))
{
sb::Log::sdl_error("Could not convert pixel format");
}
else
{
if (!(shaded = rotozoomSurfaceXY(shaded, 0, 1, -1, 0)))
{
sb::Log::sdl_error("Could not flip surface");
}
else
{
/* Generate texture and create storage. Load the pixels from the text rendering surface into the default texture. The texture object will handle
* destroying the previous texture. Then destroy the pixels surface. */
texture().generate({shaded->w, shaded->h}, GL_RGBA8, GL_LINEAR);
texture().load(shaded);
}
}
SDL_FreeSurface(shaded);
}
}
else
{
sb::Log::log("No font set and no default font found", sb::Log::ERROR);
}
}

64
src/Text.hpp Normal file
View File

@ -0,0 +1,64 @@
#include "SDL.h"
#include "SDL_ttf.h"
#include "SDL2_rotozoom.h"
#include "Model.hpp"
#include "Color.hpp"
namespace sb
{
class Text : public sb::Plane
{
private:
inline static const sb::Color DEFAULT_FG {0.0f, 0.0f, 0.0f, 255.0f};
inline static const sb::Color DEFAULT_BG {0.0f, 0.0f, 0.0f, 0.0f};
std::string _content;
sb::Color _foreground, _background;
std::shared_ptr<TTF_Font> _font;
/*!
* Set the texture to the appropriate SDL_Surface created by the SDL TTF library.
*/
void refresh();
public:
Text(const std::string& content = "", const sb::Color& foreground = DEFAULT_FG, const sb::Color& background = DEFAULT_BG) :
_content(content), _foreground(foreground), _background(background)
{
/* Assign a default constructed Texture object to be the default texture */
texture(sb::Texture());
}
Text(std::shared_ptr<TTF_Font> font, const std::string& content = "", const sb::Color& foreground = DEFAULT_FG,
const sb::Color& background = DEFAULT_BG) : Text(content, foreground, background)
{
_font = font;
}
/*!
* @param content text to be displayed
*/
void content(const std::string& content);
/*!
* @param foreground text color
*/
void foreground(const sb::Color& foreground);
/*!
* @param background text background color which fills the Plane
*/
void background(const sb::Color& background);
/*!
* @param font shared pointer to a TTF_Font
*/
void font(std::shared_ptr<TTF_Font> font);
};
}
#include "Game.hpp"

View File

@ -20,8 +20,6 @@ Texture::Texture(fs::path path) : Texture()
associate(path);
}
/* Store an image path as a member variable for loading later. Each texture should have one image
* path (support for multiple mipmap levels may be added later) */
void Texture::associate(fs::path path)
{
this->path = path;
@ -32,13 +30,13 @@ void Texture::generate()
GLObject::generate(glGenTextures);
}
void Texture::generate(glm::vec2 size, GLenum format)
void Texture::generate(glm::vec2 size, GLenum format, GLint filter)
{
generate();
bind();
glTexStorage2D(GL_TEXTURE_2D, 1, format, size.x, size.y);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter);
sb::Log::gl_errors();
}
@ -90,19 +88,17 @@ void Texture::load(SDL_RWops* rw)
void Texture::load(SDL_Surface* surface)
{
std::ostringstream message;
sb::Log::Level message_level;
if (surface->w > 0 && surface->h > 0)
{
message << "Loading image from SDL surface (" << surface->w << "×" << surface->h << ", " << SDL_GetPixelFormatName(surface->format->format) << ")";
sb::Log::log(message, sb::Log::DEBUG);
load(surface->pixels, {surface->w, surface->h}, GL_RGBA, GL_UNSIGNED_BYTE);
message_level = sb::Log::DEBUG;
}
else
{
message << "Cannot load into texture, invalid image data without dimensions found";
message_level = sb::Log::WARN;
sb::Log::log(message, sb::Log::WARN);
}
sb::Log::log(message, message_level);
}
void Texture::load(void* pixels, glm::vec2 size, GLenum format, GLenum type)

View File

@ -48,7 +48,14 @@ namespace sb
Texture();
Texture(fs::path);
void associate(fs::path);
/*!
* Store an image path as a member variable for loading later. Each texture should have one image
* path (support for multiple mipmap levels may be added later).
*
* @param path path to an image that can be loaded by the SDL image library
*/
void associate(fs::path path);
/*!
* Forward the GL texture generate function to the base class
@ -60,8 +67,9 @@ namespace sb
*
* @param size Width and height of the texture in texels
* @param format Sized internal format to be used to store texture data (for example, GL_RGBA8, GL_RGB8)
* @param filter Resize function to use (see glTexParameter)
*/
void generate(glm::vec2 size, GLenum format = GL_RGBA8);
void generate(glm::vec2 size, GLenum format = GL_RGBA8, GLint filter = GL_NEAREST);
/*!
* @overload load(fs::path path)

View File

@ -209,7 +209,7 @@ std::vector<SDL_Texture*> sb::get_portal_frames(
for (int ellipse_ii = 0, y = max_y; y > y_margin - 3; ellipse_ii++, y -= dy)
{
color.a = y / max_y * 255.0f;
color.set_hsv(hues[mod(ellipse_ii - frame_ii, count)]);
color.hsv(hues[mod(ellipse_ii - frame_ii, count)]);
aaFilledEllipseRGBA(renderer, size.x / 2, y, size.x / 2, y_margin - 3, color.r, color.g, color.b, color.a);
}
frames.push_back(frame);