Initial commit

This commit is contained in:
2026-04-29 21:54:47 +01:00
commit 02241bbd9e
60 changed files with 1561 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
BasedOnStyle: LLVM
IndentWidth: 2
TabWidth: 2
ContinuationIndentWidth: 2
ColumnLimit: 0
UseTab: Never
BreakBeforeBraces: Attach
AllowShortFunctionsOnASingleLine: Empty
AlignAfterOpenBracket: BlockIndent
BinPackArguments: false
AllowAllArgumentsOnNextLine: false
+4
View File
@@ -0,0 +1,4 @@
**old**
build/*
bin/*
+99
View File
@@ -0,0 +1,99 @@
SRC_DIR := src
BIN_DIR := bin
OBJ_DIR := build
INCLUDE_DIR := include
TARGET_DEBUG := $(BIN_DIR)/game-dbg
TARGET_RELEASE := $(BIN_DIR)/game
PCH_DEBUG := $(OBJ_DIR)/debug/pch.h.gch
PCH_RELEASE := $(OBJ_DIR)/release/pch.h.gch
CCACHE := ccache
CXX := $(CCACHE) clang++
CC := $(CCACHE) clang
# ---------------- SDL3 ----------------
SDL_CFLAGS := $(shell pkg-config --cflags sdl3 sdl3-ttf sdl3-image)
SDL_LIBS_DEBUG := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image)
SDL_LIBS_RELEASE := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image)
# ---------------- FLAGS ----------------
CFLAGS_DEBUG := \
-std=c++20 -Wall -Wextra \
-O0 -g -fno-omit-frame-pointer \
-Wno-unused-command-line-argument \
-I./include \
$(SDL_CFLAGS)
CFLAGS_RELEASE := \
-std=c++20 -O3 -march=x86-64 -mtune=generic \
-flto=thin \
-fno-rtti -fomit-frame-pointer -DNDEBUG -s \
-Wno-unused-command-line-argument \
-I./include \
$(SDL_CFLAGS)
PCH_CFLAGS_DEBUG := $(CFLAGS_DEBUG) -x c++-header
PCH_CFLAGS_RELEASE := $(CFLAGS_RELEASE) -x c++-header
# ---------------- SOURCES ----------------
SRC := $(wildcard $(SRC_DIR)/*.cc) $(wildcard $(SRC_DIR)/**/*.cc)
OBJ_DEBUG := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/debug/%.o,$(SRC))
OBJ_RELEASE := $(patsubst $(SRC_DIR)/%.cc,$(OBJ_DIR)/release/%.o,$(SRC))
DEP_DEBUG := $(OBJ_DEBUG:.o=.d)
DEP_RELEASE := $(OBJ_RELEASE:.o=.d)
# ---------------- TARGETS ----------------
.PHONY: all debug release clean
all: debug
debug: $(TARGET_DEBUG)
release: $(TARGET_RELEASE)
# ---------------- PCH ----------------
$(PCH_DEBUG): $(INCLUDE_DIR)/pch.h
mkdir -p $(dir $@)
$(CXX) $(PCH_CFLAGS_DEBUG) -o $@ $<
$(PCH_RELEASE): $(INCLUDE_DIR)/pch.h
mkdir -p $(dir $@)
$(CXX) $(PCH_CFLAGS_RELEASE) -o $@ $<
# ---------------- LINK ----------------
$(TARGET_DEBUG): $(PCH_DEBUG) $(OBJ_DEBUG)
mkdir -p $(BIN_DIR)
$(CXX) $(CFLAGS_DEBUG) -o $@ $(OBJ_DEBUG) $(SDL_LIBS_DEBUG)
$(TARGET_RELEASE): $(PCH_RELEASE) $(OBJ_RELEASE)
mkdir -p $(BIN_DIR)
$(CXX) $(CFLAGS_RELEASE) -o $@ $(OBJ_RELEASE) $(SDL_LIBS_RELEASE)
# ---------------- COMPILE ----------------
$(OBJ_DIR)/debug/%.o: $(SRC_DIR)/%.cc $(PCH_DEBUG)
mkdir -p $(dir $@)
$(CXX) $(CFLAGS_DEBUG) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@
$(OBJ_DIR)/release/%.o: $(SRC_DIR)/%.cc $(PCH_RELEASE)
mkdir -p $(dir $@)
$(CXX) $(CFLAGS_RELEASE) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@
# ---------------- DEP ----------------
-include $(DEP_DEBUG)
-include $(DEP_RELEASE)
# ---------------- CLEAN ----------------
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 233 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 811 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 453 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 386 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

