First commit

This commit is contained in:
2025-11-17 03:42:30 -05:00
commit d0f9669b8b
41 changed files with 36106 additions and 0 deletions

146
src/canvas.cpp Normal file
View File

@@ -0,0 +1,146 @@
#include "canvas.h"
#include <glm/gtc/type_ptr.hpp>
using namespace glm;
using namespace game::resource;
namespace game
{
GLuint Canvas::textureVAO = 0;
GLuint Canvas::textureVBO = 0;
GLuint Canvas::textureEBO = 0;
bool Canvas::isStaticInit = false;
Canvas::Canvas(vec2 size, bool isDefault)
{
this->size = size;
if (isDefault)
{
fbo = 0;
rbo = 0;
texture = 0;
this->isDefault = true;
}
else
{
glGenFramebuffers(1, &fbo);
glGenRenderbuffers(1, &rbo);
glGenTextures(1, &texture);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, size.x, size.y);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
if (!isStaticInit)
{
glGenVertexArrays(1, &textureVAO);
glGenBuffers(1, &textureVBO);
glGenBuffers(1, &textureEBO);
glBindVertexArray(textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 4 * 4, nullptr, GL_DYNAMIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, textureEBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(TEXTURE_INDICES), TEXTURE_INDICES, GL_DYNAMIC_DRAW);
glEnableVertexAttribArray(0);
glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)0);
glEnableVertexAttribArray(1);
glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(float), (void*)(2 * sizeof(float)));
glBindVertexArray(0);
}
}
Canvas::~Canvas()
{
if (!isDefault)
{
if (fbo) glDeleteFramebuffers(1, &fbo);
if (rbo) glDeleteRenderbuffers(1, &rbo);
if (texture) glDeleteTextures(1, &texture);
}
}
mat4 Canvas::view_get() const { return mat4{1.0f}; }
mat4 Canvas::projection_get() const
{
if (isDefault) return glm::ortho(0.0f, (float)size.x, (float)size.y, 0.0f, -1.0f, 1.0f);
return glm::ortho(0.0f, (float)size.x, 0.0f, (float)size.y, -1.0f, 1.0f);
}
void Canvas::texture_render(Shader& shader, GLuint textureId, mat4& model, vec4 tint, vec3 colorOffset,
float* vertices) const
{
auto view = view_get();
auto projection = projection_get();
glUseProgram(shader.id);
glUniform1i(glGetUniformLocation(shader.id, shader::UNIFORM_TEXTURE), 0);
glUniform3fv(glGetUniformLocation(shader.id, shader::UNIFORM_COLOR_OFFSET), 1, value_ptr(colorOffset));
glUniform4fv(glGetUniformLocation(shader.id, shader::UNIFORM_TINT), 1, value_ptr(tint));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_MODEL), 1, GL_FALSE, value_ptr(model));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_VIEW), 1, GL_FALSE, value_ptr(view));
glUniformMatrix4fv(glGetUniformLocation(shader.id, shader::UNIFORM_PROJECTION), 1, GL_FALSE, value_ptr(projection));
glBindVertexArray(textureVAO);
glBindBuffer(GL_ARRAY_BUFFER, textureVBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(TEXTURE_VERTICES), vertices, GL_DYNAMIC_DRAW);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureId);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
glBindBuffer(GL_ARRAY_BUFFER, 0);
glBindTexture(GL_TEXTURE_2D, 0);
glUseProgram(0);
}
void Canvas::render(Shader& shader, mat4& model, vec4 tint, vec3 colorOffset) const
{
texture_render(shader, texture, model, tint, colorOffset);
}
void Canvas::bind() const
{
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glViewport(0, 0, size.x, size.y);
}
void Canvas::clear(vec4 color) const
{
glClearColor(color.r, color.g, color.b, color.a);
glClear(GL_COLOR_BUFFER_BIT);
}
void Canvas::unbind() const
{
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
}
bool Canvas::is_valid() const { return fbo != 0 || isDefault; };
}

51
src/canvas.h Normal file
View File

@@ -0,0 +1,51 @@
#pragma once
#ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif
#include "resource/shader.h"
#include <glm/ext/matrix_clip_space.hpp>
#include <glm/ext/matrix_transform.hpp>
#include <glm/glm.hpp>
namespace game
{
class Canvas
{
static constexpr float TEXTURE_VERTICES[] = {0, 0, 0.0f, 0.0f, 1, 0, 1.0f, 0.0f,
1, 1, 1.0f, 1.0f, 0, 1, 0.0f, 1.0f};
static constexpr GLuint TEXTURE_INDICES[] = {0, 1, 2, 2, 3, 0};
static GLuint textureVAO;
static GLuint textureVBO;
static GLuint textureEBO;
static bool isStaticInit;
public:
GLuint fbo{};
GLuint rbo{};
GLuint texture{};
glm::vec2 size{};
bool isDefault{};
Canvas() = default;
Canvas(glm::vec2, bool isDefault = false);
~Canvas();
glm::mat4 transform_get() const;
glm::mat4 view_get() const;
glm::mat4 projection_get() const;
void texture_render(resource::Shader&, GLuint, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {},
float* = (float*)TEXTURE_VERTICES) const;
void render(resource::Shader&, glm::mat4&, glm::vec4 = glm::vec4(1.0f), glm::vec3 = {}) const;
void bind() const;
void unbind() const;
void clear(glm::vec4 color = glm::vec4(0, 0, 0, 1)) const;
bool is_valid() const;
};
}

173
src/loader.cpp Normal file
View File

