← All examples
Rendering · Assets
Text Rendering
Fonts, sizes and color
Bakes the same font at three different pixel heights — each ASGE Font is rasterized at one fixed size, so multiple on-screen sizes means loading it multiple times, each with its own atlas texture.
A headline drawn alongside them continuously cycles through hue space, exercising per-draw color tinting on top of the baked glyph atlas.
No input — purely a visual reference for this subsystem.

Run it yourself
build just this example
cmake --build --preset windows-debug --target text_demo
# binary lands under bin/ (e.g. bin/Debug/text_demo.exe on Windows)
See Installation for the full build setup.
Source
examples/text_demo/Game.hpp
#pragma once
#include <ASGE/ASGE.hpp>
class TextDemoState final : public asge::game::state::IGameState<int>
{
// Each Font is baked at one fixed pixel height, so showing multiple
// sizes on screen means loading the font multiple times -- once per
// size -- each with its own atlas and uploaded texture.
std::unique_ptr<asge::media::Font> m_SmallFont;
std::unique_ptr<asge::media::Font> m_MediumFont;
std::unique_ptr<asge::media::Font> m_LargeFont;
std::unique_ptr<asge::video::ITexture> m_SmallAtlas;
std::unique_ptr<asge::video::ITexture> m_MediumAtlas;
std::unique_ptr<asge::video::ITexture> m_LargeAtlas;
float m_HueTime{0.0f}; // drives the color-cycling headline
void EnsureFontsLoaded(asge::video::IRenderer& inRenderer);
public:
[[nodiscard]] std::optional<asge::game::state::Transition<int>>
Update(float inDeltaTime, asge::input::InputState const& inInput) override;
void Render(asge::video::IRenderer& inRenderer) override;
void OnSystemEvent(asge::event::SystemEvent const& inSysEvent) override;
};
class TextDemoGame final : public asge::game::Game<int>
{
public:
explicit TextDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev);
protected:
[[nodiscard]] std::unique_ptr<StateType> CreateState(int inId) override;
};
examples/text_demo/Game.cpp
#include "Game.hpp"
#include <cmath>
#include <filesystem>
namespace
{
constexpr float HUE_CYCLE_SPEED = 0.6f; // full cycles per ~10 seconds
// Cheap HSV(hue in turns, full sat/value) -> RGB, just enough to cycle the
// headline's color smoothly without needing a real color-space library.
asge::graphics::RGBA_Color HueToColor(float inHueTurns)
{
float const h = (inHueTurns - std::floor(inHueTurns)) * 6.0f;
float const x = 1.0f - std::fabs(std::fmod(h, 2.0f) - 1.0f);
float r = 0.0f, g = 0.0f, b = 0.0f;
if (h < 1.0f) { r = 1.0f; g = x; b = 0.0f; }
else if (h < 2.0f) { r = x; g = 1.0f; b = 0.0f; }
else if (h < 3.0f) { r = 0.0f; g = 1.0f; b = x; }
else if (h < 4.0f) { r = 0.0f; g = x; b = 1.0f; }
else if (h < 5.0f) { r = x; g = 0.0f; b = 1.0f; }
else { r = 1.0f; g = 0.0f; b = x; }
return {
static_cast<std::uint8_t>(r * 255.0f),
static_cast<std::uint8_t>(g * 255.0f),
static_cast<std::uint8_t>(b * 255.0f),
255
};
}
std::unique_ptr<asge::media::Font> LoadFont(int inPixelHeight)
{
// ASGE_TEXT_DEMO_ASSET_DIR is injected by CMakeLists.txt.
auto const path = std::filesystem::path(ASGE_TEXT_DEMO_ASSET_DIR) / "PTSans-Regular.ttf";
auto fontResult = asge::media::Font::Load(path, inPixelHeight);
if (!fontResult)
{
fontResult.LogError();
return nullptr;
}
return std::make_unique<asge::media::Font>(std::move(fontResult).Value());
}
}
void TextDemoState::EnsureFontsLoaded(asge::video::IRenderer& inRenderer)
{
if (!m_SmallFont)
{
m_SmallFont = LoadFont(18);
if (m_SmallFont) m_SmallAtlas = inRenderer.CreateTexture(m_SmallFont->GetAtlasImage());
}
if (!m_MediumFont)
{
m_MediumFont = LoadFont(28);
if (m_MediumFont) m_MediumAtlas = inRenderer.CreateTexture(m_MediumFont->GetAtlasImage());
}
if (!m_LargeFont)
{
m_LargeFont = LoadFont(56);
if (m_LargeFont) m_LargeAtlas = inRenderer.CreateTexture(m_LargeFont->GetAtlasImage());
}
}
std::optional<asge::game::state::Transition<int>>
TextDemoState::Update(float inDeltaTime, [[maybe_unused]] asge::input::InputState const& inInput)
{
m_HueTime += inDeltaTime * (HUE_CYCLE_SPEED / 10.0f);
return std::nullopt;
}
void TextDemoState::Render(asge::video::IRenderer &inRenderer)
{
inRenderer.Clear({ 20, 20, 25, 255 });
EnsureFontsLoaded(inRenderer);
if (m_SmallFont && m_SmallAtlas)
{
inRenderer.DrawString(
"The quick brown fox jumps over the lazy dog.",
*m_SmallFont, *m_SmallAtlas, { 40.0f, 70.0f }, { 220, 220, 220, 255 });
inRenderer.DrawString(
"Font::GetAtlasImage() ->",
*m_SmallFont, *m_SmallAtlas, { 460.0f, 400.0f }, { 150, 150, 150, 255 });
}
if (m_MediumFont && m_MediumAtlas)
{
inRenderer.DrawString(
"DrawString renders glyphs from a baked atlas",
*m_MediumFont, *m_MediumAtlas, { 40.0f, 150.0f }, { 120, 200, 255, 255 });
}
if (m_LargeFont && m_LargeAtlas)
{
// SetColorMod is applied per DrawString call, not baked into the
// texture -- this line's color keeps changing every frame while
// reusing the exact same atlas texture as every other draw below.
inRenderer.DrawString(
"ASGE", *m_LargeFont, *m_LargeAtlas, { 40.0f, 280.0f }, HueToColor(m_HueTime));
// The atlas itself, drawn like any other texture -- every glyph
// DrawString just rendered lives somewhere in this packed bitmap.
inRenderer.DrawTexture(*m_LargeAtlas, asge::math::Rect{ 460.0f, 410.0f, 150.0f, 150.0f });
}
}
void TextDemoState::OnSystemEvent([[maybe_unused]] asge::event::SystemEvent const &inSysEvent)
{
}
TextDemoGame::TextDemoGame(asge::video::IRenderer& inRenderer, asge::audio::AudioDevice& inAudioDev)
: Game(inRenderer, inAudioDev)
{
SetInitialState(0);
}
std::unique_ptr<TextDemoGame::StateType> TextDemoGame::CreateState([[maybe_unused]] int inId)
{
return std::make_unique<TextDemoState>();
}