Download unify/README.md from Snapkitty/repoverse: direct link, hf CLI and curl.
- Browser
- Download file 11.3 kB
-
https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/README.md
- Command line
-
hf download hf://Snapkitty/repoverse/unify/README.md
-
curl -L -o README.md https://huggingface.co/Snapkitty/repoverse/resolve/main/unify/README.md
UNIFY
UNIFY is a 2D game engine runtime written in C++17, with Lua gameplay, a CUDA-like kernel compiler that
targets its own WASM runtime, and interchangeable platform backends. game/ holds a small platformer
built on it, which proves the runtime end to end.
UNIFY where
βββ renderer engine/render RenderDevice, Renderer2D (sort + batch), camera, atlas, QOI/PNG, bitmap font
βββ physics engine/physics rigid bodies, SAP broadphase, SAT/clip manifolds, sequential impulses, CCD, islands
βββ audio engine/audio mixer, buses, voices, WAV decoder, streaming ring buffer, device thread
βββ input engine/input timestamped event queue β per-tick InputFrame, actions/axes, buffering
βββ animation engine/animation sprite clips, keyframe tracks, loop/once/ping-pong, crossfade blending
βββ scene engine/runtime scene stack with per-scene resource ownership, prefabs, transitions
βββ ECS engine/ecs generational entities, sparse-set component pools
βββ Lua engine/script Lua 5.4 VM: own allocator + budget, engine-scheduled GC, sandbox
βββ memory / GC engine/memory Frame/Pool/Object/Asset/Buffer allocators, pool-backed STL allocator
βββ assets engine/assets registry, ref-counted handles, cache, loaders
βββ serialization engine/serialization tagged, versioned binary sections
βββ save states engine/runtime save_state / load_state / snapshot / restore / state_hash
βββ networking-ready state engine/net rollback buffer: snapshots + input log, rewind & resimulate
βββ WASM runtime engine/wasm binary decoder, interpreter, shared linear memory, buffers
βββ kernel compiler engine/kernel CUDA-like source β AST β IR β constant folding β WASM
βββ Unreal backend backends/unreal UE5 plugin implementing unify::Backend (not compiled here, see Status)
Build and run
Requires CMake β₯ 3.16, a C++17 compiler, and optionally SDL2 (libsdl2-dev) for the windowed backend.
cmake -S . -B build -G Ninja
cmake --build build # also generates the game's sprites, SFX and music (unify_assetgen)
ctest --test-dir build # 58 unit/integration tests + the definition-of-done playtest
./build/unify_game # SDL2 window: arrows/AD move, Space jump, F5 save, F9 load, Esc menu
./build/unify_game --headless --frames 600 --screenshot out.png
./build/unify_playtest game/assets out/ # scripted playthrough with verification + screenshots
The engine loop
Every real-time frame runs these stages. The playtest prints this order from the engine's own trace.
INPUT β INPUT BUFFER β FIXED SIMULATION β PHYSICS β GAMEPLAY / LUA β ANIMATION β AUDIO β RENDER PREPARATION β RENDER β PRESENT
ββββββββββββββββββββ zero or more fixed 60 Hz ticks βββββββββββββββββββ
There are five separate clocks:
| Clock | Where | Used for |
|---|---|---|
| Wall / input clock | Backend::now_ns() |
timestamping OS events, deciding when ticks are due |
| Simulation | SimClock (integer ticks) |
all gameplay, physics and animation. Seconds are derived from ticks, never accumulated |
| Render | RenderClock |
interpolation factor between the last two sim states |
| Audio | Mixer::frames_rendered() |
advanced by the audio device thread, independent of frame rate |
| Input | event timestamps | an event is consumed by the tick whose window contains it |
A tick is due when origin + (tick+1)Β·1e9/rate β€ now. After max_ticks_per_frame, a stall drops time
rather than spiralling. Rendering interpolates transforms, so 30, 60 and 144 FPS all run the same
60 ticks per second (tested).
Input buffering. OS events are queued with timestamps. Each tick drains the events stamped before
its end and folds them into an InputFrame: pressed, released, repeated, held, axes and pointer.
A press and release that both land between two ticks still reads as pressed in the next tick; the
playtest checks this with a 3 ms tap. input:buffered("jump", n) gives gameplay a press window.
Determinism, save states, networking
- One seeded RNG (xoshiro128**) is the only random source. Lua's
math.randomis rebound to it. Lua has noosorio, so no wall-clock reads. - The build uses
-ffp-contract=off -fno-fast-math, and every container that affects the simulation iterates in a deterministic order. - Save state = tagged binary sections:
HEAD(magic, version, engine, tick rate),TIME(tick, timers),RNG,SCEN(scene stack, owned assets by name, buffers, kernels),ECS(entity pool and component pools in dense order),PHYS(bodies, and contact impulses for warm starting),ANIM,INPT,CAMR,COMP(live WASM buffers and kernel sources),LUA(thegametable plus each scene'sstate), andAUDI(voices by asset name and position). There are no raw pointers; handles and names only. Unknown sections are skippable, and newer versions are refused. save_state()/load_state()from Lua are applied at the tick boundary.snapshot()/restore()are the in-memory forms, andsave_to_file()writes atomically.- Round trip: restore reproduces the saved
state_hash()exactly, in the same engine or a fresh one. Replaying the recorded input from a restored state reproduces the original continuation bit for bit. engine/net/rollback.hkeeps periodic snapshots plus the input log.correct(tick, frame)rewinds and re-simulates. A test proves that a late-corrected timeline equals the true one.
Lua API
Scenes are Lua files returning a table with init, load, update(dt), render, on_contact(a, b, kind, nx, ny, impulse), on_timer(name), on_saved, on_loaded and unload. UNIFY owns the lifecycle.
entity:set_position(x, y) entity:set_velocity(x, y) entity:set_animation("run", blend)
physics:apply_force(entity, x, y) physics:raycast(x1, y1, x2, y2, mask, ignore) physics:add_static_box(...)
audio:play("audio/jump") audio:stream("music/theme") audio:stop(id, fade) audio:set_bus_volume("music", v)
input:is_pressed("jump") input:is_held("left") input:buffered("jump", ticks) input:axis("move")
world:spawn("prefabs/player", x, y) world:find(name) camera:set(x, y, zoom) ui:text(...) -- ui only in render()
timer.after(sec, name) scene.transition("level") animation.define{...} map.load("maps/level1")
buffer.create(n) buffer.upload(b, t) buffer.download(b) kernel.compile(src) kernel.load(k) kernel.dispatch(k, grid, block, ...)
save_state([slot]) load_state([slot]) gc_step() gc_budget(kb) gc_pause() gc_resume() gc_collect()
Lua only ever holds handles: entities, buffers and kernels are userdata that wrap a generational handle,
checked on every call. A destroyed entity raises stale entity handle instead of touching freed memory.
Lua memory comes from its own allocator with a hard cap (the default is 64 MB). The collector is stopped,
and the engine runs one budgeted incremental step per tick, plus a full collection only on scene loads.
Kernel compiler and WASM runtime
__global__ void vadd(const float* A, const float* B, float* C, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) C[i] = A[i] + B[i];
}
Source β AST. A lexer and recursive-descent parser handle int/float, pointers, if/for/while,
break/continue, casts, sqrtf/fabsf/fminf/fmaxf/min/max, and threadIdx/blockIdx/blockDim/gridDim.
AST β IR. A typed, name-resolved, structured IR with explicit conversions, printed by print_ir.
Constant folding and dead-branch elimination run on it.
IR β WASM. Each module imports env.memory and exports the kernel plus <name>__dispatch(grid, block, ...),
which loops over the whole grid inside WASM.
Runtime. It decodes and validates the binary, builds block side tables, and interprets i32/i64/f32/f64.
Out-of-bounds access, divide by zero, stack overflow and fuel exhaustion are traps, returned as errors.
All kernels share one linear memory, and buffers are ranges handed out by BufferAllocator.
The demo runs vadd on the title screen and integrates the sparkle particles with a second kernel.
Backends
| Backend | Render | Audio | Input | Status |
|---|---|---|---|---|
| Headless | software device, offscreen | null device thread (real time) | programmatic / replay | built and tested |
| SDL2 (native) | software device β SDL texture, integer scaling | SDL audio callback | keyboard, mouse, game controller, touch | built; smoke-tested with SDL's dummy drivers |
| Unreal | FCanvasTriangleItem per batch, transient UTexture2D |
USynthComponent |
AnyKey bindings |
source only, not compiled (no UE toolchain in CI) |
The renderer is a RenderDevice interface. The engine ships a software rasterizer as its reference
device, and a GPU device would plug in at the same interface.
Verification
unify_tests: 58 tests covering every physics edge case in the spec (tunneling, bullets, resting contacts, stacking, penetration, zero and infinite mass, tiny and huge bodies, coincident centres, parallel edges, corner, edge and multi contacts, triggers, sleep and wake, filtering, NaN containment, exact save/restore). They also cover input buffering, the mixer, streaming and loop points, the audio device thread, renderer batching, blending, scissor and rotation, animation modes and blending, the allocators, handles, RNG, the kernel pipeline, WASM traps, scene-transition leak checks, determinism across engines, fresh-engine restore, Lua sandboxing, the handle and memory caps, GC scheduling, zero heap allocations per steady-state tick, stage order, frame-rate independence, and rollback.unify_playtest: the definition of done, from BOOT to CONTINUE PLAYING, on the real game with scripted input. Each stage is verified from engine state (hashes, counts, clips, voices) and screenshots are written.- The whole suite and the playtest are clean under AddressSanitizer and UndefinedBehaviorSanitizer.
Not included / known limits
- The WASM platform backend (compiling UNIFY itself to WebAssembly for browsers) is not built. It needs Emscripten, which isn't available here. The engine core has no platform dependencies, so it is a backend-sized job. UNIFY's WASM runtime for kernels is fully built.
- Unreal backend: see the table above.
- Compilers: builds with GCC and Clang, and with MinGW-w64 GCC on Windows (runtime linked statically).
MSVC is not supported: the code uses
__builtin_clzand__uint128_t. - Audio decodes PCM16 WAV only. Physics has no joints. Scissor rectangles are not applied on the Unreal backend. The software renderer is the only render device.