Cookbook
Quick recipes organized by what you want to accomplish. Each recipe is self-contained and uses real Nightshade API patterns.
I Want To... Move Things
Move a player with WASD
#![allow(unused)] fn main() { fn player_movement(world: &mut World, player: Entity, speed: f32) { let dt = world.resources.window.timing.delta_time; let keyboard = &world.resources.input.keyboard; let mut direction = Vec3::zeros(); if keyboard.is_key_pressed(KeyCode::KeyW) { direction.z -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyS) { direction.z += 1.0; } if keyboard.is_key_pressed(KeyCode::KeyA) { direction.x -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyD) { direction.x += 1.0; } if direction.magnitude() > 0.0 { direction = direction.normalize(); if let Some(transform) = world.core.get_local_transform_mut(player) { transform.translation += direction * speed * dt; } mark_local_transform_dirty(world, player); } } }
Move a player relative to the camera
#![allow(unused)] fn main() { fn camera_relative_movement( world: &mut World, player: Entity, camera: Entity, speed: f32, ) { let dt = world.resources.window.timing.delta_time; let keyboard = &world.resources.input.keyboard; let mut input = Vec2::zeros(); if keyboard.is_key_pressed(KeyCode::KeyW) { input.y -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyS) { input.y += 1.0; } if keyboard.is_key_pressed(KeyCode::KeyA) { input.x -= 1.0; } if keyboard.is_key_pressed(KeyCode::KeyD) { input.x += 1.0; } if input.magnitude() < 0.01 { return; } input = input.normalize(); let Some(camera_transform) = world.core.get_global_transform(camera) else { return }; let forward = camera_transform.forward_vector(); let forward_flat = Vec3::new(forward.x, 0.0, forward.z).normalize(); let right_flat = Vec3::new(forward.z, 0.0, -forward.x).normalize(); let world_direction = forward_flat * -input.y + right_flat * input.x; if let Some(transform) = world.core.get_local_transform_mut(player) { transform.translation += world_direction * speed * dt; let target_yaw = world_direction.x.atan2(world_direction.z); let target_rotation = nalgebra_glm::quat_angle_axis(target_yaw, &Vec3::y()); transform.rotation = nalgebra_glm::quat_slerp( &transform.rotation, &target_rotation, dt * 10.0, ); } mark_local_transform_dirty(world, player); } }
Add jumping with gravity
#![allow(unused)] fn main() { struct JumpState { velocity_y: f32, grounded: bool, } fn handle_jumping( world: &mut World, player: Entity, state: &mut JumpState, jump_force: f32, gravity: f32, ) { let dt = world.resources.window.timing.delta_time; if state.grounded && world.resources.input.keyboard.is_key_pressed(KeyCode::Space) { state.velocity_y = jump_force; state.grounded = false; } if !state.grounded { state.velocity_y -= gravity * dt; } if let Some(transform) = world.core.get_local_transform_mut(player) { transform.translation.y += state.velocity_y * dt; if transform.translation.y <= 0.0 { transform.translation.y = 0.0; state.velocity_y = 0.0; state.grounded = true; } } mark_local_transform_dirty(world, player); } }
Make an object bob up and down
#![allow(unused)] fn main() { fn bob_system(world: &mut World, entity: Entity, time: f32, amplitude: f32, frequency: f32) { if let Some(transform) = world.core.get_local_transform_mut(entity) { transform.translation.y = 1.0 + (time * frequency).sin() * amplitude; } mark_local_transform_dirty(world, entity); } }
Rotate an object continuously
#![allow(unused)] fn main() { fn spin_system(world: &mut World, entity: Entity, time: f32) { if let Some(transform) = world.core.get_local_transform_mut(entity) { transform.rotation = nalgebra_glm::quat_angle_axis(time, &Vec3::y()); } mark_local_transform_dirty(world, entity); } }
I Want To... Set Up Cameras
Make a first-person camera
Horizontal yaw on the player body, vertical pitch on the camera. The camera is parented to the player so it follows automatically:
#![allow(unused)] fn main() { fn setup_fps_camera(world: &mut World, player: Entity) -> Entity { let camera = world.spawn_entities( LOCAL_TRANSFORM | GLOBAL_TRANSFORM | CAMERA | PARENT, 1, )[0]; world.core.set_local_transform(camera, LocalTransform { translation: Vec3::new(0.0, 0.7, 0.0), ..Default::default() }); world.core.set_camera(camera, Camera::default()); world.core.set_parent(camera, Parent(Some(player))); world.resources.active_camera = Some(camera); camera } fn fps_look(world: &mut World, player: Entity, camera: Entity) { let mouse_delta = world.resources.input.mouse.position_delta; let sensitivity = 0.002; if let Some(transform) = world.core.get_local_transform_mut(player) { let yaw = nalgebra_glm::quat_angle_axis(-mouse_delta.x * sensitivity, &Vec3::y()); transform.rotation = yaw * transform.rotation; } mark_local_transform_dirty(world, player); if let Some(transform) = world.core.get_local_transform_mut(camera) { let pitch = nalgebra_glm::quat_angle_axis(-mouse_delta.y * sensitivity, &Vec3::x()); transform.rotation = transform.rotation * pitch; } mark_local_transform_dirty(world, camera); } }
Make a third-person orbit camera
#![allow(unused)] fn main() { struct OrbitCamera { target: Entity, distance: f32, yaw: f32, pitch: f32, } fn orbit_camera_system(world: &mut World, camera: Entity, orbit: &mut OrbitCamera) { let mouse_delta = world.resources.input.mouse.position_delta; let scroll = world.resources.input.mouse.wheel_delta; orbit.yaw -= mouse_delta.x * 0.003; orbit.pitch -= mouse_delta.y * 0.003; orbit.pitch = orbit.pitch.clamp(-1.4, 1.4); orbit.distance = (orbit.distance - scroll.y * 0.5).clamp(2.0, 20.0); let Some(target_transform) = world.core.get_global_transform(orbit.target) else { return }; let target_pos = target_transform.translation() + Vec3::new(0.0, 1.5, 0.0); let offset = Vec3::new( orbit.yaw.sin() * orbit.pitch.cos(), orbit.pitch.sin(), orbit.yaw.cos() * orbit.pitch.cos(), ) * orbit.distance; let camera_pos = target_pos + offset; if let Some(transform) = world.core.get_local_transform_mut(camera) { transform.translation = camera_pos; let direction = (target_pos - camera_pos).normalize(); let pitch = (-direction.y).asin(); let yaw = direction.x.atan2(direction.z); transform.rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y()) * nalgebra_glm::quat_angle_axis(pitch, &Vec3::x()); } mark_local_transform_dirty(world, camera); } }
Make a smooth follow camera
#![allow(unused)] fn main() { fn follow_camera( world: &mut World, target: Entity, camera: Entity, offset: Vec3, smoothness: f32, ) { let dt = world.resources.window.timing.delta_time; let Some(target_transform) = world.core.get_global_transform(target) else { return }; let target_pos = target_transform.translation() + offset; if let Some(cam_transform) = world.core.get_local_transform_mut(camera) { cam_transform.translation = nalgebra_glm::lerp( &cam_transform.translation, &target_pos, dt * smoothness, ); let look_at = target_transform.translation(); let direction = (look_at - cam_transform.translation).normalize(); let pitch = (-direction.y).asin(); let yaw = direction.x.atan2(direction.z); cam_transform.rotation = nalgebra_glm::quat_angle_axis(yaw, &Vec3::y()) * nalgebra_glm::quat_angle_axis(pitch, &Vec3::x()); } mark_local_transform_dirty(world, camera); } }
I Want To... Spawn Objects
Spawn a colored cube
#![allow(unused)] fn main() { fn spawn_colored_cube(world: &mut World, position: Vec3, color: [f32; 4]) -> Entity { let cube = spawn_cube_at(world, position); material_registry_insert( &mut world.resources.material_registry, format!("cube_{}", cube.id), Material { base_color: color, ..Default::default() }, ); let material_name = format!("cube_{}", cube.id); if let Some(&index) = world.resources.material_registry.registry.name_to_index.get(&material_name) { world.resources.material_registry.registry.add_reference(index); } world.core.set_material_ref(cube, MaterialRef::new(material_name)); cube } }
Spawn objects at random positions
#![allow(unused)] fn main() { fn random_position_in_box(center: Vec3, half_extents: Vec3) -> Vec3 { Vec3::new( center.x + (rand::random::<f32>() - 0.5) * 2.0 * half_extents.x, center.y + (rand::random::<f32>() - 0.5) * 2.0 * half_extents.y, center.z + (rand::random::<f32>() - 0.5) * 2.0 * half_extents.z, ) } fn random_position_on_circle(center: Vec3, radius: f32) -> Vec3 { let angle = rand::random::<f32>() * std::f32::consts::TAU; Vec3::new( center.x + angle.cos() * radius, center.y, center.z + angle.sin() * radius, ) } }
Spawn a physics object that falls
#![allow(unused)] fn main() { use nightshade::ecs::physics::commands::spawn_dynamic_physics_cube_with_material; fn spawn_physics_cube(world: &mut World, position: Vec3) -> Entity { spawn_dynamic_physics_cube_with_material( world, position, Vec3::new(1.0, 1.0, 1.0), 1.0, Material { base_color: [0.6, 0.4, 0.2, 1.0], ..Default::default() }, ) } }
Spawn a wave of enemies at intervals
#![allow(unused)] fn main() { struct WaveSpawner { wave: u32, enemies_remaining: u32, spawn_timer: f32, spawn_interval: f32, } impl WaveSpawner { fn update(&mut self, world: &mut World, dt: f32) { if self.enemies_remaining == 0 { self.wave += 1; self.enemies_remaining = 5 + self.wave * 2; self.spawn_interval = (2.0 - self.wave as f32 * 0.1).max(0.3); return; } self.spawn_timer -= dt; if self.spawn_timer <= 0.0 { let position = random_position_on_circle(Vec3::zeros(), 20.0); spawn_cube_at(world, position); self.enemies_remaining -= 1; self.spawn_timer = self.spawn_interval; } } } }
Load a 3D model
#![allow(unused)] fn main() { use nightshade::ecs::prefab::commands::gltf_import::import_gltf_from_path; fn initialize(&mut self, world: &mut World) { let result = import_gltf_from_path(std::path::Path::new("assets/models/character.glb")) .expect("Failed to load model"); if let Some(prefab) = result.prefabs.first() { let root = spawn_prefab_with_animations(world, prefab, &result.animations, Vec3::zeros()); world.core.set_local_transform(root, LocalTransform { translation: Vec3::new(0.0, 0.0, 0.0), scale: Vec3::new(1.0, 1.0, 1.0), ..Default::default() }); } } }
I Want To... Use Physics
Apply an explosion force
#![allow(unused)] fn main() { fn explosion(world: &mut World, center: Vec3, radius: f32, force: f32) { for entity in world.core.query_entities(RIGID_BODY | GLOBAL_TRANSFORM) { let Some(transform) = world.core.get_global_transform(entity) else { continue }; let to_entity = transform.translation() - center; let distance = to_entity.magnitude(); if distance < radius && distance > 0.1 { let falloff = 1.0 - (distance / radius); let impulse = to_entity.normalize() * force * falloff; if let Some(body) = world.core.get_rigid_body_mut(entity) { body.linvel = [ body.linvel[0] + impulse.x, body.linvel[1] + impulse.y, body.linvel[2] + impulse.z, ]; } } } } }
Pick entity from the camera
#![allow(unused)] fn main() { fn shoot_from_camera(world: &mut World) { let (width, height) = world.resources.window.cached_viewport_size.unwrap_or((800, 600)); let screen_center = Vec2::new(width as f32 / 2.0, height as f32 / 2.0); if let Some(hit) = pick_closest_entity_trimesh(world, screen_center) { let hit_position = hit.world_position; let hit_entity = hit.entity; let hit_distance = hit.distance; } } }
Grab and throw objects
#![allow(unused)] fn main() { struct GrabState { entity: Option<Entity>, distance: f32, } fn grab_object(world: &mut World, state: &mut GrabState) { let Some(camera) = world.resources.active_camera else { return }; let Some(transform) = world.core.get_global_transform(camera) else { return }; let origin = transform.translation(); let direction = transform.forward_vector(); for entity in world.core.query_entities(RIGID_BODY | GLOBAL_TRANSFORM) { let Some(entity_transform) = world.core.get_global_transform(entity) else { continue }; let to_entity = entity_transform.translation() - origin; let distance = to_entity.magnitude(); let dot = direction.dot(&to_entity.normalize()); if distance < 20.0 && dot > 0.95 { state.entity = Some(entity); state.distance = distance; break; } } } fn update_held_object(world: &mut World, state: &GrabState) { let Some(entity) = state.entity else { return }; let Some(camera) = world.resources.active_camera else { return }; let Some(camera_transform) = world.core.get_global_transform(camera) else { return }; let target = camera_transform.translation() + camera_transform.forward_vector() * state.distance; if let Some(transform) = world.core.get_local_transform(entity) { let to_target = target - transform.translation; if let Some(body) = world.core.get_rigid_body_mut(entity) { body.linvel = [to_target.x * 20.0, to_target.y * 20.0, to_target.z * 20.0]; } } } fn throw_object(world: &mut World, state: &mut GrabState) { if let Some(entity) = state.entity.take() { let Some(camera) = world.resources.active_camera else { return }; let Some(transform) = world.core.get_global_transform(camera) else { return }; let direction = transform.forward_vector(); if let Some(body) = world.core.get_rigid_body_mut(entity) { body.linvel = [direction.x * 20.0, direction.y * 20.0, direction.z * 20.0]; } } } }
I Want To... Create Materials
Make a glowing emissive material
#![allow(unused)] fn main() { let neon = Material { base_color: [0.2, 0.8, 1.0, 1.0], emissive_factor: [0.2, 0.8, 1.0], emissive_strength: 10.0, roughness: 0.8, ..Default::default() }; }
Make glass
#![allow(unused)] fn main() { let glass = Material { base_color: [0.95, 0.95, 1.0, 1.0], roughness: 0.05, metallic: 0.0, transmission_factor: 0.95, ior: 1.5, ..Default::default() }; }
Make a metallic surface
#![allow(unused)] fn main() { let gold = Material { base_color: [1.0, 0.84, 0.0, 1.0], roughness: 0.3, metallic: 1.0, ..Default::default() }; }
Make a transparent ghost-like material
#![allow(unused)] fn main() { let ghost = Material { base_color: [0.9, 0.95, 1.0, 0.3], alpha_mode: AlphaMode::Blend, roughness: 0.1, ..Default::default() }; }
I Want To... Show UI
Display an FPS counter
#![allow(unused)] fn main() { struct FpsCounter { samples: Vec<f32>, text_entity: Entity, } impl FpsCounter { fn update(&mut self, world: &mut World) { let fps = world.resources.window.timing.frames_per_second; self.samples.push(fps); if self.samples.len() > 60 { self.samples.remove(0); } let avg: f32 = self.samples.iter().sum::<f32>() / self.samples.len() as f32; if let Some(text) = world.core.get_text_mut(self.text_entity) { world.resources.text_cache.set_text(text.text_index, &format!("FPS: {:.0}", avg)); text.dirty = true; } } } }
Display a health bar as HUD text
#![allow(unused)] fn main() { fn update_health_bar(world: &mut World, text_entity: Entity, current: f32, max: f32) { let bar_length = 20; let filled = ((current / max) * bar_length as f32) as usize; let bar = format!( "[{}{}] {}/{}", "|".repeat(filled.min(bar_length)), ".".repeat(bar_length - filled.min(bar_length)), current as u32, max as u32, ); if let Some(text) = world.core.get_text_mut(text_entity) { world.resources.text_cache.set_text(text.text_index, &bar); text.dirty = true; } } }
Show a scoreboard with egui
#![allow(unused)] fn main() { fn ui(&mut self, _world: &mut World, ctx: &egui::Context) { egui::Window::new("Score") .anchor(egui::Align2::CENTER_TOP, [0.0, 10.0]) .resizable(false) .collapsible(false) .title_bar(false) .show(ctx, |ui| { ui.heading(format!("{} - {}", self.left_score, self.right_score)); }); } }
I Want To... Handle Game States
Pause the game
#![allow(unused)] fn main() { fn on_keyboard_input(&mut self, world: &mut World, key: KeyCode, state: ElementState) { if state == ElementState::Pressed && key == KeyCode::Escape { self.paused = !self.paused; world.set_cursor_visible(self.paused); world.set_cursor_locked(!self.paused); } } fn run_systems(&mut self, world: &mut World) { if self.paused { return; } self.update_game_logic(world); } }
Build a state machine for player actions
#![allow(unused)] fn main() { #[derive(Clone, Copy, PartialEq, Eq)] enum PlayerAction { Idle, Walking, Running, Attacking, Dodging, } struct ActionState { current: PlayerAction, timer: f32, } impl ActionState { fn transition(&mut self, new_state: PlayerAction) { if self.current != new_state { self.current = new_state; self.timer = 0.0; } } fn update(&mut self, dt: f32) { self.timer += dt; } fn can_interrupt(&self) -> bool { match self.current { PlayerAction::Attacking => self.timer > 0.5, PlayerAction::Dodging => self.timer > 0.3, _ => true, } } } }
I Want To... Use Timers
Cooldown timer
#![allow(unused)] fn main() { struct Cooldown { duration: f32, remaining: f32, } impl Cooldown { fn new(duration: f32) -> Self { Self { duration, remaining: 0.0 } } fn update(&mut self, dt: f32) { self.remaining = (self.remaining - dt).max(0.0); } fn ready(&self) -> bool { self.remaining <= 0.0 } fn trigger(&mut self) { self.remaining = self.duration; } fn progress(&self) -> f32 { 1.0 - (self.remaining / self.duration) } } }
Repeating timer
#![allow(unused)] fn main() { struct RepeatingTimer { interval: f32, elapsed: f32, } impl RepeatingTimer { fn new(interval: f32) -> Self { Self { interval, elapsed: 0.0 } } fn tick(&mut self, dt: f32) -> bool { self.elapsed += dt; if self.elapsed >= self.interval { self.elapsed -= self.interval; true } else { false } } } }
I Want To... Debug Things
Draw wireframe collision boxes
#![allow(unused)] fn main() { fn debug_draw_boxes( world: &mut World, lines_entity: Entity, entities: &[Entity], half_extents: Vec3, ) { let mut lines = vec![]; for &entity in entities { let Some(transform) = world.core.get_global_transform(entity) else { continue }; let pos = transform.translation(); let color = Vec4::new(0.0, 1.0, 0.0, 1.0); let half = half_extents; let corners = [ pos + Vec3::new(-half.x, -half.y, -half.z), pos + Vec3::new( half.x, -half.y, -half.z), pos + Vec3::new( half.x, -half.y, half.z), pos + Vec3::new(-half.x, -half.y, half.z), pos + Vec3::new(-half.x, half.y, -half.z), pos + Vec3::new( half.x, half.y, -half.z), pos + Vec3::new( half.x, half.y, half.z), pos + Vec3::new(-half.x, half.y, half.z), ]; let edges = [ (0,1), (1,2), (2,3), (3,0), (4,5), (5,6), (6,7), (7,4), (0,4), (1,5), (2,6), (3,7), ]; for (a, b) in edges { lines.push(Line { start: corners[a], end: corners[b], color }); } } world.core.set_lines(lines_entity, Lines { lines, version: 0 }); } }
I Want To... Save and Load
Save game state to JSON
#![allow(unused)] fn main() { use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] struct SaveData { player_position: [f32; 3], player_health: f32, score: u32, level: u32, } fn save_game(data: &SaveData, path: &str) -> std::io::Result<()> { let json = serde_json::to_string_pretty(data)?; std::fs::write(path, json)?; Ok(()) } fn load_game(path: &str) -> std::io::Result<SaveData> { let json = std::fs::read_to_string(path)?; let data: SaveData = serde_json::from_str(&json)?; Ok(data) } }
I Want To... Play Audio
Footstep sounds while moving
#![allow(unused)] fn main() { struct FootstepSystem { timer: f32, interval: f32, sounds: Vec<String>, last_index: usize, audio_entity: Entity, } impl FootstepSystem { fn update(&mut self, world: &mut World, is_moving: bool, is_running: bool, dt: f32) { if !is_moving { self.timer = 0.0; return; } let interval = if is_running { self.interval * 0.6 } else { self.interval }; self.timer += dt; if self.timer >= interval { self.timer = 0.0; let mut index = rand::random::<usize>() % self.sounds.len(); if index == self.last_index && self.sounds.len() > 1 { index = (index + 1) % self.sounds.len(); } self.last_index = index; if let Some(audio) = world.core.get_audio_source_mut(self.audio_entity) { audio.audio_ref = Some(self.sounds[index].clone()); audio.playing = true; } } } } }
I Want To... Pool Entities
Reuse entities instead of spawning/despawning
#![allow(unused)] fn main() { struct EntityPool { available: Vec<Entity>, active: Vec<Entity>, spawn_fn: fn(&mut World) -> Entity, } impl EntityPool { fn new(world: &mut World, initial_size: usize, spawn_fn: fn(&mut World) -> Entity) -> Self { let mut available = Vec::with_capacity(initial_size); for _ in 0..initial_size { let entity = spawn_fn(world); world.core.set_visibility(entity, Visibility { visible: false }); available.push(entity); } Self { available, active: Vec::new(), spawn_fn } } fn acquire(&mut self, world: &mut World) -> Entity { let entity = self.available.pop().unwrap_or_else(|| (self.spawn_fn)(world)); world.core.set_visibility(entity, Visibility { visible: true }); self.active.push(entity); entity } fn release(&mut self, world: &mut World, entity: Entity) { if let Some(index) = self.active.iter().position(|&entity_in_pool| entity_in_pool == entity) { self.active.swap_remove(index); world.core.set_visibility(entity, Visibility { visible: false }); self.available.push(entity); } } } }
I Want To... Attach Things to Other Things
Parent an object to another entity
#![allow(unused)] fn main() { world.core.set_parent(child, Parent(Some(parent))); }
The child's LocalTransform becomes relative to the parent. The engine computes the GlobalTransform automatically via the transform hierarchy.
Attach a weapon to a camera
#![allow(unused)] fn main() { fn attach_weapon_to_camera(world: &mut World, camera: Entity) -> Entity { let weapon = spawn_cube_at(world, Vec3::zeros()); world.core.set_local_transform(weapon, LocalTransform { translation: Vec3::new(0.3, -0.2, -0.5), rotation: nalgebra_glm::quat_angle_axis(std::f32::consts::PI, &Vec3::y()), scale: Vec3::new(0.05, 0.05, 0.3), }); set_material_with_textures(world, weapon, Material { base_color: [0.2, 0.2, 0.2, 1.0], metallic: 0.9, ..Default::default() }); world.core.set_parent(weapon, Parent(Some(camera))); weapon } }
Add weapon sway from mouse movement
#![allow(unused)] fn main() { fn weapon_sway(world: &mut World, weapon: Entity, rest_x: f32, rest_y: f32) { let dt = world.resources.window.timing.delta_time; let mouse_delta = world.resources.input.mouse.position_delta; if let Some(transform) = world.core.get_local_transform_mut(weapon) { let target_x = rest_x - mouse_delta.x * 0.001; let target_y = rest_y - mouse_delta.y * 0.001; transform.translation.x += (target_x - transform.translation.x) * dt * 10.0; transform.translation.y += (target_y - transform.translation.y) * dt * 10.0; } mark_local_transform_dirty(world, weapon); } }