@@ -0,0 +1,173 @@
#include "loader.h"
#ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h>
#include <imgui.h>
#include <iostream>
#include <SDL3_mixer/SDL_mixer.h>
#ifdef __EMSCRIPTEN__
constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 0;
constexpr auto GLSL_VERSION = "#version 300 es";
#else
constexpr auto GL_VERSION_MAJOR = 3;
constexpr auto GL_VERSION_MINOR = 3;
constexpr auto GLSL_VERSION = "#version 330";
#endif
constexpr auto WINDOW_ROUNDING = 6.0f;
constexpr auto WINDOW_COLOR = ImVec4(0.05f, 0.35f, 0.08f, 1.0f);
namespace game
{
Loader::Loader()
{
if (!SDL_Init(SDL_INIT_VIDEO))
{
std::cout << "Failed to initialize SDL: " << SDL_GetError();
isError = true;
return;
}
std::cout << "Initialized SDL" << "\n";
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, GL_VERSION_MAJOR);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, GL_VERSION_MINOR);
#ifdef __EMSCRIPTEN__
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES);
#else
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
#endif
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
window = SDL_CreateWindow("Snivy", SIZE.x, SIZE.y, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
if (!window)
{
std::cout << "Failed to initialize window: " << SDL_GetError();
;
isError = true;
return;
}
std::cout << "Initialized window" << "\n";
context = SDL_GL_CreateContext(window);
if (!context)
{
std::cout << "Failed to initialize GL context: " << SDL_GetError();
isError = true;
return;
}
if (!SDL_GL_MakeCurrent(window, context))
{
std::cout << "Failed to make GL context current: " << SDL_GetError();
isError = true;
return;
}
#ifndef __EMSCRIPTEN__
if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
{
std::cout << "Failed to initialize GLAD" << "\n";
isError = true;
return;
}
std::cout << "Initialized GLAD" << "\n";
#endif
glEnable(GL_BLEND);
glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DEPTH_TEST);
std::cout << "Initialized GL context: " << glGetString(GL_VERSION) << "\n";
SDL_GL_SetSwapInterval(1);
if (!MIX_Init())
{
std::cout << "Failed to initialize SDL mixer: " << SDL_GetError();
isError = true;
return;
}
std::cout << "Initialized SDL mixer" << "\n";
IMGUI_CHECKVERSION();
ImGuiContext* imguiContext = ImGui::CreateContext();
if (!imguiContext)
{
std::cout << "Failed to initialize Dear ImGui" << "\n";
isError = true;
return;
}
std::cout << "Initialized Dear ImGui" << "\n";
ImGui::StyleColorsDark();
ImGuiIO& io = ImGui::GetIO();
ImGuiStyle& style = ImGui::GetStyle();
io.Fonts->AddFontFromFileTTF("resources/font/pmd.ttf");
style.WindowRounding = WINDOW_ROUNDING;
style.ChildRounding = style.WindowRounding;
style.FrameRounding = style.WindowRounding;
style.GrabRounding = style.WindowRounding;
style.PopupRounding = style.WindowRounding;
style.ScrollbarRounding = style.WindowRounding;
style.Colors[ImGuiCol_TitleBg] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgActive] = WINDOW_COLOR;
style.Colors[ImGuiCol_TitleBgCollapsed] = WINDOW_COLOR;
io.IniFilename = nullptr;
if (!ImGui_ImplSDL3_InitForOpenGL(window, context))
{
std::cout << "Failed to initialize Dear ImGui SDL3 backend" << "\n";
ImGui::DestroyContext(imguiContext);
isError = true;
return;
}
std::cout << "Initialize Dear ImGui SDL3 backend" << "\n";
if (!ImGui_ImplOpenGL3_Init(GLSL_VERSION))
{
std::cout << "Failed to initialize Dear ImGui OpenGL backend" << "\n";
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(imguiContext);
isError = true;
return;
}
std::cout << "Initialize Dear ImGui OpenGL backend" << "\n";
}
Loader::~Loader()
{
if (ImGui::GetCurrentContext())
{
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext();
}
if (context) SDL_GL_DestroyContext(context);
if (window) SDL_DestroyWindow(window);
SDL_Quit();
std::cout << "Exiting..." << "\n";
}
};

21
src/loader.h Normal file
View File

@@ -0,0 +1,21 @@
#pragma once
#include "glm/ext/vector_float2.hpp"
#include <SDL3/SDL.h>
namespace game
{
class Loader
{
public:
static constexpr glm::vec2 SIZE = {1080, 720};
SDL_Window* window{};
SDL_GLContext context{};
bool isError{};
Loader();
~Loader();
};
};

38
src/main.cpp Normal file
View File

@@ -0,0 +1,38 @@
#include <cstdlib>
#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#endif
#include "loader.h"
#include "state.h"
using namespace game;
#ifdef __EMSCRIPTEN__
static void emscripten_loop(void* arg)
{
auto* state = (State*)(arg);
state->loop();
if (!state->isRunning) emscripten_cancel_main_loop();
}
#endif
int main()
{
Loader loader;
if (loader.isError) return EXIT_FAILURE;
State state(loader.window, loader.context, Loader::SIZE);
#ifdef __EMSCRIPTEN__
emscripten_set_main_loop_arg(emscripten_loop, &state, 0, true);
#else
while (state.isRunning)
state.loop();
#endif
return EXIT_SUCCESS;
}

197
src/resource/actor.cpp Normal file
View File

