Skip to main content

Crate nightshade

Crate nightshade 

Source
Expand description

§Nightshade

A data-oriented game engine written in Rust.

§Getting Started

§1. Add to Cargo.toml

[dependencies]
nightshade = "0.6"

§2. Create Your Game

use nightshade::prelude::*;

struct Game {
    camera: Option<Entity>,
}

impl Default for Game {
    fn default() -> Self {
        Self { camera: None }
    }
}

impl State for Game {
    fn title(&self) -> &str { "My Game" }

    fn initialize(&mut self, world: &mut World) {
        // Graphics setup
        world.resources.graphics.atmosphere = Atmosphere::Sky;
        spawn_sun(world);

        // Camera
        let camera = spawn_camera(world, Vec3::new(5.0, 3.0, 5.0), "Camera".to_string());
        world.resources.active_camera = Some(camera);
        self.camera = Some(camera);

        // Spawn objects
        spawn_cube_at(world, Vec3::new(0.0, 0.5, 0.0));
    }

    fn run_systems(&mut self, world: &mut World) {
        let _dt = world.resources.window.timing.delta_time;
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    nightshade::launch(Game::default())
}

§3. Loading 3D Models

let model_data = include_bytes!("../assets/character.glb");
let result = import_gltf_from_bytes(model_data).unwrap();
let prefab = &result.prefabs[0];
let entity = spawn_prefab_with_animations(world, prefab, &result.animations, Vec3::zeros());

if let Some(player) = world.get_animation_player_mut(entity) {
    player.play(0);
    player.looping = true;
}

§Architecture

§World

The World contains all game state:

  • Entities: Unsigned integer handles (Entity) identifying objects
  • Components: Data attached to entities (transforms, meshes, physics, etc.)
  • Resources: Global singletons (world.resources.window.timing, .input, .graphics, .physics)

§Component Flags

Entities are created with bitflags specifying their components:

let entity = world.spawn_entities(
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | RENDER_MESH,
    1
)[0];
FlagDescription
LOCAL_TRANSFORMPosition/rotation/scale relative to parent
GLOBAL_TRANSFORMWorld-space transform (computed)
LOCAL_TRANSFORM_DIRTYMarks transform for hierarchy update
RENDER_MESHVisible 3D geometry
MATERIAL_REFMaterial reference
CAMERACamera component
PARENT / CHILDRENHierarchy relationships
RIGID_BODYPhysics body
COLLIDERCollision shape
ANIMATION_PLAYERAnimation controller
PARTICLE_EMITTERGPU particles
AUDIO_SOURCESound playback
WATERWater surface/volume
DECALProjected texture

§Resources

// Timing
let dt = world.resources.window.timing.delta_time;
let fps = world.resources.window.timing.frames_per_second;
let uptime_ms = world.resources.window.timing.uptime_milliseconds;

// Input
let keyboard = &world.resources.input.keyboard;
let mouse = &world.resources.input.mouse;

// Graphics
world.resources.graphics.bloom_enabled = true;
world.resources.graphics.atmosphere = Atmosphere::Sky;

// Camera
world.resources.active_camera = Some(camera_entity);

§Render Graph

Rendering uses a pass-based graph. Override State::configure_render_graph for bloom, SSAO, or custom effects. See the render module.

§Troubleshooting

Black Screen

  • Set world.resources.active_camera to a valid camera
  • Ensure camera can see objects (position/orientation)
  • Add lighting: spawn_sun(world)

No Audio (WASM)

  • Browsers require user interaction first
  • Trigger audio from click/key handlers

Physics Falls Through

  • Floor needs RigidBodyComponent::new_static() AND ColliderComponent
  • Check collider dimensions match mesh

Animations Not Playing

  • Use spawn_prefab_with_animations() not spawn_prefab()
  • Call player.play(index) to start

Textures Missing

  • Call world.texture_cache_add_reference(path) before material creation
  • Paths are case-sensitive

Entity Invisible

  • Needs RENDER_MESH flag
  • Needs valid MaterialRef
  • Check transform position

§Features

§Aggregate Features

  • engine (default) - Full engine functionality including asset loading, scene graphs, terrain, file dialogs, and more. This is the standard feature for building games.

  • runtime - Minimal rendering without asset loading. Use with wgpu and egui for lightweight apps that don’t need gltf/image loading.

  • full - Everything in engine plus audio, navmesh, scripting, fbx, etc.

