feat(kotlin-compose): design system + 33 components + gallery_kt + e2e android emulator + scaffolder fixes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
#pragma once
|
||||
|
||||
// math2d — primitive 2D types used across gamedev stack (issue 0072b).
|
||||
// Pure value types. No allocations, no virtual, trivial copy.
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace fn::math2d {
|
||||
|
||||
struct Vec2 {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
|
||||
constexpr Vec2() = default;
|
||||
constexpr Vec2(float xv, float yv) : x(xv), y(yv) {}
|
||||
|
||||
constexpr Vec2 operator+(Vec2 b) const { return {x + b.x, y + b.y}; }
|
||||
constexpr Vec2 operator-(Vec2 b) const { return {x - b.x, y - b.y}; }
|
||||
constexpr Vec2 operator*(float s) const { return {x * s, y * s}; }
|
||||
constexpr Vec2 operator/(float s) const { return {x / s, y / s}; }
|
||||
constexpr Vec2& operator+=(Vec2 b) { x += b.x; y += b.y; return *this; }
|
||||
constexpr Vec2& operator-=(Vec2 b) { x -= b.x; y -= b.y; return *this; }
|
||||
constexpr Vec2& operator*=(float s) { x *= s; y *= s; return *this; }
|
||||
|
||||
float length() const { return std::sqrt(x * x + y * y); }
|
||||
constexpr float length_sq() const { return x * x + y * y; }
|
||||
Vec2 normalized() const {
|
||||
float l = length();
|
||||
return l > 0.0f ? Vec2{x / l, y / l} : Vec2{0.0f, 0.0f};
|
||||
}
|
||||
static constexpr float dot(Vec2 a, Vec2 b) { return a.x * b.x + a.y * b.y; }
|
||||
static constexpr float cross(Vec2 a, Vec2 b) { return a.x * b.y - a.y * b.x; }
|
||||
};
|
||||
|
||||
struct Rect {
|
||||
float x = 0.0f;
|
||||
float y = 0.0f;
|
||||
float w = 0.0f;
|
||||
float h = 0.0f;
|
||||
|
||||
constexpr Rect() = default;
|
||||
constexpr Rect(float xv, float yv, float wv, float hv) : x(xv), y(yv), w(wv), h(hv) {}
|
||||
|
||||
constexpr float right() const { return x + w; }
|
||||
constexpr float bottom() const { return y + h; }
|
||||
constexpr Vec2 center() const { return {x + w * 0.5f, y + h * 0.5f}; }
|
||||
constexpr Vec2 min() const { return {x, y}; }
|
||||
constexpr Vec2 max() const { return {x + w, y + h}; }
|
||||
|
||||
constexpr bool contains(Vec2 p) const {
|
||||
return p.x >= x && p.x < x + w && p.y >= y && p.y < y + h;
|
||||
}
|
||||
constexpr bool overlaps(Rect b) const {
|
||||
return !(b.x >= x + w || b.x + b.w <= x ||
|
||||
b.y >= y + h || b.y + b.h <= y);
|
||||
}
|
||||
};
|
||||
|
||||
struct Color {
|
||||
float r = 1.0f;
|
||||
float g = 1.0f;
|
||||
float b = 1.0f;
|
||||
float a = 1.0f;
|
||||
|
||||
constexpr Color() = default;
|
||||
constexpr Color(float rv, float gv, float bv, float av = 1.0f)
|
||||
: r(rv), g(gv), b(bv), a(av) {}
|
||||
|
||||
static constexpr Color white() { return {1, 1, 1, 1}; }
|
||||
static constexpr Color black() { return {0, 0, 0, 1}; }
|
||||
static constexpr Color transparent() { return {0, 0, 0, 0}; }
|
||||
|
||||
static Color rgba(unsigned char r8, unsigned char g8, unsigned char b8,
|
||||
unsigned char a8 = 255) {
|
||||
return {r8 / 255.0f, g8 / 255.0f, b8 / 255.0f, a8 / 255.0f};
|
||||
}
|
||||
static Color hex(unsigned int packed) {
|
||||
return Color::rgba((packed >> 24) & 0xFF, (packed >> 16) & 0xFF,
|
||||
(packed >> 8) & 0xFF, packed & 0xFF);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace fn::math2d
|
||||
@@ -0,0 +1,38 @@
|
||||
// UNICA TU del proyecto que define MINIAUDIO_IMPLEMENTATION.
|
||||
#define MINIAUDIO_IMPLEMENTATION
|
||||
#include "../../vendor/miniaudio/miniaudio.h"
|
||||
|
||||
#include "audio_engine.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace fn::audio {
|
||||
|
||||
Engine engine_init() {
|
||||
Engine e{nullptr, false};
|
||||
ma_engine* eng = static_cast<ma_engine*>(std::malloc(sizeof(ma_engine)));
|
||||
if (!eng) return e;
|
||||
if (ma_engine_init(NULL, eng) != MA_SUCCESS) {
|
||||
std::free(eng);
|
||||
return e;
|
||||
}
|
||||
e.impl = eng;
|
||||
e.ok = true;
|
||||
return e;
|
||||
}
|
||||
|
||||
void engine_shutdown(Engine& e) {
|
||||
if (!e.ok || !e.impl) return;
|
||||
ma_engine* eng = static_cast<ma_engine*>(e.impl);
|
||||
ma_engine_uninit(eng);
|
||||
std::free(eng);
|
||||
e.impl = nullptr;
|
||||
e.ok = false;
|
||||
}
|
||||
|
||||
void engine_set_volume(Engine& e, float v) {
|
||||
if (!e.ok || !e.impl) return;
|
||||
ma_engine_set_volume(static_cast<ma_engine*>(e.impl), v);
|
||||
}
|
||||
|
||||
} // namespace fn::audio
|
||||
@@ -0,0 +1,21 @@
|
||||
// audio_engine — lifecycle del engine de audio (miniaudio wrapper).
|
||||
// Issue 0072b — runtime gamedev nucleo (PC desktop + WASM + futuro mobile).
|
||||
#pragma once
|
||||
|
||||
namespace fn::audio {
|
||||
|
||||
struct Engine {
|
||||
void* impl; // ma_engine* opaco
|
||||
bool ok;
|
||||
};
|
||||
|
||||
// Crea engine con device default. Engine.ok=false si falla.
|
||||
Engine engine_init();
|
||||
|
||||
// Libera engine. Idempotente con Engine.ok=false.
|
||||
void engine_shutdown(Engine& e);
|
||||
|
||||
// Master volume 0..1.
|
||||
void engine_set_volume(Engine& e, float v);
|
||||
|
||||
} // namespace fn::audio
|
||||
@@ -0,0 +1,47 @@
|
||||
---
|
||||
name: audio_engine
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gamedev
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "engine_init() -> Engine; engine_shutdown(Engine&); engine_set_volume(Engine&, float)"
|
||||
description: "Lifecycle del engine de audio basado en miniaudio (single-header, public domain). Inicializa device default, expone master volume, y libera recursos. Cross-platform: Windows/Linux/macOS via WASAPI/ALSA/CoreAudio y WebAudio bajo emscripten. Issue 0072b — runtime gamedev nucleo. Esta TU es la unica del proyecto que define MINIAUDIO_IMPLEMENTATION."
|
||||
tags: [gamedev, audio, miniaudio]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
fn::audio::Engine eng = fn::audio::engine_init();
|
||||
if (!eng.ok) { /* fallback silencioso */ }
|
||||
fn::audio::engine_set_volume(eng, 0.8f);
|
||||
// ... loop principal ...
|
||||
fn::audio::engine_shutdown(eng);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gamedev/audio_engine.cpp"
|
||||
params:
|
||||
- name: e
|
||||
desc: "Engine handle. ok=true tras engine_init exitoso. Estructura opaca: impl apunta a ma_engine internamente."
|
||||
- name: v
|
||||
desc: "Volumen master 0..1 (lineal). Valores >1 amplifican (riesgo de clipping)."
|
||||
output: "Engine con impl=ma_engine* y ok=true si MA_SUCCESS, ok=false en cualquier fallo (malloc o init de miniaudio). engine_shutdown deja ok=false e impl=nullptr — llamadas posteriores son no-op seguros."
|
||||
---
|
||||
|
||||
# audio_engine
|
||||
|
||||
Wrapper minimo del engine de [miniaudio](https://miniaud.io/) (v0.11.25, single-header, public domain / MIT-0). Vendored en `cpp/vendor/miniaudio/`.
|
||||
|
||||
## Por que
|
||||
|
||||
Audio cross-platform sin depender de SDL_mixer / OpenAL / FMOD. Compila en Windows/Linux/macOS y WASM (emscripten) desde el mismo source. Sin excepciones, sin RTTI, sin std::string.
|
||||
|
||||
## Notas
|
||||
|
||||
- Estado de fallo se reporta via `Engine.ok=false`. No hay `error_type` porque no devolvemos `error_go_core`-style; el consumidor revisa `ok` antes de operar.
|
||||
- `MINIAUDIO_IMPLEMENTATION` se define UNICAMENTE en `audio_engine.cpp`. Otras TU que usen miniaudio deben hacer solo `#include "miniaudio.h"`.
|
||||
- Memoria via `malloc`/`free` (no `new`) para mantener compat con `-fno-exceptions`.
|
||||
@@ -0,0 +1,59 @@
|
||||
// NO definir MINIAUDIO_IMPLEMENTATION aqui — vive en audio_engine.cpp.
|
||||
#include "../../vendor/miniaudio/miniaudio.h"
|
||||
|
||||
#include "audio_play.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
namespace fn::audio {
|
||||
|
||||
Sound sound_load(Engine& e, const char* path) {
|
||||
Sound s{nullptr, false};
|
||||
if (!e.ok || !e.impl || !path) return s;
|
||||
ma_sound* snd = static_cast<ma_sound*>(std::malloc(sizeof(ma_sound)));
|
||||
if (!snd) return s;
|
||||
ma_engine* eng = static_cast<ma_engine*>(e.impl);
|
||||
if (ma_sound_init_from_file(eng, path, 0, NULL, NULL, snd) != MA_SUCCESS) {
|
||||
std::free(snd);
|
||||
return s;
|
||||
}
|
||||
s.impl = snd;
|
||||
s.ok = true;
|
||||
return s;
|
||||
}
|
||||
|
||||
void sound_play(Sound& s) {
|
||||
if (!s.ok || !s.impl) return;
|
||||
ma_sound_start(static_cast<ma_sound*>(s.impl));
|
||||
}
|
||||
|
||||
void sound_stop(Sound& s) {
|
||||
if (!s.ok || !s.impl) return;
|
||||
ma_sound_stop(static_cast<ma_sound*>(s.impl));
|
||||
}
|
||||
|
||||
void sound_set_volume(Sound& s, float v) {
|
||||
if (!s.ok || !s.impl) return;
|
||||
ma_sound_set_volume(static_cast<ma_sound*>(s.impl), v);
|
||||
}
|
||||
|
||||
void sound_destroy(Sound& s) {
|
||||
if (!s.ok || !s.impl) return;
|
||||
ma_sound* snd = static_cast<ma_sound*>(s.impl);
|
||||
ma_sound_uninit(snd);
|
||||
std::free(snd);
|
||||
s.impl = nullptr;
|
||||
s.ok = false;
|
||||
}
|
||||
|
||||
void play_sound_oneshot(Engine& e, const char* path, float volume) {
|
||||
if (!e.ok || !e.impl || !path) return;
|
||||
ma_engine* eng = static_cast<ma_engine*>(e.impl);
|
||||
// ma_engine_play_sound respeta el master volume; volume per-call se aplica
|
||||
// creando un sonido ad-hoc si el caller quiere control fino. Para fire-and-forget
|
||||
// usamos el helper directo y dejamos el master modular.
|
||||
(void)volume;
|
||||
ma_engine_play_sound(eng, path, NULL);
|
||||
}
|
||||
|
||||
} // namespace fn::audio
|
||||
@@ -0,0 +1,32 @@
|
||||
// audio_play — reproducir sonidos one-shot y streaming sobre fn::audio::Engine.
|
||||
// Issue 0072b — runtime gamedev nucleo.
|
||||
#pragma once
|
||||
|
||||
#include "audio_engine.h"
|
||||
|
||||
namespace fn::audio {
|
||||
|
||||
struct Sound {
|
||||
void* impl; // ma_sound* opaco
|
||||
bool ok;
|
||||
};
|
||||
|
||||
// Carga y prepara un sonido (decodifica streaming desde disco). Sound.ok=false si falla.
|
||||
Sound sound_load(Engine& e, const char* path);
|
||||
|
||||
// Arranca/reanuda reproduccion.
|
||||
void sound_play(Sound& s);
|
||||
|
||||
// Detiene reproduccion (no libera).
|
||||
void sound_stop(Sound& s);
|
||||
|
||||
// Volumen 0..1 del sonido (independiente del master).
|
||||
void sound_set_volume(Sound& s, float v);
|
||||
|
||||
// Libera recursos del sonido.
|
||||
void sound_destroy(Sound& s);
|
||||
|
||||
// Fire-and-forget: reproduce path una vez sin handle. No-op si engine no esta listo.
|
||||
void play_sound_oneshot(Engine& e, const char* path, float volume = 1.0f);
|
||||
|
||||
} // namespace fn::audio
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
name: audio_play
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gamedev
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "sound_load(Engine&, const char*) -> Sound; sound_play/stop/set_volume/destroy(Sound&); play_sound_oneshot(Engine&, const char*, float)"
|
||||
description: "Reproduccion de audio sobre fn::audio::Engine: carga sonidos con streaming desde disco (wav/mp3/flac/ogg), play/stop/volumen por sonido, y helper fire-and-forget para one-shots sin handle. Cross-platform via miniaudio. Issue 0072b — runtime gamedev nucleo."
|
||||
tags: [gamedev, audio, miniaudio]
|
||||
uses_functions: ["audio_engine_cpp_gamedev"]
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
fn::audio::Engine eng = fn::audio::engine_init();
|
||||
fn::audio::Sound bgm = fn::audio::sound_load(eng, "assets/bgm.ogg");
|
||||
fn::audio::sound_set_volume(bgm, 0.5f);
|
||||
fn::audio::sound_play(bgm);
|
||||
// SFX one-shot:
|
||||
fn::audio::play_sound_oneshot(eng, "assets/jump.wav");
|
||||
// ...
|
||||
fn::audio::sound_destroy(bgm);
|
||||
fn::audio::engine_shutdown(eng);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gamedev/audio_play.cpp"
|
||||
params:
|
||||
- name: e
|
||||
desc: "Engine inicializado por audio_engine_cpp_gamedev. Si e.ok=false, todas las operaciones son no-op."
|
||||
- name: path
|
||||
desc: "Ruta al archivo de audio (relativa al cwd o absoluta). Formatos: wav/mp3/flac/ogg via decoders integrados de miniaudio."
|
||||
- name: s
|
||||
desc: "Sound handle. ok=true tras sound_load exitoso. Operaciones tras destroy son no-op."
|
||||
- name: v
|
||||
desc: "Volumen 0..1 lineal, multiplicativo con el master del Engine."
|
||||
- name: volume
|
||||
desc: "Volumen sugerido para play_sound_oneshot (actualmente delega al master via ma_engine_play_sound; reservado para futura implementacion per-instance)."
|
||||
output: "Sound con impl=ma_sound* y ok=true en sound_load exitoso; ok=false ante cualquier fallo (engine no listo, malloc, decoder). play_sound_oneshot no devuelve handle — el sonido se gestiona internamente por el engine."
|
||||
---
|
||||
|
||||
# audio_play
|
||||
|
||||
Reproduccion de audio one-shot y streaming sobre `fn::audio::Engine`. Wrapper minimo de la API `ma_sound` de miniaudio.
|
||||
|
||||
## Estructura `Sound`
|
||||
|
||||
Handle opaco con `impl` (apunta a `ma_sound`) y `ok` (bool). Gestion explicita: cada `sound_load` requiere `sound_destroy` para liberar (no hay RAII porque mantenemos `-fno-exceptions` y compat con structs trivial).
|
||||
|
||||
## Patrones de uso
|
||||
|
||||
- **BGM / loops largos:** `sound_load` + `sound_play`. Por defecto miniaudio hace streaming desde disco (no carga todo a memoria).
|
||||
- **SFX cortos:** `play_sound_oneshot` — fire-and-forget, sin handle, ideal para clicks, jumps, hits.
|
||||
- **SFX repetidos con control:** `sound_load` + `sound_play` cada vez. Para concurrent voices del mismo sample, considerar `ma_sound_init_copy` (no expuesto aun).
|
||||
|
||||
## Notas
|
||||
|
||||
- `play_sound_oneshot` recibe `volume` como hint pero actualmente delega al master del engine. Iterar si el caller real lo necesita.
|
||||
- `sound_load` con `path=NULL` o engine no listo devuelve `Sound{nullptr, false}` — siempre comprobar `ok` antes de operar.
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "camera_2d.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace fn::cam {
|
||||
|
||||
using fn::math2d::Vec2;
|
||||
using fn::math2d::Rect;
|
||||
|
||||
Vec2 world_to_screen(const Camera2D& c, Vec2 world) {
|
||||
float dx = world.x - c.pos.x;
|
||||
float dy = world.y - c.pos.y;
|
||||
|
||||
if (c.rotation != 0.0f) {
|
||||
float cs = std::cos(-c.rotation);
|
||||
float sn = std::sin(-c.rotation);
|
||||
float rx = dx * cs - dy * sn;
|
||||
float ry = dx * sn + dy * cs;
|
||||
dx = rx;
|
||||
dy = ry;
|
||||
}
|
||||
|
||||
float sx = dx * c.zoom + (float)c.viewport_w * 0.5f;
|
||||
float sy = dy * c.zoom + (float)c.viewport_h * 0.5f;
|
||||
return {sx, sy};
|
||||
}
|
||||
|
||||
Vec2 screen_to_world(const Camera2D& c, Vec2 screen) {
|
||||
float dx = (screen.x - (float)c.viewport_w * 0.5f) / c.zoom;
|
||||
float dy = (screen.y - (float)c.viewport_h * 0.5f) / c.zoom;
|
||||
|
||||
if (c.rotation != 0.0f) {
|
||||
float cs = std::cos(c.rotation);
|
||||
float sn = std::sin(c.rotation);
|
||||
float rx = dx * cs - dy * sn;
|
||||
float ry = dx * sn + dy * cs;
|
||||
dx = rx;
|
||||
dy = ry;
|
||||
}
|
||||
|
||||
return {c.pos.x + dx, c.pos.y + dy};
|
||||
}
|
||||
|
||||
Rect visible_world_rect(const Camera2D& c) {
|
||||
// For rotated cameras, return the AABB that contains the rotated viewport.
|
||||
float hw = (float)c.viewport_w * 0.5f / c.zoom;
|
||||
float hh = (float)c.viewport_h * 0.5f / c.zoom;
|
||||
|
||||
if (c.rotation == 0.0f) {
|
||||
return {c.pos.x - hw, c.pos.y - hh, hw * 2.0f, hh * 2.0f};
|
||||
}
|
||||
|
||||
float cs = std::fabs(std::cos(c.rotation));
|
||||
float sn = std::fabs(std::sin(c.rotation));
|
||||
float ehw = hw * cs + hh * sn;
|
||||
float ehh = hw * sn + hh * cs;
|
||||
return {c.pos.x - ehw, c.pos.y - ehh, ehw * 2.0f, ehh * 2.0f};
|
||||
}
|
||||
|
||||
void view_proj_matrix(const Camera2D& c, float out[16]) {
|
||||
// Orthographic projection mapping a viewport-sized box around camera pos
|
||||
// to clip space [-1, 1].
|
||||
float hw = (float)c.viewport_w * 0.5f / c.zoom;
|
||||
float hh = (float)c.viewport_h * 0.5f / c.zoom;
|
||||
|
||||
float l = c.pos.x - hw;
|
||||
float r = c.pos.x + hw;
|
||||
// Screen Y goes down, world Y can go either; we pick world-Y-up convention:
|
||||
// top of screen = pos.y + hh, bottom = pos.y - hh.
|
||||
float b = c.pos.y - hh;
|
||||
float t = c.pos.y + hh;
|
||||
|
||||
float cs = std::cos(-c.rotation);
|
||||
float sn = std::sin(-c.rotation);
|
||||
|
||||
// Build column-major:
|
||||
// M = Ortho(l,r,b,t) * Rotate(-rotation around camera center)
|
||||
// Compose by hand to avoid temporaries.
|
||||
|
||||
float ortho[16] = {
|
||||
2.0f / (r - l), 0.0f, 0.0f, 0.0f,
|
||||
0.0f, 2.0f / (t - b), 0.0f, 0.0f,
|
||||
0.0f, 0.0f, -1.0f, 0.0f,
|
||||
-(r + l) / (r - l), -(t + b) / (t - b), 0.0f, 1.0f,
|
||||
};
|
||||
|
||||
if (c.rotation == 0.0f) {
|
||||
for (int i = 0; i < 16; ++i) out[i] = ortho[i];
|
||||
return;
|
||||
}
|
||||
|
||||
// Rotation around camera pos in world space:
|
||||
// T(pos) * R * T(-pos)
|
||||
// We bake it as a column-major 4x4 then multiply: out = ortho * rot.
|
||||
float px = c.pos.x;
|
||||
float py = c.pos.y;
|
||||
|
||||
float rot[16] = {
|
||||
cs, sn, 0.0f, 0.0f,
|
||||
-sn, cs, 0.0f, 0.0f,
|
||||
0.0f, 0.0f, 1.0f, 0.0f,
|
||||
px - cs * px + sn * py,
|
||||
py - sn * px - cs * py,
|
||||
0.0f, 1.0f,
|
||||
};
|
||||
|
||||
// out = ortho * rot (column-major).
|
||||
for (int col = 0; col < 4; ++col) {
|
||||
for (int row = 0; row < 4; ++row) {
|
||||
float sum = 0.0f;
|
||||
for (int k = 0; k < 4; ++k) {
|
||||
sum += ortho[k * 4 + row] * rot[col * 4 + k];
|
||||
}
|
||||
out[col * 4 + row] = sum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fn::cam
|
||||
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
// camera_2d — pure orthographic 2D camera (issue 0072b).
|
||||
// No state, no I/O. World <-> screen transforms + ortho view-projection matrix.
|
||||
|
||||
#include "../core/math2d.h"
|
||||
|
||||
namespace fn::cam {
|
||||
|
||||
struct Camera2D {
|
||||
fn::math2d::Vec2 pos = {0.0f, 0.0f}; // world position of camera center
|
||||
float zoom = 1.0f; // 1 = no zoom; >1 zoom in
|
||||
float rotation = 0.0f; // radians
|
||||
int viewport_w = 1280;
|
||||
int viewport_h = 720;
|
||||
};
|
||||
|
||||
fn::math2d::Vec2 world_to_screen(const Camera2D& c, fn::math2d::Vec2 world);
|
||||
fn::math2d::Vec2 screen_to_world(const Camera2D& c, fn::math2d::Vec2 screen);
|
||||
fn::math2d::Rect visible_world_rect(const Camera2D& c);
|
||||
|
||||
// Column-major 4x4 view-projection matrix (orthographic).
|
||||
void view_proj_matrix(const Camera2D& c, float out[16]);
|
||||
|
||||
} // namespace fn::cam
|
||||
@@ -0,0 +1,54 @@
|
||||
---
|
||||
name: camera_2d
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gamedev
|
||||
version: "0.1.0"
|
||||
purity: pure
|
||||
signature: "world_to_screen(Camera2D, Vec2) -> Vec2; screen_to_world(Camera2D, Vec2) -> Vec2; visible_world_rect(Camera2D) -> Rect; view_proj_matrix(Camera2D, float[16])"
|
||||
description: "Camara ortografica 2D pura: pos (centro), zoom, rotacion (rad) y viewport en pixeles. Conversiones world<->screen, AABB visible y matriz view-projection 4x4 column-major lista para cualquier renderer (sokol_gfx, OpenGL, WebGPU). Fast-path sin trig si rotation==0. Issue 0072b."
|
||||
tags: [gamedev, camera, 2d, math, pure]
|
||||
uses_functions: []
|
||||
uses_types: ["Vec2_cpp_core", "Rect_cpp_core"]
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
example: |
|
||||
fn::cam::Camera2D cam;
|
||||
cam.pos = {100, 50};
|
||||
cam.zoom = 2.0f;
|
||||
cam.viewport_w = 1280;
|
||||
cam.viewport_h = 720;
|
||||
// mouse picking:
|
||||
fn::math2d::Vec2 world = fn::cam::screen_to_world(cam, {input.mx, input.my});
|
||||
// upload to shader:
|
||||
float mvp[16];
|
||||
fn::cam::view_proj_matrix(cam, mvp);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gamedev/camera_2d.cpp"
|
||||
params:
|
||||
- name: camera
|
||||
desc: "Camera2D con pos (centro mundo), zoom (>1 acerca), rotation (radianes) y viewport_w/h (pixeles)."
|
||||
- name: world
|
||||
desc: "Punto en world space (Vec2) para world_to_screen."
|
||||
- name: screen
|
||||
desc: "Punto en screen/pixel space (Vec2) para screen_to_world."
|
||||
- name: out
|
||||
desc: "Buffer de 16 floats donde escribir la matriz column-major en view_proj_matrix."
|
||||
output: "Vec2 (transformaciones), Rect (AABB visible) o matriz column-major 4x4 (mapa world->clip [-1,1])."
|
||||
---
|
||||
|
||||
# camera_2d
|
||||
|
||||
Camara 2D minima y pura. Zero estado global, zero I/O — apta para reusar en N renderers (sokol_gfx, OpenGL, WebGPU) y para correr en tests headless.
|
||||
|
||||
Convencion:
|
||||
- `pos` es el centro de la camara en world coords.
|
||||
- Eje Y en world apunta hacia ARRIBA. La proyeccion mapea `pos.y + hh` a top de clip (y=+1).
|
||||
- Zoom multiplicativo: `zoom=2` muestra la mitad del area mundial en el mismo viewport.
|
||||
- Rotation en radianes, sentido antihorario en world (la matriz invierte para clip).
|
||||
- `visible_world_rect` para rotation != 0 devuelve el AABB ENVOLVENTE (no el rect rotado), util para frustum culling barato.
|
||||
- `view_proj_matrix` es column-major (compatible con sokol_gfx / OpenGL `glUniformMatrix4fv` con `transpose=GL_FALSE`).
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "game_loop.h"
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
#include <emscripten/emscripten.h>
|
||||
#endif
|
||||
|
||||
namespace fn::game {
|
||||
|
||||
namespace {
|
||||
|
||||
struct LoopRT {
|
||||
LoopCfg cfg;
|
||||
Uint64 last_ticks;
|
||||
float accumulator;
|
||||
Uint64 perf_freq;
|
||||
};
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
static LoopRT g_rt;
|
||||
|
||||
void em_tick() {
|
||||
LoopRT& rt = g_rt;
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
float frame_time = (float)((double)(now - rt.last_ticks) / (double)rt.perf_freq);
|
||||
rt.last_ticks = now;
|
||||
|
||||
float cap = rt.cfg.fixed_dt * (float)rt.cfg.max_steps_per_frame;
|
||||
if (frame_time > cap) frame_time = cap;
|
||||
|
||||
rt.accumulator += frame_time;
|
||||
int steps = 0;
|
||||
while (rt.accumulator >= rt.cfg.fixed_dt && steps < rt.cfg.max_steps_per_frame) {
|
||||
if (rt.cfg.on_fixed_update) rt.cfg.on_fixed_update(rt.cfg.user, rt.cfg.fixed_dt);
|
||||
rt.accumulator -= rt.cfg.fixed_dt;
|
||||
steps++;
|
||||
}
|
||||
|
||||
float interp = rt.accumulator / rt.cfg.fixed_dt;
|
||||
if (rt.cfg.on_render) rt.cfg.on_render(rt.cfg.user, interp);
|
||||
|
||||
if (rt.cfg.should_quit && rt.cfg.should_quit(rt.cfg.user)) {
|
||||
emscripten_cancel_main_loop();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
void loop_run(SDL_Window* /*win*/, const LoopCfg& cfg) {
|
||||
if (!cfg.on_fixed_update && !cfg.on_render) return;
|
||||
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
g_rt.cfg = cfg;
|
||||
g_rt.last_ticks = SDL_GetPerformanceCounter();
|
||||
g_rt.accumulator = 0.0f;
|
||||
g_rt.perf_freq = SDL_GetPerformanceFrequency();
|
||||
emscripten_set_main_loop(em_tick, 0, 1);
|
||||
#else
|
||||
Uint64 perf_freq = SDL_GetPerformanceFrequency();
|
||||
Uint64 last = SDL_GetPerformanceCounter();
|
||||
float accumulator = 0.0f;
|
||||
|
||||
for (;;) {
|
||||
if (cfg.should_quit && cfg.should_quit(cfg.user)) break;
|
||||
|
||||
Uint64 now = SDL_GetPerformanceCounter();
|
||||
float frame_time = (float)((double)(now - last) / (double)perf_freq);
|
||||
last = now;
|
||||
|
||||
float cap = cfg.fixed_dt * (float)cfg.max_steps_per_frame;
|
||||
if (frame_time > cap) frame_time = cap;
|
||||
|
||||
accumulator += frame_time;
|
||||
int steps = 0;
|
||||
while (accumulator >= cfg.fixed_dt && steps < cfg.max_steps_per_frame) {
|
||||
if (cfg.on_fixed_update) cfg.on_fixed_update(cfg.user, cfg.fixed_dt);
|
||||
accumulator -= cfg.fixed_dt;
|
||||
steps++;
|
||||
}
|
||||
|
||||
float interp = accumulator / cfg.fixed_dt;
|
||||
if (cfg.on_render) cfg.on_render(cfg.user, interp);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace fn::game
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
// game_loop — fixed-timestep game loop (Glenn Fiedler) for SDL3 + WASM.
|
||||
// Issue 0072b. Decouples physics/sim (fixed dt) from rendering (interp).
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace fn::game {
|
||||
|
||||
struct LoopCfg {
|
||||
float fixed_dt = 1.0f / 60.0f;
|
||||
int max_steps_per_frame = 5;
|
||||
void (*on_fixed_update)(void* user, float dt) = nullptr;
|
||||
void (*on_render)(void* user, float interp) = nullptr;
|
||||
bool (*should_quit)(void* user) = nullptr; // optional poll
|
||||
void* user = nullptr;
|
||||
};
|
||||
|
||||
// Blocking on desktop. On WASM uses emscripten_set_main_loop and returns immediately.
|
||||
void loop_run(SDL_Window* win, const LoopCfg& cfg);
|
||||
|
||||
} // namespace fn::game
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
name: game_loop
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gamedev
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "loop_run(SDL_Window*, const LoopCfg&) -> void"
|
||||
description: "Game loop fixed-timestep estilo Glenn Fiedler ('Fix Your Timestep'). Desacopla simulacion (on_fixed_update con dt fijo) de renderizado (on_render con factor de interpolacion). Acumulador con cap anti spiral-of-death. Branch automatico desktop (while loop bloqueante) vs __EMSCRIPTEN__ (emscripten_set_main_loop). Issue 0072b."
|
||||
tags: [gamedev, game-loop, sdl3, wasm, fixed-timestep]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
struct State { bool quit = false; float t = 0; };
|
||||
State st;
|
||||
fn::game::LoopCfg cfg;
|
||||
cfg.user = &st;
|
||||
cfg.on_fixed_update = [](void* u, float dt) {
|
||||
auto s = (State*)u;
|
||||
s->t += dt;
|
||||
};
|
||||
cfg.on_render = [](void* u, float interp) {
|
||||
// render with interp factor between [0, 1)
|
||||
};
|
||||
cfg.should_quit = [](void* u) { return ((State*)u)->quit; };
|
||||
fn::game::loop_run(window, cfg);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gamedev/game_loop.cpp"
|
||||
params:
|
||||
- name: window
|
||||
desc: "SDL_Window activo (no usado actualmente; reservado para futuras integraciones swap/vsync)."
|
||||
- name: cfg
|
||||
desc: "LoopCfg con fixed_dt (default 1/60), max_steps_per_frame (cap), callbacks on_fixed_update/on_render/should_quit y user pointer."
|
||||
output: "Bloquea hasta should_quit==true (desktop). En WASM retorna inmediatamente y registra emscripten_set_main_loop."
|
||||
---
|
||||
|
||||
# game_loop
|
||||
|
||||
Loop canonico para apps gamedev del registry. Garantiza que la simulacion corra a `fixed_dt` constante (default 60 Hz) independientemente del framerate de render, y expone factor `interp` para que el renderer interpole posiciones entre estados de fisica.
|
||||
|
||||
Detalles:
|
||||
- `frame_time` se cap a `fixed_dt * max_steps_per_frame` para evitar la espiral de la muerte cuando el debugger pausa.
|
||||
- En `__EMSCRIPTEN__` el estado del acumulador vive en variable static (`g_rt`) — solo un loop activo por modulo WASM.
|
||||
- `should_quit` se consulta antes de cada frame; en WASM dispara `emscripten_cancel_main_loop`.
|
||||
- `loop_run` retorna sin hacer nada si ambos callbacks son nulos.
|
||||
@@ -0,0 +1,149 @@
|
||||
#include "input_unified.h"
|
||||
|
||||
namespace fn::input {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr float STICK_DEADZONE = 0.15f;
|
||||
constexpr float STICK_MAX = 32767.0f;
|
||||
|
||||
float normalize_axis(int16_t v) {
|
||||
float f = (float)v / STICK_MAX;
|
||||
if (f > 1.0f) f = 1.0f;
|
||||
if (f < -1.0f) f = -1.0f;
|
||||
if (f > -STICK_DEADZONE && f < STICK_DEADZONE) return 0.0f;
|
||||
return f;
|
||||
}
|
||||
|
||||
// Set logical button + rising-edge flag if transitioning false->true.
|
||||
void set_btn(bool& held, bool& pressed, bool down) {
|
||||
if (down && !held) pressed = true;
|
||||
held = down;
|
||||
}
|
||||
|
||||
void apply_key(InputState& s, SDL_Keycode key, bool down) {
|
||||
switch (key) {
|
||||
case SDLK_A: case SDLK_LEFT: set_btn(s.left, s.left_pressed, down); break;
|
||||
case SDLK_D: case SDLK_RIGHT: set_btn(s.right, s.right_pressed, down); break;
|
||||
case SDLK_W: case SDLK_UP: set_btn(s.up, s.up_pressed, down); break;
|
||||
case SDLK_S: case SDLK_DOWN: set_btn(s.down, s.down_pressed, down); break;
|
||||
case SDLK_SPACE: set_btn(s.action_a, s.a_pressed, down); break;
|
||||
case SDLK_RETURN: set_btn(s.start, s.start_pressed, down); break;
|
||||
case SDLK_ESCAPE: set_btn(s.back, s.back_pressed, down); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
void apply_gpad_button(InputState& s, Uint8 button, bool down) {
|
||||
switch (button) {
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_LEFT: set_btn(s.left, s.left_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_RIGHT: set_btn(s.right, s.right_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_UP: set_btn(s.up, s.up_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_DPAD_DOWN: set_btn(s.down, s.down_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_SOUTH: set_btn(s.action_a, s.a_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_EAST: set_btn(s.action_b, s.b_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_WEST: set_btn(s.action_x, s.x_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_NORTH: set_btn(s.action_y, s.y_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_START: set_btn(s.start, s.start_pressed, down); break;
|
||||
case SDL_GAMEPAD_BUTTON_BACK: set_btn(s.back, s.back_pressed, down); break;
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
int find_touch_slot(InputState& s, int id) {
|
||||
for (int i = 0; i < s.touch_count; ++i) {
|
||||
if (s.touches[i].id == id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void input_begin_frame(InputState& s) {
|
||||
s.left_pressed = s.right_pressed = s.up_pressed = s.down_pressed = false;
|
||||
s.a_pressed = s.b_pressed = s.x_pressed = s.y_pressed = false;
|
||||
s.start_pressed = s.back_pressed = false;
|
||||
s.m_left_pressed = s.m_right_pressed = false;
|
||||
}
|
||||
|
||||
void input_process_event(InputState& s, const SDL_Event* e) {
|
||||
if (!e) return;
|
||||
|
||||
switch (e->type) {
|
||||
case SDL_EVENT_KEY_DOWN:
|
||||
if (!e->key.repeat) apply_key(s, e->key.key, true);
|
||||
break;
|
||||
case SDL_EVENT_KEY_UP:
|
||||
apply_key(s, e->key.key, false);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
s.mx = e->motion.x;
|
||||
s.my = e->motion.y;
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if (e->button.button == SDL_BUTTON_LEFT) set_btn(s.m_left, s.m_left_pressed, true);
|
||||
if (e->button.button == SDL_BUTTON_RIGHT) set_btn(s.m_right, s.m_right_pressed, true);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if (e->button.button == SDL_BUTTON_LEFT) set_btn(s.m_left, s.m_left_pressed, false);
|
||||
if (e->button.button == SDL_BUTTON_RIGHT) set_btn(s.m_right, s.m_right_pressed, false);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_DOWN:
|
||||
apply_gpad_button(s, e->gbutton.button, true);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_BUTTON_UP:
|
||||
apply_gpad_button(s, e->gbutton.button, false);
|
||||
break;
|
||||
case SDL_EVENT_GAMEPAD_AXIS_MOTION: {
|
||||
float v = normalize_axis(e->gaxis.value);
|
||||
switch (e->gaxis.axis) {
|
||||
case SDL_GAMEPAD_AXIS_LEFTX: s.lx = v; break;
|
||||
case SDL_GAMEPAD_AXIS_LEFTY: s.ly = v; break;
|
||||
case SDL_GAMEPAD_AXIS_RIGHTX: s.rx = v; break;
|
||||
case SDL_GAMEPAD_AXIS_RIGHTY: s.ry = v; break;
|
||||
default: break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_GAMEPAD_ADDED:
|
||||
SDL_OpenGamepad(e->gdevice.which);
|
||||
break;
|
||||
|
||||
case SDL_EVENT_FINGER_DOWN: {
|
||||
if (s.touch_count < 8) {
|
||||
auto& t = s.touches[s.touch_count++];
|
||||
t.id = (int)e->tfinger.fingerID;
|
||||
t.x = e->tfinger.x;
|
||||
t.y = e->tfinger.y;
|
||||
t.pressed = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_FINGER_MOTION: {
|
||||
int idx = find_touch_slot(s, (int)e->tfinger.fingerID);
|
||||
if (idx >= 0) {
|
||||
s.touches[idx].x = e->tfinger.x;
|
||||
s.touches[idx].y = e->tfinger.y;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case SDL_EVENT_FINGER_UP: {
|
||||
int idx = find_touch_slot(s, (int)e->tfinger.fingerID);
|
||||
if (idx >= 0) {
|
||||
// compact array
|
||||
for (int i = idx; i < s.touch_count - 1; ++i) {
|
||||
s.touches[i] = s.touches[i + 1];
|
||||
}
|
||||
s.touch_count--;
|
||||
s.touches[s.touch_count] = {};
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace fn::input
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
// input_unified — frame-based unified input snapshot for SDL3.
|
||||
// Issue 0072b. Keyboard + mouse + gamepad + touch mapped to logical buttons.
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
namespace fn::input {
|
||||
|
||||
struct InputState {
|
||||
// Logical buttons (any source mapped here).
|
||||
bool left = false, right = false, up = false, down = false;
|
||||
bool action_a = false, action_b = false, action_x = false, action_y = false;
|
||||
bool start = false, back = false;
|
||||
|
||||
// Same buttons but "just pressed this frame" (rising edge).
|
||||
bool left_pressed = false, right_pressed = false, up_pressed = false, down_pressed = false;
|
||||
bool a_pressed = false, b_pressed = false, x_pressed = false, y_pressed = false;
|
||||
bool start_pressed = false, back_pressed = false;
|
||||
|
||||
// Analog sticks [-1, 1].
|
||||
float lx = 0.0f, ly = 0.0f, rx = 0.0f, ry = 0.0f;
|
||||
|
||||
// Mouse (window coords, pixels).
|
||||
float mx = 0.0f, my = 0.0f;
|
||||
bool m_left = false, m_right = false;
|
||||
bool m_left_pressed = false, m_right_pressed = false;
|
||||
|
||||
// Touch (mobile / WASM). x/y normalized [0, 1].
|
||||
struct Touch {
|
||||
float x = 0.0f, y = 0.0f;
|
||||
bool pressed = false;
|
||||
int id = -1;
|
||||
};
|
||||
Touch touches[8];
|
||||
int touch_count = 0;
|
||||
};
|
||||
|
||||
// Call once per frame BEFORE processing SDL events. Clears *_pressed edges.
|
||||
void input_begin_frame(InputState& s);
|
||||
|
||||
// Call for each SDL_Event in PollEvent loop.
|
||||
void input_process_event(InputState& s, const SDL_Event* e);
|
||||
|
||||
} // namespace fn::input
|
||||
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: input_unified
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gamedev
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "input_begin_frame(InputState&); input_process_event(InputState&, const SDL_Event*)"
|
||||
description: "Snapshot unificado de input por frame para SDL3. Mapea keyboard (WASD+arrows), mouse, gamepad (SDL_Gamepad) y touch a botones logicos (left/right/up/down/action_a..y/start/back) y ejes analogicos. Expone flags *_pressed con rising edge limpio cada frame. Issue 0072b — runtime gamedev PC + WASM."
|
||||
tags: [gamedev, input, sdl3, touch, gamepad]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
fn::input::InputState input{};
|
||||
// per frame:
|
||||
fn::input::input_begin_frame(input);
|
||||
SDL_Event e;
|
||||
while (SDL_PollEvent(&e)) {
|
||||
fn::input::input_process_event(input, &e);
|
||||
}
|
||||
if (input.a_pressed) jump();
|
||||
player.x += input.lx * speed * dt;
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gamedev/input_unified.cpp"
|
||||
params:
|
||||
- name: state
|
||||
desc: "InputState mantenido por la app entre frames (bools held + analog axes + touch slots)."
|
||||
- name: event
|
||||
desc: "Puntero al SDL_Event devuelto por SDL_PollEvent en el loop principal."
|
||||
output: "Mutaciones in-place sobre InputState. Sin retorno. Sin asignaciones dinamicas."
|
||||
---
|
||||
|
||||
# input_unified
|
||||
|
||||
Capa fina sobre SDL3 que normaliza todas las fuentes de input a un struct plano consultable cada frame. Sirve como fundacion para apps gamedev del registry (issue 0072b) y para tests headless.
|
||||
|
||||
Reglas de uso:
|
||||
- `input_begin_frame` se llama UNA vez por frame antes de bombear eventos. Limpia los flags `*_pressed` (rising edge), no los `*` held.
|
||||
- `input_process_event` se llama por cada `SDL_Event` recibido. Acumula state hasta que el siguiente `begin_frame` resetee edges.
|
||||
- Stick deadzone fijo a 0.15 — si la app necesita custom, reescribir snapshot tras la lectura.
|
||||
- Touch ids estables mientras el dedo este pressed; al soltar se compacta el array.
|
||||
- Gamepads conectados se abren automaticamente en `SDL_EVENT_GAMEPAD_ADDED`.
|
||||
@@ -0,0 +1,24 @@
|
||||
#include "sokol_setup.h"
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
sg_environment make_environment() {
|
||||
sg_environment env{};
|
||||
env.defaults.color_format = SG_PIXELFORMAT_RGBA8;
|
||||
env.defaults.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL;
|
||||
env.defaults.sample_count = 1;
|
||||
return env;
|
||||
}
|
||||
|
||||
sg_swapchain make_swapchain(int width, int height) {
|
||||
sg_swapchain sw{};
|
||||
sw.width = width;
|
||||
sw.height = height;
|
||||
sw.sample_count = 1;
|
||||
sw.color_format = SG_PIXELFORMAT_RGBA8;
|
||||
sw.depth_format = SG_PIXELFORMAT_DEPTH_STENCIL;
|
||||
sw.gl.framebuffer = 0; // default framebuffer of current GL context
|
||||
return sw;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
// sokol_setup — helpers para inicializar sokol_gfx sobre un GL context
|
||||
// creado por SDL3 (no por sokol_app). Issue 0072b.
|
||||
//
|
||||
// Uso tipico:
|
||||
// SDL_Window* w = SDL_CreateWindow(...);
|
||||
// SDL_GLContext gl = SDL_GL_CreateContext(w);
|
||||
// sg_setup({ .environment = fn::gfx::make_environment(),
|
||||
// .logger.func = slog_func });
|
||||
// ...
|
||||
// sg_pass p{}; p.swapchain = fn::gfx::make_swapchain(width, height);
|
||||
// sg_begin_pass(&p);
|
||||
|
||||
#include "sokol_gfx.h"
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
// Default environment for an SDL3-managed GL context.
|
||||
// color RGBA8, depth+stencil, no MSAA. Override fields as needed.
|
||||
sg_environment make_environment();
|
||||
|
||||
// Build a default swapchain for the current SDL3 window framebuffer.
|
||||
// Pass current drawable dimensions in pixels.
|
||||
sg_swapchain make_swapchain(int width, int height);
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,43 @@
|
||||
---
|
||||
name: sokol_setup
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "0.1.0"
|
||||
purity: pure
|
||||
signature: "make_environment() -> sg_environment; make_swapchain(int w, int h) -> sg_swapchain"
|
||||
description: "Builders puros para inicializar sokol_gfx encima de un GL context creado por SDL3 (no por sokol_app). Construye sg_environment con defaults RGBA8 + depth/stencil y sg_swapchain con el default framebuffer del contexto activo. Issue 0072b — base del runtime gamedev en PC + WASM."
|
||||
tags: [gamedev, sokol, gfx, sdl3, wasm]
|
||||
uses_functions: []
|
||||
uses_types: []
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: ""
|
||||
imports: []
|
||||
example: |
|
||||
// tras SDL_GL_CreateContext():
|
||||
sg_desc d{};
|
||||
d.environment = fn::gfx::make_environment();
|
||||
d.logger.func = slog_func;
|
||||
sg_setup(&d);
|
||||
// por frame:
|
||||
sg_pass p{};
|
||||
p.swapchain = fn::gfx::make_swapchain(w, h);
|
||||
sg_begin_pass(&p);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gfx/sokol_setup.cpp"
|
||||
params:
|
||||
- name: width
|
||||
desc: "Ancho del framebuffer en pixeles. Usar SDL_GetWindowSizeInPixels."
|
||||
- name: height
|
||||
desc: "Alto del framebuffer en pixeles."
|
||||
output: "Estructuras sg_environment / sg_swapchain listas para sokol_gfx."
|
||||
---
|
||||
|
||||
# sokol_setup
|
||||
|
||||
Helpers minimos para usar `sokol_gfx` con SDL3 sin depender de `sokol_glue.h` (que importa simbolos de `sokol_app` y rompe en stacks SDL3-driven).
|
||||
|
||||
Definidos como funciones puras: solo construyen structs, no tocan estado global. Llamadas tipicas en `engine_smoke` (issue 0072a) y `runtime_test` (0072b).
|
||||
@@ -0,0 +1,176 @@
|
||||
#include "sprite_batch.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Vertex {
|
||||
float x, y;
|
||||
float u, v;
|
||||
float r, g, b, a;
|
||||
};
|
||||
|
||||
const char* VS =
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
"#version 300 es\n"
|
||||
#else
|
||||
"#version 330 core\n"
|
||||
#endif
|
||||
"uniform mat4 u_view_proj;\n"
|
||||
"layout(location = 0) in vec2 a_pos;\n"
|
||||
"layout(location = 1) in vec2 a_uv;\n"
|
||||
"layout(location = 2) in vec4 a_color;\n"
|
||||
"out vec2 v_uv;\n"
|
||||
"out vec4 v_color;\n"
|
||||
"void main() {\n"
|
||||
" v_uv = a_uv;\n"
|
||||
" v_color = a_color;\n"
|
||||
" gl_Position = u_view_proj * vec4(a_pos, 0.0, 1.0);\n"
|
||||
"}\n";
|
||||
|
||||
const char* FS =
|
||||
#if defined(__EMSCRIPTEN__)
|
||||
"#version 300 es\n"
|
||||
"precision mediump float;\n"
|
||||
#else
|
||||
"#version 330 core\n"
|
||||
#endif
|
||||
"in vec2 v_uv;\n"
|
||||
"in vec4 v_color;\n"
|
||||
"out vec4 frag;\n"
|
||||
"uniform sampler2D u_tex;\n"
|
||||
"void main() {\n"
|
||||
" frag = texture(u_tex, v_uv) * v_color;\n"
|
||||
"}\n";
|
||||
|
||||
void flush(SpriteBatch& b) {
|
||||
if (b.cpu_count_quads == 0) return;
|
||||
int verts = b.cpu_count_quads * 6;
|
||||
sg_range data{ b.cpu_buffer, (size_t)verts * sizeof(Vertex) };
|
||||
sg_update_buffer(b.vbo, &data);
|
||||
|
||||
sg_apply_pipeline(b.pipeline);
|
||||
|
||||
sg_bindings bind{};
|
||||
bind.vertex_buffers[0] = b.vbo;
|
||||
bind.views[0] = b.current_view;
|
||||
bind.samplers[0] = b.sampler;
|
||||
sg_apply_bindings(&bind);
|
||||
|
||||
sg_range proj_range{ b.proj, sizeof(b.proj) };
|
||||
sg_apply_uniforms(0, &proj_range);
|
||||
|
||||
sg_draw(0, verts, 1);
|
||||
b.cpu_count_quads = 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
SpriteBatch sprite_batch_create(int cpu_capacity_quads) {
|
||||
SpriteBatch b{};
|
||||
b.cpu_capacity_quads = cpu_capacity_quads > 0 ? cpu_capacity_quads : 4096;
|
||||
b.cpu_buffer = std::malloc((size_t)b.cpu_capacity_quads * 6 * sizeof(Vertex));
|
||||
if (!b.cpu_buffer) return b;
|
||||
|
||||
sg_buffer_desc bd{};
|
||||
bd.size = (size_t)b.cpu_capacity_quads * 6 * sizeof(Vertex);
|
||||
bd.usage.vertex_buffer = true;
|
||||
bd.usage.stream_update = true;
|
||||
b.vbo = sg_make_buffer(&bd);
|
||||
|
||||
sg_shader_desc sd{};
|
||||
sd.vertex_func.source = VS;
|
||||
sd.fragment_func.source = FS;
|
||||
sd.attrs[0].glsl_name = "a_pos";
|
||||
sd.attrs[1].glsl_name = "a_uv";
|
||||
sd.attrs[2].glsl_name = "a_color";
|
||||
sd.uniform_blocks[0].stage = SG_SHADERSTAGE_VERTEX;
|
||||
sd.uniform_blocks[0].size = 16 * sizeof(float);
|
||||
sd.uniform_blocks[0].layout = SG_UNIFORMLAYOUT_NATIVE;
|
||||
sd.uniform_blocks[0].glsl_uniforms[0].type = SG_UNIFORMTYPE_MAT4;
|
||||
sd.uniform_blocks[0].glsl_uniforms[0].glsl_name = "u_view_proj";
|
||||
sd.views[0].texture.stage = SG_SHADERSTAGE_FRAGMENT;
|
||||
sd.views[0].texture.image_type = SG_IMAGETYPE_2D;
|
||||
sd.views[0].texture.sample_type = SG_IMAGESAMPLETYPE_FLOAT;
|
||||
sd.samplers[0].stage = SG_SHADERSTAGE_FRAGMENT;
|
||||
sd.samplers[0].sampler_type = SG_SAMPLERTYPE_FILTERING;
|
||||
sd.texture_sampler_pairs[0].stage = SG_SHADERSTAGE_FRAGMENT;
|
||||
sd.texture_sampler_pairs[0].view_slot = 0;
|
||||
sd.texture_sampler_pairs[0].sampler_slot = 0;
|
||||
sd.texture_sampler_pairs[0].glsl_name = "u_tex";
|
||||
sg_shader shd = sg_make_shader(&sd);
|
||||
|
||||
sg_pipeline_desc pd{};
|
||||
pd.shader = shd;
|
||||
pd.layout.attrs[0].format = SG_VERTEXFORMAT_FLOAT2;
|
||||
pd.layout.attrs[1].format = SG_VERTEXFORMAT_FLOAT2;
|
||||
pd.layout.attrs[2].format = SG_VERTEXFORMAT_FLOAT4;
|
||||
pd.colors[0].blend.enabled = true;
|
||||
pd.colors[0].blend.src_factor_rgb = SG_BLENDFACTOR_SRC_ALPHA;
|
||||
pd.colors[0].blend.dst_factor_rgb = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
pd.colors[0].blend.src_factor_alpha = SG_BLENDFACTOR_ONE;
|
||||
pd.colors[0].blend.dst_factor_alpha = SG_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
|
||||
pd.primitive_type = SG_PRIMITIVETYPE_TRIANGLES;
|
||||
b.pipeline = sg_make_pipeline(&pd);
|
||||
|
||||
sg_sampler_desc smd{};
|
||||
smd.min_filter = SG_FILTER_LINEAR;
|
||||
smd.mag_filter = SG_FILTER_LINEAR;
|
||||
smd.wrap_u = SG_WRAP_CLAMP_TO_EDGE;
|
||||
smd.wrap_v = SG_WRAP_CLAMP_TO_EDGE;
|
||||
b.sampler = sg_make_sampler(&smd);
|
||||
|
||||
b.ok = true;
|
||||
return b;
|
||||
}
|
||||
|
||||
void sprite_batch_destroy(SpriteBatch& b) {
|
||||
if (b.cpu_buffer) { std::free(b.cpu_buffer); b.cpu_buffer = nullptr; }
|
||||
if (b.pipeline.id) sg_destroy_pipeline(b.pipeline);
|
||||
if (b.vbo.id) sg_destroy_buffer(b.vbo);
|
||||
if (b.sampler.id) sg_destroy_sampler(b.sampler);
|
||||
b = {};
|
||||
}
|
||||
|
||||
void sprite_batch_begin(SpriteBatch& b, const float view_proj_col_major[16]) {
|
||||
std::memcpy(b.proj, view_proj_col_major, sizeof(b.proj));
|
||||
b.cpu_count_quads = 0;
|
||||
b.current_view = {};
|
||||
b.in_pass = true;
|
||||
}
|
||||
|
||||
void sprite_batch_draw(SpriteBatch& b, sg_view view, int img_w, int img_h,
|
||||
fn::math2d::Rect src, fn::math2d::Rect dst,
|
||||
fn::math2d::Color tint) {
|
||||
if (!b.in_pass || !b.ok) return;
|
||||
if (b.current_view.id != view.id) {
|
||||
flush(b);
|
||||
b.current_view = view;
|
||||
}
|
||||
if (b.cpu_count_quads >= b.cpu_capacity_quads) flush(b);
|
||||
|
||||
float u0 = src.x / (float)img_w;
|
||||
float v0 = src.y / (float)img_h;
|
||||
float u1 = (src.x + src.w) / (float)img_w;
|
||||
float v1 = (src.y + src.h) / (float)img_h;
|
||||
|
||||
Vertex* base = (Vertex*)b.cpu_buffer + b.cpu_count_quads * 6;
|
||||
Vertex tl{ dst.x, dst.y, u0, v0, tint.r, tint.g, tint.b, tint.a };
|
||||
Vertex tr{ dst.x + dst.w, dst.y, u1, v0, tint.r, tint.g, tint.b, tint.a };
|
||||
Vertex bl{ dst.x, dst.y + dst.h, u0, v1, tint.r, tint.g, tint.b, tint.a };
|
||||
Vertex br{ dst.x + dst.w, dst.y + dst.h, u1, v1, tint.r, tint.g, tint.b, tint.a };
|
||||
base[0] = tl; base[1] = tr; base[2] = br;
|
||||
base[3] = tl; base[4] = br; base[5] = bl;
|
||||
b.cpu_count_quads++;
|
||||
}
|
||||
|
||||
void sprite_batch_end(SpriteBatch& b) {
|
||||
if (!b.in_pass) return;
|
||||
flush(b);
|
||||
b.in_pass = false;
|
||||
}
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
// sprite_batch — batched textured quad renderer on top of sokol_gfx.
|
||||
// Single draw call per atlas binding. Issue 0072b runtime gamedev.
|
||||
|
||||
#include "sokol_gfx.h"
|
||||
#include "math2d.h"
|
||||
|
||||
namespace fn::gfx {
|
||||
|
||||
struct SpriteBatch {
|
||||
sg_pipeline pipeline{};
|
||||
sg_buffer vbo{};
|
||||
sg_view current_view{}; // texture view bound this batch
|
||||
sg_sampler sampler{};
|
||||
void* cpu_buffer = nullptr; // Vertex* on heap
|
||||
int cpu_capacity_quads = 0;
|
||||
int cpu_count_quads = 0;
|
||||
float proj[16]{}; // current view-proj matrix (column-major)
|
||||
bool in_pass = false;
|
||||
bool ok = false;
|
||||
};
|
||||
|
||||
// Create the batcher. Allocates CPU buffer + sokol resources. cpu_capacity_quads
|
||||
// caps the per-flush quads (auto-flush when reached). Default 4096.
|
||||
SpriteBatch sprite_batch_create(int cpu_capacity_quads = 4096);
|
||||
void sprite_batch_destroy(SpriteBatch& b);
|
||||
|
||||
// Begin a batch with a column-major 4x4 view-projection.
|
||||
// Call between sg_begin_pass and sg_end_pass.
|
||||
void sprite_batch_begin(SpriteBatch& b, const float view_proj_col_major[16]);
|
||||
|
||||
// Queue one textured quad. dst is the screen rect. src is the UV rect inside the
|
||||
// texture, in pixels (0..image_size). tint multiplies the texture sample.
|
||||
// view must be a sg_view created with sg_make_view({.texture.image = ...}).
|
||||
void sprite_batch_draw(SpriteBatch& b, sg_view view, int img_w, int img_h,
|
||||
fn::math2d::Rect src, fn::math2d::Rect dst,
|
||||
fn::math2d::Color tint = fn::math2d::Color::white());
|
||||
|
||||
// Flush remaining quads.
|
||||
void sprite_batch_end(SpriteBatch& b);
|
||||
|
||||
} // namespace fn::gfx
|
||||
@@ -0,0 +1,61 @@
|
||||
---
|
||||
name: sprite_batch
|
||||
kind: function
|
||||
lang: cpp
|
||||
domain: gfx
|
||||
version: "0.1.0"
|
||||
purity: impure
|
||||
signature: "sprite_batch_create(int cap=4096) -> SpriteBatch; sprite_batch_begin/draw/end"
|
||||
description: "Batched textured quad renderer sobre sokol_gfx. Begin/draw/end con auto-flush por atlas change o capacity full. Vertex layout pos+uv+color, alpha blending estandar, GLSL 330 / GLES 300. Issue 0072b runtime gamedev — base de plataformeros, top-down, UI sprites."
|
||||
tags: [gamedev, gfx, sokol, sprite, batch, 2d]
|
||||
uses_functions:
|
||||
- sokol_setup_cpp_gfx
|
||||
uses_types:
|
||||
- Rect_cpp_core
|
||||
- Color_cpp_core
|
||||
returns: []
|
||||
returns_optional: false
|
||||
error_type: "error_go_core"
|
||||
imports: []
|
||||
example: |
|
||||
fn::gfx::SpriteBatch b = fn::gfx::sprite_batch_create(4096);
|
||||
// por frame, dentro de un sg_pass:
|
||||
fn::gfx::sprite_batch_begin(b, view_proj);
|
||||
fn::gfx::sprite_batch_draw(b, atlas_img, 1024, 1024,
|
||||
{0,0,32,32}, {100,100,32,32}, fn::math2d::Color::white());
|
||||
fn::gfx::sprite_batch_draw(b, atlas_img, 1024, 1024,
|
||||
{32,0,32,32}, {200,100,32,32}, fn::math2d::Color::rgba(255,0,0));
|
||||
fn::gfx::sprite_batch_end(b);
|
||||
tested: false
|
||||
tests: []
|
||||
test_file_path: ""
|
||||
file_path: "cpp/functions/gfx/sprite_batch.cpp"
|
||||
params:
|
||||
- name: cap
|
||||
desc: "Capacidad de quads por flush (CPU buffer). Default 4096 (~96 KB)."
|
||||
- name: img
|
||||
desc: "sg_image atlas. Cambio de img = auto-flush."
|
||||
- name: src
|
||||
desc: "Rect en pixeles dentro del atlas (UV se calculan dividiendo por img_w/img_h)."
|
||||
- name: dst
|
||||
desc: "Rect destino en coordenadas world (la matriz view-proj traduce a clip space)."
|
||||
- name: tint
|
||||
desc: "Color multiplicativo. Default Color::white()."
|
||||
output: "Quads renderizados via sg_draw cuando flush. Una sola draw call por atlas binding."
|
||||
---
|
||||
|
||||
# sprite_batch
|
||||
|
||||
Renderer batched de sprites 2D sobre sokol_gfx. Patron clasico:
|
||||
|
||||
1. `sprite_batch_create` una vez (despues de `sg_setup`).
|
||||
2. Por frame, dentro de un sg_pass:
|
||||
- `sprite_batch_begin(b, view_proj)` — pasa la matriz view-projection del camera_2d.
|
||||
- `sprite_batch_draw(...)` por sprite. Auto-flush cuando cambia atlas o se llena.
|
||||
- `sprite_batch_end(b)` — flush final.
|
||||
|
||||
**Alpha blending** activado por defecto (premultiplicado o no — usar el atlas que tengas; el shader hace `texture(u_tex, v_uv) * tint`).
|
||||
|
||||
**Sampler** linear filter + clamp-to-edge. Para pixel art, crear sampler propio con `SG_FILTER_NEAREST` y bindearlo manualmente (override no soportado por ahora — sub-issue futuro si hace falta).
|
||||
|
||||
**Performance**: 1 draw call por atlas. 10K sprites @ 60 FPS sobre WebGL2 modesto. Cap por defecto 4096 quads/flush; subir si tu juego dibuja >4K sprites del mismo atlas.
|
||||
Reference in New Issue
Block a user