+99
View File
@@ -0,0 +1,99 @@
#version 120
uniform vec2 resolution; // 720x480
uniform vec2 offset; // padding in pixels
uniform float scale; // scale factor
uniform vec2 torch_lights[32];
uniform int num_torches;
uniform float time_sec;
uniform float lorentz_field;
// External snippet
// Simplex 2D noise
// Source: https://gist.github.com/patriciogonzalezvivo/670c22f3966e662d2f83
vec3 permute(vec3 x) { return mod(((x*34.0)+1.0)*x, 289.0); }
float snoise(vec2 v){
const vec4 C = vec4(0.211324865405187, 0.366025403784439,
-0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1;
i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod(i, 289.0);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 ))
+ i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy),
dot(x12.zw,x12.zw)), 0.0);
m = m*m ;
m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
// End of external snippet.
void main() {
vec2 virtual_coord = (gl_FragCoord.xy - offset) / scale;
if (virtual_coord.x < 0.0 || virtual_coord.x > resolution.x ||
virtual_coord.y < 0.0 || virtual_coord.y > resolution.y) {
return;
}
// tint
vec3 lf_color = vec3(3.0/255.0, 9.0/255.0, 42.0/255.0);
vec3 tint = mix(vec3(0.0), lf_color, lorentz_field);
// radii
float p_i_radius = mix(100.0, 120.0, lorentz_field);
float p_o_radius = mix(340.0, 235.0, lorentz_field);
float t_i_radius = 10.0;
float t_o_radius = 160.0;
// player is always at screen center in virtual coords
vec2 player_screen = vec2(resolution.x / 2.0, resolution.y / 2.0);
float light_strength = 0.0;
// Player light
float d = distance(virtual_coord, player_screen);
if (d < p_i_radius) {
light_strength = 1.0;
} else if (d < p_o_radius) {
float t = (d - p_i_radius) / (p_o_radius - p_i_radius);
light_strength = 1.0 - t;
}
// Torch lights
for (int i = 0; i < num_torches; i++) {
float td = distance(virtual_coord, torch_lights[i]);
float strength = 0.0;
if (td < t_i_radius) {
strength = 1.0;
} else if (td < t_o_radius) {
float t = (td - t_i_radius) / (t_o_radius - t_i_radius);
strength = 1.0 - t;
}
light_strength += strength;
}
light_strength = clamp(light_strength, 0.0, 1.0);
// Noise
float noise = (snoise(virtual_coord / resolution * 3.0 + vec2(time_sec * 0.25, 0)) + 1.0) / 2.0;
float t = 1.0 - light_strength;
float alpha = clamp(t * (1.0 + noise), 0.0, 1.0);
gl_FragColor = vec4(tint, alpha);
}
+5
View File
@@ -0,0 +1,5 @@
#version 120
void main() {
gl_Position = gl_Vertex;
}
View File
+15
View File
@@ -0,0 +1,15 @@
#ifndef PCH_H
#define PCH_H
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <stdio.h>
#include <vector>
#endif
+145
View File
@@ -0,0 +1,145 @@
#ifndef UTILS_H
#define UTILS_H
#include "pch.h"
#define MIN(x, y) ((x) < (y) ? (x) : (y))
#define MAX(x, y) ((x) > (y) ? (x) : (y))
struct Color {
uint8_t r, g, b, a;
};
template <typename T>
struct Vec2 {
T x, y;
template <typename U>
Vec2 operator*(U s) const {
return {x * s, y * s};
}
template <typename U>
Vec2 operator/(U s) const {
return {x / s, y / s};
}
Vec2 operator+(const Vec2 &other) const {
return {x + other.x, y + other.y};
}
Vec2 operator-(const Vec2 &other) const {
return {x - other.x, y - other.y};
}
Vec2 operator-() const {
return {-x, -y};
}
T dot(const Vec2 &o) const {
return x * o.x + y * o.y;
}
float dist(const Vec2 &other) const {
float dx = other.x - x;
float dy = other.y - y;
return std::sqrt(dx * dx + dy * dy);
}
Vec2<float> normalized() const {
float length = std::sqrt(x * x + y * y);
if (length == 0)
return {0, 0};
return {x / length, y / length};
}
float angle_to(const Vec2 &other) const {
float dx = other.x - x;
float dy = other.y - y;
return std::atan2(dy, dx);
}
};
struct Line {
Vec2<float> start;
Vec2<float> end;
Line(Vec2<float> start, Vec2<float> end) : start(start), end(end) {}
bool intersects(const Line &other) const {
float denom = (other.end.y - other.start.y) * (end.x - start.x) -
(other.end.x - other.start.x) * (end.y - start.y);
if (std::abs(denom) < 1e-6f) // Lines are parallel (or very close to it)
return false;
float ua = ((other.end.x - other.start.x) * (start.y - other.start.y) -
(other.end.y - other.start.y) * (start.x - other.start.x)) /
denom;
float ub = ((end.x - start.x) * (start.y - other.start.y) -
(end.y - start.y) * (start.x - other.start.x)) /
denom;
return ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1;
}
};
struct Rect {
Line diagonal; // from top-left to bottom-right
Rect(Vec2<float> a, Vec2<float> b) : diagonal(a, b) {
diagonal.start = {
MIN(a.x, b.x),
MIN(a.y, b.y)
};
diagonal.end = {
MAX(a.x, b.x),
MAX(a.y, b.y)
};
}
bool contains(const Vec2<float> &point) const {
return point.x >= diagonal.start.x && point.x <= diagonal.end.x &&
point.y >= diagonal.start.y && point.y <= diagonal.end.y;
}
bool intersects(const Rect &other) const {
return !(diagonal.start.x > other.diagonal.end.x || diagonal.end.x < other.diagonal.start.x || diagonal.start.y > other.diagonal.end.y || diagonal.end.y < other.diagonal.start.y);
}
bool intersects(const Line &line) const {
if (contains(line.start) || contains(line.end))
return true;
Vec2<float> rect_points[4] = {
diagonal.start,
{diagonal.end.x, diagonal.start.y},
{diagonal.start.x, diagonal.end.y},
diagonal.end
};
for (int i = 0; i < 4; i++) {
Vec2<float> edge_start = rect_points[i];
Vec2<float> edge_end = rect_points[(i + 1) % 4];
if (Line{edge_start, edge_end}.intersects(line))
return true;
}
return false;
}
float x() const {
return diagonal.start.x;
}
float y() const {
return diagonal.start.y;
}
float w() const {
return diagonal.end.x - diagonal.start.x;
}
float h() const {
return diagonal.end.y - diagonal.start.y;
}
};
#endif
+174
View File
@@ -0,0 +1,174 @@
#ifndef WINDOW_H
#define WINDOW_H
#include "../pch.h"
#include "../utils.h"
using ImageID = uint32_t;
using FontID = uint32_t;
using TextID = uint32_t;
struct Font {
TTF_Font *font;
bool pixel_art;
};
struct Event {
enum Type {
NONE,
QUIT,
KEY_UP,
KEY_DOWN,
TEXT_INPUT,
SCROLL,
MOUSE_BUTTON_UP,
MOUSE_BUTTON_DOWN,
MOUSE_MOVE
} type;
union {
struct {
uint32_t key;
} key;
struct {
char *text;
uint32_t length;
} text_input;
struct {
float x, y;
} scroll;
struct {
uint8_t button;
Vec2<float> position;
} mouse_button;
struct {
Vec2<float> position;
Vec2<float> delta;
} mouse_move;
};
};
struct Window {
public:
// title can be free'd by the caller after the window is created, as it's copied internally.
Window(Vec2<float> size, const char *title);
~Window();
// Rendering functions
ImageID load_image(const char *path, bool pixel_art = false);
void unload_image(ImageID image_id);
FontID load_font(const char *path, int font_size, bool bold = false, bool italic = false, bool underline = false, bool pixel_art = false);
void unload_font(FontID font_id);
// text is copied internally, so it can be free'd by the caller after the text is created.
TextID create_text(FontID font_id, const char *text, uint32_t length, Color color);
void unload_text(TextID text_id);
bool write(TextID text_id, Vec2<float> position, float z, float angle = 0.0f, Vec2<float> scale = {1.0f, 1.0f});
bool draw(ImageID image_id, Vec2<float> position, float z, float angle = 0.0f, Vec2<float> scale = {1.0f, 1.0f}, float alpha = 1.0f);
bool draw_rect(Rect rect, Color color, float z, float angle = 0.0f);
void render(); // the rest of the calls only build the instruction list, this actually renders everything added to the screen (following z order)
void clear(); // clears the instruction list
// Event handling
bool poll(Event &event); // returns true if an event was polled, false otherwise
// consumes an event, (mostly for text input, to free the text input buffer)
// but also so the user can decide when they're done with the event explicitly.
void consume(Event &event);
// Text input
// Starts accepting Unicode text input events, and stops acceptong normal key down/up events for those keys.
void start_text_input();
void stop_text_input();
// Clipboard get
// To be free'd by the caller after use.
const char *paste();
// Time
uint64_t get_time(); // returns the time in milliseconds since the window was created
uint64_t delta_time(); // returns the time in milliseconds since the last render call
private:
// internal data and functions
SDL_Window *window;
SDL_Renderer *renderer;
Vec2<float> real_size;
Vec2<float> virtual_size;
Vec2<float> offset;
float scale;
// White pixel texture for drawing colored rectangles
SDL_Texture *white_tex;
// Image pool
std::vector<SDL_Texture *> images;
std::vector<int> free_image_indices;
// Font pool
std::vector<struct Font> fonts;
std::vector<int> free_font_indices;
// Text pool
std::vector<SDL_Texture *> texts;
std::vector<int> free_text_indices;
struct RenderInstruction {
enum Type {
DRAW_IMAGE,
DRAW_TEXT,
DRAW_RECT
} type;
float z;
float angle;
Vec2<float> scale;
union {
struct {
ImageID image_id;
Vec2<float> position;
float alpha;
} draw_image;
struct {
TextID text_id;
Vec2<float> position;
} draw_text;
struct {
Rect rect;
Color color;
} draw_rect;
};
};
std::vector<struct RenderInstruction> render_instructions;
bool text_input_active;
void compute_transform();
bool mouse_in_window(const Vec2<float> real_position, Vec2<float> &virtual_position);
void draw_letterbox();
};
#endif
+110
View File
@@ -0,0 +1,110 @@
Game Engine - With Mist a maze game
# What?
### The engine
- A high performance - low level game engine that provides abstactions for 2d games to be built on.
- A scene system
- A scene is a set of scene components used to create it,
- For example, a scene for menu, for game over screen, and most importantly, for the main game scene.
- A scene component can be one of,
- A basic geometric shape
- A text, with localization maybe?
- An image
- A game world
- A block, which allows merging other components into a single unit. (can be used to implement HUD systems)
- each component has coordinates on screen and z ordering.
- And they can handle clicks / keyboard events and have scripts attached to each.
- The bus
- any event can be fired on the bus by any object and then used by other objects for actions like enemy attacking player or something,
- as the game "world" captures input, the bus can be used to forward it to other objects, or just propogated through "update" loop functions
- they can be scoped with lifetime rules for tighter control and avoiding a event mess
- they need to be predefined in a centralized table for making debugging easier, but this can be optional.
- A game world is just the game world (everything is coordinates in the world map relative, and this object controls the camera), \
and it can be defined as topdown or side (for z ordering rules).
- everything is an object (defined later)
- with specail object for tiling spritesheets where a set of tiles can be defined and the engine will place the right kind of tile based on the scenario \
(for example in a maze if i define a spritesheet with tiles for corner / edge peices and then tell the maze as a grid of cells with 1 for wall and 0 \
for empty it will automatically place the correct tile from that list.)
- and support for maze generation and solving algorithms, which can be used with tiling object to create maze maps but also just usable for anything.
- pixel perfect or anti-aliased modes
- Every object is just a set of components added to it to form something in a game "world"
- An object joins together components to create an entity
- Some components could be
1. Body
- Shape (Dynamic or static)
- light level
- Mass, physics interactions
- Coordinates
2. Spritesheet (state based or singleton)/(animated or static)
- State based is for like player sprite can switch between states like facing direction etc.
- It will have an api to load and manage game sprites with animations handled automatically.
- The developer can define sprite locations for static and for dynamic a list of files or pixel \
areas within a bitmap image for each animation frame and the fps of the animation.
3. Render, only for those that dont use a Spritesheet
- Can be used for rendering a box or other simple shapes / text for fine grained control, this is automatically handled if a spritesheet exists.
4. Update
- Can be used to connect a script to run on every game loop for this object (delta based)
5. Event Subscription
- Can be used to subscribe to global / scoped events in the game event bus. (input can be handled through this or in update)
- In debug mode the collisions will be visible and other info like coords etc. logged for that game "world"
- It will have a physics engine for physics calculations like collisions, velocity, friction, forces, los, etc. attached to the game world
- It applies to all objects with collision bodies
- It has a center property that explains where the objects center is relative to the shape, and the size of the shape
- it has a mass which shows its inertia
- And they can be static or dynamic, rigid is used for walls and dynamic is for player / enemies etc. \
with an api to simulate a force on a dynamic object, with collision handling with other static objects to stop it and with other dynamic \
objects for firing collision events (for exmple checking if player collides with enemy)
- it also has setting for adding gravity (added for side view games.), or defining friction force relative to a coefficient defined per object for texturing.
- Calculations for interacting these objects in the world and updating their positions based on velocity / forces each game loop.
- Calculations for raycasting and testing if objects have line of sight.
- See if i can GPU accelerate los/raycasting. (and maybe use haskell for that bit, would be fun to show cross language c abi)
- Lighting support for fog based systems and shaders for common scenarios like clouds / fog and api to create custom shaders
- A light system to render torches/light systems
- With api to implement shaders for special effects
- These are GPU accelerated shaders
- owned by world
- An api for game "world" persistence. and generic data store.
- Normal classes / logic can be written outside and reused in the system.
- The scripting is done in mruby with most common ruby gems available. (the actual project will be in c++)
- The scene can be defined and setup in a markup-style custom ruby DSL.
- Then components in the scene have scripts for components connected to it (like linking it to a do end block or class or smthn)
- Maybe use ffi or libruby instead of mruby?
- And special predefined scenes for menu / settings types with settings having predefined settings for keybinds used.
- And the godot style project settings where we can define key value pairs with datatype for value that can be used as globals. \
and interfaced through autogenerated settings scene, or hidden but used as globals internally.
- And as a sample game implemented using it I will make mist.
### Mist - A maze game
- The main focus of the game is to show off all features of this engine.
- It also focuses on maze generation and solving with choices to select different types of maze generation algorithms and solving ai for npc's
- The main game has 4 modes
1. _Classic_ - A finite maze is generated on world creation and player has to fight enemies, collect resources / weapons and find the exit to win
2. _Tutorial_ - A smaller map with basic elements and npc's for player to inteact with and tooltips for helping them learn how the game works
3. _Infinite_ - A maze is procedurally generated as the player walks using predefined seeds and memory optimizations to allow storing such information, \
the goal is to get as far from spawn as possible without dying.
4. _Editor Mode_ - A mode in which teh user controls the map itself and can test out various maze generation algorithms and place npcs / rooms and tweak \
game rules to create custom game scenarios.
# Who?
- This game engine is targeted towards basic to intermediate indie developers trying out 2d game development for 2d open world like scenarios with \
physics etc. (in both topdown or side view mode)
- They can use the scene editor to make scene setups for all the visible stuff in teh game and then write scripts for behaviour of it using ruby syntax, \
which is a high level OOP interpreted langauge with a low learning curve due to its simple to learn syntax, and tons of helper functions for rapid development.
- It could be used by educators for teaching game development basics to students and getting hands on experience.
- This engine also indirectly effects the gamers, the end users as they are the ones to feel any performance costs when enjoying the games.
+185
View File
@@ -0,0 +1,185 @@
# Sample ruby DSL to create a game window.
width, height = 720, 480
image = Image.new "assets/loading.png"
window = Window.new width, height, title: "My Game", loading_bg: image
window.map_key :forward, "w"
window.map_key :backward, "s"
window.map_key :left, "a"
window.map_key :right, "d"
menu_scene = Scene.new
default_font = Font.new "assets/DejaVuSans.ttf", 12, italic: true
start_button = ElementText.new "Start Game", font: default_font, position: {x: 100, y: 100}
start_button.on_click do
window.start :game_scene
end
menu_scene.add_element start_button
game_scene = Scene.new
world = ElementWorld.new
# world.cam # camera object to control the view of the world, has position, zoom, rotation
# can define one of 5 types of components connectable to any entity
world.create_body, :sample_body do |entity|
# these properties exist in the dsl, and are used by the physics engine, but you can also add any custom properties you want to the body and use them in your controller code or whatever
shape = :rectangle
size = {width: 50, height: 50}
angle = 0
mass = 1 # this can be set to 0 for static bodies that should not be affected by physics, these bodies will not move but can still collide with other bodies (velocity will be ignored for static bodies)
velocity = {x: 1, y: 0}
collision = true # whether this body should collide with other bodies, if false it will not collide with anything and will not be affected by physics, but can still be moved by setting the position directly or by using a controller, useful for things like ghosts or whatever
end
world.create_sprite, :sample_sprite do |entity|
tiles = Tiles.from "assets/sprite.png", tile_width: 50, tile_height: 50
animation1 = {tiles: 5..6, frame_rate: 10, loop: true}
animations = {idle: animation1}
current_animation = :idle
end
world.create_light, :sample_light do |entity|
color = {r: 255, g: 255, b: 255, a: 255}
intensity = 1.0
radius = 100
# useful if using inbuilt fog light shader only
# mostly cuz i need it though most engines wouldnt have such a feature explicitly (it is dumb addition but i added it cuz i want to)
# it could be achived using other methods / custom properties but this is just a convenient way to do it
end
world.create_render, :sample_render do |entity|
# for extra rendering options like speech bubbles, health bars, etc
z_index = 0 # by default all are y-sorted, but can override with z_index for more control in here
renderer do |entity|
draw_rect 0, 0, 50, 50, {r: 255, g: 0, b: 0, a: 255} # example of custom rendering code for this entity (0,0 is the position of the entity)
draw_text "Hello World", default_font, 0, 0, {r: 255, g: 255, b: 255, a: 255}
end
# offset from the position of the entity, useful for things like speech bubbles or health bars that should be above the entity, etc
offset = {x: 0, y: 0}
end
world.create_controller, :sample_controller do |entity|
# event subscriptions for this entity
events = [:damage] # custom events that can be triggered on this entity, useful for things like taking damage, opening a chest, etc
# these are defined in the window and mapped to the actual keys, so you can use whatever keys you want and just map them to these actions in the window
# teh user can also remap them in the settings of the game
keyboard_inputs = [
:forward, :backward, :left, :right
]
# direct means only clicks on this entity will trigger the mouse inputs, useful for like chests or doors or whatever
# global means mouse inputs will be triggered as long as the mouse is within the window, useful for like aiming or whatever
# none means mouse inputs will not be triggered for this entity, useful for when the entity should not be interactable with the mouse
# on direct and global mouse mode, you can also get the mouse position relative to the entity or the world
mouse_input_mode = :direct
mouse_position_mode = :relative
mouse_inputs = [:left, :right, :hover]
# when multiple entities match the mouse input, the one with the highest priority will receive the input, useful for when you have multiple entities on top of each other and you want to control which one should receive the mouse input
mouse_priority = 100
# doesnt work without a body component (as they have no size)
update do |delta_time|
# example of custom controller code for this entity, delta_time is the time since the last update in ms, useful for making movement frame rate independent
if key_down?(:forward)
# move the entity forward, you can access the body component of the entity to move it with physics, or you can set the position directly for more arcade style movement, etc
# if the entity has a body component, you can also apply forces or impulses to it for more realistic physics interactions
# if the entity has a sprite component, you can also change the current animation based on the input or whatever
entity.body&.velocity.y = -1
elsif key_down?(:backward)
entity.body&.velocity.y = 1
else
entity.body&.velocity.y = 0
end
if colliding_with?([:enemies]) # list of groups to check collisions with
puts "Colliding with an enemy!"
end
if mouse_down?(:left)
# example of getting the mouse position relative to the entity or the world
puts "Mouse position relative to entity: #{mouse_position}"
end
end
on_event :damage do |amount|
# example of custom event handling code for this entity, amount is the damage amount passed when the event is triggered, useful for things like taking damage, opening a chest, etc
puts "Entity took #{amount} damage!"
entity.store :health, entity.fetch(:health, 100) - amount # example of using the entity's store to keep track of custom properties like health, etc
# these store properties cane be used in any components or at entity creation
end
end
world.add_entity "Enemy1" do |entity| # Name is optional (as entities can be referenced by group or name)
groups = [:enemies]
position = {x: 100, y: 100} # entities always have a position, even if they don't have a body component, this is useful for rendering and for entities that don't need physics but still need to be positioned in the world
body = :sample_body
sprite = :sample_sprite
render = :sample_render
controller = :sample_controller
entity.store :health, 100
end
world.add_entity "Trap Preview" do |entity|
groups = [:trap_previews]
position = {x: 0, y: 0}
sprite = :trap_sprite
disabled = true # this entity will not be updated or rendered normally
end
# systems are called on the world every update frame
world.add_system :physics # default basic physics + collisions system
world.add_system :trap_placement do |dt| # example from teh game mist trying to be made with this engine
# do trap placement logic when a trap is held
if world.fetch(:holding_trap, false) # example of using the world's store to keep track of global state like whether the player is currently holding a trap or not
if mouse_down?(:left)
# place a trap at the mouse position, you can use the mouse position relative to the world for this
world.add_entity "Trap" do |entity|
groups = [:traps]
position = mouse_position_world
body = :trap_body
sprite = :trap_sprite
render = :trap_render
controller = :trap_controller
end
world.store :holding_trap, false # stop placing traps until the player picks up another one
else
# show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this
world.entity("Trap Preview").update do |entity|
position = mouse_position_world
disabled = false # enable the trap preview entity to show it at the mouse position
end
end
else
# hide the trap preview when not holding a trap
world.entity("Trap Preview").update do |entity|
disabled = true
end
end
end
# this is used by teh engine like so
# when a bunch of enities reuse the same components, you can define the components once and then just add them to the entities, which is more efficient than defining the components for each entity separately
# and also for example all the entites that have a body component can be packed and calculated together in the physics engine, all the entities that have a sprite component can be packed and rendered together in the rendering engine, etc.
window.add_scene :menu_scene => menu_scene
window.add_scene :game_scene => game_scene
window.start :menu_scene # first scene to show when the game starts stops the loading bg
+70
View File
@@ -0,0 +1,70 @@
#include "../include/window/window.h"
int main() {
printf("Starting engine...\n");
Window w({800, 600}, "Render Test");
ImageID img = w.load_image("./assets/images/arrow.png", true);
FontID font = w.load_font(
"./assets/fonts/charybdis.ttf",
24,
false,
false,
false,
true
);
TextID text = w.create_text(font, "Hello Engine", 12, {255, 255, 255, 255});
float t = 0.0f;
bool running = true;
printf("Entering main loop...\n");
int i = 0;
while (running) {
Event e;
while (w.poll(e)) {
printf("Event polled: %d\n", e.type);
switch (e.type) {
case Event::QUIT:
printf("Exiting\n");
running = false;
break;
case Event::KEY_DOWN:
printf("Key down: %d\n", e.key.key);
if (e.key.key == 41) {
printf("Exiting\n");
running = false;
}
break;
default:
break;
}
w.consume(e);
}
w.clear();
// animated position
t += 0.016f;
float x = 200 + std::sin(t) * 100;
float y = 150 + std::cos(t) * 100;
// background rect (low z)
w.draw_rect({{0, 0}, {800, 600}}, {20, 20, 30, 255}, 0, 20.0f);
// image (mid z)
w.draw(img, {x, y}, 1.0f, t * 50.0f, {2.0f, 2.0f}, 1.0f);
// text (top z)
w.write(text, {0, 0}, 2.0f, 0.0f, {1.0f, 1.0f});
w.render();
}
return 0;
}
+110
View File
@@ -0,0 +1,110 @@
#include <SDL3/SDL.h>
#include <SDL3/SDL_events.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>
#include <stdio.h>
int main() {
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();
SDL_Window* window =
SDL_CreateWindow("SDL3 Demo", 800, 600, SDL_WINDOW_RESIZABLE);
SDL_Renderer* renderer =
SDL_CreateRenderer(window, NULL);
// log window size
int w, h;
SDL_GetWindowSize(window, &w, &h);
SDL_StartTextInput(window);
printf("Window size: %d x %d\n", w, h);
// ---------------- LOAD IMAGE ----------------
SDL_Texture* image = IMG_LoadTexture(renderer, "./assets/images/arrow.png");
SDL_SetTextureScaleMode(image, SDL_SCALEMODE_NEAREST);
// ---------------- LOAD FONT ----------------
TTF_Font* font = TTF_OpenFont("./assets/fonts/charybdis.ttf", 24);
SDL_Color white = {255, 255, 255, 255};
SDL_Surface* text_surface =
TTF_RenderText_Solid(font, "Hello SDL3 + TTF!", 17, white);
SDL_Texture* text_tex =
SDL_CreateTextureFromSurface(renderer, text_surface);
SDL_SetTextureScaleMode(text_tex, SDL_SCALEMODE_NEAREST);
int running = 1;
float angle = 0.0f;
while (running) {
SDL_Event e;
while (SDL_PollEvent(&e)) {
if (e.type == SDL_EVENT_QUIT) {
running = 0;
}
if (e.type == SDL_EVENT_TEXT_INPUT) {
printf("Text input: %s\n", e.text.text);
}
if (e.type == SDL_EVENT_KEY_DOWN) {
printf("key down: %d\n", e.key.key);
}
}
float mx, my;
SDL_GetMouseState(&mx, &my);
// ---------------- UPDATE ----------------
angle += 1.0f;
// ---------------- RENDER ----------------
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// ---- draw mouse box ----
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255);
SDL_FRect mouse_rect = { mx, my, 10, 10 };
SDL_RenderFillRect(renderer, &mouse_rect);
// ---- draw image (scaled + rotated) ----
SDL_FRect dst_img = { 200, 150, 200, 200 };
SDL_RenderTextureRotated(
renderer,
image,
NULL, // full texture
&dst_img,
angle, // rotation degrees
NULL, // center (NULL = center of rect)
SDL_FLIP_NONE
);
// ---- draw text ----
SDL_FRect dst_text = { 50, 50, 400, 50 };
SDL_RenderTexture(renderer, text_tex, NULL, &dst_text);
SDL_RenderPresent(renderer);
SDL_Delay(16);
}
// ---------------- CLEANUP ----------------
SDL_DestroyTexture(text_tex);
SDL_DestroyTexture(image);
TTF_CloseFont(font);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
return 0;
}
+534
View File
@@ -0,0 +1,534 @@
#include "../../include/window/window.h"
Window::Window(Vec2<float> size, const char *title) {
SDL_Init(SDL_INIT_VIDEO);
TTF_Init();
virtual_size = size;
window = SDL_CreateWindow(title, size.x, size.y, SDL_WINDOW_RESIZABLE);
renderer = SDL_CreateRenderer(window, NULL);
int w, h;
SDL_GetWindowSize(window, &w, &h);
real_size = {(float)w, (float)h};
compute_transform();
images.reserve(100);
free_image_indices.reserve(100);
fonts.reserve(100);
free_font_indices.reserve(100);
texts.reserve(100);
free_text_indices.reserve(100);
text_input_active = false;
white_tex = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STATIC,
1,
1
);
uint32_t pixel = 0xFFFFFFFF;
SDL_UpdateTexture(white_tex, nullptr, &pixel, sizeof(pixel));
};
Window::~Window() {
for (auto tex : images)
if (tex)
SDL_DestroyTexture(tex);
for (auto tex : texts)
if (tex)
SDL_DestroyTexture(tex);
for (auto &f : fonts)
if (f.font)
TTF_CloseFont(f.font);
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
}
ImageID Window::load_image(const char *path, bool pixel_art) {
SDL_Texture *texture = IMG_LoadTexture(renderer, path);
if (pixel_art) {
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
}
if (texture == nullptr) {
fprintf(stderr, "Failed to load image: %s\n", SDL_GetError());
return UINT32_MAX;
}
if (!free_image_indices.empty()) {
ImageID id = free_image_indices.back();
free_image_indices.pop_back();
images[id] = texture;
return id;
} else {
images.push_back(texture);
return images.size() - 1;
}
return UINT32_MAX;
}
void Window::unload_image(ImageID image_id) {
if (image_id >= images.size() || images[image_id] == nullptr) {
fprintf(stderr, "Invalid image ID: %d\n", image_id);
return;
}
SDL_DestroyTexture(images[image_id]);
images[image_id] = nullptr;
free_image_indices.push_back(image_id);
}
FontID Window::load_font(const char *path, int font_size, bool bold, bool italic, bool underline, bool pixel_art) {
TTF_Font *font = TTF_OpenFont(path, font_size);
if (font == nullptr) {
fprintf(stderr, "Failed to load font: %s\n", SDL_GetError());
return UINT32_MAX;
}
int style = TTF_STYLE_NORMAL;
if (bold)
style |= TTF_STYLE_BOLD;
if (italic)
style |= TTF_STYLE_ITALIC;
if (underline)
style |= TTF_STYLE_UNDERLINE;
TTF_SetFontStyle(font, style);
if (!free_font_indices.empty()) {
FontID id = free_font_indices.back();
free_font_indices.pop_back();
fonts[id] = {font, pixel_art};
return id;
} else {
fonts.push_back({font, pixel_art});
return fonts.size() - 1;
}
return UINT32_MAX;
}
void Window::unload_font(FontID font_id) {
if (font_id >= fonts.size() || fonts[font_id].font == nullptr) {
fprintf(stderr, "Invalid font ID: %d\n", font_id);
return;
}
TTF_CloseFont(fonts[font_id].font);
fonts[font_id].font = nullptr;
free_font_indices.push_back(font_id);
}
TextID Window::create_text(FontID font_id, const char *text, uint32_t length, Color color) {
if (font_id >= fonts.size() || fonts[font_id].font == nullptr) {
fprintf(stderr, "Invalid font ID: %d\n", font_id);
return UINT32_MAX;
}
SDL_Color sdl_color = {color.r, color.g, color.b, color.a};
SDL_Surface *surface = TTF_RenderText_Solid(fonts[font_id].font, text, length, sdl_color);
if (surface == nullptr) {
fprintf(stderr, "Failed to create text surface: %s\n", SDL_GetError());
return UINT32_MAX;
}
SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface);
SDL_DestroySurface(surface);
if (texture == nullptr) {
fprintf(stderr, "Failed to create text texture: %s\n", SDL_GetError());
return UINT32_MAX;
}
if (fonts[font_id].pixel_art)
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
if (!free_text_indices.empty()) {
TextID id = free_text_indices.back();
free_text_indices.pop_back();
texts[id] = texture;
return id;
} else {
texts.push_back(texture);
return texts.size() - 1;
}
return UINT32_MAX;
}
void Window::unload_text(TextID text_id) {
if (text_id >= texts.size() || texts[text_id] == nullptr) {
fprintf(stderr, "Invalid text ID: %d\n", text_id);
return;
}
SDL_DestroyTexture(texts[text_id]);
texts[text_id] = nullptr;
free_text_indices.push_back(text_id);
}
bool Window::write(TextID text_id, Vec2<float> position, float z, float angle, Vec2<float> scale) {
if (text_id >= texts.size() || texts[text_id] == nullptr) {
fprintf(stderr, "Invalid text ID: %d\n", text_id);
return false;
}
render_instructions.push_back(
{.type = RenderInstruction::DRAW_TEXT,
.z = z,
.angle = angle,
.scale = scale,
.draw_text = {
.text_id = text_id,
.position = position,
}}
);
return true;
};
bool Window::draw(ImageID image_id, Vec2<float> position, float z, float angle, Vec2<float> scale, float alpha) {
if (image_id >= images.size() || images[image_id] == nullptr) {
fprintf(stderr, "Invalid image ID: %d\n", image_id);
return false;
}
render_instructions.push_back(
{.type = RenderInstruction::DRAW_IMAGE,
.z = z,
.angle = angle,
.scale = scale,
.draw_image = {
.image_id = image_id,
.position = position,
.alpha = alpha,
}}
);
return true;
};
bool Window::draw_rect(Rect rect, Color color, float z, float angle) {
render_instructions.push_back(
{.type = RenderInstruction::DRAW_RECT,
.z = z,
.angle = angle,
.scale = {1.0f, 1.0f},
.draw_rect = {
.rect = rect,
.color = color,
}}
);
return true;
};
void Window::draw_letterbox() {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
// left bar
SDL_FRect left = {0, 0, offset.x, real_size.y};
SDL_RenderFillRect(renderer, &left);
// right bar
SDL_FRect right = {
real_size.x - offset.x,
0,
offset.x,
real_size.y
};
SDL_RenderFillRect(renderer, &right);
// top bar
SDL_FRect top = {0, 0, real_size.x, offset.y};
SDL_RenderFillRect(renderer, &top);
// bottom bar
SDL_FRect bottom = {
0,
real_size.y - offset.y,
real_size.x,
offset.y
};
SDL_RenderFillRect(renderer, &bottom);
}
void Window::render() {
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// sort render instructions by z value
std::stable_sort(
render_instructions.begin(),
render_instructions.end(),
[](const RenderInstruction &a, const RenderInstruction &b) {
return a.z < b.z;
}
);
for (const RenderInstruction &instruction : render_instructions) {
switch (instruction.type) {
case RenderInstruction::DRAW_TEXT: {
SDL_Texture *texture = texts[instruction.draw_text.text_id];
SDL_FlipMode flip = SDL_FLIP_NONE;
if (instruction.scale.x < 0)
flip = (SDL_FlipMode)(flip | SDL_FLIP_HORIZONTAL);
if (instruction.scale.y < 0)
flip = (SDL_FlipMode)(flip | SDL_FLIP_VERTICAL);
float scale_x = std::abs(instruction.scale.x) * scale;
float scale_y = std::abs(instruction.scale.y) * scale;
float w, h;
SDL_GetTextureSize(texture, &w, &h);
SDL_FRect dst_rect;
dst_rect.x = instruction.draw_text.position.x * scale + offset.x;
dst_rect.y = instruction.draw_text.position.y * scale + offset.y;
dst_rect.w = scale_x * w;
dst_rect.h = scale_y * h;
SDL_RenderTextureRotated(
renderer,
texture,
nullptr,
&dst_rect,
instruction.angle,
nullptr,
flip
);
} break;
case RenderInstruction::DRAW_IMAGE: {
SDL_Texture *texture = images[instruction.draw_image.image_id];
float w, h;
SDL_GetTextureSize(texture, &w, &h);
SDL_FlipMode flip = SDL_FLIP_NONE;
if (instruction.scale.x < 0)
flip = (SDL_FlipMode)(flip | SDL_FLIP_HORIZONTAL);
if (instruction.scale.y < 0)
flip = (SDL_FlipMode)(flip | SDL_FLIP_VERTICAL);
float scale_x = std::abs(instruction.scale.x) * scale;
float scale_y = std::abs(instruction.scale.y) * scale;
SDL_FRect dst_rect;
dst_rect.x = instruction.draw_image.position.x * scale + offset.x;
dst_rect.y = instruction.draw_image.position.y * scale + offset.y;
dst_rect.w = scale_x * w;
dst_rect.h = scale_y * h;
SDL_SetTextureAlphaMod(texture, static_cast<Uint8>(instruction.draw_image.alpha * 255));
SDL_RenderTextureRotated(
renderer,
texture,
nullptr,
&dst_rect,
instruction.angle,
nullptr,
flip
);
} break;
case RenderInstruction::DRAW_RECT: {
SDL_FRect dst_rect;
dst_rect.x = instruction.draw_rect.rect.x() * scale + offset.x;
dst_rect.y = instruction.draw_rect.rect.y() * scale + offset.y;
dst_rect.w = instruction.draw_rect.rect.w() * scale;
dst_rect.h = instruction.draw_rect.rect.h() * scale;
SDL_SetTextureColorMod(
white_tex,
instruction.draw_rect.color.r,
instruction.draw_rect.color.g,
instruction.draw_rect.color.b
);
SDL_SetTextureAlphaMod(white_tex, instruction.draw_rect.color.a);
SDL_RenderTextureRotated(
renderer,
white_tex,
nullptr,
&dst_rect,
instruction.angle,
nullptr,
SDL_FLIP_NONE
);
} break;
}
}
draw_letterbox();
SDL_RenderPresent(renderer);
}
void Window::clear() {
render_instructions.clear();
}
bool Window::poll(Event &event) {
SDL_Event sdl_event;
if (SDL_PollEvent(&sdl_event)) {
switch (sdl_event.type) {
case SDL_EVENT_QUIT:
event.type = Event::QUIT;
break;
case SDL_EVENT_KEY_DOWN:
if (text_input_active && sdl_event.key.key > '0' && sdl_event.key.key < 'z')
return false;
event.type = Event::KEY_DOWN;
event.key.key = sdl_event.key.scancode;
break;
case SDL_EVENT_KEY_UP:
if (text_input_active && sdl_event.key.key > '0' && sdl_event.key.key < 'z')
return false;
event.type = Event::KEY_UP;
event.key.key = sdl_event.key.scancode;
break;
case SDL_EVENT_TEXT_INPUT:
if (!text_input_active)
return false;
event.type = Event::TEXT_INPUT;
event.text_input.length = strlen(sdl_event.text.text);
event.text_input.text = (char *)malloc(event.text_input.length + 1);
strcpy(event.text_input.text, sdl_event.text.text);
event.text_input.text[event.text_input.length] = '\0';
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN: {
event.type = Event::MOUSE_BUTTON_DOWN;
event.mouse_button.button = sdl_event.button.button;
Vec2<float> virtual_position;
if (mouse_in_window({sdl_event.button.x, sdl_event.button.y}, virtual_position))
event.mouse_button.position = virtual_position;
else
return false;
} break;
case SDL_EVENT_MOUSE_BUTTON_UP: {
event.type = Event::MOUSE_BUTTON_UP;
event.mouse_button.button = sdl_event.button.button;
Vec2<float> virtual_position;
if (mouse_in_window({sdl_event.button.x, sdl_event.button.y}, virtual_position))
event.mouse_button.position = virtual_position;
else
return false;
} break;
case SDL_EVENT_MOUSE_MOTION: {
event.type = Event::MOUSE_MOVE;
event.mouse_move.delta = {sdl_event.motion.xrel, sdl_event.motion.yrel};
Vec2<float> virtual_position;
if (mouse_in_window({sdl_event.button.x, sdl_event.button.y}, virtual_position))
event.mouse_button.position = virtual_position;
else
return false;
} break;
case SDL_EVENT_MOUSE_WHEEL:
event.type = Event::SCROLL;
event.scroll.x = sdl_event.wheel.x;
event.scroll.y = sdl_event.wheel.y;
break;
case SDL_EVENT_WINDOW_RESIZED:
sdl_event.window.data1 = MAX(sdl_event.window.data1, 1);
sdl_event.window.data2 = MAX(sdl_event.window.data2, 1);
real_size = {(float)sdl_event.window.data1, (float)sdl_event.window.data2};
compute_transform();
break;
default:
return false;
}
return true;
}
return false;
}
void Window::consume(Event &event) {
if (event.type == Event::TEXT_INPUT) {
free(event.text_input.text);
event.text_input.text = nullptr;
event.text_input.length = 0;
}
event.type = Event::NONE;
}
void Window::start_text_input() {
text_input_active = true;
SDL_StartTextInput(window);
}
void Window::stop_text_input() {
text_input_active = false;
SDL_StopTextInput(window);
}
const char *Window::paste() {
const char *clipboard_text = SDL_GetClipboardText();
if (clipboard_text == nullptr) {
fprintf(stderr, "Failed to get clipboard text: %s\n", SDL_GetError());
return nullptr;
}
const char *copy = strdup(clipboard_text);
SDL_free((void *)clipboard_text);
return copy;
}
uint64_t Window::get_time() {
return SDL_GetTicks();
}
uint64_t Window::delta_time() {
static uint64_t last_time = 0;
uint64_t current_time = SDL_GetTicks();
uint64_t delta = current_time - last_time;
last_time = current_time;
return delta;
}
void Window::compute_transform() {
float scale_factor_x = real_size.x / virtual_size.x;
float scale_factor_y = real_size.y / virtual_size.y;
scale = MIN(scale_factor_x, scale_factor_y);
offset.x = (real_size.x - virtual_size.x * scale) / 2.0f;
offset.y = (real_size.y - virtual_size.y * scale) / 2.0f;
}
bool Window::mouse_in_window(const Vec2<float> real_position, Vec2<float> &virtual_position) {
virtual_position = (real_position - offset) / scale;
return virtual_position.x >= 0 && virtual_position.x <= virtual_size.x &&
virtual_position.y >= 0 && virtual_position.y <= virtual_size.y;
}