Journey Engine API Reference
Public API documentation for the
journey-enginecrate with usage examples fromgame.Version: 1.1.2 · Crate type:
rlib
Table of Contents
- Quick Start
- GameApp Trait
- Context
- Input
- Physics
- Sprite and Rendering
- Texture Management
- Camera and Screen Shake
- Audio
- Animation
- Time
- Math Utilities
- Atmosphere and Scene
- Re-exports
Quick Start
Minimal Game
use engine::{Context, GameAction, GameApp, Vec2}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum Action { MoveLeft, MoveRight, Jump } impl GameAction for Action { fn count() -> usize { 3 } fn index(&self) -> usize { *self as usize } fn from_index(i: usize) -> Option<Self> { match i { 0 => Some(Self::MoveLeft), 1 => Some(Self::MoveRight), 2 => Some(Self::Jump), _ => None } } fn move_negative_x() -> Option<Self> { Some(Self::MoveLeft) } fn move_positive_x() -> Option<Self> { Some(Self::MoveRight) } } struct MyGame; impl GameApp for MyGame { type Action = Action; fn init(ctx: &mut Context<Action>) -> Self { MyGame } fn update(&mut self, ctx: &mut Context<Action>) { //? Variable-rate update logic } fn render(&mut self, ctx: &mut Context<Action>) { ctx.draw_rect(Vec2::new(100.0, 100.0), Vec2::new(32.0, 32.0), [1.0, 0.0, 0.0, 1.0]); } } fn main() { engine::run::<MyGame>(); }
WASM Entry Point
#![allow(unused)] fn main() { use wasm_bindgen::prelude::*; #[wasm_bindgen(start)] pub fn wasm_main() { engine::run_wasm::<MyGame>(); } }
GameApp Trait
The core contract between engine and game. Implement this trait to define your game's lifecycle.
Module: engine (root)
#![allow(unused)] fn main() { pub trait GameApp: 'static { type Action: GameAction; fn init(ctx: &mut Context<Self::Action>) -> Self; fn fixed_update(&mut self, ctx: &mut Context<Self::Action>, fixed_time: &FixedTime) {} fn update(&mut self, ctx: &mut Context<Self::Action>); fn render(&mut self, ctx: &mut Context<Self::Action>); fn ui(&mut self, egui_ctx: &egui::Context, ctx: &mut Context<Self::Action>, scene_params: &mut SceneParams) {} //? Optional overrides with defaults: fn window_title() -> &'static str { "Journey Engine" } fn window_icon() -> Option<&'static [u8]> { None } fn wasm_ready_event() -> Option<&'static str> { None } fn internal_resolution() -> (u32, u32) { (640, 360) } } }
How game Uses It
#![allow(unused)] fn main() { impl GameApp for JourneyGame { type Action = JourneyAction; fn window_title() -> &'static str { "Journey" } fn window_icon() -> Option<&'static [u8]> { Some(include_bytes!("../../web/public/favicon.png")) } fn wasm_ready_event() -> Option<&'static str> { Some("journey:first-frame") } fn internal_resolution() -> (u32, u32) { (640, 360) } fn init(ctx: &mut Context<JourneyAction>) -> Self { //? Load textures during init and return a 1-based texture ID let tex_player = ctx.load_texture( include_bytes!("../assets/player/player.png"), "Player Spritesheet", ); //? Set up input bindings input::setup_default_bindings(&mut ctx.input); let level = Level::new(ctx.screen_width, ctx.screen_height); let player = Player::new(level.player_spawn, anim_state); Self { player, level, /* ... */ } } fn fixed_update(&mut self, ctx: &mut Context<JourneyAction>, fixed_time: &FixedTime) { //? All physics and combat run here at a fixed rate (default 60Hz) self.player.fixed_update(ctx.delta_time, fixed_time.tick, fixed_time.tick_rate(), /* ... */); } fn update(&mut self, ctx: &mut Context<JourneyAction>) { //? Camera smoothing with interpolation alpha let alpha = ctx.interpolation_alpha; let cam_x = lerp(self.prev_camera_x, self.camera_x, alpha); } fn render(&mut self, ctx: &mut Context<JourneyAction>) { //? Draw player sprite from sheet ctx.draw_sprite_from_sheet( position, size, color, source_rect, flip_x, texture_id, ); } } }
Context
The single mutable handle games use to interact with the engine. Passed to every GameApp method.
Module: engine::context
Public Fields
| Field | Type | Description |
|---|---|---|
input | InputState<A> | Current keyboard, mouse, and gamepad state (generic over your action enum) |
delta_time | f32 | Frame delta (variable in update, fixed in fixed_update) |
screen_width | f32 | Current viewport width in pixels |
screen_height | f32 | Current viewport height in pixels |
camera_offset_x | f32 | Horizontal camera pan offset |
camera_offset_y | f32 | Vertical camera pan offset |
fps | f32 | Current frames per second |
frame_time_ms | f32 | Last frame time in milliseconds |
fixed_tick_rate | u32 | Fixed update rate in Hz (default: 60) |
target_fps | u32 | Native display frame cap; WASM follows browser RAF/vsync |
interpolation_alpha | f32 | 0.0–1.0 fraction for render-time smoothing |
freeze_frames | u16 | Remaining freeze frames (hitstop) |
pending_shakes | Vec<(f32, f32)> | Queued screen shakes: (intensity, duration) |
request_exit | bool | Set to true to close the window |
fullscreen_enabled | bool | Whether fullscreen is currently enabled |
hdr_enabled | bool | Whether HDR output is active |
audio | AudioManager | Direct access to the audio system |
pending_ui_audio | Vec<UiAudioEvent> | UI audio events queued this frame |
bloom | BloomSettings | Persistent post-process bloom settings |
Methods
Texture Loading
#![allow(unused)] fn main() { pub fn load_texture(&mut self, bytes: &'static [u8], label: &str) -> usize }
Queue a texture for loading during init(). Returns a 1-based texture ID (0 is the built-in white pixel). The engine processes these after init completes.
#![allow(unused)] fn main() { //? In GameApp::init() let tex_player = ctx.load_texture( include_bytes!("../assets/player/player.png"), "Player Spritesheet", ); //? tex_player == 1 (first loaded texture) }
Drawing
#![allow(unused)] fn main() { //? Draw a colored rectangle pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: [f32; 4]) pub fn draw_rect_layer(&mut self, layer: RenderLayer, position: Vec2, size: Vec2, color: [f32; 4]) pub fn draw_rect_additive(&mut self, position: Vec2, size: Vec2, color: [f32; 4]) //? Draw a sprite with optional horizontal flip pub fn draw_sprite(&mut self, position: Vec2, size: Vec2, color: [f32; 4], flip_x: bool) //? Draw a region from a sprite sheet pub fn draw_sprite_from_sheet( &mut self, position: Vec2, //? Top-left corner in screen space size: Vec2, //? Width and height in pixels color: [f32; 4], //? RGBA tint (0.0–1.0 each) source_rect: Rect, //? Sprite sheet region in pixel coordinates flip_x: bool, //? Horizontal mirror texture_id: usize, //? 1+ for loaded textures, 0 for white pixel ) //? Same as above with additive blending (for effects/glow) pub fn draw_sprite_from_sheet_additive( &mut self, position: Vec2, size: Vec2, color: [f32; 4], source_rect: Rect, flip_x: bool, texture_id: usize, ) }
Usage from game:
#![allow(unused)] fn main() { //? Draw a solid red rectangle (health bar) ctx.draw_rect(Vec2::new(10.0, 10.0), Vec2::new(100.0, 8.0), [1.0, 0.0, 0.0, 1.0]); //? Draw the player from a spritesheet let frame_rect = Rect::new(col as f32 * 100.0, row as f32 * 100.0, 100.0, 100.0); ctx.draw_sprite_from_sheet( player_pos, Vec2::new(50.0, 50.0), [1.0, 1.0, 1.0, 1.0], //? White tint = original colors frame_rect, !player.facing_right(), 1, //? texture ID from load_texture ); //? Additive glow effect on a hit flash ctx.draw_sprite_from_sheet_additive( position, size, [1.0, 0.8, 0.2, 0.6], source_rect, flip_x, texture_id, ); //? Additive colored rectangle on the effects layer ctx.draw_rect_additive(position, size, [0.2, 0.9, 1.0, 0.5]); //? Debug geometry draws after world/effects sprites ctx.draw_rect_layer(RenderLayer::Debug, position, size, [0.0, 1.0, 0.0, 0.8]); }
Effects
#![allow(unused)] fn main() { //? Freeze physics and FSM for N render frames (hitstop) pub fn trigger_freeze(&mut self, frames: u16) //? Queue a screen shake with intensity and duration in seconds pub fn trigger_shake(&mut self, intensity: f32, duration: f32) //? Temporarily override bloom for this frame only pub fn override_bloom(&mut self, settings: BloomSettings) }
Usage from game:
#![allow(unused)] fn main() { //? Parry hitstop: 3-frame freeze + screen shake ctx.trigger_freeze(3); ctx.trigger_shake(4.0, 0.15); }
Bloom
#![allow(unused)] fn main() { pub struct BloomSettings { pub enabled: bool, pub threshold: f32, pub intensity: f32, pub radius: f32, } }
Bloom is a post-process primitive. It extracts bright pixels from the finished internal-resolution scene, samples a compact neighborhood, and composites the glow during the final blit. Use additive draw calls for neon/effect sources, then enable bloom globally through ctx.bloom or temporarily with ctx.override_bloom(...).
#![allow(unused)] fn main() { ctx.bloom.enabled = true; ctx.bloom.threshold = 0.65; ctx.bloom.intensity = 0.35; ctx.bloom.radius = 2.0; ctx.override_bloom(BloomSettings { enabled: true, threshold: 0.58, intensity: 0.28, radius: 2.0, }); }
Audio
The engine only manages UI-level audio events (UiAudioEvent). Game-specific audio events should be defined and queued by the game crate. The runtime automatically deduplicates pending_ui_audio each frame.
#![allow(unused)] fn main() { //? UI audio is handled automatically via the AudioResponse trait on egui widgets. //? Game-specific audio (jump, dash, etc.) is managed by the game crate's own event queue. }
Utility
#![allow(unused)] fn main() { pub fn screen_center(&self) -> Vec2 pub fn set_fullscreen_enabled(&mut self, enabled: bool) pub fn set_hdr_enabled(&mut self, enabled: bool) }
Input
Generic, action-based input system that decouples hardware keys from game intent. Each game defines its own action enum by implementing the GameAction trait.
Module: engine::input
GameAction Trait
The engine's input system is generic over this trait. There are no hardcoded actions, each game defines its own.
#![allow(unused)] fn main() { pub trait GameAction: Copy + Eq + Debug + 'static { fn count() -> usize; fn index(&self) -> usize; fn from_index(index: usize) -> Option<Self>; //? Optional: wire these up for get_move_x() / get_move_y() axis helpers fn move_negative_x() -> Option<Self> { None } fn move_positive_x() -> Option<Self> { None } fn move_negative_y() -> Option<Self> { None } fn move_positive_y() -> Option<Self> { None } } }
Defining Actions (Game-Side)
#![allow(unused)] fn main() { #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum JourneyAction { MoveLeft, MoveRight, MoveUp, MoveDown, Jump, Attack, Block, Dash, Grapple, } impl GameAction for JourneyAction { fn count() -> usize { 9 } fn index(&self) -> usize { *self as usize } fn from_index(i: usize) -> Option<Self> { match i { 0 => Some(Self::MoveLeft), 1 => Some(Self::MoveRight), 2 => Some(Self::MoveUp), 3 => Some(Self::MoveDown), 4 => Some(Self::Jump), 5 => Some(Self::Attack), 6 => Some(Self::Block), 7 => Some(Self::Dash), 8 => Some(Self::Grapple), _ => None, } } fn move_negative_x() -> Option<Self> { Some(Self::MoveLeft) } fn move_positive_x() -> Option<Self> { Some(Self::MoveRight) } fn move_negative_y() -> Option<Self> { Some(Self::MoveUp) } fn move_positive_y() -> Option<Self> { Some(Self::MoveDown) } } }
Key
#![allow(unused)] fn main() { pub enum Key { W, A, S, D, Space, Shift, Alt, Up, Down, Left, Right, F12, Escape, } }
MouseBinding
#![allow(unused)] fn main() { pub enum MouseBinding { Left, Right, Middle, } }
InputState<A> Methods
#![allow(unused)] fn main() { //? Is the action currently held down? pub fn is_action_pressed(&self, action: A) -> bool //? Was the action pressed this frame (rising edge)? pub fn is_action_just_pressed(&self, action: A) -> bool //? Was the action pressed within a recent time window? (for jump buffering) pub fn was_action_pressed_buffered(&self, action: A, buffer_window: f32) -> bool //? Raw key queries (for non-action keys like Escape) pub fn is_key_pressed(&self, key: Key) -> bool pub fn is_key_just_pressed(&self, key: Key) -> bool //? Mouse button state pub fn is_mouse_pressed(&self, button: MouseButton) -> bool //? Axis values: -1.0 to 1.0 (combines keyboard + gamepad) //? Requires move_negative_x/move_positive_x on your GameAction impl pub fn get_move_x(&self) -> f32 pub fn get_move_y(&self) -> f32 //? Device detection pub fn any_keyboard_or_mouse(&self) -> bool pub fn any_gamepad(&self) -> bool //? Access binding configuration pub fn input_map_mut(&mut self) -> &mut InputMap<A> }
InputMap<A> (Custom Bindings)
The engine ships with no default bindings. Games register their own during init():
#![allow(unused)] fn main() { pub fn bind_key(&mut self, key: Key, action: A) pub fn bind_mouse(&mut self, button: MouseBinding, action: A) //? Native only: pub fn bind_button(&mut self, button: gilrs::Button, action: A) }
Setting Up Bindings (Game-Side)
#![allow(unused)] fn main() { fn init(ctx: &mut Context<JourneyAction>) -> Self { let map = ctx.input.input_map_mut(); map.bind_key(Key::A, JourneyAction::MoveLeft); map.bind_key(Key::Left, JourneyAction::MoveLeft); map.bind_key(Key::D, JourneyAction::MoveRight); map.bind_key(Key::Right, JourneyAction::MoveRight); map.bind_key(Key::Space, JourneyAction::Jump); map.bind_mouse(MouseBinding::Left, JourneyAction::Attack); map.bind_mouse(MouseBinding::Right, JourneyAction::Block); //? ... } }
Usage from game
#![allow(unused)] fn main() { fn fixed_update(&mut self, ctx: &mut Context<JourneyAction>, fixed_time: &FixedTime) { let move_x = ctx.input.get_move_x(); //? Jump with buffering (8 ticks ≈ 133ms at 60Hz) let buffer_window = self.stats.jump_buffer_ticks as f32 / 60.0; if ctx.input.was_action_pressed_buffered(JourneyAction::Jump, buffer_window) { self.execute_jump(); } //? Dash on just-pressed (no buffering) if ctx.input.is_action_just_pressed(JourneyAction::Dash) { self.start_dash(move_x); } } }
Journey’s Key Bindings (Game-Side)
| Key | Action | Key | Action | |
|---|---|---|---|---|
| A / ← | MoveLeft | D / → | MoveRight | |
| W / ↑ | MoveUp | S / ↓ | MoveDown | |
| Space | Jump | Shift | Dash | |
| Alt | Grapple | Mouse L | Attack | |
| Mouse R | Block |
Journey’s Gamepad Bindings (Game-Side, Native)
| Button | Action |
|---|---|
| South (A/Cross) | Jump |
| West (X/Square) | Attack |
| RightTrigger (RB) | Block |
| RightTrigger2 (RT) | Dash |
| LeftTrigger2 (LT) | Grapple |
Physics
2D collision detection primitives.
Module: engine::physics
AABB
Axis-Aligned Bounding Box: the primary collision shape.
#![allow(unused)] fn main() { pub struct AABB { pub center: Vec2, pub size: Vec2, } }
Constructors
#![allow(unused)] fn main() { //? From center position and size pub fn new(center: Vec2, size: Vec2) -> Self //? From top-left corner and size (common for sprites) pub fn from_top_left(top_left: Vec2, size: Vec2) -> Self }
Queries
#![allow(unused)] fn main() { pub fn min(&self) -> Vec2 //? Top-left corner pub fn max(&self) -> Vec2 //? Bottom-right corner pub fn top_left(&self) -> Vec2 //? Alias for min() pub fn check_collision(&self, other: &AABB) -> bool pub fn get_overlap(&self, other: &AABB) -> Vec2 //? Per-axis overlap }
Collision Resolution
#![allow(unused)] fn main() { //? Minimum Translation Vector to push mover out of obstacle //? Returns None if no overlap. Pushes along smallest-overlap axis. //? Y-axis bias toward upward push (landing on platforms). pub fn resolve_collision(mover: &AABB, obstacle: &AABB) -> Option<Vec2> }
Swept Collision (CCD)
#![allow(unused)] fn main() { pub struct SweepResult { pub time: f32, //? 0.0–1.0, fraction of displacement before hit pub normal: Vec2, //? Surface normal at contact point } //? Continuous collision: move self along displacement, find earliest hit. //? Uses Minkowski-expanded ray cast. pub fn swept_collision(&self, displacement: Vec2, obstacle: &AABB) -> Option<SweepResult> }
CollisionLayer
#![allow(unused)] fn main() { pub enum CollisionLayer { Pushbox, //? Physics body (blocks movement) Hurtbox, //? Vulnerable area (takes damage) Hitbox, //? Damaging area (deals damage) Parrybox, //? Parry detection area } }
BoxVolume
A positioned collision volume relative to an entity center. Auto-flips X offset based on facing direction.
#![allow(unused)] fn main() { pub struct BoxVolume { pub layer: CollisionLayer, pub local_offset: Vec2, pub size: Vec2, pub active: bool, } pub fn new(layer: CollisionLayer, offset: Vec2, size: Vec2) -> Self pub fn world_aabb(&self, entity_pos: Vec2, facing_right: bool) -> AABB }
Usage from game
#![allow(unused)] fn main() { //? Player pushbox let player_aabb = AABB::new(player.position, Vec2::new(PLAYER_WIDTH, PLAYER_HEIGHT)); //? Check collision with each platform for platform in &platforms { if let Some(mtv) = AABB::resolve_collision(&player_aabb, &platform.aabb) { player.position += mtv; } } //? Swept collision for high-speed movement let displacement = velocity * dt; if let Some(hit) = player_aabb.swept_collision(displacement, &wall) { //? Move to contact point player.position += displacement * hit.time; //? Slide along surface let remaining = displacement * (1.0 - hit.time); let slide = remaining - hit.normal * remaining.dot(hit.normal); player.position += slide; } //? Hitbox that flips with facing direction let hitbox = BoxVolume::new( CollisionLayer::Hitbox, Vec2::new(12.0, 0.0), //? 12px in front of center Vec2::new(20.0, 24.0), ); let world_hitbox = hitbox.world_aabb(entity.position, entity.facing_right); }
Sprite and Rendering
Instanced 2D sprite rendering with sprite sheet and blend mode support.
Module: engine::sprite
Rect
#![allow(unused)] fn main() { pub struct Rect { pub x: f32, pub y: f32, pub w: f32, pub h: f32, } pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self pub fn from_pos_size(pos: Vec2, size: Vec2) -> Self }
BlendMode
#![allow(unused)] fn main() { pub enum BlendMode { Alpha, //? Standard transparency (default) Additive, //? Additive blending (effects, glow) } }
RenderLayer
#![allow(unused)] fn main() { pub enum RenderLayer { Background, World, //? Default gameplay layer Effects, //? Glows, trails, particles, additive rects Debug, //? Collision boxes and debug overlays } }
Layers are a coarse draw-order contract, not a scene system. The renderer draws layers in declaration order, and within each layer draws alpha before additive.
Sprite (Internal)
The Sprite struct is not typically created directly by game code. Instead, use the Context draw methods which build sprites internally:
#![allow(unused)] fn main() { //? Solid colored rectangle ctx.draw_rect(pos, size, color); //? Explicit layer for debug or background/world split ctx.draw_rect_layer(RenderLayer::Debug, pos, size, color); //? Additive colored rectangle for neon/effects ctx.draw_rect_additive(pos, size, color); //? Textured sprite from sheet ctx.draw_sprite_from_sheet(pos, size, color, source_rect, flip_x, texture_id); //? Additive blended sprite ctx.draw_sprite_from_sheet_additive(pos, size, color, source_rect, flip_x, texture_id); }
Rendering Details
- Up to 65_536 sprites per frame (instanced rendering)
- Sprites are ordered by
RenderLayer, thenBlendMode, then batched bytexture_id - Two render pipelines: alpha blend (default) and additive blend
- Bloom is applied during the final blit when enabled
- Horizontal flipping is done in UV-space, avoiding anchor-offset artifacts
texture_id = 0uses a built-in 1×1 white pixel (for colored rectangles)
Texture Management
Modules: engine::texture, engine::texture_manager
Texture
Low-level GPU texture. Not typically used directly, go through Context::load_texture() or TextureManager.
#![allow(unused)] fn main() { pub struct Texture { pub texture: wgpu::Texture, pub view: wgpu::TextureView, pub sampler: wgpu::Sampler, pub width: u32, pub height: u32, } pub fn from_bytes(device, queue, bytes, label) -> Result<Self, ImageError> pub fn from_image(device, queue, img, label) -> Result<Self, ImageError> pub fn white_pixel(device, queue) -> Self }
TextureHandle
Opaque handle to a loaded texture in the TextureManager.
#![allow(unused)] fn main() { pub struct TextureHandle(pub(crate) usize); }
Loading Textures in game
The simplest path is Context::load_texture() during init:
#![allow(unused)] fn main() { fn init(ctx: &mut Context) -> Self { //? Returns a 1-based ID used with draw_sprite_from_sheet let tex_player = ctx.load_texture( include_bytes!("../assets/player/player.png"), "Player Spritesheet", ); //? tex_player == 1 let tex_effects = ctx.load_texture( include_bytes!("../assets/effects/particles.png"), "Effects Sheet", ); //? tex_effects == 2 } }
Camera and Screen Shake
Module: engine::camera
ScreenShake
Decaying sinusoidal screen shake with Lissajous-like orbits for organic feel.
#![allow(unused)] fn main() { pub struct ScreenShake { pub intensity: f32, pub duration: f32, pub frequency: f32, //? Default: 40.0 pub decay: f32, //? Default: 8.0 pub elapsed: f32, } pub fn new(intensity: f32, duration: f32) -> Self pub fn sample(&self) -> Vec2 //? Current shake offset pub fn update(&mut self, dt: f32) -> bool //? Returns true if still active pub fn is_active(&self) -> bool }
Triggering Shakes from game
#![allow(unused)] fn main() { //? Via Context (most common) ctx.trigger_shake(4.0, 0.15); //? intensity=4.0, duration=0.15s //? Multiple shakes stack additively ctx.trigger_shake(2.0, 0.1); //? Light shake ctx.trigger_shake(8.0, 0.3); //? Heavy shake (both run simultaneously) }
Camera (Internal)
The camera is managed by the engine runtime. Game code interacts with it via Context fields:
#![allow(unused)] fn main() { //? Read camera state let offset_x = ctx.camera_offset_x; let offset_y = ctx.camera_offset_y; //? Camera follow is typically done in update(): fn update(&mut self, ctx: &mut Context) { let target_x = (self.player.position().x - ctx.screen_width / 2.0).max(0.0); self.camera_x = lerp(self.prev_camera_x, target_x, 0.1); ctx.camera_offset_x = self.camera_x; ctx.camera_offset_y = self.camera_y; } }
Audio
Cross-platform audio system wrapping Kira with lazy WASM initialization.
Module: engine::audio
AudioTrack
#![allow(unused)] fn main() { pub enum AudioTrack { Music, //? Looping background music Ambience, //? Looping ambient sounds Sfx, //? Sound effects (one-shot + looping) Ui, //? UI sounds (one-shot) } }
UiAudioEvent
Engine-level UI audio events (game-specific events are defined by the game crate):
#![allow(unused)] fn main() { pub enum UiAudioEvent { Hover, Click, CheckboxOn, CheckboxOff, TabChange, } }
AudioManager Methods
Playback
#![allow(unused)] fn main() { //? One-shot sound on a specific track pub fn play_oneshot(&mut self, data: &StaticSoundData, track: AudioTrack) //? Looping music with crossfade pub fn play_music(&mut self, data: &StaticSoundData, fade_in_secs: f32) pub fn stop_music(&mut self, fade_out_secs: f32) //? Looping ambience with crossfade pub fn play_ambience(&mut self, data: &StaticSoundData, fade_in_secs: f32) pub fn stop_ambience(&mut self, fade_out_secs: f32) //? Looping SFX (e.g., footstep loop while running) pub fn play_loop_sfx(&mut self, data: &StaticSoundData) pub fn stop_loop_sfx(&mut self, fade_out_secs: f32) }
Volume Control
#![allow(unused)] fn main() { //? Track-level volume (0.0–1.0) pub fn set_master_volume(&mut self, volume: f64) pub fn set_music_volume(&mut self, volume: f64) pub fn set_ambience_volume(&mut self, volume: f64) pub fn set_sfx_volume(&mut self, volume: f64) pub fn set_ui_volume(&mut self, volume: f64) //? Read current volume pub fn master_volume(&self) -> f64 pub fn music_volume(&self) -> f64 //? ... (same pattern for other tracks) //? Computed effective volume (master × track) pub fn effective_volume(&self, track: AudioTrack) -> f64 //? Live ducking with fade pub fn set_music_live_volume(&mut self, amp: f64, fade_secs: f32) pub fn set_ambience_live_volume(&mut self, amp: f64, fade_secs: f32) }
State Queries
#![allow(unused)] fn main() { pub fn has_active_music(&self) -> bool pub fn has_active_ambience(&self) -> bool pub fn notify_user_gesture(&mut self) //? WASM: unlock Web Audio context }
Loading Sounds
#![allow(unused)] fn main() { //? Decode sound from embedded bytes (cross-platform) pub fn load_sound_data(bytes: &'static [u8]) -> Option<StaticSoundData> }
AudioResponse Trait (egui Integration)
#![allow(unused)] fn main() { pub trait AudioResponse { fn with_ui_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self; fn with_checkbox_sound(self, checked: bool, pending: &mut Vec<UiAudioEvent>) -> Self; fn with_tab_sound(self, pending: &mut Vec<UiAudioEvent>) -> Self; } }
Usage from game
#![allow(unused)] fn main() { //? Loading audio assets at init struct AudioAssets { sfx_jump: Option<StaticSoundData>, bg_music: Option<StaticSoundData>, } impl AudioAssets { fn load() -> Self { Self { sfx_jump: load_sound_data(include_bytes!("../assets/audio/sfx_jump.ogg")), bg_music: load_sound_data(include_bytes!("../assets/audio/bg_music.ogg")), } } } //? Playing music (in update) if let Some(ref data) = self.audio_assets.bg_music { ctx.audio.play_music(data, 1.0); //? 1s fade-in } //? Game-specific audio: define your own enum and queue pub enum AudioEvent { Jump, Dash, Hit, /* ... */ } let mut pending_game_audio: Vec<AudioEvent> = Vec::new(); pending_game_audio.push(AudioEvent::Jump); //? Dispatch game events (in update) for event in pending_game_audio.drain(..) { self.audio_assets.dispatch(event, &mut ctx.audio); } //? Dispatch engine UI events for ui_event in ctx.pending_ui_audio.drain(..) { self.audio_assets.dispatch_ui(ui_event, &mut ctx.audio); } //? Music ducking during menus let ducked_vol = ctx.audio.effective_volume(AudioTrack::Music) * 0.3; ctx.audio.set_music_live_volume(ducked_vol, 0.4); //? egui UI sounds (push UiAudioEvent automatically) ui.button("Start") .with_ui_sound(&mut ctx.pending_ui_audio) .clicked(); ui.checkbox(&mut show_fps, "Show FPS") .with_checkbox_sound(show_fps, &mut ctx.pending_ui_audio); }
Animation
Asset-agnostic animation runtime. The engine provides the state machine; the game maps frames to textures.
Module: engine::animation
AnimationDef
#![allow(unused)] fn main() { pub struct AnimationDef { pub name: String, pub start_frame: usize, pub frame_count: usize, pub frame_duration: f32, //? Seconds per frame pub looping: bool, } pub fn new( name: impl Into<String>, start_frame: usize, frame_count: usize, frame_duration: f32, looping: bool, ) -> Self }
AnimationState
#![allow(unused)] fn main() { pub struct AnimationState { pub current_anim: String, pub frame_index: usize, pub timer: f32, } pub fn new(animations: Vec<AnimationDef>, default_anim: &str) -> Self pub fn update(&mut self, dt: f32) pub fn current(&self) -> Option<(&AnimationDef, usize)> pub fn play(&mut self, anim_name: &str) //? Switch animation (resets frame/timer) pub fn is_finished(&self) -> bool //? Non-looping anim reached last frame? pub fn current_animation_name(&self) -> Option<&str> pub fn get_progress(&self) -> f32 //? 0.0–1.0 through current animation }
Usage from game
The game wraps the engine's AnimationState with spritesheet-specific logic:
#![allow(unused)] fn main() { //? Define animations with grid-based start frames let animations = vec![ Animation::new("Idle", 0, 4, 0.12, true), //? Row 0, 4 frames, looping Animation::new("Run", 5, 4, 0.08, true), //? Row 1, 4 frames, looping Animation::new("Jump", 10, 3, 0.1, false), //? Row 2, 3 frames, one-shot Animation::new("Fall", 13, 2, 0.1, true), //? Row 2 continued Animation::new("Death", 15, 4, 0.1, false), //? Row 3, one-shot ]; let mut anim_state = AnimationState::new(animations, "Idle"); //? In fixed_update: switch based on player state match player.state { PlayerState::Idle => anim_state.play("Idle"), PlayerState::Run => anim_state.play("Run"), PlayerState::Jump => anim_state.play("Jump"), PlayerState::Fall => anim_state.play("Fall"), _ => {} } //? In update: advance timer anim_state.update(ctx.delta_time); //? In render: get the current animation def and frame index if let Some((anim_def, frame_idx)) = anim_state.current() { let sprite_frame = anim_def.start_frame + frame_idx; let col = sprite_frame % SHEET_COLS; let row = sprite_frame / SHEET_COLS; let frame_rect = Rect::new(col as f32 * FRAME_W, row as f32 * FRAME_H, FRAME_W, FRAME_H); ctx.draw_sprite_from_sheet( position, size, [1.0, 1.0, 1.0, 1.0], frame_rect, !facing_right, texture_id, ); } //? Combat animations derive frame duration from FSM data let move_data = move_db.get_base(MoveId::AttackHorizontal); let frame_dur = move_data.anim_frame_duration(4); //? 4 sprite frames Animation::new("AttackHorizontal", 25, 4, frame_dur, false); }
Time
Fixed-timestep timing system for deterministic game logic.
Module: engine::time
Constants
#![allow(unused)] fn main() { pub const DEFAULT_FIXED_HZ: u32 = 60; pub const MAX_STEPS: u32 = 5; //? Spiral-of-death cap }
FixedTime
#![allow(unused)] fn main() { pub struct FixedTime { pub tick: u64, //? Monotonic tick counter pub fixed_dt: f32, //? Duration of one fixed step } pub fn new(hz: u32) -> Self pub fn accumulate(&mut self, dt: f32) -> u32 //? Returns number of steps to run pub fn advance(&mut self) //? Consume one step, increment tick pub fn interpolation_alpha(&self) -> f32 //? Leftover fraction for smoothing pub fn tick_rate(&self) -> u32 pub fn set_tick_rate(&mut self, hz: u32) pub fn freeze(&mut self, frames: u16) //? Pause physics for N frames pub fn is_frozen(&self) -> bool pub fn freeze_remaining(&self) -> u16 }
Usage from game
FixedTime is managed by the engine and passed to fixed_update():
#![allow(unused)] fn main() { fn fixed_update(&mut self, ctx: &mut Context, fixed_time: &FixedTime) { //? Use tick for deterministic frame-data combat let current_tick = fixed_time.tick; self.player.current_tick = current_tick; //? Use fixed_dt for physics integration entity.velocity.y += GRAVITY * fixed_time.fixed_dt; entity.position += entity.velocity * fixed_time.fixed_dt; //? Hitstop via freeze ctx.trigger_freeze(5); //? 5-frame pause on a heavy hit } }
Math Utilities
Module: engine::math
move_towards
#![allow(unused)] fn main() { pub fn move_towards(current: f32, target: f32, max_delta: f32) -> f32 }
Move current toward target by at most max_delta. Snaps exactly to target if within max_delta, avoiding overshooting. Mirrors Unity's Mathf.MoveTowards.
#![allow(unused)] fn main() { use engine::math::move_towards; //? Smooth deceleration velocity_x = move_towards(velocity_x, 0.0, GROUND_DECEL * dt); //? Smooth acceleration toward target speed velocity_x = move_towards(velocity_x, target_speed, ACCELERATION * dt); }
Atmosphere and Scene
Procedural sky and fog rendering.
Module: engine::atmosphere
SkyParams
#![allow(unused)] fn main() { pub struct SkyParams { pub enabled: bool, pub horizon_glow: f32, pub top_color: [f32; 3], pub horizon_color: [f32; 3], pub bottom_color: [f32; 3], pub horizon_y: f32, pub horizon_width: f32, } }
SkyParams controls the gradient sky primitive. The engine defaults this on for Journey's game scene and options UI, replacing the older solid background_color path for normal gameplay. The old background color remains as a fallback for explicit overrides such as benchmark scenes.
SkyParams also exposes a lerp method for transitions:
#![allow(unused)] fn main() { impl SkyParams { pub fn lerp(&self, other: &SkyParams, t: f32) -> SkyParams; } }
SkyTransition
#![allow(unused)] fn main() { pub struct SkyTransition { pub current: SkyParams, pub target: SkyParams, pub duration: f32, pub elapsed: f32, } }
A one-shot transition helper that interpolates from current to target sky over duration seconds.
#![allow(unused)] fn main() { let mut t = SkyTransition::new(day_sky, night_sky, 2.0); t.advance(dt); let params = t.lerp(); //* returns interpolated SkyParams at current progress if t.done() { /* transition complete */ } }
SceneParams
#![allow(unused)] fn main() { pub struct SceneParams { pub background_color: [f32; 3], //? Fallback RGB (0.0–1.0) pub sky: SkyParams, pub seed: u32, pub fog_enabled: bool, pub fog_density: f32, pub fog_opacity: f32, pub fog_color: [f32; 3], pub fog_anim_speed: f32, pub time: f32, } }
SceneParams is passed to GameApp::ui() so games can expose debug or options controls. The runtime renders two 32x32 CPU-generated atmosphere textures:
- Sky texture: gradient/horizon data, sampled linearly by the fullscreen shader so the sky blends smoothly.
- Fog texture: Perlin fog overlay with alpha coverage, sampled with nearest filtering so fog keeps the chunky retro look.
The fullscreen background shader composites sky.rgb with fog.rgb using fog.a, then world sprites render on top at the game's internal resolution.
Atmosphere Functions
#![allow(unused)] fn main() { pub fn hex_to_rgb(hex: &str) -> (u8, u8, u8) pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 pub fn draw_gradient(buffer, width, height, top_rgb, bot_rgb) pub fn render_sky_to_buffer(buffer, width, height, params) pub fn render_fog_overlay(buffer, width, height, time, density, opacity, perlin, seed, fog_rgb, anim_speed) pub fn render_fog_to_buffer(buffer, width, height, params, fog_noise_cache) pub fn render_atmosphere_to_buffer(buffer, width, height, params, fog_noise_cache) }
These are called internally by the engine render loop. render_atmosphere_to_buffer remains as a CPU-composited helper for tests and non-GPU callers.
Re-exports
The engine's lib.rs re-exports commonly used types for ergonomic imports:
#![allow(unused)] fn main() { //? Instead of engine::context::Context, just use: use engine::Context; //? All re-exports: pub use audio::{AudioManager, AudioResponse, AudioTrack, UiAudioEvent, load_sound_data}; pub use camera::ScreenShake; pub use context::Context; pub use glam::{Vec2, Vec3, Vec4}; pub use input::{GameAction, InputMap, InputState, Key, MouseBinding}; pub use kira::sound::static_sound::StaticSoundData; pub use math::move_towards; pub use physics::{AABB, BoxVolume, CollisionLayer, SweepResult}; pub use sprite::{BlendMode, Rect, RenderLayer}; pub use BloomSettings; pub use SkyParams; pub use SkyTransition; pub use SceneParams; pub use animation::{AnimationDef, AnimationState}; pub use texture::Texture; pub use texture_manager::TextureHandle; pub use time::FixedTime; //? Re-exported dependencies for direct use by game crates: pub use egui; #[cfg(not(target_arch = "wasm32"))] pub use gilrs; }
Entry Points
#![allow(unused)] fn main() { //? Native: blocks on async GPU init pub fn run<G: GameApp>() //? WASM: non-blocking entry pub fn run_wasm<G: GameApp>() }