  • wgpu (default) - WebGPU-based rendering with DirectX 12, Metal, Vulkan, and WebGPU.

§Granular Features

These allow fine-grained control over dependencies:

  • core - Foundation: ECS (freecs), math (nalgebra), windowing (winit), time, petgraph.

  • text - 3D text rendering using fontdue. Requires core.

  • assets - Asset loading: gltf, image, half, bincode, serde_json. Requires core.

  • scene_graph - Scene hierarchy system. Requires assets.

  • terrain - Procedural terrain generation using noise. Requires core.

  • file_dialog - Native file dialogs using rfd. Requires core.

  • async_runtime - Tokio async runtime. Requires core. Falls back to pollster if disabled.

  • screenshot - PNG screenshot saving using image. Standalone feature.

§Optional Features

  • egui - Immediate mode GUI. Enables fn ui() on State trait.

  • shell - Developer console with command registration. Requires egui.

  • audio - Audio playback using Kira.

  • physics - 3D physics using Rapier.

  • navmesh - Navigation mesh generation using Recast.

  • scripting - Rhai scripting runtime.

  • picking - Ray-based entity picking. Trimesh picking requires physics.

  • gamepad - Gamepad input using gilrs.

  • fbx - FBX model loading using ufbx. Requires assets.

  • lattice - Lattice deformation system.

  • sdf_sculpt - SDF sculpting system.

§Platform Features

  • openxr - OpenXR VR support.

  • steam - Steamworks integration.

  • webview - Bidirectional IPC for hosting web frontends (Leptos, Yew, etc.). On native (with runtime + egui), provides WebviewContext and serve_embedded_dir. On WASM, provides connect and send for the frontend bridge.

§Profiling Features

  • tracing - Rolling log files and RUST_LOG support.

  • tracy - Real-time profiling with Tracy. Implies tracing.

  • chrome - Chrome tracing output. Implies tracing.

§Plugin Features

  • plugins - Guest-side WASM plugin API.

  • plugin_runtime - WASM plugin hosting via Wasmtime. Requires assets.

  • mosaic - Multi-pane desktop application framework with tile-based layouts, themes, modals, notifications, and built-in viewport widgets. Requires egui.

  • editor - Scene editor infrastructure with gizmos, undo/redo, component inspectors, entity picking, selection management, and scene tree UI. Requires mosaic and picking.

  • mcp - HTTP-based Model Context Protocol server on http://127.0.0.1:3333/mcp. Exposes 50+ tools for AI clients to manipulate the running scene. Native only. Requires async_runtime and behaviors.

  • claude - Background worker for spawning Claude Code CLI as a subprocess and streaming its JSON output. Lets applications embed an AI chat interface. Native only.

§Feature Quick Reference

§Audio (audio feature)

// Load and play a sound
let sound = world.resources.audio.load_sound(include_bytes!("sound.ogg"));
world.resources.audio.play(sound);

// 3D spatial audio
let source = world.spawn_entities(AUDIO_SOURCE | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
world.set_audio_source(source, AudioSource::new(sound).with_spatial(true));
world.set_local_transform(source, LocalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0)));

§Physics (physics feature)

// Dynamic rigid body with collider
let entity = world.spawn_entities(
    RIGID_BODY | COLLIDER | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY,
    1
)[0];
world.set_rigid_body(entity, RigidBodyComponent::new_dynamic());
world.set_collider(entity, ColliderComponent::cuboid(1.0, 1.0, 1.0));
world.set_local_transform(entity, LocalTransform::from_translation(Vec3::new(0.0, 5.0, 0.0)));

