libdonut  2.3.2
Application framework for cross-platform game development in C++20
example_game.cpp

This example shows a basic game project consisting of a single source file. The main application class, ExampleGame, is defined at the top while the main function is defined at the bottom.

This can be used to study how various libdonut features are combined to form a working application. Note however that for a real project, the code that this example represents would typically be split across multiple files to make the main application file less cluttered.

The game uses the included examples/data/ folder as its main resource directory for all asset files that are loaded at runtime.

#include <donut/donut.hpp>
#include <array> // std::array
#include <charconv> // std::from_chars_result, std::from_chars
#include <chrono> // std::chrono_literals
#include <concepts> // std::integral
#include <cstddef> // std::size_t, std::byte, std::max_align_t
#include <cstdio> // stderr, std::sscanf, std::fprintf
#include <cstdlib> // EXIT_SUCCESS, EXIT_FAILURE
#include <exception> // std::exception
#include <fmt/format.h> // fmt::format
#include <forward_list> // std::forward_list
#include <optional> // std::optional
#include <span> // std::span
#include <stdexcept> // std::runtime_error
#include <string> // std::string
#include <string_view> // std::string_view
#include <system_error> // std::errc
#include <unordered_map> // std::unordered_map
namespace {
struct GameOptions {
app::ApplicationOptions applicationOptions{
.tickRate = 30.0f,
.maxFrameRate = 240.0f,
};
gfx::WindowOptions windowOptions{
.title = "Example Game",
.size{640, 480},
.resizable = true,
};
const char* mainMenuMusicFilepath = "sounds/music/donauwalzer.ogg";
float fieldOfView = 90.0f;
};
class ExampleGame final : public app::Application {
public:
ExampleGame(Filesystem& filesystem, const GameOptions& options)
: app::Application(options.applicationOptions)
, window(options.windowOptions)
, testTexture(gfx::Image{filesystem, "textures/test.png"})
, circleTexture(gfx::Image{filesystem, "textures/circle.png"}, {.useLinearFiltering = false, .useMipmap = false})
, carrotCakeModel(filesystem, "models/carrot_cake.obj")
, testSprite(spriteAtlas.insert(renderer, gfx::Image{filesystem, "textures/test.png"}))
, testSubSprite(spriteAtlas.createSubSprite(testSprite, 200, 200, 100, 100, gfx::SpriteAtlas::FLIP_HORIZONTALLY))
, mainFont(filesystem, "fonts/unscii/unscii-8.ttf")
, verticalFieldOfView(2.0f * atan((3.0f / 4.0f) * tan(radians(options.fieldOfView) * 0.5f))) {
loadBindingsConfiguration(filesystem, "configuration/bindings.json");
initializeSoundStage();
playMainMenuMusic(filesystem, options.mainMenuMusicFilepath);
loadCircles();
resize();
}
protected:
void update(app::FrameInfo frameInfo) override {
using namespace std::chrono_literals;
handleEvents();
if (soundStage) {
soundStage->update(frameInfo.deltaTime, listener);
}
if (inputManager.justPressed(events::Input::KEY_F10)) {
quit();
}
if (inputManager.justPressed(events::Input::KEY_F11) ||
(inputManager.justPressed(events::Input::KEY_RETURN) && (inputManager.isPressed(events::Input::KEY_LALT) || inputManager.isPressed(events::Input::KEY_RALT)))) {
window.setFullscreen(!window.isFullscreen());
}
if (inputManager.justPressed(events::Input::KEY_F2)) {
if (soundStage) {
soundStage->stopSound(musicId);
}
}
const float sprintInput = (inputManager.isPressed(Action::SPRINT)) ? 4.0f : 1.0f;
vec2 movementInput = inputManager.getAbsoluteValue(Action::MOVE_LEFT, Action::MOVE_RIGHT, Action::MOVE_DOWN, Action::MOVE_UP);
if (const float movementInputLengthSquared = length2(movementInput); movementInputLengthSquared > 1.0f) {
movementInput /= sqrt(movementInputLengthSquared);
}
const float carrotCakeSpeed = 2.0f * sprintInput;
carrotCakeVelocity = {movementInput * carrotCakeSpeed, 0.0f};
if (inputManager.isPressed(Action::CONFIRM)) {
const vec2 aimInput = inputManager.getRelativeValue(Action::AIM_LEFT, Action::AIM_RIGHT, Action::AIM_DOWN, Action::AIM_UP);
carrotCakeScale = clamp(carrotCakeScale + aimInput * 10.0f, 0.25f, 4.0f);
}
const float scrollInput = inputManager.getRelativeValue(Action::SCROLL_DOWN, Action::SCROLL_UP);
carrotCakeCurrentPosition.z -= scrollInput * 0.25f * sprintInput;
const bool triggerInput = inputManager.isPressed(Action::CANCEL);
counterA += timerA.countUpLoop(frameInfo.deltaTime, 1s, triggerInput);
counterB += timerB.countDownLoop(frameInfo.deltaTime, 1s, triggerInput);
}
void tick(app::TickInfo tickInfo) override {
carrotCakePreviousPosition = carrotCakeCurrentPosition;
carrotCakeCurrentPosition += carrotCakeVelocity * tickInfo.tickInterval;
}
void display(app::TickInfo /*tickInfo*/, app::FrameInfo frameInfo) override {
carrotCakeDisplayPosition = mix(carrotCakePreviousPosition, carrotCakeCurrentPosition, frameInfo.tickInterpolationAlpha);
uploadShaderData(frameInfo);
gfx::Framebuffer& framebuffer = window.getFramebuffer();
renderer.clearFramebufferColorAndDepth(framebuffer, Color::PURPLE * 0.25f);
alignas(std::max_align_t) std::array<std::byte, 1024> renderPassStorage;
{
gfx::RenderPass renderPass{renderPassStorage};
drawWorld3D(renderPass, frameInfo);
renderer.render(framebuffer, renderPass, worldViewport, worldCamera);
}
{
gfx::RenderPass renderPass{renderPassStorage};
drawWorld2D(renderPass, frameInfo);
renderer.render(framebuffer, renderPass, screenViewport, screenCamera, worldScissor);
}
{
gfx::RenderPass renderPass{renderPassStorage};
drawUserInterface(renderPass, frameInfo);
drawFrameRateCounter(renderPass);
renderer.render(framebuffer, renderPass, screenViewport, screenCamera);
}
window.present();
}
private:
enum class Action {
CONFIRM,
CANCEL,
MOVE_UP,
MOVE_DOWN,
MOVE_LEFT,
MOVE_RIGHT,
AIM_UP,
AIM_DOWN,
AIM_LEFT,
AIM_RIGHT,
SPRINT,
ATTACK,
SCROLL_UP,
SCROLL_DOWN,
};
struct ExampleShader2D : gfx::Shader2D {
static constexpr const char* FRAGMENT_SHADER_SOURCE_CODE = R"GLSL(
in vec2 fragmentTextureCoordinates;
in vec4 fragmentTintColor;
out vec4 outputColor;
uniform sampler2D textureUnit;
uniform float time;
void main() {
outputColor = fragmentTintColor * vec4(0.5 + 0.5 * cos(time), 0.5 + 0.5 * sin(time), 0.5 + 0.5 * sin(time + 1.5), 1.0) * texture(textureUnit, fragmentTextureCoordinates);
}
)GLSL";
ExampleShader2D()
.fragmentShaderSourceCode = FRAGMENT_SHADER_SOURCE_CODE,
}) {}
void setTime(float elapsedTime) {
program.setUniformFloat(time, elapsedTime);
}
private:
gfx::ShaderParameter time{program, "time"};
};
struct ExampleShader3D : gfx::Shader3D {
struct PointLight {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constantFalloff;
float linearFalloff;
float quadraticFalloff;
};
struct PointLightParameters {
PointLightParameters(const gfx::ShaderProgram& program, const char* name)
: position(program, fmt::format("{}.position", name).c_str())
, ambient(program, fmt::format("{}.ambient", name).c_str())
, diffuse(program, fmt::format("{}.diffuse", name).c_str())
, specular(program, fmt::format("{}.specular", name).c_str())
, constantFalloff(program, fmt::format("{}.constantFalloff", name).c_str())
, linearFalloff(program, fmt::format("{}.linearFalloff", name).c_str())
, quadraticFalloff(program, fmt::format("{}.quadraticFalloff", name).c_str()) {}
gfx::ShaderParameter constantFalloff;
gfx::ShaderParameter linearFalloff;
gfx::ShaderParameter quadraticFalloff;
};
static constexpr std::size_t POINT_LIGHT_COUNT = 4;
static constexpr const char* FRAGMENT_SHADER_SOURCE_CODE = R"GLSL(
#ifndef GAMMA
#define GAMMA 2.2
#endif
struct PointLight {
vec3 position;
vec3 ambient;
vec3 diffuse;
vec3 specular;
float constantFalloff;
float linearFalloff;
float quadraticFalloff;
};
in vec3 fragmentPosition;
in vec3 fragmentNormal;
in vec3 fragmentTangent;
in vec3 fragmentBitangent;
in vec2 fragmentTextureCoordinates;
in vec4 fragmentTintColor;
in vec3 fragmentSpecularFactor;
in vec3 fragmentEmissiveFactor;
out vec4 outputColor;
uniform sampler2D diffuseMap;
uniform sampler2D specularMap;
uniform sampler2D normalMap;
uniform sampler2D emissiveMap;
uniform vec3 diffuseColor;
uniform vec3 specularColor;
uniform vec3 normalScale;
uniform vec3 emissiveColor;
uniform float specularExponent;
uniform float dissolveFactor;
uniform float occlusionFactor;
uniform PointLight pointLights[POINT_LIGHT_COUNT];
uniform vec3 viewPosition;
uniform sampler2D tintTexture;
float lambert(float cosine) {
return max(cosine, 0.0);
}
float blinnPhong(vec3 normal, vec3 lightDirection, vec3 viewDirection) {
vec3 halfwayDirection = normalize(lightDirection + viewDirection);
return pow(max(dot(normal, halfwayDirection), 0.0), specularExponent);
}
vec3 calculatePointLight(PointLight light, vec3 normal, vec3 viewDirection, vec3 ambient, vec3 diffuse, vec3 specular) {
vec3 lightDifference = light.position - fragmentPosition;
float lightDistanceSquared = dot(lightDifference, lightDifference);
float lightDistance = sqrt(lightDistanceSquared);
vec3 lightDirection = lightDifference * (1.0 / lightDistance);
float cosine = dot(normal, lightDirection);
float diffuseFactor = lambert(cosine);
float specularFactor = blinnPhong(normal, lightDirection, viewDirection);
float attenuation = 1.0 / (light.constantFalloff + light.linearFalloff * lightDistance + light.quadraticFalloff * lightDistanceSquared);
vec3 ambientTerm = light.ambient * ambient;
vec3 diffuseTerm = light.diffuse * diffuseFactor * diffuse;
vec3 specularTerm = light.specular * specularFactor * specular;
const float visibility = 1.0;
return attenuation * (ambientTerm * occlusionFactor + (diffuseTerm + specularTerm) * visibility);
}
vec3 tonemap(vec3 color) {
return color / (color + vec3(1.0));
}
void main() {
vec4 sampledDiffuse = texture(diffuseMap, fragmentTextureCoordinates);
vec4 diffuse = fragmentTintColor * vec4(diffuseColor, 1.0 - dissolveFactor) * vec4(pow(sampledDiffuse.rgb, vec3(GAMMA)), sampledDiffuse.a);
vec3 specular = fragmentSpecularFactor * specularColor * texture(specularMap, fragmentTextureCoordinates).rgb;
vec3 emissive = fragmentEmissiveFactor * emissiveColor * texture(emissiveMap, fragmentTextureCoordinates).rgb;
mat3 TBN = mat3(normalize(fragmentTangent), normalize(fragmentBitangent), normalize(fragmentNormal));
vec3 surfaceNormal = normalScale * (texture(normalMap, fragmentTextureCoordinates).xyz * 2.0 - vec3(1.0));
vec3 normal = normalize(TBN * surfaceNormal);
vec3 viewDirection = normalize(viewPosition - fragmentPosition);
vec3 color = emissive;
for (uint i = uint(0); i < uint(POINT_LIGHT_COUNT); ++i) {
color += calculatePointLight(pointLights[i], normal, viewDirection, vec3(1.0), diffuse.rgb, specular);
}
color *= texture(tintTexture, fragmentTextureCoordinates).rgb;
outputColor = vec4(pow(tonemap(color), vec3(1.0 / GAMMA)), diffuse.a);
}
)GLSL";
ExampleShader3D()
.definitions = fmt::format("#define POINT_LIGHT_COUNT {}", POINT_LIGHT_COUNT).c_str(),
.fragmentShaderSourceCode = FRAGMENT_SHADER_SOURCE_CODE,
}) {}
void setPointLights(std::span<const PointLight, POINT_LIGHT_COUNT> values) {
for (std::size_t i = 0; i < POINT_LIGHT_COUNT; ++i) {
program.setUniformVec3(pointLights[i].position, values[i].position);
program.setUniformVec3(pointLights[i].ambient, values[i].ambient);
program.setUniformVec3(pointLights[i].diffuse, values[i].diffuse);
program.setUniformVec3(pointLights[i].specular, values[i].specular);
program.setUniformFloat(pointLights[i].constantFalloff, values[i].constantFalloff);
program.setUniformFloat(pointLights[i].linearFalloff, values[i].linearFalloff);
program.setUniformFloat(pointLights[i].quadraticFalloff, values[i].quadraticFalloff);
}
}
void setViewPosition(vec3 position) {
program.setUniformVec3(viewPosition, position);
}
void setTintTexture(const gfx::Texture* texture) {
program.setUniformSampler(tintTexture, texture);
}
private:
gfx::ShaderArray<PointLightParameters, POINT_LIGHT_COUNT> pointLights{program, "pointLights"};
gfx::ShaderParameter viewPosition{program, "viewPosition"};
gfx::ShaderParameter tintTexture{program, "tintTexture"};
};
void resize() {
constexpr ivec2 RENDER_RESOLUTION{640, 480};
constexpr ivec2 WORLD_VIEWPORT_POSITION{15, 15};
constexpr ivec2 WORLD_VIEWPORT_SIZE{380, 450};
const ivec2 size = window.getDrawableSize();
const auto [viewport, scale] = gfx::Viewport::createIntegerScaled(size, RENDER_RESOLUTION);
screenViewport = viewport;
.offset{0.0f, 0.0f},
.size = RENDER_RESOLUTION,
});
worldViewport = {
.position = screenViewport.position + WORLD_VIEWPORT_POSITION * scale,
.size = WORLD_VIEWPORT_SIZE * scale,
};
worldScissor = {.position = worldViewport.position, .size = worldViewport.size};
.verticalFieldOfView = verticalFieldOfView,
.aspectRatio = static_cast<float>(worldViewport.size.x) / static_cast<float>(worldViewport.size.y),
.nearZ = 0.1f,
.farZ = 100.0f,
});
}
void loadBindingsConfiguration(const Filesystem& filesystem, const char* filepath) {
const std::unordered_map<std::string_view, Action> actionsByIdentifier{
{"confirm", Action::CONFIRM},
{"cancel", Action::CANCEL},
{"move_up", Action::MOVE_UP},
{"move_down", Action::MOVE_DOWN},
{"move_left", Action::MOVE_LEFT},
{"move_right", Action::MOVE_RIGHT},
{"aim_up", Action::AIM_UP},
{"aim_down", Action::AIM_DOWN},
{"aim_left", Action::AIM_LEFT},
{"aim_right", Action::AIM_RIGHT},
{"sprint", Action::SPRINT},
{"attack", Action::ATTACK},
{"scroll_up", Action::SCROLL_UP},
{"scroll_down", Action::SCROLL_DOWN},
};
try {
json::StringParser{filesystem.openFile(filepath).readAllIntoString()}.parseObject(
json::onProperty([&](const json::SourceLocation&, const json::String& key, json::StringParser& parser) -> void {
if (const events::Input input = events::findInput(key); input != events::Input::UNKNOWN) {
const auto bindAction = [&](const json::SourceLocation&, const json::String& value) -> void {
if (const auto it = actionsByIdentifier.find(value); it != actionsByIdentifier.end()) {
inputManager.addBinding(input, it->second);
} else {
throw std::runtime_error{fmt::format("Invalid action identifier \"{}\".", value)};
}
};
parser.parseValue(json::onArray([&](const json::SourceLocation&, json::StringParser& parser) -> void { parser.parseArray(json::onString(bindAction)); }) |
json::onString(bindAction));
} else {
throw std::runtime_error{fmt::format("Invalid input identifier \"{}\".", key)};
}
}));
} catch (const json::Error& e) {
throw std::runtime_error{fmt::format("{}:{}:{}: {}", filepath, e.source.lineNumber, e.source.columnNumber, e.what())};
} catch (const std::exception& e) {
throw std::runtime_error{fmt::format("{}: {}", filepath, e.what())};
}
}
void initializeSoundStage() {
try {
soundStage.emplace();
} catch (const audio::Error& e) {
// Don't crash on failure, since the user might just not have a working sound card.
// Just print an error message instead.
std::fprintf(stderr, "%s\n", e.what());
}
}
void playMainMenuMusic(const Filesystem& filesystem, const char* filepath) {
using namespace std::chrono_literals;
if (soundStage) {
if (filesystem.fileExists(filepath)) {
music.emplace(filesystem, filepath,
.attenuationModel = audio::SoundAttenuationModel::NO_ATTENUATION,
.volume = 0.1f,
.listenerRelative = true,
.looping = true,
});
musicId = soundStage->createPausedSoundInBackground(*music);
soundStage->seekToSoundTime(musicId, 46700ms);
soundStage->resumeSound(musicId);
}
}
}
void loadCircles() {
constexpr Circle<float> CIRCLE_A{.center{60.0f, 80.0f}, .radius = 20.0f};
constexpr Circle<float> CIRCLE_B{.center{50.0f, 90.0f}, .radius = 20.0f};
constexpr Circle<float> CIRCLE_C{.center{60.0f, 120.0f}, .radius = 20.0f};
constexpr Circle<float> CIRCLE_D{.center{300.0f, 100.0f}, .radius = 10.0f};
constexpr Circle<float> CIRCLE_E{.center{200.0f, 180.0f}, .radius = 30.0f};
constexpr Circle<float> CIRCLE_F{.center{140.0f, 440.0f}, .radius = 20.0f};
quadtree[getAabbOf(CIRCLE_A)].push_front(CIRCLE_A);
quadtree[getAabbOf(CIRCLE_B)].push_front(CIRCLE_B);
quadtree[getAabbOf(CIRCLE_C)].push_front(CIRCLE_C);
quadtree[getAabbOf(CIRCLE_D)].push_front(CIRCLE_D);
quadtree[getAabbOf(CIRCLE_E)].push_front(CIRCLE_E);
quadtree[getAabbOf(CIRCLE_F)].push_front(CIRCLE_F);
}
void handleEvents() {
inputManager.prepareForEvents();
for (const events::Event& event : eventPump.pollEvents()) {
quit();
} else if (event.is<events::WindowSizeChangedEvent>()) {
resize();
}
inputManager.handleEvent(event);
}
}
void uploadShaderData(app::FrameInfo frameInfo) {
const ExampleShader3D::PointLight baseLight = {
.position = carrotCakeDisplayPosition,
.ambient{0.001f, 0.001f, 0.001f},
.diffuse{0.5f + 0.5f * sin(frameInfo.elapsedTime), 0.8f, 0.8f},
.specular{0.8f, 0.8f, 0.8f},
.constantFalloff = 1.0f,
.linearFalloff = 0.04f,
.quadraticFalloff = 0.03f,
};
const auto baseLightWithOffset = [&](vec3 offset) -> ExampleShader3D::PointLight {
ExampleShader3D::PointLight result = baseLight;
result.position += offset;
return result;
};
const std::array<ExampleShader3D::PointLight, ExampleShader3D::POINT_LIGHT_COUNT> pointLights{{
baseLightWithOffset({-2.0f, 0.0f, 0.0f}),
baseLightWithOffset({0.0f, -2.0f, 0.0f}),
baseLightWithOffset({0.0f, 2.0f, 0.0f}),
baseLightWithOffset({0.0f, 0.0f, 2.0f}),
}};
const vec3 viewPosition{0.0f, 0.0f, 0.0f};
exampleShader2D.setTime(frameInfo.elapsedTime);
exampleShader3D.setPointLights(pointLights);
exampleShader3D.setViewPosition(viewPosition);
exampleShader3D.setTintTexture(&testTexture);
}
void drawWorld3D(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
constexpr vec3 BACKGROUND_OFFSET{0.0f, 3.5f, -10.0f};
constexpr vec2 BACKGROUND_SCALE{9.0f, 9.0f};
constexpr float BACKGROUND_ANGLE = -30.0f;
constexpr float BACKGROUND_SPEED = 2.0f;
.shader = &exampleShader3D,
.model = gfx::Model::QUAD,
.diffuseMapOverride = &testTexture,
.transformation = translate(BACKGROUND_OFFSET) * //
orientate4(vec3{radians(BACKGROUND_ANGLE), 0.0f, 0.0f}) * //
scale(vec3{BACKGROUND_SCALE, 1.0f}),
.textureOffset{0.0f, frameInfo.elapsedTime * BACKGROUND_SPEED},
.textureScale = 1000.0f * BACKGROUND_SCALE / testTexture.getSize2D(),
});
.shader = &exampleShader3D,
.model = &carrotCakeModel,
.transformation = translate(vec3{-0.6f, 0.2f, -3.0f}) * //
scale(vec3{5.0f, 5.0f, 5.0f}) * //
orientate4(vec3{0.0f, frameInfo.elapsedTime * 1.5f, frameInfo.elapsedTime * 2.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});
.shader = &exampleShader3D,
.model = &carrotCakeModel,
.transformation = translate(vec3{-0.5f, -2.0f, -5.0f}) * //
scale(vec3{5.0f, 5.0f, 5.0f}) * //
orientate4(vec3{frameInfo.elapsedTime * 2.0f, frameInfo.elapsedTime * 1.5f, 0.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});
.transformation = translate(vec3{1.0f, -1.0f, -5.0f}) * //
scale(vec3{0.5f, 0.5, 0.5f}) * //
orientate4(vec3{frameInfo.elapsedTime * 2.0f, frameInfo.elapsedTime * 1.5f, 0.0f}),
.tintColor = Color::RED,
});
.model = &carrotCakeModel,
.transformation = translate(vec3{0.6f, 0.7f, -3.0f} + carrotCakeDisplayPosition) * //
scale(vec3{5.0f * carrotCakeScale.x, 5.0f * carrotCakeScale.y, 5.0f}) * //
orientate4(vec3{0.0f, frameInfo.elapsedTime * 1.5f, frameInfo.elapsedTime * 2.0f}) * //
translate(vec3{0.0f, -0.05f, 0.0f}),
});
}
void drawWorld2D(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
.shader = &exampleShader2D,
.texture = &testTexture,
.position{40.0f, 370.0f},
.size{180.0f, 70.0f},
.angle = frameInfo.elapsedTime,
.origin{0.5f, 0.5f},
.tintColor{1.0f, 1.0f, 1.0f, 0.2f},
});
.texture = &testTexture,
.position{100.0f, 380.0f},
.size{180.0f, 70.0f},
.angle = frameInfo.elapsedTime,
.origin{0.5f, 0.5f},
});
.shader = &exampleShader2D,
.texture = &testTexture,
.position{160.0f, 390.0f},
.size{180.0f, 70.0f},
.angle = frameInfo.elapsedTime,
.origin{0.5f, 0.5f},
.tintColor{1.0f, 1.0f, 1.0f, 0.2f},
});
}
void drawUserInterface(gfx::RenderPass& renderPass, const app::FrameInfo& frameInfo) {
.texture = &testTexture,
.position{200.0f + cos(frameInfo.elapsedTime) * 50.0f, 120.0f + sin(frameInfo.elapsedTime) * 50.0f},
.scale{0.2f + sin(frameInfo.elapsedTime) * 0.1f, 0.2f + cos(frameInfo.elapsedTime) * 0.1f},
.origin{0.5f, 0.5f},
});
.atlas = &spriteAtlas,
.id = testSprite,
.position{450.0f + cos(frameInfo.elapsedTime) * 50.0f, 120.0f + sin(frameInfo.elapsedTime) * 50.0f},
.scale{0.2f + sin(frameInfo.elapsedTime) * 0.1f, 0.2f + cos(frameInfo.elapsedTime) * 0.1f},
.origin{0.5f, 0.5f},
});
.atlas = &spriteAtlas,
.id = testSubSprite,
.position{450.0f + cos(frameInfo.elapsedTime) * 50.0f, 320.0f + sin(frameInfo.elapsedTime) * 50.0f},
.scale{0.2f + sin(frameInfo.elapsedTime) * 0.1f, 0.2f + cos(frameInfo.elapsedTime) * 0.1f},
.origin{0.5f, 0.5f},
});
renderPass.draw(gfx::TextInstance{
.text = &longTestText,
.position{410.0f, 416.0f},
.color = Color::LIME,
});
temporaryText.reshape(mainFont, 8,
fmt::format("Position:\n({:.2f}, {:.2f}, {:.2f})\n\nScale:\n({:.2f}, {:.2f})", carrotCakeDisplayPosition.x, carrotCakeDisplayPosition.y, carrotCakeDisplayPosition.z,
carrotCakeScale.x, carrotCakeScale.y));
.text = &temporaryText,
.position{410.0f, 310.0f},
});
if (inputManager.isPressed(Action::MOVE_UP) || inputManager.justPressed(Action::MOVE_UP)) {
renderPass.draw(gfx::TextInstance{.text = &upArrowText, .position{590.0f, 320.0f}});
}
if (inputManager.isPressed(Action::MOVE_DOWN) || inputManager.justPressed(Action::MOVE_DOWN)) {
renderPass.draw(gfx::TextInstance{.text = &downArrowText, .position{590.0f, 300.0f}});
}
if (inputManager.isPressed(Action::MOVE_LEFT) || inputManager.justPressed(Action::MOVE_LEFT)) {
renderPass.draw(gfx::TextInstance{.text = &leftArrowText, .position{580.0f, 310.0f}});
}
if (inputManager.isPressed(Action::MOVE_RIGHT) || inputManager.justPressed(Action::MOVE_RIGHT)) {
renderPass.draw(gfx::TextInstance{.text = &rightArrowText, .position{600.0f, 310.0f}});
}
if (inputManager.isPressed(Action::AIM_UP) || inputManager.justPressed(Action::AIM_UP)) {
renderPass.draw(gfx::TextInstance{.text = &upArrowText, .position{590.0f, 280.0f}});
}
if (inputManager.isPressed(Action::AIM_DOWN) || inputManager.justPressed(Action::AIM_DOWN)) {
renderPass.draw(gfx::TextInstance{.text = &downArrowText, .position{590.0f, 260.0f}});
}
if (inputManager.isPressed(Action::AIM_LEFT) || inputManager.justPressed(Action::AIM_LEFT)) {
renderPass.draw(gfx::TextInstance{.text = &leftArrowText, .position{580.0f, 270.0f}});
}
if (inputManager.isPressed(Action::AIM_RIGHT) || inputManager.justPressed(Action::AIM_RIGHT)) {
renderPass.draw(gfx::TextInstance{.text = &rightArrowText, .position{600.0f, 270.0f}});
}
.font = &mainFont,
.characterSize = 8,
.position{410.0f, 240.0f},
.string =
fmt::format("Timer A: {:.2f}\nCounter A: {}\n\nTimer B: {:.2f}\nCounter B: {}", static_cast<float>(timerA), counterA, static_cast<float>(timerB), counterB),
});
if (inputManager.isPressed(events::Input::KEY_SPACE)) {
constexpr Capsule<2, float> STATIC_CAPSULE{.centerLine{.pointA{80.0f, 80.0f}, .pointB{300.0f, 200.0f}}, .radius = 50.0f};
constexpr vec2 STATIC_CAPSULE_VECTOR = STATIC_CAPSULE.centerLine.pointB - STATIC_CAPSULE.centerLine.pointA;
const Circle<float> movingCircle{.center = vec2{200.0f, 50.0f} + vec2{carrotCakeDisplayPosition} * 50.0f, .radius = 32.0f};
const Color movingCircleColor = (intersects(movingCircle, STATIC_CAPSULE)) ? Color::RED : Color::YELLOW;
.texture = &circleTexture,
.position = STATIC_CAPSULE.centerLine.pointA,
.size{STATIC_CAPSULE.radius * 2.0f, STATIC_CAPSULE.radius * 2.0f},
.origin{0.5f, 0.5f},
.tintColor = Color::GREEN,
});
.texture = &circleTexture,
.position = STATIC_CAPSULE.centerLine.pointB,
.size{STATIC_CAPSULE.radius * 2.0f, STATIC_CAPSULE.radius * 2.0f},
.origin{0.5f, 0.5f},
.tintColor = Color::GREEN,
});
.position = STATIC_CAPSULE.centerLine.pointA,
.size{length(STATIC_CAPSULE_VECTOR), STATIC_CAPSULE.radius * 2.0f},
.angle = static_cast<float>(atan2(STATIC_CAPSULE_VECTOR.y, STATIC_CAPSULE_VECTOR.x)),
.origin{0.0f, 0.5f},
.tintColor = Color::GREEN,
});
.texture = &circleTexture,
.position = movingCircle.center,
.size{movingCircle.radius * 2.0f, movingCircle.radius * 2.0f},
.origin{0.5f, 0.5f},
.tintColor = movingCircleColor,
});
const auto drawBorder = [&](const Box<2, float>& box, float lineThickness, Color color) -> void {
const vec2 extent = box.max - box.min;
renderPass.draw(gfx::RectangleInstance{.position = box.min, .size{extent.x, lineThickness}, .origin{0.0f, 0.0f}, .tintColor = color});
renderPass.draw(gfx::RectangleInstance{.position{box.min.x, box.max.y}, .size{extent.x, lineThickness}, .origin{0.0f, 1.0f}, .tintColor = color});
renderPass.draw(gfx::RectangleInstance{.position = box.min, .size{lineThickness, extent.y}, .origin{0.0f, 0.0f}, .tintColor = color});
renderPass.draw(gfx::RectangleInstance{.position{box.max.x, box.min.y}, .size{lineThickness, extent.y}, .origin{1.0f, 0.0f}, .tintColor = color});
};
quadtree.traverseActiveNodes([&](const Box<2, float>& looseBounds, const std::forward_list<Circle<float>>* circles) -> void {
drawBorder(looseBounds, 2.0f, Color::BLANCHED_ALMOND);
if (circles) {
for (const Circle<float>& circle : *circles) {
.texture = &circleTexture,
.position = circle.center,
.size{circle.radius * 2.0f, circle.radius * 2.0f},
.origin{0.5f, 0.5f},
.tintColor = Color::BLUE,
});
}
}
});
const Box<2, float> movingCircleAabb = getAabbOf(movingCircle);
std::size_t aabbTestCount = 0;
std::size_t circleTestCount = 0;
quadtree.traverseActiveNodes(
[&](const Box<2, float>& looseBounds, const std::forward_list<Circle<float>>* circles) -> void {
drawBorder(looseBounds, 2.0f, Color::DARK_BLUE);
if (circles) {
for (const Circle<float>& circle : *circles) {
++circleTestCount;
if (intersects(circle, movingCircle)) {
.texture = &circleTexture,
.position = circle.center,
.size{circle.radius * 2.0f, circle.radius * 2.0f},
.origin{0.5f, 0.5f},
.tintColor = Color::DARK_GOLDEN_ROD,
});
}
}
}
},
[&](const Box<2, float>& looseBounds) -> bool {
++aabbTestCount;
return intersects(movingCircleAabb, looseBounds);
});
.font = &mainFont,
.characterSize = 8,
.position{410.0f, 450.0f},
.string = fmt::format("AABB tests: {}\nCircle tests: {}", aabbTestCount, circleTestCount),
});
}
if (inputManager.justReleased(events::Input::KEY_SPACE)) {
inputManager.resetAllInputs();
}
}
void drawFrameRateCounter(gfx::RenderPass& renderPass) {
const unsigned fps = getLastSecondFrameCount();
temporaryText.reshape(mainFont, 8, fmt::format("FPS: {}", fps), {0.0f, 0.0f}, {2.0f, 2.0f});
const vec2 fpsPosition{15.0f + 2.0f, 480.0f - 15.0f - 20.0f};
const Color fpsColor = (fps < 60) ? Color::RED : (fps < 120) ? Color::YELLOW : (fps < 240) ? Color::GRAY : Color::LIME;
renderPass.draw(gfx::TextCopyInstance{.text = &temporaryText, .position = fpsPosition + vec2{1.0f, -1.0f}, .color = Color::BLACK});
renderPass.draw(gfx::TextCopyInstance{.text = &temporaryText, .position = fpsPosition, .color = fpsColor});
}
events::EventPump eventPump{};
gfx::Window window;
gfx::Renderer renderer{};
gfx::Viewport screenViewport{};
gfx::Viewport worldViewport{};
Rectangle<int> worldScissor{};
gfx::Camera screenCamera{};
gfx::Camera worldCamera{};
audio::Listener listener{};
gfx::SpriteAtlas spriteAtlas{};
gfx::Texture testTexture;
gfx::Texture circleTexture;
gfx::Model carrotCakeModel;
gfx::SpriteAtlas::SpriteId testSprite;
gfx::SpriteAtlas::SpriteId testSubSprite;
gfx::Font mainFont;
gfx::Text longTestText{mainFont, 8,
"The quick brown fox\n"
"jumps over the lazy dog\n"
"\n"
"FLYGANDE BÄCKASINER SÖKA\n"
"HWILA PÅ MJUKA TUVOR QXZ\n"
"0123456789\n"
"\n"
"+!\"#%&/()=?`@${[]}\\\n"
"~\'<>|,.-;:_"};
gfx::Text upArrowText{mainFont, 8, "^"};
gfx::Text downArrowText{mainFont, 8, "v"};
gfx::Text leftArrowText{mainFont, 8, "<"};
gfx::Text rightArrowText{mainFont, 8, ">"};
gfx::Text temporaryText{};
ExampleShader2D exampleShader2D{};
ExampleShader3D exampleShader3D{};
events::InputManager inputManager{};
std::optional<audio::SoundStage> soundStage{};
std::optional<audio::Sound> music{};
float verticalFieldOfView;
vec3 carrotCakeCurrentPosition{0.0f, 0.0f, 0.0f};
vec3 carrotCakePreviousPosition{0.0f, 0.0f, 0.0f};
vec3 carrotCakeDisplayPosition{0.0f, 0.0f, 0.0f};
vec2 carrotCakeScale{1.0f, 1.0f};
vec3 carrotCakeVelocity{0.0f, 0.0f, 0.0f};
Time<float> timerA{};
Time<float> timerB{};
std::size_t counterA = 0;
std::size_t counterB = 0;
LooseQuadtree<std::forward_list<Circle<float>>> quadtree{
Box<2, float>{.min{15.0f, 15.0f}, .max{15.0f + 380.0f, 15.0f + 450.0f}},
vec2{32.0f, 32.0f},
};
};
class OptionsParser {
public:
OptionsParser(int argc, char* argv[]) noexcept
: argumentCount(argc)
, arguments(argv) {
assert(argc > 0);
}
[[nodiscard]] Variant<GameOptions, std::string> parseGameOptions() {
GameOptions options{};
while (argumentIndex < argumentCount) {
const std::string_view argument{arguments[argumentIndex]};
if (argument == "-help" || argument == "--help" || argument == "-?" || argument == "/?") {
return "Options:\n"
" -help Show this information.\n"
" -title <string> Title of the main window.\n"
" -width <pixels> Width of the main window.\n"
" -height <pixels> Height of the main window.\n"
" -resizable Enable window resizing.\n"
" -fullscreen Enable fullscreen.\n"
" -vsync Enable vertical synchronization.\n"
" -min-fps <Hz> Minimum frame rate before slowdown.\n"
" -max-fps <Hz> Frame rate limit. 0 = unlimited.\n"
" -msaa <level> Level of multisample anti-aliasing.\n"
" -main-menu-music <filepath> Music file to use for the main menu.\n"
" -fov <degrees> Field of view for world rendering.";
}
if (argument == "-title") {
parseOptionValue("title", options.windowOptions.title);
} else if (argument == "-width") {
parseOptionValue("width", options.windowOptions.size.x);
} else if (argument == "-height") {
parseOptionValue("height", options.windowOptions.size.y);
} else if (argument == "-resizable") {
parseOptionValue("resizable", options.windowOptions.resizable);
} else if (argument == "-fullscreen") {
parseOptionValue("fullscreen", options.windowOptions.fullscreen);
} else if (argument == "-vsync") {
parseOptionValue("vsync", options.windowOptions.vSync);
} else if (argument == "-min-fps") {
parseOptionValue("min fps", options.applicationOptions.minFrameRate);
} else if (argument == "-max-fps") {
parseOptionValue("max fps", options.applicationOptions.maxFrameRate);
} else if (argument == "-msaa") {
parseOptionValue("msaa", options.windowOptions.msaaLevel);
} else if (argument == "-main-menu-music") {
parseOptionValue("main menu music file", options.mainMenuMusicFilepath);
} else if (argument == "-fov") {
parseOptionValue("fov", options.fieldOfView);
} else {
throw std::runtime_error{fmt::format("Unknown option {}. Try -help.", argument)};
}
++argumentIndex;
}
return options;
}
private:
void parseOptionValue(std::string_view optionName, const char*& output) {
if (++argumentIndex >= argumentCount) {
throw std::runtime_error{fmt::format("Missing {} value.", optionName)};
}
output = arguments[argumentIndex];
}
void parseOptionValue(std::string_view optionName, std::integral auto& output) {
if (++argumentIndex >= argumentCount) {
throw std::runtime_error{fmt::format("Missing {} value.", optionName)};
}
const std::string_view string{arguments[argumentIndex]};
if (const std::from_chars_result result = std::from_chars(string.data(), string.data() + string.size(), output); result.ec != std::errc{}) {
throw std::runtime_error{fmt::format("Invalid {} value \"{}\": {}", optionName, string, std::make_error_code(result.ec).message())};
}
}
void parseOptionValue(std::string_view optionName, float& output) {
if (++argumentIndex >= argumentCount) {
throw std::runtime_error{fmt::format("Missing {} value.", optionName)};
}
if (std::sscanf(arguments[argumentIndex], "%f", &output) != 1) {
throw std::runtime_error{fmt::format("Invalid {} value \"{}\".", optionName, arguments[argumentIndex])};
}
}
void parseOptionValue(std::string_view /*optionName*/, bool& output) {
output = true;
}
const int argumentCount;
char** const arguments;
int argumentIndex = 1;
};
} // namespace
int main(int argc, char* argv[]) {
try {
Filesystem filesystem{argv[0],
FilesystemOptions{
.organizationName = "Donut",
.applicationName = "ExampleGame",
.dataDirectory = "data",
.archiveSearchPath = ".",
.archiveSearchFileExtension = "pk3",
}};
const Variant<GameOptions, std::string> options = OptionsParser{argc, argv}.parseGameOptions();
if (options.is<std::string>()) {
std::fprintf(stderr, "%s\n", options.as<std::string>().c_str());
return EXIT_SUCCESS;
}
ExampleGame game{filesystem, options.as<GameOptions>()};
game.run();
} catch (const std::exception& e) {
std::fprintf(stderr, "%s\n", e.what());
events::MessageBox::show(events::MessageBox::Type::ERROR_MESSAGE, "Error", e.what());
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
static const Color BURLY_WOOD
Definition: Color.hpp:26
static const Color LIME
Definition: Color.hpp:96
static const Color YELLOW
Definition: Color.hpp:160
static const Color BLUE
Definition: Color.hpp:23
static const Color GREEN
Definition: Color.hpp:69
static const Color DARK_GOLDEN_ROD
Definition: Color.hpp:37
Main application base class.
Definition: Application.hpp:131
unsigned getLastSecondFrameCount() const noexcept
Get the number of frames displayed during the last measured second of the application's run time,...
Definition: Application.hpp:217
virtual void quit()
Initiate the shutdown process, meaning that the current frame will be the last to be processed and di...
virtual void update(FrameInfo frameInfo)
Per-frame update callback, called in the main loop once at the beginning of each frame,...
Definition: Application.hpp:304
virtual void tick(TickInfo tickInfo)
Fixed-rate tick callback, called in the main loop 0 or more times during tick processing,...
Definition: Application.hpp:328
virtual void display(TickInfo tickInfo, FrameInfo frameInfo)
Frame rendering callback, called in the main loop once at the end of each frame after processing tick...
Definition: Application.hpp:354
unsigned SoundInstanceId
Opaque handle to a specific instance of a sound in the sound stage.
Definition: SoundStage.hpp:61
Persistent system for polling Event data and other user input from the host environment on demand.
Definition: EventPump.hpp:23
Persistent system for mapping physical Input controls to abstract output numbers and processing input...
Definition: InputManager.hpp:129
static void show(Type type, const char *title, const char *message)
Display a simple message box that blocks execution on the current thread until the user presses OK.
Combined view-projection matrix, defining the perspective for a Renderer to render from.
Definition: Camera.hpp:53
static Camera createOrthographic(const CameraOrthographicOptions &options, const mat4 &viewMatrix) noexcept
Create a camera with an orthographic projection.
Definition: Camera.hpp:63
static Camera createPerspective(const CameraPerspectiveOptions &options, const mat4 &viewMatrix) noexcept
Create a camera with a perspective projection.
Definition: Camera.hpp:100
Typeface describing an assortment of character glyphs that may be rendered on-demand into an expandin...
Definition: Font.hpp:42
Unique resource handle with exclusive ownership of a GPU framebuffer.
Definition: Framebuffer.hpp:15
Container for a 2D image.
Definition: Image.hpp:359
Graphics drawing queue for batch rendering using a Renderer.
Definition: RenderPass.hpp:718
RenderPass & draw(const ModelInstance &model)
Enqueue a ModelInstance to be drawn when the render pass is rendered.
Persistent system for rendering the batched draw commands of a RenderPass onto a Framebuffer,...
Definition: Renderer.hpp:38
ShaderProgram specialized for rendering TexturedQuad instances in 2D.
Definition: Shader2D.hpp:29
static const char *const VERTEX_SHADER_SOURCE_CODE_INSTANCED_TEXTURED_QUAD
Pointer to a statically allocated string containing the GLSL source code for a plain vertex shader.
Definition: Shader2D.hpp:35
ShaderProgram specialized for rendering Model instances in 3D.
Definition: Shader3D.hpp:25
static const char *const VERTEX_SHADER_SOURCE_CODE_INSTANCED_MODEL
Pointer to a statically allocated string containing the GLSL source code for a plain vertex shader.
Definition: Shader3D.hpp:31
Fixed-size array of uniform shader variable identifiers representing an array inside a ShaderProgram.
Definition: ShaderParameter.hpp:52
Identifier for a uniform shader variable inside a ShaderProgram.
Definition: ShaderParameter.hpp:17
Compiled and linked GPU shader program.
Definition: ShaderProgram.hpp:46
Expandable texture atlas for packing 2D images into a spritesheet to enable batch rendering.
Definition: SpriteAtlas.hpp:34
@ FLIP_HORIZONTALLY
Flip the sprite along the X axis.
Definition: SpriteAtlas.hpp:46
Facility for shaping text, according to a Font, into renderable glyphs.
Definition: Text.hpp:19
Storage for multidimensional data, such as 2D images, on the GPU, combined with a sampler configurati...
Definition: Texture.hpp:75
Graphical window that can be rendered to.
Definition: Window.hpp:81
Syntactic analyzer for parsing input in the JSON5 format obtained from a json::Lexer.
Definition: json.hpp:1132
void parseObject(PropertyVisitor &visitor)
Read a single JSON object from the input and visit each of its properties.
Definition: json.hpp:1538
void parseValue(ValueVisitor &visitor)
Read a single JSON value from the input and visit it.
Definition: json.hpp:1470
void parseArray(ValueVisitor &visitor)
Read a single JSON array from the input and visit each of its values.
Definition: json.hpp:1614
int main(int argc, char *argv[])
Definition: example_game.cpp:936
Input
Unique identifier for a specific control on a physical input device, such as a certain keyboard key,...
Definition: Input.hpp:323
@ UNKNOWN
Unknown input.
constexpr Input findInput(std::string_view identifier) noexcept
Find the Input corresponding to a given identifier.
Definition: Input.hpp:404
std::string String
JSON string type.
Definition: json.hpp:189
auto onArray(Callback callback)
Build a Parser::ValueVisitor that handles arrays with a given callback function.
Definition: json.hpp:3415
auto onString(Callback callback)
Build a Parser::ValueVisitor that handles String values with a given callback function.
Definition: json.hpp:3334
auto onProperty(Callback callback)
Build a Parser::PropertyVisitor that handles object properties with a given callback function.
Definition: json.hpp:3435
constexpr float e
Definition: math.hpp:34
constexpr Box< L, T > getAabbOf(const LineSegment< L, T > &line) noexcept
Get the axis-aligned bounding box of a line segment.
Definition: shapes.hpp:157
constexpr bool intersects(const Sphere< L, T > &a, const Sphere< L, T > &b) noexcept
Check if two spheres intersect.
Definition: shapes.hpp:243
Configuration options for an Application.
Definition: Application.hpp:14
float tickRate
The tick rate of the application, in hertz (ticks per second).
Definition: Application.hpp:39
Transient information about the current frame of an Application.
Definition: FrameInfo.hpp:11
Time< float > deltaTime
The time, in seconds, elapsed between the beginning of the previous frame and the beginning of the cu...
Definition: FrameInfo.hpp:28
float tickInterpolationAlpha
The ratio of the latest processed tick's importance compared to the tick processed before it,...
Definition: FrameInfo.hpp:16
Time< float > elapsedTime
The time, in seconds, that had elapsed since the start of the application at the beginning of the cur...
Definition: FrameInfo.hpp:22
Transient information about the current tick of an Application.
Definition: TickInfo.hpp:13
Time< float > tickInterval
The average time, in seconds, that should elapse between each tick.
Definition: TickInfo.hpp:33
Exception type for domain-specific errors originating from the donut::audio module.
Definition: Error.hpp:14
Current state of the sound listener, i.e.
Definition: Listener.hpp:15
Configuration options for a Sound.
Definition: Sound.hpp:87
SoundAttenuationModel attenuationModel
Which distance attenuation/falloff model to use for 3D positional audio when playing this sound.
Definition: Sound.hpp:96
Application was requested to quit by the user.
Definition: Event.hpp:129
Data structure containing information about an event.
Definition: Event.hpp:339
Window size was changed.
Definition: Event.hpp:169
Configuration of a 3D Model instance, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:32
const Model * model
Non-owning pointer to the model to be drawn.
Definition: RenderPass.hpp:47
Shader3D * shader
Non-owning pointer the shader to use when rendering this model.
Definition: RenderPass.hpp:39
Container for a set of 3D triangle meshes stored on the GPU, combined with associated materials.
Definition: Model.hpp:20
static const Model *const QUAD
Pointer to the statically allocated storage for the built-in quad model.
Definition: Model.hpp:126
static const Model *const CUBE
Pointer to the statically allocated storage for the built-in cube model.
Definition: Model.hpp:135
Configuration of a 2D rectangle instance, optionally textured, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:300
const Texture * texture
Non-owning pointer to a texture to apply to the rectangle.
Definition: RenderPass.hpp:315
vec2 position
Position, in world coordinates, to render the rectangle at, with respect to its RectangleInstance::or...
Definition: RenderPass.hpp:321
Shader2D * shader
Non-owning pointer to the shader to use when rendering this rectangle.
Definition: RenderPass.hpp:307
Configuration of a 2D sprite instance from a SpriteAtlas, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:388
const SpriteAtlas * atlas
Non-owning pointer to the texture atlas in which the sprite resides.
Definition: RenderPass.hpp:403
Configuration of a copied 2D instance of Text shaped from a Font, for drawing as part of a RenderPass...
Definition: RenderPass.hpp:514
const Text * text
Non-owning read-only pointer to the shaped text to copy, and later draw.
Definition: RenderPass.hpp:531
Configuration of a 2D instance of Text shaped from a Font, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:466
const Text * text
Non-owning read-only pointer to the shaped text to draw.
Definition: RenderPass.hpp:483
Configuration of a 2D instance of a string of text with a Font, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:644
Font * font
Non-owning pointer to the font from which to shape the text.
Definition: RenderPass.hpp:660
Configuration of a 2D textured quad instance, for drawing as part of a RenderPass.
Definition: RenderPass.hpp:216
const Texture * texture
Non-owning pointer to the texture to be drawn.
Definition: RenderPass.hpp:231
Rectangular region of a framebuffer.
Definition: Viewport.hpp:13
static constexpr std::pair< Viewport, int > createIntegerScaled(ivec2 framebufferSize, ivec2 renderResolution) noexcept
Create an integer-scaled viewport that fits into the middle of a framebuffer at the largest positive ...
Definition: Viewport.hpp:32
Configuration options for a Window.
Definition: Window.hpp:16
const char * title
Non-owning pointer to a null-terminated UTF-8 string of the displayed title of the window.
Definition: Window.hpp:23
Exception type for errors originating from the JSON API.
Definition: json.hpp:82
Line and column numbers of a location in a JSON source string.
Definition: json.hpp:56