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.57"

§2. Create Your Game

Programs assemble from plugins: the engine capabilities are default plugins, and the game is one more plugin added in the builder.

use nightshade::prelude::*;

#[derive(Default)]
struct MyGame {
    camera: Option<Entity>,
}

struct MyGamePlugin;

impl Plugin for MyGamePlugin {
    fn build(&self, app: &mut App) {
        app.world.res_mut::<nightshade::platform::window::Window>().title = "My Game".to_string();
        app.insert_resource(MyGame::default());
        app.add_system(Stage::Startup, initialize);
        app.add_systems(Stage::Update, (tick, spawn_on_space));
    }
}

fn initialize(game: &mut MyGame, world: &mut World) {
    world.res_mut::<crate::render::config::RenderSettings>().atmosphere = Atmosphere::Sky;
    spawn_sun(world);

    let camera = spawn_camera(world, Vec3::new(5.0, 3.0, 5.0), "Camera".to_string());
    world.res_mut::<crate::ecs::camera::resources::ActiveCamera>().0 = Some(camera);
    game.camera = Some(camera);

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

// Systems take resources as `Res<T>` / `ResMut<T>` parameters, resolved out
// of the resource map. The game state comes first and a trailing `&mut World`
// stays free for spawning, queries, and anything the parameters don't name.
fn tick(game: &mut MyGame, input: Res<Input>, world: &mut World) {
    if input.keyboard.just_pressed(KeyCode::Space) {
        spawn_cube_at(world, Vec3::new(0.0, 5.0, 0.0));
    }
}

// A system that only needs resources drops `&mut World` entirely.
fn spawn_on_space(mut settings: ResMut<crate::render::config::RenderSettings>, input: Res<Input>) {
    settings.bloom_enabled = input.keyboard.is_key_pressed(KeyCode::KeyB);
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugin(MyGamePlugin)
        .run()
}

§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_mut::<crate::ecs::animation::components::AnimationPlayer>(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 read as Res<T> / ResMut<T> system parameters (for example Input, Window, RenderSettings), or through world.res::<T>() inside a &mut World system

§Component Flags

Entities are created with bitflags specifying their components:

let entity = spawn_entities(world,
    LOCAL_TRANSFORM | GLOBAL_TRANSFORM | RENDER_MESH,
    1
)[0];
FlagDescription
LOCAL_TRANSFORMPosition/rotation/scale relative to parent
GLOBAL_TRANSFORMWorld-space transform (computed)
RENDER_MESHVisible 3D geometry
MATERIAL_REFMaterial reference
CAMERACamera component
PARENTHierarchy parent link
ANIMATION_PLAYERAnimation controller
PARTICLE_EMITTERGPU particles
CLOTHGPU cloth sheet
DECALProjected texture

§Resources

Systems reach resources as Res<T> / ResMut<T> parameters:

fn example(mut settings: ResMut<crate::render::config::RenderSettings>, input: Res<Input>) {
    settings.bloom_enabled = true;
    settings.atmosphere = Atmosphere::Sky;

    let keyboard = &input.keyboard;
    let mouse = &input.mouse;
}

The same resources are reachable through &mut World for the ad-hoc access a parameter list cannot name (one field of a resource, a resource held next to a query, or a helper deep in a call graph):

let dt = world.res::<crate::ecs::time::Time>().delta_time;
let fps = world.res::<crate::ecs::time::Time>().frames_per_second;
let uptime_ms = world.res::<crate::ecs::time::Time>().uptime_milliseconds;
world.res_mut::<crate::ecs::camera::resources::ActiveCamera>().0 = 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.res::<crate::ecs::camera::resources::ActiveCamera>().0 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

  • Textures acquired through materials are released on despawn; keep hand-loaded textures alive with texture_cache_protect(world.res_mut::<crate::render::wgpu::texture_cache::TextureCache>(), name)
  • Names 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, file dialogs, and more. This is the standard feature for building games.

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

  • full - Everything in engine plus audio, physics, gamepad, navmesh.

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

§Granular Features

These allow fine-grained control over dependencies:

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

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

  • scene_graph - Scene hierarchy system. Requires assets.

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

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

§Optional Features

  • egui - Opt-in immediate-mode UI. Add EguiPlugin, then draw from any system with egui_context(world). Composites over the scene and retained UI.

  • shell - Developer console with command registration, rendered via the retained UI.

  • audio - Audio playback using Kira. Add AudioPlugin to open the device and install the audio systems.

  • physics - 3D physics using Rapier.

  • navmesh - Navigation mesh generation using Recast.

  • grass - GPU-driven procedural grass rendering.

  • terrain - GPU-driven clipmap terrain with procedural height generation. Implies grass.

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

  • gamepad - Gamepad input using gilrs. Add GamepadPlugin to poll devices.

§Profiling Features

  • tracing - Rolling log files and RUST_LOG support.

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

  • chrome - Chrome tracing output. Implies tracing.

§Feature Quick Reference

§Audio (audio feature)

// Decode and register a sound by name
let data = load_sound_from_bytes(include_bytes!("sound.ogg")).unwrap();
audio_engine_load_sound(world.plugin_resource_mut::<AudioEngine>(), "shot", data);

// 3D spatial audio: play it through an AudioSource component
let source = spawn_entities(world, LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
world.set(source, AudioSource::new("shot").with_spatial(true).playing());
world.set(source, LocalTransform::from_translation(Vec3::new(5.0, 0.0, 0.0)));

§Physics (physics feature)

// Dynamic rigid body with collider. The physics components live in the
// physics plugin's own member world, so they attach through `world.set`
// rather than a core spawn mask.
let entity = spawn_entities(world, LOCAL_TRANSFORM | GLOBAL_TRANSFORM, 1)[0];
world.set(entity, RigidBodyComponent::new_dynamic());
world.set(entity, ColliderComponent::cuboid(1.0, 1.0, 1.0));
world.set(entity, LocalTransform::from_translation(Vec3::new(0.0, 5.0, 0.0)));

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

// Step physics each frame
fn run_systems(&mut self, world: &mut World) {
    let delta_time = world.res::<crate::ecs::time::Time>().delta_time;
    physics_world_step(world.plugin_resource_mut::<PhysicsWorld>(), 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.res::<crate::ecs::time::Time>().delta_time);
}

§Entity Picking (picking feature)

// Bounding-box picking under the cursor
let mouse_pos = world.res::<crate::platform::input::resources::Input>().mouse.position;
if let Some(hit) = pick_closest_entity(world, mouse_pos) {
    println!("Hit {:?} at distance {}", hit.entity, hit.distance);
}

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

§Developer Console (shell feature)

use nightshade::shell::{Command, ShellState};

fn spawn_cubes(args: &[&str], world: &mut World, _ctx: &mut ()) -> String {
    let count: usize = args.first().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()));
    }
    format!("Spawned {} cubes", count)
}