// Static floor
let floor = world.spawn_entities(RIGID_BODY | COLLIDER | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
world.set_rigid_body(floor, RigidBodyComponent::new_static());
world.set_collider(floor, ColliderComponent::cuboid(10.0, 0.1, 10.0));

// Step physics each frame
fn run_systems(&mut self, world: &mut World) {
    world.resources.physics.step(world.resources.window.timing.delta_time);
    sync_physics_transforms(world);
}
// Generate navmesh from world geometry
let config = RecastNavMeshConfig::default();
generate_navmesh_recast(world, &config);

// Spawn an agent
let agent = spawn_navmesh_agent(world, Vec3::new(0.0, 0.0, 0.0), 0.5, 2.0);

// Set destination
set_agent_destination(world, agent, Vec3::new(10.0, 0.0, 10.0));
set_agent_speed(world, agent, 3.5);

// Update each frame
fn run_systems(&mut self, world: &mut World) {
    run_navmesh_systems(world, world.resources.window.timing.delta_time);
}

§Entity Picking (picking feature)

// Ray picking (bounding box)
let mouse_pos = world.resources.input.mouse.position;
let ray = PickingRay::from_screen_position(world, mouse_pos);
if let Some(hit) = pick_closest_entity(world, &ray, &PickingOptions::default()) {
    println!("Hit {:?} at distance {}", hit.entity, hit.distance);
}

// GPU picking (pixel-perfect)
world.resources.gpu_picking.request_pick(mouse_pos.x as u32, mouse_pos.y as u32);
// Next frame:
if let Some(result) = world.resources.gpu_picking.take_result() {
    let world_pos = result.world_position;
    let normal = result.world_normal;
}

§Immediate Mode GUI (egui feature)

impl State for Game {
    fn ui(&mut self, world: &mut World, ctx: &egui::Context) {
        egui::Window::new("Debug").show(ctx, |ui| {
            ui.label(format!("FPS: {:.0}", world.resources.window.timing.frames_per_second));
            if ui.button("Spawn Cube").clicked() {
                spawn_cube_at(world, Vec3::zeros());
            }
            ui.add(egui::Slider::new(&mut self.speed, 0.0..=10.0).text("Speed"));
        });
    }
}

§Developer Console (shell feature)

fn initialize(&mut self, world: &mut World) {
    // Register custom command
    world.resources.shell.register_command(
        "spawn",
        "Spawn entities",
        |args, world| {
            let count: usize = args.get(0).and_then(|s| s.parse().ok()).unwrap_or(1);
            for _ in 0..count {
                spawn_cube_at(world, Vec3::new(rand::random(), rand::random(), rand::random()));
            }
            Ok(format!("Spawned {} cubes", count))
        },
    );
}
// Press ` (backtick) to open console, type "spawn 10"

§Procedural Terrain (terrain feature)

let config = TerrainConfig {
    size: 100.0,
    resolution: 128,
    height_scale: 10.0,
    noise: NoiseConfig {
        noise_type: NoiseType::Perlin,
        frequency: 0.02,
        octaves: 4,
        ..Default::default()
    },
};
let terrain = spawn_terrain_with_material(world, &config, material_id);

§Lattice Deformation (lattice feature)

// Create lattice covering a region
let lattice = create_lattice_entity(
    world,
    Vec3::new(-2.0, 0.0, -2.0),  // Min bounds
    Vec3::new(2.0, 4.0, 2.0),    // Max bounds
    [3, 4, 3],                    // Grid dimensions
);

// Bind mesh to lattice
register_entity_for_lattice_deformation(world, mesh_entity, lattice);

// Deform by moving control points
if let Some(lat) = world.get_lattice_mut(lattice) {
    lat.set_displacement(1, 3, 1, Vec3::new(0.5, 0.0, 0.0));
}

// Apply deformations each frame
lattice_deformation_system(world);

§Gamepad Input (gamepad feature)

fn run_systems(&mut self, world: &mut World) {
    if let Some(gamepad) = &world.resources.input.gamepad {
        // Analog sticks
        let move_x = gamepad.left_stick.x;
        let move_y = gamepad.left_stick.y;
        let look_x = gamepad.right_stick.x;
        let look_y = gamepad.right_stick.y;

        // Triggers (0.0 - 1.0)
        let accelerate = gamepad.right_trigger;
        let brake = gamepad.left_trigger;

        // Buttons
        if gamepad.pressed(GamepadButton::South) { /* A/Cross */ }
        if gamepad.just_pressed(GamepadButton::North) { /* Y/Triangle */ }
    }
}

§OpenXR VR (openxr feature)

// Launch in VR mode
fn main() -> Result<(), Box<dyn std::error::Error>> {
    launch_xr(Game::default())
}

// Read VR input
fn run_systems(&mut self, world: &mut World) {
    if let Some(input) = &world.resources.xr.input {
        let head_pos = input.head_position;
        let left_hand = input.left_hand_position();
        let right_trigger = input.right_trigger;

        if input.a_button_pressed() {
            // A button action
        }
    }

    // Enable locomotion
    world.resources.xr.locomotion_enabled = true;
    world.resources.xr.locomotion_speed = 3.0;
}

§Steam Integration (steam feature)

fn initialize(&mut self, world: &mut World) {
    world.resources.steam.initialize().ok();
    world.resources.steam.request_stats();
}

fn run_systems(&mut self, world: &mut World) {
    world.resources.steam.run_callbacks();

    // Unlock achievement
    if self.score >= 1000 {
        world.resources.steam.unlock_achievement("ACH_HIGH_SCORE").ok();
        world.resources.steam.store_stats().ok();
    }

    // Update stat
    world.resources.steam.set_stat_int("total_score", self.score).ok();
}

§Webview (webview feature)

Host a web frontend (Leptos, Yew, etc.) inside a nightshade window with bidirectional IPC.

Native side (use webview with runtime and egui):

use include_dir::{Dir, include_dir};
use nightshade::prelude::*;
use nightshade::webview::{WebviewContext, serve_embedded_dir};

static DIST: Dir = include_dir!("$CARGO_MANIFEST_DIR/site/dist");

struct App {
    port: u16,
    ctx: WebviewContext<FrontendCommand, BackendEvent>,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    launch(App {
        port: serve_embedded_dir(&DIST),
        ctx: WebviewContext::default(),
    })
}

impl State for App {
    fn ui(&mut self, world: &mut World, ctx: &egui::Context) {
        // Handle messages from frontend
        for cmd in self.ctx.drain_messages() {
            match cmd {
                FrontendCommand::Ready => self.ctx.send(BackendEvent::Connected),
                // ...
            }
        }

        // Render webview in a panel
        egui::CentralPanel::default().frame(egui::Frame::NONE).show(ctx, |ui| {
            if let Some(handle) = &world.resources.window.handle {
                self.ctx.ensure_webview(handle.clone(), self.port, ui.available_rect_before_wrap());
            }
        });
    }
}

WASM frontend (use webview with default-features = false):

use nightshade::webview::{connect, send};

fn main() {
    connect(FrontendCommand::Ready, |event: BackendEvent| {
        // Handle events from native app
    });

    // Send commands to native app
    send(&FrontendCommand::DoSomething);
}

§Mosaic Framework (mosaic feature)

Build multi-pane desktop applications with dockable, serializable widgets:

use nightshade::prelude::*;
use nightshade::mosaic::{Mosaic, ViewportWidget, Widget, WidgetContext, WidgetEntry, Pane};

#[derive(Clone, serde::Serialize, serde::Deserialize)]
enum AppWidget {
    Viewport(ViewportWidget),
}

impl Widget for AppWidget {
    fn title(&self) -> String {
        match self {
            AppWidget::Viewport(v) => v.title(),
        }
    }

    fn ui(&mut self, ui: &mut egui::Ui, context: &mut WidgetContext) {
        match self {
            AppWidget::Viewport(v) => v.ui(ui, context),
        }
    }

    fn catalog() -> Vec<WidgetEntry<Self>> {
        vec![WidgetEntry {
            name: "Viewport".to_string(),
            create: || AppWidget::Viewport(ViewportWidget::default()),
        }]
    }
}

struct MyApp {
    mosaic: Mosaic<AppWidget>,
}

impl State for MyApp {
    fn title(&self) -> &str { "My App" }

    fn initialize(&mut self, world: &mut World) {
        self.mosaic = Mosaic::with_panes(vec![
            AppWidget::Viewport(ViewportWidget::default()),
        ]);
    }

    fn ui(&mut self, world: &mut World, ctx: &egui::Context) {
        self.mosaic.show(world, ctx, &mut ());
    }
}

§Debug Lines

// Create debug lines entity
let debug = world.spawn_entities(LINES, 1)[0];

fn run_systems(&mut self, world: &mut World) {
    if let Some(lines) = world.get_lines_mut(self.debug) {
        lines.clear();

        // Draw a line
        lines.push(Line {
            start: Vec3::zeros(),
            end: Vec3::new(1.0, 1.0, 1.0),
            color: Vec4::new(1.0, 0.0, 0.0, 1.0),
        });

        // Draw coordinate axes
        lines.push(Line { start: Vec3::zeros(), end: Vec3::x(), color: Vec4::new(1.0, 0.0, 0.0, 1.0) });
        lines.push(Line { start: Vec3::zeros(), end: Vec3::y(), color: Vec4::new(0.0, 1.0, 0.0, 1.0) });
        lines.push(Line { start: Vec3::zeros(), end: Vec3::z(), color: Vec4::new(0.0, 0.0, 1.0, 1.0) });
    }
}

§Particles

let emitter = world.spawn_entities(
    PARTICLE_EMITTER | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY,
    1
)[0];

world.set_particle_emitter(emitter, ParticleEmitter {
    max_particles: 1000,
    emission_rate: 100.0,
    lifetime: 2.0,
    initial_velocity: Vec3::new(0.0, 5.0, 0.0),
    velocity_randomness: 1.0,
    size: 0.1,
    color: ColorGradient::new(
        Vec4::new(1.0, 0.5, 0.0, 1.0),  // Start: orange
        Vec4::new(1.0, 0.0, 0.0, 0.0),  // End: red, faded
    ),
    shape: EmitterShape::Sphere { radius: 0.5 },
    ..Default::default()
});

world.set_local_transform(emitter, LocalTransform::from_translation(Vec3::new(0.0, 0.0, 0.0)));

// Update each frame
update_particle_emitters(world, world.resources.window.timing.delta_time);

§Sprites

let sprite_entity = world.spawn_entities(SPRITE, 1)[0];
world.set_sprite(sprite_entity, Sprite::new(
    Vec2::new(100.0, 100.0),  // Screen position
    Vec2::new(64.0, 64.0),    // Size
)
    .with_texture(texture_index)
    .with_color([1.0, 1.0, 1.0, 1.0])
    .with_depth(0.0)  // Lower = in front
);

§Water

// Simple water plane
spawn_water_plane_at(world, Vec3::new(0.0, 0.0, 0.0), 100.0);

// Or with full control
let water = world.spawn_entities(WATER | LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
world.set_water(water, Water {
    color: Vec3::new(0.0, 0.3, 0.5),
    opacity: 0.8,
    wave_speed: 1.0,
    wave_amplitude: 0.2,
    ..Default::default()
});

§Decals

let decal = world.spawn_entities(
    DECAL | LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY,
    1
)[0];

world.set_decal(decal, Decal {
    texture_index: world.texture_cache_lookup("decal_texture"),
    size: Vec2::new(2.0, 2.0),
    depth: 1.0,  // Projection depth
    opacity: 1.0,
});

// Position and orient decal (projects along -Y)
world.set_local_transform(decal, LocalTransform {
    translation: hit_position + hit_normal * 0.01,
    rotation: UnitQuaternion::face_towards(&(-hit_normal), &Vec3::x()),
    ..Default::default()
});

§Minimal Example

For a lightweight egui app without asset loading:

nightshade = { default-features = false, features = ["runtime", "wgpu", "egui"] }

§Example

use nightshade::prelude::*;

struct Game;

impl State for Game {
    fn title(&self) -> &str { "My Game" }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    launch(Game)
}

§Frame Lifecycle

Each frame executes in this order:

  1. Process window/input events
  2. Update keyboard/mouse/gamepad state
  3. Calculate delta time
  4. Run egui frame (if enabled)
  5. Call State::run_systems()
  6. Dispatch event bus messages
  7. Process MCP commands (if mcp feature enabled)
  8. Run FrameSchedule (engine systems):
    • Initialize and update audio (if audio enabled)
    • Update camera aspect ratios
    • Step physics simulation (if physics enabled)
    • Run scripts (if scripting enabled)
    • Update animation players, apply to transforms
    • Propagate transform hierarchy
    • Update instanced mesh caches, sync text
    • Reset input state
    • Process deferred commands, cleanup
  9. Execute render graph passes

The frame schedule is a resource at world.resources.frame_schedule. Modify it in State::initialize() to insert, remove, or reorder engine systems. See system_names for the available system name constants.

§Threading Model

The engine runs on a single thread with async GPU operations via wgpu, optional async asset loading (with async_runtime feature), and fixed timestep physics with interpolation for smooth rendering.

§Platform Notes

  • Native (Windows/macOS/Linux): Full feature support with DirectX 12, Metal, or Vulkan.
  • WebAssembly: WebGPU backend. Files via drag-and-drop or HTTP. No WASI plugins. Async initialization.
  • XR (with openxr): Vulkan backend required. Use launch_xr() entry point.

Re-exports§

pub use crate::ecs::camera::systems::*;
pub use crate::ecs::input::systems::*;
pub use crate::ecs::sprite_animator::systems::*;
pub use crate::ecs::transform::systems::*;
pub use crate::ecs::tween::systems::*;

Modules§

ecs
Entity Component System module.
filesystem
multi_window
plugin
prelude
render
Rendering system using WebGPU (wgpu).
run
Application entry point and main loop.
schedule