Yet Another Raycast Implementation
A small C game engine for building vintage first-person games in the spirit of Wolfenstein 3D. YARI is focused on ESP32 hardware, but the same game code also runs on macOS, Linux and WebAssembly through raylib, and on desktop through SDL2.
- What is YARI?
- Highlights
- Quick Start
- Supported Targets
- Project Layout
- Writing a Game
- Core API
- Maps, Assets and Fonts
- Map Builder
- Build Commands
- ESP32 Configuration
- Compatibility and Current Limits
- Included Examples
yari is a compact raycasting engine for old-school 2.5D games: grid maps,
textured walls, floor and ceiling rendering, depth-sorted sprites, simple
collisions and cross-platform input.
It is not a general-purpose game engine. The goal is to stay small, understandable and practical on constrained hardware, while keeping desktop development fast enough to iterate without flashing an ESP32 after every change.
The repository includes:
- the engine core in
src/yari; - renderer and input backends for ESP32, raylib and SDL2;
- desktop, web and ESP-IDF examples;
- tools that convert images and fonts into C headers;
- a visual level editor that generates YARI-compatible
level.hfiles.
- C raycasting renderer: column-based wall rendering, sprite z-buffering, distance shading and floor/ceiling casting.
- ESP32-first design: ST7789 SPI backend, RGB565 framebuffer, configurable LCD pins and ready-to-build ESP-IDF examples.
- Desktop iteration: the same game can run on macOS/Linux through raylib or SDL2, which makes development and debugging much faster.
- Web output: the full game example can be compiled to WebAssembly and
served from
docs/. - Static assets: textures and fonts are packed into C arrays, making them easy to ship inside embedded firmware.
- Built-in map editor:
map_buildergenerates map data, player settings, surfaces, entities, collision layers and reloadable editor metadata. - Small game-facing API: a game implements
yr_init_game()andyr_update_game(). YARI owns the platform loop, rendering setup and input setup.
Requirements:
- ESP-IDF installed and configured;
- a connected ESP32 board;
- an ST7789 display matching the default pin configuration, or custom LCD/pin macros supplied at build time.
The project has been developed with ESP-IDF 5.5.2.
make esp32-build
make esp32-flash-monitorIf ESP-IDF is not installed under the path used by the Makefile, pass
ESP32_HOME explicitly:
make ESP32_HOME="$HOME/esp/v5.5.2/esp-idf" esp32-buildRequirements:
- a C compiler;
make;pkg-config;- raylib and/or SDL2 available through
pkg-config.
For the default raylib backend on macOS with Homebrew:
brew install raylib pkg-config
make runFor the SDL2 backend on macOS with Homebrew:
brew install sdl2 pkg-config
make run-sdlOn Linux, install the raylib or SDL2 development package with your distribution's package manager, then run one of:
make run
make run-sdlmake run builds the raylib backend. make run-sdl builds the SDL2 backend.
Both start the complete example in example/game/main/main.c.
Requirements:
- Emscripten active in the shell (
emccandemarinPATH); npxonly if you want to servedocs/locally.
make wasm
make run-wasmThe web build writes docs/index.html, docs/index.js and docs/index.wasm.
| Target | Backend | Status |
|---|---|---|
| ESP32 | ST7789 renderer + GPIO/ADC input | supported |
| macOS | raylib or SDL2 | supported |
| Linux | raylib or SDL2 | supported |
| WebAssembly | raylib PLATFORM_WEB + Emscripten | supported |
| Windows | potentially raylib or SDL2 | no build scripts are provided yet |
.
├── assets/ # Source PNG/JPG files and fonts
├── docs/ # WebAssembly output
├── example/
│ ├── base/ # Minimal ESP-IDF/raylib example
│ └── game/ # Full example with assets, fonts and a level
├── src/
│ ├── tools/ # assets_packer, font_baker, map_builder
│ └── yari/ # Engine source
│ ├── platform/esp32/ # ESP32 backend
│ ├── platform/raylib/ # Desktop/web backend
│ └── platform/sdl/ # Desktop SDL2 backend
└── Makefile # Desktop, WASM, ESP32 and tool builds
A YARI game includes yari.h, defines YARI_MAIN and implements two
functions:
yr_init_game(YrGameState *state): configures the screen, map, player, assets, target FPS and initial state.yr_update_game(YrGameState *state): runs one game frame.
Youc can find a minimal example in example/base/main.c:
YARI_NO_PREFIX is optional. Without it, use the explicit yr_ and Yr
symbols, such as yr_draw_game() and YrGameState.
When YARI_MAIN is defined, YARI provides main() on desktop/web and
app_main() on ESP32.
The internal loop:
- initializes the global game state and calls
yr_init_game(); - initializes the renderer and input backend;
- calls
yr_begin_drawing()at the beginning of each frame; - updates
state->game_time; - calls
yr_update_game(); - transfers the framebuffer with
yr_render_screen(); - calls
yr_end_drawing().
YrGameState is the central structure used by the engine.
| Field | Purpose |
|---|---|
player |
player position, direction and collision radius |
screen_width, screen_height |
logical framebuffer resolution |
game_title |
desktop window title |
target_fps |
target frame rate, expected to be greater than zero |
map, map_cols, map_rows |
tile map stored as uint8_t cells |
entities |
dynamic array of sprites/objects |
ray_res |
pixel width of each cast ray; larger values are faster but blockier |
zbuffer |
internal wall-depth buffer allocated by YARI |
assets_map |
texture lookup table generated by assets_packer |
floor_texture, ceil_texture |
floor and ceiling texture ids, or 0 for black |
game_time |
milliseconds since game start |
game_data |
user-defined pointer for custom game state |
| Function | Description |
|---|---|
yr_draw_game() |
draws background, walls and entities using the global state |
yr_draw_background(state) |
draws floor and ceiling |
yr_draw_walls(state) |
raycasts and draws walls |
yr_draw_entities(state) |
updates, sorts and draws entities |
yr_draw_text(text, x, y, font, color) |
draws bitmap text |
yr_draw_texture(...) |
draws a scaled 2D texture, useful for HUD and weapons |
yr_clear_screen(color) |
fills the framebuffer |
yr_draw_rectangle(x, y, w, h, color) |
low-level renderer primitive |
yr_get_frame_time() |
frame delta time in seconds |
yr_get_time() |
platform time in seconds |
yr_get_fps() |
current FPS estimate |
The raycaster assumes 64x64 textures for walls, floors, ceilings and
entities (YR_TEXTURE_SIZE). HUD textures can be drawn with yr_draw_texture()
by passing the correct source stride.
| Function | Description |
|---|---|
yr_move(pos, dir, movement, speed) |
moves a point using delta time |
yr_rotate(vector, rotation, speed) |
rotates a vector using delta time |
yr_check_collision(state, pos, radius, mask) |
checks collisions against walls and entities |
yr_check_ray_collision(state, origin, dir, dist, mask) |
checks collisions along a ray |
yr_slide_collision(state, from, to, hit, radius, mask) |
moves while sliding along obstacles |
Built-in collision masks:
#define YR_CMSK_NONE 0
#define YR_CMSK_WALL 1
#define YR_CMSK_ALL -1Entities can use custom bit masks for collision layers. The map builder can
define custom layers and generate macros such as YR_CMSK_ENTITY, YR_CMSK_PLAYER...
| Function | Desktop raylib/SDL2 and web raylib | ESP32 |
|---|---|---|
yr_is_key_down(key) |
reads the platform keyboard | reads a registered GPIO |
yr_is_key_up(key) |
reads the platform keyboard | reads a registered GPIO |
yr_is_key_pressed(key) |
platform edge press | GPIO edge press |
yr_esp_key_init(pin, key) |
no-op | maps a pull-up GPIO to a YARI key |
yr_joystick_init(pin_x, pin_y) |
stub | registers two ADC pins |
yr_joystick_get_axis(id, axis) |
returns 0 |
returns an axis around -1..1 |
Key codes are defined in src/yari/inputs.h and follow raylib values. The SDL2
backend maps SDL key events into the same YARI key enum, so game code can be
shared across desktop backends and embedded targets.
YrEntity represents a sprite in the world.
| Field | Purpose |
|---|---|
pos |
position in map space |
texture_id |
index inside assets_map |
dist |
distance from the player, maintained by the renderer |
vdiv, hdiv |
vertical/horizontal sprite size reduction |
vmove |
perspective vertical offset |
disabled |
if true, the entity is skipped |
entity_data |
user-defined pointer |
collision_mask |
entity collision layer |
collision_threshold |
collision radius |
update |
callback invoked by yr_draw_entities() |
To add or remove entities, use the dynamic array macros from da.h:
yr_da_append(&state->entities, entity);
yr_da_remove_unordered(&state->entities, index);
yr_da_free(&state->entities);With YARI_NO_PREFIX, these become da_append, da_remove_unordered and
da_free.
The map is a linear uint8_t array with map_rows * map_cols cells.
| Cell value | Meaning |
|---|---|
0 |
empty space |
1..127 |
texture id, indexed through assets_map |
128..255 |
solid color, indexed as yr_color_map[tile - 128] |
Player and entity coordinates are floating-point values in the same map space.
Cell (x, y) covers the area [x, x+1), [y, y+1).
Source assets live in assets/ and are converted into
example/game/main/assets.h:
make assetsassets_packer:
- reads
.pngand.jpgfiles; - generates one
yr_pixel_tarray per image; - generates a
TextureIdenum withtx_<file_name>symbols; - generates
assets_map[]; - emits both RGB565 data for
COLOR_565builds and 32-bit data for desktop/web builds.
Use simple C-friendly file names, for example wal_001.png, wep_gun0.png or
door_metal.png.
.ttf files under assets/font/ are baked into
example/game/main/fonts.h:
make assetsfont_baker generates four font sizes:
YR_FONT_SM;YR_FONT_MD;YR_FONT_LG;YR_FONT_XL.
Example:
draw_text("HP: 100", 10, 15, fonts[YR_FONT_SM], YR_GREEN);YARI includes a raylib/raygui visual level editor:
make run-map-builderThe editor needs raylib available through pkg-config; raygui.h is already
vendored in src/tools/. To build the editor without launching it:
make map-buildermake run-map-builder builds the tool and runs:
build/map_builder assets example/game/main/level.hThe executable accepts optional paths:
build/map_builder [assets_dir] [output_file]If omitted, assets_dir defaults to assets and output_file defaults to
level.h. The asset directory is scanned for .png, .jpg and .jpeg files;
their names are converted to the same tx_<file_name> symbols generated by
assets_packer, so run make assets after adding or renaming textures.
On startup, the editor tries to load the MAP_BUILDER_STATE_BEGIN/END metadata
from the output file. Press Save to overwrite the output header and Load to
reload the last saved state.
The generated file contains:
- map dimensions (
YR_MAP_COLS,YR_MAP_ROWS); - wall grid data;
- floor and ceiling texture ids;
- player position, direction and collision radius;
- custom collision layers;
- inline factories for entities;
level_append_exported_entities(); // appends entities marked asexportedin the editor to the game statelevel_get_map();- a commented
MAP_BUILDER_STATE_BEGIN/ENDblock used to reload the level in the editor.
Include the generated header from game code and wire it into yr_init_game():
#include "assets.h"
#include "level.h"
void yr_init_game(GameState *state) {
load_level(state);
// ...
}Entities marked as exported in the editor are appended in the game state entities.
If you want to spawn entities at runtime you can remove the exported flag and call the factory functions directly, for example:
YrEntity enemy = create_enemy_pos((Vector2){10.0f, 5.0f}, NULL);
yr_da_append(&state->entities, enemy);Entities with a named update callback generate a forward declaration for that function, so implement it in game code with this signature:
void update_enemy(YrGameState *state, YrEntity *self, size_t index) {
(void)state;
(void)self;
(void)index;
}Useful map builder controls:
| Action | Control |
|---|---|
| save | Save button or Ctrl/Cmd+S |
| reload level | Load button |
| fit view | Fit button |
| copy selection | Ctrl/Cmd+C |
| paste selection | Ctrl/Cmd+V |
| delete selection | Delete or Backspace |
| pan | middle mouse button, or Space + left drag |
| zoom | mouse wheel with Ctrl/Cmd |
Main editor modes:
Wall: draw walls as points, rectangles or circles.Entity: place sprites with texture, update callback, collision mask.Player: edit player position, direction, collision radius and collision layers.Floor/Ceil: assign floor and ceiling textures.
| Command | Effect |
|---|---|
make run |
builds and runs example/game with raylib on desktop |
make run-sdl |
builds and runs example/game with SDL2 on desktop |
make run-base |
runs the minimal example |
make assets |
regenerates assets.h and fonts.h |
make run-map-builder |
runs the map editor |
make run-wasm |
serves docs/ with npx serve |
make esp32-build |
builds example/game with ESP-IDF |
make esp32-flash |
builds and flashes example/game |
make esp32-monitor |
opens the serial monitor |
make esp32-flash-monitor |
flashes and opens the serial monitor |
make esp32-clean |
runs idf.py fullclean in example/game |
make esp32-base-build |
builds example/base for ESP32 |
make esp32-base-flash-monitor |
flashes and monitors the base example |
make all builds desktop, WebAssembly and ESP32 targets. For day-to-day work,
use narrower targets such as make run or make esp32-flash.
The ESP32 backend is implemented in src/yari/platform/esp32/renderer.c and
targets an ST7789 display over SPI in landscape orientation.
Main configuration macros:
// Framebuffer
#define FB_DOUBLE_BUFFER // enable double buffering (two framebuffers allocated in RAM, faster but more memory usage)
// Display
#define LCD_W 240
#define LCD_H 136
#define LCD_X_OFF 40
#define LCD_Y_OFF 53
// ST7789 pins
#define PIN_MOSI 19
#define PIN_CLK 18
#define PIN_CS 5
#define PIN_DC 16
#define PIN_RST 23
#define PIN_BL 4
// SPI
#define SPI_CLOCK_SPEED (80 * 1000 * 1000)The ESP-IDF examples already add:
idf_build_set_property(COMPILE_OPTIONS "-DESP32" APPEND)
idf_build_set_property(COMPILE_OPTIONS "-DCOLOR_565" APPEND)To use YARI as an ESP-IDF component in another project:
set(EXTRA_COMPONENT_DIRS "/path/to/yari/src/yari")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)Then require yari from your main component:
idf_component_register(
SRCS "main.c"
REQUIRES yari
)Digital buttons are configured as pull-up GPIOs:
esp_key_init(25, YR_KEY_Q);
esp_key_init(2, YR_KEY_E);
esp_key_init(15, YR_KEY_X);
esp_key_init(26, YR_KEY_SPACE);The analog joystick uses two ADC pins:
int joystick_id = joystick_init(32, 36);
float x = joystick_get_axis(joystick_id, YR_X_AXIS);
float y = joystick_get_axis(joystick_id, YR_Y_AXIS);- The project is plain C.
- The ESP32 renderer currently targets ST7789 SPI displays with an RGB565 framebuffer.
- Desktop rendering can use raylib or SDL2; web rendering uses raylib.
- Windows is not currently covered by dedicated build scripts.
- Raycaster textures are expected to be
64x64. - Map cells are
uint8_t: textured walls must use values1..127, because128..255is reserved for solid colors. - Desktop joystick backends are currently stubs.
A minimal example with an in-memory map and solid-color walls. Use it to learn the engine contract without the asset pipeline.
make run-baseA complete example with:
- packed assets from
assets/; - bitmap fonts;
- a level generated by the map builder;
- player movement;
- HUD rendering;
- a weapon pickup;
- desktop, ESP32 and WebAssembly builds.
make esp32-flash
make run
make run-sdl
make run-wasm