Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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);
}
}