fn initialize(shell: &mut ShellState<()>) {
    shell.register_builtin_commands();
    shell.registry.register(Command {
        name: "spawn",
        description: "Spawn random cubes",
        usage: "spawn [count]",
        execute: spawn_cubes,
    });
}
// Press Alt+C to open console, type "spawn 10"

§Immediate Mode UI (egui feature)

Opt in with the egui feature and EguiPlugin (or set world.plugin_resource_mut::<EguiState>().enabled). The pass opens before the app stages each frame, so draw from any system by pulling the frame’s context with egui_context. The tessellated output composites over the scene and the retained UI.

impl Plugin for DebugUiPlugin {
    fn build(&self, app: &mut App) {
        app.world.plugin_resource_mut::<EguiState>().enabled = true;
        app.add_system(Stage::Update, |world: &mut World| {
            if let Some(ctx) = egui_context(world) {
                egui::Window::new("Debug").show(&ctx, |ui| {
                    ui.label(format!("FPS: {:.0}", world.res::<crate::ecs::time::Time>().frames_per_second));
                    if ui.button("Spawn Cube").clicked() {
                        spawn_cube_at(world, Vec3::zeros());
                    }
                });
            }
        });
    }
}

§Gamepad Input (gamepad feature)

Add GamepadPlugin so the engine polls pads at the top of every frame.

fn tick(world: &mut World) {
    let gamepad = world.res::<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 */ }
    }
}

