Compare commits

...

30 Commits

Author SHA1 Message Date
syedm 8a3da07c6c Cleanup 2026-07-26 19:59:14 +01:00
syedm e188501d19 Setup python env 2026-07-26 19:59:10 +01:00
syedm e0994b42c0 Add some useful mgems 2026-07-26 19:49:39 +01:00
syedm d484db4e84 Format files (using clang-format) 2026-07-26 19:17:24 +01:00
syedm f82ab718f3 Make fread call error handle 2026-07-26 19:12:32 +01:00
syedm 2f70c44ad5 Fix incorrect comparision 2026-07-26 19:10:53 +01:00
syedm 2006ee55ab Remove clang specific warning 2026-07-26 19:08:56 +01:00
syedm 9f4c345c74 Fix nix flake and use gcc as compiler 2026-07-26 19:04:49 +01:00
syedm edad8ff5cd Refractor directory name 2026-07-26 18:33:26 +01:00
syedm 1935be52ac try fixing flake 2026-07-26 18:29:13 +01:00
syedm a9054b3be4 Cleanup 2026-07-14 13:07:13 +01:00
syedm 7a9b44a735 Fix clangd 2026-07-08 15:41:53 +01:00
syedm 8f73f09b58 Remove setup script 2026-07-08 15:22:03 +01:00
syedm 9bd1ee7a29 Cleanup 2026-07-08 15:18:05 +01:00
syedm fabcad8dc8 Add flake.nix 2026-07-08 15:16:17 +01:00
syedm 9620b57b96 Merge notes and analysis 2026-07-03 14:39:07 +01:00
syedm 4eae85cffb Add design stage and touch up analysis writeup 2026-07-02 15:00:02 +01:00
syedm 5e1b92e683 Rearrange writeup 2026-07-02 14:44:32 +01:00
syedm 11135a56e8 Writeup update 2026-07-02 14:40:11 +01:00
syedm dd34bc6837 Updare writeup and gitignore 2026-07-02 14:26:45 +01:00
syedm 4c53e5fee6 Setup ecs system scaffolding 2026-07-02 13:09:11 +01:00
syedm 821255a065 Add tileset loading 2026-06-05 12:02:40 +01:00
syedm 419e95ac89 Remove element script and allow images to be pixel updated for similar behaviour 2026-06-05 11:00:15 +01:00
syedm e7418fde08 Added pixel texture element 2026-06-04 23:46:34 +01:00
syedm 55241b9c93 Fix missing function declaration. 2026-06-01 23:23:40 +01:00
syedm c24285b0af Refractor headers and split code into program files for app & localization files.
Split keybinds from window.h file. General cleanup
2026-06-01 23:20:07 +01:00
syedm dcb8b46245 chore: split monolithic header to proper program files, add vec2 ruby class to proxy element position etc., other cleanup 2026-06-01 22:13:50 +01:00
syedm 872d878ddc bugs and perfomance cleanup. 2026-05-21 21:56:36 +01:00
syedm 582b2fcd9e Make localization parameters better, and add a single frame animation helper 2026-05-21 19:01:44 +01:00
syedm e1d18743b5 Add global update loop, and cleanup 2026-05-21 09:43:13 +01:00
50 changed files with 3906 additions and 2830 deletions
+9 -1
View File
@@ -3,6 +3,14 @@
build/* build/*
bin/* bin/*
compile_commands.json
.clangd .clangd
include/binding/ruby_compiled.h include/bindings/ruby_compiled.h
p.vim
.cache/
result
+9
View File
@@ -2,3 +2,12 @@
path = libs/mruby path = libs/mruby
url = https://github.com/mruby/mruby.git url = https://github.com/mruby/mruby.git
ignore = dirty ignore = dirty
[submodule "libs/mgems/mruby-process"]
path = libs/mgems/mruby-process
url = https://github.com/katzer/mruby-process
[submodule "libs/mgems/mruby-env"]
path = libs/mgems/mruby-env
url = https://github.com/iij/mruby-env.git
[submodule "libs/mgems/mruby-marshal-c"]
path = libs/mgems/mruby-marshal-c
url = https://github.com/LanzaSchneider/mruby-marshal-c.git
+43 -45
View File
@@ -2,16 +2,16 @@ SRC_DIR := src
BIN_DIR := bin BIN_DIR := bin
OBJ_DIR := build OBJ_DIR := build
INCLUDE_DIR := include INCLUDE_DIR := include
SCRIPT_DIR := scripts
TARGET_DEBUG := $(BIN_DIR)/game-dbg TARGET_DEBUG := $(BIN_DIR)/misth-dbg
TARGET_RELEASE := $(BIN_DIR)/game TARGET_RELEASE := $(BIN_DIR)/misth
PCH_DEBUG := $(OBJ_DIR)/debug/pch.h.gch PCH_DEBUG := $(OBJ_DIR)/debug/pch.h.gch
PCH_RELEASE := $(OBJ_DIR)/release/pch.h.gch PCH_RELEASE := $(OBJ_DIR)/release/pch.h.gch
CCACHE := ccache CC ?= gcc
CXX := $(CCACHE) clang++ CXX ?= g++
CC := $(CCACHE) clang
# ---------------- SDL3 ---------------- # ---------------- SDL3 ----------------
@@ -19,32 +19,28 @@ 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_DEBUG := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image)
SDL_LIBS_RELEASE := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image) SDL_LIBS_RELEASE := $(shell pkg-config --libs sdl3 sdl3-ttf sdl3-image)
# ----------------- MRUBY --------------
MRUBY_CFLAGS := -I$(MRUBY_INCLUDE)
MRUBY_LIBS := $(MRUBY_LIB)
# ---------------- FLAGS ---------------- # ---------------- FLAGS ----------------
CFLAGS_DEBUG := \ CFLAGS_DEBUG := \
-std=c++20 -Wall -Wextra \ -std=c++20 -Wall -Wextra \
-O0 -g -fno-omit-frame-pointer \ -Og -g -fno-omit-frame-pointer \
-Wno-unused-command-line-argument \ -I./include -I./libs/mruby/build/host/include \
-I./include -I./libs/mruby/include \ $(SDL_CFLAGS) $(MRUBY_CFLAGS)
$(SDL_CFLAGS)
CFLAGS_RELEASE := \ CFLAGS_RELEASE := \
-std=c++20 -O3 -march=x86-64 -mtune=generic \ -std=c++20 -O3 -mtune=generic \
-flto=thin \
-fno-rtti -fomit-frame-pointer -DNDEBUG -s \ -fno-rtti -fomit-frame-pointer -DNDEBUG -s \
-Wno-unused-command-line-argument \ -I./include -I./libs/mruby/build/host/include \
-I./include -I./libs/mruby/include \ $(SDL_CFLAGS) $(MRUBY_CFLAGS)
$(SDL_CFLAGS)
PCH_CFLAGS_DEBUG := $(CFLAGS_DEBUG) -x c++-header PCH_CFLAGS_DEBUG := $(CFLAGS_DEBUG) -x c++-header
PCH_CFLAGS_RELEASE := $(CFLAGS_RELEASE) -x c++-header PCH_CFLAGS_RELEASE := $(CFLAGS_RELEASE) -x c++-header
# ---------------- LIBS ----------------
LIBS := ./libs/mruby/build/host/lib/libmruby.a
MGEM_DIR := ./libs/l_mgems
# ---------------- SOURCES ---------------- # ---------------- SOURCES ----------------
SRC := $(shell find $(SRC_DIR) -name "*.cc") SRC := $(shell find $(SRC_DIR) -name "*.cc")
@@ -57,54 +53,54 @@ DEP_RELEASE := $(OBJ_RELEASE:.o=.d)
# ---------------- TARGETS ---------------- # ---------------- TARGETS ----------------
.PHONY: all debug release clean setup mgems .PHONY: all debug release clean scripts
all: debug all: release
debug: mgems $(TARGET_DEBUG) debug: scripts $(TARGET_DEBUG)
release: mgems $(TARGET_RELEASE) release: scripts $(TARGET_RELEASE)
# ---------------- CLANGD ----------------
setup: setup.rb
@echo "Setting up clangd configuration..."
ruby setup.rb
# ---------------- MRB_MGEMS ---------------- # ---------------- MRB_MGEMS ----------------
mgems: scripts:
$(MGEM_DIR)/compile.sh bash $(SCRIPT_DIR)/compile.sh
# ---------------- PCH ---------------- # ---------------- PCH ----------------
$(PCH_DEBUG): $(INCLUDE_DIR)/pch.h $(PCH_DEBUG): $(INCLUDE_DIR)/pch.h
mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(CXX) $(PCH_CFLAGS_DEBUG) -o $@ $< @$(CXX) $(PCH_CFLAGS_DEBUG) -o $@ $<
@echo "Precompiled header (debug) generated at $@"
$(PCH_RELEASE): $(INCLUDE_DIR)/pch.h $(PCH_RELEASE): $(INCLUDE_DIR)/pch.h
mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(CXX) $(PCH_CFLAGS_RELEASE) -o $@ $< @$(CXX) $(PCH_CFLAGS_RELEASE) -o $@ $<
@echo "Precompiled header (release) generated at $@"
# ---------------- LINK ---------------- # ---------------- LINK ----------------
$(TARGET_DEBUG): $(PCH_DEBUG) $(OBJ_DEBUG) $(TARGET_DEBUG): $(PCH_DEBUG) $(OBJ_DEBUG)
mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
$(CXX) $(CFLAGS_DEBUG) -o $@ $(OBJ_DEBUG) $(SDL_LIBS_DEBUG) $(LIBS) @$(CXX) $(CFLAGS_DEBUG) -o $@ $(OBJ_DEBUG) $(SDL_LIBS_DEBUG) $(MRUBY_LIBS)
@echo "Debug build complete: $@"
$(TARGET_RELEASE): $(PCH_RELEASE) $(OBJ_RELEASE) $(TARGET_RELEASE): $(PCH_RELEASE) $(OBJ_RELEASE)
mkdir -p $(BIN_DIR) @mkdir -p $(BIN_DIR)
$(CXX) $(CFLAGS_RELEASE) -o $@ $(OBJ_RELEASE) $(SDL_LIBS_RELEASE) $(LIBS) @$(CXX) $(CFLAGS_RELEASE) -o $@ $(OBJ_RELEASE) $(SDL_LIBS_RELEASE) $(MRUBY_LIBS)
@echo "Release build complete: $@"
# ---------------- COMPILE ---------------- # ---------------- COMPILE ----------------
$(OBJ_DIR)/debug/%.o: $(SRC_DIR)/%.cc $(PCH_DEBUG) $(OBJ_DIR)/debug/%.o: $(SRC_DIR)/%.cc $(PCH_DEBUG)
mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(CXX) $(CFLAGS_DEBUG) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@ @$(CXX) $(CFLAGS_DEBUG) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@
@echo "[CXX] Debug object file generated: $@ from $<"
$(OBJ_DIR)/release/%.o: $(SRC_DIR)/%.cc $(PCH_RELEASE) $(OBJ_DIR)/release/%.o: $(SRC_DIR)/%.cc $(PCH_RELEASE)
mkdir -p $(dir $@) @mkdir -p $(dir $@)
$(CXX) $(CFLAGS_RELEASE) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@ @$(CXX) $(CFLAGS_RELEASE) -include $(INCLUDE_DIR)/pch.h -MMD -MP -c $< -o $@
@echo "[CXX] Release object file generated: $@ from $<"
# ---------------- DEP ---------------- # ---------------- DEP ----------------
@@ -114,4 +110,6 @@ $(OBJ_DIR)/release/%.o: $(SRC_DIR)/%.cc $(PCH_RELEASE)
# ---------------- CLEAN ---------------- # ---------------- CLEAN ----------------
clean: clean:
rm -rf $(OBJ_DIR) $(BIN_DIR) @rm -rf $(OBJ_DIR) $(BIN_DIR)
bash $(SCRIPT_DIR)/compile.sh clean
@echo "Cleaned build artifacts."
Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1783224372,
"narHash": "sha256-8i/87eeoqiGE4yOTjwSA3Eh/ziJRQEmd/unYU+K27sk=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "d407951447dcd00442e97087bf374aad70c04cea",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+130
View File
@@ -0,0 +1,130 @@
{
description = "Project Misth";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
self.submodules = true;
};
outputs =
{ nixpkgs, ... }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
mruby = pkgs.stdenv.mkDerivation {
pname = "mruby";
version = "0.0.1";
src = pkgs.lib.cleanSourceWith {
src = ./libs;
name = "mruby-src";
};
patches = [
./libs/changes.patch
];
nativeBuildInputs = with pkgs; [
ruby
gnumake
];
buildPhase = ''
cd mruby
rake
'';
installPhase = ''
mkdir -p $out
cp -r build/host/lib $out/lib
cp -r build/host/include $out/include
mkdir -p $out/bin
cp build/host/bin/mrbc $out/bin/mrbc
'';
};
misth = pkgs.stdenv.mkDerivation {
pname = "misth";
version = "0.1.0";
src = ./.;
nativeBuildInputs = with pkgs; [
pkg-config
gnumake
bear
xxd
bash
];
buildInputs = with pkgs; [
sdl3
sdl3-ttf
sdl3-image
mruby
];
buildPhase = ''
make \
MRBC=${mruby}/bin/mrbc \
MRUBY_INCLUDE=${mruby}/include \
MRUBY_LIB=${mruby}/lib/libmruby.a
'';
installPhase = ''
mkdir -p $out/bin
cp bin/misth $out/bin/misth
'';
};
pythonEnv = pkgs.python3.withPackages (
ps: with ps; [
pandas
matplotlib
]
);
in
{
packages.${system} = {
default = misth;
misth = misth;
mruby = mruby;
pythonEnv = pythonEnv;
};
devShells.${system}.default = pkgs.mkShell {
inputsFrom = [ misth ];
packages = with pkgs; [
clang-tools
pkg-config
ccache
gnumake
bear
ruby
xxd
sdl3
sdl3-ttf
sdl3-image
pythonEnv
ruff
];
shellHook = ''
export CC="ccache gcc"
export CXX="ccache g++"
export MRBC=${mruby}/bin/mrbc
export MRUBY_INCLUDE=${mruby}/include
export MRUBY_LIB=${mruby}/lib/libmruby.a
'';
};
};
}
+9 -323
View File
@@ -1,236 +1,12 @@
#ifndef APP_H #ifndef APP_H
#define APP_H #define APP_H
#include "binding/ruby.h" #include "app/scene.h"
#include "localization/localization.h" #include "bindings/ruby.h"
#include "utils.h" #include "utils.h"
#include "window/window.h" #include "window/window.h"
enum Type { IMAGE,
TEXT,
RECT };
enum ClickMode { PASS,
BLOCK,
IGNORE };
namespace app { namespace app {
struct Element {
Type type;
Vec2<float> position = {0, 0};
float rotation = 0;
Vec2<float> pivot = {0, 0};
Vec2<float> scale = {1, 1};
float alpha = 1.0f;
float z = 0;
ClickMode click_mode = ClickMode::BLOCK;
Ruby::Block on_click;
Element(Type type) : type(type) {}
virtual ~Element() = default;
virtual void render(Window &window, uint64_t abs_time) = 0;
virtual Vec2<float> size() = 0;
void click() {
on_click.call();
}
};
struct EImage : Element {
uint64_t animation_id = UINT64_MAX;
EImage(uint64_t animation_id) : Element(Type::IMAGE), animation_id(animation_id) {
animation_pool.use(animation_id);
}
~EImage() {
if (animation_id != UINT64_MAX)
animation_pool.release(animation_id);
}
Vec2<float> size() override {
Animation &animation = *animation_pool[animation_id];
if (animation.frames.empty())
return {0, 0};
return window.get_image_size(animation.frames[0]);
}
void render(Window &window, uint64_t abs_time) override {
Animation &animation = *animation_pool[animation_id];
if (animation.frames.empty())
return;
int frame_index = (int)((abs_time * animation.frame_rate) / 1e9) % animation.frames.size();
window.draw(animation.frames[frame_index], position, z, rotation, pivot, scale, alpha);
}
};
struct EText : Element {
Localization::Key key;
uint64_t font_id = UINT64_MAX;
Color color = {255, 255, 255};
mrb_value params = mrb_nil_value();
~EText() {
if (text_id != UINT64_MAX)
text_pool.release(text_id);
if (font_id != UINT64_MAX)
font_pool.release(font_id);
if (!mrb_nil_p(params))
mrb_gc_unregister(Ruby::mrb, params);
}
EText(Localization::Key key, uint64_t font_id, Color color, mrb_value params)
: Element(Type::TEXT), key(key), font_id(font_id), color(color), params(params) {
mrb_gc_register(Ruby::mrb, params);
font_pool.use(font_id);
}
void reload_params() {
// Force re-render by resetting last_key (without affecting this frame)
// this can be changed to use a dirty flag or version counter if needed
last_key = UINT16_MAX;
}
Vec2<float> size() override {
if (text_id == UINT64_MAX)
return {0, 0};
return window.get_text_size(text_id);
};
void render(Window &window, uint64_t) override {
if (
last_language != Localization::current_language
|| last_key != key || text_id == UINT64_MAX
|| last_color != color
) {
std::string localized_str = Localization::get(Ruby::mrb, key, params);
if (text_id != UINT64_MAX)
text_pool.release(text_id);
text_id = window.create_text(font_id, localized_str.c_str(), localized_str.size(), color);
last_language = Localization::current_language;
last_key = key;
last_color = color;
}
window.write(text_id, position, z, rotation, pivot, scale, alpha);
}
private:
uint64_t text_id = UINT64_MAX;
Color last_color = color;
Localization::LanguageCode last_language = UINT32_MAX;
Localization::Key last_key = UINT16_MAX;
};
struct ERect : Element {
Vec2<float> shape = {0, 0};
Color color = {255, 255, 255};
Vec2<float> size() override {
return shape;
};
ERect(Vec2<float> shape, Color color) : Element(Type::RECT), shape(shape), color(color) {}
void render(Window &window, uint64_t) override {
window.draw_rect(Rect::from(position, shape), color, z, rotation, pivot, scale, alpha);
}
};
inline Pool<Element> element_pool;
struct Scene {
std::vector<uint64_t> element_ids;
Ruby::Block update;
~Scene() {
for (const uint64_t &id : element_ids)
element_pool.release(id);
}
void add_element(uint64_t element_id) {
auto it = std::lower_bound(
element_ids.begin(),
element_ids.end(),
element_id,
[](auto &a, auto &b) {
auto e_a = element_pool[a];
auto e_b = element_pool[b];
return e_a->z < e_b->z || (e_a->z == e_b->z && a < b);
}
);
element_pool.use(element_id);
element_ids.insert(it, element_id);
}
void delete_element(uint64_t element_id) {
auto it = std::find(element_ids.begin(), element_ids.end(), element_id);
if (it != element_ids.end()) {
element_pool.release(element_id);
element_ids.erase(it);
}
}
void update_scene(uint64_t delta_time) {
if (mrb_nil_p(update.proc))
return;
mrb_value dt_val = mrb_float_value(Ruby::mrb, delta_time / 1e9);
update.call(1, &dt_val);
}
void click(Vec2<float> position) {
std::vector<uint64_t> clicked_elements = elements_at(position);
for (const uint64_t &id : clicked_elements)
element_pool[id]->click();
}
void render(Window &window, uint64_t abs_time) {
for (const uint64_t &e : element_ids)
element_pool[e]->render(window, abs_time);
}
std::vector<uint64_t> elements_at(Vec2<float> position) {
std::vector<uint64_t> result;
for (auto it = element_ids.rbegin(); it != element_ids.rend(); ++it) {
Element *e = element_pool[*it];
Vec2<float> size = e->size();
Rect rect = Rect::from(size);
if (rect.contains(world_to_local(position, e))) {
if (e->click_mode == ClickMode::PASS) {
result.push_back(*it);
} else if (e->click_mode == ClickMode::BLOCK) {
result.push_back(*it);
break;
}
}
}
return result;
}
Vec2<float> world_to_local(Vec2<float> p, Element *e) {
Vec2<float> size = e->size();
Vec2<float> pivot = e->pivot * size * e->scale;
p = p - e->position;
float rad = -e->rotation * (M_PI / 180.0f);
float c = cos(rad);
float s = sin(rad);
p = p - pivot;
p = {
p.x * c - p.y * s,
p.x * s + p.y * c
};
p = p + pivot;
p = p / e->scale;
return p;
}
};
inline Pool<Scene> scene_pool;
struct App { struct App {
uint64_t current_scene_id = 0; uint64_t current_scene_id = 0;
bool running = true; bool running = true;
@@ -240,109 +16,19 @@ struct App {
std::vector<Key> released_keys; std::vector<Key> released_keys;
Vec2<float> scroll = {0, 0}; Vec2<float> scroll = {0, 0};
std::string text = ""; std::string text = "";
Ruby::Block update;
void exit() { App(Vec2<float> size, const char *title);
running = false;
}
void switch_to_scene(uint64_t scene_id) { void exit();
scene_pool.release(current_scene_id);
scene_pool.use(scene_id);
current_scene_id = scene_id;
}
App(Vec2<float> size, const char *title) { void switch_to_scene(uint64_t scene_id);
window.update(size, title);
Scene *loading_scene = new Scene{};
current_scene_id = scene_pool.acquire(loading_scene);
}
void set_fps(float fps) { void set_fps(float fps);
this->fps = fps;
target_frame_time = 1e9 / fps;
}
uint64_t get_fps() const { uint64_t get_fps() const;
return fps;
}
void loop() { void loop();
uint64_t frame_start = window.get_time();
uint64_t frame_end = window.get_time();
uint64_t previous_frame_end = 0;
while (running) {
frame_start = window.get_time();
uint64_t frame_time = frame_start - frame_end;
if (frame_time <= target_frame_time) {
struct timespec req;
req.tv_sec = 0;
req.tv_nsec = target_frame_time - frame_time;
nanosleep(&req, nullptr);
}
previous_frame_end = frame_end;
frame_end = window.get_time();
float dt = frame_end - previous_frame_end;
Scene *scene = scene_pool[current_scene_id];
pressed_keys.clear();
released_keys.clear();
scroll = {0, 0};
text = "";
Event e;
while (window.poll(e)) {
switch (e.type) {
case Event::QUIT:
running = false;
break;
case Event::MOUSE_BUTTON_DOWN:
scene->click(e.mouse_button.position);
down_keys[e.mouse_button.button] = true;
pressed_keys.push_back(e.mouse_button.button);
break;
case Event::MOUSE_BUTTON_UP:
down_keys[e.mouse_button.button] = false;
released_keys.push_back(e.mouse_button.button);
break;
case Event::MOUSE_MOVE:
mouse_position = e.mouse_move.position;
break;
case Event::MOUSE_LEAVE:
mouse_position = {-1, -1};
break;
case Event::KEY_DOWN:
down_keys[e.key.key] = true;
pressed_keys.push_back(e.key.key);
break;
case Event::KEY_UP:
down_keys[e.key.key] = false;
released_keys.push_back(e.key.key);
break;
case Event::SCROLL:
scroll = e.scroll;
break;
case Event::TEXT_INPUT:
text += *e.text_input;
delete e.text_input;
break;
default:
break;
}
}
scene->update_scene(dt);
scene->render(window, window.get_time());
window.render();
window.clear();
}
}
private: private:
uint64_t fps = 60; uint64_t fps = 60;
+100
View File
@@ -0,0 +1,100 @@
#ifndef ELEMENTS_H
#define ELEMENTS_H
#include "bindings/ruby.h"
#include "localization/localization.h"
#include "utils.h"
#include "window/window.h"
enum Type { IMAGE,
TEXT,
RECT,
WORLD };
enum ClickMode { PASS,
BLOCK,
IGNORE };
namespace app {
struct Element {
Type type;
Vec2<float> position = {0, 0};
float rotation = 0;
Vec2<float> pivot = {0, 0};
Vec2<float> scale = {1, 1};
float alpha = 1.0f;
float z = 0;
ClickMode click_mode = ClickMode::BLOCK;
Ruby::Block on_click;
Element(Type type) : type(type) {}
virtual ~Element() = default;
virtual void render(uint64_t) = 0;
virtual Vec2<float> size() = 0;
void click();
};
struct EImage : Element {
uint64_t animation_id = UINT64_MAX;
EImage(uint64_t animation_id) : Element(Type::IMAGE), animation_id(animation_id) {}
~EImage();
Vec2<float> size() override;
void render(uint64_t) override;
};
struct EText : Element {
Localization::Key key;
uint64_t font_id = UINT64_MAX;
Color color = {255, 255, 255};
std::unordered_map<std::string, std::string> params;
~EText();
EText(Localization::Key key, uint64_t font_id, Color color, std::unordered_map<std::string, std::string> params)
: Element(Type::TEXT), key(key), font_id(font_id), color(color), params(params) {}
Vec2<float> size() override;
void reload_text();
void render(uint64_t) override;
private:
uint64_t text_id = UINT64_MAX;
Color last_color = color;
Localization::LanguageCode last_language = UINT32_MAX;
Localization::Key last_key = UINT16_MAX;
bool params_changed = true;
};
struct ERect : Element {
Vec2<float> shape = {0, 0};
Color color = {255, 255, 255};
Vec2<float> size() override;
ERect(Vec2<float> shape, Color color) : Element(Type::RECT), shape(shape), color(color) {}
void render(uint64_t) override;
};
struct EWorld : Element {
#warning need to implement ecs system first before implementing this
uint64_t world_id = UINT64_MAX;
EWorld(uint64_t world_id) : Element(Type::WORLD), world_id(world_id) {}
Vec2<float> size() override;
void render(uint64_t) override;
};
inline Pool<Element> element_pool;
} // namespace app
#endif
+33
View File
@@ -0,0 +1,33 @@
#ifndef SCENE_H
#define SCENE_H
#include "app/elements.h"
#include "bindings/ruby.h"
#include "utils.h"
#include "window/window.h"
namespace app {
struct Scene {
std::vector<uint64_t> element_ids;
Ruby::Block update;
~Scene();
void add_element(uint64_t element_id);
void delete_element(uint64_t element_id);
void update_scene(mrb_value dt_val);
void click(Vec2<float> position);
void render(uint64_t abs_time);
std::vector<uint64_t> elements_at(Vec2<float> position);
Vec2<float> world_to_local(Vec2<float> p, Element *e);
};
inline Pool<Scene> scene_pool;
} // namespace app
#endif
File diff suppressed because it is too large Load Diff
+63
View File
@@ -0,0 +1,63 @@
#ifndef DEFINITIONS_H
#define DEFINITIONS_H
#include "bindings/ruby.h"
namespace definitions {
struct Binding {
RClass *cls;
const mrb_data_type *type;
};
inline struct RubyBindings {
Binding Vec2;
Binding Params;
Binding Font;
Binding Image;
Binding Animation;
Binding ElementText;
Binding ElementRect;
Binding ElementImage;
Binding Scene;
} bindings;
inline mrb_value wrap(Binding binding, uint64_t id) {
RBasic *obj_b = mrb_obj_alloc(Ruby::mrb, MRB_TT_DATA, binding.cls);
mrb_value obj = mrb_obj_value(obj_b);
Wrapper *w = new Wrapper();
w->id = id;
mrb_data_init(obj, w, binding.type);
return obj;
}
mrb_value Vec2_wrap(uint64_t id, Vec2<float> *vec);
void register_vec2();
void register_params();
void register_font();
void register_image();
void register_animation();
void register_element_text();
void register_element_rect();
void register_element_image();
void register_scene();
void register_app();
inline void setup() {
register_vec2();
register_params();
register_font();
register_image();
register_animation();
register_element_text();
register_element_rect();
register_element_image();
register_scene();
register_app();
}
} // namespace definitions
#endif
@@ -4,7 +4,7 @@
#include "pch.h" #include "pch.h"
#include "utils.h" #include "utils.h"
#include "binding/ruby_compiled.h" #include "bindings/ruby_compiled.h"
#define DEF_SYM(name) inline mrb_sym sym_##name = mrb_intern_cstr(Ruby::mrb, #name); #define DEF_SYM(name) inline mrb_sym sym_##name = mrb_intern_cstr(Ruby::mrb, #name);
#define SYM(name) mrb_symbol_value(sym_##name) #define SYM(name) mrb_symbol_value(sym_##name)
@@ -20,8 +20,6 @@
out = mrb_float(v); \ out = mrb_float(v); \
else if (mrb_fixnum_p(v)) \ else if (mrb_fixnum_p(v)) \
out = (float)mrb_fixnum(v); \ out = (float)mrb_fixnum(v); \
else \
out = 0; \
} while (0) } while (0)
#define HASH_BOOL(h, key, out) \ #define HASH_BOOL(h, key, out) \
@@ -35,9 +33,15 @@ struct Wrapper {
uint64_t id; uint64_t id;
}; };
#define ADD_FUNCTION(NAME, FUNC, ARGS) \
mrb_define_method(Ruby::mrb, Ruby::mrb->object_class, NAME, FUNC, ARGS);
#define ADD_METHOD(NAME, METHOD, FUNC, ARGS) \ #define ADD_METHOD(NAME, METHOD, FUNC, ARGS) \
mrb_define_method(Ruby::mrb, NAME##_class, METHOD, FUNC, ARGS); mrb_define_method(Ruby::mrb, NAME##_class, METHOD, FUNC, ARGS);
#define ADD_CLASS_METHOD(CLASS, METHOD, FUNC, ARGS) \
mrb_define_class_method(Ruby::mrb, CLASS##_class, METHOD, FUNC, ARGS);
#define DEF_CLASS(NAME, ARGS) \ #define DEF_CLASS(NAME, ARGS) \
RClass *NAME##_class = mrb_define_class(Ruby::mrb, #NAME, Ruby::mrb->object_class); \ RClass *NAME##_class = mrb_define_class(Ruby::mrb, #NAME, Ruby::mrb->object_class); \
MRB_SET_INSTANCE_TT(NAME##_class, MRB_TT_CDATA); \ MRB_SET_INSTANCE_TT(NAME##_class, MRB_TT_CDATA); \
@@ -112,6 +116,8 @@ DEF_SYM(pass)
DEF_SYM(block) DEF_SYM(block)
DEF_SYM(ignore) DEF_SYM(ignore)
DEF_SYM(call) DEF_SYM(call)
DEF_SYM(padding)
DEF_SYM(offset)
DEF_SYM(x) DEF_SYM(x)
DEF_SYM(y) DEF_SYM(y)
DEF_SYM(w) DEF_SYM(w)
+14
View File
@@ -0,0 +1,14 @@
#ifndef ECS_H
#define ECS_H
#include "utils.h"
struct Entity {};
struct Component;
struct System;
struct EntityPool {
Entity entities[];
};
#endif
+12 -107
View File
@@ -1,3 +1,6 @@
#ifndef LOCALIZATION_H
#define LOCALIZATION_H
#include "pch.h" #include "pch.h"
#include "utils.h" #include "utils.h"
@@ -25,121 +28,23 @@ struct Segment {
inline std::unordered_map<JoinedKey, std::vector<Segment>> localized_strings; inline std::unordered_map<JoinedKey, std::vector<Segment>> localized_strings;
inline std::unordered_map<std::string, mrb_sym> var_sym_cache; inline std::unordered_map<std::string, mrb_sym> var_sym_cache;
inline mrb_sym get_var_sym(mrb_state *mrb, const std::string &name) { std::vector<Segment> parse_localized(const std::string &input);
auto it = var_sym_cache.find(name);
if (it != var_sym_cache.end())
return it->second;
mrb_sym sym = mrb_intern_cstr(mrb, name.c_str());
var_sym_cache[name] = sym;
return sym;
}
inline std::vector<Segment> parse_localized(const std::string &input) {
std::vector<Segment> out;
size_t i = 0;
while (i < input.size()) {
size_t start = input.find("%{", i);
if (start == std::string::npos) {
out.push_back({false, input.substr(i)});
break;
}
if (start > i) {
out.push_back({false, input.substr(i, start - i)});
}
size_t end = input.find("}", start);
if (end == std::string::npos) {
// malformed fallback
out.push_back({false, input.substr(start)});
break;
}
std::string var = input.substr(start + 2, end - (start + 2));
out.push_back({true, var});
i = end + 1;
}
return out;
}
inline void set(LanguageCode lang_code, Key key, const std::string &text) { inline void set(LanguageCode lang_code, Key key, const std::string &text) {
localized_strings[join_keys(lang_code, key)] = parse_localized(text); localized_strings[join_keys(lang_code, key)] = parse_localized(text);
} }
inline std::string get(mrb_state *mrb, Key key, mrb_value hash) { mrb_sym get_var_sym(mrb_state *mrb, const std::string &name);
auto it = localized_strings.find(join_keys(current_language, key));
if (it == localized_strings.end())
return "Localization not set!";
const auto &entry = it->second; std::string get(Key key, const std::unordered_map<std::string, std::string> &params);
std::string result; LanguageCode lang_from_sym(mrb_state *mrb, mrb_sym sym);
result.reserve(10);
for (const auto &seg : entry) { mrb_sym lang_to_sym(mrb_state *mrb, LanguageCode code);
if (!seg.is_var) {
result += seg.text;
} else {
mrb_sym sym = get_var_sym(mrb, seg.text);
if (mrb_nil_p(hash) || mrb_nil_p(mrb_hash_get(mrb, hash, mrb_symbol_value(sym)))) {
result += "%{" + seg.text + "}";
continue;
}
mrb_value val = mrb_hash_get(mrb, hash, mrb_symbol_value(sym));
if (mrb_nil_p(val))
continue;
result += mrb_str_to_cstr(mrb, val);
}
}
return result; Key key_from_sym(mrb_state *mrb, mrb_sym sym);
}
inline LanguageCode lang_from_sym(mrb_state *mrb, mrb_sym sym) { std::string get_key_name(Key key);
const char *s = mrb_sym2name(mrb, sym);
size_t len = strlen(s);
if (len == 0 || len > 4)
return 0;
LanguageCode code = 0;
for (size_t i = 0; i < len; ++i) {
code |= (static_cast<LanguageCode>(s[i]) << (8 * (3 - i)));
}
return code;
}
inline mrb_sym lang_to_sym(mrb_state *mrb, LanguageCode code) {
char s[5] = {0};
for (int i = 0; i < 4; ++i)
s[i] = (code >> (8 * (3 - i))) & 0xFF;
return mrb_intern_cstr(mrb, s);
}
inline Key key_from_sym(mrb_state *mrb, mrb_sym sym) {
const char *key_name = mrb_sym2name(mrb, sym);
auto it = localization_key_map.find(key_name);
if (it != localization_key_map.end())
return it->second;
Key new_id = (Key)localization_key_map.size();
localization_key_map[key_name] = new_id;
return new_id;
}
// This is only used for debugging and inspect output, so performance is not a concern
inline std::string get_key_name(Key key) {
for (const auto &[str, k] : localization_key_map)
if (k == key)
return str;
return "Unknown Key";
}
} // namespace Localization } // namespace Localization
#endif
-1
View File
@@ -58,7 +58,6 @@
#include <shared_mutex> #include <shared_mutex>
#include <signal.h> #include <signal.h>
#include <stack> #include <stack>
#include <stdio.h>
#include <string.h> #include <string.h>
#include <string> #include <string>
#include <sys/ioctl.h> #include <sys/ioctl.h>
+6 -3
View File
@@ -163,6 +163,10 @@ struct Rect {
template <typename T = void *> template <typename T = void *>
struct Pool { struct Pool {
uint32_t size() const {
return slots.size() - free_indices.size();
}
T *operator[](uint64_t index) { T *operator[](uint64_t index) {
if (!is_valid(index)) { if (!is_valid(index)) {
fprintf(stderr, "Invalid pool index: %lu\n", (uint64_t)index); fprintf(stderr, "Invalid pool index: %lu\n", (uint64_t)index);
@@ -191,7 +195,7 @@ struct Pool {
slots.push_back({}); slots.push_back({});
} }
slots[id].ptr = item; slots[id].ptr = item;
slots[id].refcount = 1; slots[id].refcount = 0;
slots[id].alive = true; slots[id].alive = true;
slots[id].generation++; slots[id].generation++;
uint64_t index = ((uint64_t)slots[id].generation << 32) | id; uint64_t index = ((uint64_t)slots[id].generation << 32) | id;
@@ -222,8 +226,7 @@ struct Pool {
bool is_valid(uint64_t index) const { bool is_valid(uint64_t index) const {
uint32_t id = (uint32_t)(index & 0xFFFFFFFFULL); uint32_t id = (uint32_t)(index & 0xFFFFFFFFULL);
uint32_t gen = (uint32_t)(index >> 32); uint32_t gen = (uint32_t)(index >> 32);
return id >= 0 return id < slots.size()
&& id < slots.size()
&& slots[id].alive && slots[id].alive
&& slots[id].generation == gen; && slots[id].generation == gen;
} }
+221
View File
@@ -0,0 +1,221 @@
#ifndef KEYBINDS_H
#define KEYBINDS_H
#include "pch.h"
#include "utils.h"
enum Key : uint16_t {
Unknown = SDL_SCANCODE_UNKNOWN,
A = SDL_SCANCODE_A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Num0 = SDL_SCANCODE_0,
Num1,
Num2,
Num3,
Num4,
Num5,
Num6,
Num7,
Num8,
Num9,
Space = SDL_SCANCODE_SPACE,
Enter = SDL_SCANCODE_RETURN,
Escape = SDL_SCANCODE_ESCAPE,
Tab = SDL_SCANCODE_TAB,
Backspace = SDL_SCANCODE_BACKSPACE,
LeftShift = SDL_SCANCODE_LSHIFT,
RightShift = SDL_SCANCODE_RSHIFT,
LeftCtrl = SDL_SCANCODE_LCTRL,
RightCtrl = SDL_SCANCODE_RCTRL,
LeftAlt = SDL_SCANCODE_LALT,
RightAlt = SDL_SCANCODE_RALT,
CapsLock = SDL_SCANCODE_CAPSLOCK,
Up = SDL_SCANCODE_UP,
Down = SDL_SCANCODE_DOWN,
Left = SDL_SCANCODE_LEFT,
Right = SDL_SCANCODE_RIGHT,
Home = SDL_SCANCODE_HOME,
End = SDL_SCANCODE_END,
PageUp = SDL_SCANCODE_PAGEUP,
PageDown = SDL_SCANCODE_PAGEDOWN,
Delete = SDL_SCANCODE_DELETE,
Insert = SDL_SCANCODE_INSERT,
Minus = SDL_SCANCODE_MINUS,
Equals = SDL_SCANCODE_EQUALS,
LeftBracket = SDL_SCANCODE_LEFTBRACKET,
RightBracket = SDL_SCANCODE_RIGHTBRACKET,
Semicolon = SDL_SCANCODE_SEMICOLON,
Apostrophe = SDL_SCANCODE_APOSTROPHE,
Grave = SDL_SCANCODE_GRAVE,
Comma = SDL_SCANCODE_COMMA,
Period = SDL_SCANCODE_PERIOD,
Slash = SDL_SCANCODE_SLASH,
Backslash = SDL_SCANCODE_NONUSBACKSLASH,
F1 = SDL_SCANCODE_F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
MouseLeft = 513,
MouseMiddle,
MouseRight
};
constexpr std::pair<std::string, Key> KEY_NAMES[] = {
{"a", Key::A},
{"b", Key::B},
{"c", Key::C},
{"d", Key::D},
{"e", Key::E},
{"f", Key::F},
{"g", Key::G},
{"h", Key::H},
{"i", Key::I},
{"j", Key::J},
{"k", Key::K},
{"l", Key::L},
{"m", Key::M},
{"n", Key::N},
{"o", Key::O},
{"p", Key::P},
{"q", Key::Q},
{"r", Key::R},
{"s", Key::S},
{"t", Key::T},
{"u", Key::U},
{"v", Key::V},
{"w", Key::W},
{"x", Key::X},
{"y", Key::Y},
{"z", Key::Z},
{"num0", Key::Num0},
{"num1", Key::Num1},
{"num2", Key::Num2},
{"num3", Key::Num3},
{"num4", Key::Num4},
{"num5", Key::Num5},
{"num6", Key::Num6},
{"num7", Key::Num7},
{"num8", Key::Num8},
{"num9", Key::Num9},
{"space", Key::Space},
{"enter", Key::Enter},
{"escape", Key::Escape},
{"tab", Key::Tab},
{"backspace", Key::Backspace},
{"left_shift", Key::LeftShift},
{"right_shift", Key::RightShift},
{"left_ctrl", Key::LeftCtrl},
{"right_ctrl", Key::RightCtrl},
{"left_alt", Key::LeftAlt},
{"right_alt", Key::RightAlt},
{"capslock", Key::CapsLock},
{"up", Key::Up},
{"down", Key::Down},
{"left", Key::Left},
{"right", Key::Right},
{"home", Key::Home},
{"end", Key::End},
{"page_up", Key::PageUp},
{"page_down", Key::PageDown},
{"delete", Key::Delete},
{"insert", Key::Insert},
{"minus", Key::Minus},
{"equals", Key::Equals},
{"left_bracket", Key::LeftBracket},
{"right_bracket", Key::RightBracket},
{"semicolon", Key::Semicolon},
{"apostrophe", Key::Apostrophe},
{"grave", Key::Grave},
{"comma", Key::Comma},
{"period", Key::Period},
{"slash", Key::Slash},
{"backslash", Key::Backslash},
{"f1", Key::F1},
{"f2", Key::F2},
{"f3", Key::F3},
{"f4", Key::F4},
{"f5", Key::F5},
{"f6", Key::F6},
{"f7", Key::F7},
{"f8", Key::F8},
{"f9", Key::F9},
{"f10", Key::F10},
{"f11", Key::F11},
{"f12", Key::F12},
{"mouse_left", Key::MouseLeft},
{"mouse_middle", Key::MouseMiddle},
{"mouse_right", Key::MouseRight}
};
inline Key get_key_from_name(std::string_view name) {
for (const auto &[key_name, key] : KEY_NAMES)
if (key_name == name)
return key;
return Key::Unknown;
}
inline std::unordered_map<std::string, Key> action_key_map;
inline void set_action_mapping(const std::string &action_name, const std::string &key_name) {
Key key = get_key_from_name(key_name);
if (key != Key::Unknown)
action_key_map[action_name] = key;
}
inline Key get_key(std::string &name) {
auto it = action_key_map.find(name);
if (it != action_key_map.end())
return it->second;
return get_key_from_name(name);
}
#endif
+12 -215
View File
@@ -3,6 +3,7 @@
#include "pch.h" #include "pch.h"
#include "utils.h" #include "utils.h"
#include "window/keybinds.h"
struct Image { struct Image {
SDL_Texture *texture; SDL_Texture *texture;
@@ -40,220 +41,6 @@ inline Pool<Font> font_pool;
// Text pool // Text pool
inline Pool<Text> text_pool; inline Pool<Text> text_pool;
enum Key : uint16_t {
Unknown = SDL_SCANCODE_UNKNOWN,
A = SDL_SCANCODE_A,
B,
C,
D,
E,
F,
G,
H,
I,
J,
K,
L,
M,
N,
O,
P,
Q,
R,
S,
T,
U,
V,
W,
X,
Y,
Z,
Num0 = SDL_SCANCODE_0,
Num1,
Num2,
Num3,
Num4,
Num5,
Num6,
Num7,
Num8,
Num9,
Space = SDL_SCANCODE_SPACE,
Enter = SDL_SCANCODE_RETURN,
Escape = SDL_SCANCODE_ESCAPE,
Tab = SDL_SCANCODE_TAB,
Backspace = SDL_SCANCODE_BACKSPACE,
LeftShift = SDL_SCANCODE_LSHIFT,
RightShift = SDL_SCANCODE_RSHIFT,
LeftCtrl = SDL_SCANCODE_LCTRL,
RightCtrl = SDL_SCANCODE_RCTRL,
LeftAlt = SDL_SCANCODE_LALT,
RightAlt = SDL_SCANCODE_RALT,
CapsLock = SDL_SCANCODE_CAPSLOCK,
Up = SDL_SCANCODE_UP,
Down = SDL_SCANCODE_DOWN,
Left = SDL_SCANCODE_LEFT,
Right = SDL_SCANCODE_RIGHT,
Home = SDL_SCANCODE_HOME,
End = SDL_SCANCODE_END,
PageUp = SDL_SCANCODE_PAGEUP,
PageDown = SDL_SCANCODE_PAGEDOWN,
Delete = SDL_SCANCODE_DELETE,
Insert = SDL_SCANCODE_INSERT,
Minus = SDL_SCANCODE_MINUS,
Equals = SDL_SCANCODE_EQUALS,
LeftBracket = SDL_SCANCODE_LEFTBRACKET,
RightBracket = SDL_SCANCODE_RIGHTBRACKET,
Semicolon = SDL_SCANCODE_SEMICOLON,
Apostrophe = SDL_SCANCODE_APOSTROPHE,
Grave = SDL_SCANCODE_GRAVE,
Comma = SDL_SCANCODE_COMMA,
Period = SDL_SCANCODE_PERIOD,
Slash = SDL_SCANCODE_SLASH,
Backslash = SDL_SCANCODE_NONUSBACKSLASH,
F1 = SDL_SCANCODE_F1,
F2,
F3,
F4,
F5,
F6,
F7,
F8,
F9,
F10,
F11,
F12,
MouseLeft = 513,
MouseMiddle,
MouseRight
};
constexpr std::pair<std::string, Key> KEY_NAMES[] = {
{"a", Key::A},
{"b", Key::B},
{"c", Key::C},
{"d", Key::D},
{"e", Key::E},
{"f", Key::F},
{"g", Key::G},
{"h", Key::H},
{"i", Key::I},
{"j", Key::J},
{"k", Key::K},
{"l", Key::L},
{"m", Key::M},
{"n", Key::N},
{"o", Key::O},
{"p", Key::P},
{"q", Key::Q},
{"r", Key::R},
{"s", Key::S},
{"t", Key::T},
{"u", Key::U},
{"v", Key::V},
{"w", Key::W},
{"x", Key::X},
{"y", Key::Y},
{"z", Key::Z},
{"num0", Key::Num0},
{"num1", Key::Num1},
{"num2", Key::Num2},
{"num3", Key::Num3},
{"num4", Key::Num4},
{"num5", Key::Num5},
{"num6", Key::Num6},
{"num7", Key::Num7},
{"num8", Key::Num8},
{"num9", Key::Num9},
{"space", Key::Space},
{"enter", Key::Enter},
{"escape", Key::Escape},
{"tab", Key::Tab},
{"backspace", Key::Backspace},
{"left_shift", Key::LeftShift},
{"right_shift", Key::RightShift},
{"left_ctrl", Key::LeftCtrl},
{"right_ctrl", Key::RightCtrl},
{"left_alt", Key::LeftAlt},
{"right_alt", Key::RightAlt},
{"capslock", Key::CapsLock},
{"up", Key::Up},
{"down", Key::Down},
{"left", Key::Left},
{"right", Key::Right},
{"home", Key::Home},
{"end", Key::End},
{"page_up", Key::PageUp},
{"page_down", Key::PageDown},
{"delete", Key::Delete},
{"insert", Key::Insert},
{"minus", Key::Minus},
{"equals", Key::Equals},
{"left_bracket", Key::LeftBracket},
{"right_bracket", Key::RightBracket},
{"semicolon", Key::Semicolon},
{"apostrophe", Key::Apostrophe},
{"grave", Key::Grave},
{"comma", Key::Comma},
{"period", Key::Period},
{"slash", Key::Slash},
{"backslash", Key::Backslash},
{"f1", Key::F1},
{"f2", Key::F2},
{"f3", Key::F3},
{"f4", Key::F4},
{"f5", Key::F5},
{"f6", Key::F6},
{"f7", Key::F7},
{"f8", Key::F8},
{"f9", Key::F9},
{"f10", Key::F10},
{"f11", Key::F11},
{"f12", Key::F12},
{"mouse_left", Key::MouseLeft},
{"mouse_middle", Key::MouseMiddle},
{"mouse_right", Key::MouseRight}
};
inline Key get_key_from_name(std::string_view name) {
for (const auto &[key_name, key] : KEY_NAMES)
if (key_name == name)
return key;
return Key::Unknown;
}
inline std::unordered_map<std::string, Key> action_key_map;
inline void set_action_mapping(const std::string &action_name, const std::string &key_name) {
Key key = get_key_from_name(key_name);
if (key != Key::Unknown)
action_key_map[action_name] = key;
}
inline Key get_key(std::string &name) {
auto it = action_key_map.find(name);
if (it != action_key_map.end())
return it->second;
return get_key_from_name(name);
}
struct Event { struct Event {
enum Type { enum Type {
NONE, NONE,
@@ -311,8 +98,11 @@ public:
~Window(); ~Window();
// Rendering functions // Rendering functions
std::vector<uint64_t> load_image_set(const char *path, Vec2<float> tile_size, Vec2<float> padding, Vec2<float> offset, bool pixel_art);
uint64_t load_image(const char *path, bool pixel_art); uint64_t load_image(const char *path, bool pixel_art);
uint64_t create_image(Vec2<float> size, bool pixel_art);
Vec2<float> get_image_size(uint64_t image_id); Vec2<float> get_image_size(uint64_t image_id);
bool update_image(uint64_t image_id, Rect content_rect, const char *new_content, uint32_t length);
uint64_t load_font(const char *path, int font_size, bool bold, bool italic, bool underline, bool pixel_art); uint64_t load_font(const char *path, int font_size, bool bold, bool italic, bool underline, bool pixel_art);
@@ -326,6 +116,8 @@ public:
bool draw_rect(Rect rect, Color color, float z, float angle, Vec2<float> pivot, Vec2<float> scale, float alpha); bool draw_rect(Rect rect, Color color, float z, float angle, Vec2<float> pivot, Vec2<float> scale, float alpha);
bool draw_scripted(uint64_t scripted_id, Vec2<float> position, float z, float angle, Vec2<float> pivot, Vec2<float> scale, float alpha);
void render(); // the rest of the calls only build the instruction list, this actually renders everything added to the screen (following z order) 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 void clear(); // clears the instruction list
@@ -384,10 +176,15 @@ private:
Rect rect; Rect rect;
Color color; Color color;
} draw_rect; } draw_rect;
struct {
uint64_t scripted_id;
Vec2<float> position;
} draw_scripted;
}; };
}; };
std::vector<struct RenderInstruction> render_instructions; std::vector<RenderInstruction> render_instructions;
void compute_transform(); void compute_transform();
+53
View File
@@ -0,0 +1,53 @@
--- ./mruby/build_config/default.rb
+++ ./mruby/build_config/default.rb
@@ -1,6 +1,18 @@
MRuby::Build.new do |conf|
# load specific toolchain settings
- conf.toolchain
+ conf.toolchain :gcc
+
+ conf.cc do |cc|
+ cc.command = ENV['CC']
+ end
+
+ conf.cxx do |cxx|
+ cxx.command = ENV['CXX']
+ end
+
+ conf.linker do |linker|
+ linker.command = ENV['CXX']
+ end
# Use mrbgems
# conf.gem 'examples/mrbgems/ruby_extension_example'
@@ -16,6 +28,10 @@
# include the GEM box
conf.gembox 'default'
+ conf.gem '../mgems/mruby-env'
+ conf.gem '../mgems/mruby-process'
+ conf.gem '../mgems/mruby-marshal-c'
+
# C compiler settings
# conf.cc do |cc|
# cc.command = ENV['CC'] || 'gcc'
@@ -79,5 +79,5 @@
# Turn on `enable_debug` for better debugging
# conf.enable_debug
- conf.enable_bintest
- conf.enable_test
+ # conf.enable_bintest
+ # conf.enable_test
end
--- ./mgems/mruby-process/mrbgem.rake
+++ ./mgems/mruby-process/mrbgem.rake
@@ -54,4 +54,9 @@
else
spec.objs.delete objfile("#{build_dir}/src/win32")
end
+
+ spec.objs.delete objfile("#{build_dir}/src/internal")
+ spec.objs.delete objfile("#{build_dir}/src/dln")
+ spec.objs.delete objfile("#{build_dir}/src/signal")
+ spec.objs.delete objfile("#{build_dir}/src/status")
end
-103
View File
@@ -1,103 +0,0 @@
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 elements used to create it,
- For example, a scene for menu, for game over screen, and most importantly, for the main game scene.
- A scene element can be one of,
- A basic geometric shape
- A text, with localization codes and text db
- An image
- A game world
- A scripted element, for stuff like minimaps
- each element 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,
- they can be scoped with lifetime rules for tighter control and avoiding a event mess
- 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 entity
- with specail entities 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 entity to create maze maps but also just usable for anything.
- Every entity is just a set of components added to it to form something in a game "world"
- An entity joins together components to create an entity
- Components are:
1. Body
- Shape (Dynamic or static)
- 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. Controlles
- Can be used to connect a script to run on every game loop for this object (delta based)
- 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)
5. Light
- Light level info for fog light shader
- 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.
- 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.
- 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.
+57 -15
View File
@@ -4,48 +4,87 @@ main_scene = Scene.new
default_font = Font.new "assets/fonts/charybdis.ttf", 24, pixel_art: true default_font = Font.new "assets/fonts/charybdis.ttf", 24, pixel_art: true
bg_image1 = Image.new "assets/images/menu_bg.png", pixel_art: true bg_image1 = Image.load "assets/images/menu_bg.png", pixel_art: true
bg_image2 = Image.new "assets/images/game_over_dead_bg.png", pixel_art: true bg_image2 = Image.load "assets/images/game_over_dead_bg.png", pixel_art: true
tile_set = Image.load_set "assets/images/walls.png", 20, 20, padding: {x: 0, y: 20}, offset: {x: 0, y: 0}, pixel_art: true
# tile_set is an array of images, so we can create an animation from it directly or use the images separately
pp tile_set, "Tile set size: #{tile_set.size}"
tile_animation = Animation.new tile_set, fps: 10
bg_animation = Animation.new [bg_image1, bg_image2], fps: 5 bg_animation = Animation.new [bg_image1, bg_image2], fps: 5
bg_image1_a = Animation.from(bg_image1)
App.language = :en App.language = :en
App.localize(:en, :btn_text, "Click %{name}!") App.localize(:en, :btn_text, "Click %{name}!")
text = ElementText.new :btn_text, default_font, 0xFFFF00, {name: "Me"}, position: {x: 50, y: 50}, z: 1, click_mode: :pass, alpha: 1 text = ElementText.new :btn_text, default_font, 0xFFFF00, {name: ENV.fetch("name", "me")}, position: {x: 50, y: 50}, z: 1, click_mode: :block, alpha: 1
rect = ElementRect.new 100, 25, 0xFF0000, position: {x: 50, y: 50}, z: 0, rotation: 60, scale: { x: 1, y: 1 }, alpha: 0.4 rect = ElementRect.new 100, 25, 0xFF0000, position: {x: 50, y: 50}, z: 0, rotation: 60, scale: { x: 1, y: 1 }, alpha: 0.8, click_mode: :block
image_element = ElementImage.new bg_animation, position: {x: 0, y: 0}, z: -1, click_mode: :ignore, alpha: 1 image_element = ElementImage.new tile_animation, position: {x: 0, y: 0}, scale: { x: 20, y: 20 }, z: -1, click_mode: :block, alpha: 1
pp text, rect, image_element image1 = Image.new 50, 50, pixel_art: true
image2_animation = Animation.from(image1)
image_element2 = ElementImage.new image2_animation, position: {x: 200, y: 200}, z: 0, click_mode: :block, alpha: 1
pp text, rect, image_element, image_element2
end_scene = Scene.new
end_scene << rect
text.click do
#App.switch_to end_scene
end
image_element.click do
pp "Image clicked"
end
rect.click do
pp "Rect clicked", rect.color.to_s(16)
rect.on_click do
rect.color = rect.color == 0xFF0000 ? rect.color = rect.color == 0xFF0000 ?
0x00FF00 : 0x00FF00 :
0xFF0000 0xFF0000
main_scene << text unless main_scene.elements.include?(text)
pp text.params[:name]
text.params[:name] = text.params[:name] == "Me" ? text.params[:name] = text.params[:name] == "Me" ?
"You" : "You" :
"Me" "Me"
text.params!
end end
App.map_key :mouse_left, :action_name App.map_key :mouse_left, :action_name
App.map_key :q, :quit
start = Time.now start = Time.now
c_ = 0.0 c_ = 0.0
c_c = 0 c_c = 0
main_scene.on_update do |delta_time| App.update do |delta_time|
if App.pressed?(:quit)
App.exit!
next
end
c_ += delta_time c_ += delta_time
c_c += 1 c_c += 1
# puts "Delta time: #{delta_time}s" # puts "Delta time: #{delta_time}s"
# commented out to avoid spamming the console, but you can uncomment to see the delta time being printed every frame
# runs at 60fps, so you should see around 16-17ms being printed
el = main_scene.elements_at(App.mouse_position) red_pixel = [0xFF, 0, 0, 255] # ARGB
green_pixel = [0xFF, 0, 255, 0] # ARGB
c_p = ((c_c / 10) % 2) == 1 ? red_pixel : green_pixel
raw_bytes = (c_p * (50 * 50)).pack('C*')
# this is not the most efficient way to create a raw byte array, but it works for demonstration purposes
# you can use ` commands to get image data from an external source or generate it procedurally
bg_image1.update(0, 0, 50, 50, raw_bytes)
image1.update(0, 0, 50, 50, raw_bytes)
el = App.scene.elements_at(App.mouse_position)
pp App.text_input if App.text_input != "" pp App.text_input if App.text_input != ""
@@ -61,14 +100,17 @@ main_scene.on_update do |delta_time|
elsif elem == text elsif elem == text
pp "Down over text element" pp "Down over text element"
end end
dy = rand(-10..10)
dx = rand(-10..10)
elem.position.y += dy
elem.position.x += dx
end end
end end
main_scene << text main_scene << text
main_scene << rect main_scene << rect
main_scene << image_element main_scene << image_element
main_scene << image_element2
main_scene.delete(text)
App.start main_scene App.start main_scene
+6 -4
View File
@@ -96,7 +96,7 @@ end
# for entities it does similarly # for entities it does similarly
# mouse state can be read in systems always # mouse state can be read in systems always
query = tag(:enemy) & component(:health, true) & anti(name("Trap Preview", true)) query = Query.from(tag(:enemy) & component(:health, true) & anti(name("Trap Preview", true)))
# in the match name will return just the one entity # in the match name will return just the one entity
# tag and componet return list-like tables but they will be parallel and in order # tag and componet return list-like tables but they will be parallel and in order
# empty columns will be filled with nil (not possible if only & is used) # empty columns will be filled with nil (not possible if only & is used)
@@ -105,7 +105,7 @@ query = tag(:enemy) & component(:health, true) & anti(name("Trap Preview", true)
world.system :health_regen, for: query do |match, dt| # no trigger means always world.system :health_regen, for: query do |match, dt| # no trigger means always
# example of a simple health regeneration system that regenerates health for all entities with a health component # example of a simple health regeneration system that regenerates health for all entities with a health component
healths = match[0] healths = query.get[0] # get the first component in the query, in this case health, this will be a list of all health components for entities that match the query
healths.add_scalar! :health, 1 * dt healths.add_scalar! :health, 1 * dt
healths.clamp! :health, 0, 100 healths.clamp! :health, 0, 100
end end
@@ -136,12 +136,14 @@ world.system :trap_placement, trigger: trigger do |dt|
end end
end end
name_query = Query.from(name("Trap Preview", true) & component(:sprite, true))
# could be done as so or # could be done as so or
world.system :trap_preview, for: name("Trap Preview", true) do |match, dt| # example from teh game mist trying to be made with this engine world.system :trap_preview do |match, dt| # example from teh game mist trying to be made with this engine
# do trap placement logic when a trap is held # do trap placement logic when a trap is held
return unless world.holding_trap? return unless world.holding_trap?
# show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this # show a preview of the trap at the mouse position, you can use the mouse position relative to the world for this
e = match.first e = name_query.get[0] # get the first entity that matches the name query, in this case the trap preview entity, this will be nil if no entity matches
e.position = world.mouse_position_world e.position = world.mouse_position_world
e.active = true # enable the trap preview entity to show it at the mouse position e.active = true # enable the trap preview entity to show it at the mouse position
end end
+129
View File
@@ -0,0 +1,129 @@
App.run 720, 480, "Stress Test", fps: 60
scene = Scene.new
font = Font.new "assets/fonts/charybdis.ttf", 24, pixel_art: true
# primary factor in slowing teh system down is number of gpu textures
# i.e if we increase images or unique texts, we will see a significant drop in fps,
# but if we just increase number of elements with same image, the engine should be able to handle it much better
# --- Generate images ---
images = []
5.times do |i|
images << Image.new("assets/images/menu_bg.png", pixel_art: true)
end
# --- Animation pool stress ---
animations = []
5.times do |i|
anim = Animation.new(images.sample(5), fps: 5 + (i % 30))
animations << anim
end
# --- Massive element pool ---
elements = []
App.language = :en
App.localize(:en, :btn_text, "Stress Test %{count}")
App.localize(:en, :text, "Random Text")
1000.times do |i|
elements << ElementText.new(
:text,
font,
rand(0xFFFFFF),
{},
position: { x: rand(0..700), y: rand(0..450) },
z: rand(-10..10),
click_mode: :pass,
alpha: rand
)
end
1000.times do |i|
elements << ElementImage.new(
animations.sample,
position: { x: rand(0..700), y: rand(0..450) },
z: rand(-10..10),
alpha: rand,
scale: { x: rand * 2, y: rand * 2 },
click_mode: :pass
)
end
1000.times do |i|
elements << ElementRect.new(
20 + rand(100),
20 + rand(100),
rand(0xFFFFFF),
position: { x: rand(0..700), y: rand(0..450) },
z: rand(-10..10),
rotation: rand(360),
alpha: rand,
click_mode: :pass
)
end
text = ElementText.new(
:btn_text,
font,
0xFF0000,
{ count: 0 },
position: { x: 20, y: 20 },
z: 100,
click_mode: :block
)
scene << text
elements.each { |e| scene << e }
# --- FPS thrashing test ---
fps_values = [30, 60, 120, 240]
i = 0
click_counter = 0
text.click do
click_counter += 1
text.params[:count] = click_counter
end
start_time ||= Time.now
frame_count ||= 0
accum_dt ||= 0.0
last_log ||= 0.0
App.update do |dt|
frame_count += 1
accum_dt += dt
now = Time.now
elapsed = now - start_time
# log every second
if elapsed - last_log >= 1.0
dt_fps = frame_count / accum_dt
real_fps = frame_count / elapsed
puts "---- FPS REPORT ----"
puts "Engine FPS (dt): #{dt_fps.round(2)}"
puts "Real FPS (wall): #{real_fps.round(2)}"
puts "Avg dt: #{(accum_dt / frame_count * 1000).round(3)} ms"
puts "Frame count: #{frame_count}"
puts "--------------------"
last_log = elapsed
end
i += 1
# --- Input spam check ---
if App.pressed?(:mouse_left)
puts "Mouse pressed at #{App.mouse_position}"
end
end
App.start scene
@@ -4,7 +4,7 @@ set -e
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT_TMP=$(mktemp) OUTPUT_TMP=$(mktemp)
OUTPUT="$SCRIPT_DIR/../../include/binding/ruby_compiled.h" OUTPUT="$SCRIPT_DIR/../include/bindings/ruby_compiled.h"
if test "$1" = "clean"; then if test "$1" = "clean"; then
rm -f "$OUTPUT" rm -f "$OUTPUT"
@@ -26,7 +26,9 @@ if test "$OUTPUT_TIME" -ge "$LATEST_FILE_TIME"; then
exit 0 exit 0
fi fi
"$SCRIPT_DIR/../mruby/bin/mrbc" -o "$OUTPUT_TMP" "${FILES[@]}" echo "Compiling mgems..."
"${MRBC:-mrbc}" -o "$OUTPUT_TMP" "${FILES[@]}"
{ {
echo "#ifndef MGEMS_H" echo "#ifndef MGEMS_H"
-14
View File
@@ -1,14 +0,0 @@
dir = File.expand_path(__dir__)
clangd = <<CLANGD
CompileFlags:
Add: [
-I#{dir}/include,
-I#{dir}/libs/mruby/include,
-std=c++23
]
Remove: []
Compiler: clang++
CLANGD
File.write ".clangd", clangd
+111
View File
@@ -0,0 +1,111 @@
#include "app/app.h"
namespace app {
void App::exit() {
running = false;
}
void App::switch_to_scene(uint64_t scene_id) {
scene_pool.release(current_scene_id);
scene_pool.use(scene_id);
current_scene_id = scene_id;
}
App::App(Vec2<float> size, const char *title) {
window.update(size, title);
Scene *loading_scene = new Scene{};
current_scene_id = scene_pool.acquire(loading_scene);
}
void App::set_fps(float fps) {
this->fps = fps;
target_frame_time = 1e9 / fps;
}
uint64_t App::get_fps() const {
return fps;
}
void App::loop() {
uint64_t frame_start = window.get_time();
uint64_t frame_end = window.get_time();
uint64_t previous_frame_end = 0;
while (running) {
frame_start = window.get_time();
uint64_t frame_time = frame_start - frame_end;
if (frame_time <= target_frame_time) {
struct timespec req;
req.tv_sec = 0;
req.tv_nsec = target_frame_time - frame_time;
nanosleep(&req, nullptr);
}
previous_frame_end = frame_end;
frame_end = window.get_time();
float dt = frame_end - previous_frame_end;
Scene *scene = scene_pool[current_scene_id];
pressed_keys.clear();
released_keys.clear();
scroll = {0, 0};
text = "";
Event e;
while (window.poll(e)) {
switch (e.type) {
case Event::QUIT:
running = false;
break;
case Event::MOUSE_BUTTON_DOWN:
scene->click(e.mouse_button.position);
down_keys[e.mouse_button.button] = true;
pressed_keys.push_back(e.mouse_button.button);
break;
case Event::MOUSE_BUTTON_UP:
down_keys[e.mouse_button.button] = false;
released_keys.push_back(e.mouse_button.button);
break;
case Event::MOUSE_MOVE:
mouse_position = e.mouse_move.position;
break;
case Event::MOUSE_LEAVE:
mouse_position = {-1, -1};
break;
case Event::KEY_DOWN:
down_keys[e.key.key] = true;
pressed_keys.push_back(e.key.key);
break;
case Event::KEY_UP:
down_keys[e.key.key] = false;
released_keys.push_back(e.key.key);
break;
case Event::SCROLL:
scroll = e.scroll;
break;
case Event::TEXT_INPUT:
text += *e.text_input;
delete e.text_input;
break;
default:
break;
}
}
int ai = mrb_gc_arena_save(Ruby::mrb);
mrb_value dt_val = mrb_float_value(Ruby::mrb, dt / 1e9);
update.call(1, &dt_val);
scene->update_scene(dt_val);
mrb_gc_arena_restore(Ruby::mrb, ai);
scene->render(window.get_time());
window.render();
window.clear();
}
}
} // namespace app
+77
View File
@@ -0,0 +1,77 @@
#include "app/elements.h"
namespace app {
void Element::click() {
on_click.call();
}
// EImage
EImage::~EImage() {
if (animation_id != UINT64_MAX)
animation_pool.release(animation_id);
}
Vec2<float> EImage::size() {
Animation &animation = *animation_pool[animation_id];
if (animation.frames.empty())
return {0, 0};
return window.get_image_size(animation.frames[0]);
}
void EImage::render(uint64_t abs_time) {
Animation &animation = *animation_pool[animation_id];
if (animation.frames.empty())
return;
int frame_index = (int)((abs_time * animation.frame_rate) / 1e9) % animation.frames.size();
window.draw(animation.frames[frame_index], position, z, rotation, pivot, scale, alpha);
}
// EText
EText::~EText() {
if (text_id != UINT64_MAX)
text_pool.release(text_id);
if (font_id != UINT64_MAX)
font_pool.release(font_id);
}
Vec2<float> EText::size() {
if (text_id == UINT64_MAX)
return {0, 0};
return window.get_text_size(text_id);
}
void EText::reload_text() {
if (text_id != UINT64_MAX) {
text_pool.release(text_id);
text_id = UINT64_MAX;
}
}
void EText::render(uint64_t) {
if (
last_language != Localization::current_language
|| last_key != key || text_id == UINT64_MAX
|| last_color != color
|| params_changed
) {
std::string localized_str = Localization::get(key, params);
if (text_id != UINT64_MAX)
text_pool.release(text_id);
text_id = window.create_text(font_id, localized_str.c_str(), localized_str.size(), color);
last_language = Localization::current_language;
last_key = key;
last_color = color;
params_changed = false;
}
window.write(text_id, position, z, rotation, pivot, scale, alpha);
}
// ERect
Vec2<float> ERect::size() {
return shape;
};
void ERect::render(uint64_t) {
window.draw_rect(Rect::from(position, shape), color, z, rotation, pivot, scale, alpha);
}
} // namespace app
+91
View File
@@ -0,0 +1,91 @@
#include "app/scene.h"
namespace app {
Scene::~Scene() {
for (const uint64_t &id : element_ids)
element_pool.release(id);
}
void Scene::add_element(uint64_t element_id) {
auto it = std::lower_bound(
element_ids.begin(),
element_ids.end(),
element_id,
[](auto &a, auto &b) {
auto e_a = element_pool[a];
auto e_b = element_pool[b];
return e_a->z < e_b->z || (e_a->z == e_b->z && a < b);
}
);
element_pool.use(element_id);
element_ids.insert(it, element_id);
}
void Scene::delete_element(uint64_t element_id) {
auto it = std::find(element_ids.begin(), element_ids.end(), element_id);
if (it != element_ids.end()) {
element_pool.release(element_id);
element_ids.erase(it);
}
}
void Scene::update_scene(mrb_value dt_val) {
if (mrb_nil_p(update.proc))
return;
update.call(1, &dt_val);
}
void Scene::click(Vec2<float> position) {
std::vector<uint64_t> clicked_elements = elements_at(position);
for (const uint64_t &id : clicked_elements)
element_pool[id]->click();
}
void Scene::render(uint64_t abs_time) {
for (const uint64_t &e : element_ids)
element_pool[e]->render(abs_time);
}
std::vector<uint64_t> Scene::elements_at(Vec2<float> position) {
std::vector<uint64_t> result;
for (auto it = element_ids.rbegin(); it != element_ids.rend(); ++it) {
Element *e = element_pool[*it];
Vec2<float> size = e->size();
Rect rect = Rect::from(size);
if (rect.contains(world_to_local(position, e))) {
if (e->click_mode == ClickMode::PASS) {
result.push_back(*it);
} else if (e->click_mode == ClickMode::BLOCK) {
result.push_back(*it);
break;
} else {
continue;
}
}
}
return result;
}
Vec2<float> Scene::world_to_local(Vec2<float> p, Element *e) {
Vec2<float> size = e->size();
Vec2<float> pivot = e->pivot * size * e->scale;
p = p - e->position;
float rad = -e->rotation * (M_PI / 180.0f);
float c = cos(rad);
float s = sin(rad);
p = p - pivot;
p = {
p.x * c - p.y * s,
p.x * s + p.y * c
};
p = p + pivot;
p = p / e->scale;
return p;
}
} // namespace app
+186
View File
@@ -0,0 +1,186 @@
#include "bindings/definitions.h"
#include "window/window.h"
namespace {
void Animation_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
animation_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Animation_type = {
"Animation", Animation_free
};
mrb_value Animation_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Animation_type);
Wrapper *s = new Wrapper();
const mrb_value *frames_val;
mrb_int frames_len;
mrb_value kwargs;
mrb_get_args(mrb, "a|H", &frames_val, &frames_len, &kwargs);
float fps = 1.0f;
if (!mrb_nil_p(kwargs))
HASH_FLOAT(kwargs, fps, fps);
std::vector<uint64_t> frame_ids;
for (mrb_int i = 0; i < frames_len; i++) {
mrb_value frame_val = frames_val[i];
if (!mrb_obj_is_kind_of(mrb, frame_val, definitions::bindings.Image.cls)) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an array of Image objects"
);
return mrb_nil_value();
}
Wrapper *image_wrapper = (Wrapper *)DATA_PTR(frame_val);
uint64_t frame_id = image_wrapper->id;
image_pool.use(frame_id);
frame_ids.push_back(frame_id);
}
Animation *animation = new Animation{frame_ids, fps};
s->id = animation_pool.acquire(animation);
animation_pool.use(s->id);
mrb_data_init(self, s, &Animation_type);
return self;
}
mrb_value static_image_animation(mrb_state *mrb, mrb_value) {
mrb_value image_val;
mrb_get_args(mrb, "o", &image_val);
if (!mrb_obj_is_kind_of(mrb, image_val, definitions::bindings.Image.cls)) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an Image object"
);
return mrb_nil_value();
}
Wrapper *image_wrapper = (Wrapper *)DATA_PTR(image_val);
uint64_t frame_id = image_wrapper->id;
image_pool.use(frame_id);
std::vector<uint64_t> frames = {frame_id};
Animation *animation = new Animation{frames, 1.0f};
uint64_t id = animation_pool.acquire(animation);
animation_pool.use(id);
return definitions::wrap(definitions::bindings.Animation, id);
}
mrb_value animation_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.Animation.cls))
return mrb_false_value();
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id_self = self_wrapper->id;
Wrapper *other_wrapper = (Wrapper *)DATA_PTR(other);
uint64_t id_other = other_wrapper->id;
return mrb_bool_value(id_self == id_other);
}
mrb_value animation_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
return mrb_fixnum_value(wrapper->id);
}
mrb_value animation_frames_set(mrb_state *mrb, mrb_value self) {
const mrb_value *frames_val;
mrb_int frames_len;
mrb_get_args(mrb, "a", &frames_val, &frames_len);
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = self_wrapper->id;
Animation *animation = animation_pool[id];
std::vector<uint64_t> new_frame_ids;
for (mrb_int i = 0; i < frames_len; i++) {
mrb_value frame_val = frames_val[i];
if (!mrb_obj_is_kind_of(mrb, frame_val, definitions::bindings.Image.cls)) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an array of Image objects"
);
return mrb_nil_value();
}
Wrapper *image_wrapper = (Wrapper *)DATA_PTR(frame_val);
uint64_t frame_id = image_wrapper->id;
image_pool.use(frame_id);
new_frame_ids.push_back(frame_id);
}
// Release old frames
for (uint64_t frame_id : animation->frames)
image_pool.release(frame_id);
animation->frames = std::move(new_frame_ids);
return mrb_nil_value();
}
mrb_value animation_frames_get(mrb_state *mrb, mrb_value self) {
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = self_wrapper->id;
Animation *animation = animation_pool[id];
mrb_value frames_array = mrb_ary_new_capa(mrb, animation->frames.size());
for (uint64_t frame_id : animation->frames) {
image_pool.use(frame_id);
mrb_value frame_obj = definitions::wrap(definitions::bindings.Image, frame_id);
mrb_ary_push(mrb, frames_array, frame_obj);
}
return frames_array;
}
mrb_value animation_fps_set(mrb_state *mrb, mrb_value self) {
mrb_float fps;
mrb_get_args(mrb, "f", &fps);
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = self_wrapper->id;
Animation *animation = animation_pool[id];
animation->frame_rate = fps;
return mrb_nil_value();
}
mrb_value animation_fps_get(mrb_state *mrb, mrb_value self) {
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = self_wrapper->id;
Animation *animation = animation_pool[id];
return mrb_float_value(mrb, animation->frame_rate);
}
} // namespace
namespace definitions {
void register_animation() {
DEF_CLASS(Animation, MRB_ARGS_REQ(1) | MRB_ARGS_OPT(1));
ADD_CLASS_METHOD(Animation, "from", static_image_animation, MRB_ARGS_REQ(1));
ADD_METHOD(Animation, "==", animation_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Animation, "eql?", animation_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Animation, "hash", animation_hash, MRB_ARGS_NONE());
ADD_METHOD(Animation, "fps=", animation_fps_set, MRB_ARGS_REQ(1));
ADD_METHOD(Animation, "fps", animation_fps_get, MRB_ARGS_NONE());
ADD_METHOD(Animation, "frames=", animation_frames_set, MRB_ARGS_REQ(1));
ADD_METHOD(Animation, "frames", animation_frames_get, MRB_ARGS_NONE());
bindings.Animation = {Animation_class, &Animation_type};
}
} // namespace definitions
+206
View File
@@ -0,0 +1,206 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
mrb_value app_run(mrb_state *mrb, mrb_value) {
mrb_int width, height;
mrb_value options_hash;
char *title = nullptr;
mrb_get_args(mrb, "iiz|H", &width, &height, &title, &options_hash);
app_ = new app::App({(float)width, (float)height}, title);
float fps = 60;
if (!mrb_nil_p(options_hash))
HASH_FLOAT(options_hash, fps, fps);
app_->set_fps(fps);
return mrb_nil_value();
}
mrb_value app_fps_get(mrb_state *mrb, mrb_value) {
return mrb_float_value(mrb, app_->get_fps());
}
mrb_value app_fps_set(mrb_state *mrb, mrb_value) {
mrb_float fps;
mrb_get_args(mrb, "f", &fps);
app_->set_fps(fps);
return mrb_nil_value();
}
mrb_value app_start(mrb_state *mrb, mrb_value) {
mrb_value scene_val;
mrb_get_args(mrb, "o", &scene_val);
if (!mrb_obj_is_kind_of(mrb, scene_val, mrb_class_get(mrb, "Scene"))) {
mrb_raise(mrb, mrb_class_get(mrb, "Exception"), "Expected a Scene object");
return mrb_nil_value();
}
Wrapper *scene_wrapper = (Wrapper *)DATA_PTR(scene_val);
uint64_t id = scene_wrapper->id;
app_->switch_to_scene(id);
app_->loop();
return mrb_nil_value();
}
mrb_value app_current_scene(mrb_state *, mrb_value) {
uint64_t id = app_->current_scene_id;
if (id == UINT64_MAX)
return mrb_nil_value();
app::scene_pool.use(id);
return definitions::wrap(definitions::bindings.Scene, id);
}
mrb_value app_on_update(mrb_state *mrb, mrb_value) {
mrb_value block;
mrb_get_args(mrb, "&", &block);
app_->update.set_proc(block);
return mrb_nil_value();
}
mrb_value app_switch_to(mrb_state *mrb, mrb_value) {
mrb_value scene_val;
mrb_get_args(mrb, "o", &scene_val);
if (!mrb_obj_is_kind_of(mrb, scene_val, mrb_class_get(mrb, "Scene"))) {
mrb_raise(mrb, mrb_class_get(mrb, "Exception"), "Expected a Scene object");
return mrb_nil_value();
}
Wrapper *scene_wrapper = (Wrapper *)DATA_PTR(scene_val);
uint64_t id = scene_wrapper->id;
app_->switch_to_scene(id);
return mrb_nil_value();
}
mrb_value app_localize(mrb_state *mrb, mrb_value) {
mrb_sym lang_sym, key_sym;
char *text;
mrb_get_args(mrb, "nnz", &lang_sym, &key_sym, &text);
auto lang = Localization::lang_from_sym(mrb, lang_sym);
const char *key_name = mrb_sym2name(mrb, key_sym);
auto it = Localization::localization_key_map.find(key_name);
Localization::Key key_id;
if (it == Localization::localization_key_map.end()) {
key_id = (Localization::Key)Localization::localization_key_map.size();
Localization::localization_key_map[key_name] = key_id;
} else {
key_id = it->second;
}
Localization::set(lang, key_id, text);
return mrb_nil_value();
}
mrb_value app_language_set(mrb_state *mrb, mrb_value) {
mrb_sym lang_sym;
mrb_get_args(mrb, "n", &lang_sym);
Localization::current_language = Localization::lang_from_sym(mrb, lang_sym);
return mrb_nil_value();
}
mrb_value app_language_get(mrb_state *mrb, mrb_value) {
return mrb_symbol_value(Localization::lang_to_sym(mrb, Localization::current_language));
}
mrb_value app_mouse_position(mrb_state *mrb, mrb_value) {
Vec2<float> mouse_pos = app_->mouse_position;
mrb_value result = mrb_hash_new(mrb);
HASH_SET(result, x, mrb_float_value(mrb, mouse_pos.x));
HASH_SET(result, y, mrb_float_value(mrb, mouse_pos.y));
return result;
}
mrb_value app_is_down(mrb_state *mrb, mrb_value) {
mrb_sym key_sym;
mrb_get_args(mrb, "n", &key_sym);
std::string key_name = mrb_sym2name(mrb, key_sym);
Key key = get_key(key_name);
return mrb_bool_value(app_->down_keys[key]);
}
mrb_value app_is_pressed(mrb_state *mrb, mrb_value) {
mrb_sym key_sym;
mrb_get_args(mrb, "n", &key_sym);
std::string key_name = mrb_sym2name(mrb, key_sym);
Key key = get_key(key_name);
for (Key pressed_key : app_->pressed_keys)
if (pressed_key == key)
return mrb_bool_value(true);
return mrb_bool_value(false);
}
mrb_value app_is_released(mrb_state *mrb, mrb_value) {
mrb_sym key_sym;
mrb_get_args(mrb, "n", &key_sym);
std::string key_name = mrb_sym2name(mrb, key_sym);
Key key = get_key(key_name);
for (Key released_key : app_->released_keys)
if (released_key == key)
return mrb_bool_value(true);
return mrb_bool_value(false);
}
mrb_value app_map_key(mrb_state *mrb, mrb_value) {
mrb_sym key_sym;
mrb_sym action_sym;
mrb_get_args(mrb, "nn", &key_sym, &action_sym);
std::string key_name = mrb_sym2name(mrb, key_sym);
std::string action_name = mrb_sym2name(mrb, action_sym);
set_action_mapping(action_name, key_name);
return mrb_nil_value();
}
mrb_value app_scroll(mrb_state *mrb, mrb_value) {
Vec2<float> scroll = app_->scroll;
mrb_value result = mrb_hash_new(mrb);
HASH_SET(result, x, mrb_float_value(mrb, scroll.x));
HASH_SET(result, y, mrb_float_value(mrb, scroll.y));
return result;
}
mrb_value app_text_input(mrb_state *mrb, mrb_value) {
return mrb_str_new_cstr(mrb, app_->text.c_str());
}
mrb_value app_exit(mrb_state *, mrb_value) {
app_->exit();
return mrb_nil_value();
}
} // namespace
namespace definitions {
void register_app() {
DEF_MODULE(App);
ADD_MODULE_METHOD(App, "run", app_run, MRB_ARGS_REQ(3) | MRB_ARGS_OPT(1));
ADD_MODULE_METHOD(App, "start", app_start, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "switch_to", app_switch_to, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "scene", app_current_scene, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "update", app_on_update, MRB_ARGS_BLOCK());
ADD_MODULE_METHOD(App, "fps=", app_fps_set, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "fps", app_fps_get, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "localize", app_localize, MRB_ARGS_REQ(3));
ADD_MODULE_METHOD(App, "language=", app_language_set, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "language", app_language_get, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "mouse_position", app_mouse_position, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "down?", app_is_down, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "pressed?", app_is_pressed, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "released?", app_is_released, MRB_ARGS_REQ(1));
ADD_MODULE_METHOD(App, "map_key", app_map_key, MRB_ARGS_REQ(2));
ADD_MODULE_METHOD(App, "scroll", app_scroll, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "text_input", app_text_input, MRB_ARGS_NONE());
ADD_MODULE_METHOD(App, "exit!", app_exit, MRB_ARGS_NONE());
}
} // namespace definitions
+326
View File
@@ -0,0 +1,326 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
void ElementImage_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::element_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type ElementImage_type = {
"ElementImage",
ElementImage_free
};
mrb_value ElementImage_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &ElementImage_type);
Wrapper *s = new Wrapper();
mrb_value animation_val;
mrb_value kwargs;
mrb_get_args(mrb, "o|H", &animation_val, &kwargs);
if (!mrb_obj_is_kind_of(
mrb,
animation_val,
definitions::bindings.Animation.cls
)) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an Animation object"
);
return mrb_nil_value();
}
float x = 0, y = 0, z = 0, rotation = 0;
float scale_x = 1, scale_y = 1, alpha = 1, pivot_x = 0.5, pivot_y = 0.5;
mrb_sym click_mode_sym = sym_block;
if (!mrb_nil_p(kwargs)) {
mrb_value pos_hash = HASH_GET(kwargs, position);
if (!mrb_nil_p(pos_hash)) {
HASH_FLOAT(pos_hash, x, x);
HASH_FLOAT(pos_hash, y, y);
}
mrb_value scale_hash = HASH_GET(kwargs, scale);
if (!mrb_nil_p(scale_hash)) {
HASH_FLOAT(scale_hash, x, scale_x);
HASH_FLOAT(scale_hash, y, scale_y);
}
mrb_value pivot_hash = HASH_GET(kwargs, pivot);
if (!mrb_nil_p(pivot_hash)) {
HASH_FLOAT(pivot_hash, x, pivot_x);
HASH_FLOAT(pivot_hash, y, pivot_y);
}
HASH_FLOAT(kwargs, z, z);
HASH_FLOAT(kwargs, rotation, rotation);
HASH_FLOAT(kwargs, alpha, alpha);
mrb_value v = HASH_GET(kwargs, click_mode);
if (!mrb_nil_p(v)) {
click_mode_sym = mrb_symbol(v);
}
}
Wrapper *animation_wrapper = (Wrapper *)DATA_PTR(animation_val);
uint64_t id = animation_wrapper->id;
animation_pool.use(id);
app::EImage *element = new app::EImage(id);
element->position = {x, y};
element->scale = {scale_x, scale_y};
element->rotation = rotation;
element->pivot = {pivot_x, pivot_y};
element->alpha = alpha;
element->z = z;
std::string click_mode = mrb_sym2name(mrb, click_mode_sym);
if (click_mode == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode == "ignore")
element->click_mode = ClickMode::IGNORE;
s->id = app::element_pool.acquire(element);
app::element_pool.use(s->id);
mrb_data_init(self, s, &ElementImage_type);
return self;
}
mrb_value element_image_on_click(mrb_state *mrb, mrb_value self) {
mrb_value block;
mrb_get_args(mrb, "&", &block);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::Element *element = app::element_pool[id];
element->on_click.set_proc(block);
return self;
}
mrb_value element_image_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.ElementImage.cls))
return mrb_false_value();
Wrapper *wrapper_self = (Wrapper *)DATA_PTR(self);
Wrapper *wrapper_other = (Wrapper *)DATA_PTR(other);
return mrb_bool_value(wrapper_self->id == wrapper_other->id);
}
mrb_value element_image_inspect(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
std::string str = "#<ElementImage:0x"
+ std::format("0x{:016x}", id)
+ " animation_id=0x"
+ std::format("{:016x}", element->animation_id)
+ ">";
return mrb_str_new_cstr(mrb, str.c_str());
}
mrb_value element_image_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
return mrb_fixnum_value(wrapper->id);
}
mrb_value element_image_animation_set(mrb_state *mrb, mrb_value self) {
mrb_value animation;
mrb_get_args(mrb, "o", &animation);
if (!mrb_obj_is_kind_of(
mrb,
animation,
definitions::bindings.Animation.cls
)) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an Animation object"
);
return mrb_nil_value();
}
Wrapper *animation_wrapper = (Wrapper *)DATA_PTR(animation);
uint64_t animation_id = animation_wrapper->id;
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
animation_pool.use(animation_id);
animation_pool.release(element->animation_id);
element->animation_id = animation_id;
return self;
}
mrb_value element_image_animation_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
uint64_t animation_id = element->animation_id;
if (animation_id == UINT64_MAX)
return mrb_nil_value();
animation_pool.use(animation_id);
return definitions::wrap(definitions::bindings.Animation, animation_id);
}
mrb_value element_image_position_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->position);
}
mrb_value element_image_rotation_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return mrb_float_value(mrb, element->rotation);
}
mrb_value element_image_rotation_set(mrb_state *mrb, mrb_value self) {
mrb_float rotation;
mrb_get_args(mrb, "f", &rotation);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
element->rotation = rotation;
return mrb_nil_value();
}
mrb_value element_image_pivot_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->pivot);
}
mrb_value element_image_scale_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->scale);
}
mrb_value element_image_alpha_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return mrb_float_value(mrb, element->alpha);
}
mrb_value element_image_alpha_set(mrb_state *mrb, mrb_value self) {
mrb_float alpha;
mrb_get_args(mrb, "f", &alpha);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
element->alpha = alpha;
return mrb_nil_value();
}
mrb_value element_image_z_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
return mrb_float_value(mrb, element->z);
}
mrb_value element_image_z_set(mrb_state *mrb, mrb_value self) {
mrb_float z;
mrb_get_args(mrb, "f", &z);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
element->z = z;
return mrb_nil_value();
}
mrb_value element_image_click_mode_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
std::string click_mode_str;
switch (element->click_mode) {
case ClickMode::PASS:
click_mode_str = "pass";
break;
case ClickMode::BLOCK:
click_mode_str = "block";
break;
case ClickMode::IGNORE:
click_mode_str = "ignore";
break;
}
return mrb_symbol_value(mrb_intern_cstr(mrb, click_mode_str.c_str()));
}
mrb_value element_image_click_mode_set(mrb_state *mrb, mrb_value self) {
mrb_sym click_mode_sym;
mrb_get_args(mrb, "n", &click_mode_sym);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EImage *element = (app::EImage *)app::element_pool[id];
std::string click_mode = mrb_sym2name(mrb, click_mode_sym);
if (click_mode == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode == "ignore")
element->click_mode = ClickMode::IGNORE;
return mrb_nil_value();
}
} // namespace
namespace definitions {
void register_element_image() {
DEF_CLASS(ElementImage, MRB_ARGS_REQ(1) | MRB_ARGS_OPT(1));
ADD_METHOD(ElementImage, "click", element_image_on_click, MRB_ARGS_BLOCK());
ADD_METHOD(ElementImage, "==", element_image_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "eql?", element_image_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "inspect", element_image_inspect, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "to_s", element_image_inspect, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "hash", element_image_hash, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "animation=", element_image_animation_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "animation", element_image_animation_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "position", element_image_position_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "scale", element_image_scale_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "rotation=", element_image_rotation_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "rotation", element_image_rotation_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "pivot", element_image_pivot_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "alpha=", element_image_alpha_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "alpha", element_image_alpha_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "z=", element_image_z_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "z", element_image_z_get, MRB_ARGS_NONE());
ADD_METHOD(ElementImage, "click_mode=", element_image_click_mode_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementImage, "click_mode", element_image_click_mode_get, MRB_ARGS_NONE());
bindings.ElementImage = {ElementImage_class, &ElementImage_type};
}
} // namespace definitions
+349
View File
@@ -0,0 +1,349 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
void ElementRect_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::element_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type ElementRect_type = {
"ElementRect",
ElementRect_free
};
mrb_value ElementRect_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &ElementRect_type);
Wrapper *s = new Wrapper();
mrb_float w, h;
mrb_int color_i = 0xFFFFFF;
mrb_value kwargs;
mrb_get_args(mrb, "ffi|H", &w, &h, &color_i, &kwargs);
float x = 0, y = 0, z = 0, rotation = 0;
float scale_x = 1, scale_y = 1, alpha = 1, pivot_x = 0.5, pivot_y = 0.5;
mrb_sym click_mode_sym = sym_block;
if (!mrb_nil_p(kwargs)) {
mrb_value pos_hash = HASH_GET(kwargs, position);
if (!mrb_nil_p(pos_hash)) {
HASH_FLOAT(pos_hash, x, x);
HASH_FLOAT(pos_hash, y, y);
}
mrb_value scale_hash = HASH_GET(kwargs, scale);
if (!mrb_nil_p(scale_hash)) {
HASH_FLOAT(scale_hash, x, scale_x);
HASH_FLOAT(scale_hash, y, scale_y);
}
mrb_value pivot_hash = HASH_GET(kwargs, pivot);
if (!mrb_nil_p(pivot_hash)) {
HASH_FLOAT(pivot_hash, x, pivot_x);
HASH_FLOAT(pivot_hash, y, pivot_y);
}
HASH_FLOAT(kwargs, z, z);
HASH_FLOAT(kwargs, rotation, rotation);
HASH_FLOAT(kwargs, alpha, alpha);
mrb_value v = HASH_GET(kwargs, click_mode);
if (!mrb_nil_p(v)) {
click_mode_sym = mrb_symbol(v);
}
}
Vec2<float> shape = {(float)w, (float)h};
Color color = {
(uint8_t)((color_i >> 16) & 0xFF),
(uint8_t)((color_i >> 8) & 0xFF),
(uint8_t)((color_i) & 0xFF)
};
app::ERect *element = new app::ERect(
shape,
color
);
element->position = {x, y};
element->scale = {scale_x, scale_y};
element->rotation = rotation;
element->pivot = {pivot_x, pivot_y};
element->alpha = alpha;
element->z = z;
std::string click_mode = mrb_sym2name(mrb, click_mode_sym);
if (click_mode == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode == "ignore")
element->click_mode = ClickMode::IGNORE;
s->id = app::element_pool.acquire(element);
app::element_pool.use(s->id);
mrb_data_init(self, s, &ElementRect_type);
return self;
}
mrb_value element_rect_on_click(mrb_state *mrb, mrb_value self) {
mrb_value block;
mrb_get_args(mrb, "&", &block);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::Element *element = app::element_pool[id];
element->on_click.set_proc(block);
return self;
}
mrb_value element_rect_width_set(mrb_state *mrb, mrb_value self) {
mrb_float width;
mrb_get_args(mrb, "f", &width);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->shape.x = width;
return mrb_nil_value();
}
mrb_value element_rect_width_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return mrb_float_value(mrb, element->shape.x);
}
mrb_value element_rect_height_set(mrb_state *mrb, mrb_value self) {
mrb_float height;
mrb_get_args(mrb, "f", &height);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->shape.y = height;
return mrb_nil_value();
}
mrb_value element_rect_height_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return mrb_float_value(mrb, element->shape.y);
}
mrb_value element_rect_color_set(mrb_state *mrb, mrb_value self) {
mrb_value color_val;
mrb_get_args(mrb, "o", &color_val);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->color = {
(uint8_t)((mrb_fixnum(color_val) >> 16) & 0xFF),
(uint8_t)((mrb_fixnum(color_val) >> 8) & 0xFF),
(uint8_t)((mrb_fixnum(color_val)) & 0xFF)
};
return self;
}
mrb_value element_rect_color_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
uint32_t color =
((uint32_t)element->color.r << 16) | ((uint32_t)element->color.g << 8)
| ((uint32_t)element->color.b);
return mrb_fixnum_value(color);
}
mrb_value element_rect_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.ElementRect.cls))
return mrb_false_value();
Wrapper *wrapper_self = (Wrapper *)DATA_PTR(self);
Wrapper *wrapper_other = (Wrapper *)DATA_PTR(other);
uint64_t id_self = wrapper_self->id;
uint64_t id_other = wrapper_other->id;
return mrb_bool_value(id_self == id_other);
}
mrb_value element_rect_inspect(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
std::string str = "#<ElementRect:0x"
+ std::format("0x{:016x}", id)
+ " width=" + std::format("{}", element->shape.x)
+ " height=" + std::format("{}", element->shape.y)
+ ">";
return mrb_str_new_cstr(mrb, str.c_str());
}
mrb_value element_rect_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
return mrb_fixnum_value(id);
}
mrb_value element_rect_position_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->position);
}
mrb_value element_rect_rotation_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return mrb_float_value(mrb, element->rotation);
}
mrb_value element_rect_rotation_set(mrb_state *mrb, mrb_value self) {
mrb_float rotation;
mrb_get_args(mrb, "f", &rotation);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->rotation = rotation;
return mrb_nil_value();
}
mrb_value element_rect_pivot_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->pivot);
}
mrb_value element_rect_scale_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->scale);
}
mrb_value element_rect_alpha_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return mrb_float_value(mrb, element->alpha);
}
mrb_value element_rect_alpha_set(mrb_state *mrb, mrb_value self) {
mrb_float alpha;
mrb_get_args(mrb, "f", &alpha);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->alpha = alpha;
return mrb_nil_value();
}
mrb_value element_rect_z_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
return mrb_float_value(mrb, element->z);
}
mrb_value element_rect_z_set(mrb_state *mrb, mrb_value self) {
mrb_float z;
mrb_get_args(mrb, "f", &z);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
element->z = z;
return mrb_nil_value();
}
mrb_value element_rect_click_mode_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
std::string click_mode_str;
switch (element->click_mode) {
case ClickMode::PASS:
click_mode_str = "pass";
break;
case ClickMode::BLOCK:
click_mode_str = "block";
break;
case ClickMode::IGNORE:
click_mode_str = "ignore";
break;
}
return mrb_symbol_value(mrb_intern_cstr(mrb, click_mode_str.c_str()));
}
mrb_value element_rect_click_mode_set(mrb_state *mrb, mrb_value self) {
mrb_sym click_mode_sym;
mrb_get_args(mrb, "n", &click_mode_sym);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::ERect *element = (app::ERect *)app::element_pool[id];
std::string click_mode = mrb_sym2name(mrb, click_mode_sym);
if (click_mode == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode == "ignore")
element->click_mode = ClickMode::IGNORE;
return mrb_nil_value();
}
} // namespace
namespace definitions {
void register_element_rect() {
DEF_CLASS(ElementRect, MRB_ARGS_REQ(3) | MRB_ARGS_OPT(1));
ADD_METHOD(ElementRect, "==", element_rect_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "eql?", element_rect_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "inspect", element_rect_inspect, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "to_s", element_rect_inspect, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "hash", element_rect_hash, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "click", element_rect_on_click, MRB_ARGS_BLOCK());
ADD_METHOD(ElementRect, "width=", element_rect_width_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "width", element_rect_width_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "height=", element_rect_height_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "height", element_rect_height_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "color=", element_rect_color_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "color", element_rect_color_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "position", element_rect_position_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "scale", element_rect_scale_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "rotation=", element_rect_rotation_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "rotation", element_rect_rotation_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "pivot", element_rect_pivot_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "alpha=", element_rect_alpha_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "alpha", element_rect_alpha_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "z=", element_rect_z_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "z", element_rect_z_get, MRB_ARGS_NONE());
ADD_METHOD(ElementRect, "click_mode=", element_rect_click_mode_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementRect, "click_mode", element_rect_click_mode_get, MRB_ARGS_NONE());
bindings.ElementRect = {ElementRect_class, &ElementRect_type};
}
} // namespace definitions
+411
View File
@@ -0,0 +1,411 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
void ElementText_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::element_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type ElementText_type = {
"ElementText", ElementText_free
};
mrb_value ElementText_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &ElementText_type);
Wrapper *s = new Wrapper();
mrb_sym key;
mrb_value font_val;
mrb_int color_i = 0xFFFFFF;
mrb_value t_args;
mrb_value kwargs;
mrb_get_args(mrb, "noi|HH", &key, &font_val, &color_i, &t_args, &kwargs);
auto key_id = Localization::key_from_sym(mrb, key);
if (!mrb_obj_is_kind_of(mrb, font_val, definitions::bindings.Font.cls)) {
mrb_raise(mrb, mrb_class_get(mrb, "Exception"), "Expected a Font object");
return mrb_nil_value();
}
float x = 0, y = 0, z = 0, rotation = 0;
float scale_x = 1, scale_y = 1, alpha = 1;
float pivot_x = 0.5, pivot_y = 0.5;
mrb_sym click_mode_sym = sym_block;
if (!mrb_nil_p(kwargs)) {
mrb_value pos_hash = HASH_GET(kwargs, position);
if (!mrb_nil_p(pos_hash)) {
HASH_FLOAT(pos_hash, x, x);
HASH_FLOAT(pos_hash, y, y);
}
mrb_value scale_hash = HASH_GET(kwargs, scale);
if (!mrb_nil_p(scale_hash)) {
HASH_FLOAT(scale_hash, x, scale_x);
HASH_FLOAT(scale_hash, y, scale_y);
}
mrb_value pivot_hash = HASH_GET(kwargs, pivot);
if (!mrb_nil_p(pivot_hash)) {
HASH_FLOAT(pivot_hash, x, pivot_x);
HASH_FLOAT(pivot_hash, y, pivot_y);
}
HASH_FLOAT(kwargs, z, z);
HASH_FLOAT(kwargs, rotation, rotation);
HASH_FLOAT(kwargs, alpha, alpha);
mrb_value v = HASH_GET(kwargs, click_mode);
if (!mrb_nil_p(v)) {
click_mode_sym = mrb_symbol(v);
}
}
Wrapper *font_wrapper = (Wrapper *)DATA_PTR(font_val);
uint64_t id = font_wrapper->id;
font_pool.use(id);
Color color = {
(uint8_t)((color_i >> 16) & 0xFF),
(uint8_t)((color_i >> 8) & 0xFF),
(uint8_t)((color_i) & 0xFF)
};
std::unordered_map<std::string, std::string> params;
if (!mrb_nil_p(t_args)) {
mrb_value keys = mrb_hash_keys(mrb, t_args);
for (mrb_int i = 0; i < RARRAY_LEN(keys); i++) {
mrb_value key = RARRAY_PTR(keys)[i];
mrb_value val = mrb_hash_get(mrb, t_args, key);
params[mrb_str_to_cstr(mrb, mrb_obj_as_string(mrb, key))] =
mrb_str_to_cstr(mrb, mrb_obj_as_string(mrb, val));
}
}
app::EText *element = new app::EText(key_id, id, color, params);
element->position = {x, y};
element->scale = {scale_x, scale_y};
element->rotation = rotation;
element->pivot = {pivot_x, pivot_y};
element->alpha = alpha;
element->z = z;
std::string click_mode = mrb_sym2name(mrb, click_mode_sym);
if (click_mode == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode == "ignore")
element->click_mode = ClickMode::IGNORE;
s->id = app::element_pool.acquire(element);
app::element_pool.use(s->id);
mrb_data_init(self, s, &ElementText_type);
return self;
}
mrb_value element_text_on_click(mrb_state *mrb, mrb_value self) {
mrb_value block;
mrb_get_args(mrb, "&", &block);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::Element *element = app::element_pool[id];
element->on_click.set_proc(block);
return self;
}
mrb_value element_text_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.ElementText.cls))
return mrb_false_value();
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
Wrapper *other_wrapper = (Wrapper *)DATA_PTR(other);
return mrb_bool_value(self_wrapper->id == other_wrapper->id);
}
mrb_value element_text_inspect(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
std::string str = "#<ElementText:0x"
+ std::format("0x{:016x}", id)
+ " key=:" + Localization::get_key_name(element->key)
+ " font_id=" + std::format("0x{:016x}", element->font_id)
+ ">";
return mrb_str_new_cstr(mrb, str.c_str());
}
mrb_value element_text_to_s(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
std::string localized_text = Localization::get(element->key, element->params);
return mrb_str_new_cstr(mrb, localized_text.c_str());
}
mrb_value element_text_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
return mrb_fixnum_value(wrapper->id);
}
mrb_value element_text_key_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_symbol_value(Localization::lang_to_sym(mrb, element->key));
}
mrb_value element_text_key_set(mrb_state *mrb, mrb_value self) {
mrb_sym key_sym;
mrb_get_args(mrb, "n", &key_sym);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
element->key = Localization::lang_from_sym(mrb, key_sym);
return mrb_nil_value();
}
mrb_value element_text_font_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
if (element->font_id == UINT64_MAX)
return mrb_nil_value();
font_pool.use(element->font_id);
return wrap(definitions::bindings.Font, element->font_id);
}
mrb_value element_text_params(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::element_pool.use(id);
return wrap(definitions::bindings.Params, id);
}
mrb_value element_text_font_set(mrb_state *mrb, mrb_value self) {
mrb_value font_val;
mrb_get_args(mrb, "o", &font_val);
if (!mrb_obj_is_kind_of(mrb, font_val, definitions::bindings.Font.cls)) {
mrb_raise(mrb, mrb_class_get(mrb, "Exception"), "Expected a Font object");
return mrb_nil_value();
}
Wrapper *font_wrapper = (Wrapper *)DATA_PTR(font_val);
uint64_t font_id = font_wrapper->id;
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = self_wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
if (element->font_id != UINT64_MAX)
font_pool.release(element->font_id);
element->font_id = font_id;
font_pool.use(font_id);
return mrb_nil_value();
}
mrb_value element_text_color_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_fixnum_value(
(element->color.r << 16) | (element->color.g << 8) | (element->color.b)
);
}
mrb_value element_text_color_set(mrb_state *mrb, mrb_value self) {
mrb_int color_val;
mrb_get_args(mrb, "i", &color_val);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
element->color = {
(uint8_t)((color_val >> 16) & 0xFF),
(uint8_t)((color_val >> 8) & 0xFF),
(uint8_t)((color_val) & 0xFF)
};
return mrb_nil_value();
}
mrb_value element_text_position_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->position);
}
mrb_value element_text_rotation_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_float_value(mrb, element->rotation);
}
mrb_value element_text_rotation_set(mrb_state *mrb, mrb_value self) {
mrb_float rotation;
mrb_get_args(mrb, "f", &rotation);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
element->rotation = rotation;
return mrb_nil_value();
}
mrb_value element_text_pivot_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->pivot);
}
mrb_value element_text_scale_get(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return definitions::Vec2_wrap(id, &element->scale);
}
mrb_value element_text_alpha_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_float_value(mrb, element->alpha);
}
mrb_value element_text_alpha_set(mrb_state *mrb, mrb_value self) {
mrb_float alpha;
mrb_get_args(mrb, "f", &alpha);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
element->alpha = alpha;
return mrb_nil_value();
}
mrb_value element_text_z_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_float_value(mrb, element->z);
}
mrb_value element_text_z_set(mrb_state *mrb, mrb_value self) {
mrb_float z;
mrb_get_args(mrb, "f", &z);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
element->z = z;
return mrb_nil_value();
}
mrb_value element_text_click_mode_get(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
std::string click_mode_str;
switch (element->click_mode) {
case ClickMode::PASS:
click_mode_str = "pass";
break;
case ClickMode::BLOCK:
click_mode_str = "block";
break;
case ClickMode::IGNORE:
click_mode_str = "ignore";
break;
}
return mrb_symbol_value(mrb_intern_cstr(mrb, click_mode_str.c_str()));
}
mrb_value element_text_click_mode_set(mrb_state *mrb, mrb_value self) {
mrb_sym click_mode_sym;
mrb_get_args(mrb, "n", &click_mode_sym);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
std::string click_mode_str = mrb_sym2name(mrb, click_mode_sym);
if (click_mode_str == "pass")
element->click_mode = ClickMode::PASS;
else if (click_mode_str == "block")
element->click_mode = ClickMode::BLOCK;
else if (click_mode_str == "ignore")
element->click_mode = ClickMode::IGNORE;
return mrb_nil_value();
}
mrb_value element_text_width(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_float_value(mrb, element->size().x);
}
mrb_value element_text_height(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
app::EText *element = (app::EText *)app::element_pool[id];
return mrb_float_value(mrb, element->size().y);
}
} // namespace
namespace definitions {
void register_element_text() {
DEF_CLASS(ElementText, MRB_ARGS_REQ(3) | MRB_ARGS_OPT(2));
ADD_METHOD(ElementText, "==", element_text_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "eql?", element_text_equal, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "inspect", element_text_inspect, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "hash", element_text_hash, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "click", element_text_on_click, MRB_ARGS_BLOCK());
ADD_METHOD(ElementText, "to_s", element_text_to_s, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "key=", element_text_key_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "key", element_text_key_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "color=", element_text_color_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "color", element_text_color_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "font=", element_text_font_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "font", element_text_font_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "params", element_text_params, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "position", element_text_position_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "scale", element_text_scale_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "rotation=", element_text_rotation_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "rotation", element_text_rotation_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "pivot", element_text_pivot_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "alpha=", element_text_alpha_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "alpha", element_text_alpha_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "z=", element_text_z_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "z", element_text_z_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "click_mode=", element_text_click_mode_set, MRB_ARGS_REQ(1));
ADD_METHOD(ElementText, "click_mode", element_text_click_mode_get, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "width", element_text_width, MRB_ARGS_NONE());
ADD_METHOD(ElementText, "height", element_text_height, MRB_ARGS_NONE());
bindings.ElementText = {ElementText_class, &ElementText_type};
}
} // namespace definitions
+72
View File
@@ -0,0 +1,72 @@
#include "bindings/definitions.h"
#include "window/window.h"
namespace {
void Font_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
font_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Font_type = {"Font", Font_free};
mrb_value Font_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Font_type);
Wrapper *s = new Wrapper();
char *path;
mrb_int size;
mrb_value kwargs;
mrb_get_args(mrb, "zi|H", &path, &size, &kwargs);
bool pixel_art = false;
bool bold = false;
bool italic = false;
bool underline = false;
if (!mrb_nil_p(kwargs)) {
HASH_BOOL(kwargs, pixel_art, pixel_art);
HASH_BOOL(kwargs, bold, bold);
HASH_BOOL(kwargs, italic, italic);
HASH_BOOL(kwargs, underline, underline);
}
s->id = window.load_font(path, size, bold, italic, underline, pixel_art);
mrb_data_init(self, s, &Font_type);
return self;
}
mrb_value font_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.Font.cls))
return mrb_false_value();
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id_self = wrapper->id;
Wrapper *other_wrapper = (Wrapper *)DATA_PTR(other);
uint64_t id_other = other_wrapper->id;
return mrb_bool_value(id_self == id_other);
}
mrb_value font_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
return mrb_fixnum_value(wrapper->id);
}
} // namespace
namespace definitions {
void register_font() {
DEF_CLASS(Font, MRB_ARGS_REQ(2) | MRB_ARGS_OPT(1));
ADD_METHOD(Font, "==", font_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Font, "eql?", font_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Font, "hash", font_hash, MRB_ARGS_NONE());
bindings.Font = {Font_class, &Font_type};
}
} // namespace definitions
+160
View File
@@ -0,0 +1,160 @@
#include "bindings/definitions.h"
#include "window/window.h"
namespace {
void Image_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
image_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Image_type = {"Image", Image_free};
mrb_value Image_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Image_type);
Wrapper *s = new Wrapper();
mrb_float width, height;
mrb_value kwargs;
mrb_get_args(mrb, "ff|H", &width, &height, &kwargs);
bool pixel_art = false;
if (!mrb_nil_p(kwargs))
HASH_BOOL(kwargs, pixel_art, pixel_art);
s->id = window.create_image({(float)width, (float)height}, pixel_art);
mrb_data_init(self, s, &Image_type);
return self;
}
mrb_value image_load(mrb_state *mrb, mrb_value) {
char *path;
mrb_value kwargs;
mrb_get_args(mrb, "z|H", &path, &kwargs);
bool pixel_art = false;
if (!mrb_nil_p(kwargs))
HASH_BOOL(kwargs, pixel_art, pixel_art);
uint64_t id = window.load_image(path, pixel_art);
return definitions::wrap(definitions::bindings.Image, id);
}
mrb_value image_load_set(mrb_state *mrb, mrb_value) {
char *path;
mrb_float tile_width, tile_height;
mrb_value kwargs;
mrb_get_args(mrb, "zff|H", &path, &tile_width, &tile_height, &kwargs);
bool pixel_art = false;
int padding_x = 0, padding_y = 0, offset_x = 0, offset_y = 0;
if (!mrb_nil_p(kwargs)) {
HASH_BOOL(kwargs, pixel_art, pixel_art);
mrb_value padding_hash = HASH_GET(kwargs, padding);
if (!mrb_nil_p(padding_hash)) {
HASH_FLOAT(padding_hash, x, padding_x);
HASH_FLOAT(padding_hash, y, padding_y);
}
mrb_value offset_hash = HASH_GET(kwargs, offset);
if (!mrb_nil_p(offset_hash)) {
HASH_FLOAT(offset_hash, x, offset_x);
HASH_FLOAT(offset_hash, y, offset_y);
}
}
std::vector<uint64_t> images = window.load_image_set(
path,
{(float)tile_width, (float)tile_height},
{(float)padding_x, (float)padding_y},
{(float)offset_x, (float)offset_y},
pixel_art
);
mrb_value image_array = mrb_ary_new_capa(mrb, images.size());
for (uint64_t image_id : images) {
image_pool.use(image_id);
mrb_value image_obj = definitions::wrap(definitions::bindings.Image, image_id);
mrb_ary_push(mrb, image_array, image_obj);
}
return image_array;
}
mrb_value image_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.Image.cls))
return mrb_false_value();
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id_self = wrapper->id;
Wrapper *other_wrapper = (Wrapper *)DATA_PTR(other);
uint64_t id_other = other_wrapper->id;
return mrb_bool_value(id_self == id_other);
}
mrb_value image_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
return mrb_fixnum_value(wrapper->id);
}
mrb_value image_width(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
return mrb_float_value(mrb, window.get_image_size(id).x);
}
mrb_value image_height(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
return mrb_float_value(mrb, window.get_image_size(id).y);
}
mrb_value image_update(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
mrb_float x, y, width, height;
char *new_content;
mrb_int length;
mrb_get_args(mrb, "ffffs", &x, &y, &width, &height, &new_content, &length);
bool success = window.update_image(
id,
Rect::from(
{(float)x, (float)y},
{(float)width, (float)height}
),
new_content,
(uint32_t)length
);
return mrb_bool_value(success);
}
} // namespace
namespace definitions {
void register_image() {
DEF_CLASS(Image, MRB_ARGS_REQ(2) | MRB_ARGS_OPT(1));
ADD_CLASS_METHOD(Image, "load", image_load, MRB_ARGS_REQ(1) | MRB_ARGS_OPT(1));
ADD_CLASS_METHOD(Image, "load_set", image_load_set, MRB_ARGS_REQ(3) | MRB_ARGS_OPT(1));
ADD_METHOD(Image, "==", image_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Image, "eql?", image_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Image, "hash", image_hash, MRB_ARGS_NONE());
ADD_METHOD(Image, "width", image_width, MRB_ARGS_NONE());
ADD_METHOD(Image, "height", image_height, MRB_ARGS_NONE());
ADD_METHOD(Image, "update", image_update, MRB_ARGS_REQ(5));
bindings.Image = {Image_class, &Image_type};
}
} // namespace definitions
+68
View File
@@ -0,0 +1,68 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
void Params_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::element_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Params_type = {"Params", Params_free};
mrb_value Params_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Params_type);
Wrapper *s = new Wrapper();
mrb_int id;
mrb_get_args(mrb, "z", &id);
app::element_pool.use(id);
s->id = id;
mrb_data_init(self, s, &Params_type);
return self;
}
mrb_value params_get(mrb_state *mrb, mrb_value self) {
mrb_sym key;
mrb_get_args(mrb, "n", &key);
uint64_t id = ((Wrapper *)DATA_PTR(self))->id;
app::element_pool.use(id);
app::EText *element = (app::EText *)app::element_pool[id];
auto it = element->params.find(mrb_sym2name(mrb, key));
if (it == element->params.end())
return mrb_nil_value();
return mrb_str_new_cstr(mrb, it->second.c_str());
}
mrb_value params_set(mrb_state *mrb, mrb_value self) {
mrb_sym key;
mrb_value value;
mrb_get_args(mrb, "no", &key, &value);
uint64_t id = ((Wrapper *)DATA_PTR(self))->id;
app::element_pool.use(id);
app::EText *element = (app::EText *)app::element_pool[id];
element->params[mrb_sym2name(mrb, key)] = mrb_str_to_cstr(mrb, mrb_obj_as_string(mrb, value));
element->reload_text();
return mrb_nil_value();
}
} // namespace
namespace definitions {
void register_params() {
DEF_CLASS(Params, MRB_ARGS_REQ(1));
ADD_METHOD(Params, "[]", params_get, MRB_ARGS_REQ(1));
ADD_METHOD(Params, "[]=", params_set, MRB_ARGS_REQ(2));
bindings.Params = {Params_class, &Params_type};
}
} // namespace definitions
+225
View File
@@ -0,0 +1,225 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
void Scene_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::scene_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Scene_type = {"Scene", Scene_free};
mrb_value Scene_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Scene_type);
Wrapper *s = new Wrapper();
app::Scene *loading_scene = new app::Scene{};
s->id = app::scene_pool.acquire(loading_scene);
app::scene_pool.use(s->id);
mrb_data_init(self, s, &Scene_type);
return self;
}
mrb_value scene_add_element(mrb_state *mrb, mrb_value self) {
mrb_value element_val;
mrb_get_args(mrb, "o", &element_val);
if (
!mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementText.cls)
&& !mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementRect.cls)
&& !mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementImage.cls)
) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an ElementText, ElementRect, or ElementImage object"
);
return mrb_nil_value();
}
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
app::Scene *scene = app::scene_pool[wrapper->id];
Wrapper *element_wrapper = (Wrapper *)DATA_PTR(element_val);
scene->add_element(element_wrapper->id);
return self;
}
mrb_value scene_delete_element(mrb_state *mrb, mrb_value self) {
mrb_value element_val;
mrb_get_args(mrb, "o", &element_val);
if (
!mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementText.cls)
&& !mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementRect.cls)
&& !mrb_obj_is_kind_of(mrb, element_val, definitions::bindings.ElementImage.cls)
) {
mrb_raise(
mrb,
mrb_class_get(mrb, "Exception"),
"Expected an ElementText, ElementRect, or ElementImage object"
);
return mrb_nil_value();
}
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
app::Scene *scene = app::scene_pool[wrapper->id];
Wrapper *element_wrapper = (Wrapper *)DATA_PTR(element_val);
scene->delete_element(element_wrapper->id);
return self;
}
mrb_value scene_on_update(mrb_state *mrb, mrb_value self) {
mrb_value block;
mrb_get_args(mrb, "&", &block);
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
app::Scene *scene = app::scene_pool[wrapper->id];
scene->update.set_proc(block);
return self;
}
mrb_value scene_elements_at(mrb_state *mrb, mrb_value self) {
mrb_value position_val;
mrb_get_args(mrb, "o", &position_val);
float x = 0, y = 0;
HASH_FLOAT(position_val, x, x);
HASH_FLOAT(position_val, y, y);
Vec2<float> position = {x, y};
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
app::Scene *scene = app::scene_pool[wrapper->id];
std::vector<uint64_t> element_ids = scene->elements_at(position);
mrb_value result = mrb_ary_new_capa(mrb, element_ids.size());
for (uint64_t element_id : element_ids) {
app::Element *element = app::element_pool[element_id];
app::element_pool.use(element_id);
mrb_value obj;
switch (element->type) {
case Type::TEXT:
obj = definitions::wrap(
definitions::bindings.ElementText,
element_id
);
break;
case Type::RECT:
obj = definitions::wrap(
definitions::bindings.ElementRect,
element_id
);
break;
case Type::IMAGE:
obj = definitions::wrap(
definitions::bindings.ElementImage,
element_id
);
mrb_ary_push(mrb, result, obj);
break;
default:
continue;
}
mrb_ary_push(mrb, result, obj);
}
return result;
}
mrb_value scene_elements(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
app::Scene *scene = app::scene_pool[wrapper->id];
std::vector<uint64_t> element_ids = scene->element_ids;
mrb_value result = mrb_ary_new_capa(mrb, element_ids.size());
for (uint64_t element_id : element_ids) {
app::Element *element = app::element_pool[element_id];
app::element_pool.use(element_id);
mrb_value obj;
switch (element->type) {
case Type::TEXT:
obj = definitions::wrap(
definitions::bindings.ElementText,
element_id
);
break;
case Type::RECT:
obj = definitions::wrap(
definitions::bindings.ElementRect,
element_id
);
break;
case Type::IMAGE:
obj = definitions::wrap(
definitions::bindings.ElementImage,
element_id
);
mrb_ary_push(mrb, result, obj);
break;
default:
continue;
}
mrb_ary_push(mrb, result, obj);
}
return result;
}
mrb_value scene_equal(mrb_state *mrb, mrb_value self) {
mrb_value other;
mrb_get_args(mrb, "o", &other);
if (!mrb_obj_is_kind_of(mrb, other, definitions::bindings.Scene.cls))
return mrb_false_value();
Wrapper *self_wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id_self = self_wrapper->id;
Wrapper *other_wrapper = (Wrapper *)DATA_PTR(other);
uint64_t id_other = other_wrapper->id;
return mrb_bool_value(id_self == id_other);
}
mrb_value scene_inspect(mrb_state *mrb, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
return mrb_str_new_cstr(mrb, ("#<Scene:" + std::to_string(id) + ">").c_str());
}
mrb_value scene_hash(mrb_state *, mrb_value self) {
Wrapper *wrapper = (Wrapper *)DATA_PTR(self);
uint64_t id = wrapper->id;
return mrb_fixnum_value(id);
}
} // namespace
namespace definitions {
void register_scene() {
DEF_CLASS(Scene, MRB_ARGS_NONE());
ADD_METHOD(Scene, "==", scene_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "eql?", scene_equal, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "inspect", scene_inspect, MRB_ARGS_NONE());
ADD_METHOD(Scene, "to_s", scene_inspect, MRB_ARGS_NONE());
ADD_METHOD(Scene, "hash", scene_hash, MRB_ARGS_NONE());
ADD_METHOD(Scene, "<<", scene_add_element, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "delete", scene_delete_element, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "remove", scene_delete_element, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "update", scene_on_update, MRB_ARGS_BLOCK());
ADD_METHOD(Scene, "elements_at", scene_elements_at, MRB_ARGS_REQ(1));
ADD_METHOD(Scene, "elements", scene_elements, MRB_ARGS_NONE());
bindings.Scene = {Scene_class, &Scene_type};
}
} // namespace definitions
+90
View File
@@ -0,0 +1,90 @@
#include "app/app.h"
#include "bindings/definitions.h"
namespace {
struct Vec2Wrapper : public Wrapper {
Vec2<float> *vec;
};
void Vec2_free(mrb_state *mrb, void *ptr) {
UNUSED(mrb);
uint64_t id = ((Wrapper *)ptr)->id;
app::element_pool.release(id);
delete (Wrapper *)ptr;
}
const struct mrb_data_type Vec2_type = {"Vec2", Vec2_free};
mrb_value Vec2_init(mrb_state *mrb, mrb_value self) {
Wrapper *old = (Wrapper *)DATA_PTR(self);
if (old)
mrb_free(mrb, old);
mrb_data_init(self, nullptr, &Vec2_type);
Vec2Wrapper *s = new Vec2Wrapper();
mrb_int id;
mrb_get_args(mrb, "i", &id);
app::element_pool.use(id);
s->id = id;
s->vec = &((app::EText *)app::element_pool[id])->position;
mrb_data_init(self, s, &Vec2_type);
return self;
}
mrb_value vec2_x(mrb_state *mrb, mrb_value self) {
Vec2Wrapper *s = (Vec2Wrapper *)DATA_PTR(self);
return mrb_float_value(mrb, s->vec->x);
}
mrb_value vec2_y(mrb_state *mrb, mrb_value self) {
Vec2Wrapper *s = (Vec2Wrapper *)DATA_PTR(self);
return mrb_float_value(mrb, s->vec->y);
}
mrb_value vec2_x_set(mrb_state *mrb, mrb_value self) {
mrb_float x;
mrb_get_args(mrb, "f", &x);
Vec2Wrapper *s = (Vec2Wrapper *)DATA_PTR(self);
s->vec->x = (float)x;
return mrb_nil_value();
}
mrb_value vec2_y_set(mrb_state *mrb, mrb_value self) {
mrb_float y;
mrb_get_args(mrb, "f", &y);
Vec2Wrapper *s = (Vec2Wrapper *)DATA_PTR(self);
s->vec->y = (float)y;
return mrb_nil_value();
}
} // namespace
namespace definitions {
void register_vec2() {
DEF_CLASS(Vec2, MRB_ARGS_REQ(2));
ADD_METHOD(Vec2, "x", vec2_x, MRB_ARGS_NONE());
ADD_METHOD(Vec2, "y", vec2_y, MRB_ARGS_NONE());
ADD_METHOD(Vec2, "x=", vec2_x_set, MRB_ARGS_REQ(1));
ADD_METHOD(Vec2, "y=", vec2_y_set, MRB_ARGS_REQ(1));
bindings.Vec2 = {Vec2_class, &Vec2_type};
}
mrb_value Vec2_wrap(uint64_t id, Vec2<float> *vec) {
RBasic *obj_b = mrb_obj_alloc(Ruby::mrb, MRB_TT_DATA, bindings.Vec2.cls);
mrb_value obj = mrb_obj_value(obj_b);
Vec2Wrapper *w = new Vec2Wrapper();
app::element_pool.use(id);
w->id = id;
w->vec = vec;
mrb_data_init(obj, w, &Vec2_type);
return obj;
}
} // namespace definitions
+114
View File
@@ -0,0 +1,114 @@
#include "localization/localization.h"
namespace Localization {
mrb_sym get_var_sym(mrb_state *mrb, const std::string &name) {
auto it = var_sym_cache.find(name);
if (it != var_sym_cache.end())
return it->second;
mrb_sym sym = mrb_intern_cstr(mrb, name.c_str());
var_sym_cache[name] = sym;
return sym;
}
std::vector<Segment> parse_localized(const std::string &input) {
std::vector<Segment> out;
size_t i = 0;
while (i < input.size()) {
size_t start = input.find("%{", i);
if (start == std::string::npos) {
out.push_back({false, input.substr(i)});
break;
}
if (start > i) {
out.push_back({false, input.substr(i, start - i)});
}
size_t end = input.find("}", start);
if (end == std::string::npos) {
// malformed fallback
out.push_back({false, input.substr(start)});
break;
}
std::string var = input.substr(start + 2, end - (start + 2));
out.push_back({true, var});
i = end + 1;
}
return out;
}
std::string get(Key key, const std::unordered_map<std::string, std::string> &params) {
auto it = localized_strings.find(join_keys(current_language, key));
if (it == localized_strings.end())
return "Localization not set!";
const auto &entry = it->second;
std::string result;
result.reserve(10);
for (const auto &seg : entry) {
if (!seg.is_var) {
result += seg.text;
} else {
auto param_it = params.find(seg.text);
if (param_it == params.end()) {
result += "%{" + seg.text + "}";
continue;
}
result += param_it->second;
}
}
return result;
}
LanguageCode lang_from_sym(mrb_state *mrb, mrb_sym sym) {
const char *s = mrb_sym2name(mrb, sym);
size_t len = strlen(s);
if (len == 0 || len > 4)
return 0;
LanguageCode code = 0;
for (size_t i = 0; i < len; ++i) {
code |= (static_cast<LanguageCode>(s[i]) << (8 * (3 - i)));
}
return code;
}
mrb_sym lang_to_sym(mrb_state *mrb, LanguageCode code) {
char s[5] = {0};
for (int i = 0; i < 4; ++i)
s[i] = (code >> (8 * (3 - i))) & 0xFF;
return mrb_intern_cstr(mrb, s);
}
Key key_from_sym(mrb_state *mrb, mrb_sym sym) {
const char *key_name = mrb_sym2name(mrb, sym);
auto it = localization_key_map.find(key_name);
if (it != localization_key_map.end())
return it->second;
Key new_id = (Key)localization_key_map.size();
localization_key_map[key_name] = new_id;
return new_id;
}
// This is only used for debugging and inspect output, so performance is not a concern
std::string get_key_name(Key key) {
for (const auto &[str, k] : localization_key_map)
if (k == key)
return str;
return "Unknown Key";
}
} // namespace Localization
+10 -2
View File
@@ -1,4 +1,4 @@
#include "binding/definitions.h" #include "bindings/definitions.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
Ruby::load_mgems(); Ruby::load_mgems();
@@ -15,7 +15,15 @@ int main(int argc, char *argv[]) {
fseek(file, 0, SEEK_SET); fseek(file, 0, SEEK_SET);
char *script_content = (char *)malloc(size + 1); char *script_content = (char *)malloc(size + 1);
fread(script_content, 1, size, file); size_t read = fread(script_content, 1, size, file);
if (read != size) {
fprintf(stderr, "Failed to read startup script: %s\n", startup_script);
free(script_content);
fclose(file);
return 1;
}
script_content[size] = '\0'; script_content[size] = '\0';
fclose(file); fclose(file);
+173 -6
View File
@@ -57,17 +57,52 @@ Window::~Window() {
} }
uint64_t Window::load_image(const char *path, bool pixel_art) { uint64_t Window::load_image(const char *path, bool pixel_art) {
SDL_Texture *texture = IMG_LoadTexture(renderer, path); SDL_Surface *surface = IMG_Load(path);
if (!surface) {
fprintf(stderr, "Failed to load image: %s\n", SDL_GetError());
return UINT64_MAX;
}
SDL_Surface *rgba = SDL_ConvertSurface(
surface,
SDL_PIXELFORMAT_RGBA8888
);
SDL_DestroySurface(surface);
if (!rgba) {
fprintf(stderr, "Failed to convert image: %s\n", SDL_GetError());
return UINT64_MAX;
}
SDL_Texture *texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING,
rgba->w,
rgba->h
);
if (!texture) {
SDL_DestroySurface(rgba);
fprintf(stderr, "Failed to create texture: %s\n", SDL_GetError());
return UINT64_MAX;
}
SDL_UpdateTexture(
texture,
nullptr,
rgba->pixels,
rgba->pitch
);
SDL_DestroySurface(rgba);
if (pixel_art) { if (pixel_art) {
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST); SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
} }
if (texture == nullptr) {
fprintf(stderr, "Failed to load image: %s\n", SDL_GetError());
return UINT64_MAX;
}
float w, h; float w, h;
SDL_GetTextureSize(texture, &w, &h); SDL_GetTextureSize(texture, &w, &h);
@@ -78,6 +113,138 @@ uint64_t Window::load_image(const char *path, bool pixel_art) {
return id; return id;
} }
std::vector<uint64_t> Window::load_image_set(const char *path, Vec2<float> tile_size, Vec2<float> padding, Vec2<float> offset, bool pixel_art) {
SDL_Surface *surface = IMG_Load(path);
if (!surface) {
fprintf(stderr, "Failed to load image set: %s\n", SDL_GetError());
return {};
}
std::vector<uint64_t> image_ids;
int columns = (int)((surface->w - offset.x + padding.x) / (tile_size.x + padding.x));
int rows = (int)((surface->h - offset.y + padding.y) / (tile_size.y + padding.y));
for (int y = 0; y < rows; y++) {
for (int x = 0; x < columns; x++) {
SDL_Rect src_rect = {
(int)(offset.x + x * (tile_size.x + padding.x)),
(int)(offset.y + y * (tile_size.y + padding.y)),
(int)tile_size.x,
(int)tile_size.y
};
SDL_Surface *tile_surface = SDL_CreateSurface(src_rect.w, src_rect.h, SDL_PIXELFORMAT_RGBA8888);
SDL_BlitSurface(surface, &src_rect, tile_surface, nullptr);
SDL_Texture *texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING,
tile_surface->w,
tile_surface->h
);
if (!texture) {
fprintf(stderr, "Failed to create texture for tile: %s\n", SDL_GetError());
SDL_DestroySurface(tile_surface);
continue;
}
SDL_UpdateTexture(
texture,
nullptr,
tile_surface->pixels,
tile_surface->pitch
);
if (pixel_art) {
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
}
Image *img = new Image{texture, {(float)tile_surface->w, (float)tile_surface->h}};
uint64_t id = image_pool.acquire(img);
image_pool.use(id);
image_ids.push_back(id);
SDL_DestroySurface(tile_surface);
}
}
SDL_DestroySurface(surface);
return image_ids;
}
uint64_t Window::create_image(Vec2<float> size, bool pixel_art) {
SDL_Texture *texture = SDL_CreateTexture(
renderer,
SDL_PIXELFORMAT_RGBA8888,
SDL_TEXTUREACCESS_STREAMING,
(int)size.x,
(int)size.y
);
if (!texture) {
fprintf(stderr, "Failed to create scripted texture: %s\n", SDL_GetError());
return UINT64_MAX;
}
SDL_SetTextureBlendMode(texture, SDL_BLENDMODE_BLEND);
if (pixel_art)
SDL_SetTextureScaleMode(texture, SDL_SCALEMODE_NEAREST);
Image *img = new Image{texture, size};
uint64_t id = image_pool.acquire(img);
image_pool.use(id);
char *buffer = (char *)malloc(size.x * size.y * 4 * sizeof(char));
for (unsigned int i = 0; i < (unsigned int)(size.x * size.y * 4); i += 4) {
buffer[i] = 0xFF;
buffer[i + 1] = 0xFF;
buffer[i + 2] = 0xFF;
buffer[i + 3] = 0xFF;
}
SDL_UpdateTexture(
img->texture,
nullptr,
buffer,
(int)(size.x * 4)
);
free(buffer);
return id;
}
bool Window::update_image(uint64_t image_id, Rect content_rect, const char *new_content, uint32_t length) {
if (!image_pool.is_valid(image_id)) {
fprintf(stderr, "Invalid image ID: %llu\n", (unsigned long long)image_id);
return false;
}
Image *img = image_pool[image_id];
SDL_Rect sdl_rect = {
(int)content_rect.x(),
(int)content_rect.y(),
(int)content_rect.w(),
(int)content_rect.h()
};
SDL_UpdateTexture(
img->texture,
&sdl_rect,
new_content,
(int)(content_rect.w() * 4)
);
return true;
};
Vec2<float> Window::get_image_size(uint64_t image_id) { Vec2<float> Window::get_image_size(uint64_t image_id) {
if (!image_pool.is_valid(image_id)) { if (!image_pool.is_valid(image_id)) {
fprintf(stderr, "Invalid image ID: %llu\n", (unsigned long long)image_id); fprintf(stderr, "Invalid image ID: %llu\n", (unsigned long long)image_id);
-1
View File
@@ -1 +0,0 @@
puts "hello"
+265 -85
View File
@@ -1,20 +1,18 @@
# Outline
# Analysis # Analysis
## What is a game engine? ## What is a game engine?
The goal of this project is to build a high performance cross-desktop game engine for 2d games, The goal of this project is to build a high performance cross-desktop game engine for 2d games,
When a developer wishes to build a game they generally have 2 options, either they can code the entire game themselves by using a pure rendering library and handling all teh assets etc. themselves, this gives them a lot of control over how the game behaves and can usually also allow them to do a lot of optimizations specific to their idea, but it means they will have to make the coordinate systems and data store, asset management systems etc. all by themselves, on the other hand they could use a game engine. When a developer wishes to build a game they generally have 2 options, either they can code the entire game themselves by using a pure rendering library and handling all the assets etc. themselves, this gives them a lot of control over how the game behaves and can usually also allow them to do a lot of optimizations specific to their idea, but it means they will have to make the coordinate systems and data store, asset management systems etc. all by themselves, on the other hand they could use a game engine.
the most significant advantage of using game engines when building a game is simplifying development process. the most significant advantage of using game engines when building a game is simplifying development process.
They do this by allowing the developer to do a lot more out of teh box without having to reinvent the wheel themselves: They do this by allowing the developer to do a lot more out of the box without having to reinvent the wheel themselves:
- They allow making teh game only once but having it work crossplatform on multiple different types of systems - They allow making the game only once but having it work crossplatform on multiple different types of systems
- They allow asset management of game asstes like images, textures, audios etc. without teh need to ensure a uniform format and loading them or decoding them manually - They allow asset management of game asstes like images, textures, audios etc. without the need to ensure a uniform format and loading them or decoding them manually
- They also usually have built in physics engines for the coordinate system, which do not need to be reimplemented with al that math by teh developer, and these are done in optimized hot loops for greater performance. - They also usually have built in physics engines for the coordinate system, which do not need to be reimplemented with al that math by the developer, and these are done in optimized hot loops for greater performance.
The engine is expected to be built in a low level context (for high performance), but allow using it from a high level simpler environment without losing any of the performance. It should also make the development process of a game easy with any common parts for games implemented and reusable. The engine is expected to be built in a low level context (for high performance), but allow using it from a high level simpler environment without losing any of the performance. It should also make the development process of a game easy with any common parts for games implemented and reusable.
@@ -39,7 +37,7 @@ They extend a node's behaviour.
So in godot you first define the blueprint, the structure of the game using the ui (or the markup files), and then attach scripts to the nodes to extend their behaviour and actually make them work, these scripts can intercommunicate using events. So in godot you first define the blueprint, the structure of the game using the ui (or the markup files), and then attach scripts to the nodes to extend their behaviour and actually make them work, these scripts can intercommunicate using events.
Then the godot runtime extracts these scenes and builds teh node tree out of them and runs the scripts attached to the loaded nodes. Then the godot runtime extracts these scenes and builds the node tree out of them and runs the scripts attached to the loaded nodes.
these scripts are generally written in gdscript which is an interpreted language that is prcompiled to bytecode when the project is built and later on is executed by a runtime-VM. these scripts are generally written in gdscript which is an interpreted language that is prcompiled to bytecode when the project is built and later on is executed by a runtime-VM.
@@ -72,8 +70,8 @@ It is primaririly a ruby library for rendering and input capture.
It is very good for fast prototyping as it has very simple methods and uses ruby which is very good for fast prototyping (see next sec). It is very good for fast prototyping as it has very simple methods and uses ruby which is very good for fast prototyping (see next sec).
But it does not have a scene or nodes or object system of it's own, all such systems are to be implemented by the developer themselves, and while it provides methods for loading and using of assets they still need to be managed manually. This comes with the up that the developer can chose what kind of system they want to use. But it does not have a scene or nodes or object system of it's own, all such systems are to be implemented by the developer themselves, and while it provides methods for loading and using of assets they still need to be managed manually. This comes with the up that the developer can chose what kind of system they want to use.
It also requires teh ruby runtime and teh full source code when sharing making it less than ideal for any production games. It also requires the ruby runtime and the full source code when sharing making it less than ideal for any production games.
Also any kinds of GPU shading requires teh OpenGL libraries to be used directly and it provides no proper access to them. Also any kinds of GPU shading requires the OpenGL libraries to be used directly and it provides no proper access to them.
As per their own homepage: Gosu is mainly used to teach or learn Ruby. As per their own homepage: Gosu is mainly used to teach or learn Ruby.
Comparisions of architectures: Comparisions of architectures:
@@ -81,42 +79,22 @@ Comparisions of architectures:
- Data layout - Data layout
- In the node based system that godot uses each entity (node), stores all it's data in a single object - In the node based system that godot uses each entity (node), stores all it's data in a single object
- This means that when an object is to be worked on it can be used from a single place. It also closely follows a OOP like idea meaning it is simpler for developers more used to it. - This means that when an object is to be worked on it can be used from a single place. It also closely follows a OOP like idea meaning it is simpler for developers more used to it.
- But it creates cache locality problems for example when teh physics system wants to move all elements forward, each object will have to be loaded and then updated and as objects do not have to be together in memory it causes a lot of access overhead. - But it creates cache locality problems for example when the physics system wants to move all elements forward, each object will have to be loaded and then updated and as objects do not have to be together in memory it causes a lot of access overhead.
- On the other hand ECS systems (like the one that can be used in unity), seperate pure data (compoenents) from teh entities. - On the other hand ECS systems (like the one that can be used in unity), seperate pure data (compoenents) from the entities.
- This means that when a system wishes to work on a single kind of compoenent it can use tight loops to leverage performance gains from cpu cache locality. - This means that when a system wishes to work on a single kind of compoenent it can use tight loops to leverage performance gains from cpu cache locality.
- It also scales very well for having way more entities as entities only use up space for the compoenents they require. - It also scales very well for having way more entities as entities only use up space for the compoenents they require.
- But it is also a much more complex mental model for the developer to work with. - But it is also a much more complex mental model for the developer to work with.
- Behaviour control - Behaviour control
- In godot the bahaviour is very tightly attached to each node, and when executing teh game the behaviour of each node is run fully before going on to the next, this means that similar behaviour like physics is recalculated for each node seperately - In godot the bahaviour is very tightly attached to each node, and when executing the game the behaviour of each node is run fully before going on to the next, this means that similar behaviour like physics is recalculated for each node seperately
- While in unity most scripts are global systems, these systems can bulk operate on data making them better at potential multithreading or cpu optimizations. - While in unity most scripts are global systems, these systems can bulk operate on data making them better at potential multithreading or cpu optimizations.
- but they require ore boilerplate code as logic has to be filtered down to apply to the correct nodes manually rather than by vrtue of existing on a node. - but they require ore boilerplate code as logic has to be filtered down to apply to the correct nodes manually rather than by vrtue of existing on a node.
- While in a more simple framework like gosu these are all dependant on teh developers implementation. - While in a more simple framework like gosu these are all dependant on the developers implementation.
^ Gosu. (n.d.). Gosu Homepage. [online] Available at: https://www.libgosu.org/ [Accessed 13 May 2026]. ^ Gosu. (n.d.). Gosu Homepage. [online] Available at: https://www.libgosu.org/ [Accessed 13 May 2026].
^ Unity Docs. (n.d.). Unity Entity Component System concepts. [online] Available at: https://docs.unity3d.com/Packages/com.unity.entities@6.5/manual/concepts-intro.html [Accessed 13 May 2026]. See sub-links. ^ Unity Docs. (n.d.). Unity Entity Component System concepts. [online] Available at: https://docs.unity3d.com/Packages/com.unity.entities@6.5/manual/concepts-intro.html [Accessed 13 May 2026]. See sub-links.
^ Godot Engine documentation. (n.d.). Overview of Godots key concepts. [online] Available at: https://docs.godotengine.org/en/stable/getting_started/introduction/key_concepts_overview.html [Accessed 13 May 2026]. ^ Godot Engine documentation. (n.d.). Overview of Godots key concepts. [online] Available at: https://docs.godotengine.org/en/stable/getting_started/introduction/key_concepts_overview.html [Accessed 13 May 2026].
^ Godot Engine documentation. (2026). Godots architecture overview. [online] Available at: https://docs.godotengine.org/en/stable/engine_details/architecture/godot_architecture_diagram.html [Accessed 13 May 2026]. ^ Godot Engine documentation. (2026). Godots architecture overview. [online] Available at: https://docs.godotengine.org/en/stable/engine_details/architecture/godot_architecture_diagram.html [Accessed 13 May 2026].
## Embedded scripting
This engine does not use a UI builder. And so all gameplay logic, entity behaviour, definitions etc. must be implemented directly through code.
Because of this it is important to use a well established language in which teh developer can write their code.
Embedded scripting languages are programming lanaguges that are designed to be embedded in a host programming language. The embedded language is often an interpreted language which teh host is a compiled static typed language. They allow adding code to teh program that can be modified and interpreted at runtime. They are often used to extend on teh functionality of teh host language. And they also allow using functions defined in the host language which are usually much more performance optimized.
They are usually very lightweight (so that teh final binary doesn't get bloated and runs fast enough). They are very useful for cuztom DSL/syntax.
And according to [this aticle](https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html) they are particularly useful for game engines.
We can also see a list [list](https://dbohdan.github.io/embedded-scripting-languages/) of embedded scripting languages that exist, common ones being Lua, javascript variants, squirrel, and ruby variants (mruby in particular).
These all have a C-ABI, meaning they can be used by almost any compiled language.
The author [here](https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html)
^ Rordrigues, C. (2023). Embedded Scripting Languages. [online] caiorss.github.io. Available at: https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html [Accessed 13 May 2026].
^ Github. (2022). Embedded scripting languages. [online] Available at: https://dbohdan.github.io/embedded-scripting-languages/ [Accessed 13 May 2026].
## Target users ## Target users
The target users for this project could be one of 2 categories: The target users for this project could be one of 2 categories:
@@ -125,12 +103,12 @@ The target users for this project could be one of 2 categories:
- The engine can be used by game developers to create high performance games quickly. - The engine can be used by game developers to create high performance games quickly.
- The games are also built cross-platform meaning they can use the same project to export for different kinds of users. - The games are also built cross-platform meaning they can use the same project to export for different kinds of users.
- The standalone output of teh game engine means tehy wont have to worry about sharing multiple files and makes releases easier. - The standalone output of the game engine means they wont have to worry about sharing multiple files and makes releases easier.
- And for my own games it would help create a unique brand image for my games, it also helps out with any potential licensing issues with using external engines in my games. - And for my own games it would help create a unique brand image for my games, it also helps out with any potential licensing issues with using external engines in my games.
2. Educators 2. Educators
- The game engine can be used by educators to teach programming / game development to students without making them build all teh systems from scratch. - The game engine can be used by educators to teach programming / game development to students without making them build all the systems from scratch.
- Using such an engine means that the students could be taught about paradigms like ecs and OOP in a simple environment. - Using such an engine means that the students could be taught about paradigms like ecs and OOP in a simple environment.
- They would also be learning ruby, which is an arguably very good for rapid prototyping due to its forgiving and highly readable syntax. - They would also be learning ruby, which is an arguably very good for rapid prototyping due to its forgiving and highly readable syntax.
@@ -138,7 +116,7 @@ The target users for this project could be one of 2 categories:
Survey: Survey:
In order to help me gague what my users would like, In order to help me gauge what my users would like,
we'll be splitting the survey into 2 sections, one for indie developers and one for educators we'll be splitting the survey into 2 sections, one for indie developers and one for educators
@@ -218,56 +196,196 @@ we'll be splitting the survey into 2 sections, one for indie developers and one
## Results ## Results
Summarization of survey results and its limitations Summarization of survey results / not done yet as not enought data collected.
## What more is needed? ### Limitations
Other features that a game engine should have (of the results)
core: ## Hardware
- app, defined with fixed heght/width and aspect ratio is maintained with letterboxing, and fps get steam data on hardware usage around the world explain my setup testing on a msi core 7 / 5060 and iGPU and ideapad
for software make sure it works on linux and make it theoretically work on other os but do not test or check for it
- scenes ## Libraries / dependencies
- elements in scenes (in screen coordinates)
- ElementText
- ElementRect
- ElementImage
- ElementScript - uses function to return a pizel buffer each frame, can use frame caching (for stuff liek minimaps)
- should not reallocate every frame, can use dirty flag based system, uses a single string \
from ruby and maybe keep string alive (add to gc exception) on c++ side too and use the same buffer to send to sdl. \
so 2 copies 1 from ruby which can eb directly edited and then 1 a sdl texture object
- ElementWorld - (It is a view through a camera)
- The world has to have an ecs system buitl in it ### Rendering
- components are type definitions (and thier instances are stored in cache efficient arrays)
- systems use queries to select components (with filtering) and can perform bulk operations on them
- entities - an id and position which can relate to compoenent instances
- The world can render everything in view based on camera, and everything inside here is in world coordinates #### why use a rendering lib
- shaders, layers on top of teh world, (per world, not possible per entity) To set up and render something to the screen in the first place we need a system to be able to do so.
extra: I plan of supporting development for windows/linux where linux has 2 main systems for window management (X11 / wayland).
- physics (forces, mass, movement and collisions) But each of these systems have different interfaces:
- Localization
- keymappings (and settings)
- tilesets
- sprite system (tiled, animated, static etc.)
- 9 segment ui system
- audio playing
- network interface, with a lib to work with rack backends (and generic socket api)
- persistence store
- rng helpers
## Embedded scripting language choice. For example this is how the api works on windows:
```c
// Register the window class.
const wchar_t CLASS_NAME[] = L"Sample Window Class";
WNDCLASS wc = { };
wc.lpfnWndProc = WindowProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// Create the window.
HWND hwnd = CreateWindowEx(
0, // Optional window styles.
CLASS_NAME, // Window class
L"Learn to Program Windows", // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
NULL, // Parent window
NULL, // Menu
hInstance, // Instance handle
NULL // Additional application data
);
if (hwnd == NULL)
{
return 0;
}
ShowWindow(hwnd, nCmdShow);
```
source: https://learn.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window
x11:
```c
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <X11/Xlib.h>
bool quited = false;
void on_delete(Display * display, Window window)
{
XDestroyWindow(display, window);
quited = true;
}
extern int main(int argc, char *argv[])
{
Display * display = XOpenDisplay(NULL);
if (NULL == display) {
fprintf(stderr, "Failed to initialize display");
return EXIT_FAILURE;
}
Window root = DefaultRootWindow(display);
if (None == root) {
fprintf(stderr, "No root window found");
XCloseDisplay(display);
return EXIT_FAILURE;
}
Window window = XCreateSimpleWindow(display, root, 0, 0, 800, 600, 0, 0, 0xffffffff);
if (None == window) {
fprintf(stderr, "Failed to create window");
XCloseDisplay(display);
return EXIT_FAILURE;
}
XMapWindow(display, window);
Atom wm_delete_window = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display, window, & wm_delete_window, 1);
XEvent event;
while (!quited) {
XNextEvent(display, &event);
switch(event.type) {
case ClientMessage:
if(event.xclient.data.l[0] == wm_delete_window) {
on_delete(event.xclient.display, event.xclient.window);
}
break;
}
}
XCloseDisplay(display);
return 0;
}
```
source: https://gist.github.com/m1nuz/8f8f10a7f8715b62fe79#file-simple-x11-window-c
wayland:
https://github.com/zezba9000/WaylandClientWindow
https://wayland-book.com/protocol-design/wire-protocol.html
As wayland is much more structured as a protocol than just a library setting a window up for it requires much more boilerplate if done from scratch.
And it requires much more in depth knowledge of how the protocol works, which would be out of the scope of what this project aims for.
As you can see each one has a vastly different setup and hardcoding each one manually would take time and focus away from more neccessary parts of the project.
But if i instead use a library that automatically detects and uses the appropriate system on the device, it would make building the core process of the project far simpler instead of wasting time building seperate rendering systems for each of these manually.
#### why use SDL3 / what options there are
- raylib vs sdl - https://gist.github.com/raysan5/17392498d40e2cb281f5d09c0a4bf798
- & glfw
(industry standard)
#### SDL_TTF and SDL_Image & SDL_mixer / why i use them
(talk about custom harbuzz / open type fonts setup and hwo it is much more complicated than using such a library that simplifies it.)
(and ffmpeg for image and audio)
#### how i will use sdl3 exactly
(as a system managed package, actual way of integrating it through the single file that blends in with SDL api so it can be switched out in the future.)
### Embedded Scripting
#### why do i need an embedded scripting system
This engine does not use a UI based builder. And so all gameplay logic, entity behaviour, definitions etc. must be implemented directly through code.
Because of this it is important to use a well established language in which the developer can write their code.
Embedded scripting languages are programming lanaguges that are designed to be embedded in a host programming language. The embedded language is often an interpreted language which the host is a compiled static typed language. They allow adding code to the program that can be modified and interpreted at runtime. They are often used to extend on the functionality of the host language. And they also allow using functions defined in the host language which are usually much more performance optimized.
They are usually very lightweight (so that the final binary doesn't get bloated and runs fast enough). They are very useful for cuztom DSL/syntax.
And according to [this aticle](https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html) they are particularly useful for game engines.
(elaborate here on that based on how godot also does so)
We can also see a list [list](https://dbohdan.github.io/embedded-scripting-languages/) of embedded scripting languages that exist, common ones being Lua, javascript variants, squirrel, and ruby variants (mruby in particular).
These all have a C-ABI, meaning they can be used by almost any compiled language.
The author [here](https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html) (finish this sentence)
^ Rordrigues, C. (2023). Embedded Scripting Languages. [online] caiorss.github.io. Available at: https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html [Accessed 13 May 2026].
^ Github. (2022). Embedded scripting languages. [online] Available at: https://dbohdan.github.io/embedded-scripting-languages/ [Accessed 13 May 2026].
#### What options do i have.
(https://caiorss.github.io/C-Cpp-Notes/embedded_scripting_languages.html, and mention commonality of lua etc.)
#### Why use Mruby
Why ruby is great for development for the runtime of this engine: Why ruby is great for development for the runtime of this engine:
- Dynamic typing. it has almost no type declarations on variables, and duck typing means any object with particular behaviour can be treated the same. - Dynamic typing. it has almost no type declarations on variables, and duck typing means any object with particular behaviour can be treated the same.
- Expressive syntax. ruby has one of the most expressive syntax that improves code readability by a lot, it allows operator overloading, very clean dsl structures, minimal puntuation neccessary, and a lot of syntactic shortcuts. - Expressive syntax. ruby has one of the most expressive syntax that improves code readability by a lot, it allows operator overloading, very clean dsl structures, minimal puntuation neccessary, and a lot of syntactic shortcuts.
- Very rich standard library. teh ruby std lib has a lot of helpers for json, sockets, networking, file management, multithreading, regex etc. - Very rich standard library. the ruby std lib has a lot of helpers for json, sockets, networking, file management, multithreading, regex etc.
- It also has a lot of inbuilt datatypes with helper methods that allow doing a lot of stuff. it has integers with no hard maximum, floating point numbers, strings and arrays, but it also has symbols, hashmaps and regex etc. for much faster development. - It also has a lot of inbuilt datatypes with helper methods that allow doing a lot of stuff. it has integers with no hard maximum, floating point numbers, strings and arrays, but it also has symbols, hashmaps and regex etc. for much faster development.
And as its official website itself states, ruby is easy to write, easy to read with natural syntax like spoken language. And as its official website itself states, ruby is easy to write, easy to read with natural syntax like spoken language.
@@ -279,31 +397,37 @@ And as its official website itself states, ruby is easy to write, easy to read w
For these reasons and my personal familiarity with the languge i've planned to use ruby as the languge in which the game engine is written code for. For these reasons and my personal familiarity with the languge i've planned to use ruby as the languge in which the game engine is written code for.
More specifically we will use the `mruby` version as it allows for embedding a lightweight ruby VM into our engine that can run the scripts without needing an external interpreter. More specifically we will use the `mruby` version as it allows for embedding a lightweight ruby VM into our engine that can run the scripts without needing an external interpreter.
`Mruby` is a very lightwieght impleemntation of teh ruby runtime and also supports semi-compiled bytecodes. `Mruby` is a very lightwieght impleemntation of the ruby runtime and also supports semi-compiled bytecodes.
The engine could compile the developers scripts into mruby bytecode when building the application and when it is to be used the teh mruby vm can run these bytecodes. The engine could compile the developers scripts into mruby bytecode when building the application and when it is to be used the the mruby vm can run these bytecodes.
^ Mruby.org. (2026). mruby - Lightweight Ruby. [online] Available at: https://mruby.org/ [Accessed 13 May 2026]. ^ Mruby.org. (2026). mruby - Lightweight Ruby. [online] Available at: https://mruby.org/ [Accessed 13 May 2026].
^ Ruby (2019). Ruby Programming Language. [online] Ruby-lang.org. Available at: https://www.ruby-lang.org/en/ [Accessed 13 May 2026]. ^ Ruby (2019). Ruby Programming Language. [online] Ruby-lang.org. Available at: https://www.ruby-lang.org/en/ [Accessed 13 May 2026].
## #### how i plan on using it along with all precompiling/packing plans i have.
- then a draft one of teh requirements with justification of each based on the previous sections (talk about teh planned packing and server etc. system i have)
Requirements v1:
- A game loop capable of handling any max fps given to it. ## Requirements
- App, defined with fixed heght/width and aspect ratio is maintained with letterboxing, and fps
- A scene system - A scene system
- A scene is a set of scene elements used to create it, - A scene is a set of scene elements used to create it,
- For example, a scene for menu, for game over screen, and most importantly, for the main game scene. - For example, a scene for menu, for game over screen, and most importantly, for the main game scene.
- A scene element can be one of, - A scene element can be one of,
- A rectangle - A rectangle
- A text - A text, with localization codes and text db
- An image - An image
- A game world - A game world
- A pixel scripted element, for stuff like minimaps (called Surface). - A scripted element, for stuff like minimaps
- A sdl command batch element, for something that batches a bunch of image/rect/text into one element. (Called Render)
- each element has coordinates on screen and z ordering. - each element has coordinates on screen and z ordering.
- And they can handle clicks / keyboard events and have scripts attached to each or the scene itself. - 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,
- they can be scoped with lifetime rules for tighter control and avoiding a event mess
- A game world is just the game world (everything is coordinates in the world map relative, and this object controls the camera), \ - 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). and it can be defined as topdown or side (for z ordering rules).
- everything is an entity - everything is an entity
@@ -312,10 +436,66 @@ Requirements v1:
for empty it will automatically place the correct tile from that list.) 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 entity to create maze maps but also just usable for anything. - and support for maze generation and solving algorithms, which can be used with tiling entity to create maze maps but also just usable for anything.
## - Every entity is just a set of components added to it to form something in a game "world"
- An entity joins together components to create an entity
- Components are:
1. Body
- Shape (Dynamic or static)
- 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.
5. Light
- Light level info for fog light shader
- In debug mode the collisions will be visible and other info like coords etc. logged for that game "world"
- Systems are scripts run every game loop on a set of entities (or globally) based on queries.
- then any limitations / problems that can arise with certain choices in that list and how i might tackle them or ignore them - 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.
- then specifics of what ill need to actually make this, like the libaries, languages & hardware ill use with reasons of why, can have a big section on stuff like explaining why i chose sdl3 for rendering, or mruby (mostly because of mruby precompiling and how i can prepack the engine into a single binary) - 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
- shaders, layers on top of the world, (per world, not possible per entity)
- and finally a rigid list of requirements of things it should be doing by the end with 2 priority levels and this time just the list and not any justifications this time - An api for game "world" persistence. and generic data store.
- Normal classes / logic can be written outside and reused in the system.
- 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.
- Extra useful features:
- Localization
- keymapping system (and settings)
- tilesets
- sprite system (tiled, animated, static etc.)
- 9 segment ui system
- audio playing
- network interface, with a lib to stream data rom & a server instance also built into the packer binary (should allow scripting the backend)
- rng helpers
- tweening helpers
- layout system (with tree like grouping of elements that inherit transform of parent)
- video player (ffmpeg texture and audio streams) (requires ffmpeg at runtime)
###
- then any limitations / problems that can arise with the choices in this list and how i might tackle them or ignore them
- then define success criteria for major bits in the requirements as a final list (ie. how do i decide it that point is ticked off)
+12
View File
@@ -0,0 +1,12 @@
# Design
(then section on what kinds of ui should be buildable by the user)
(ecs implementation plan)
(design of how the cli/programming interface works)
- in 2 sections,
- about mruby/json scripting
- and usage of packer
(architectural design and file structure etc. of teh project itself)