@@ -0,0 +1,197 @@
#include "actor.h"
#include "../util/map_.h"
#include "../util/math_.h"
#include "../util/unordered_map_.h"
#include "../util/vector_.h"
#include "../resource/audio.h"
#include "../resource/texture.h"
#include <glm/glm.hpp>
using namespace glm;
using namespace game::util;
using namespace game::anm2;
namespace game::resource
{
std::shared_ptr<void> texture_callback(const std::filesystem::path& path) { return std::make_shared<Texture>(path); }
std::shared_ptr<void> sound_callback(const std::filesystem::path& path) { return std::make_shared<Audio>(path); }
Actor::Actor(const std::filesystem::path& path, vec2 position) : anm2(path, texture_callback, sound_callback)
{
this->position = position;
play(anm2.animations.defaultAnimation);
}
anm2::Animation* Actor::animation_get() { return vector::find(anm2.animations.items, animationIndex); }
anm2::Item* Actor::item_get(anm2::Type type, int id)
{
if (auto animation = animation_get())
{
switch (type)
{
case anm2::ROOT:
return &animation->rootAnimation;
break;
case anm2::LAYER:
return unordered_map::find(animation->layerAnimations, id);
case anm2::NULL_:
return map::find(animation->nullAnimations, id);
break;
case anm2::TRIGGER:
return &animation->triggers;
default:
return nullptr;
}
}
return nullptr;
}
anm2::Frame* Actor::trigger_get(int atFrame)
{
if (auto item = item_get(anm2::TRIGGER))
for (auto& trigger : item->frames)
if (trigger.atFrame == atFrame) return &trigger;
return nullptr;
}
anm2::Frame* Actor::frame_get(int index, anm2::Type type, int id)
{
if (auto item = item_get(type, id)) return vector::find(item->frames, index);
return nullptr;
}
anm2::Frame Actor::frame_generate(anm2::Item& item, float time)
{
anm2::Frame frame{};
frame.isVisible = false;
if (item.frames.empty()) return frame;
time = time < 0.0f ? 0.0f : time;
anm2::Frame* frameNext = nullptr;
int durationCurrent = 0;
int durationNext = 0;
for (int i = 0; i < item.frames.size(); i++)
{
anm2::Frame& checkFrame = item.frames[i];
frame = checkFrame;
durationNext += frame.duration;
if (time >= durationCurrent && time < durationNext)
{
if (i + 1 < (int)item.frames.size())
frameNext = &item.frames[i + 1];
else
frameNext = nullptr;
break;
}
durationCurrent += frame.duration;
}
if (frame.isInterpolated && frameNext && frame.duration > 1)
{
auto interpolation = (time - durationCurrent) / (durationNext - durationCurrent);
frame.rotation = glm::mix(frame.rotation, frameNext->rotation, interpolation);
frame.position = glm::mix(frame.position, frameNext->position, interpolation);
frame.scale = glm::mix(frame.scale, frameNext->scale, interpolation);
frame.colorOffset = glm::mix(frame.colorOffset, frameNext->colorOffset, interpolation);
frame.tint = glm::mix(frame.tint, frameNext->tint, interpolation);
}
return frame;
}
void Actor::play(const std::string& name)
{
for (int i = 0; i < anm2.animations.items.size(); i++)
{
if (anm2.animations.items[i].name == name)
{
animationIndex = i;
time = 0.0f;
isPlaying = true;
break;
}
}
}
void Actor::tick()
{
if (!isPlaying) return;
auto animation = animation_get();
if (!animation) return;
time += anm2.info.fps / 30.0f;
auto intTime = (int)time;
if (auto trigger = trigger_get(intTime))
{
if (!playedTriggers.contains(intTime))
{
if (auto sound = map::find(anm2.content.sounds, trigger->soundID)) sound->audio.play();
playedTriggers.insert(intTime);
}
}
if (time >= animation->frameNum)
{
if (animation->isLoop)
time = 0.0f;
else
isPlaying = false;
playedTriggers.clear();
}
}
void Actor::render(Shader& shader, Canvas& canvas)
{
auto animation = animation_get();
if (!animation) return;
auto root = frame_generate(animation->rootAnimation, time);
auto rootModel = math::quad_model_parent_get(root.position + position, root.pivot,
math::percent_to_unit(root.scale), root.rotation);
for (auto& i : animation->layerOrder)
{
auto& layerAnimation = animation->layerAnimations[i];
if (!layerAnimation.isVisible) continue;
auto layer = map::find(anm2.content.layers, i);
if (!layer) continue;
auto spritesheet = map::find(anm2.content.spritesheets, layer->spritesheetID);
if (!spritesheet) continue;
auto frame = frame_generate(layerAnimation, time);
if (!frame.isVisible) continue;
auto model = math::quad_model_get(frame.size, frame.position, frame.pivot, math::percent_to_unit(frame.scale),
frame.rotation);
model = rootModel * model;
auto& texture = spritesheet->texture;
if (!texture.is_valid()) return;
auto uvMin = frame.crop / vec2(texture.size);
auto uvMax = (frame.crop + frame.size) / vec2(texture.size);
auto uvVertices = math::uv_vertices_get(uvMin, uvMax);
canvas.texture_render(shader, texture.id, model, frame.tint, frame.colorOffset, uvVertices.data());
}
}
};

33
src/resource/actor.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <unordered_set>
#include "../canvas.h"
#include "anm2.h"
namespace game::resource
{
class Actor
{
public:
anm2::Anm2 anm2{};
glm::vec2 position{};
float time{};
bool isPlaying{};
int animationIndex{-1};
std::unordered_set<int> playedTriggers{};
Actor(const std::filesystem::path&, glm::vec2);
anm2::Animation* animation_get();
anm2::Animation* animation_get(std::string&);
int animation_index_get(anm2::Animation&);
anm2::Item* item_get(anm2::Type, int = -1);
anm2::Frame* trigger_get(int);
anm2::Frame* frame_get(int, anm2::Type, int = -1);
anm2::Frame frame_generate(anm2::Item&, float);
void play(const std::string&);
void tick();
void render(Shader&, Canvas&);
};
}

248
src/resource/anm2.cpp Normal file
View File

