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

Project Structure

A Nightshade project is a regular Cargo binary crate. The engine is a single dependency, and the project layout is whatever shape the game needs. The recommendations below are the conventions Nightshade's own apps follow.

my_game/
├── Cargo.toml
├── src/
│   ├── main.rs           # Entry point
│   ├── game.rs           # Game state
│   └── systems/          # Game systems
│       ├── player.rs
│       ├── camera.rs
│       └── ...
├── assets/
│   ├── models/           # glTF/GLB files
│   ├── textures/         # PNG, JPG, HDR
│   └── sounds/           # Audio files
└── README.md

main.rs stays tiny. The State implementation lives in game.rs. Per-frame logic gets split into systems/<name>.rs files, one per concern. Game logic belongs in the systems directory, not lumped into state.rs.

Cargo.toml

[package]
name = "my_game"
version = "0.1.0"
edition = "2024"

[dependencies]
nightshade = { git = "https://github.com/matthewjberger/nightshade.git", features = ["engine", "wgpu"] }

The engine and wgpu features are the minimum. Add physics, audio, and other features as the game needs them. Each one pulls in its dependency tree, so omit anything unused.

Entry point

main.rs does two things. It declares the game module and calls nightshade::launch with the game state.

mod game;

use nightshade::prelude::*;

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

That is the whole entry point. Anything more belongs in game.rs or a system module.

Game state

The State implementation owns whatever game data outlives a single frame. Entity handles for the player, score counters, level identifiers, and similar bookkeeping go on the struct.

#![allow(unused)]
fn main() {
use nightshade::prelude::*;

#[derive(Default)]
pub struct MyGame {
    player: Option<Entity>,
    score: u32,
}

impl State for MyGame {
    fn initialize(&mut self, world: &mut World) {
        world.resources.window.title = "My Game".to_string();
    }

    fn run_systems(&mut self, world: &mut World) {
    }
}
}

State stored on the MyGame struct is per-game data. State stored in world.resources is per-engine data. The split is intentional. The engine never touches the game struct, and the game does not need to extend the engine's resource list to track its own bookkeeping.

Embedding assets

For distribution as a single binary, embed assets at compile time with include_bytes!.

#![allow(unused)]
fn main() {
const MODEL_BYTES: &[u8] = include_bytes!("../assets/models/character.glb");
const SKY_HDR: &[u8] = include_bytes!("../assets/textures/sky.hdr");
}

The bytes live in the binary and load synchronously. For larger games, leave the assets on disk and stream them at runtime through the asset cache.

A second ECS for game data

For non-trivial games, define a second freecs::ecs! world alongside the engine's World. The engine world handles rendering, physics, and transforms. The game world handles game-specific components like inventories, AI states, and stats. This is the dual-world pattern that apps/game/ and the example games in nightshade-examples use.

#![allow(unused)]
fn main() {
use freecs::ecs;

ecs! {
    GameWorld {
        components {
            player_state: PlayerState,
            inventory: Inventory,
            health: Health,
        },
        resources {
            game_time: GameTime,
            score: u32,
        }
    }
}

pub struct MyGame {
    game: GameWorld,
}
}

The two worlds are linked through the engine's EngineEntity(Entity) component (at nightshade::ecs::sync::EngineEntity). Game-side entities store the engine-side entity they correspond to, and a render-sync pass copies positions or transforms from the game world to the engine world each frame. The helpers sync_engine_translation, sync_engine_transform, and despawn_linked cover the common cases. The pattern is documented in detail in the ECS chapters.

Splitting systems into modules

Once run_systems grows past a screen of code, break it apart by concern.

#![allow(unused)]
fn main() {
// src/systems/mod.rs
pub mod camera;
pub mod player;
pub mod enemies;
pub mod ui;

// src/game.rs
mod systems;

impl State for MyGame {
    fn run_systems(&mut self, world: &mut World) {
        systems::player::update(&mut self.game, world);
        systems::camera::follow(&self.game, world);
        systems::enemies::ai(&mut self.game, world);
        systems::ui::update(&self.game, world);
    }
}
}

Each system file is a flat set of free functions over (&mut GameWorld, &mut World) or whichever subset it needs. No traits, no inheritance, no manager objects. The naming follows the engine's own convention. Behavior lives in systems/<name>.rs, not in state.rs.