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

Recipes grouped by the thing you want to do. Each one stands on its own 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

Yaw on the player body, pitch on the camera. Parenting the camera to the player makes the camera follow without an extra system.

#![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;
    }
}
}

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 and despawning

Spawning and despawning has overhead. Pre-spawn a set of entities, hide them, and reuse them as needed.

#![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 is now expressed relative to the parent. The engine computes the GlobalTransform through 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);
}
}