@@ -0,0 +1,248 @@
#include "anm2.h"
#include <iostream>
using namespace tinyxml2;
using namespace game::resource;
namespace game::anm2
{
XMLError query_string_attribute(XMLElement* element, const char* attribute, std::string* value)
{
const char* temp = nullptr;
auto result = element->QueryStringAttribute(attribute, &temp);
if (result == XML_SUCCESS && temp && value) *value = temp;
return result;
}
XMLError query_path_attribute(XMLElement* element, const char* attribute, std::filesystem::path* value)
{
std::string temp{};
auto result = query_string_attribute(element, attribute, &temp);
if (value) *value = std::filesystem::path(temp);
return result;
}
XMLError query_color_attribute(XMLElement* element, const char* attribute, float* value)
{
int temp{};
auto result = element->QueryIntAttribute(attribute, &temp);
if (result == XML_SUCCESS && value) *value = (temp / 255.0f);
return result;
}
Info::Info(XMLElement* element)
{
if (!element) return;
element->QueryIntAttribute("Fps", &fps);
}
Spritesheet::Spritesheet(XMLElement* element, int& id, TextureCallback textureCallback)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_path_attribute(element, "Path", &path);
texture = Texture(path);
}
Layer::Layer(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("SpritesheetId", &spritesheetID);
}
Null::Null(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
element->QueryBoolAttribute("ShowRect", &isShowRect);
}
Event::Event(XMLElement* element, int& id)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_string_attribute(element, "Name", &name);
}
Sound::Sound(XMLElement* element, int& id, SoundCallback soundCallback)
{
if (!element) return;
element->QueryIntAttribute("Id", &id);
query_path_attribute(element, "Path", &path);
audio = Audio(path);
}
Content::Content(XMLElement* element, TextureCallback textureCallback, SoundCallback soundCallback)
{
if (auto spritesheetsElement = element->FirstChildElement("Spritesheets"))
{
for (auto child = spritesheetsElement->FirstChildElement("Spritesheet"); child;
child = child->NextSiblingElement("Spritesheet"))
{
int spritesheetId{};
Spritesheet spritesheet(child, spritesheetId, textureCallback);
spritesheets.emplace(spritesheetId, std::move(spritesheet));
}
}
if (auto layersElement = element->FirstChildElement("Layers"))
{
for (auto child = layersElement->FirstChildElement("Layer"); child; child = child->NextSiblingElement("Layer"))
{
int layerId{};
Layer layer(child, layerId);
layers.emplace(layerId, std::move(layer));
}
}
if (auto nullsElement = element->FirstChildElement("Nulls"))
{
for (auto child = nullsElement->FirstChildElement("Null"); child; child = child->NextSiblingElement("Null"))
{
int nullId{};
Null null(child, nullId);
nulls.emplace(nullId, std::move(null));
}
}
if (auto eventsElement = element->FirstChildElement("Events"))
{
for (auto child = eventsElement->FirstChildElement("Event"); child; child = child->NextSiblingElement("Event"))
{
int eventId{};
Event event(child, eventId);
events.emplace(eventId, std::move(event));
}
}
if (auto soundsElement = element->FirstChildElement("Sounds"))
{
for (auto child = soundsElement->FirstChildElement("Sound"); child; child = child->NextSiblingElement("Sound"))
{
int soundId{};
Sound sound(child, soundId, soundCallback);
sounds.emplace(soundId, std::move(sound));
}
}
}
Frame::Frame(XMLElement* element, Type type)
{
if (type != TRIGGER)
{
element->QueryFloatAttribute("XPosition", &position.x);
element->QueryFloatAttribute("YPosition", &position.y);
if (type == LAYER)
{
element->QueryFloatAttribute("XPivot", &pivot.x);
element->QueryFloatAttribute("YPivot", &pivot.y);
element->QueryFloatAttribute("XCrop", &crop.x);
element->QueryFloatAttribute("YCrop", &crop.y);
element->QueryFloatAttribute("Width", &size.x);
element->QueryFloatAttribute("Height", &size.y);
}
element->QueryFloatAttribute("XScale", &scale.x);
element->QueryFloatAttribute("YScale", &scale.y);
element->QueryIntAttribute("Delay", &duration);
element->QueryBoolAttribute("Visible", &isVisible);
query_color_attribute(element, "RedTint", &tint.r);
query_color_attribute(element, "GreenTint", &tint.g);
query_color_attribute(element, "BlueTint", &tint.b);
query_color_attribute(element, "AlphaTint", &tint.a);
query_color_attribute(element, "RedOffset", &colorOffset.r);
query_color_attribute(element, "GreenOffset", &colorOffset.g);
query_color_attribute(element, "BlueOffset", &colorOffset.b);
element->QueryFloatAttribute("Rotation", &rotation);
element->QueryBoolAttribute("Interpolated", &isInterpolated);
}
else
{
element->QueryIntAttribute("EventId", &eventID);
element->QueryIntAttribute("SoundId", &soundID);
element->QueryIntAttribute("AtFrame", &atFrame);
}
}
Item::Item(XMLElement* element, Type type, int& id)
{
if (type == LAYER) element->QueryIntAttribute("LayerId", &id);
if (type == NULL_) element->QueryIntAttribute("NullId", &id);
element->QueryBoolAttribute("Visible", &isVisible);
for (auto child = type == TRIGGER ? element->FirstChildElement("Trigger") : element->FirstChildElement("Frame");
child; child = type == TRIGGER ? child->NextSiblingElement("Trigger") : child->NextSiblingElement("Frame"))
frames.emplace_back(Frame(child, type));
}
Animation::Animation(XMLElement* element)
{
query_string_attribute(element, "Name", &name);
element->QueryIntAttribute("FrameNum", &frameNum);
element->QueryBoolAttribute("Loop", &isLoop);
int id{-1};
if (auto rootAnimationElement = element->FirstChildElement("RootAnimation"))
rootAnimation = Item(rootAnimationElement, ROOT, id);
if (auto layerAnimationsElement = element->FirstChildElement("LayerAnimations"))
{
for (auto child = layerAnimationsElement->FirstChildElement("LayerAnimation"); child;
child = child->NextSiblingElement("LayerAnimation"))
{
Item layerAnimation(child, LAYER, id);
layerOrder.push_back(id);
layerAnimations.emplace(id, std::move(layerAnimation));
}
}
if (auto nullAnimationsElement = element->FirstChildElement("NullAnimations"))
{
for (auto child = nullAnimationsElement->FirstChildElement("NullAnimation"); child;
child = child->NextSiblingElement("NullAnimation"))
{
Item nullAnimation(child, NULL_, id);
nullAnimations.emplace(id, std::move(nullAnimation));
}
}
if (auto triggersElement = element->FirstChildElement("Triggers")) triggers = Item(triggersElement, TRIGGER, id);
}
Animations::Animations(XMLElement* element)
{
query_string_attribute(element, "DefaultAnimation", &defaultAnimation);
for (auto child = element->FirstChildElement("Animation"); child; child = child->NextSiblingElement("Animation"))
items.emplace_back(Animation(child));
}
Anm2::Anm2(const std::filesystem::path& path, TextureCallback textureCallback, SoundCallback soundCallback)
{
XMLDocument document;
if (document.LoadFile(path.c_str()) != XML_SUCCESS)
{
std::cout << "Failed to initialize anm2: " << document.ErrorStr() << "\n";
return;
}
std::cout << "Initialzed anm2: " << path.string() << "\n";
auto previousPath = std::filesystem::current_path();
std::filesystem::current_path(path.parent_path());
auto element = document.RootElement();
if (auto infoElement = element->FirstChildElement("Info")) info = Info(infoElement);
if (auto contentElement = element->FirstChildElement("Content"))
content = Content(contentElement, textureCallback, soundCallback);
if (auto animationsElement = element->FirstChildElement("Animations")) animations = Animations(animationsElement);
std::filesystem::current_path(previousPath);
}
}