§Debug Lines

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

fn tick(game: &mut MyGame, world: &mut World) {
    if let Some(lines) = world.get_mut::<crate::ecs::lines::components::Lines>(game.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 = spawn_entities(world,
    PARTICLE_EMITTER | LOCAL_TRANSFORM | GLOBAL_TRANSFORM,
    1
)[0];

world.set(emitter, ParticleEmitter {
    spawn_rate: 100.0,
    particle_lifetime_min: 1.5,
    particle_lifetime_max: 2.0,
    size_start: 0.2,
    size_end: 0.0,
    color_gradient: ColorGradient::fire(),
    shape: EmitterShape::Sphere { radius: 0.5 },
    ..Default::default()
});

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

// Runs automatically via frame schedule

§Decals

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

world.set(decal, Decal {
    texture: Some("decal_texture".to_string()),
    size: Vec2::new(2.0, 2.0),
    depth: 1.0,  // projection depth
    ..Default::default()
});

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

§Minimal Example

For a lightweight app without asset loading:

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

§Example

use nightshade::prelude::*;

struct GamePlugin;

impl Plugin for GamePlugin {
    fn build(&self, app: &mut App) {
        app.world.res_mut::<nightshade::platform::window::Window>().title = "My Game".to_string();
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugin(GamePlugin)
        .run()
}

§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 the app’s per-frame stages, in order:
    • Game stages first, update, late_update
    • frame_start: event bus swap, audio (when AudioPlugin is composed)
    • frame_update: simulation (physics, animation, tweens, particles, cloth, vfx)
    • frame_post_update: transform propagation, instanced mesh caches, retained UI
    • render: the render sync systems (when RenderPlugin is composed)
    • last: input reset, deferred commands, cleanup
  5. Execute render graph passes

The engine’s per-frame systems live in the same Stages as game logic: they are registered by App::new and the capability plugins, after the game stages, so they run every frame through the one scheduler. Within a stage, systems run in registration order: the core systems land first, then each plugin’s in composition order. Register your own with App::add_system.

§Threading Model

The engine runs on a single thread with async GPU operations via wgpu 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.

Re-exports§

pub use nightshade_paint as paint;
pub use nightshade_platform as platform;
pub use nightshade_renderer as render;
pub use nightshade_text as text;
pub use nightshade_ui as ui;
pub use hearsay as networking;
pub use crate::ecs::camera::systems::*;
pub use crate::ecs::input::systems::*;
pub use crate::ecs::transform::systems::*;

Modules§

app
The plugin-composed application builder.
assets
Assets: what a program loads from disk and what it writes back out.
default_plugins
The standard plugin bundle composed above the App core and the capability plugins it names.
ecs
Entity Component System module.
egui
Immediate-mode egui overlay integration, enabled by the egui feature.
filesystem
plugins
The engine’s capability plugins, each a self-contained unit: the plugin struct plus the systems, components, and resources its feature owns. They are shaped so any of them could stand alone as a crate, and are built into this crate behind feature flags on purpose: cargo features decide what code exists, composing the plugin is what activates it.
prelude
Common re-exports for application code.
render_driver
The engine-side render driver: everything that reads or writes the ECS World around a frame. Before the frame it polls readbacks, drains the loading pipeline, generates text meshes, and composes the RenderInputs snapshot; after the frame it restores the taken state and applies the frame’s outputs. The frame itself never sees the World: once the inputs are composed, rendering runs against them alone, so a mid-frame read of a taken resource is unrepresentable rather than a latent bug.
run
Application entry point and main loop.
schedule
shell
Developer console for runtime debugging and commands.
state
Application state trait and render-graph resource handles.
states
App-builder sugar over nightshade_ecs’s state machine (the state feature). The machinery lives in nightshade_ecs::state; this wires it onto the App builder and the Stage::First transition step, and adapts current_state to the engine World so call sites name only the state type.
user_interface
What the interface layer reports to the rest of the engine each frame.
viewport
Viewport and camera-tile render state. A leaf resource: it names only the renderer’s ViewportRect config and engine entities, so domains, plugins, and the render driver can all read it without depending on the driver or pulling the renderer into nightshade-platform.