The State Trait
The State trait is the primary interface between your game and the Nightshade engine. Every game implements this trait to define its behavior.
Trait Definition
#![allow(unused)] fn main() { pub trait State { fn title(&self) -> &str { "Nightshade" } fn log_config(&self) -> LogConfig { LogConfig::default() } fn icon_bytes(&self) -> Option<&'static [u8]> { /* default: built-in icon */ } fn initialize(&mut self, _world: &mut World) {} fn next_state(&mut self, _world: &mut World) -> Option<Box<dyn State>> { None } fn configure_render_graph( &mut self, graph: &mut RenderGraph<World>, device: &wgpu::Device, surface_format: wgpu::TextureFormat, resources: RenderResources, ) { /* default: bloom + post-processing + swapchain blit */ } fn ui(&mut self, _world: &mut World, _ui_context: &egui::Context) {} fn secondary_ui(&mut self, _world: &mut World, _window_index: usize, _ui_context: &egui::Context) {} fn run_systems(&mut self, _world: &mut World) {} fn pre_render(&mut self, _renderer: &mut dyn Render, _world: &mut World) {} fn update_render_graph(&mut self, _graph: &mut RenderGraph<World>, _world: &World) {} fn handle_event(&mut self, _world: &mut World, _message: &Message) {} fn on_keyboard_input(&mut self, _world: &mut World, _key_code: KeyCode, _key_state: ElementState) {} fn on_dropped_file(&mut self, _world: &mut World, _path: &std::path::Path) {} fn on_dropped_file_data(&mut self, _world: &mut World, _name: &str, _data: &[u8]) {} fn on_hovered_file(&mut self, _world: &mut World, _path: &std::path::Path) {} fn on_hovered_file_cancelled(&mut self, _world: &mut World) {} fn on_gamepad_event(&mut self, _world: &mut World, _event: gilrs::Event) {} fn on_suspend(&mut self, _world: &mut World) {} fn on_resume(&mut self, _world: &mut World) {} fn on_mouse_input(&mut self, _world: &mut World, _state: ElementState, _button: MouseButton) {} fn handle_mcp_command(&mut self, _world: &mut World, _command: &McpCommand) -> Option<McpResponse> { None } fn after_mcp_command(&mut self, _world: &mut World, _command: &McpCommand, _response: &McpResponse) {} } }
All methods have default implementations, so you only need to implement the ones relevant to your game.
Commonly Used Methods
title()
Returns the window title. Defaults to "Nightshade" if not overridden:
#![allow(unused)] fn main() { fn title(&self) -> &str { "My Awesome Game" } }
log_config()
Configure file-based logging. Desktop only, requires the tracing feature. Returns a LogConfig with directory, rotation strategy, and default log filter:
#![allow(unused)] fn main() { fn log_config(&self) -> LogConfig { LogConfig { directory: "logs".to_string(), rotation: LogRotation::Daily, default_filter: "info".to_string(), timestamp_format: "%Y-%m-%d_%H-%M-%S".to_string(), } } }
Rotation options: LogRotation::PerSession (new file each launch), LogRotation::Daily, LogRotation::Never (append to single file).
initialize()
Called once when the application starts. Use this to set up your initial game state:
#![allow(unused)] fn main() { fn initialize(&mut self, world: &mut World) { world.resources.graphics.atmosphere = Atmosphere::Sky; let camera = spawn_camera(world, Vec3::new(0.0, 5.0, 10.0), "Camera".to_string()); world.resources.active_camera = Some(camera); self.player = Some(spawn_player(world)); } }
run_systems()
Called every frame. This is where your game logic lives:
#![allow(unused)] fn main() { fn run_systems(&mut self, world: &mut World) { player_movement_system(world); enemy_ai_system(world); collision_response_system(world); } }
Optional Methods
ui()
For egui-based user interfaces. Requires the egui feature. Note that world comes before ui_context:
#![allow(unused)] fn main() { fn ui(&mut self, world: &mut World, ui_context: &egui::Context) { egui::Window::new("Debug").show(ui_context, |ui| { ui.label(format!("FPS: {:.0}", world.resources.window.timing.frames_per_second)); ui.label(format!("Entities: {}", world.core.query_entities(RENDER_MESH).count())); }); } }
on_keyboard_input()
Handle keyboard events directly:
#![allow(unused)] fn main() { fn on_keyboard_input(&mut self, world: &mut World, key_code: KeyCode, key_state: ElementState) { if key_state == ElementState::Pressed { match key_code { KeyCode::Escape => self.paused = !self.paused, KeyCode::F11 => toggle_fullscreen(world), _ => {} } } } }
on_mouse_input()
Handle mouse button events:
#![allow(unused)] fn main() { fn on_mouse_input(&mut self, world: &mut World, state: ElementState, button: MouseButton) { if state == ElementState::Pressed { match button { MouseButton::Left => self.shoot(world), MouseButton::Right => self.aim(world), _ => {} } } } }
on_gamepad_event()
Handle gamepad button presses:
#![allow(unused)] fn main() { fn on_gamepad_event(&mut self, world: &mut World, event: gilrs::Event) { if let gilrs::EventType::ButtonPressed(button, _) = event.event { match button { gilrs::Button::Start => self.paused = !self.paused, gilrs::Button::South => self.player_jump(), _ => {} } } } }
on_suspend() / on_resume()
Android lifecycle hooks. Called when the app is suspended (backgrounded) or resumed. Use these to release and restore GPU resources:
#![allow(unused)] fn main() { fn on_suspend(&mut self, world: &mut World) { self.save_state(world); } fn on_resume(&mut self, world: &mut World) { self.restore_state(world); } }
secondary_ui()
For multi-window applications using egui. Called with a window_index parameter that identifies which secondary window is being drawn, allowing you to render different UI per window. Requires the egui feature:
#![allow(unused)] fn main() { fn secondary_ui(&mut self, world: &mut World, window_index: usize, ui_context: &egui::Context) { match window_index { 0 => { egui::Window::new("Inspector").show(ui_context, |ui| { ui.label("Secondary window 0"); }); } 1 => { egui::Window::new("Scene View").show(ui_context, |ui| { ui.label("Secondary window 1"); }); } _ => {} } } }
configure_render_graph()
Customize the rendering pipeline. Called once during initialization. The default implementation sets up bloom, post-processing, and swapchain blit passes. Override this to replace or extend the default pipeline:
#![allow(unused)] fn main() { fn configure_render_graph( &mut self, graph: &mut RenderGraph<World>, device: &wgpu::Device, surface_format: wgpu::TextureFormat, resources: RenderResources, ) { let bloom_pass = passes::BloomPass::new(device, 1920, 1080); graph.add_pass( Box::new(bloom_pass), &[("input", resources.scene_color), ("output", resources.bloom)], ); } }
update_render_graph()
Called each frame to update render graph state dynamically:
#![allow(unused)] fn main() { fn update_render_graph(&mut self, graph: &mut RenderGraph<World>, world: &World) { if self.bloom_changed { graph.set_enabled("bloom_pass", self.bloom_enabled); self.bloom_changed = false; } } }
pre_render()
Called before rendering begins each frame. Useful for custom GPU uploads or renderer state changes:
#![allow(unused)] fn main() { fn pre_render(&mut self, renderer: &mut dyn Render, world: &mut World) { renderer.update_custom_buffer(world, &self.custom_data); } }
next_state()
Allows transitioning to a different State. Return Some(new_state) to switch:
#![allow(unused)] fn main() { fn next_state(&mut self, world: &mut World) -> Option<Box<dyn State>> { if self.transition_to_gameplay { Some(Box::new(GameplayState::new())) } else { None } } }
on_dropped_file() / on_dropped_file_data()
Handle files dropped onto the window:
#![allow(unused)] fn main() { fn on_dropped_file(&mut self, world: &mut World, path: &Path) { if path.extension() == Some("glb".as_ref()) { self.load_model(world, path); } } fn on_dropped_file_data(&mut self, world: &mut World, name: &str, data: &[u8]) { self.process_dropped_data(world, name, data); } }
on_hovered_file() / on_hovered_file_cancelled()
Handle file drag hover events:
#![allow(unused)] fn main() { fn on_hovered_file(&mut self, world: &mut World, path: &Path) { self.show_drop_indicator = true; } fn on_hovered_file_cancelled(&mut self, world: &mut World) { self.show_drop_indicator = false; } }
handle_mcp_command()
Intercept MCP commands before the engine processes them. Requires the mcp feature. Return Some(response) to handle a command yourself, or None to let the engine handle it with default behavior:
#![allow(unused)] fn main() { #[cfg(all(feature = "mcp", not(target_arch = "wasm32")))] fn handle_mcp_command( &mut self, world: &mut World, command: &McpCommand, ) -> Option<McpResponse> { match command { McpCommand::SpawnEntity { name, .. } => { self.pending_scene_refresh = true; None } _ => None, } } }
See AI Integration for full details on the MCP system.
after_mcp_command()
Called after an MCP command has been processed (either by the engine or by your handle_mcp_command override). Receives both the command and the response. Useful for recording undo entries, refreshing UI state, or reacting to the outcome of MCP operations:
#![allow(unused)] fn main() { #[cfg(all(feature = "mcp", not(target_arch = "wasm32")))] fn after_mcp_command( &mut self, world: &mut World, command: &McpCommand, response: &McpResponse, ) { if let McpResponse::Success(_) = response { match command { McpCommand::SpawnEntity { name, .. } => { self.scene_tree_dirty = true; } McpCommand::DespawnEntity { .. } => { self.scene_tree_dirty = true; } _ => {} } } } }
handle_event()
Handle custom EventBus messages:
#![allow(unused)] fn main() { fn handle_event(&mut self, world: &mut World, message: &Message) { match message { Message::Custom(data) => self.process_event(world, data), _ => {} } } }
Launching Your Game
Use the nightshade::launch function to run your game:
fn main() -> Result<(), Box<dyn std::error::Error>> { nightshade::launch(MyGame::default()) }
Or with an HDR skybox:
#![allow(unused)] fn main() { fn initialize(&mut self, world: &mut World) { load_hdr_skybox(world, include_bytes!("../assets/sky.hdr").to_vec()); } }