163
src/resource/anm2.h Normal file
View File

@@ -0,0 +1,163 @@
#pragma once
#include <functional>
#include <tinyxml2/tinyxml2.h>
#include <filesystem>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/glm.hpp>
#include "audio.h"
#include "texture.h"
namespace game::anm2
{
enum Type
{
NONE,
ROOT,
LAYER,
NULL_,
TRIGGER
};
using TextureCallback = std::function<std::shared_ptr<void>(const std::filesystem::path&)>;
using SoundCallback = std::function<std::shared_ptr<void>(const std::filesystem::path&)>;
class Info
{
public:
int fps = 30;
Info() = default;
Info(tinyxml2::XMLElement*);
};
class Spritesheet
{
public:
std::filesystem::path path{};
resource::Texture texture{};
Spritesheet(tinyxml2::XMLElement*, int&, TextureCallback = nullptr);
};
class Layer
{
public:
std::string name{"New Layer"};
int spritesheetID{-1};
Layer(tinyxml2::XMLElement*, int&);
};
class Null
{
public:
std::string name{"New Null"};
bool isShowRect{};
Null(tinyxml2::XMLElement*, int&);
};
class Event
{
public:
std::string name{"New Event"};
Event(tinyxml2::XMLElement*, int&);
};
class Sound
{
public:
std::filesystem::path path{};
resource::Audio audio{};
Sound(tinyxml2::XMLElement*, int&, SoundCallback = nullptr);
};
class Content
{
public:
std::map<int, Spritesheet> spritesheets{};
std::map<int, Layer> layers{};
std::map<int, Null> nulls{};
std::map<int, Event> events{};
std::map<int, Sound> sounds{};
Content() = default;
Content(tinyxml2::XMLElement*, TextureCallback = nullptr, SoundCallback = nullptr);
};
struct Frame
{
glm::vec2 crop{};
glm::vec2 position{};
glm::vec2 pivot{};
glm::vec2 size{};
glm::vec2 scale{100, 100};
float rotation{};
int duration{};
glm::vec4 tint{1.0f, 1.0f, 1.0f, 1.0f};
glm::vec3 colorOffset{};
bool isInterpolated{};
int eventID{-1};
int soundID{-1};
int atFrame{-1};
bool isVisible{true};
Frame() = default;
Frame(tinyxml2::XMLElement*, Type);
};
class Item
{
public:
std::vector<Frame> frames{};
bool isVisible{};
Item() = default;
Item(tinyxml2::XMLElement*, Type, int&);
};
class Animation
{
public:
std::string name{"New Animation"};
int frameNum{};
bool isLoop{};
Item rootAnimation{};
std::unordered_map<int, Item> layerAnimations{};
std::vector<int> layerOrder{};
std::map<int, Item> nullAnimations{};
Item triggers{};
Animation() = default;
Animation(tinyxml2::XMLElement*);
};
class Animations
{
public:
std::string defaultAnimation{};
std::vector<Animation> items{};
Animations() = default;
Animations(tinyxml2::XMLElement*);
};
class Anm2
{
public:
Info info;
Content content{};
Animations animations{};
Anm2() = default;
Anm2(const std::filesystem::path&, TextureCallback = nullptr, SoundCallback = nullptr);
};
}

147
src/resource/audio.cpp Normal file
View File

