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];| Flag | Description |
|---|---|
LOCAL_TRANSFORM | Position/rotation/scale relative to parent |
GLOBAL_TRANSFORM | World-space transform (computed) |
LOCAL_TRANSFORM_DIRTY | Marks transform for hierarchy update |
RENDER_MESH | Visible 3D geometry |
MATERIAL_REF | Material reference |
CAMERA | Camera component |
PARENT / CHILDREN | Hierarchy relationships |
RIGID_BODY | Physics body |
COLLIDER | Collision shape |
ANIMATION_PLAYER | Animation controller |
PARTICLE_EMITTER | GPU particles |
AUDIO_SOURCE | Sound playback |
WATER | Water surface/volume |
DECAL | Projected 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_camerato 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()ANDColliderComponent - Check collider dimensions match mesh
Animations Not Playing
- Use
spawn_prefab_with_animations()notspawn_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_MESHflag - 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 withwgpuandeguifor lightweight apps that don’t need gltf/image loading. -
full- Everything inengineplusaudio,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. Requirescore. -
assets- Asset loading: gltf, image, half, bincode, serde_json. Requirescore. -
scene_graph- Scene hierarchy system. Requiresassets. -
terrain- Procedural terrain generation using noise. Requirescore. -
file_dialog- Native file dialogs using rfd. Requirescore. -
async_runtime- Tokio async runtime. Requirescore. Falls back to pollster if disabled. -
screenshot- PNG screenshot saving using image. Standalone feature.
§Optional Features
-
egui- Immediate mode GUI. Enablesfn ui()on State trait. -
shell- Developer console with command registration. Requiresegui. -
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 requiresphysics. -
gamepad- Gamepad input using gilrs. -
fbx- FBX model loading using ufbx. Requiresassets. -
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 (withruntime+egui), providesWebviewContextandserve_embedded_dir. On WASM, providesconnectandsendfor the frontend bridge.
§Profiling Features
-
tracing- Rolling log files andRUST_LOGsupport. -
tracy- Real-time profiling with Tracy. Impliestracing. -
chrome- Chrome tracing output. Impliestracing.
§Plugin Features
-
plugins- Guest-side WASM plugin API. -
plugin_runtime- WASM plugin hosting via Wasmtime. Requiresassets. -
mosaic- Multi-pane desktop application framework with tile-based layouts, themes, modals, notifications, and built-in viewport widgets. Requiresegui. -
editor- Scene editor infrastructure with gizmos, undo/redo, component inspectors, entity picking, selection management, and scene tree UI. Requiresmosaicandpicking. -
mcp- HTTP-based Model Context Protocol server onhttp://127.0.0.1:3333/mcp. Exposes 50+ tools for AI clients to manipulate the running scene. Native only. Requiresasync_runtimeandbehaviors. -
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);
}§Navigation Mesh (navmesh feature)
// 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:
- Process window/input events
- Update keyboard/mouse/gamepad state
- Calculate delta time
- Run egui frame (if enabled)
- Call
State::run_systems() - Dispatch event bus messages
- Process MCP commands (if
mcpfeature enabled) - Run
FrameSchedule(engine systems):- Initialize and update audio (if
audioenabled) - Update camera aspect ratios
- Step physics simulation (if
physicsenabled) - Run scripts (if
scriptingenabled) - Update animation players, apply to transforms
- Propagate transform hierarchy
- Update instanced mesh caches, sync text
- Reset input state
- Process deferred commands, cleanup
- Initialize and update audio (if
- 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. Uselaunch_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