@@ -0,0 +1,147 @@
#include "audio.h"
#include <SDL3/SDL_properties.h>
#include <iostream>
namespace game::resource
{
MIX_Mixer* Audio::mixer_get()
{
static auto mixer = MIX_CreateMixerDevice(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, nullptr);
return mixer;
}
void Audio::retain()
{
if (refCount) ++(*refCount);
}
void Audio::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (internal) MIX_DestroyAudio(internal);
delete refCount;
}
refCount = nullptr;
}
internal = nullptr;
}
Audio::Audio(const std::filesystem::path& path)
{
internal = MIX_LoadAudio(mixer_get(), path.c_str(), true);
if (internal)
{
refCount = new int(1);
std::cout << "Initialized audio: '" << path.string() << "'\n";
}
else
{
std::cout << "Failed to initialize audio: '" << path.string() << "'\n";
}
}
Audio::Audio(const Audio& other)
{
internal = other.internal;
refCount = other.refCount;
retain();
track = nullptr;
}
Audio::Audio(Audio&& other) noexcept
{
internal = other.internal;
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
Audio& Audio::operator=(const Audio& other)
{
if (this != &other)
{
unload();
internal = other.internal;
refCount = other.refCount;
retain();
}
return *this;
}
Audio& Audio::operator=(Audio&& other) noexcept
{
if (this != &other)
{
unload();
internal = other.internal;
track = other.track;
refCount = other.refCount;
other.internal = nullptr;
other.track = nullptr;
other.refCount = nullptr;
}
return *this;
}
void Audio::unload()
{
if (track)
{
MIX_DestroyTrack(track);
track = nullptr;
}
release();
}
void Audio::play(bool isLoop)
{
if (!internal) return;
auto mixer = mixer_get();
if (track && MIX_GetTrackMixer(track) != mixer)
{
MIX_DestroyTrack(track);
track = nullptr;
}
if (!track)
{
track = MIX_CreateTrack(mixer);
if (!track) return;
}
MIX_SetTrackAudio(track, internal);
SDL_PropertiesID options = 0;
if (isLoop)
{
options = SDL_CreateProperties();
if (options) SDL_SetNumberProperty(options, MIX_PROP_PLAY_LOOPS_NUMBER, -1);
}
MIX_PlayTrack(track, options);
if (options) SDL_DestroyProperties(options);
}
void Audio::stop()
{
if (track) MIX_StopTrack(track, 0);
}
bool Audio::is_playing() const { return track && MIX_TrackPlaying(track); }
Audio::~Audio() { unload(); }
bool Audio::is_valid() const { return internal != nullptr; }
}

31
src/resource/audio.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include <SDL3_mixer/SDL_mixer.h>
#include <filesystem>
namespace game::resource
{
class Audio
{
MIX_Audio* internal{nullptr};
MIX_Track* track{nullptr};
int* refCount{nullptr};
MIX_Mixer* mixer_get();
void unload();
void retain();
void release();
public:
Audio() = default;
Audio(const std::filesystem::path&);
Audio(const Audio&);
Audio(Audio&&) noexcept;
Audio& operator=(const Audio&);
Audio& operator=(Audio&&) noexcept;
~Audio();
bool is_valid() const;
void play(bool isLoop = false);
void stop();
bool is_playing() const;
};
}

79
src/resource/shader.cpp Normal file
View File

@@ -0,0 +1,79 @@
#include "shader.h"
#include <iostream>
namespace game::resource
{
Shader::Shader(const char* vertex, const char* fragment)
{
id = glCreateProgram();
auto compile = [&](const GLuint& shaderHandle, const char* text, const char* stage)
{
int isCompile{};
glShaderSource(shaderHandle, 1, &text, nullptr);
glCompileShader(shaderHandle);
glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &isCompile);
if (!isCompile)
{
GLint logLength = 0;
glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &logLength);
std::string log(logLength, '\0');
if (logLength > 0) glGetShaderInfoLog(shaderHandle, logLength, nullptr, log.data());
std::cout << "Failed to compile shader: " << log << '\n';
return false;
}
return true;
};
auto vertexHandle = glCreateShader(GL_VERTEX_SHADER);
auto fragmentHandle = glCreateShader(GL_FRAGMENT_SHADER);
if (!(compile(vertexHandle, vertex, "vertex") && compile(fragmentHandle, fragment, "fragment")))
{
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);
glDeleteProgram(id);
id = 0;
return;
}
glAttachShader(id, vertexHandle);
glAttachShader(id, fragmentHandle);
glLinkProgram(id);
auto isLinked = GL_FALSE;
glGetProgramiv(id, GL_LINK_STATUS, &isLinked);
if (!isLinked)
{
glDeleteProgram(id);
id = 0;
std::cout << "Failed to link shader: " << id << "\n";
}
else
std::cout << "Initialized shader: " << id << "\n";
glDeleteShader(vertexHandle);
glDeleteShader(fragmentHandle);
}
Shader::~Shader()
{
if (is_valid()) glDeleteProgram(id);
}
bool Shader::is_valid() const { return id != 0; }
Shader& Shader::operator=(Shader&& other) noexcept
{
if (this != &other)
{
if (is_valid()) glDeleteProgram(id);
id = other.id;
other.id = 0;
}
return *this;
}
}

118
src/resource/shader.h Normal file
View File

@@ -0,0 +1,118 @@
#pragma once
#ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif
namespace game::resource::shader
{
struct Info
{
const char* vertex{};
const char* fragment{};
};
#ifdef __EMSCRIPTEN__
constexpr auto VERTEX = R"(#version 300 es
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 v_uv;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
void main()
{
v_uv = i_uv;
mat4 transform = u_projection * u_view * u_model;
gl_Position = transform * vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto FRAGMENT = R"(#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D u_texture;
uniform vec4 u_tint;
uniform vec3 u_color_offset;
out vec4 o_fragColor;
void main()
{
vec4 texColor = texture(u_texture, v_uv);
texColor *= u_tint;
texColor.rgb += u_color_offset;
o_fragColor = texColor;
}
)";
#else
constexpr auto VERTEX = R"(#version 330 core
layout (location = 0) in vec2 i_position;
layout (location = 1) in vec2 i_uv;
out vec2 v_uv;
uniform mat4 u_model;
uniform mat4 u_view;
uniform mat4 u_projection;
void main()
{
v_uv = i_uv;
mat4 transform = u_projection * u_view * u_model;
gl_Position = transform * vec4(i_position, 0.0, 1.0);
}
)";
constexpr auto FRAGMENT = R"(#version 330 core
in vec2 v_uv;
uniform sampler2D u_texture;
uniform vec4 u_tint;
uniform vec3 u_color_offset;
out vec4 o_fragColor;
void main()
{
vec4 texColor = texture(u_texture, v_uv);
texColor *= u_tint;
texColor.rgb += u_color_offset;
o_fragColor = texColor;
}
)";
#endif
constexpr auto UNIFORM_MODEL = "u_model";
constexpr auto UNIFORM_VIEW = "u_view";
constexpr auto UNIFORM_PROJECTION = "u_projection";
constexpr auto UNIFORM_TEXTURE = "u_texture";
constexpr auto UNIFORM_TINT = "u_tint";
constexpr auto UNIFORM_COLOR_OFFSET = "u_color_offset";
#define SHADERS X(TEXTURE, VERTEX, FRAGMENT)
enum Type
{
#define X(symbol, vertex, fragment) symbol,
SHADERS
#undef X
COUNT
};
constexpr Info INFO[] = {
#define X(symbol, vertex, fragment) {vertex, fragment},
SHADERS
#undef X
};
}
namespace game::resource
{
class Shader
{
public:
GLint id{};
Shader() = default;
Shader(const char*, const char*);
bool is_valid() const;
~Shader();
Shader& operator=(Shader&&) noexcept;
};
}

132
src/resource/texture.cpp Normal file
View File

@@ -0,0 +1,132 @@
#include "texture.h"
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmissing-field-initializers"
#pragma GCC diagnostic ignored "-Wunused-function"
#endif
#define STBI_ONLY_PNG
#define STB_IMAGE_IMPLEMENTATION
#include <stb_image.h>
#if defined(__clang__) || defined(__GNUC__)
#pragma GCC diagnostic pop
#endif
#include <iostream>
using namespace glm;
namespace game::resource
{
bool Texture::is_valid() const { return id != 0; }
void Texture::retain()
{
if (refCount) ++(*refCount);
}
void Texture::release()
{
if (refCount)
{
if (--(*refCount) == 0)
{
if (is_valid()) glDeleteTextures(1, &id);
delete refCount;
}
refCount = nullptr;
}
else if (is_valid())
{
glDeleteTextures(1, &id);
}
id = 0;
}
Texture::~Texture() { release(); }
Texture::Texture(const Texture& other)
{
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
}
Texture::Texture(Texture&& other) noexcept
{
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
Texture& Texture::operator=(const Texture& other)
{
if (this != &other)
{
release();
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
retain();
}
return *this;
}
Texture& Texture::operator=(Texture&& other) noexcept
{
if (this != &other)
{
release();
id = other.id;
size = other.size;
channels = other.channels;
refCount = other.refCount;
other.id = 0;
other.size = {};
other.channels = 0;
other.refCount = nullptr;
}
return *this;
}
Texture::Texture(const std::filesystem::path& path)
{
if (auto data = stbi_load(path.c_str(), &size.x, &size.y, nullptr, CHANNELS); data)
{
glGenTextures(1, &id);
glBindTexture(GL_TEXTURE_2D, id);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, size.x, size.y, 0, GL_RGBA, GL_UNSIGNED_BYTE, data);
glBindTexture(GL_TEXTURE_2D, 0);
stbi_image_free(data);
channels = CHANNELS;
refCount = new int(1);
std::cout << "Initialized texture: '" << path.string() << "\n";
}
else
{
id = 0;
size = {};
channels = 0;
refCount = nullptr;
std::cout << "Failed to initialize texture: '" << path.string() << "'\n";
}
}
}

38
src/resource/texture.h Normal file
View File

@@ -0,0 +1,38 @@
#pragma once
#ifdef __EMSCRIPTEN__
#include <GLES3/gl3.h>
#else
#include <glad/glad.h>
#endif
#include <filesystem>
#include <glm/ext/vector_int2.hpp>
namespace game::resource
{
class Texture
{
public:
static constexpr auto CHANNELS = 4;
GLuint id{};
glm::ivec2 size{};
int channels{};
bool is_valid() const;
Texture() = default;
~Texture();
Texture(const Texture&);
Texture(Texture&&) noexcept;
Texture& operator=(const Texture&);
Texture& operator=(Texture&&) noexcept;
Texture(const std::filesystem::path&);
private:
int* refCount{nullptr};
void retain();
void release();
};
}

12
src/resources.cpp Normal file
View File

@@ -0,0 +1,12 @@
#include "resources.h"
using namespace game::resource;
namespace game
{
Resources::Resources()
{
for (int i = 0; i < shader::COUNT; i++)
shaders[i] = Shader(shader::INFO[i].vertex, shader::INFO[i].fragment);
}
}

14
src/resources.h Normal file
View File

@@ -0,0 +1,14 @@
#pragma once
#include "resource/shader.h"
namespace game
{
class Resources
{
public:
resource::Shader shaders[resource::shader::COUNT];
Resources();
};
}

87
src/state.cpp Normal file
View File

@@ -0,0 +1,87 @@
#include "state.h"
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl3.h>
#include <imgui.h>
using namespace glm;
using namespace game::resource;
namespace game
{
constexpr auto TICK_RATE = 30;
constexpr auto TICK_INTERVAL = (1000 / TICK_RATE);
constexpr auto UPDATE_RATE = 120;
constexpr auto UPDATE_INTERVAL = (1000 / UPDATE_RATE);
State::State(SDL_Window* inWindow, SDL_GLContext inContext, vec2 size)
: window(inWindow), context(inContext), canvas(size, true)
{
}
void State::tick() { actor.tick(); }
void State::update()
{
SDL_Event event;
while (SDL_PollEvent(&event))
{
ImGui_ImplSDL3_ProcessEvent(&event);
if (event.type == SDL_EVENT_QUIT) isRunning = false;
}
if (!isRunning) return;
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL3_NewFrame();
ImGui::NewFrame();
ImGui::Begin("Metrics");
ImGui::Text("Time: %f", actor.time);
ImGui::Text("IS Playing: %s", actor.isPlaying ? "true" : "false");
auto animation = actor.animation_get();
ImGui::Text("Animation: %s", animation ? animation->name.c_str() : "null");
ImGui::End();
ImGui::Render();
}
void State::render()
{
SDL_GL_MakeCurrent(window, context);
int width{};
int height{};
SDL_GetWindowSize(window, &width, &height);
canvas.bind();
canvas.clear(glm::vec4(0, 0, 0, 1));
actor.render(resources.shaders[shader::TEXTURE], canvas);
canvas.unbind();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window);
}
void State::loop()
{
auto currentTick = SDL_GetTicks();
auto currentUpdate = SDL_GetTicks();
if (currentUpdate - previousUpdate >= UPDATE_INTERVAL)
{
update();
render();
previousUpdate = currentUpdate;
}
if (currentTick - previousTick >= TICK_INTERVAL)
{
tick();
previousTick = currentTick;
}
SDL_Delay(1);
}
}

35
src/state.h Normal file
View File

@@ -0,0 +1,35 @@
#pragma once
#include <SDL3/SDL.h>
#include "resource/actor.h"
#include "canvas.h"
#include "resources.h"
namespace game
{
class State
{
SDL_Window* window{};
SDL_GLContext context{};
Resources resources;
resource::Actor actor{"resources/anm2/snivy.anm2", glm::vec2(400, 400)};
long previousUpdate{};
long previousTick{};
void tick();
void update();
void render();
public:
bool isRunning{true};
Canvas canvas{};
State(SDL_Window*, SDL_GLContext, glm::vec2);
void loop();
};
};

13
src/util/map_.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <map>
namespace game::util::map
{
template <typename T0, typename T1> T1* find(std::map<T0, T1>& map, T0 key)
{
auto it = map.find(key);
if (it != map.end()) return &it->second;
return nullptr;
}
}

41
src/util/math_.cpp Normal file
View File

@@ -0,0 +1,41 @@
#include "math_.h"
#include "glm/ext/matrix_transform.hpp"
using namespace glm;
namespace game::util::math
{
mat4 quad_model_get(vec2 size, vec2 position, vec2 pivot, vec2 scale, float rotation)
{
vec2 scaleAbsolute = glm::abs(scale);
vec2 scaleSign = glm::sign(scale);
vec2 pivotScaled = pivot * scaleAbsolute;
vec2 sizeScaled = size * scaleAbsolute;
mat4 model(1.0f);
model = glm::translate(model, vec3(position - pivotScaled, 0.0f));
model = glm::translate(model, vec3(pivotScaled, 0.0f));
model = glm::scale(model, vec3(scaleSign, 1.0f));
model = glm::rotate(model, glm::radians(rotation), vec3(0, 0, 1));
model = glm::translate(model, vec3(-pivotScaled, 0.0f));
model = glm::scale(model, vec3(sizeScaled, 1.0f));
return model;
}
mat4 quad_model_parent_get(vec2 position, vec2 pivot, vec2 scale, float rotation)
{
vec2 scaleSign = glm::sign(scale);
vec2 scaleAbsolute = glm::abs(scale);
float handedness = (scaleSign.x * scaleSign.y) < 0.0f ? -1.0f : 1.0f;
mat4 local(1.0f);
local = glm::translate(local, vec3(pivot, 0.0f));
local = glm::scale(local, vec3(scaleSign, 1.0f));
local = glm::rotate(local, glm::radians(rotation) * handedness, vec3(0, 0, 1));
local = glm::translate(local, vec3(-pivot, 0.0f));
local = glm::scale(local, vec3(scaleAbsolute, 1.0f));
return glm::translate(mat4(1.0f), vec3(position, 0.0f)) * local;
}
}

19
src/util/math_.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "glm/ext/matrix_float4x4.hpp"
#include "glm/ext/vector_float2.hpp"
namespace game::util::math
{
glm::mat4 quad_model_get(glm::vec2, glm::vec2, glm::vec2, glm::vec2, float);
glm::mat4 quad_model_parent_get(glm::vec2 position, glm::vec2 pivot, glm::vec2, float);
template <typename T> constexpr T percent_to_unit(T value) { return value / 100.0f; }
template <typename T> constexpr T unit_to_percent(T value) { return value * 100.0f; }
constexpr std::array<float, 16> uv_vertices_get(glm::vec2 uvMin, glm::vec2 uvMax)
{
return {0.0f, 0.0f, uvMin.x, uvMin.y, 1.0f, 0.0f, uvMax.x, uvMin.y,
1.0f, 1.0f, uvMax.x, uvMax.y, 0.0f, 1.0f, uvMin.x, uvMax.y};
}
}

13
src/util/unordered_map_.h Normal file
View File

@@ -0,0 +1,13 @@
#pragma once
#include <unordered_map>
namespace game::util::unordered_map
{
template <typename T0, typename T1> T1* find(std::unordered_map<T0, T1>& map, T0 key)
{
auto it = map.find(key);
if (it != map.end()) return &it->second;
return nullptr;
}
}

17
src/util/vector_.h Normal file
View File

@@ -0,0 +1,17 @@
#pragma once
#include <vector>
namespace game::util::vector
{
template <typename T> bool in_bounds(std::vector<T>& vector, int index)
{
return (index >= 0 && index < vector.size());
}
template <typename T> T* find(std::vector<T>& vector, int index)
{
if (!in_bounds(vector, index)) return nullptr;
return &vector[index];
}
}