Introduction
Build worlds where intelligent agents are a core system, not an afterthought.
A free, MIT-licensed Rust game engine where AI agents are a first-class runtime system. Deterministic simulation, tool-validated behavior, and Criterion-backed performance — inspectable, reproducible, and open.
Jump straight to the area you need.
Set up the workspace and run your first system.
- Quick startOpen quick start
- InstallationInstall dependencies
- First companionBuild your first AI companion
Understand the runtime model.
- OverviewRead architecture overview
- AI-native designInspect AI-native design
- ValidationStudy tool validation
Explore the engine subsystems.
- AI systemOpen AI systems
- Physics and fluidsInspect simulation systems
- Terrain and navigationBrowse world systems
Demos, benchmarks, and optimization.
- ExamplesBrowse examples
- BenchmarksOpen benchmark dashboard
- OptimizationRead optimization guide
Contribute and build from source.
- ContributingOpen contribution guide
- BuildingBuild from source
- TestingReview validation workflow
Subsystem APIs and crate documentation.
- API overviewOpen API reference
- Crate mapBrowse crate documentation
- CLI and configInspect tools and configuration
Architecture, safety, and performance you can verify.
Perception → reasoning → planning → action, built into the runtime.
World snapshots, plan intents, tool validation, behavior trees, GOAP, and LLM-backed planning all plug into a deterministic loop.
Bit-identical replay, validator-gated actions, and formal verification.
Core unsafe paths are exercised under Miri. Kani proofs back critical ECS, math, and SDK invariants. Replay validation is built in, not bolted on.
Every performance claim links to a reproducible Criterion measurement.
ECS, AI planning, physics, rendering frame times, and SIMD throughput are all measured with specific numbers, not broad adjectives.
Perception → reasoning → planning → validation → action.
A focused stack for intelligent, simulation-heavy games.
Six validated modes plus hybrid arbiters.
Classical planners, behavior trees, utility logic, LLM orchestration, ensemble patterns, and hybrid arbiters.
Ordered simulation for replay, tooling, and scale.
Archetype storage, system staging, iteration guarantees, and event channels form a reproducible simulation backbone.
wgpu-based rendering with real engine workloads.
PBR materials, clustered lighting, GPU skinning, post-processing, and LOD tooling.
Character control, spatial hashing, fluids, and more.
Collision, character motion, destructibles, ragdolls, vehicles, gravity zones, and fluid simulation.
Navmesh, terrain, scene streaming, and gameplay layers.
Terrain generation, navigation meshes, crafting, quests, dialogue, and procedural content.
Example suite, editor tooling, and a C ABI for embedding.
A large example suite, editor workflows, and a stable C SDK layer.
Current measurements from the codebase.
- Agent capacity at 60 FPS12,700+
- AI validation throughput6.48M checks/sec
- Frame time at 1,000 entities2.70 ms
- ECS world creation25.8 ns
- Character move cost114 ns
- SIMD batch over 10k entities9.879 us
Verification and testing across the stack.
- Weighted line coverage59.3% across measured crates
- High-coverage crates14 crates at 85%+
- Miri validation977 tests, 0 UB
- Kani verification71+ harnesses
- Prompt mutation testing100% adjusted kill rate
- Desktop targetsWindows, Linux, macOS
Pick an entry point.
- System designOpen architecture overview
- Performance dataOpen benchmarks
- Build and runRead quick start
- Examples and demosBrowse examples
- Contributing workflowOpen contribution guide
- Reference implementationInspect Veilweaver
Where AstraWeave fits best.
Companions, directors, and systemic encounters.
Projects where NPCs need to observe, plan, and react with more depth than state-machine scripting allows.
Validation and replayability matter.
Deterministic simulation and tool-gated actions for anti-cheat, reproducibility, and replay validation.
Benchmarkable AI-native architecture.
Test agent scale, planning strategies, and hybrid AI control under measurable conditions.
Rust core with a C ABI.
Adopt focused subsystems through the modular crate structure instead of committing to the whole stack.
Games this engine could help realize.
In the lineage of Dwarf Fortress or RimWorld.
Agent autonomy, world-state memory, logistics, and emergent story generation.
In the lineage of Civilization or Crusader Kings.
Multi-agent diplomacy, advisor systems, strategic planners, and explainable AI.
In the lineage of X-COM or Battle Brothers.
Tool validation, cover awareness, action planning, and replay-safe combat loops.
In the lineage of Kenshi or Mount and Blade.
Large numbers of autonomous actors with persistent world consequences.
In the lineage of Deus Ex or Dragon Age.
Companion decisions, quest reactivity, systemic encounters, and director-style orchestration.
Licensed ports or original successors.
Rebuild ambitious systemic designs with modern AI-native architecture.
Clone, build, and decide from evidence.
AstraWeave is free and MIT licensed. Evaluate, adopt subsystems, or contribute back without platform lock-in.
Quick Start
Get up and running with AstraWeave in minutes! This guide will help you install the engine, run your first AI companion, and understand the basic concepts.
Prerequisites
- Rust: 1.89.0+ (managed automatically via
rust-toolchain.toml) - Platform: Linux, macOS, or Windows
- GPU: Vulkan-compatible graphics card
- Memory: 4GB+ RAM recommended for AI models
Installation
1. Clone the Repository
git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine.git
cd AstraWeave-AI-Native-Gaming-Engine
2. System Dependencies (Linux)
If you’re on Linux, install the required system packages:
sudo apt-get update
sudo apt-get install -y build-essential pkg-config cmake ninja-build \
libx11-dev libxi-dev libxcursor-dev libxrandr-dev libxinerama-dev \
libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev libxcb1-dev \
libxcb-randr0-dev libxcb-xfixes0-dev libxcb-shape0-dev libxcb-xkb-dev \
libgl1-mesa-dev libegl1-mesa-dev wayland-protocols libwayland-dev \
libasound2-dev libpulse-dev libudev-dev mesa-vulkan-drivers vulkan-tools
3. Build Core Components
Build the stable, working core components:
cargo build -p astraweave-core -p astraweave-ai -p astraweave-physics \
-p astraweave-nav -p astraweave-render -p hello_companion
This typically takes 8-15 seconds after initial dependency download.
Your First AI Companion
Let’s run the most basic example to see AstraWeave in action:
cargo run -p hello_companion --release
What You’ll See
The demo will show:
- AI Perception: The companion perceives the world state
- Planning: AI generates a plan using its understanding
- Tool Validation: The engine validates what the AI wants to do
- Expected Panic: The demo will panic with “LosBlocked” - this is expected behavior demonstrating the validation system
Example Output
[INFO] AI Companion initialized
[INFO] Perception snapshot captured: 1 entities
[INFO] Planning phase: generating intent for companion
[INFO] Generated plan: MoveTo(target_position)
[INFO] Validating tool usage: MovementTool
[ERROR] Validation failed: LosBlocked
thread 'main' panicked at 'LOS validation failed'
This panic is intentional! It demonstrates AstraWeave’s core principle: the AI can only do what the engine validates as possible.
Understanding What Happened
The hello_companion example showcases AstraWeave’s fundamental architecture:
- Fixed-Tick Simulation: The world runs at deterministic 60Hz
- AI Perception: AI agents receive structured world snapshots
- Planning Layer: AI generates intentions using LLM-based planning
- Tool Validation: Engine validates every AI action before execution
- Safety First: Invalid actions are rejected, maintaining game integrity
Next Steps
Now that you’ve seen the core loop in action:
- Learn the Architecture: Read AI-Native Design
- Build Your First Game: Follow Building Your First Game
- Explore More Examples: Check out Working Examples
- Dive Deeper: Study Core Systems
Troubleshooting
Build Errors
If you encounter build errors:
- Make sure you have the correct Rust version (check
rust-toolchain.toml) - Install system dependencies for your platform
- Some examples have known compilation issues - stick to the working core components listed above
Runtime Issues
- Graphics errors: Ensure you have Vulkan drivers installed
- Audio errors: Install audio system dependencies (ALSA/PulseAudio on Linux)
- Permission errors: Make sure your user can access graphics and audio devices
For more help, see Troubleshooting.
🎉 Congratulations! You’ve successfully run your first AstraWeave AI companion. The engine validated the AI’s actions and maintained world integrity, just as it should.
Installation Guide
This guide covers detailed installation instructions for AstraWeave on different platforms, including all dependencies and troubleshooting common issues.
System Requirements
Minimum Requirements
- CPU: x64 processor with SSE2 support
- Memory: 4GB RAM (8GB+ recommended for AI models)
- GPU: Vulkan 1.0 compatible graphics card
- Storage: 2GB free space (more for AI models)
- Rust: 1.89.0+ (managed via rust-toolchain.toml)
Recommended Requirements
- CPU: Multi-core x64 processor (4+ cores)
- Memory: 16GB+ RAM for development and multiple AI models
- GPU: Modern Vulkan 1.2+ compatible GPU with 2GB+ VRAM
- Storage: SSD with 10GB+ free space
Platform-Specific Installation
Linux
Ubuntu/Debian
# Update package lists
sudo apt-get update
# Install build essentials
sudo apt-get install -y build-essential pkg-config cmake ninja-build
# Install graphics dependencies
sudo apt-get install -y libx11-dev libxi-dev libxcursor-dev libxrandr-dev \
libxinerama-dev libxkbcommon-dev libxkbcommon-x11-dev libx11-xcb-dev \
libxcb1-dev libxcb-randr0-dev libxcb-xfixes0-dev libxcb-shape0-dev \
libxcb-xkb-dev
# Install rendering dependencies
sudo apt-get install -y libgl1-mesa-dev libegl1-mesa-dev wayland-protocols \
libwayland-dev mesa-vulkan-drivers vulkan-tools
# Install audio dependencies
sudo apt-get install -y libasound2-dev libpulse-dev
# Install additional system dependencies
sudo apt-get install -y libudev-dev
Arch Linux
# Install base development tools
sudo pacman -S base-devel cmake ninja
# Install graphics and audio
sudo pacman -S vulkan-devel mesa alsa-lib libpulse wayland wayland-protocols
# Install X11 dependencies
sudo pacman -S libx11 libxcb libxrandr libxinerama libxcursor libxi
Fedora/RHEL
# Install development tools
sudo dnf groupinstall "Development Tools"
sudo dnf install cmake ninja-build pkg-config
# Install graphics dependencies
sudo dnf install libX11-devel libXi-devel libXcursor-devel libXrandr-devel \
libXinerama-devel libxkbcommon-devel libxkbcommon-x11-devel
# Install Vulkan and Mesa
sudo dnf install vulkan-devel mesa-dri-drivers
# Install audio
sudo dnf install alsa-lib-devel pulseaudio-libs-devel
macOS
Prerequisites
First, install Xcode Command Line Tools:
xcode-select --install
Using Homebrew (Recommended)
# Install Homebrew if not already installed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# Install dependencies
brew install cmake ninja pkg-config
# For Intel Macs, ensure MoltenVK is installed
brew install molten-vk
Manual Installation
- Download and install Xcode from the App Store
- Install CMake from cmake.org
- Ensure MoltenVK is available for Vulkan support
Windows
Using Visual Studio (Recommended)
- Install Visual Studio 2019 or later with C++ build tools
- Install Git for Windows
- Install CMake (either standalone or via Visual Studio Installer)
Using MSYS2/MinGW
# Install MSYS2 from https://www.msys2.org/
# Then in MSYS2 terminal:
pacman -S mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
pacman -S mingw-w64-x86_64-vulkan-devel
Rust Installation
AstraWeave uses a specific Rust version defined in rust-toolchain.toml. The installation process will automatically use the correct version.
Install Rust
# Install rustup (Rust installer)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Follow the prompts, then restart your terminal or run:
source ~/.cargo/env
# Verify installation
rustc --version
cargo --version
Rust Components
The following components will be installed automatically when needed:
cargo- Package manager and build toolclippy- Linter for catching common mistakesrustfmt- Code formatterrust-analyzer- Language server for IDE support
Clone and Build
1. Clone the Repository
git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine.git
cd AstraWeave-AI-Native-Gaming-Engine
2. Verify Rust Toolchain
The correct Rust version will be installed automatically:
# This will show the version from rust-toolchain.toml
rustc --version
3. Build Core Components
Start with the stable, working components:
cargo build -p astraweave-core -p astraweave-ai -p astraweave-physics \
-p astraweave-nav -p astraweave-render -p hello_companion
4. Run Tests
Verify the installation with tests:
cargo test -p astraweave-input
5. Run Example
Test the installation:
cargo run -p hello_companion --release
Verification
Check GPU Support
# Linux: Check Vulkan
vulkaninfo | grep "deviceName"
# macOS: Check Metal
system_profiler SPDisplaysDataType
# Windows: Use dxdiag or GPU-Z
Check Audio
# Linux: Test audio devices
aplay -l
# macOS: Check audio
system_profiler SPAudioDataType
# Windows: Check audio devices in Device Manager
Development Environment Setup
IDE Recommendations
VS Code (Recommended)
Install these extensions:
rust-analyzer- Rust language supportCodeLLDB- Debugging supportEven Better TOML- TOML file supportError Lens- Inline error display
Other IDEs
- CLion: Has good Rust support with the Rust plugin
- Vim/Neovim: Use with rust-analyzer LSP
- Emacs: Use with rust-analyzer and rustic-mode
Performance Considerations
Release Builds
For better performance during development:
# Always use release mode for examples
cargo run -p hello_companion --release
# Build in release mode
cargo build --release
Parallel Compilation
Speed up builds by using multiple CPU cores:
# Set in ~/.cargo/config.toml
[build]
jobs = 4 # or number of CPU cores
Target Directory
Use a shared target directory to reduce disk usage:
# Set CARGO_TARGET_DIR environment variable
export CARGO_TARGET_DIR=/path/to/shared/target
Troubleshooting
Common Build Errors
“linker not found”
- Linux: Install
build-essentialorgcc - macOS: Install Xcode Command Line Tools
- Windows: Install Visual Studio with C++ tools
Vulkan errors
- Linux: Install
mesa-vulkan-driversandvulkan-tools - macOS: Ensure MoltenVK is installed
- Windows: Update graphics drivers
Audio errors
- Linux: Install
libasound2-devandlibpulse-dev - macOS: Usually works out of the box
- Windows: Ensure Windows Audio service is running
Performance Issues
Slow Compilation
- Use
cargo build --releasefor better runtime performance - Consider using
sccacheto cache compilation results - Increase parallel build jobs in Cargo config
Runtime Performance
- Always use
--releaseflag for examples and demos - Ensure GPU drivers are up to date
- Check system has adequate RAM (4GB minimum)
Platform-Specific Issues
Linux Wayland vs X11
AstraWeave supports both Wayland and X11:
# Force X11 if needed
export WAYLAND_DISPLAY=""
# Force Wayland if needed
export DISPLAY=""
macOS Code Signing
For distribution on macOS, you may need to sign binaries:
codesign --force --deep --sign - target/release/hello_companion
Windows Antivirus
Some antivirus software may flag Rust binaries. Add exclusions for:
- The project directory
~/.cargodirectorytarget/build directory
Next Steps
With AstraWeave installed:
- Run through the Quick Start Guide
- Explore Working Examples
- Read about Architecture
- Build Your First Game
For ongoing development, see the Contributing Guide and Building from Source.
Your First AI Companion
This tutorial will guide you through creating your first AI companion in AstraWeave. By the end, you’ll have a companion that can perceive its environment, experience emotions, and exhibit autonomous behaviors.
Table of Contents
- Prerequisites
- Project Setup
- Creating a Basic Companion
- Adding Perception
- Adding Emotions
- Adding Behaviors
- Testing Your Companion
- Next Steps
Prerequisites
Before starting, ensure you have:
- Rust 1.75.0 or later installed
- AstraWeave installed (see Installation)
- A compatible GPU (see System Requirements)
- Basic Rust knowledge
New to Rust? Check out [The Rust Book](https://doc.rust-lang.org/book/) for a comprehensive introduction.
Project Setup
Create a New Project
# Create a new Rust project
cargo new my_first_companion
cd my_first_companion
Add AstraWeave Dependencies
Edit Cargo.toml to add AstraWeave dependencies:
[package]
name = "my_first_companion"
version = "0.1.0"
edition = "2021"
[dependencies]
# Core AstraWeave crates
astraweave-ai = "0.1.0"
astraweave-render = "0.1.0"
bevy = "0.12"
# Utilities
anyhow = "1.0"
Version numbers may change. Check [crates.io](https://crates.io) for the latest versions.
Verify Setup
# Build to ensure dependencies are resolved
cargo build
Creating a Basic Companion
Let’s start with a simple companion that just exists in the world.
Basic Structure
Edit src/main.rs:
use bevy::prelude::*;
use astraweave_ai::prelude::*;
use anyhow::Result;
fn main() -> Result<()> {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(AstraWeaveAIPlugin)
.add_systems(Startup, setup_companion)
.add_systems(Update, update_companion)
.run();
Ok(())
}
fn setup_companion(mut commands: Commands) {
// Create a basic companion
let companion = CompanionBuilder::new()
.with_name("Buddy")
.build();
commands.spawn(companion);
info!("Spawned companion: Buddy");
}
fn update_companion(
mut companions: Query<&mut Companion>,
time: Res<Time>,
) {
for mut companion in companions.iter_mut() {
companion.update(time.delta_seconds());
}
}
Run Your First Companion
cargo run
You should see log output indicating your companion was spawned!
Congratulations! You've created your first AI companion. Now let's make it more interesting.
Adding Perception
Companions need to perceive their environment to interact meaningfully.
Visual Perception
Add visual perception to detect nearby entities:
#![allow(unused)]
fn main() {
use astraweave_ai::perception::*;
fn setup_companion(mut commands: Commands) {
let companion = CompanionBuilder::new()
.with_name("Buddy")
.with_perception(PerceptionConfig {
visual_range: 10.0,
visual_fov: 120.0, // 120-degree field of view
update_rate: 10.0, // 10 updates per second
..default()
})
.build();
commands.spawn(companion);
}
}
Adding Stimuli
Create entities that the companion can perceive:
#![allow(unused)]
fn main() {
fn setup_companion(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Spawn the companion
let companion = CompanionBuilder::new()
.with_name("Buddy")
.with_perception(PerceptionConfig {
visual_range: 10.0,
visual_fov: 120.0,
update_rate: 10.0,
..default()
})
.build();
commands.spawn((
companion,
Transform::from_xyz(0.0, 0.0, 0.0),
));
// Spawn a ball that the companion can see
commands.spawn((
PbrBundle {
mesh: meshes.add(Sphere::new(0.5)),
material: materials.add(Color::rgb(0.8, 0.2, 0.2)),
transform: Transform::from_xyz(3.0, 0.5, 0.0),
..default()
},
Stimulus::new(StimulusType::Visual, 1.0),
));
// Add camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 5.0, 10.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// Add light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
}
}
Reacting to Perception
Add a system to log what the companion perceives:
#![allow(unused)]
fn main() {
fn update_companion(
mut companions: Query<(&mut Companion, &Transform)>,
stimuli: Query<(&Stimulus, &Transform)>,
time: Res<Time>,
) {
for (mut companion, companion_transform) in companions.iter_mut() {
companion.update(time.delta_seconds());
// Check what the companion can see
if let Some(perception) = companion.perception() {
for (stimulus, stimulus_transform) in stimuli.iter() {
let distance = companion_transform
.translation
.distance(stimulus_transform.translation);
if distance <= perception.visual_range {
info!(
"{} sees {} stimulus at distance {:.2}",
companion.name(),
stimulus.stimulus_type,
distance
);
}
}
}
}
}
}
Adding Emotions
Emotions make companions feel alive and responsive.
Emotion System
Add emotion processing to your companion:
#![allow(unused)]
fn main() {
use astraweave_ai::emotion::*;
fn setup_companion(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let companion = CompanionBuilder::new()
.with_name("Buddy")
.with_perception(PerceptionConfig {
visual_range: 10.0,
visual_fov: 120.0,
update_rate: 10.0,
..default()
})
.with_emotions(vec![
EmotionConfig::new("joy", 0.5, 0.95),
EmotionConfig::new("curiosity", 0.3, 0.98),
EmotionConfig::new("calm", 0.7, 0.99),
])
.build();
commands.spawn((
companion,
Transform::from_xyz(0.0, 0.0, 0.0),
));
// ... spawn stimuli and camera as before
}
}
Emotion Responses
Make emotions respond to stimuli:
fn process_emotions(
mut companions: Query<&mut Companion>,
stimuli: Query<(&Stimulus, &Transform)>,
) {
for mut companion in companions.iter_mut() {
if let Some(emotion_system) = companion.emotion_system_mut() {
// Process nearby stimuli
for (stimulus, _) in stimuli.iter() {
match stimulus.stimulus_type {
StimulusType::Visual => {
emotion_system.increase_emotion("curiosity", 0.1);
}
StimulusType::Positive => {
emotion_system.increase_emotion("joy", 0.2);
}
StimulusType::Negative => {
emotion_system.decrease_emotion("calm", 0.15);
}
_ => {}
}
}
// Log dominant emotion
if let Some(dominant) = emotion_system.dominant_emotion() {
info!(
"{} feels {} (intensity: {:.2})",
companion.name(),
dominant.name,
dominant.intensity
);
}
}
}
}
// Add this system to your app
fn main() -> Result<()> {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(AstraWeaveAIPlugin)
.add_systems(Startup, setup_companion)
.add_systems(Update, (update_companion, process_emotions))
.run();
Ok(())
}
Adding Behaviors
Behaviors allow companions to act autonomously based on their internal state.
Basic Movement Behavior
Add a simple behavior that makes the companion wander:
#![allow(unused)]
fn main() {
use astraweave_ai::behavior::*;
fn setup_companion(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
let companion = CompanionBuilder::new()
.with_name("Buddy")
.with_perception(PerceptionConfig::default())
.with_emotions(vec![
EmotionConfig::new("joy", 0.5, 0.95),
EmotionConfig::new("curiosity", 0.3, 0.98),
EmotionConfig::new("calm", 0.7, 0.99),
])
.with_behavior(BehaviorConfig {
wander_speed: 2.0,
wander_radius: 5.0,
..default()
})
.build();
commands.spawn((
companion,
PbrBundle {
mesh: meshes.add(Capsule3d::new(0.5, 1.0)),
material: materials.add(Color::rgb(0.3, 0.5, 0.8)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
));
// ... rest of scene setup
}
}
Emotion-Driven Behavior
Make behavior change based on emotions:
fn emotion_driven_behavior(
mut companions: Query<(&mut Companion, &mut Transform)>,
time: Res<Time>,
) {
for (mut companion, mut transform) in companions.iter_mut() {
if let Some(emotion_system) = companion.emotion_system() {
let curiosity = emotion_system
.get_emotion("curiosity")
.map(|e| e.intensity)
.unwrap_or(0.0);
let calm = emotion_system
.get_emotion("calm")
.map(|e| e.intensity)
.unwrap_or(0.0);
// High curiosity = move around more
if curiosity > 0.6 {
let speed = 2.0 * curiosity;
let direction = Vec3::new(
(time.elapsed_seconds() * 0.5).sin(),
0.0,
(time.elapsed_seconds() * 0.5).cos(),
);
transform.translation += direction * speed * time.delta_seconds();
info!("{} is exploring curiously!", companion.name());
}
// High calm = stay still
else if calm > 0.7 {
info!("{} is resting calmly.", companion.name());
}
}
}
}
// Update main to include the behavior system
fn main() -> Result<()> {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(AstraWeaveAIPlugin)
.add_systems(Startup, setup_companion)
.add_systems(Update, (
update_companion,
process_emotions,
emotion_driven_behavior,
))
.run();
Ok(())
}
Testing Your Companion
Complete Example
Here’s the full code with all features:
use bevy::prelude::*;
use astraweave_ai::prelude::*;
use anyhow::Result;
fn main() -> Result<()> {
App::new()
.add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "My First Companion".to_string(),
resolution: (1280.0, 720.0).into(),
..default()
}),
..default()
}))
.add_plugins(AstraWeaveAIPlugin)
.add_systems(Startup, setup_companion)
.add_systems(Update, (
update_companion,
process_emotions,
emotion_driven_behavior,
))
.run();
Ok(())
}
fn setup_companion(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
) {
// Create companion
let companion = CompanionBuilder::new()
.with_name("Buddy")
.with_perception(PerceptionConfig {
visual_range: 10.0,
visual_fov: 120.0,
update_rate: 10.0,
..default()
})
.with_emotions(vec![
EmotionConfig::new("joy", 0.5, 0.95),
EmotionConfig::new("curiosity", 0.3, 0.98),
EmotionConfig::new("calm", 0.7, 0.99),
])
.with_behavior(BehaviorConfig::default())
.build();
commands.spawn((
companion,
PbrBundle {
mesh: meshes.add(Capsule3d::new(0.5, 1.0)),
material: materials.add(Color::rgb(0.3, 0.5, 0.8)),
transform: Transform::from_xyz(0.0, 0.5, 0.0),
..default()
},
));
// Add ground plane
commands.spawn(PbrBundle {
mesh: meshes.add(Plane3d::new(Vec3::Y, Vec2::new(20.0, 20.0))),
material: materials.add(Color::rgb(0.3, 0.5, 0.3)),
..default()
});
// Add interactive objects
for i in 0..5 {
let angle = (i as f32 / 5.0) * std::f32::consts::TAU;
let pos = Vec3::new(angle.cos() * 4.0, 0.5, angle.sin() * 4.0);
commands.spawn((
PbrBundle {
mesh: meshes.add(Sphere::new(0.5)),
material: materials.add(Color::rgb(0.8, 0.2, 0.2)),
transform: Transform::from_translation(pos),
..default()
},
Stimulus::new(StimulusType::Visual, 1.0),
));
}
// Add camera
commands.spawn(Camera3dBundle {
transform: Transform::from_xyz(0.0, 8.0, 12.0)
.looking_at(Vec3::ZERO, Vec3::Y),
..default()
});
// Add light
commands.spawn(PointLightBundle {
point_light: PointLight {
intensity: 1500.0,
shadows_enabled: true,
..default()
},
transform: Transform::from_xyz(4.0, 8.0, 4.0),
..default()
});
}
fn update_companion(
mut companions: Query<&mut Companion>,
time: Res<Time>,
) {
for mut companion in companions.iter_mut() {
companion.update(time.delta_seconds());
}
}
fn process_emotions(
mut companions: Query<(&mut Companion, &Transform)>,
stimuli: Query<(&Stimulus, &Transform)>,
) {
for (mut companion, companion_transform) in companions.iter_mut() {
if let Some(emotion_system) = companion.emotion_system_mut() {
for (stimulus, stimulus_transform) in stimuli.iter() {
let distance = companion_transform
.translation
.distance(stimulus_transform.translation);
if distance <= 5.0 {
emotion_system.increase_emotion("curiosity", 0.05);
emotion_system.increase_emotion("joy", 0.02);
}
}
}
}
}
fn emotion_driven_behavior(
mut companions: Query<(&mut Companion, &mut Transform)>,
time: Res<Time>,
) {
for (companion, mut transform) in companions.iter_mut() {
if let Some(emotion_system) = companion.emotion_system() {
let curiosity = emotion_system
.get_emotion("curiosity")
.map(|e| e.intensity)
.unwrap_or(0.0);
if curiosity > 0.4 {
let speed = 1.5 * curiosity;
let direction = Vec3::new(
(time.elapsed_seconds() * 0.8).sin(),
0.0,
(time.elapsed_seconds() * 0.8).cos(),
);
transform.translation += direction * speed * time.delta_seconds();
transform.translation.y = 0.5; // Keep on ground
}
}
}
}
Run and Observe
cargo run --release
Watch your companion:
- Wander around the scene
- React to nearby objects
- Show emotional responses
- Exhibit autonomous behavior
You've created a fully functional AI companion with perception, emotions, and behaviors!
Next Steps
Now that you have a working companion, try:
- Add more emotions: Experiment with fear, excitement, anger
- Complex behaviors: Implement approach/avoid behaviors based on emotions
- Social interactions: Add multiple companions that interact
- Save/Load: Persist companion state between runs
- Visual feedback: Change companion color based on dominant emotion
Advanced Topics
Example Projects
Check out more examples in the repository:
# Clone AstraWeave
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
# Run advanced examples
cargo run --release --example companion_emotions
cargo run --release --example multi_companion
cargo run --release --example behavior_showcase
Join the Community
- Discord: Join our server
- GitHub: Open issues or discussions
- Forum: Community forums
Share your companion creations in our Discord showcase channel!
Happy companion building!
System Requirements
This page outlines the hardware and software requirements for developing and running AstraWeave applications.
Table of Contents
- Minimum Requirements
- Recommended Requirements
- GPU Requirements
- Software Dependencies
- Platform-Specific Notes
Minimum Requirements
These are the bare minimum specifications to run AstraWeave applications:
Hardware
| Component | Specification |
|---|---|
| CPU | Quad-core processor (Intel i5-6600K / AMD Ryzen 3 1300X or equivalent) |
| RAM | 8 GB |
| GPU | NVIDIA GTX 1060 (6GB) / AMD RX 580 (8GB) / Intel Arc A380 |
| VRAM | 4 GB |
| Storage | 2 GB available space (SSD recommended) |
| Display | 1920x1080 resolution |
Minimum requirements may result in reduced performance. For optimal experience, see recommended specifications.
Operating System
- Windows: Windows 10 (64-bit) version 1909 or later
- Linux: Any modern distribution with kernel 5.4+ (Ubuntu 20.04, Fedora 33, etc.)
- macOS: macOS 10.15 (Catalina) or later
Recommended Requirements
For optimal performance and development experience:
Hardware
| Component | Specification |
|---|---|
| CPU | 8-core processor (Intel i7-9700K / AMD Ryzen 7 3700X or better) |
| RAM | 16 GB or more |
| GPU | NVIDIA RTX 3060 / AMD RX 6700 XT / Intel Arc A750 or better |
| VRAM | 8 GB or more |
| Storage | 10 GB available space on NVMe SSD |
| Display | 2560x1440 or higher resolution |
Operating System
- Windows: Windows 11 (64-bit)
- Linux: Ubuntu 22.04 LTS / Fedora 38 or later
- macOS: macOS 13 (Ventura) or later
More RAM and VRAM allow for larger scenes, more AI companions, and higher-quality assets.
GPU Requirements
AstraWeave is GPU-accelerated and requires modern graphics hardware with compute shader support.
Graphics API Support
Your GPU must support one of the following:
| Platform | Graphics API | Minimum Version |
|---|---|---|
| Windows | DirectX 12 | Feature Level 12.0 |
| Windows/Linux | Vulkan | 1.2 |
| macOS | Metal | Metal 2 |
Supported GPUs
NVIDIA
Minimum:
- GTX 1060 (6GB)
- GTX 1070
- GTX 1660 Ti
Recommended:
- RTX 3060 or better
- RTX 4060 or better
- RTX A2000 or better (workstation)
Optimal:
- RTX 4070 Ti or better
- RTX 4080 / 4090
- RTX A4000 or better (workstation)
AMD
Minimum:
- RX 580 (8GB)
- RX 5500 XT (8GB)
- RX 6600
Recommended:
- RX 6700 XT or better
- RX 7600 XT or better
Optimal:
- RX 7800 XT or better
- RX 7900 XTX
- Radeon Pro W6800 or better (workstation)
Intel
Minimum:
- Arc A380
Recommended:
- Arc A750 or better
- Arc A770
Optimal:
- Arc A770 (16GB)
Intel Arc GPUs require the latest drivers for optimal performance. Visit [Intel's driver page](https://www.intel.com/content/www/us/en/download/726609/intel-arc-graphics-windows-dch-driver.html) for updates.
Integrated Graphics
Integrated GPUs are not recommended but may work for basic development:
- Intel Iris Xe (11th gen or later)
- AMD Radeon Graphics (Ryzen 5000 series or later)
Integrated graphics will have significant performance limitations. Dedicated GPU strongly recommended.
Verifying GPU Support
Windows
# Check DirectX version
dxdiag
# Check GPU with PowerShell
Get-WmiObject Win32_VideoController | Select-Object Name, DriverVersion
Linux
# Check Vulkan support
vulkaninfo | grep "deviceName"
# Check GPU details
lspci | grep -i vga
nvidia-smi # For NVIDIA GPUs
macOS
# Check Metal support
system_profiler SPDisplaysDataType
Software Dependencies
Development Dependencies
Required
-
Rust: Version 1.75.0 or later
- Install via rustup
- Verify:
rustc --version
-
Git: Any recent version
- Install from git-scm.com
- Verify:
git --version
Platform-Specific
Windows
-
Visual Studio Build Tools 2022 or Visual Studio 2022
- Required for MSVC linker
- Download: Visual Studio Downloads
- Select “Desktop development with C++” workload
-
Vulkan SDK (optional, for Vulkan backend)
- Download: LunarG Vulkan SDK
Linux
Debian/Ubuntu:
sudo apt install -y \
build-essential \
pkg-config \
libx11-dev \
libxcursor-dev \
libxrandr-dev \
libxi-dev \
libasound2-dev \
libudev-dev \
vulkan-tools \
libvulkan-dev
Fedora/RHEL:
sudo dnf install -y \
gcc gcc-c++ \
pkg-config \
libX11-devel \
libXcursor-devel \
libXrandr-devel \
libXi-devel \
alsa-lib-devel \
systemd-devel \
vulkan-tools \
vulkan-loader-devel
Arch Linux:
sudo pacman -S --needed \
base-devel \
libx11 \
libxcursor \
libxrandr \
libxi \
alsa-lib \
vulkan-tools \
vulkan-headers
macOS
-
Xcode Command Line Tools
xcode-select --install -
Homebrew (recommended for package management)
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
GPU Drivers
Always use the latest drivers for optimal performance:
NVIDIA
- Download from NVIDIA Driver Downloads
- Linux: Use distribution’s package manager or NVIDIA’s
.runinstaller - Minimum driver version: 525.60.11 (Linux) / 528.24 (Windows)
AMD
- Windows: AMD Software Adrenalin Edition
- Linux: Mesa drivers (usually pre-installed)
# Ubuntu/Debian sudo apt install mesa-vulkan-drivers # Fedora sudo dnf install mesa-vulkan-drivers
Intel
- Intel Graphics Drivers
- Linux: Mesa drivers (usually pre-installed)
Platform-Specific Notes
Windows
-
Windows Defender: May slow initial builds. Add exclusion for project directory:
Settings > Windows Security > Virus & threat protection > Exclusion settings -
Long Path Support: Enable for deep project structures:
# Run as Administrator New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" ` -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force -
Power Settings: Use “High Performance” or “Ultimate Performance” power plan for best results
Linux
-
GPU Permissions: Ensure user is in the
videogroup:sudo usermod -a -G video $USER # Log out and back in -
Wayland: Some features work better with X11. Switch if experiencing issues:
# Edit /etc/gdm3/custom.conf (Ubuntu/Debian) # Uncomment: WaylandEnable=false -
Memory Limits: Increase limits for large projects:
# Add to ~/.bashrc ulimit -n 4096
macOS
-
Metal Support: Requires macOS 10.15+ and Metal-compatible GPU
- Check:
system_profiler SPDisplaysDataType | grep Metal
- Check:
-
Apple Silicon (M1/M2/M3): Fully supported with native ARM builds
# Verify ARM toolchain rustc --version --verbose | grep host # Should show: aarch64-apple-darwin -
Rosetta 2: Not required for M1/M2/M3 Macs (native ARM support)
-
Security Settings: May need to allow apps in System Preferences on first run
Performance Expectations
Development Workloads
| Specification | Compile Time (full rebuild) | Test Suite Runtime | Editor Performance |
|---|---|---|---|
| Minimum | 5-10 minutes | 2-3 minutes | Playable (30+ FPS) |
| Recommended | 2-4 minutes | 1-2 minutes | Smooth (60+ FPS) |
| Optimal | 1-2 minutes | <1 minute | Excellent (120+ FPS) |
Runtime Performance
Example scene: 10 AI companions with full perception and emotion systems
| Specification | Average FPS | 1% Low FPS | Max Companions |
|---|---|---|---|
| Minimum | 30-45 | 25-30 | 10-20 |
| Recommended | 60-90 | 50-60 | 50-100 |
| Optimal | 120-144+ | 90-100 | 200-500+ |
Performance varies based on scene complexity, AI behavior count, and graphics settings.
Checking Your System
Run this command to verify your setup:
# Clone and run the system check example
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
cargo run --example system_check
The system check will report:
- Rust version
- GPU capabilities
- Graphics API support
- Available memory
- Recommended settings
If all checks pass, your system is ready for AstraWeave development!
Upgrading Your System
If your system doesn’t meet requirements:
Priority Upgrades
- GPU: Biggest impact on runtime performance
- RAM: Enables larger scenes and faster compilation
- SSD: Dramatically reduces build times
- CPU: Improves compilation speed and AI performance
Budget Recommendations
- Entry Level ($300-500): Used RTX 3060 or RX 6700 XT + 16GB RAM
- Mid Range ($800-1200): RTX 4060 Ti or RX 7700 XT + 32GB RAM + NVMe SSD
- High End ($2000+): RTX 4080 or RX 7900 XTX + 64GB RAM + Fast CPU
Next Steps
Once your system meets the requirements:
If you have questions about requirements, join our Discord community.
Architecture Overview
AstraWeave represents a fundamental shift in game engine design: instead of treating AI as an afterthought, it places intelligent agents at the core of the architecture. This document explains the key architectural decisions and how they enable truly AI-native gameplay.
Core Philosophy: AI-First Design
Traditional game engines follow this pattern:
Game Logic → AI System → Scripted Behaviors
AstraWeave inverts this relationship:
AI Agents ← Tool Validation ← Engine Authority
Why This Matters
- No Cheating AI: AI can only act through validated game systems
- Emergent Behavior: Complex interactions emerge from simple, validated tools
- Multiplayer Ready: Same validation works for human players and AI
- Predictable Performance: Deterministic simulation enables reliable testing
High-Level Architecture
graph TB
subgraph "Fixed-Tick Simulation (60Hz)"
ECS[ECS World]
Physics[Physics System]
Audio[Audio System]
Render[Render System]
end
subgraph "AI Pipeline"
Perception[Perception Bus]
Planning[AI Planning]
Tools[Tool Sandbox]
Validation[Engine Validation]
end
subgraph "Game Layer"
Gameplay[Gameplay Systems]
Content[Content Systems]
UI[UI Systems]
end
ECS --> Perception
Perception --> Planning
Planning --> Tools
Tools --> Validation
Validation --> ECS
ECS --> Physics
ECS --> Audio
ECS --> Render
Gameplay --> ECS
Content --> ECS
UI --> ECS
The Deterministic Core
Fixed-Tick Simulation
AstraWeave runs the simulation at exactly 60Hz, regardless of rendering framerate:
#![allow(unused)]
fn main() {
const TICK_RATE: f64 = 60.0;
const TICK_DURATION: Duration = Duration::from_nanos(16_666_667); // 1/60 second
// Simulation always advances by exactly this amount
fn tick_simulation(&mut self) {
self.world.step(TICK_DURATION);
}
}
Benefits:
- Deterministic physics and AI behavior
- Consistent timing across different hardware
- Reliable networking and replay systems
- Predictable performance testing
Entity-Component-System (ECS)
AstraWeave uses an archetype-based ECS for cache-friendly performance:
#![allow(unused)]
fn main() {
// Components are pure data
#[derive(Component)]
struct Position(Vec3);
#[derive(Component)]
struct AIAgent {
perception_range: f32,
planning_cooldown: Duration,
}
// Systems operate on component combinations
fn ai_perception_system(
query: Query<(&Position, &AIAgent, &mut PerceptionState)>
) {
for (pos, agent, mut perception) in query.iter_mut() {
// Update agent perception based on world state
}
}
}
Key Benefits:
- Cache-friendly data layout
- Parallel system execution
- Clean separation of data and logic
- Easy to reason about and test
The AI Pipeline
1. Perception Bus
AI agents receive structured snapshots of the world state:
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct PerceptionSnapshot {
pub timestamp: u64,
pub agent_id: EntityId,
pub visible_entities: Vec<EntityData>,
pub audio_events: Vec<AudioEvent>,
pub world_state: WorldState,
}
}
Design Principles:
- Filtered Information: Agents only perceive what they should be able to see/hear
- Structured Data: JSON-serializable for easy AI model consumption
- Temporal Consistency: Snapshots include timing information
- Bandwidth Efficient: Only relevant changes are included
2. Planning Layer
AI models generate plans using the perception data:
#![allow(unused)]
fn main() {
pub struct AIPlan {
pub agent_id: EntityId,
pub intent: Intent,
pub tools: Vec<ToolUsage>,
pub confidence: f32,
pub reasoning: String, // For debugging and learning
}
pub enum Intent {
MoveTo { target: Vec3, urgency: f32 },
AttackTarget { target: EntityId, weapon: Option<EntityId> },
Interact { target: EntityId, interaction_type: String },
Communicate { target: EntityId, message: String },
}
}
Key Features:
- High-Level Intents: AI thinks in terms of goals, not implementation
- Tool-Based Actions: All actions go through validated tools
- Confidence Scoring: Enables dynamic difficulty and behavior tuning
- Explainable Reasoning: For debugging and player understanding
3. Tool Validation
Every AI action must be validated by the engine:
#![allow(unused)]
fn main() {
pub trait Tool {
fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult;
fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult;
}
// Example: Movement tool
impl Tool for MovementTool {
fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
let agent = world.get::<Position>(usage.agent_id)?;
let target = usage.target_position;
// Check line of sight
if !world.line_of_sight(agent.0, target) {
return ValidationResult::Blocked(BlockReason::LineOfSight);
}
// Check movement cooldown
if !self.cooldown_ready(usage.agent_id) {
return ValidationResult::Blocked(BlockReason::Cooldown);
}
ValidationResult::Valid
}
fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult {
// Actually perform the movement
world.get_mut::<Position>(usage.agent_id)?.0 = usage.target_position;
ExecutionResult::Success
}
}
}
Validation Categories:
- Physics Constraints: Can the action physically happen?
- Resource Requirements: Does the agent have what’s needed?
- Cooldowns: Is the action available now?
- Line of Sight: Can the agent see the target?
- Game Rules: Does this follow the game’s rules?
Networking Architecture
Server Authority
AstraWeave uses server-authoritative validation for multiplayer:
Client AI → Intent → Server Validation → World Update → State Broadcast
Benefits:
- No client-side cheating possible
- Consistent world state across all clients
- AI agents validated same as human players
- Deterministic simulation enables easy rollback
Intent Replication
Instead of replicating low-level actions, AstraWeave replicates high-level intents:
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct NetworkIntent {
pub player_id: PlayerId,
pub intent: Intent,
pub timestamp: u64,
pub predicted_outcome: Option<PredictedResult>,
}
}
Advantages:
- Lower bandwidth than action replication
- Natural lag compensation through prediction
- AI and human intents handled identically
- Easy to implement anti-cheat
Performance Architecture
CPU Performance
- ECS Archetype Iteration: Cache-friendly component access
- Parallel Systems: Independent systems run in parallel
- Incremental Updates: Only changed data is processed
- Fixed Timestep: Predictable CPU load
Memory Management
- Pool Allocation: Entities and components use object pools
- Streaming: World chunks loaded/unloaded based on relevance
- Compression: Perception data compressed before AI processing
- Garbage Collection: Minimal allocations in hot paths
GPU Utilization
- Deferred Rendering: Efficient handling of many lights
- Instanced Rendering: Batch similar objects
- Compute Shaders: Physics and AI calculations on GPU
- Temporal Upsampling: Maintain quality at lower resolution
Modularity and Extensibility
Crate Organization
astraweave-core/ # ECS, validation, core types
astraweave-ai/ # AI planning and perception
astraweave-physics/ # Physics integration
astraweave-render/ # Rendering pipeline
astraweave-audio/ # Audio system
astraweave-nav/ # Navigation and pathfinding
astraweave-net/ # Networking layer
Plugin System
#![allow(unused)]
fn main() {
pub trait EnginePlugin {
fn build(&self, app: &mut App);
}
pub struct CustomGamePlugin;
impl EnginePlugin for CustomGamePlugin {
fn build(&self, app: &mut App) {
app.add_system(custom_ai_behavior_system)
.add_tool(CustomInteractionTool)
.register_component::<CustomComponent>();
}
}
}
Security Model
AI Sandboxing
AI agents cannot:
- Access arbitrary memory
- Execute arbitrary code
- Bypass tool validation
- Affect systems outside their permissions
AI agents can only:
- Receive perception data
- Generate high-level intents
- Use validated tools
- Learn from feedback
Deterministic Security
The deterministic simulation enables:
- Replay Verification: Detect desync and cheating
- Formal Verification: Mathematical proof of certain properties
- Predictable Testing: Reliable automated testing
- Audit Trails: Complete history of all decisions
Development Philosophy
Composition Over Inheritance
#![allow(unused)]
fn main() {
// Instead of inheritance hierarchies
class AIAgent extends Entity { ... }
// Use component composition
struct Entity {
components: HashMap<ComponentId, Box<dyn Component>>
}
}
Data-Driven Design
#![allow(unused)]
fn main() {
// Behavior configured through data
#[derive(Deserialize)]
struct AIProfile {
aggression: f32,
curiosity: f32,
risk_tolerance: f32,
preferred_tools: Vec<String>,
}
}
Testable Architecture
#![allow(unused)]
fn main() {
// Every system is pure and testable
fn ai_planning_system(
world: &World,
perceptions: &[PerceptionSnapshot]
) -> Vec<AIPlan> {
// Pure function - easy to test
}
}
Comparison with Traditional Engines
| Aspect | Traditional Engine | AstraWeave |
|---|---|---|
| AI Integration | Bolted-on scripting | Core architecture |
| Action Validation | Trust-based | Engine-validated |
| Determinism | Variable | Fixed-tick |
| Networking | Action replication | Intent replication |
| Performance | Variable | Predictable |
| Testing | Difficult | Deterministic |
Next Steps
To understand specific systems:
- AI-Native Design - Deep dive into AI architecture
- ECS Architecture - Entity-Component-System details
- Deterministic Simulation - Fixed-tick simulation
- Tool Validation System - How AI actions are validated
To start building:
AI-Native Design
AstraWeave’s AI-native design represents a fundamental shift in how game engines approach artificial intelligence. Instead of treating AI as an add-on feature, we’ve built the entire engine around the principle that AI agents are first-class citizens.
The Traditional Approach (And Why It Fails)
Most game engines follow this pattern:
Game Engine → Game Logic → AI Scripting Layer → NPC Behaviors
Problems with this approach:
- AI is disconnected from core game systems
- AI agents can cheat (access hidden information, ignore physics)
- Difficult to create consistent multiplayer behavior
- AI behavior is scripted, not emergent
- Hard to test and debug AI interactions
AstraWeave’s AI-Native Approach
AstraWeave inverts this relationship:
AI Agents ← Tool Validation ← Engine Core ← Game Logic
Benefits of this approach:
- AI and human players use identical systems
- No AI cheating possible
- Emergent behavior from simple rules
- Natural multiplayer compatibility
- Testable and debuggable AI behavior
Core Principles
1. Perception-Based Decision Making
AI agents only know what they can perceive:
#![allow(unused)]
fn main() {
#[derive(Serialize, Deserialize)]
pub struct PerceptionSnapshot {
// Only information the AI should have access to
pub visible_entities: Vec<EntityData>,
pub audible_events: Vec<AudioEvent>,
pub remembered_information: Vec<MemoryItem>,
pub world_constraints: Vec<Constraint>,
}
}
No omniscience: AI cannot access:
- Hidden game state
- Other agents’ thoughts
- Perfect world information
- Player UI state
- Debug information
Realistic limitations: AI must work within:
- Line of sight restrictions
- Hearing range limitations
- Memory capacity constraints
- Processing time limits
2. Intent-Based Actions
AI generates high-level intents, not low-level commands:
#![allow(unused)]
fn main() {
pub enum Intent {
// High-level goals
ExploreArea { target_region: Region, curiosity: f32 },
SeekCover { threat_direction: Vec3, urgency: f32 },
ProtectAlly { ally_id: EntityId, commitment: f32 },
// Not low-level commands like "move left 3 pixels"
}
}
Benefits:
- AI thinks strategically, not tactically
- Natural language mapping for LLMs
- Easy to understand and debug
- Platform and implementation independent
3. Tool-Based Execution
All AI actions go through validated tools:
#![allow(unused)]
fn main() {
pub trait Tool {
// Every action must be validated first
fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult;
// Only valid actions are executed
fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult;
// Tools have constraints and cooldowns
fn get_constraints(&self) -> ToolConstraints;
}
}
No direct world manipulation: AI cannot:
- Teleport entities
- Spawn infinite resources
- Ignore physics constraints
- Break game rules
The AI Pipeline Architecture
Phase 1: Perception
#![allow(unused)]
fn main() {
fn perception_system(
mut agents: Query<(&Position, &AIAgent, &mut PerceptionState)>,
world_entities: Query<&Position, &EntityType>,
audio_events: Res<AudioEventBuffer>,
) {
for (pos, agent, mut perception) in agents.iter_mut() {
let mut snapshot = PerceptionSnapshot::new();
// Gather visible entities (line of sight)
for (other_pos, entity_type) in world_entities.iter() {
if world.line_of_sight(pos.0, other_pos.0) {
let distance = pos.0.distance(other_pos.0);
if distance <= agent.vision_range {
snapshot.visible_entities.push(EntityData {
position: other_pos.0,
entity_type: entity_type.clone(),
distance,
});
}
}
}
// Gather audible events
for event in audio_events.iter() {
let distance = pos.0.distance(event.position);
let volume = event.calculate_volume_at_distance(distance);
if volume > agent.hearing_threshold {
snapshot.audible_events.push(event.clone());
}
}
// Include relevant memories
snapshot.remembered_information = agent.memory.query_relevant(
&snapshot,
agent.memory_capacity
);
perception.current_snapshot = Some(snapshot);
}
}
}
Phase 2: Planning
#![allow(unused)]
fn main() {
fn ai_planning_system(
mut agents: Query<(&AIAgent, &PerceptionState, &mut PlanningState)>,
ai_service: Res<AIService>,
) {
for (agent, perception, mut planning) in agents.iter_mut() {
if let Some(snapshot) = &perception.current_snapshot {
// Prepare input for AI model
let planning_request = PlanningRequest {
agent_profile: agent.profile.clone(),
perception_data: snapshot.clone(),
available_tools: tool_registry.get_available_tools(agent.id),
current_goals: agent.goal_stack.clone(),
recent_memory: agent.memory.get_recent(10),
};
// Generate plan using LLM
match ai_service.generate_plan(planning_request) {
Ok(plan) => {
info!("AI generated plan: {:?}", plan);
planning.current_plan = Some(plan);
},
Err(e) => {
warn!("AI planning failed: {}", e);
// Fallback to simple behaviors
planning.current_plan = Some(generate_fallback_plan(agent));
}
}
}
}
}
}
Phase 3: Validation
#![allow(unused)]
fn main() {
fn tool_validation_system(
mut agents: Query<(&PlanningState, &mut ActionQueue)>,
tool_registry: Res<ToolRegistry>,
world: &World,
) {
for (planning, mut actions) in agents.iter_mut() {
if let Some(plan) = &planning.current_plan {
for tool_usage in &plan.tool_usages {
let tool = tool_registry.get_tool(&tool_usage.tool_name)?;
match tool.validate(world, tool_usage) {
ValidationResult::Valid => {
actions.push(ValidatedAction {
tool_usage: tool_usage.clone(),
validation_timestamp: world.current_tick(),
});
},
ValidationResult::Blocked(reason) => {
warn!("Tool validation failed: {:?}", reason);
// AI learns from failure
agent.memory.record_failure(tool_usage, reason);
},
ValidationResult::Delayed(wait_time) => {
actions.push_delayed(tool_usage.clone(), wait_time);
}
}
}
}
}
}
}
Phase 4: Execution
#![allow(unused)]
fn main() {
fn action_execution_system(
mut agents: Query<&mut ActionQueue>,
tool_registry: Res<ToolRegistry>,
mut world: ResMut<World>,
) {
for mut actions in agents.iter_mut() {
while let Some(action) = actions.pop_ready() {
let tool = tool_registry.get_tool(&action.tool_usage.tool_name)?;
match tool.execute(&mut world, &action.tool_usage) {
ExecutionResult::Success => {
// AI learns from success
agent.memory.record_success(&action.tool_usage);
},
ExecutionResult::Failed(reason) => {
// Even validated actions can fail during execution
warn!("Action execution failed: {:?}", reason);
agent.memory.record_execution_failure(&action.tool_usage, reason);
}
}
}
}
}
}
Tool Design Philosophy
Tools as Affordances
In AstraWeave, tools represent what an agent can do, not what it will do:
#![allow(unused)]
fn main() {
pub struct MovementTool {
max_speed: f32,
acceleration: f32,
valid_surfaces: Vec<SurfaceType>,
}
impl Tool for MovementTool {
fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
// Check if movement is physically possible
let agent_pos = world.get::<Position>(usage.agent_id)?;
let target_pos = usage.parameters.get_vec3("target")?;
// Line of sight check
if !world.line_of_sight(agent_pos.0, target_pos) {
return ValidationResult::Blocked(BlockReason::ObstructedPath);
}
// Surface validity check
let surface_type = world.get_surface_type(target_pos);
if !self.valid_surfaces.contains(&surface_type) {
return ValidationResult::Blocked(BlockReason::InvalidSurface);
}
// Speed limit check
let distance = agent_pos.0.distance(target_pos);
let time_required = distance / self.max_speed;
if time_required > usage.max_execution_time {
return ValidationResult::Blocked(BlockReason::TooSlow);
}
ValidationResult::Valid
}
}
}
Tool Composition
Complex behaviors emerge from combining simple tools:
#![allow(unused)]
fn main() {
// AI plans using multiple tools in sequence
let complex_plan = AIPlan {
steps: vec![
ToolUsage {
tool_name: "MovementTool",
parameters: movement_params,
},
ToolUsage {
tool_name: "InteractionTool",
parameters: interaction_params,
},
ToolUsage {
tool_name: "CommunicationTool",
parameters: communication_params,
},
],
};
}
Learning and Adaptation
Memory System Integration
#![allow(unused)]
fn main() {
pub struct AIMemory {
// Short-term working memory
working_memory: VecDeque<MemoryItem>,
// Long-term episodic memory
episodic_memory: Vec<Episode>,
// Learned patterns and strategies
strategy_memory: HashMap<Situation, Strategy>,
// Failed actions and why they failed
failure_memory: Vec<FailureRecord>,
}
impl AIMemory {
pub fn record_success(&mut self, action: &ToolUsage, outcome: &ExecutionResult) {
// Reinforce successful strategies
let situation = self.extract_situation_features(action);
let strategy = self.extract_strategy_features(action);
self.strategy_memory.entry(situation)
.or_default()
.reinforce(strategy, outcome.success_metric());
}
pub fn record_failure(&mut self, action: &ToolUsage, reason: &BlockReason) {
// Learn from failures to avoid them
self.failure_memory.push(FailureRecord {
action: action.clone(),
reason: reason.clone(),
context: self.current_context.clone(),
timestamp: Instant::now(),
});
}
}
}
Dynamic Behavior Adaptation
#![allow(unused)]
fn main() {
fn adaptation_system(
mut agents: Query<(&mut AIAgent, &AIMemory)>,
) {
for (mut agent, memory) in agents.iter_mut() {
// Adjust behavior based on recent experiences
let recent_failures = memory.get_recent_failures(Duration::from_secs(300));
if recent_failures.iter().any(|f| matches!(f.reason, BlockReason::TooAggressive)) {
agent.profile.aggression *= 0.9; // Become less aggressive
}
if recent_failures.iter().any(|f| matches!(f.reason, BlockReason::TooSlow)) {
agent.profile.urgency *= 1.1; // Become more urgent
}
// Adapt strategy preferences
let successful_strategies = memory.get_successful_strategies();
for (situation, strategy) in successful_strategies {
agent.strategy_preferences.insert(situation, strategy);
}
}
}
}
Emergent Behavior Examples
Cooperative Pathfinding
When multiple AI agents need to navigate through a narrow passage:
#![allow(unused)]
fn main() {
// No explicit coordination code needed
// Emergent behavior arises from:
// 1. Each agent perceives others as obstacles
// 2. Movement tool validates non-collision
// 3. Agents naturally take turns or find alternate routes
}
Dynamic Alliance Formation
#![allow(unused)]
fn main() {
// Agents can form alliances based on shared threats
fn threat_response_planning(
agent: &AIAgent,
perception: &PerceptionSnapshot,
) -> Intent {
let threats = perception.identify_threats();
let potential_allies = perception.identify_potential_allies();
if threats.is_empty() {
return Intent::Explore { target: random_area() };
}
if potential_allies.is_empty() {
return Intent::Flee { threat_direction: threats[0].position };
}
// Emergent alliance formation
Intent::CoordinateDefense {
allies: potential_allies,
threat: threats[0],
strategy: choose_defensive_strategy(threats, potential_allies),
}
}
}
Adaptive Combat Tactics
#![allow(unused)]
fn main() {
// AI learns and counters player strategies
fn combat_planning(
agent: &AIAgent,
perception: &PerceptionSnapshot,
memory: &AIMemory,
) -> Intent {
let player = perception.find_player()?;
// Analyze player's recent tactics
let player_patterns = memory.analyze_player_behavior(&player);
// Choose counter-strategy
let counter_strategy = match player_patterns.primary_tactic {
PlayerTactic::RushAttack => CombatStrategy::DefensiveCounter,
PlayerTactic::RangedKiting => CombatStrategy::ClosingPincer,
PlayerTactic::DefensiveTurtle => CombatStrategy::AreaDenial,
PlayerTactic::Unpredictable => CombatStrategy::AdaptiveReactive,
};
Intent::ExecuteCombatStrategy {
target: player.entity_id,
strategy: counter_strategy,
commitment: calculate_commitment(player_patterns.skill_level),
}
}
}
Performance Considerations
Computational Efficiency
#![allow(unused)]
fn main() {
// AI planning can be expensive, so we use various optimizations:
pub struct AIService {
// LLM inference can be slow
model_cache: LRUCache<PlanningRequest, AIPlan>,
// Batch multiple planning requests
batch_processor: BatchProcessor<PlanningRequest>,
// Use cheaper models for simple decisions
model_hierarchy: Vec<AIModel>, // Fast → Accurate
}
impl AIService {
pub fn generate_plan(&self, request: PlanningRequest) -> Result<AIPlan> {
// Check cache first
if let Some(cached_plan) = self.model_cache.get(&request) {
return Ok(cached_plan.clone());
}
// Use appropriate model based on complexity
let model = self.select_model(request.complexity());
// Generate plan
let plan = model.generate_plan(request)?;
// Cache result
self.model_cache.insert(request, plan.clone());
Ok(plan)
}
}
}
Memory Management
#![allow(unused)]
fn main() {
// AI memory systems need careful management
impl AIMemory {
pub fn cleanup_old_memories(&mut self) {
// Remove memories older than threshold
let cutoff = Instant::now() - Duration::from_secs(3600); // 1 hour
self.episodic_memory.retain(|episode| episode.timestamp > cutoff);
// Compress similar memories
self.compress_similar_episodes();
// Keep only the most important failures
self.failure_memory.sort_by_key(|f| f.importance_score());
self.failure_memory.truncate(100); // Keep top 100
}
}
}
Debugging AI Behavior
Explainable AI
#![allow(unused)]
fn main() {
#[derive(Debug, Serialize)]
pub struct AIPlan {
pub intent: Intent,
pub reasoning: String, // Natural language explanation
pub confidence: f32,
pub alternative_plans: Vec<AlternativePlan>,
pub decision_factors: Vec<DecisionFactor>,
}
// Example reasoning output:
"I can see an enemy at position (10, 5) who appears to be low on health.
My ally is engaged in combat nearby and could use support. I have a clear
line of sight and my weapon is ready. I'm choosing to attack rather than
flank because the enemy seems focused on my ally and won't see me coming."
}
Debug Visualization
#![allow(unused)]
fn main() {
// In development builds, expose AI decision making
#[cfg(debug_assertions)]
impl AIAgent {
pub fn get_debug_info(&self) -> AIDebugInfo {
AIDebugInfo {
current_perception: self.perception.clone(),
active_plan: self.planning.current_plan.clone(),
recent_decisions: self.memory.get_recent_decisions(10),
personality_state: self.profile.clone(),
tool_availability: self.get_available_tools(),
}
}
}
}
Integration with Traditional Game Systems
Physics Integration
#![allow(unused)]
fn main() {
// AI respects physics constraints
impl Tool for MovementTool {
fn validate(&self, world: &World, usage: &ToolUsage) -> ValidationResult {
let physics_world = world.resource::<PhysicsWorld>();
let agent_body = physics_world.get_body(usage.agent_id)?;
// Check if movement would cause collision
let proposed_movement = usage.parameters.get_vec3("target")?;
if physics_world.would_collide(agent_body, proposed_movement) {
return ValidationResult::Blocked(BlockReason::PhysicsCollision);
}
ValidationResult::Valid
}
}
}
Animation Integration
#![allow(unused)]
fn main() {
// AI actions trigger appropriate animations
impl Tool for CombatTool {
fn execute(&self, world: &mut World, usage: &ToolUsage) -> ExecutionResult {
let attack_type = usage.parameters.get_string("attack_type")?;
// Trigger combat animation
world.get_mut::<AnimationController>(usage.agent_id)?
.play_animation(format!("attack_{}", attack_type));
// Execute combat logic
self.resolve_combat(world, usage)
}
}
}
Comparison: Traditional vs AI-Native
| Aspect | Traditional Approach | AstraWeave AI-Native |
|---|---|---|
| Decision Making | Scripted state machines | LLM-based planning |
| World Knowledge | Omniscient access | Perception-limited |
| Action Execution | Direct world manipulation | Tool-validated actions |
| Behavior Adaptation | Manual script updates | Automatic learning |
| Multiplayer | Separate AI/player code | Unified validation |
| Debugging | Complex state inspection | Natural language reasoning |
| Performance | Predictable overhead | Variable AI complexity |
| Emergence | Limited by scripts | Unbounded combinations |
Best Practices for AI-Native Development
1. Design Affordances, Not Behaviors
#![allow(unused)]
fn main() {
// Good: Define what an agent CAN do
pub struct InteractionTool {
pub interaction_range: f32,
pub valid_targets: Vec<EntityType>,
pub cooldown: Duration,
}
// Avoid: Scripting what an agent WILL do
// pub fn npc_behavior_script() { ... }
}
2. Embrace Failure as Learning
#![allow(unused)]
fn main() {
// AI failures are features, not bugs
if let Err(validation_error) = tool.validate(world, usage) {
// Don't just log the error - let the AI learn from it
agent.memory.record_lesson(validation_error, current_context);
// AI will avoid this mistake in similar situations
}
}
3. Provide Rich Perception
#![allow(unused)]
fn main() {
// Give AI agents the information they need to make good decisions
pub struct PerceptionSnapshot {
// Not just positions, but meaningful context
pub entities: Vec<EntityPerception>,
pub environmental_cues: Vec<EnvironmentalCue>,
pub social_context: SocialContext,
pub recent_events: Vec<GameEvent>,
}
}
4. Use Hierarchical Planning
#![allow(unused)]
fn main() {
// Break complex goals into manageable sub-goals
pub enum Intent {
// High-level strategic goals
DefendTerritory { area: Region },
// Mid-level tactical goals
EstablishDefensivePosition { chokepoint: Vec3 },
// Low-level operational goals
MoveToCover { cover_position: Vec3 },
}
}
Future Directions
Advanced AI Architectures
- Multi-Agent Planning: Coordinated group decision making
- Hierarchical Temporal Memory: Better long-term memory systems
- Causal Reasoning: Understanding cause-and-effect relationships
- Meta-Learning: AI that learns how to learn better
Performance Optimizations
- Neural Network Compression: Smaller, faster AI models
- Predictive Caching: Pre-compute likely AI decisions
- Distributed Processing: AI planning across multiple cores/machines
- Hybrid Approaches: Combine neural networks with symbolic reasoning
AI-native design is not just about making NPCs smarter - it’s about creating fundamentally new types of interactive experiences where AI agents are true participants in the game world, subject to the same rules and constraints as human players.
ECS Architecture
AstraWeave uses an archetype-based Entity Component System (ECS) designed for deterministic, AI-native game development. The architecture provides cache-friendly iteration, deterministic execution ordering, and efficient event propagation for AI perception.
Core Concepts
Entity
An Entity is a unique identifier representing a game object. Internally, entities use a 32-bit ID and 32-bit generation counter for safe recycling:
#![allow(unused)]
fn main() {
use astraweave_ecs::*;
let mut world = World::new();
// Spawn returns a unique Entity handle
let player = world.spawn();
let enemy = world.spawn();
// Check if entity is alive (generation-safe)
assert!(world.is_alive(player));
// Despawn removes entity and all components
world.despawn(enemy);
assert!(!world.is_alive(enemy));
}
The generation counter prevents “dangling entity” bugs—if you hold a stale entity handle after despawn, operations silently fail rather than affecting the recycled entity.
Component
A Component is data attached to an entity. Any 'static + Send + Sync type automatically implements Component:
#![allow(unused)]
fn main() {
// Components are just plain structs
#[derive(Clone, Copy)]
struct Position { x: f32, y: f32 }
#[derive(Clone, Copy)]
struct Velocity { x: f32, y: f32 }
struct Health(i32);
// Insert components
let mut world = World::new();
let e = world.spawn();
world.insert(e, Position { x: 0.0, y: 0.0 });
world.insert(e, Velocity { x: 1.0, y: 0.0 });
world.insert(e, Health(100));
// Query components
if let Some(pos) = world.get::<Position>(e) {
println!("Entity at ({}, {})", pos.x, pos.y);
}
// Mutate components
if let Some(health) = world.get_mut::<Health>(e) {
health.0 -= 10;
}
}
Resource
A Resource is a singleton value accessible across systems—perfect for shared state like input, time, or game configuration:
#![allow(unused)]
fn main() {
struct DeltaTime(f32);
struct InputState {
move_direction: (f32, f32),
attack_pressed: bool,
}
let mut world = World::new();
// Insert resources
world.insert_resource(DeltaTime(1.0 / 60.0));
world.insert_resource(InputState {
move_direction: (0.0, 0.0),
attack_pressed: false,
});
// Query resources
let dt = world.get_resource::<DeltaTime>().unwrap();
let input = world.get_resource::<InputState>().unwrap();
}
Archetype Storage
What is an Archetype?
An Archetype groups all entities with the same set of component types. When you add or remove components, the entity moves to a different archetype.
Archetype 0: [Position]
├── Entity(1): Position(0,0)
└── Entity(4): Position(5,3)
Archetype 1: [Position, Velocity]
├── Entity(2): Position(1,1), Velocity(1,0)
└── Entity(3): Position(2,2), Velocity(0,1)
Archetype 2: [Position, Velocity, Health]
└── Entity(5): Position(3,3), Velocity(1,1), Health(100)
This design provides:
- Cache-friendly iteration: Components of the same type are stored contiguously
- Efficient queries: Filter by archetype signature, not per-entity checks
- Predictable memory layout: Improves CPU prefetching
Storage Modes
AstraWeave supports two storage backends:
Box Mode (Legacy): Components stored as Box<dyn Any>. Works for any component type but has heap indirection overhead.
BlobVec Mode (Optimized): Components stored in contiguous byte arrays. Requires component registration but provides 2-10× faster iteration:
#![allow(unused)]
fn main() {
let mut world = World::new();
// Register component for optimized storage
world.register_component::<Position>();
world.register_component::<Velocity>();
// Now Position and Velocity use BlobVec storage
}
SparseSet Entity Lookup
Entity-to-archetype mapping uses a SparseSet for O(1) lookup:
Memory Layout:
sparse: [None, Some(0), None, Some(1), None, Some(2), ...]
↓ ↓ ↓
dense: [Entity(1), Entity(3), Entity(5), ...]
This replaced the previous BTreeMap approach, providing 12-57× faster entity lookups.
System Architecture
System Stages
AstraWeave uses fixed stages for deterministic execution—critical for AI agents that must produce identical behavior across game sessions:
#![allow(unused)]
fn main() {
use astraweave_ecs::*;
// System stages execute in order
pub struct SystemStage;
impl SystemStage {
pub const PRE_SIMULATION: &'static str = "pre_simulation";
pub const PERCEPTION: &'static str = "perception"; // Build AI snapshots
pub const SIMULATION: &'static str = "simulation"; // Game logic
pub const AI_PLANNING: &'static str = "ai_planning"; // Generate plans
pub const PHYSICS: &'static str = "physics"; // Apply forces
pub const POST_SIMULATION: &'static str = "post_simulation";
pub const PRESENTATION: &'static str = "presentation"; // Render, audio
}
}
The AI-native game loop follows: Perception → Reasoning → Planning → Action
┌───────────────────────────────────────────────────────────────────────┐
│ Frame N │
├─────────────┬─────────────┬──────────────┬─────────────┬─────────────┤
│ pre_sim │ perception │ simulation │ ai_planning │ physics │
│ (setup) │ (sensors) │ (game logic) │ (decide) │ (movement) │
├─────────────┴─────────────┴──────────────┴─────────────┴─────────────┤
│ post_sim → presentation │
└───────────────────────────────────────────────────────────────────────┘
Registering Systems
Systems are functions that operate on the World:
#![allow(unused)]
fn main() {
fn movement_system(world: &mut World) {
world.each_mut::<Position>(|entity, pos| {
if let Some(vel) = world.get::<Velocity>(entity) {
pos.x += vel.x;
pos.y += vel.y;
}
});
}
fn ai_perception_system(world: &mut World) {
// Build WorldSnapshot for AI agents
// (see AI documentation for details)
}
let mut app = App::new();
app.add_system("simulation", movement_system);
app.add_system("perception", ai_perception_system);
}
Query Types
AstraWeave provides ergonomic query iterators:
#![allow(unused)]
fn main() {
// Single-component read-only query
let query = Query::<Position>::new(&world);
for (entity, pos) in query {
println!("Entity {:?} at ({}, {})", entity, pos.x, pos.y);
}
// Two-component query
let query2 = Query2::<Position, Velocity>::new(&world);
for (entity, pos, vel) in query2 {
println!("Entity {:?} moving at ({}, {})", entity, vel.x, vel.y);
}
// Mutable queries
let mut query = Query2Mut::<Position, Velocity>::new(&mut world);
for (entity, pos, vel) in query.iter_mut() {
pos.x += vel.x;
pos.y += vel.y;
}
}
Event System
Events enable reactive AI behaviors and decoupled communication between systems.
Sending Events
#![allow(unused)]
fn main() {
use astraweave_ecs::*;
// Define custom events
#[derive(Clone)]
struct DamageEvent {
target: Entity,
amount: i32,
source: Option<Entity>,
}
impl Event for DamageEvent {}
// Send events via Events resource
let mut events = Events::new();
events.send(DamageEvent {
target: player,
amount: 25,
source: Some(enemy),
});
}
Reading Events
#![allow(unused)]
fn main() {
// Systems read events via EventReader
fn damage_system(world: &mut World) {
let mut events = world.get_resource_mut::<Events>().unwrap();
for event in events.drain::<DamageEvent>() {
if let Some(health) = world.get_mut::<Health>(event.target) {
health.0 -= event.amount;
}
}
}
}
Built-in Events
AstraWeave provides common events for AI perception:
| Event | Purpose |
|---|---|
EntitySpawnedEvent | Entity creation notification |
EntityDespawnedEvent | Entity removal notification |
HealthChangedEvent | Health changes (for AI threat assessment) |
AiPlanningFailedEvent | AI plan generation failures |
ToolValidationFailedEvent | AI action validation failures |
App Builder Pattern
The App struct provides a Bevy-like builder pattern:
use astraweave_ecs::*;
#[derive(Clone, Copy)]
struct Position { x: f32, y: f32 }
#[derive(Clone, Copy)]
struct Velocity { x: f32, y: f32 }
fn movement_system(world: &mut World) {
world.each_mut::<Position>(|entity, pos| {
if let Some(vel) = world.get::<Velocity>(entity) {
pos.x += vel.x;
pos.y += vel.y;
}
});
}
fn main() {
let mut app = App::new();
app.add_system("simulation", movement_system);
// Spawn entities
let e = app.world.spawn();
app.world.insert(e, Position { x: 0.0, y: 0.0 });
app.world.insert(e, Velocity { x: 1.0, y: 0.0 });
// Run 100 simulation ticks
app = app.run_fixed(100);
// Entity moved 100 units
let pos = app.world.get::<Position>(e).unwrap();
assert_eq!(pos.x, 100.0);
}
Plugin Architecture
Plugins encapsulate related systems and resources:
#![allow(unused)]
fn main() {
pub trait Plugin {
fn build(&self, app: &mut App);
}
struct PhysicsPlugin;
impl Plugin for PhysicsPlugin {
fn build(&self, app: &mut App) {
app.add_system("physics", physics_tick);
app.world.insert_resource(PhysicsConfig::default());
}
}
// Add plugins to app
let app = App::new().add_plugin(PhysicsPlugin);
}
Command Buffer
For deferred operations (avoiding borrow conflicts), use CommandBuffer:
#![allow(unused)]
fn main() {
use astraweave_ecs::*;
// Register components first
let mut world = World::new();
world.register_component::<Position>();
world.register_component::<Velocity>();
// Queue commands
let mut cmd = CommandBuffer::new();
cmd.spawn()
.insert(Position { x: 0.0, y: 0.0 })
.insert(Velocity { x: 1.0, y: 0.0 });
cmd.entity(existing_entity)
.remove::<Velocity>();
// Apply all commands at once
cmd.apply(&mut world);
}
Determinism
AstraWeave ECS is designed for deterministic replay and multiplayer synchronization:
Ordered Iteration
Entities within an archetype iterate in spawn order (using packed arrays), ensuring consistent system behavior.
Seeded RNG
Use the built-in deterministic RNG:
#![allow(unused)]
fn main() {
use astraweave_ecs::Rng;
let mut rng = Rng::from_seed(42);
let damage = rng.next_f32() * 10.0; // Same value every time with seed 42
}
Event Ordering
Events are stored in order-of-send, with frame tracking for cleanup:
#![allow(unused)]
fn main() {
let events = Events::new()
.with_keep_frames(2); // Keep events for 2 frames
}
Performance Characteristics
Benchmarked on the AstraWeave test suite (Week 10):
| Operation | Time | Notes |
|---|---|---|
| World creation | 25.8 ns | Empty world |
| Entity spawn | 420 ns | Includes archetype assignment |
| Component insert | 1-2 µs | Archetype migration if needed |
| Entity lookup | O(1) | SparseSet, 12-57× faster than BTreeMap |
| Iteration (per entity) | <1 ns | Packed array iteration |
| Query creation | 50-100 ns | Archetype filtering |
60 FPS Budget
With 16.67 ms per frame, current performance provides:
- 1,000 entities: 1.14 ms frame time (93% headroom)
- 10,000 entities: ~11 ms frame time (34% headroom)
- Movement system: 106 µs for 1,000 entities (9.4× faster post-optimization)
Advanced Topics
Custom Component Storage
For specialized use cases, you can interact with archetype storage directly:
#![allow(unused)]
fn main() {
// Access archetypes for low-level iteration
for archetype in world.archetypes().iter() {
println!("Archetype {} has {} entities",
archetype.id, archetype.len());
for &entity in archetype.entities_vec() {
// Direct entity access
}
}
}
Profiling Integration
Enable the profiling feature for Tracy integration:
[dependencies]
astraweave-ecs = { version = "0.4", features = ["profiling"] }
Key spans are automatically instrumented: ECS::World::spawn, ECS::World::get, ECS::Schedule::run.
See Also
- API Reference: ECS — Full API documentation
- Core Systems: AI — AI integration with ECS
- Patterns — Common ECS design patterns
- First Companion Tutorial — Hands-on ECS usage
Deterministic Simulation
AstraWeave provides bit-identical deterministic simulation across all core systems. This enables reproducible AI behavior, lockstep multiplayer, replay systems, and reliable regression testing.
Why Determinism Matters
For AI-native gameplay, determinism is critical:
#![allow(unused)]
fn main() {
// Without determinism (BAD):
// Run 1: Entity 42 attacks first (random HashMap iteration)
// Run 2: Entity 17 attacks first (different outcome!)
// Multiplayer: Host and client see different results
// With determinism (GOOD):
// Run 1, 2, 3...: Same entity order, same decisions, same outcome
// Multiplayer: All clients see identical simulation
// Replays: Record inputs, reproduce exact gameplay
}
Use Cases:
- Reproducible AI: Same world state → same AI decisions
- Lockstep Multiplayer: Only sync inputs, not full game state
- Replay Systems: Record/playback for debugging and spectating
- Regression Testing: Validate AI changes don’t break behavior
- Competitive Gaming: Provably fair gameplay
Determinism Guarantees
What IS Guaranteed
| System | Guarantee |
|---|---|
| ECS Iteration | Archetypes visited in ID order (BTreeMap) |
| Within-Archetype | Entities maintain relative order |
| Repeated Iterations | Same order every time |
| Cross-World | Same operations → same archetype IDs |
| Physics | Deterministic parallel iteration (Rayon) |
| Capture/Replay | Bit-identical state restoration |
| Events | FIFO ordering |
What is NOT Guaranteed
- Spawn order across archetypes: Entity order changes when components added/removed
- Floating-point across platforms: Different CPUs may produce slightly different results
- Non-deterministic RNG: Must use seeded RNG explicitly
Fixed-Tick Simulation
AstraWeave runs physics and AI at exactly 60Hz:
#![allow(unused)]
fn main() {
const TICK_RATE: f64 = 60.0;
const TICK_DURATION: Duration = Duration::from_nanos(16_666_667);
// Every simulation step advances by exactly this amount
pub fn step(world: &mut World, cfg: &SimConfig) {
world.tick(cfg.dt); // cfg.dt = 1/60 second
}
}
Benefits:
- Consistent timing across different hardware
- Decoupled from render frame rate
- Predictable performance testing
- Reliable networking
ECS Determinism
BTreeMap Storage
The ECS uses BTreeMap instead of HashMap for archetype storage:
#![allow(unused)]
fn main() {
/// CRITICAL: BTreeMap for deterministic iteration by ID
pub struct ArchetypeSet {
archetypes: BTreeMap<ArchetypeId, Archetype>,
// NOT HashMap - iteration order would be non-deterministic!
}
}
Why this matters:
- HashMap iteration order depends on hash function and memory layout
- BTreeMap iteration order is sorted by key (deterministic)
- AI agents iterate entities in identical order every run
Entity Iteration Order
#![allow(unused)]
fn main() {
// Entities are stored per-archetype
let e1 = world.spawn(); // Empty archetype (ID 0)
let e2 = world.spawn(); // Empty archetype (ID 0)
world.insert(e1, Position::new(1.0, 1.0)); // Moves e1 to Position archetype (ID 1)
// Iteration order: [e2, e1]
// - Archetype 0 (empty) visited first: contains e2
// - Archetype 1 (Position) visited second: contains e1
// NOT spawn order [e1, e2] - but IS deterministic!
}
Preserving Spawn Order
If spawn order is critical, track it explicitly:
#![allow(unused)]
fn main() {
#[derive(Component, Clone, Copy)]
struct SpawnOrder(u64);
// When spawning:
let e = world.spawn();
world.insert(e, SpawnOrder(world.entity_count() as u64));
// When iterating in spawn order:
let mut entities = world.entities_with::<SpawnOrder>();
entities.sort_by_key(|&e| world.get::<SpawnOrder>(e).unwrap().0);
}
Capture & Replay
AstraWeave provides state capture for debugging and determinism validation:
#![allow(unused)]
fn main() {
use astraweave_core::{capture_state, replay_state, SimConfig};
// Capture world state at tick 100
capture_state(100, "save.json", &world)?;
// Later: Replay from saved state
let cfg = SimConfig { dt: 1.0 / 60.0 };
let world = replay_state("save.json", 100, &cfg)?; // Run 100 ticks
}
Snapshot Format
{
"tick": 100,
"world": {
"t": 1.6666,
"next_id": 42,
"obstacles": [[5, 10], [0, 0], [15, 20]]
}
}
Stability guarantees:
- Obstacles sorted for consistent serialization
- JSON format for human readability
- Minimal state for fast save/load
Physics Determinism
Parallel Processing
Physics uses Rayon with deterministic iteration:
#![allow(unused)]
fn main() {
/// Parallel iterator helpers for deterministic physics
impl PhysicsParallelOps {
/// Process rigid bodies in parallel (deterministic order)
pub fn par_process_bodies<F>(&self, bodies: &mut [RigidBody], f: F)
where
F: Fn(&mut RigidBody) + Sync,
{
// Rayon's par_iter preserves element order
bodies.par_iter_mut().for_each(f);
}
}
}
Fixed Timestep
Physics simulation uses fixed timesteps:
#![allow(unused)]
fn main() {
// Physics world with deterministic gravity
let physics = PhysicsWorld::new(Vec3::new(0.0, -9.81, 0.0));
// Fixed step (not variable dt!)
physics.step(1.0 / 60.0); // Always 16.67ms
}
Testing Determinism
Validation Tests
The ECS includes comprehensive determinism tests:
#![allow(unused)]
fn main() {
#[test]
fn test_query_iteration_deterministic() {
let mut world = World::new();
// Spawn entities with components
for i in 0..100 {
let e = world.spawn();
world.insert(e, Position { x: i as f32, y: 0.0 });
}
// First iteration
let order1: Vec<Entity> = world.entities_with::<Position>().collect();
// Second iteration - must be identical
let order2: Vec<Entity> = world.entities_with::<Position>().collect();
assert_eq!(order1, order2, "Iteration order must be deterministic");
}
}
Multi-Run Verification
#![allow(unused)]
fn main() {
#[test]
fn test_determinism_across_runs() {
let results: Vec<Vec<Entity>> = (0..5)
.map(|_| {
let mut world = World::new();
// Same operations each run
setup_world(&mut world);
world.entities_with::<AIAgent>().collect()
})
.collect();
// All runs must produce identical results
for run in &results[1..] {
assert_eq!(&results[0], run);
}
}
}
Best Practices
DO
#![allow(unused)]
fn main() {
// ✅ Use fixed timestep
world.step(FIXED_DT);
// ✅ Use seeded RNG
let mut rng = StdRng::seed_from_u64(12345);
// ✅ Use BTreeMap for deterministic iteration
let entities: BTreeMap<EntityId, Entity> = ...;
// ✅ Sort before iteration if order matters
let mut entities = query.iter().collect::<Vec<_>>();
entities.sort_by_key(|e| e.id());
}
DON’T
#![allow(unused)]
fn main() {
// ❌ Variable timestep
world.step(delta_time);
// ❌ Unseeded RNG
let mut rng = rand::thread_rng();
// ❌ HashMap for gameplay-critical iteration
let entities: HashMap<EntityId, Entity> = ...;
// ❌ Rely on spawn order across archetype changes
let first = world.entities().next(); // Order may change!
}
Performance Impact
Determinism adds minimal overhead:
| Operation | HashMap | BTreeMap | Overhead |
|---|---|---|---|
| Lookup | O(1) | O(log n) | ~10-20 ns |
| Iteration | O(n) | O(n) | None |
| Insert | O(1) | O(log n) | ~10-20 ns |
In practice: For typical games (<10,000 entities), the overhead is negligible (<1% of frame budget).
Related Documentation
- ECS Architecture - Entity-Component-System details
- AI-Native Design - How determinism enables AI
- Performance Optimization - Frame budget analysis
See Also
astraweave-ecs/src/determinism_tests.rs- 763 lines of determinism validationastraweave-core/src/capture_replay.rs- State capture/replay implementationastraweave-physics/src/async_scheduler.rs- Deterministic parallel physics
Tool Validation System
The AstraWeave LLM integration includes a comprehensive tool validation system that ensures LLM-generated plans are safe and comply with the game’s allowed actions.
Overview
The tool validation system works in three layers:
- Schema Validation: Ensures JSON structure matches expected format
- Tool Registry Validation: Verifies all actions are in the allowed tool set
- Engine Validation: Runtime checks for cooldowns, line-of-sight, and other constraints
Components
ToolRegistry
The ToolRegistry defines which tools are available to the LLM:
#![allow(unused)]
fn main() {
pub struct ToolRegistry {
pub tools: Vec<ToolSpec>,
pub constraints: Constraints,
}
pub struct ToolSpec {
pub name: String,
pub args: BTreeMap<String, String>, // argument name -> type
}
}
Validation Process
- LLM Response Parsing: Parse the JSON response into a
PlanIntent - Tool Allowlist Check: Verify each action is in the registry
- Type Validation: Ensure arguments match expected types (future enhancement)
- Runtime Validation: Engine checks constraints during execution
Supported Actions
- MoveTo: Move companion to specified coordinates
- Throw: Throw items (smoke, grenade) at target location
- CoverFire: Provide covering fire at target for duration
- Revive: Revive allied units
Error Handling
The system provides clear error messages for validation failures:
- Invalid JSON format
- Disallowed tools used by LLM
- Missing required arguments
- Type mismatches (future)
Security Features
- Allowlist-only: Only pre-approved actions can be executed
- No dynamic code execution: All actions are statically defined
- Input sanitization: JSON parsing prevents injection attacks
- Constraint enforcement: Runtime validation prevents illegal moves
Usage Example
#![allow(unused)]
fn main() {
use astraweave_llm::{parse_llm_plan, MockLlm, plan_from_llm};
// Create tool registry
let registry = ToolRegistry {
tools: vec![
ToolSpec {
name: "move_to".into(),
args: [("x", "i32"), ("y", "i32")].into_iter().collect(),
}
],
constraints: Constraints::default(),
};
// Parse and validate LLM response
let plan = parse_llm_plan(llm_response, ®istry)?;
// Full end-to-end validation
let plan = plan_from_llm(&client, &world_snapshot, ®istry).await?;
}
Testing
The validation system includes comprehensive tests covering:
- Valid plan parsing
- Invalid JSON handling
- Disallowed tool detection
- Empty plan handling
- All action types
- Error message verification
Run tests with:
cargo test -p astraweave-llm
Future Enhancements
- Argument type validation
- Parameter range checking
- Cost/resource validation
- Complex constraint evaluation
- Custom validation plugins
AI System
AstraWeave’s AI system is the core differentiator of the engine. Unlike traditional game engines where AI is an afterthought, AstraWeave treats AI agents as first-class citizens that interact with the game world through validated tools.
In AstraWeave, AI agents cannot cheat. They must use the same validated game systems as players, ensuring fair and emergent gameplay.
Architecture Overview
graph TB
subgraph "AI Pipeline"
Perception[Perception Bus]
Memory[Agent Memory]
Planning[Planning Layer]
Tools[Tool Sandbox]
Validation[Engine Validation]
end
subgraph "Game World"
ECS[ECS World State]
Physics[Physics System]
Nav[Navigation]
end
ECS --> Perception
Perception --> Memory
Memory --> Planning
Planning --> Tools
Tools --> Validation
Validation --> ECS
Core Components
Perception Bus
The perception system provides AI agents with a filtered view of the world:
#![allow(unused)]
fn main() {
use astraweave_ai::perception::*;
#[derive(Component)]
struct AiAgent {
perception_radius: f32,
perception_filter: PerceptionFilter,
}
fn perception_system(
agents: Query<(Entity, &Transform, &AiAgent)>,
percievables: Query<(Entity, &Transform, &Percievable)>,
mut perception_bus: ResMut<PerceptionBus>,
) {
for (agent_entity, agent_transform, agent) in agents.iter() {
let mut percepts = Vec::new();
for (target, target_transform, percievable) in percievables.iter() {
let distance = agent_transform.translation
.distance(target_transform.translation);
if distance <= agent.perception_radius {
percepts.push(Percept {
entity: target,
position: target_transform.translation,
category: percievable.category,
properties: percievable.properties.clone(),
});
}
}
perception_bus.update(agent_entity, percepts);
}
}
}
See Perception Bus for details.
Planning Layer
AI agents use LLM-based planning to decide actions:
#![allow(unused)]
fn main() {
use astraweave_ai::planning::*;
fn planning_system(
mut agents: Query<(&mut AiPlanner, &PerceptionState)>,
llm: Res<LlmClient>,
) {
for (mut planner, perception) in agents.iter_mut() {
if planner.needs_replan() {
let context = build_context(perception);
match llm.plan(&context, &planner.available_tools) {
Ok(plan) => planner.set_plan(plan),
Err(e) => planner.fallback_behavior(),
}
}
}
}
}
See Planning Layer for details.
Tool Sandbox
All AI actions go through the tool sandbox for validation:
#![allow(unused)]
fn main() {
use astraweave_ai::tools::*;
fn execute_tool_system(
mut agents: Query<&mut AiPlanner>,
mut tool_executor: ResMut<ToolExecutor>,
validator: Res<ToolValidator>,
) {
for mut planner in agents.iter_mut() {
if let Some(tool_call) = planner.next_action() {
match validator.validate(&tool_call) {
ValidationResult::Valid => {
tool_executor.execute(tool_call);
}
ValidationResult::Invalid(reason) => {
planner.action_failed(reason);
}
}
}
}
}
}
See Tool Sandbox for details.
Behavior Trees
For deterministic, reactive behaviors:
#![allow(unused)]
fn main() {
use astraweave_behavior::*;
let patrol_tree = BehaviorTree::new(
Selector::new(vec![
Sequence::new(vec![
Condition::new(|ctx| ctx.enemy_visible()),
Action::new(|ctx| ctx.engage_combat()),
]),
Sequence::new(vec![
Condition::new(|ctx| ctx.at_patrol_point()),
Action::new(|ctx| ctx.next_patrol_point()),
]),
Action::new(|ctx| ctx.move_to_patrol_point()),
])
);
}
See Behavior Trees for details.
AI Agent Configuration
Basic Agent Setup
#![allow(unused)]
fn main() {
fn spawn_companion(world: &mut World) -> Entity {
world.spawn((
Transform::default(),
AiAgent::new()
.with_personality("friendly and helpful")
.with_perception_radius(15.0)
.with_tick_budget(Duration::from_millis(8)),
PerceptionState::default(),
AiPlanner::new(vec![
Tool::move_to(),
Tool::attack(),
Tool::use_item(),
Tool::speak(),
]),
NavAgent::default(),
DialogueCapable::default(),
))
}
}
Tick Budget
AI has a strict time budget per simulation tick:
#![allow(unused)]
fn main() {
let config = AiConfig {
tick_budget_ms: 8,
max_concurrent_plans: 4,
plan_cache_duration: Duration::from_secs(1),
fallback_on_timeout: true,
};
}
If planning exceeds the budget, agents use cached plans or fallback behaviors.
LLM Integration
Ollama Setup
AstraWeave uses Ollama for local LLM inference:
ollama serve
ollama pull hermes2-pro-mistral
Configuration
#![allow(unused)]
fn main() {
let llm_config = LlmConfig {
endpoint: "http://localhost:11434".into(),
model: "hermes2-pro-mistral".into(),
temperature: 0.7,
max_tokens: 256,
timeout: Duration::from_millis(100),
};
}
Tool Calling
AstraWeave uses structured tool calling:
#![allow(unused)]
fn main() {
let tools = vec![
ToolDefinition {
name: "move_to",
description: "Move to a target location",
parameters: json!({
"type": "object",
"properties": {
"target": { "type": "array", "items": { "type": "number" } }
}
}),
},
];
let response = llm.generate_with_tools(prompt, &tools).await?;
}
Performance Considerations
Batching
Group AI queries for efficiency:
#![allow(unused)]
fn main() {
let batch = agents.iter()
.filter(|a| a.needs_replan())
.take(4)
.collect::<Vec<_>>();
let results = llm.batch_plan(&batch).await;
}
Caching
Cache plans to reduce LLM calls:
#![allow(unused)]
fn main() {
let planner = AiPlanner::new(tools)
.with_plan_cache(Duration::from_secs(2))
.with_context_hash(true);
}
Fallback Behaviors
Always define fallbacks:
#![allow(unused)]
fn main() {
impl AiAgent {
fn fallback_behavior(&self) -> Action {
match self.role {
Role::Companion => Action::FollowPlayer,
Role::Guard => Action::Patrol,
Role::Merchant => Action::Idle,
}
}
}
}
Subsections
- Perception Bus - How AI perceives the world
- Planning Layer - LLM-based decision making
- Tool Sandbox - Validated action execution
- Behavior Trees - Deterministic reactive behaviors
Perception Bus
Planning Layer
Tool Sandbox
Behavior Trees
AI Arbiter System
Status: Production Ready
Crate:astraweave-ai(requiresllm_orchestratorfeature)
Documentation: See also Complete Implementation Guide
The AIArbiter is a hybrid AI control system that combines instant tactical decisions (GOAP) with deep strategic reasoning (LLM), achieving zero user-facing latency while maintaining LLM-level intelligence.
The Problem
Traditional game AI faces a dilemma:
| Approach | Latency | Intelligence |
|---|---|---|
| Fast AI (GOAP, BT) | ~100 ns | Limited reasoning |
| Smart AI (LLM) | 13-21 seconds | Deep understanding |
Players either wait 20 seconds for smart AI or get immediate but shallow responses.
The Solution
The arbiter provides zero user-facing latency by:
- Instant GOAP control - Returns tactical actions in 101.7 ns
- Background LLM planning - Generates strategic plans asynchronously
- Seamless transitions - Switches to LLM plans when ready
- Non-blocking polling - Checks LLM completion in 104.7 ns
Performance
| Operation | Latency | Target | Speedup |
|---|---|---|---|
| GOAP update | 101.7 ns | 100 µs | 982× |
| LLM polling | 575 ns | 50 µs | 86× |
| Mode transition | 221.9 ns | 10 µs | 45× |
| Full 3-step cycle | 313.7 ns | — | — |
Scalability
| Agents | Overhead | Frame Budget | Status |
|---|---|---|---|
| 1,000 | 101.7 µs | 0.6% | ✅ |
| 10,000 | 1.02 ms | 6.1% | ✅ |
| 50,000 | 5.09 ms | 30.5% | ⚠️ |
Quick Start
#![allow(unused)]
fn main() {
use astraweave_ai::{AIArbiter, LlmExecutor, GoapOrchestrator, RuleOrchestrator};
use std::sync::Arc;
// Create arbiter
let llm_orch = Arc::new(LlmOrchestrator::new(/* config */));
let runtime = tokio::runtime::Handle::current();
let llm_executor = LlmExecutor::new(llm_orch, runtime);
let goap = Box::new(GoapOrchestrator::new());
let bt = Box::new(RuleOrchestrator);
let mut arbiter = AIArbiter::new(llm_executor, goap, bt);
// Game loop
loop {
let snapshot = build_world_snapshot(/* ... */);
let action = arbiter.update(&snapshot); // 101.7 ns
execute_action(action);
}
}
Architecture
Three-Tier Control System
┌─────────────────────────────────────────────────────┐
│ AIArbiter │
│ (Orchestration Layer - 101.7 ns overhead) │
└────┬────────────────────┬────────────────────┬──────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────────┐ ┌──────────┐
│ GOAP │ │ Qwen3-8B │ │ BT │
│ (3-5 ns) │ │ (13-21s async)│ │ Fallback │
└──────────┘ └──────────────┘ └──────────┘
Mode State Machine
┌──────────────┐
│ GOAP │ ◄─────────┐
│ (Instant AI) │ │
└───────┬──────┘ │
│ │
│ LLM ready │ Plan exhausted
│ │
┌───────▼──────────┐ │
│ ExecutingLLM │───────┘
│ (Step-by-step) │
└──────────────────┘
│
│ Empty plan
▼
┌──────────────┐
│ BehaviorTree │
│ (Fallback) │
└──────────────┘
API Reference
AIArbiter
#![allow(unused)]
fn main() {
pub struct AIArbiter { /* ... */ }
impl AIArbiter {
/// Create new arbiter in GOAP mode
pub fn new(
llm_executor: LlmExecutor,
goap: Box<dyn Orchestrator>,
bt: Box<dyn Orchestrator>,
) -> Self;
/// Set LLM request cooldown (default: 15s)
pub fn with_llm_cooldown(self, cooldown: f32) -> Self;
/// Main control loop - call every frame
pub fn update(&mut self, snap: &WorldSnapshot) -> ActionStep;
/// Get current mode
pub fn mode(&self) -> AIControlMode;
/// Check if LLM task is active
pub fn is_llm_active(&self) -> bool;
/// Get performance metrics
pub fn metrics(&self) -> (
usize, // mode_transitions
usize, // llm_requests
usize, // llm_successes
usize, // llm_failures
usize, // goap_actions
usize, // llm_steps_executed
);
}
}
AIControlMode
#![allow(unused)]
fn main() {
pub enum AIControlMode {
GOAP, // Fast tactical mode
ExecutingLLM { step_index: usize }, // Executing LLM plan
BehaviorTree, // Emergency fallback
}
}
Common Patterns
Pattern 1: Basic Agent
#![allow(unused)]
fn main() {
pub struct AIAgent {
arbiter: AIArbiter,
}
impl AIAgent {
pub fn update(&mut self, snap: &WorldSnapshot) -> ActionStep {
self.arbiter.update(snap)
}
}
}
Pattern 2: Shared LLM Executor
#![allow(unused)]
fn main() {
// Create once, clone for each agent
let base_executor = LlmExecutor::new(llm_orch, runtime);
let agents: Vec<AIAgent> = (0..100)
.map(|_| AIAgent::new(base_executor.clone()))
.collect();
}
Pattern 3: Custom Cooldown
#![allow(unused)]
fn main() {
// Aggressive (more LLM requests)
let arbiter = AIArbiter::new(executor, goap, bt)
.with_llm_cooldown(5.0);
// Passive (fewer LLM requests)
let arbiter = AIArbiter::new(executor, goap, bt)
.with_llm_cooldown(30.0);
}
Pattern 4: Metrics Monitoring
#![allow(unused)]
fn main() {
let (transitions, requests, successes, failures, goap_actions, llm_steps) =
arbiter.metrics();
let success_rate = 100.0 * successes as f64 / requests as f64;
if success_rate < 50.0 {
warn!("LLM success rate low: {:.1}%", success_rate);
}
}
Pattern 5: Mode-Specific Logic
#![allow(unused)]
fn main() {
match arbiter.mode() {
AIControlMode::GOAP => {
ui.show_status("Tactical Mode");
}
AIControlMode::ExecutingLLM { step_index } => {
ui.show_status(&format!("Strategic Step {}", step_index));
ui.show_indicator("LLM Active");
}
AIControlMode::BehaviorTree => {
ui.show_warning("Fallback Mode");
}
}
}
Cooldown Configuration
The LLM cooldown controls how frequently the arbiter requests new strategic plans:
| Cooldown | Use Case |
|---|---|
| 5s | Aggressive - Frequent strategic updates |
| 15s | Default - Balanced performance |
| 30s | Passive - Reduce LLM costs |
| 0s | Immediate - Testing only |
#![allow(unused)]
fn main() {
let arbiter = AIArbiter::new(executor, goap, bt)
.with_llm_cooldown(15.0); // Default
}
Troubleshooting
LLM Never Completes
Symptoms: is_llm_active() always true, never transitions to ExecutingLLM
Causes:
- Ollama not running
- Model not loaded
- Network issues
Fix:
# Verify Ollama is running
ollama list
# Test model directly
ollama run qwen3:8b
High Failure Rate
Symptoms: llm_failures > 50% of requests
Causes:
- Model quality issues
- Bad prompts
- Timeout too short
Fix:
#![allow(unused)]
fn main() {
// Increase cooldown to reduce impact
let arbiter = AIArbiter::new(executor, goap, bt)
.with_llm_cooldown(30.0);
}
Stuck in ExecutingLLM
Symptoms: Same action repeated, step_index doesn’t advance
Causes:
- Plan has duplicate steps
- Plan too long
Fix: Validate plan length before execution:
#![allow(unused)]
fn main() {
if plan.steps.len() > 50 {
warn!("LLM plan too long: {} steps", plan.steps.len());
}
}
Running the Demo
# GOAP-only mode
cargo run -p hello_companion --release
# Arbiter mode (GOAP + Qwen3-8B)
cargo run -p hello_companion --release --features llm_orchestrator -- --arbiter
Expected output:
Frame 0: MoveTo { x: 5, y: 5 } (GOAP)
Frame 1: TakeCover { position: Some((3, 2)) } (GOAP)
[INFO] LLM plan ready: 3 steps
Frame 3: MoveTo { x: 4, y: 0 } (ExecutingLLM[step 1])
Frame 4: TakeCover { position: Some((4, 1)) } (ExecutingLLM[step 2])
Frame 5: Attack { target: 1 } (ExecutingLLM[step 3])
[INFO] Plan exhausted, returning to GOAP
Frame 6: MoveTo { x: 5, y: 5 } (GOAP)
See Also
- Complete Implementation Guide - 8,000+ word deep dive
- Quick Reference - 5-minute API guide
- AI Core Loop - Perception-Reasoning-Planning-Action
- GOAP System - Goal-oriented action planning
- Behavior Trees - Behavior tree integration
Physics System
The AstraWeave physics system provides comprehensive 3D physics simulation through integration with Rapier, a high-performance physics engine written in Rust.
Overview
The physics system runs at a fixed 60Hz tick rate to ensure deterministic simulation.
Key features:
- Rigid Body Dynamics - Full 3D rigid body simulation
- Character Controllers - Player and NPC movement with collision
- Collision Detection - Broad and narrow phase collision
- Spatial Queries - Raycasting, shape casting, overlap tests
- Joints & Constraints - Connect bodies with various joint types
- Continuous Collision Detection - Prevents tunneling for fast objects
Architecture
graph TB
subgraph Physics Pipeline
A[Collect Inputs] --> B[Broad Phase]
B --> C[Narrow Phase]
C --> D[Constraint Solver]
D --> E[Position Integration]
E --> F[Sync to ECS]
end
G[ECS World] --> A
F --> G
Core Components
RigidBody
Represents a physics-simulated body:
#![allow(unused)]
fn main() {
use astraweave_physics::{RigidBody, RigidBodyType};
let body = RigidBody {
body_type: RigidBodyType::Dynamic,
mass: 1.0,
linear_damping: 0.1,
angular_damping: 0.1,
gravity_scale: 1.0,
..Default::default()
};
}
Body types:
Dynamic- Fully simulated, responds to forcesStatic- Immovable, used for environmentKinematic- Moved by code, pushes dynamic bodies
Collider
Defines collision shapes:
#![allow(unused)]
fn main() {
use astraweave_physics::{Collider, ColliderShape};
let collider = Collider {
shape: ColliderShape::Box { half_extents: Vec3::new(1.0, 1.0, 1.0) },
friction: 0.5,
restitution: 0.3,
sensor: false,
..Default::default()
};
}
Available shapes:
Box- Axis-aligned boxSphere- Perfect sphereCapsule- Cylinder with spherical capsCylinder- Circular cylinderConvexHull- Convex meshTriMesh- Triangle mesh (static only)HeightField- Terrain heightmap
CharacterController
Handles player/NPC movement:
#![allow(unused)]
fn main() {
use astraweave_physics::CharacterController;
let controller = CharacterController {
height: 1.8,
radius: 0.3,
step_height: 0.3,
max_slope: 45.0_f32.to_radians(),
..Default::default()
};
}
Spatial Queries
Raycasting
#![allow(unused)]
fn main() {
use astraweave_physics::{RaycastQuery, RaycastHit};
let query = RaycastQuery {
origin: Vec3::new(0.0, 5.0, 0.0),
direction: Vec3::NEG_Y,
max_distance: 100.0,
filter: CollisionFilter::default(),
};
if let Some(hit) = physics.raycast(&query) {
println!("Hit entity {:?} at distance {}", hit.entity, hit.distance);
}
}
Shape Casting
#![allow(unused)]
fn main() {
use astraweave_physics::{ShapeCastQuery, ColliderShape};
let query = ShapeCastQuery {
shape: ColliderShape::Sphere { radius: 0.5 },
origin: start_pos,
direction: velocity.normalize(),
max_distance: velocity.length(),
};
let hits = physics.shape_cast(&query);
}
Overlap Tests
#![allow(unused)]
fn main() {
use astraweave_physics::OverlapQuery;
let query = OverlapQuery {
shape: ColliderShape::Sphere { radius: 5.0 },
position: explosion_center,
filter: CollisionFilter::default(),
};
for entity in physics.overlap(&query) {
apply_explosion_damage(entity);
}
}
Collision Filtering
Control which objects can collide:
#![allow(unused)]
fn main() {
use astraweave_physics::{CollisionFilter, CollisionGroup};
let player_filter = CollisionFilter {
membership: CollisionGroup::PLAYER,
filter: CollisionGroup::WORLD | CollisionGroup::ENEMY | CollisionGroup::PROJECTILE,
};
}
Performance Optimization
Spatial Hash
The physics system uses a spatial hash for broad-phase acceleration:
#![allow(unused)]
fn main() {
use astraweave_physics::SpatialHash;
let spatial = SpatialHash::new(10.0);
spatial.insert(entity, aabb);
let nearby = spatial.query_aabb(&query_aabb);
}
Async Scheduling
For large simulations, physics can run asynchronously:
#![allow(unused)]
fn main() {
use astraweave_physics::AsyncPhysicsScheduler;
let scheduler = AsyncPhysicsScheduler::new(4);
scheduler.step(&mut physics_world, delta_time).await;
}
Integration with ECS
Physics components sync automatically with ECS transforms:
#![allow(unused)]
fn main() {
fn physics_sync_system(
query: Query<(&mut Transform, &RigidBodyHandle)>,
physics: Res<PhysicsWorld>,
) {
for (mut transform, handle) in query.iter_mut() {
if let Some(body) = physics.get_body(*handle) {
transform.translation = body.position();
transform.rotation = body.rotation();
}
}
}
}
See Also
- API Documentation -
astraweave_physicsAPI - Character Controller Tutorial
- Navigation System - Pathfinding integration
- Deterministic Simulation
Rendering System
AstraWeave’s rendering system is built on top of WGPU, providing a modern, high-performance graphics pipeline with support for physically-based rendering (PBR), advanced lighting techniques, and post-processing effects.
Architecture Overview
The rendering system follows a modular architecture with clear separation of concerns:
graph TD
A[Render Graph] --> B[Material System]
A --> C[Lighting System]
A --> D[Post-Processing]
B --> E[Texture Streaming]
C --> F[Clustered Forward]
D --> G[Bloom/Tonemap]
E --> H[GPU Upload]
F --> I[Light Culling]
Key Components
- Render Graph: Declarative frame composition with automatic resource barriers
- Material System: PBR materials with metallic-roughness workflow
- Lighting: Clustered forward rendering supporting thousands of dynamic lights
- Texture Streaming: Virtual texture system with priority-based loading
- Post-Processing: Modular effects stack (bloom, tonemap, FXAA, etc.)
The rendering system is designed to scale from integrated graphics to high-end GPUs, with automatic quality adaptation based on detected hardware capabilities.
WGPU-Based Renderer
AstraWeave uses WGPU as its graphics abstraction layer, providing cross-platform support for Vulkan, Metal, DirectX 12, and WebGPU.
Initialization
#![allow(unused)]
fn main() {
use astraweave_render::{RenderContext, RenderConfig};
// Initialize the renderer
let config = RenderConfig {
width: 1920,
height: 1080,
vsync: true,
msaa_samples: 4,
..Default::default()
};
let render_ctx = RenderContext::new(&window, config).await?;
}
Device and Queue Management
The render context manages the WGPU device and queue lifecycle:
#![allow(unused)]
fn main() {
// Access device for resource creation
let device = render_ctx.device();
let queue = render_ctx.queue();
// Create a texture
let texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("game_texture"),
size: wgpu::Extent3d {
width: 1024,
height: 1024,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8UnormSrgb,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
}
PBR Materials and Lighting
AstraWeave implements a complete PBR pipeline based on the metallic-roughness workflow, closely following theglTF 2.0 specification.
Material Definition
#![allow(unused)]
fn main() {
use astraweave_render::material::{Material, PbrMaterial};
let material = PbrMaterial {
base_color: [1.0, 1.0, 1.0, 1.0],
base_color_texture: Some(texture_handle),
metallic: 0.0,
roughness: 0.5,
metallic_roughness_texture: Some(mr_texture),
normal_texture: Some(normal_map),
emissive: [0.0, 0.0, 0.0],
emissive_texture: None,
occlusion_texture: Some(ao_map),
alpha_mode: AlphaMode::Opaque,
double_sided: false,
};
// Register material with renderer
let material_id = render_ctx.register_material(material);
}
Material Properties
| Property | Type | Description |
|---|---|---|
base_color | [f32; 4] | RGB color with alpha channel |
metallic | f32 | Metallic factor (0 = dielectric, 1 = metal) |
roughness | f32 | Surface roughness (0 = smooth, 1 = rough) |
emissive | [f32; 3] | Emissive color (HDR) |
normal_texture | Option<Handle> | Tangent-space normal map |
occlusion_texture | Option<Handle> | Ambient occlusion map |
Use texture packing to reduce memory bandwidth: combine metallic (B channel), roughness (G channel), and occlusion (R channel) into a single texture.
Lighting Model
The PBR shader implements a Cook-Torrance BRDF with:
- Diffuse: Lambertian diffuse term
- Specular: GGX/Trowbridge-Reitz normal distribution
- Fresnel: Schlick’s approximation
- Geometry: Smith’s shadowing-masking function
#![allow(unused)]
fn main() {
// BRDF calculation (shader pseudo-code)
fn pbr_brdf(
N: vec3<f32>, // Normal
V: vec3<f32>, // View direction
L: vec3<f32>, // Light direction
F0: vec3<f32>, // Fresnel reflectance at 0°
roughness: f32,
metallic: f32,
) -> vec3<f32> {
let H = normalize(V + L);
let NdotH = max(dot(N, H), 0.0);
let NdotV = max(dot(N, V), 0.0);
let NdotL = max(dot(N, L), 0.0);
// Distribution term (GGX)
let D = distribution_ggx(NdotH, roughness);
// Geometry term (Smith)
let G = geometry_smith(NdotV, NdotL, roughness);
// Fresnel term (Schlick)
let F = fresnel_schlick(max(dot(H, V), 0.0), F0);
// Cook-Torrance BRDF
let specular = (D * G * F) / (4.0 * NdotV * NdotL + 0.0001);
let kD = (vec3(1.0) - F) * (1.0 - metallic);
let diffuse = kD * base_color.rgb / PI;
return (diffuse + specular) * radiance * NdotL;
}
}
Clustered Forward Rendering
Clustered forward rendering divides the view frustum into a 3D grid of clusters, assigning lights to clusters for efficient culling.
Cluster Configuration
#![allow(unused)]
fn main() {
use astraweave_render::lighting::{ClusteredLightingConfig, LightCluster};
let cluster_config = ClusteredLightingConfig {
tile_size_x: 16,
tile_size_y: 16,
z_slices: 24,
max_lights_per_cluster: 256,
};
render_ctx.configure_clustered_lighting(cluster_config);
}
Cluster Grid
graph LR
A[View Frustum] --> B[XY Tiling]
B --> C[Z Slicing]
C --> D[Cluster Grid]
D --> E[Light Assignment]
E --> F[Per-Cluster Light Lists]
The frustum is divided into:
- XY tiles: Screen-space subdivision (typically 16x16 pixels)
- Z slices: Exponential depth slices for better near-field distribution
Adding Lights
#![allow(unused)]
fn main() {
use astraweave_render::lighting::{PointLight, SpotLight, DirectionalLight};
// Point light
let point_light = PointLight {
position: [0.0, 5.0, 0.0],
color: [1.0, 0.9, 0.8],
intensity: 100.0,
radius: 10.0,
falloff: 2.0, // Inverse square falloff
};
render_ctx.add_light(point_light);
// Directional light (sun)
let sun = DirectionalLight {
direction: [-0.3, -1.0, -0.5],
color: [1.0, 0.95, 0.9],
intensity: 5.0,
cast_shadows: true,
shadow_cascade_count: 4,
};
render_ctx.set_directional_light(sun);
}
Light Culling
The system performs GPU-based light culling using a compute shader:
#![allow(unused)]
fn main() {
// Compute shader for light culling (WGSL)
@compute @workgroup_size(16, 16, 1)
fn cull_lights(
@builtin(global_invocation_id) global_id: vec3<u32>,
) {
let cluster_id = compute_cluster_id(global_id);
let cluster_aabb = get_cluster_bounds(cluster_id);
var light_count: u32 = 0u;
for (var i = 0u; i < light_buffer.count; i++) {
let light = light_buffer.lights[i];
if (sphere_aabb_intersection(light.position, light.radius, cluster_aabb)) {
light_indices[cluster_id * MAX_LIGHTS + light_count] = i;
light_count++;
}
}
cluster_light_counts[cluster_id] = light_count;
}
}
Clustered forward rendering requires compute shader support. Fallback to traditional forward rendering is provided for older hardware.
Post-Processing Effects
AstraWeave provides a flexible post-processing stack with composable effects.
Effect Pipeline
#![allow(unused)]
fn main() {
use astraweave_render::post::{PostProcessStack, BloomEffect, TonemapEffect, FxaaEffect};
let mut post_stack = PostProcessStack::new();
// Add bloom
post_stack.add_effect(BloomEffect {
threshold: 1.0,
intensity: 0.5,
blur_passes: 5,
});
// Add tonemapping
post_stack.add_effect(TonemapEffect {
exposure: 1.0,
operator: TonemapOperator::AcesFilmic,
});
// Add anti-aliasing
post_stack.add_effect(FxaaEffect {
quality: FxaaQuality::High,
});
render_ctx.set_post_processing(post_stack);
}
Available Effects
| Effect | Description | Performance Impact |
|---|---|---|
| Bloom | HDR glow effect | Medium |
| Tonemap | HDR to LDR mapping | Low |
| FXAA | Fast approximate anti-aliasing | Low |
| SMAA | Subpixel morphological anti-aliasing | Medium |
| Depth of Field | Bokeh blur based on depth | High |
| Motion Blur | Velocity-based blur | Medium |
| Color Grading | LUT-based color correction | Low |
Custom Post-Processing Effects
#![allow(unused)]
fn main() {
use astraweave_render::post::{PostEffect, PostEffectContext};
struct CustomVignetteEffect {
intensity: f32,
radius: f32,
}
impl PostEffect for CustomVignetteEffect {
fn apply(&self, ctx: &mut PostEffectContext) -> Result<()> {
let shader = ctx.load_shader("vignette.wgsl")?;
ctx.bind_uniform("intensity", &self.intensity);
ctx.bind_uniform("radius", &self.radius);
ctx.bind_texture(0, ctx.input_texture());
ctx.dispatch_fullscreen(shader)?;
Ok(())
}
}
}
Texture Streaming
The texture streaming system manages GPU memory efficiently by loading textures on-demand with priority-based eviction.
Virtual Texture System
#![allow(unused)]
fn main() {
use astraweave_render::texture::{TextureStreamingConfig, VirtualTexture};
let streaming_config = TextureStreamingConfig {
budget_mb: 512,
min_mip_level: 0,
max_mip_level: 12,
load_bias: 0.0,
anisotropy: 16,
};
render_ctx.configure_texture_streaming(streaming_config);
}
Loading Textures
#![allow(unused)]
fn main() {
use astraweave_render::texture::TextureLoader;
// Load texture with streaming
let texture = TextureLoader::new()
.with_streaming(true)
.with_mip_levels(8)
.with_compression(TextureCompression::BC7)
.load("assets/textures/ground_albedo.png")?;
// Set priority (higher = load sooner)
texture.set_priority(1.0);
}
Mip Streaming
The system automatically selects mip levels based on:
- Distance to camera
- Screen coverage
- Available GPU memory
- User-defined priority
graph TD
A[Texture Request] --> B{In Budget?}
B -->|Yes| C[Load Full Resolution]
B -->|No| D[Calculate Required Mip]
D --> E[Load Mip Level]
E --> F{Budget Exceeded?}
F -->|Yes| G[Evict Low Priority]
F -->|No| H[Complete]
G --> H
C --> H
Pre-generate mip chains offline and compress textures to reduce load times. AstraWeave supports BC1-BC7, ASTC, and ETC2 compression formats.
Performance Optimization
GPU Profiling
#![allow(unused)]
fn main() {
use astraweave_render::profiling::GpuProfiler;
let profiler = GpuProfiler::new(&render_ctx);
profiler.begin_frame();
{
profiler.begin_scope("Shadow Pass");
render_shadows(&mut render_ctx);
profiler.end_scope();
profiler.begin_scope("Main Pass");
render_main_scene(&mut render_ctx);
profiler.end_scope();
profiler.begin_scope("Post Processing");
apply_post_effects(&mut render_ctx);
profiler.end_scope();
}
profiler.end_frame();
// Print timing report
println!("{}", profiler.report());
}
Instancing
Use GPU instancing for rendering many identical meshes:
#![allow(unused)]
fn main() {
use astraweave_render::mesh::InstancedMesh;
let instances: Vec<InstanceData> = trees
.iter()
.map(|tree| InstanceData {
transform: tree.transform.matrix(),
color: tree.color,
})
.collect();
render_ctx.draw_instanced(tree_mesh, &instances);
}
Occlusion Culling
#![allow(unused)]
fn main() {
use astraweave_render::culling::OcclusionCulling;
let mut occlusion = OcclusionCulling::new(&render_ctx);
// First pass: render occluders
occlusion.render_occluders(&occluder_meshes);
// Second pass: test visibility
let visible_objects = occlusion.test_visibility(&all_objects);
// Third pass: render only visible objects
render_objects(&visible_objects);
}
Code Examples
Complete Rendering Loop
use astraweave_render::{RenderContext, Camera, Scene};
use winit::event_loop::EventLoop;
fn main() -> Result<()> {
let event_loop = EventLoop::new();
let window = Window::new(&event_loop)?;
let mut render_ctx = RenderContext::new(&window, Default::default()).await?;
let mut scene = Scene::new();
let mut camera = Camera::perspective(60.0, 16.0 / 9.0, 0.1, 1000.0);
event_loop.run(move |event, _, control_flow| {
match event {
Event::RedrawRequested(_) => {
// Update camera
camera.update(&input_state);
// Begin frame
let mut frame = render_ctx.begin_frame().unwrap();
// Render scene
frame.render_scene(&scene, &camera);
// Present
frame.present();
}
_ => {}
}
});
}
Dynamic Material Updates
#![allow(unused)]
fn main() {
// Update material properties at runtime
let material = render_ctx.get_material_mut(material_id)?;
material.base_color = [1.0, 0.0, 0.0, 1.0]; // Change to red
material.roughness = 0.8; // Make rougher
render_ctx.mark_material_dirty(material_id);
}
Related Documentation
- Asset Pipeline - Asset loading and preprocessing
- Scene Graph - Scene organization and transforms
- Editor Integration - Editor viewport rendering
- Performance Guide - Advanced optimization techniques
API Reference
For complete API documentation, see:
Audio System
AstraWeave’s audio system provides comprehensive spatial audio, dialogue management, and music/SFX playback capabilities using modern Rust audio libraries (kira/rodio).
Architecture Overview
The audio system is designed for real-time game audio with low latency, spatial positioning, and dynamic mixing.
graph TD
A[Audio Engine] --> B[Spatial Audio]
A --> C[Dialogue Runtime]
A --> D[Music Manager]
A --> E[SFX Manager]
B --> F[3D Positioning]
B --> G[Attenuation]
C --> H[Voice System]
D --> I[Adaptive Music]
E --> J[Sound Pool]
Key Components
- Audio Engine: Core audio processing and device management
- Spatial Audio: 3D positional audio with HRTF and distance attenuation
- Dialogue Runtime: Conversation system with voice line playback
- Music Manager: Adaptive music with crossfading and layering
- SFX Manager: Sound effect pooling and priority management
The audio system supports multiple backends (WASAPI, ALSA, CoreAudio) and automatically selects the best available option for the platform.
Audio Engine Initialization
Basic Setup
#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, AudioConfig};
let config = AudioConfig {
sample_rate: 48000,
buffer_size: 512,
num_channels: 2,
enable_spatial_audio: true,
max_voices: 64,
..Default::default()
};
let mut audio_engine = AudioEngine::new(config)?;
}
Backend Selection
#![allow(unused)]
fn main() {
use astraweave_audio::backend::{AudioBackend, BackendType};
// Explicitly select backend
let backend = AudioBackend::new(BackendType::WASAPI)?;
let audio_engine = AudioEngine::with_backend(backend, config)?;
// Or use automatic selection
let audio_engine = AudioEngine::auto_detect(config)?;
}
For lowest latency on Windows, use WASAPI in exclusive mode. For compatibility, use the automatic backend selection.
Spatial Audio
AstraWeave’s spatial audio system provides realistic 3D sound positioning with distance attenuation, obstruction, and HRTF (Head-Related Transfer Function) support.
3D Audio Listener
#![allow(unused)]
fn main() {
use astraweave_audio::spatial::{AudioListener, ListenerOrientation};
// Create listener (typically attached to camera/player)
let mut listener = AudioListener::new();
// Update listener position and orientation each frame
listener.set_position([player_pos.x, player_pos.y, player_pos.z]);
listener.set_orientation(ListenerOrientation {
forward: [forward.x, forward.y, forward.z],
up: [up.x, up.y, up.z],
});
audio_engine.set_listener(listener);
}
Spatial Sound Sources
#![allow(unused)]
fn main() {
use astraweave_audio::spatial::{SpatialSound, AttenuationModel};
// Load and play spatial sound
let sound = audio_engine.load_sound("assets/audio/footstep.ogg")?;
let spatial_sound = SpatialSound::new(sound)
.with_position([10.0, 0.0, 5.0])
.with_velocity([0.0, 0.0, 0.0]) // For Doppler effect
.with_attenuation(AttenuationModel::InverseDistance {
reference_distance: 1.0,
max_distance: 50.0,
rolloff_factor: 1.0,
})
.with_cone(90.0, 180.0, 0.5) // Directional sound
.with_volume(0.8);
let handle = audio_engine.play_spatial(spatial_sound)?;
}
Attenuation Models
AstraWeave supports multiple distance attenuation models:
| Model | Formula | Use Case |
|---|---|---|
| Linear | 1 - d/max | Simple, predictable falloff |
| Inverse Distance | ref / (ref + rolloff * (d - ref)) | Realistic physical attenuation |
| Exponential | (d / ref)^(-rolloff) | Dramatic falloff for emphasis |
| Custom | User-defined curve | Complete control |
#![allow(unused)]
fn main() {
use astraweave_audio::spatial::AttenuationModel;
// Custom attenuation curve
let custom_attenuation = AttenuationModel::Custom(Box::new(|distance| {
if distance < 5.0 {
1.0
} else if distance < 20.0 {
1.0 - ((distance - 5.0) / 15.0).powi(2)
} else {
0.0
}
}));
}
Doppler Effect
#![allow(unused)]
fn main() {
// Enable Doppler shift based on velocity
spatial_sound.set_doppler_factor(1.0); // 1.0 = realistic, 0.0 = disabled
spatial_sound.set_velocity([velocity.x, velocity.y, velocity.z]);
}
Obstruction and Occlusion
#![allow(unused)]
fn main() {
use astraweave_audio::spatial::Obstruction;
// Simple obstruction (lowpass filter based on raycast)
if raycast_to_listener_blocked {
spatial_sound.set_obstruction(Obstruction {
direct_gain: 0.3, // Reduce direct sound
lowpass_cutoff: 1000.0, // Apply lowpass filter
});
} else {
spatial_sound.clear_obstruction();
}
}
graph LR
A[Sound Source] --> B{Line of Sight?}
B -->|Clear| C[Direct Sound]
B -->|Blocked| D[Obstructed Sound]
D --> E[Apply Lowpass]
D --> F[Reduce Gain]
E --> G[Audio Output]
F --> G
C --> G
Dialogue Runtime
The dialogue runtime manages conversation playback with support for branching, voice lines, and subtitles.
Dialogue Graph
#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::{DialogueGraph, DialogueNode, DialogueChoice};
let mut dialogue = DialogueGraph::new();
// Add dialogue nodes
let node1 = dialogue.add_node(DialogueNode {
id: "greeting",
speaker: "NPC_Guard",
text: "Halt! What's your business here?",
voice_line: Some("assets/voice/guard_greeting.ogg"),
duration: 3.0,
});
let node2 = dialogue.add_node(DialogueNode {
id: "response_friendly",
speaker: "Player",
text: "Just passing through, friend.",
voice_line: Some("assets/voice/player_friendly.ogg"),
duration: 2.5,
});
let node3 = dialogue.add_node(DialogueNode {
id: "response_hostile",
speaker: "Player",
text: "None of your concern.",
voice_line: Some("assets/voice/player_hostile.ogg"),
duration: 2.0,
});
// Add choices
dialogue.add_choice(node1, DialogueChoice {
text: "Be friendly",
next_node: node2,
condition: None,
});
dialogue.add_choice(node1, DialogueChoice {
text: "Be hostile",
next_node: node3,
condition: None,
});
}
Playing Dialogue
#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::DialoguePlayer;
let mut player = DialoguePlayer::new(&audio_engine);
// Start dialogue
player.start_dialogue(dialogue, "greeting")?;
// Update each frame
loop {
player.update(delta_time)?;
if let Some(current_node) = player.current_node() {
println!("Speaker: {}", current_node.speaker);
println!("Text: {}", current_node.text);
// Display choices when node finishes
if player.is_waiting_for_choice() {
let choices = player.available_choices();
for (idx, choice) in choices.iter().enumerate() {
println!("{}: {}", idx, choice.text);
}
}
}
if player.is_finished() {
break;
}
}
// Handle player input
player.select_choice(0)?; // Select first choice
}
Voice System
#![allow(unused)]
fn main() {
use astraweave_audio::voice::{VoiceConfig, VoiceEffect};
// Configure voice processing
let voice_config = VoiceConfig {
normalize: true,
compress: true, // Dynamic range compression
eq_preset: Some(VoiceEffect::RadioFilter), // Apply effects
ducking: true, // Duck music/sfx during dialogue
};
player.set_voice_config(voice_config);
}
Subtitle Synchronization
#![allow(unused)]
fn main() {
use astraweave_audio::dialogue::SubtitleTrack;
// Load subtitle track
let subtitles = SubtitleTrack::load("assets/dialogue/quest1.srt")?;
player.set_subtitles(subtitles);
// Get current subtitle
if let Some(subtitle) = player.current_subtitle() {
ui.draw_subtitle(subtitle.text, subtitle.speaker);
}
}
Ensure voice line durations match actual audio file lengths. Use the `analyze_duration()` function to automatically detect durations from audio files.
Music Manager
The music manager provides adaptive music with layering, crossfading, and dynamic transitions.
Music Tracks
#![allow(unused)]
fn main() {
use astraweave_audio::music::{MusicManager, MusicTrack, MusicLayer};
let mut music_manager = MusicManager::new(&audio_engine);
// Load music track with layers
let mut combat_music = MusicTrack::new("combat");
combat_music.add_layer(MusicLayer {
name: "percussion",
file: "assets/music/combat_percussion.ogg",
volume: 1.0,
loop_start: 0.0,
loop_end: 32.0, // Loop point in seconds
});
combat_music.add_layer(MusicLayer {
name: "strings",
file: "assets/music/combat_strings.ogg",
volume: 0.8,
loop_start: 0.0,
loop_end: 32.0,
});
combat_music.add_layer(MusicLayer {
name: "brass",
file: "assets/music/combat_brass.ogg",
volume: 0.6,
loop_start: 0.0,
loop_end: 32.0,
});
music_manager.register_track(combat_music);
}
Adaptive Music System
#![allow(unused)]
fn main() {
// Play music with intensity control
music_manager.play("combat")?;
// Dynamically adjust layers based on game state
match combat_intensity {
CombatIntensity::Low => {
music_manager.set_layer_volume("combat", "percussion", 1.0);
music_manager.set_layer_volume("combat", "strings", 0.3);
music_manager.set_layer_volume("combat", "brass", 0.0);
}
CombatIntensity::Medium => {
music_manager.set_layer_volume("combat", "percussion", 1.0);
music_manager.set_layer_volume("combat", "strings", 0.8);
music_manager.set_layer_volume("combat", "brass", 0.4);
}
CombatIntensity::High => {
music_manager.set_layer_volume("combat", "percussion", 1.0);
music_manager.set_layer_volume("combat", "strings", 1.0);
music_manager.set_layer_volume("combat", "brass", 1.0);
}
}
}
Crossfading and Transitions
#![allow(unused)]
fn main() {
use astraweave_audio::music::TransitionType;
// Immediate transition
music_manager.transition_to("exploration", TransitionType::Immediate)?;
// Crossfade over 2 seconds
music_manager.transition_to("exploration", TransitionType::Crossfade(2.0))?;
// Wait for musical bar/beat
music_manager.transition_to("exploration", TransitionType::OnBeat {
bars: 1, // Wait for next bar
fade_duration: 1.0,
})?;
// Sequential (finish current, then play next)
music_manager.transition_to("victory", TransitionType::Sequential)?;
}
Music States
#![allow(unused)]
fn main() {
use astraweave_audio::music::MusicState;
// Create music state machine
let mut music_states = MusicState::new();
music_states.add_state("explore", "exploration_music");
music_states.add_state("combat", "combat_music");
music_states.add_state("boss", "boss_music");
// Define transitions
music_states.add_transition("explore", "combat", TransitionType::Crossfade(1.5));
music_states.add_transition("combat", "explore", TransitionType::OnBeat {
bars: 2,
fade_duration: 2.0,
});
// Trigger state changes
music_states.change_state("combat")?;
}
stateDiagram-v2
[*] --> Exploration
Exploration --> Combat: Enemy Detected
Combat --> Exploration: Combat Ended
Combat --> Boss: Boss Appears
Boss --> Victory: Boss Defeated
Victory --> Exploration: Return
SFX Manager
The SFX manager handles sound effect pooling, priority management, and one-shot sounds.
Sound Pool
#![allow(unused)]
fn main() {
use astraweave_audio::sfx::{SfxManager, SoundPool};
let mut sfx_manager = SfxManager::new(&audio_engine);
// Create sound pool for common effects
let footstep_pool = SoundPool::new()
.add_variant("assets/sfx/footstep_01.ogg")
.add_variant("assets/sfx/footstep_02.ogg")
.add_variant("assets/sfx/footstep_03.ogg")
.add_variant("assets/sfx/footstep_04.ogg")
.with_random_pitch(0.95, 1.05)
.with_max_instances(4); // Limit concurrent sounds
sfx_manager.register_pool("footstep", footstep_pool);
}
Playing Sound Effects
#![allow(unused)]
fn main() {
// Play from pool (automatically selects variant)
sfx_manager.play("footstep")?;
// Play with position
sfx_manager.play_at("footstep", player_position)?;
// Play with custom parameters
sfx_manager.play_with("explosion", SfxParams {
position: Some(explosion_pos),
volume: 1.0,
pitch: 1.0,
priority: SoundPriority::High,
})?;
}
Priority System
#![allow(unused)]
fn main() {
use astraweave_audio::sfx::SoundPriority;
// Define sound priorities
pub enum SoundPriority {
Critical = 100, // UI, dialogue
High = 75, // Explosions, important events
Medium = 50, // Gunfire, abilities
Low = 25, // Footsteps, ambient
VeryLow = 10, // Background details
}
// Priority-based voice stealing
sfx_manager.set_max_voices(32);
sfx_manager.set_voice_stealing(true); // Steal low priority when full
}
Sound Categories
#![allow(unused)]
fn main() {
use astraweave_audio::sfx::SoundCategory;
// Create categories for volume control
sfx_manager.create_category("master", 1.0);
sfx_manager.create_category("sfx", 0.8);
sfx_manager.create_category("ui", 1.0);
sfx_manager.create_category("ambient", 0.6);
// Assign sounds to categories
sfx_manager.set_category("footstep", "sfx");
sfx_manager.set_category("button_click", "ui");
// Adjust category volumes
sfx_manager.set_category_volume("sfx", 0.5);
}
Audio Effects and Processing
Built-in Effects
#![allow(unused)]
fn main() {
use astraweave_audio::effects::{Effect, Reverb, LowPassFilter, HighPassFilter};
// Add reverb to spatial sound
let reverb = Reverb {
room_size: 0.7,
damping: 0.5,
wet: 0.3,
dry: 0.7,
};
spatial_sound.add_effect(Effect::Reverb(reverb));
// Lowpass filter for underwater effect
let underwater_filter = LowPassFilter {
cutoff: 800.0,
resonance: 1.0,
};
audio_engine.add_global_effect(Effect::LowPass(underwater_filter));
}
Custom DSP
#![allow(unused)]
fn main() {
use astraweave_audio::dsp::{AudioProcessor, AudioBuffer};
struct CustomDistortion {
gain: f32,
threshold: f32,
}
impl AudioProcessor for CustomDistortion {
fn process(&mut self, buffer: &mut AudioBuffer) {
for sample in buffer.samples_mut() {
*sample *= self.gain;
*sample = sample.clamp(-self.threshold, self.threshold);
}
}
}
// Apply custom processor
let distortion = CustomDistortion {
gain: 2.0,
threshold: 0.8,
};
sfx_manager.add_processor("distortion", Box::new(distortion));
}
Performance Optimization
Voice Limiting
#![allow(unused)]
fn main() {
// Limit total active voices
audio_engine.set_max_voices(64);
// Per-pool limiting
let gunshot_pool = SoundPool::new()
.add_variant("gunshot.ogg")
.with_max_instances(8); // Max 8 simultaneous gunshots
}
Streaming vs. Loaded
#![allow(unused)]
fn main() {
// Load small sounds into memory
let ui_click = audio_engine.load_sound_static("click.ogg")?;
// Stream large files (music, ambience)
let music = audio_engine.load_sound_streaming("music.ogg")?;
}
Audio Budget
#![allow(unused)]
fn main() {
use astraweave_audio::budget::AudioBudget;
let budget = AudioBudget {
max_memory_mb: 256,
max_streaming_sources: 4,
max_static_sources: 60,
};
audio_engine.set_budget(budget);
}
Complete Example
Game Audio System
#![allow(unused)]
fn main() {
use astraweave_audio::*;
pub struct GameAudio {
engine: AudioEngine,
music: MusicManager,
sfx: SfxManager,
dialogue: DialoguePlayer,
}
impl GameAudio {
pub fn new() -> Result<Self> {
let mut engine = AudioEngine::new(AudioConfig::default())?;
let music = MusicManager::new(&engine);
let sfx = SfxManager::new(&engine);
let dialogue = DialoguePlayer::new(&engine);
Ok(Self { engine, music, sfx, dialogue })
}
pub fn update(&mut self, delta_time: f32, listener_pos: Vec3, listener_forward: Vec3) {
// Update listener
let mut listener = AudioListener::new();
listener.set_position([listener_pos.x, listener_pos.y, listener_pos.z]);
listener.set_orientation(ListenerOrientation {
forward: [listener_forward.x, listener_forward.y, listener_forward.z],
up: [0.0, 1.0, 0.0],
});
self.engine.set_listener(listener);
// Update managers
self.music.update(delta_time);
self.dialogue.update(delta_time).ok();
}
pub fn play_footstep(&mut self, position: Vec3) {
self.sfx.play_at("footstep", [position.x, position.y, position.z]).ok();
}
pub fn start_combat_music(&mut self) {
self.music.transition_to("combat", TransitionType::Crossfade(1.5)).ok();
}
}
}
Related Documentation
- Asset Pipeline - Audio asset loading and processing
- ECS Integration - Audio components and systems
- Editor Tools - Audio editing and preview
- Performance Guide - Audio optimization techniques
API Reference
For complete API documentation, see:
Navigation System
AstraWeave’s navigation system provides robust pathfinding and agent movement with support for dynamic navmesh generation, A* pathfinding, local steering, and obstacle avoidance.
Architecture Overview
The navigation system consists of multiple layers working together to enable intelligent agent movement.
graph TD
A[Navigation System] --> B[Navmesh Generation]
A --> C[Pathfinding]
A --> D[Steering]
B --> E[Voxelization]
B --> F[Region Building]
C --> G[A* Search]
D --> H[Obstacle Avoidance]
D --> I[Local Movement]
Key Components
- Navmesh Generation: Automatic navigation mesh creation from level geometry
- Pathfinding: A* algorithm with hierarchical path optimization
- Steering: Local navigation with obstacle avoidance and crowd simulation
- Dynamic Updates: Runtime navmesh modification for destructible environments
- Off-Mesh Links: Manual connections for jumps, ladders, and teleports
The navigation system is based on Recast & Detour algorithms, providing industry-standard navmesh generation and pathfinding.
Navmesh Generation
Basic Navmesh Creation
#![allow(unused)]
fn main() {
use astraweave_nav::{NavmeshBuilder, NavmeshConfig};
let config = NavmeshConfig {
cell_size: 0.3, // XZ plane cell size
cell_height: 0.2, // Y axis cell size
agent_height: 2.0, // Agent height in meters
agent_radius: 0.6, // Agent radius in meters
agent_max_climb: 0.9, // Max step height
agent_max_slope: 45.0, // Max walkable slope in degrees
region_min_size: 8.0, // Min region area
region_merge_size: 20.0, // Region merge threshold
edge_max_len: 12.0, // Max edge length
edge_max_error: 1.3, // Edge simplification error
verts_per_poly: 6, // Vertices per polygon
detail_sample_dist: 6.0, // Detail mesh sample distance
detail_sample_max_error: 1.0, // Detail mesh error
};
let mut builder = NavmeshBuilder::new(config);
}
Building from Geometry
#![allow(unused)]
fn main() {
use astraweave_nav::geometry::InputGeometry;
// Collect level geometry
let mut geometry = InputGeometry::new();
// Add static meshes
for mesh in level.static_meshes() {
geometry.add_mesh(mesh.vertices(), mesh.indices());
}
// Mark areas (walkable, non-walkable, water, etc.)
geometry.mark_area(
bounding_box,
AreaType::NonWalkable,
);
// Build navmesh
let navmesh = builder.build(&geometry)?;
}
Navmesh Configuration Presets
#![allow(unused)]
fn main() {
use astraweave_nav::NavmeshConfig;
// Humanoid character (default)
let human_config = NavmeshConfig::humanoid();
// Small creature (rat, dog)
let small_config = NavmeshConfig::small_creature();
// Large creature (ogre, vehicle)
let large_config = NavmeshConfig::large_creature();
// Flying unit
let flying_config = NavmeshConfig::flying();
// Custom configuration
let custom_config = NavmeshConfig {
agent_height: 1.5,
agent_radius: 0.4,
agent_max_climb: 0.5,
agent_max_slope: 60.0,
..NavmeshConfig::humanoid()
};
}
Use smaller cell sizes for more detailed navmeshes, but be aware of increased memory usage and build times. A cell size of 0.3m works well for humanoid characters.
Navmesh Layers
#![allow(unused)]
fn main() {
use astraweave_nav::NavmeshLayer;
// Create separate navmeshes for different agent types
let mut nav_system = NavigationSystem::new();
// Layer 0: Infantry
nav_system.add_layer(
NavmeshLayer::new("infantry")
.with_config(NavmeshConfig::humanoid())
.with_geometry(&level_geometry)
);
// Layer 1: Vehicles
nav_system.add_layer(
NavmeshLayer::new("vehicles")
.with_config(NavmeshConfig::large_creature())
.with_geometry(&vehicle_geometry)
);
// Layer 2: Flying
nav_system.add_layer(
NavmeshLayer::new("flying")
.with_config(NavmeshConfig::flying())
.with_geometry(&flying_geometry)
);
}
A* Pathfinding
Basic Pathfinding
#![allow(unused)]
fn main() {
use astraweave_nav::{Pathfinder, PathQuery};
let pathfinder = Pathfinder::new(&navmesh);
// Find path from start to end
let query = PathQuery {
start: [10.0, 0.0, 5.0],
end: [50.0, 0.0, 30.0],
extents: [2.0, 4.0, 2.0], // Search extents for nearest poly
filter: None,
};
let path = pathfinder.find_path(query)?;
// Path is a Vec<[f32; 3]> of waypoints
for waypoint in path.waypoints() {
println!("Waypoint: {:?}", waypoint);
}
}
Path Smoothing
#![allow(unused)]
fn main() {
use astraweave_nav::PathSmoothingOptions;
let smoothing = PathSmoothingOptions {
max_iterations: 100,
max_smooth_distance: 0.5,
};
let smooth_path = pathfinder.smooth_path(&path, smoothing)?;
}
Path Filtering
#![allow(unused)]
fn main() {
use astraweave_nav::filter::{PathFilter, AreaFlags};
// Create filter for specific area types
let mut filter = PathFilter::new();
filter.set_area_cost(AreaType::Road, 1.0); // Prefer roads
filter.set_area_cost(AreaType::Grass, 2.0); // Neutral
filter.set_area_cost(AreaType::Water, 10.0); // Avoid water
filter.set_area_cost(AreaType::Lava, f32::MAX); // Never path through lava
filter.exclude_flags(AreaFlags::DISABLED);
let query = PathQuery {
start: start_pos,
end: end_pos,
extents: [2.0, 4.0, 2.0],
filter: Some(filter),
};
let path = pathfinder.find_path(query)?;
}
Hierarchical Pathfinding
#![allow(unused)]
fn main() {
use astraweave_nav::hierarchical::HierarchicalPathfinder;
// Create hierarchical graph for long-distance paths
let hpa_pathfinder = HierarchicalPathfinder::new(&navmesh, 32.0); // 32m clusters
// Fast long-distance pathfinding
let long_path = hpa_pathfinder.find_path(query)?;
}
graph TD
A[Start] --> B[Find Nearest Poly]
B --> C[A* Search]
C --> D{Path Found?}
D -->|Yes| E[Smooth Path]
D -->|No| F[Partial Path]
E --> G[String Pulling]
G --> H[Return Waypoints]
F --> H
Partial Paths
#![allow(unused)]
fn main() {
// Allow partial paths when destination unreachable
let query = PathQuery {
start: start_pos,
end: unreachable_pos,
extents: [2.0, 4.0, 2.0],
filter: None,
};
let result = pathfinder.find_path(query)?;
if result.is_partial() {
println!("Couldn't reach destination, got {} waypoints", result.waypoints().len());
// Path goes as close as possible to target
} else {
println!("Complete path found!");
}
}
Agent Movement
Navigation Agent
#![allow(unused)]
fn main() {
use astraweave_nav::agent::{NavAgent, AgentParams};
// Create navigation agent
let agent_params = AgentParams {
radius: 0.6,
height: 2.0,
max_acceleration: 8.0,
max_speed: 5.5,
collision_query_range: 2.0,
path_optimization_range: 5.0,
separation_weight: 2.0,
..Default::default()
};
let mut agent = NavAgent::new(agent_params);
// Set destination
agent.set_destination([50.0, 0.0, 30.0]);
// Update each frame
let delta_time = 0.016; // 60 FPS
agent.update(delta_time, &pathfinder);
// Get current velocity and position
let velocity = agent.velocity();
let position = agent.position();
// Apply to entity transform
entity.position = position;
entity.rotation = Quat::from_rotation_y(velocity.x.atan2(velocity.z));
}
Agent States
#![allow(unused)]
fn main() {
use astraweave_nav::agent::AgentState;
match agent.state() {
AgentState::Idle => {
// Agent has no destination
}
AgentState::Moving => {
// Agent is moving toward destination
}
AgentState::Arrived => {
// Agent reached destination
agent.stop();
}
AgentState::Stuck => {
// Agent couldn't make progress
agent.set_destination(fallback_position);
}
}
}
Speed Modulation
#![allow(unused)]
fn main() {
// Dynamic speed based on terrain
let current_area = navmesh.get_area_at(agent.position());
let speed_multiplier = match current_area {
AreaType::Road => 1.2,
AreaType::Grass => 1.0,
AreaType::Water => 0.5,
_ => 1.0,
};
agent.set_max_speed(5.5 * speed_multiplier);
}
Obstacle Avoidance
Local Steering
#![allow(unused)]
fn main() {
use astraweave_nav::steering::{SteeringBehavior, AvoidanceParams};
let avoidance = AvoidanceParams {
horizon_time: 2.5, // Look-ahead time
obstacle_margin: 0.3, // Extra clearance
max_neighbors: 10, // Max agents to consider
neighbor_dist: 10.0, // Neighbor search radius
separation_weight: 2.0, // Separation strength
alignment_weight: 1.0, // Velocity alignment
cohesion_weight: 1.0, // Group cohesion
};
agent.set_avoidance_params(avoidance);
}
Dynamic Obstacles
#![allow(unused)]
fn main() {
use astraweave_nav::obstacle::DynamicObstacle;
// Add temporary obstacles
let obstacle = DynamicObstacle::cylinder(
position,
1.5, // radius
2.0, // height
);
nav_system.add_obstacle(obstacle);
// Agents will automatically avoid the obstacle
// Remove when no longer needed
nav_system.remove_obstacle(obstacle.id());
}
Crowd Simulation
#![allow(unused)]
fn main() {
use astraweave_nav::crowd::{CrowdManager, CrowdAgent};
let mut crowd = CrowdManager::new(&navmesh, 100); // Max 100 agents
// Add agents to crowd
let agent_id = crowd.add_agent(CrowdAgent {
position: start_pos,
params: agent_params,
});
// Set target
crowd.set_target(agent_id, end_pos);
// Update all agents efficiently
crowd.update(delta_time);
// Get agent state
let agent = crowd.get_agent(agent_id);
println!("Position: {:?}, Velocity: {:?}", agent.position, agent.velocity);
}
CrowdManager uses spatial hashing and optimized neighbor queries to efficiently simulate hundreds of agents with local avoidance.
Off-Mesh Links
Off-mesh links enable navigation across gaps, jumps, ladders, and teleporters.
Creating Links
#![allow(unused)]
fn main() {
use astraweave_nav::offmesh::{OffMeshLink, OffMeshLinkType};
// Jump down
let jump_link = OffMeshLink {
start: [10.0, 5.0, 0.0],
end: [15.0, 0.0, 0.0],
radius: 0.5,
bidirectional: false, // One-way
link_type: OffMeshLinkType::Jump,
cost: 2.0, // Path cost
};
navmesh.add_offmesh_link(jump_link);
// Ladder (bidirectional)
let ladder_link = OffMeshLink {
start: [20.0, 0.0, 0.0],
end: [20.0, 5.0, 0.0],
radius: 0.5,
bidirectional: true,
link_type: OffMeshLinkType::Climb,
cost: 5.0,
};
navmesh.add_offmesh_link(ladder_link);
}
Traversing Links
#![allow(unused)]
fn main() {
// Agent detects off-mesh link during pathfinding
if let Some(link) = agent.current_offmesh_link() {
match link.link_type {
OffMeshLinkType::Jump => {
// Play jump animation
animation.play("jump");
// Arc trajectory
let t = link.traversal_progress();
let height = 2.0 * t * (1.0 - t); // Parabolic arc
agent.position.y += height;
}
OffMeshLinkType::Climb => {
// Play climb animation
animation.play("climb_ladder");
}
OffMeshLinkType::Teleport => {
// Instant teleport
agent.position = link.end;
}
}
}
}
Dynamic Navmesh Updates
Runtime Modifications
#![allow(unused)]
fn main() {
use astraweave_nav::dynamic::{NavmeshModifier, TileCache};
// Create tile cache for dynamic updates
let mut tile_cache = TileCache::new(&navmesh, config);
// Add obstacle (e.g., fallen tree)
let obstacle_id = tile_cache.add_obstacle(
position,
bounding_box,
);
// Rebuild affected tiles
tile_cache.update()?;
// Remove obstacle when destroyed
tile_cache.remove_obstacle(obstacle_id);
tile_cache.update()?;
}
Temporary Blocking
#![allow(unused)]
fn main() {
// Block area temporarily
let blocker_id = nav_system.block_area(
center,
radius,
duration_seconds,
);
// Automatically unblocks after duration
}
Performance Optimization
Async Pathfinding
#![allow(unused)]
fn main() {
use astraweave_nav::async_pathfinding::AsyncPathfinder;
let async_pathfinder = AsyncPathfinder::new(&navmesh);
// Request path asynchronously
let path_future = async_pathfinder.find_path_async(query);
// Continue gameplay, path calculated in background
// Check if ready
if let Some(path) = path_future.try_get() {
agent.set_path(path);
}
}
Path Caching
#![allow(unused)]
fn main() {
use astraweave_nav::cache::PathCache;
let mut path_cache = PathCache::new(1000); // Cache 1000 paths
// Check cache before pathfinding
let cache_key = PathCache::key(start, end);
if let Some(cached_path) = path_cache.get(&cache_key) {
return cached_path.clone();
}
// Calculate and cache
let path = pathfinder.find_path(query)?;
path_cache.insert(cache_key, path.clone());
}
LOD for Distant Agents
#![allow(unused)]
fn main() {
// Reduce update frequency for distant agents
let distance_to_player = (agent.position - player.position).length();
let update_interval = if distance_to_player < 20.0 {
0.0 // Every frame
} else if distance_to_player < 50.0 {
0.1 // 10 times per second
} else {
0.5 // 2 times per second
};
if agent.time_since_update() >= update_interval {
agent.update(delta_time, &pathfinder);
}
}
Complete Example
AI Character with Navigation
use astraweave_nav::*;
pub struct AICharacter {
agent: NavAgent,
current_path: Option<Path>,
destination: Option<Vec3>,
}
impl AICharacter {
pub fn new(start_position: Vec3) -> Self {
let params = AgentParams {
radius: 0.6,
height: 2.0,
max_speed: 5.0,
max_acceleration: 10.0,
..Default::default()
};
let mut agent = NavAgent::new(params);
agent.set_position([start_position.x, start_position.y, start_position.z]);
Self {
agent,
current_path: None,
destination: None,
}
}
pub fn move_to(&mut self, destination: Vec3, pathfinder: &Pathfinder) -> Result<()> {
let query = PathQuery {
start: self.agent.position(),
end: [destination.x, destination.y, destination.z],
extents: [2.0, 4.0, 2.0],
filter: None,
};
let path = pathfinder.find_path(query)?;
self.current_path = Some(path);
self.destination = Some(destination);
self.agent.set_destination([destination.x, destination.y, destination.z]);
Ok(())
}
pub fn update(&mut self, delta_time: f32, pathfinder: &Pathfinder) {
self.agent.update(delta_time, pathfinder);
// Check if arrived
if self.agent.state() == AgentState::Arrived {
self.current_path = None;
self.destination = None;
}
}
pub fn position(&self) -> Vec3 {
let pos = self.agent.position();
Vec3::new(pos[0], pos[1], pos[2])
}
pub fn velocity(&self) -> Vec3 {
let vel = self.agent.velocity();
Vec3::new(vel[0], vel[1], vel[2])
}
}
// Usage
fn main() -> Result<()> {
let navmesh = build_navmesh()?;
let pathfinder = Pathfinder::new(&navmesh);
let mut character = AICharacter::new(Vec3::new(0.0, 0.0, 0.0));
character.move_to(Vec3::new(50.0, 0.0, 30.0), &pathfinder)?;
loop {
let delta_time = 0.016;
character.update(delta_time, &pathfinder);
println!("Position: {:?}", character.position());
std::thread::sleep(std::time::Duration::from_millis(16));
}
}
Debugging and Visualization
Debug Rendering
#![allow(unused)]
fn main() {
use astraweave_nav::debug::NavmeshDebugRenderer;
let mut debug_renderer = NavmeshDebugRenderer::new();
// Render navmesh
debug_renderer.draw_navmesh(&navmesh, Color::GREEN);
// Render path
if let Some(path) = &agent.current_path {
debug_renderer.draw_path(path, Color::YELLOW);
}
// Render agent
debug_renderer.draw_agent(&agent, Color::RED);
// Render off-mesh links
debug_renderer.draw_offmesh_links(&navmesh, Color::CYAN);
}
Related Documentation
- AI System - Behavior trees and decision making
- Physics Integration - Character controllers
- Level Design - Navigation editing tools
- Performance Guide - Navigation optimization
API Reference
For complete API documentation, see:
Input System
AstraWeave’s input system provides comprehensive input handling with action mapping, binding system, controller support, input replay, and cross-platform abstraction.
Architecture Overview
The input system is designed to decouple raw input events from gameplay actions, enabling flexible control schemes and remapping.
graph TD
A[Raw Input] --> B[Input Devices]
B --> C[Keyboard]
B --> D[Mouse]
B --> E[Gamepad]
B --> F[Touch]
C --> G[Input Mapper]
D --> G
E --> G
F --> G
G --> H[Action System]
H --> I[Gameplay Actions]
H --> J[UI Actions]
Key Components
- Input Devices: Cross-platform abstraction for keyboard, mouse, gamepad, and touch
- Action Mapping: Map raw inputs to high-level gameplay actions
- Binding System: Customizable control schemes with conflict detection
- Input Context: Hierarchical input contexts for different game states
- Input Replay: Record and playback input sequences for testing and demos
The input system uses winit for window events and gilrs for gamepad support, providing consistent cross-platform input handling.
Input System Initialization
Basic Setup
#![allow(unused)]
fn main() {
use astraweave_input::{InputSystem, InputConfig};
let config = InputConfig {
deadzone: 0.15, // Gamepad deadzone
mouse_sensitivity: 1.0,
enable_raw_mouse_input: true, // For FPS games
double_click_time_ms: 300,
key_repeat_delay_ms: 500,
key_repeat_rate_ms: 50,
..Default::default()
};
let mut input_system = InputSystem::new(config);
}
Processing Window Events
#![allow(unused)]
fn main() {
use winit::event::{Event, WindowEvent};
event_loop.run(move |event, _, control_flow| {
// Feed events to input system
input_system.handle_event(&event);
match event {
Event::WindowEvent { event, .. } => {
match event {
WindowEvent::Focused(false) => {
input_system.clear_input_state();
}
_ => {}
}
}
Event::MainEventsCleared => {
// Update input system
input_system.update(delta_time);
// Process actions
update_game(&input_system);
}
_ => {}
}
});
}
Action Mapping
Defining Actions
#![allow(unused)]
fn main() {
use astraweave_input::action::{Action, ActionType};
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
pub enum GameAction {
// Movement
MoveForward,
MoveBackward,
MoveLeft,
MoveRight,
Jump,
Crouch,
Sprint,
// Camera
LookHorizontal,
LookVertical,
// Combat
Fire,
AimDownSights,
Reload,
SwitchWeapon,
// Interaction
Interact,
UseItem,
// UI
OpenMenu,
Inventory,
}
impl Action for GameAction {
fn action_type(&self) -> ActionType {
match self {
// Button actions (pressed/released)
GameAction::Jump | GameAction::Fire | GameAction::Interact => {
ActionType::Button
}
// Axis actions (continuous values)
GameAction::MoveForward | GameAction::LookHorizontal => {
ActionType::Axis
}
}
}
}
}
Creating Action Maps
#![allow(unused)]
fn main() {
use astraweave_input::mapping::{ActionMap, InputBinding};
use astraweave_input::device::{Key, MouseButton, GamepadButton, GamepadAxis};
let mut action_map = ActionMap::new("default");
// Keyboard bindings
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
action_map.bind(GameAction::MoveBackward, InputBinding::Key(Key::S));
action_map.bind(GameAction::MoveLeft, InputBinding::Key(Key::A));
action_map.bind(GameAction::MoveRight, InputBinding::Key(Key::D));
action_map.bind(GameAction::Jump, InputBinding::Key(Key::Space));
action_map.bind(GameAction::Crouch, InputBinding::Key(Key::ControlLeft));
action_map.bind(GameAction::Sprint, InputBinding::Key(Key::ShiftLeft));
// Mouse bindings
action_map.bind(GameAction::Fire, InputBinding::MouseButton(MouseButton::Left));
action_map.bind(GameAction::AimDownSights, InputBinding::MouseButton(MouseButton::Right));
action_map.bind(GameAction::LookHorizontal, InputBinding::MouseAxisX);
action_map.bind(GameAction::LookVertical, InputBinding::MouseAxisY);
// Gamepad bindings
action_map.bind(GameAction::Jump, InputBinding::GamepadButton(GamepadButton::South));
action_map.bind(GameAction::Fire, InputBinding::GamepadButton(GamepadButton::RightTrigger2));
action_map.bind(GameAction::MoveForward, InputBinding::GamepadAxis {
axis: GamepadAxis::LeftStickY,
threshold: 0.0,
inverted: false,
});
action_map.bind(GameAction::LookHorizontal, InputBinding::GamepadAxis {
axis: GamepadAxis::RightStickX,
threshold: 0.0,
inverted: false,
});
// Register action map
input_system.register_action_map(action_map);
}
Multiple Bindings
#![allow(unused)]
fn main() {
// Bind multiple inputs to the same action
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
action_map.bind(GameAction::MoveForward, InputBinding::Key(Key::ArrowUp));
action_map.bind(GameAction::MoveForward, InputBinding::GamepadAxis {
axis: GamepadAxis::LeftStickY,
threshold: 0.2,
inverted: false,
});
}
Input Modifiers
#![allow(unused)]
fn main() {
use astraweave_input::modifier::{InputModifier, ModifierKey};
// Bind with modifier keys (e.g., Shift+E for fast interaction)
action_map.bind_with_modifiers(
GameAction::Interact,
InputBinding::Key(Key::E),
&[ModifierKey::Shift],
);
// Input modifiers for axis scaling
action_map.bind_with_modifier_fn(
GameAction::LookHorizontal,
InputBinding::MouseAxisX,
|value| value * mouse_sensitivity,
);
}
Querying Input State
Button State
#![allow(unused)]
fn main() {
// Check if action is currently pressed
if input_system.is_action_pressed(GameAction::Jump) {
player.jump();
}
// Check if action was just pressed this frame
if input_system.is_action_just_pressed(GameAction::Fire) {
weapon.fire();
}
// Check if action was just released this frame
if input_system.is_action_just_released(GameAction::AimDownSights) {
player.stop_aiming();
}
// Get how long action has been held
let sprint_duration = input_system.action_duration(GameAction::Sprint);
if sprint_duration > 2.0 {
player.start_breathing_heavily();
}
}
Axis Values
#![allow(unused)]
fn main() {
// Get axis value (-1.0 to 1.0)
let move_forward = input_system.action_value(GameAction::MoveForward);
let move_right = input_system.action_value(GameAction::MoveRight);
// Create movement vector
let movement = Vec3::new(move_right, 0.0, move_forward).normalize_or_zero();
player.move_direction(movement);
// Get raw axis value (before deadzone)
let raw_look = input_system.raw_action_value(GameAction::LookHorizontal);
// Get axis delta (change since last frame)
let look_delta = input_system.action_delta(GameAction::LookHorizontal);
camera.rotate_yaw(look_delta * sensitivity);
}
Composite Inputs
#![allow(unused)]
fn main() {
use astraweave_input::composite::Vec2Input;
// Create 2D input from multiple actions
let movement_input = Vec2Input::new(
GameAction::MoveRight, // X axis
GameAction::MoveForward, // Y axis
);
let movement = movement_input.get_value(&input_system);
player.move_direction(Vec3::new(movement.x, 0.0, movement.y));
}
Binding System
Runtime Rebinding
#![allow(unused)]
fn main() {
use astraweave_input::rebinding::BindingRecorder;
// Start recording new binding
let mut recorder = BindingRecorder::new();
recorder.start_recording(GameAction::Jump);
// Wait for user input
loop {
input_system.update(delta_time);
if let Some(binding) = recorder.check_input(&input_system) {
// User pressed a key/button
println!("New binding: {:?}", binding);
// Check for conflicts
if let Some(conflict) = action_map.find_conflict(GameAction::Jump, &binding) {
println!("Warning: {} already bound to {:?}", conflict, binding);
// Optionally swap or clear conflict
action_map.unbind(conflict, &binding);
}
// Apply new binding
action_map.bind(GameAction::Jump, binding);
break;
}
}
}
Saving and Loading Bindings
#![allow(unused)]
fn main() {
use astraweave_input::persistence::BindingProfile;
// Save bindings
let profile = BindingProfile::from_action_map(&action_map);
profile.save_to_file("user_bindings.json")?;
// Load bindings
let profile = BindingProfile::load_from_file("user_bindings.json")?;
let action_map = profile.to_action_map();
input_system.register_action_map(action_map);
}
Preset Binding Schemes
#![allow(unused)]
fn main() {
// Create multiple preset schemes
let keyboard_mouse = ActionMap::keyboard_mouse_preset();
let gamepad = ActionMap::gamepad_preset();
let left_handed = ActionMap::left_handed_preset();
// Switch schemes
input_system.set_active_action_map("keyboard_mouse");
}
Input Contexts
Input contexts allow different parts of the game to handle input differently (e.g., gameplay vs. menu).
Creating Contexts
#![allow(unused)]
fn main() {
use astraweave_input::context::{InputContext, ContextPriority};
// Gameplay context
let mut gameplay_context = InputContext::new("gameplay", ContextPriority::Normal);
gameplay_context.set_action_map(gameplay_action_map);
// UI context (higher priority)
let mut ui_context = InputContext::new("ui", ContextPriority::High);
ui_context.set_action_map(ui_action_map);
// Add contexts to system
input_system.add_context(gameplay_context);
input_system.add_context(ui_context);
}
Context Activation
#![allow(unused)]
fn main() {
// Activate/deactivate contexts
input_system.activate_context("gameplay");
input_system.deactivate_context("ui");
// Push/pop context stack
input_system.push_context("menu"); // Menu now receives input
// ... menu is open ...
input_system.pop_context(); // Back to previous context
// Query which contexts are active
if input_system.is_context_active("gameplay") {
// Process gameplay input
}
}
Input Consumption
#![allow(unused)]
fn main() {
// Higher priority contexts can consume input
ui_context.set_consumes_input(true); // UI blocks input from reaching gameplay
// Check if input was consumed
if !input_system.was_action_consumed(GameAction::Fire) {
weapon.fire();
}
}
Controller Support
Gamepad Detection
#![allow(unused)]
fn main() {
use astraweave_input::gamepad::{Gamepad, GamepadEvent};
// List connected gamepads
for gamepad in input_system.gamepads() {
println!("Gamepad {}: {}", gamepad.id(), gamepad.name());
}
// Handle gamepad events
while let Some(event) = input_system.poll_gamepad_event() {
match event {
GamepadEvent::Connected(id) => {
println!("Gamepad {} connected", id);
}
GamepadEvent::Disconnected(id) => {
println!("Gamepad {} disconnected", id);
}
GamepadEvent::ButtonPressed(id, button) => {
println!("Gamepad {} button {:?} pressed", id, button);
}
_ => {}
}
}
}
Vibration/Haptics
#![allow(unused)]
fn main() {
use astraweave_input::gamepad::{VibrationEffect, VibrationDuration};
// Simple vibration
input_system.vibrate_gamepad(
gamepad_id,
0.5, // Weak motor (0.0 - 1.0)
0.8, // Strong motor (0.0 - 1.0)
VibrationDuration::Milliseconds(200),
);
// Complex effect
let effect = VibrationEffect::explosion()
.with_intensity(1.0)
.with_duration_ms(500);
input_system.play_vibration_effect(gamepad_id, effect);
}
Per-Player Input
#![allow(unused)]
fn main() {
use astraweave_input::player::PlayerInput;
// Assign gamepads to players
let player1_input = PlayerInput::new()
.with_gamepad(0)
.with_keyboard_mouse();
let player2_input = PlayerInput::new()
.with_gamepad(1);
// Query player-specific input
if player1_input.is_action_pressed(&input_system, GameAction::Jump) {
player1.jump();
}
if player2_input.is_action_pressed(&input_system, GameAction::Jump) {
player2.jump();
}
}
For split-screen games, use separate PlayerInput instances to isolate input between players.
Input Replay
Input replay enables testing, demo playback, and replay systems.
Recording Input
#![allow(unused)]
fn main() {
use astraweave_input::replay::{InputRecorder, InputRecording};
// Start recording
let mut recorder = InputRecorder::new();
recorder.start();
// Update each frame (automatically records input)
loop {
input_system.update(delta_time);
recorder.record_frame(&input_system);
// ... game logic ...
}
// Stop and save
recorder.stop();
let recording = recorder.finalize();
recording.save_to_file("replay.bin")?;
}
Playback
#![allow(unused)]
fn main() {
use astraweave_input::replay::InputPlayer;
// Load recording
let recording = InputRecording::load_from_file("replay.bin")?;
let mut player = InputPlayer::new(recording);
// Playback loop
loop {
// Apply recorded input to system
player.update(delta_time, &mut input_system);
// Game state will be driven by recorded input
update_game(&input_system);
if player.is_finished() {
break;
}
}
}
Replay Controls
#![allow(unused)]
fn main() {
// Pause/resume
player.pause();
player.resume();
// Seek
player.seek_to_time(10.0); // Seek to 10 seconds
player.seek_to_frame(600); // Seek to frame 600
// Playback speed
player.set_playback_speed(0.5); // Half speed
player.set_playback_speed(2.0); // Double speed
// Loop
player.set_loop(true);
}
Advanced Features
Input Buffering
#![allow(unused)]
fn main() {
use astraweave_input::buffer::InputBuffer;
// Create input buffer for fighting game-style combo inputs
let mut buffer = InputBuffer::new()
.with_buffer_duration(0.3) // 300ms window
.with_max_inputs(8);
// Add inputs to buffer
buffer.record_input(&input_system);
// Check for sequences
if buffer.matches_sequence(&[
GameAction::MoveForward,
GameAction::MoveDown,
GameAction::MoveForward,
GameAction::Fire,
]) {
character.perform_special_move("Hadouken");
buffer.clear();
}
}
Input Prediction (Networking)
#![allow(unused)]
fn main() {
use astraweave_input::prediction::InputPredictor;
// For networked games, predict input during lag
let mut predictor = InputPredictor::new();
// Record recent inputs
predictor.record(&input_system);
// Predict future input (e.g., during packet loss)
let predicted_input = predictor.predict_next_frame();
// Use predicted input until real input arrives
if network.has_input() {
apply_input(network.get_input());
} else {
apply_input(predicted_input);
}
}
Touch Input
#![allow(unused)]
fn main() {
use astraweave_input::touch::{TouchEvent, TouchPhase};
// Handle touch events (mobile/tablet)
while let Some(event) = input_system.poll_touch_event() {
match event.phase {
TouchPhase::Started => {
println!("Touch started at {:?}", event.position);
}
TouchPhase::Moved => {
let delta = event.position - event.previous_position;
camera.rotate(delta);
}
TouchPhase::Ended => {
println!("Touch ended");
}
TouchPhase::Cancelled => {
println!("Touch cancelled");
}
}
}
// Multi-touch gestures
if let Some(pinch) = input_system.get_pinch_gesture() {
camera.zoom(pinch.scale);
}
if let Some(swipe) = input_system.get_swipe_gesture() {
player.dodge(swipe.direction);
}
}
Complete Example
FPS Input System
#![allow(unused)]
fn main() {
use astraweave_input::*;
pub struct FPSInput {
input_system: InputSystem,
gameplay_context: ContextId,
menu_context: ContextId,
}
impl FPSInput {
pub fn new() -> Self {
let mut input_system = InputSystem::new(InputConfig::default());
// Create gameplay action map
let mut gameplay_map = ActionMap::new("gameplay");
gameplay_map.bind(GameAction::MoveForward, InputBinding::Key(Key::W));
gameplay_map.bind(GameAction::MoveBackward, InputBinding::Key(Key::S));
gameplay_map.bind(GameAction::MoveLeft, InputBinding::Key(Key::A));
gameplay_map.bind(GameAction::MoveRight, InputBinding::Key(Key::D));
gameplay_map.bind(GameAction::Jump, InputBinding::Key(Key::Space));
gameplay_map.bind(GameAction::Fire, InputBinding::MouseButton(MouseButton::Left));
gameplay_map.bind(GameAction::LookHorizontal, InputBinding::MouseAxisX);
gameplay_map.bind(GameAction::LookVertical, InputBinding::MouseAxisY);
// Create menu action map
let mut menu_map = ActionMap::new("menu");
menu_map.bind(GameAction::OpenMenu, InputBinding::Key(Key::Escape));
// Register contexts
let gameplay_context = input_system.add_context(
InputContext::new("gameplay", ContextPriority::Normal)
.with_action_map(gameplay_map)
);
let menu_context = input_system.add_context(
InputContext::new("menu", ContextPriority::High)
.with_action_map(menu_map)
.with_consumes_input(true)
);
// Start with gameplay active
input_system.activate_context(gameplay_context);
Self {
input_system,
gameplay_context,
menu_context,
}
}
pub fn update(&mut self, delta_time: f32, player: &mut Player, camera: &mut Camera) {
self.input_system.update(delta_time);
// Handle menu toggle
if self.input_system.is_action_just_pressed(GameAction::OpenMenu) {
self.toggle_menu();
}
// Process gameplay input if not in menu
if self.input_system.is_context_active(self.gameplay_context) {
self.process_gameplay_input(player, camera);
}
}
fn process_gameplay_input(&self, player: &mut Player, camera: &mut Camera) {
// Movement
let move_forward = self.input_system.action_value(GameAction::MoveForward);
let move_right = self.input_system.action_value(GameAction::MoveRight);
let movement = Vec3::new(move_right, 0.0, move_forward);
player.set_movement(movement);
// Jump
if self.input_system.is_action_just_pressed(GameAction::Jump) {
player.jump();
}
// Camera look
let look_x = self.input_system.action_delta(GameAction::LookHorizontal);
let look_y = self.input_system.action_delta(GameAction::LookVertical);
camera.rotate(look_x, look_y);
// Fire weapon
if self.input_system.is_action_pressed(GameAction::Fire) {
player.fire_weapon();
}
}
fn toggle_menu(&mut self) {
if self.input_system.is_context_active(self.menu_context) {
self.input_system.deactivate_context(self.menu_context);
self.input_system.activate_context(self.gameplay_context);
} else {
self.input_system.deactivate_context(self.gameplay_context);
self.input_system.activate_context(self.menu_context);
}
}
pub fn handle_event(&mut self, event: &Event<()>) {
self.input_system.handle_event(event);
}
}
}
Related Documentation
- Player Controller - Character movement and control
- UI System - UI interaction and focus
- Networking - Network input synchronization
- Accessibility - Accessible input options
API Reference
For complete API documentation, see:
Networking System
AstraWeave’s networking system provides robust client-server architecture with state synchronization, delta encoding, lag compensation, and prediction for responsive multiplayer gameplay.
Architecture Overview
The networking system is built on a client-server model with authoritative server and client-side prediction.
graph TD
A[Server] --> B[State Authority]
A --> C[Physics Simulation]
A --> D[Game Logic]
B --> E[Delta Encoding]
E --> F[Network Transport]
F --> G[Client 1]
F --> H[Client 2]
F --> I[Client N]
G --> J[Prediction]
G --> K[Interpolation]
J --> L[Reconciliation]
Key Components
- Transport Layer: UDP-based reliable/unreliable messaging with Quinn (QUIC)
- State Synchronization: Efficient delta compression and entity replication
- Client Prediction: Responsive input with server reconciliation
- Lag Compensation: Server-side hit detection with rewind
- Network Relevancy: Bandwidth optimization through spatial partitioning
AstraWeave uses QUIC (via Quinn) for modern, secure, and multiplexed networking with built-in congestion control and 0-RTT connection establishment.
Server Setup
Creating a Server
#![allow(unused)]
fn main() {
use astraweave_net::{Server, ServerConfig};
let config = ServerConfig {
bind_address: "0.0.0.0:7777".parse()?,
max_clients: 64,
tick_rate: 60, // Server updates per second
timeout_seconds: 10.0,
enable_encryption: true,
max_bandwidth_kbps: 1024,
..Default::default()
};
let mut server = Server::new(config)?;
}
Server Loop
#![allow(unused)]
fn main() {
use astraweave_net::{ServerEvent, ClientId};
loop {
let delta_time = 1.0 / 60.0; // 60 Hz tick rate
// Process incoming events
while let Some(event) = server.poll_event() {
match event {
ServerEvent::ClientConnected { client_id, address } => {
println!("Client {} connected from {}", client_id, address);
spawn_player(client_id);
}
ServerEvent::ClientDisconnected { client_id, reason } => {
println!("Client {} disconnected: {:?}", client_id, reason);
despawn_player(client_id);
}
ServerEvent::MessageReceived { client_id, message } => {
handle_client_message(client_id, message);
}
}
}
// Update game state
update_game_state(delta_time);
// Send state updates to clients
server.broadcast_state_update(&world_state)?;
// Tick network
server.tick(delta_time)?;
std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}
Client Setup
Connecting to Server
#![allow(unused)]
fn main() {
use astraweave_net::{Client, ClientConfig};
let config = ClientConfig {
server_address: "127.0.0.1:7777".parse()?,
timeout_seconds: 10.0,
enable_prediction: true,
interpolation_delay_ms: 100,
..Default::default()
};
let mut client = Client::connect(config).await?;
}
Client Loop
#![allow(unused)]
fn main() {
use astraweave_net::ClientEvent;
loop {
let delta_time = 1.0 / 60.0;
// Process server events
while let Some(event) = client.poll_event() {
match event {
ClientEvent::Connected => {
println!("Connected to server");
}
ClientEvent::Disconnected { reason } => {
println!("Disconnected: {:?}", reason);
break;
}
ClientEvent::StateUpdate { state } => {
apply_state_update(state);
}
ClientEvent::MessageReceived { message } => {
handle_server_message(message);
}
}
}
// Get local input
let input = gather_input();
// Send input to server
client.send_input(input)?;
// Client-side prediction
if config.enable_prediction {
simulate_local_player(input, delta_time);
}
// Interpolate remote entities
interpolate_entities(delta_time);
// Tick network
client.tick(delta_time)?;
std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}
State Synchronization
Entity Replication
#![allow(unused)]
fn main() {
use astraweave_net::replication::{NetEntity, ReplicationMode};
#[derive(NetEntity)]
pub struct Player {
#[replicate(mode = ReplicationMode::Always)]
pub entity_id: u64,
#[replicate(mode = ReplicationMode::OnChange, priority = 10)]
pub position: Vec3,
#[replicate(mode = ReplicationMode::OnChange, priority = 10)]
pub rotation: Quat,
#[replicate(mode = ReplicationMode::OnChange, priority = 5)]
pub velocity: Vec3,
#[replicate(mode = ReplicationMode::OnChange, priority = 2)]
pub health: f32,
#[replicate(mode = ReplicationMode::OnChange, priority = 1)]
pub ammo: u32,
// Not replicated
#[replicate(mode = ReplicationMode::Never)]
local_animation_state: AnimationState,
}
}
Replication Modes
| Mode | Description | Use Case |
|---|---|---|
Always | Send every frame | Entity ID, critical state |
OnChange | Send when value changes | Position, health |
OnChangeThreshold | Send when change exceeds threshold | Analog values |
Never | Never replicate | Client-only data |
World State Updates
#![allow(unused)]
fn main() {
use astraweave_net::state::{WorldState, EntityState};
// Server: Build world state
let mut world_state = WorldState::new();
for player in players.iter() {
world_state.add_entity(player.entity_id, EntityState {
position: player.position,
rotation: player.rotation,
velocity: player.velocity,
custom_data: serialize_player_data(player),
});
}
// Send to all clients
server.broadcast_state_update(&world_state)?;
// Client: Apply state update
fn apply_state_update(state: WorldState) {
for (entity_id, entity_state) in state.entities() {
if let Some(entity) = world.get_entity_mut(entity_id) {
entity.position = entity_state.position;
entity.rotation = entity_state.rotation;
entity.velocity = entity_state.velocity;
}
}
}
}
Delta Encoding
Delta encoding dramatically reduces bandwidth by only sending changed values.
Automatic Delta Compression
#![allow(unused)]
fn main() {
use astraweave_net::delta::{DeltaEncoder, DeltaDecoder};
// Server: Encode state changes
let mut encoder = DeltaEncoder::new();
// First frame: full state
let full_state = encoder.encode_full(&world_state)?;
server.broadcast(full_state)?;
// Subsequent frames: only deltas
loop {
let new_state = get_current_world_state();
let delta = encoder.encode_delta(&new_state)?;
if delta.size_bytes() < full_state.size_bytes() * 0.8 {
server.broadcast(delta)?;
} else {
// Send full state if delta is too large
server.broadcast(encoder.encode_full(&new_state)?)?;
}
}
// Client: Decode deltas
let mut decoder = DeltaDecoder::new();
match received_message {
NetworkMessage::FullState(state) => {
decoder.apply_full_state(state);
}
NetworkMessage::DeltaState(delta) => {
decoder.apply_delta(delta);
}
}
let current_state = decoder.get_state();
}
Custom Delta Serialization
#![allow(unused)]
fn main() {
use astraweave_net::serialization::{BitWriter, BitReader};
impl Player {
pub fn write_delta(&self, previous: &Player, writer: &mut BitWriter) {
// Position (quantized)
if self.position != previous.position {
writer.write_bit(true);
writer.write_vec3_quantized(self.position, -1000.0, 1000.0, 0.01);
} else {
writer.write_bit(false);
}
// Rotation (quaternion compression)
if self.rotation != previous.rotation {
writer.write_bit(true);
writer.write_quaternion_compressed(self.rotation);
} else {
writer.write_bit(false);
}
// Health (8-bit)
if self.health != previous.health {
writer.write_bit(true);
writer.write_u8((self.health * 255.0) as u8);
} else {
writer.write_bit(false);
}
}
pub fn read_delta(&mut self, reader: &mut BitReader) {
if reader.read_bit() {
self.position = reader.read_vec3_quantized(-1000.0, 1000.0, 0.01);
}
if reader.read_bit() {
self.rotation = reader.read_quaternion_compressed();
}
if reader.read_bit() {
self.health = reader.read_u8() as f32 / 255.0;
}
}
}
}
Quantize floating-point values to reduce precision and save bandwidth. For example, position precision of 1cm (0.01m) is sufficient for most games.
Client Prediction
Client-side prediction makes the game feel responsive despite network latency.
Input Prediction
#![allow(unused)]
fn main() {
use astraweave_net::prediction::{PredictionSystem, InputBuffer};
pub struct PredictedPlayer {
// Authoritative state from server
server_position: Vec3,
server_velocity: Vec3,
server_tick: u64,
// Predicted state
predicted_position: Vec3,
predicted_velocity: Vec3,
// Input history
input_buffer: InputBuffer,
}
impl PredictedPlayer {
pub fn send_input(&mut self, input: PlayerInput, client: &mut Client) {
// Store input locally
self.input_buffer.push(input.clone());
// Send to server
client.send_input(input)?;
// Predict movement
self.simulate_movement(input, TICK_DELTA);
}
pub fn reconcile(&mut self, server_state: PlayerState) {
self.server_position = server_state.position;
self.server_velocity = server_state.velocity;
self.server_tick = server_state.tick;
// Start from server state
self.predicted_position = server_state.position;
self.predicted_velocity = server_state.velocity;
// Re-simulate inputs after server tick
for input in self.input_buffer.after_tick(server_state.tick) {
self.simulate_movement(input, TICK_DELTA);
}
// Clear old inputs
self.input_buffer.clear_before_tick(server_state.tick);
}
fn simulate_movement(&mut self, input: PlayerInput, delta_time: f32) {
// Same movement code as server
let acceleration = input.move_direction * MOVE_SPEED;
self.predicted_velocity += acceleration * delta_time;
self.predicted_velocity *= 0.9; // Friction
self.predicted_position += self.predicted_velocity * delta_time;
}
}
}
Prediction Error Correction
#![allow(unused)]
fn main() {
// Smooth correction of prediction errors
pub fn correct_prediction_error(&mut self, delta_time: f32) {
let error = self.server_position - self.predicted_position;
let error_magnitude = error.length();
if error_magnitude > 0.01 {
// Smooth correction over time
let correction_speed = 10.0; // Adjust for smoothness
let correction = error * correction_speed * delta_time;
self.predicted_position += correction;
} else {
// Snap if very close
self.predicted_position = self.server_position;
}
}
}
sequenceDiagram
participant Client
participant Server
Client->>Client: Input (frame 1)
Client->>Client: Predict movement
Client->>Server: Send input (frame 1)
Client->>Client: Input (frame 2)
Client->>Client: Predict movement
Client->>Server: Send input (frame 2)
Server->>Server: Process input (frame 1)
Server->>Client: State update (frame 1)
Client->>Client: Reconcile with server
Client->>Client: Re-simulate frames 2+
Entity Interpolation
Remote entities are interpolated between snapshots for smooth movement.
Snapshot Interpolation
#![allow(unused)]
fn main() {
use astraweave_net::interpolation::{SnapshotBuffer, InterpolationTarget};
pub struct InterpolatedEntity {
snapshots: SnapshotBuffer<EntitySnapshot>,
interpolation_delay: f32, // 100ms
}
#[derive(Clone)]
pub struct EntitySnapshot {
timestamp: f32,
position: Vec3,
rotation: Quat,
velocity: Vec3,
}
impl InterpolatedEntity {
pub fn add_snapshot(&mut self, snapshot: EntitySnapshot) {
self.snapshots.push(snapshot);
}
pub fn interpolate(&self, current_time: f32) -> InterpolationTarget {
// Render time is current time minus interpolation delay
let render_time = current_time - self.interpolation_delay;
// Find bracketing snapshots
let (before, after) = self.snapshots.get_bracketing(render_time);
// Calculate interpolation factor
let t = (render_time - before.timestamp) / (after.timestamp - before.timestamp);
let t = t.clamp(0.0, 1.0);
InterpolationTarget {
position: before.position.lerp(after.position, t),
rotation: before.rotation.slerp(after.rotation, t),
velocity: before.velocity.lerp(after.velocity, t),
}
}
}
}
Extrapolation for Missing Snapshots
#![allow(unused)]
fn main() {
pub fn extrapolate(&self, current_time: f32) -> InterpolationTarget {
let latest = self.snapshots.latest();
let time_diff = current_time - latest.timestamp;
if time_diff > 0.2 {
// Too old, don't extrapolate
return InterpolationTarget {
position: latest.position,
rotation: latest.rotation,
velocity: latest.velocity,
};
}
// Linear extrapolation using velocity
InterpolationTarget {
position: latest.position + latest.velocity * time_diff,
rotation: latest.rotation,
velocity: latest.velocity,
}
}
}
Lag Compensation
Server-side lag compensation ensures fair hit detection.
Rewinding Game State
#![allow(unused)]
fn main() {
use astraweave_net::lag_compensation::{HistoryBuffer, HistoricalState};
pub struct LagCompensationSystem {
history: HistoryBuffer<WorldSnapshot>,
max_rewind_ms: u64,
}
impl LagCompensationSystem {
pub fn record_snapshot(&mut self, world: &World, timestamp: u64) {
let snapshot = WorldSnapshot {
timestamp,
entities: world.entities().map(|e| e.clone()).collect(),
};
self.history.push(snapshot);
self.history.prune_older_than(timestamp - self.max_rewind_ms);
}
pub fn rewind_and_test(&self, client_latency_ms: u64, raycast: Ray) -> Option<Hit> {
let rewind_time = current_time() - client_latency_ms;
let snapshot = self.history.get_at_time(rewind_time)?;
// Test hit detection against historical state
for entity in &snapshot.entities {
if let Some(hit) = raycast.intersect(&entity.hitbox) {
return Some(Hit {
entity_id: entity.id,
position: hit.point,
distance: hit.distance,
});
}
}
None
}
}
}
Server-Side Hit Validation
#![allow(unused)]
fn main() {
pub fn process_shot(
&mut self,
client_id: ClientId,
shot_data: ShotData,
) -> Result<Option<Hit>> {
let client = self.clients.get(client_id)?;
let latency_ms = client.round_trip_time() / 2;
// Rewind to client's view of the world
let hit = self.lag_compensation.rewind_and_test(
latency_ms,
shot_data.raycast,
);
if let Some(hit) = hit {
// Validate hit (anti-cheat)
if validate_shot(&shot_data, &hit) {
apply_damage(hit.entity_id, shot_data.damage);
return Ok(Some(hit));
}
}
Ok(None)
}
}
Limit maximum rewind time (typically 200-250ms) to prevent abuse and reduce server memory usage.
Network Relevancy
Only send relevant entities to each client to save bandwidth.
Spatial Partitioning
#![allow(unused)]
fn main() {
use astraweave_net::relevancy::{RelevancySystem, SpatialGrid};
pub struct RelevancySystem {
grid: SpatialGrid,
relevancy_range: f32,
}
impl RelevancySystem {
pub fn update_relevancy(&mut self, clients: &[Client], entities: &[Entity]) {
self.grid.clear();
// Insert entities into spatial grid
for entity in entities {
self.grid.insert(entity.id, entity.position);
}
// Calculate relevant entities for each client
for client in clients {
let player_pos = client.player_position();
let relevant = self.grid.query_radius(player_pos, self.relevancy_range);
client.set_relevant_entities(relevant);
}
}
pub fn should_replicate(&self, client_id: ClientId, entity_id: EntityId) -> bool {
let client = self.clients.get(client_id);
client.relevant_entities.contains(&entity_id)
}
}
}
Priority-Based Updates
#![allow(unused)]
fn main() {
pub struct ReplicationPriority {
pub distance_factor: f32,
pub importance: f32,
pub update_frequency: f32,
}
impl ReplicationPriority {
pub fn calculate(client_pos: Vec3, entity: &Entity) -> f32 {
let distance = (entity.position - client_pos).length();
let distance_priority = 1.0 / (1.0 + distance * 0.1);
let importance_priority = match entity.entity_type {
EntityType::Player => 1.0,
EntityType::Projectile => 0.8,
EntityType::Enemy => 0.6,
EntityType::Pickup => 0.4,
EntityType::Decoration => 0.1,
};
distance_priority * importance_priority
}
}
// Send high-priority entities every frame, low-priority less often
pub fn send_updates(&mut self, delta_time: f32) {
for client in &mut self.clients {
let mut updates = Vec::new();
for entity_id in &client.relevant_entities {
let entity = self.world.get_entity(*entity_id);
let priority = ReplicationPriority::calculate(client.position(), entity);
// Update frequency based on priority
let update_interval = 1.0 / (60.0 * priority);
if entity.time_since_update(client.id) >= update_interval {
updates.push(entity.create_update());
entity.mark_updated(client.id);
}
}
client.send_updates(updates)?;
}
}
}
Complete Example
Multiplayer FPS Game
#![allow(unused)]
fn main() {
use astraweave_net::*;
// Server
pub struct GameServer {
server: Server,
world: World,
lag_compensation: LagCompensationSystem,
relevancy: RelevancySystem,
}
impl GameServer {
pub fn run(&mut self) {
loop {
let delta_time = 1.0 / 60.0;
// Process events
while let Some(event) = self.server.poll_event() {
match event {
ServerEvent::MessageReceived { client_id, message } => {
self.handle_message(client_id, message);
}
_ => {}
}
}
// Update game logic
self.world.update(delta_time);
// Record for lag compensation
self.lag_compensation.record_snapshot(&self.world, current_time());
// Update relevancy
self.relevancy.update_relevancy(&self.server.clients(), &self.world.entities());
// Send state updates
self.send_state_updates();
self.server.tick(delta_time).unwrap();
std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}
fn handle_message(&mut self, client_id: ClientId, message: ClientMessage) {
match message {
ClientMessage::Input(input) => {
self.process_input(client_id, input);
}
ClientMessage::Shot(shot_data) => {
if let Some(hit) = self.process_shot(client_id, shot_data) {
self.broadcast_hit(hit);
}
}
}
}
}
// Client
pub struct GameClient {
client: Client,
local_player: PredictedPlayer,
remote_players: HashMap<EntityId, InterpolatedEntity>,
}
impl GameClient {
pub fn run(&mut self) {
loop {
let delta_time = 1.0 / 60.0;
// Process server messages
while let Some(event) = self.client.poll_event() {
match event {
ClientEvent::StateUpdate { state } => {
self.apply_state_update(state);
}
_ => {}
}
}
// Gather input
let input = self.gather_input();
self.local_player.send_input(input, &mut self.client);
// Interpolate remote entities
let current_time = current_time();
for entity in self.remote_players.values_mut() {
let target = entity.interpolate(current_time);
// Apply to visual representation
}
self.client.tick(delta_time).unwrap();
std::thread::sleep(std::time::Duration::from_secs_f32(delta_time));
}
}
}
}
Related Documentation
- Physics System - Server-side physics simulation
- ECS Integration - Network-replicated components
- Security - Anti-cheat and validation
- Performance Guide - Network optimization
API Reference
For complete API documentation, see:
Fluids System
Status: Production Ready (A+ Grade)
Coverage: 94.2% (2,404 tests)
Crate:astraweave-fluids
AstraWeave’s fluid simulation system provides production-quality water effects for games, featuring Position-Based Dynamics (PBD) particle simulation, volumetric grids, and comprehensive visual effects.
Overview
Core Systems
| System | Description |
|---|---|
| Position-Based Dynamics (PBD) | GPU-accelerated particle simulation |
| Volumetric Grid | Voxel-based water for building/terrain interaction |
| Terrain Integration | Automatic river, lake, and waterfall detection |
| SPH Kernels | Smoothed Particle Hydrodynamics (poly6, spiky, viscosity) |
Visual Effects
| Effect | Description |
|---|---|
| Caustics | Underwater light refraction patterns |
| God Rays | Volumetric light shafts through water |
| Reflections | Screen-space and planar water reflections |
| Foam | Dynamic whitecaps, wakes, and shore foam |
| Particles | Waterfalls, bubbles, debris, and spray |
Quick Start
#![allow(unused)]
fn main() {
use astraweave_fluids::{WaterEffectsManager, WaterQualityPreset};
// Create water effects manager with quality preset
let manager = WaterEffectsManager::from_preset(WaterQualityPreset::High)?;
// Game loop
loop {
// Update water simulation
manager.update(delta_time, camera_pos, water_height);
// Render water
manager.render(&render_context);
}
}
Architecture
Module Overview
astraweave-fluids/
├── Core Simulation
│ ├── pcisph_system.rs # PCISPH pressure solver
│ ├── unified_solver.rs # Combined solver
│ ├── viscosity.rs # Viscosity handling
│ ├── boundary.rs # Domain boundaries
│ └── simd_ops.rs # SIMD optimizations
├── Visual Effects
│ ├── caustics.rs # Light caustics
│ ├── god_rays.rs # Volumetric rays
│ ├── foam.rs # Foam generation
│ ├── water_reflections.rs # Reflection system
│ └── underwater.rs # Underwater effects
├── Terrain Integration
│ ├── terrain_integration.rs
│ ├── volume_grid.rs
│ └── waterfall.rs
└── Editor/Tools
├── editor.rs # Editor integration
├── profiling.rs # Performance stats
└── serialization.rs # Save/load
SPH Simulation
Kernel Functions
AstraWeave implements standard SPH kernels:
#![allow(unused)]
fn main() {
// Poly6 kernel for density calculation
fn poly6_kernel(r_sq: f32, h: f32) -> f32 {
let h2 = h * h;
if r_sq > h2 { return 0.0; }
let diff = h2 - r_sq;
POLY6_FACTOR / h.powi(9) * diff * diff * diff
}
// Spiky kernel for pressure gradient
fn spiky_gradient(r: Vec3, r_len: f32, h: f32) -> Vec3 {
if r_len > h || r_len < 1e-6 { return Vec3::ZERO; }
let diff = h - r_len;
SPIKY_FACTOR / h.powi(6) * diff * diff * (-r / r_len)
}
// Viscosity kernel for velocity diffusion
fn viscosity_laplacian(r_len: f32, h: f32) -> f32 {
if r_len > h { return 0.0; }
VISCOSITY_FACTOR / h.powi(3) * (h - r_len)
}
}
Performance
| Operation | 1K Particles | 10K Particles | 100K Particles |
|---|---|---|---|
| Density calc | 5.3 µs | 53 µs | 530 µs |
| Pressure solve | 12 µs | 120 µs | 1.2 ms |
| SPH kernels | — | — | 171-223 µs |
| Full step | 1.8-3.0 ms | — | — |
Volume Grid
For voxel-based water simulation:
#![allow(unused)]
fn main() {
use astraweave_fluids::{VolumeGrid, FlowDirection};
// Create volume grid
let mut grid = VolumeGrid::new(
dimensions: (64, 32, 64), // Grid size
cell_size: 0.5, // World units per cell
);
// Fill cells
grid.set_water_level(x, y, z, 1.0);
// Simulate flow
grid.simulate_step(delta_time);
// Query water at position
let water_level = grid.sample_at(world_pos);
}
Water Effects Manager
The WaterEffectsManager coordinates all water visual effects:
#![allow(unused)]
fn main() {
use astraweave_fluids::{
WaterEffectsManager,
WaterQualityPreset,
CausticsConfig,
GodRaysConfig,
FoamConfig,
};
// Create with custom configuration
let manager = WaterEffectsManager::new()
.with_caustics(CausticsConfig {
intensity: 0.8,
scale: 2.0,
speed: 0.5,
})
.with_god_rays(GodRaysConfig {
density: 0.3,
decay: 0.95,
samples: 64,
})
.with_foam(FoamConfig {
threshold: 0.7,
persistence: 2.0,
});
}
Quality Presets
| Preset | Particles | Grid Resolution | Effects |
|---|---|---|---|
| Low | 1,000 | 32³ | Basic reflections |
| Medium | 5,000 | 64³ | + Caustics |
| High | 10,000 | 128³ | + God rays, foam |
| Ultra | 25,000 | 256³ | All effects |
Terrain Integration
Automatic water body detection:
#![allow(unused)]
fn main() {
use astraweave_fluids::{TerrainWaterIntegration, WaterBodyType};
let integration = TerrainWaterIntegration::new(&terrain);
// Detect water bodies
let water_bodies = integration.detect_water_bodies();
for body in water_bodies {
match body.body_type {
WaterBodyType::Lake { depth, .. } => {
println!("Lake detected: depth = {}", depth);
}
WaterBodyType::River { flow_speed, .. } => {
println!("River detected: flow = {}", flow_speed);
}
WaterBodyType::Waterfall { height, .. } => {
println!("Waterfall detected: height = {}", height);
}
}
}
}
Building Integration
Interactive water elements for building systems:
#![allow(unused)]
fn main() {
use astraweave_fluids::{
WaterBuildingManager,
WaterDispenser,
WaterDrain,
WaterGate,
WaterWheel,
};
let mut water_manager = WaterBuildingManager::new(&volume_grid);
// Water source
let dispenser = WaterDispenser::new(position, flow_rate: 5.0);
water_manager.add_dispenser(dispenser);
// Water sink
let drain = WaterDrain::new(position, capacity: 10.0);
water_manager.add_drain(drain);
// Player-controlled gate
let gate = WaterGate::new(position, width: 2.0);
water_manager.add_gate(gate);
// Power generator
let wheel = WaterWheel::new(position, radius: 1.5);
water_manager.add_wheel(wheel);
// Query power generation
let power = water_manager.get_wheel_power(&wheel);
}
Editor Integration
Full editor support with undo/redo:
#![allow(unused)]
fn main() {
use astraweave_fluids::editor::{
FluidEditorConfig,
ConfigHistory,
ValidationIssue,
};
let mut config = FluidEditorConfig::default();
let mut history = ConfigHistory::new();
// Make changes with history tracking
history.push(config.clone());
config.physics.viscosity = 0.5;
// Undo
if let Some(prev) = history.undo() {
config = prev;
}
// Validate configuration
let issues: Vec<ValidationIssue> = config.validate();
for issue in issues {
eprintln!("{}: {}", issue.severity, issue.message);
}
}
Serialization
Save and load fluid state:
#![allow(unused)]
fn main() {
use astraweave_fluids::serialization::{FluidSnapshot, save_snapshot, load_snapshot};
// Save current state
let snapshot = FluidSnapshot::capture(&simulation);
save_snapshot(&snapshot, "saves/water_state.bin")?;
// Load state
let loaded = load_snapshot("saves/water_state.bin")?;
simulation.restore(&loaded);
}
Performance Optimization
SIMD Operations
The fluids system uses SIMD for critical operations:
#![allow(unused)]
fn main() {
use astraweave_fluids::simd_ops;
// Batch particle updates (SIMD accelerated)
simd_ops::update_positions_batch(&mut particles, delta_time);
simd_ops::compute_densities_batch(&particles, &neighbors, kernel_h);
}
LOD System
Distance-based quality reduction:
#![allow(unused)]
fn main() {
use astraweave_fluids::lod::{FluidLodConfig, LodLevel};
let lod_config = FluidLodConfig {
levels: vec![
LodLevel { distance: 0.0, particle_skip: 1 }, // Full detail
LodLevel { distance: 50.0, particle_skip: 2 }, // 50% particles
LodLevel { distance: 100.0, particle_skip: 4 }, // 25% particles
],
cull_distance: 200.0,
};
}
Profiling
Built-in performance monitoring:
#![allow(unused)]
fn main() {
use astraweave_fluids::profiling::FluidProfiler;
let profiler = FluidProfiler::new();
// Wrap simulation step
profiler.begin("simulation_step");
simulation.step(dt);
profiler.end("simulation_step");
// Get statistics
let stats = profiler.get_stats();
println!("Avg step time: {:?}", stats.avg_step_time);
println!("Particle count: {}", stats.active_particles);
}
Benchmarks
From Master Benchmark Report:
| Benchmark | Result | Notes |
|---|---|---|
| Particle operations (1K) | 5.3 µs | 100-322 Melem/s |
| Spatial hashing | 163 µs - 5.6 ms | 38-62% improvement |
| SPH kernels (100K) | 171-223 µs | poly6/spiky/viscosity |
| Simulation step (1K) | 1.8-3.0 ms | — |
| Multi-step | 450-500 µs | 45-57% faster |
| GPU data prep | 0.9-2.6 ns | Sub-nanosecond |
Examples
Basic Water Pool
cargo run --example fluids_demo -- --preset pool
Interactive Waterfall
cargo run --example fluids_demo -- --preset waterfall
Building Water System
cargo run --example fluids_demo -- --preset building
See Also
- API Reference - Detailed API documentation
- Benchmarks - Performance data
- Physics System - Physics integration
- Terrain System - Terrain integration
Terrain System
Status: Production Ready
Coverage: 71.5%
Crate:astraweave-terrain
AstraWeave’s terrain system provides procedural terrain generation using noise functions, voxel data, and biome classification with full LOD support and async streaming.
Overview
Core Features
| Feature | Description |
|---|---|
| Voxel Data | Density-based terrain with material IDs |
| Marching Cubes | 256-configuration mesh generation |
| Biome System | Climate-based biome distribution |
| Erosion Simulation | Hydraulic, thermal, and wind erosion |
| LOD Management | Distance-based quality with hysteresis |
| Async Streaming | Background chunk loading |
Performance
| Benchmark | Result |
|---|---|
| World chunk generation | 15.06 ms |
| Marching cubes mesh | ~5 ms per chunk |
| SIMD heightmap | 2-3× faster than scalar |
Quick Start
#![allow(unused)]
fn main() {
use astraweave_terrain::{
ChunkManager, TerrainChunk, ChunkId,
Heightmap, HeightmapConfig,
Biome, BiomeType,
};
// Create heightmap generator
let heightmap = Heightmap::new(HeightmapConfig {
seed: 12345,
octaves: 6,
frequency: 0.01,
amplitude: 100.0,
});
// Generate chunk
let chunk_id = ChunkId::new(0, 0);
let chunk = TerrainChunk::generate(&heightmap, chunk_id);
// Get height at world position
let height = heightmap.sample(world_x, world_z);
}
Architecture
Module Overview
astraweave-terrain/
├── Generation
│ ├── noise_gen.rs # Noise generation
│ ├── noise_simd.rs # SIMD-optimized noise
│ ├── heightmap.rs # Heightmap generation
│ └── structures.rs # Structure placement
├── Voxel System
│ ├── voxel_data.rs # Voxel grid & chunks
│ ├── chunk.rs # Chunk management
│ └── terrain_modifier.rs # Runtime modification
├── Meshing
│ ├── meshing.rs # Mesh generation
│ ├── marching_cubes_tables.rs # MC lookup tables
│ └── lod_blending.rs # LOD mesh morphing
├── Biomes
│ ├── biome.rs # Biome types
│ ├── biome_blending.rs # Smooth transitions
│ ├── climate.rs # Climate simulation
│ └── scatter.rs # Vegetation placement
├── Erosion
│ ├── erosion.rs # Basic erosion
│ └── advanced_erosion.rs # Multi-type erosion
├── Streaming
│ ├── background_loader.rs # Async loading
│ ├── lod_manager.rs # LOD control
│ └── streaming_diagnostics.rs # Debug info
└── Integration
├── partition_integration.rs # World streaming
├── texture_splatting.rs # Material blending
└── terrain_persistence.rs # Save/load
Voxel System
Voxel Data Structure
#![allow(unused)]
fn main() {
use astraweave_terrain::{Voxel, Density, MaterialId, VoxelChunk, CHUNK_SIZE};
// Voxel with density and material
let voxel = Voxel {
density: Density::new(0.7), // 0.0 = air, 1.0 = solid
material: MaterialId::STONE,
};
// Create empty chunk
let mut chunk = VoxelChunk::new();
// Set voxel at local position
chunk.set(x, y, z, voxel);
// Get voxel
let v = chunk.get(x, y, z);
}
Chunk Coordinates
#![allow(unused)]
fn main() {
use astraweave_terrain::{ChunkCoord, ChunkId, CHUNK_SIZE};
// World position to chunk
let chunk_coord = ChunkCoord::from_world_pos(world_pos);
// Chunk ID for manager
let chunk_id = ChunkId::new(chunk_coord.x, chunk_coord.z);
// Local position within chunk
let local_pos = chunk_coord.to_local(world_pos);
}
Heightmap Generation
Basic Heightmap
#![allow(unused)]
fn main() {
use astraweave_terrain::{Heightmap, HeightmapConfig};
let config = HeightmapConfig {
seed: 42,
octaves: 6, // Detail levels
frequency: 0.01, // Base frequency
amplitude: 100.0, // Height range
persistence: 0.5, // Octave decay
lacunarity: 2.0, // Octave frequency multiplier
};
let heightmap = Heightmap::new(config);
// Sample height at position
let height = heightmap.sample(x, z);
// Get normal at position
let normal = heightmap.normal(x, z);
}
SIMD-Optimized Generation
For bulk generation (2-3× faster):
#![allow(unused)]
fn main() {
use astraweave_terrain::SimdHeightmapGenerator;
let generator = SimdHeightmapGenerator::new(config);
// Generate entire chunk heightmap
let heights: Vec<f32> = generator.generate_chunk(chunk_coord);
}
Biome System
Biome Types
#![allow(unused)]
fn main() {
use astraweave_terrain::{BiomeType, Biome, BiomeConfig};
pub enum BiomeType {
Ocean,
Beach,
Plains,
Forest,
Taiga,
Desert,
Savanna,
Jungle,
Swamp,
Mountain,
SnowyMountain,
Tundra,
}
// Biome configuration
let forest = BiomeConfig {
biome_type: BiomeType::Forest,
temperature_range: (0.3, 0.7),
humidity_range: (0.5, 0.8),
height_modifier: 1.0,
vegetation_density: 0.8,
};
}
Climate-Based Distribution
#![allow(unused)]
fn main() {
use astraweave_terrain::{ClimateMap, ClimateConfig};
let climate = ClimateMap::new(ClimateConfig {
seed: 12345,
temperature_scale: 0.001,
humidity_scale: 0.002,
});
// Get biome at position
let biome = climate.get_biome_at(x, z);
// Get climate values
let (temp, humidity) = climate.sample(x, z);
}
Biome Blending
#![allow(unused)]
fn main() {
use astraweave_terrain::{BiomeBlender, BiomeBlendConfig};
let blender = BiomeBlender::new(BiomeBlendConfig {
blend_distance: 16.0, // Transition width
noise_strength: 0.2, // Edge noise
});
// Get blended biome weights at position
let weights: Vec<BiomeWeight> = blender.get_weights(x, z);
// Apply to terrain materials
for weight in weights {
material.blend(weight.biome, weight.strength);
}
}
Mesh Generation
Marching Cubes
#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::{ChunkMesh, MeshVertex};
// Generate mesh from voxel chunk
let mesh = ChunkMesh::from_voxels(&chunk, &heightmap);
// Access mesh data
for vertex in mesh.vertices() {
let pos = vertex.position;
let normal = vertex.normal;
let uv = vertex.uv;
}
}
Dual Contouring
For sharper edges:
#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::DualContouring;
let dc = DualContouring::new();
let mesh = dc.generate(&chunk, edge_sharpness: 0.8);
}
LOD Mesh Generation
#![allow(unused)]
fn main() {
use astraweave_terrain::meshing::{LodMeshGenerator, LodConfig};
let generator = LodMeshGenerator::new(LodConfig {
levels: vec![
LodLevel { distance: 0.0, subdivisions: 8 },
LodLevel { distance: 100.0, subdivisions: 4 },
LodLevel { distance: 300.0, subdivisions: 2 },
LodLevel { distance: 500.0, subdivisions: 1 },
],
});
// Generate LOD mesh for distance
let mesh = generator.generate(&chunk, camera_distance);
}
Erosion Simulation
Hydraulic Erosion
#![allow(unused)]
fn main() {
use astraweave_terrain::{
AdvancedErosionSimulator,
HydraulicErosionConfig,
ErosionPreset,
};
let erosion = AdvancedErosionSimulator::new();
// Apply preset
let config = ErosionPreset::MountainRivers.into_config();
erosion.simulate_hydraulic(&mut heightmap, config, iterations: 50000);
// Or custom config
let custom = HydraulicErosionConfig {
rain_amount: 0.01,
erosion_rate: 0.3,
deposition_rate: 0.3,
evaporation_rate: 0.02,
min_slope: 0.01,
};
}
Thermal Erosion
#![allow(unused)]
fn main() {
use astraweave_terrain::ThermalErosionConfig;
let thermal_config = ThermalErosionConfig {
talus_angle: 0.5, // Max stable slope
erosion_rate: 0.1,
};
erosion.simulate_thermal(&mut heightmap, thermal_config, iterations: 1000);
}
Wind Erosion
#![allow(unused)]
fn main() {
use astraweave_terrain::WindErosionConfig;
let wind_config = WindErosionConfig {
wind_direction: Vec3::new(1.0, 0.0, 0.5).normalize(),
wind_strength: 0.5,
particle_count: 10000,
};
erosion.simulate_wind(&mut heightmap, wind_config, iterations: 5000);
}
Async Streaming
Background Loader
#![allow(unused)]
fn main() {
use astraweave_terrain::{
BackgroundChunkLoader,
StreamingConfig,
StreamingStats,
};
let loader = BackgroundChunkLoader::new(StreamingConfig {
load_radius: 8, // Chunks to keep loaded
unload_radius: 12, // Distance to unload
max_concurrent_loads: 4, // Parallel chunk loading
priority_distance: 2, // High-priority radius
});
// Update from camera position
loader.update(camera_pos);
// Poll for completed chunks
while let Some(chunk) = loader.poll_completed() {
chunk_manager.insert(chunk);
}
// Get statistics
let stats: StreamingStats = loader.stats();
println!("Chunks loaded: {}", stats.chunks_loaded);
println!("Load queue: {}", stats.queue_length);
}
LOD Manager
#![allow(unused)]
fn main() {
use astraweave_terrain::{LodManager, LodConfig, LodStats};
let lod_manager = LodManager::new(LodConfig {
hysteresis: 0.1, // Prevent LOD flickering
update_interval: 0.5, // Seconds between LOD updates
});
// Update LOD states
lod_manager.update(camera_pos, delta_time);
// Get current LOD for chunk
let lod_level = lod_manager.get_lod(chunk_id);
}
Streaming Diagnostics
#![allow(unused)]
fn main() {
use astraweave_terrain::StreamingDiagnostics;
let diagnostics = StreamingDiagnostics::new();
// Record frame data
diagnostics.record_frame(frame_time, chunks_loaded, chunks_visible);
// Detect hitches
if let Some(hitch) = diagnostics.detect_hitch() {
warn!("Streaming hitch: {:?}", hitch);
}
// Generate report
let report = diagnostics.generate_report();
}
Runtime Modification
Terrain Modifier
#![allow(unused)]
fn main() {
use astraweave_terrain::{TerrainModifier, VoxelOp, VoxelOpType};
let mut modifier = TerrainModifier::new(&mut chunk_manager);
// Dig sphere
modifier.apply(VoxelOp {
op_type: VoxelOpType::Subtract,
center: world_pos,
radius: 5.0,
material: None,
});
// Add material
modifier.apply(VoxelOp {
op_type: VoxelOpType::Add,
center: world_pos,
radius: 3.0,
material: Some(MaterialId::STONE),
});
// Smooth area
modifier.apply(VoxelOp {
op_type: VoxelOpType::Smooth,
center: world_pos,
radius: 4.0,
material: None,
});
// Batch operations for efficiency
modifier.begin_batch();
for op in operations {
modifier.apply(op);
}
modifier.end_batch(); // Regenerates affected meshes once
}
Texture Splatting
Material Blending
#![allow(unused)]
fn main() {
use astraweave_terrain::{
SplatMapGenerator,
SplatConfig,
SplatRule,
TerrainMaterial,
};
let generator = SplatMapGenerator::new(SplatConfig {
resolution: 512,
rules: vec![
SplatRule::height_based(TerrainMaterial::Grass, 0.0, 50.0),
SplatRule::height_based(TerrainMaterial::Rock, 50.0, 100.0),
SplatRule::height_based(TerrainMaterial::Snow, 100.0, 200.0),
SplatRule::slope_based(TerrainMaterial::Cliff, 0.7, 1.0),
],
});
let splat_map = generator.generate(&heightmap);
}
Triplanar Mapping
#![allow(unused)]
fn main() {
use astraweave_terrain::TriplanarWeights;
// Calculate triplanar weights from normal
let weights = TriplanarWeights::from_normal(vertex_normal);
// Sample texture with triplanar projection
let color =
sample_xz(uv_xz) * weights.y +
sample_xy(uv_xy) * weights.z +
sample_zy(uv_zy) * weights.x;
}
Persistence
Save/Load Terrain
#![allow(unused)]
fn main() {
use astraweave_terrain::terrain_persistence::{save_terrain, load_terrain};
// Save modified terrain
save_terrain(&chunk_manager, "saves/terrain.bin")?;
// Load terrain
let loaded = load_terrain("saves/terrain.bin")?;
chunk_manager.restore(&loaded);
}
AI Integration
Terrain Solver
For AI-driven terrain queries:
#![allow(unused)]
fn main() {
use astraweave_terrain::{TerrainSolver, ResolvedLocation};
let solver = TerrainSolver::new(&chunk_manager);
// Find valid location for AI agent
let result: ResolvedLocation = solver.find_valid_location(
search_center,
radius: 10.0,
constraints: &[
Constraint::MinHeight(5.0),
Constraint::MaxSlope(0.3),
Constraint::RequireBiome(BiomeType::Forest),
],
)?;
println!("Found location: {:?}", result.position);
}
Blueprint Zone System
The Blueprint Zone system enables polygon-based spatial control over vegetation generation and heightmap injection, bridging .blend scene imports with the terrain scatter pipeline.
Overview
Editor Canvas → BlueprintZone → ZoneScatterGenerator → ZoneGenerationResult
↓ ↓ ↓ ↓
Polygon drawing ZoneRegistry Replica/Inspired placements + patches
(save/load) mode dispatch ↓
apply_heightmap_patches()
↓
TerrainChunk updates
Zone Data Model
#![allow(unused)]
fn main() {
use astraweave_terrain::blueprint_zone::*;
// Define a zone with polygon vertices
let zone = BlueprintZone {
id: ZoneId(1),
name: "Forest Clearing".into(),
vertices: vec![[0.0, 0.0], [100.0, 0.0], [100.0, 100.0], [0.0, 100.0]],
source: ZoneSource::BlendScene {
pack_path: "assets/pine_forest.biomepack".into(),
placement_mode: PlacementMode::Replica,
},
priority: 0,
enabled: true,
};
// ZoneRegistry — CRUD, spatial queries, persistence
let mut registry = ZoneRegistry::new();
registry.add_zone(zone);
let zones = registry.zones_containing_point(50.0, 50.0);
registry.save(&Path::new("zones.json"))?;
let loaded = ZoneRegistry::load(&Path::new("zones.json"))?;
}
Placement Modes
| Mode | Behavior |
|---|---|
| Replica | 1:1 reproduction — fixed positions from .blend scene, scaled by AdaptiveScaleParams |
| Inspired | Procedural scatter using ScatterConfig derived from BiomePack, respects zone polygon |
| BiomePreset | Pure biome-driven scatter (Grassland, Forest, Desert, etc.) without .blend data |
Zone-Scoped Generation
#![allow(unused)]
fn main() {
use astraweave_terrain::zone_scatter::*;
let gen = ZoneScatterGenerator::new(256.0, 128); // chunk_size, heightmap_resolution
let result = gen.generate_zone_scatter(&zone, &biome_pack)?;
// result.placements — Vec<VegetationInstance> (position, rotation, scale, model_path)
// result.heightmap_patches — Vec<HeightmapPatch> (per-chunk height modifications)
// Apply heightmap patches to terrain chunks
let results = vec![result];
apply_heightmap_patches(&mut chunk_map, &results);
}
Adaptive Scaling
When zone area differs from source scene footprint, density and scale adjust automatically:
#![allow(unused)]
fn main() {
let params = AdaptiveScaleParams::compute(reference_area, zone_area);
// density_multiplier = sqrt(zone_area / reference_area)
// scale_multiplier = (zone_area / reference_area)^0.25
// position_scale = sqrt(zone_area / reference_area)
}
Boundary Blending
apply_boundary_blending() uses smoothstep falloff at zone edges:
- Vegetation: density fades via
BlendMask::sample(x, z)→ 0.0–1.0 - Heightmap: height delta scaled by mask value at each sample point
- Manual:
BrushMode::ZoneBlendfor blend-weight painting in-editor
Heightmap Rasterization
Terrain meshes from .blend decomposition are rasterized into heightmaps:
#![allow(unused)]
fn main() {
use astraweave_blend::heightmap_raster::*;
let heightmap = rasterize_terrain_meshes(&terrain_meshes, resolution)?;
let height = heightmap.sample_bilinear(u, v); // Normalized [0,1] coords
let area = heightmap.footprint_area(); // World-space area in m²
}
Editor Integration
The BlueprintPanel provides a 2D canvas for polygon zone editing:
- Tools: Select, DrawPolygon, MoveVertex, DeleteZone
- Zone inspector with name, source selection, placement mode toggle
- Undo/redo via command stack
- Save/Load zones as
.zones.json
The BlueprintOverlay projects zone polygons as wireframe outlines into the 3D viewport, integrated with the physics renderer debug line pass.
Test Coverage
| Test Suite | Tests |
|---|---|
blueprint_zone unit tests | 24 |
zone_scatter unit + integration | 27 |
heightmap_raster unit + integration | 21 |
biome_pack extended | 9 |
blueprint_panel unit tests | 17 |
blueprint_overlay unit tests | 7 |
blend_scanner unit tests | 8 |
| Total | 113+ |
See Also
- API Reference - Detailed API documentation
- Fluids System - Water integration
- Physics System - Collision terrain
- Navigation - Navmesh from terrain
Blueprint Zone Editor
Status: Production Ready
Tests: 125+
Crates:astraweave-terrain,astraweave-blend,aw_editor
The Blueprint Zone system provides polygon-based spatial control for vegetation generation and heightmap injection. It bridges .blend scene imports with AstraWeave’s terrain scatter pipeline, enabling both exact 1:1 reproduction (Replica mode) and procedural variation (Inspired mode) of imported environments.
Overview
Pipeline
.blend Scene → Decomposition → BiomePack → BlueprintZone → ZoneScatterGenerator
↓ ↓ ↓
heightmap_raster ZoneRegistry VegetationInstance
(terrain mesh → (polygon CRUD, + HeightmapPatch
heightmap + fixed spatial queries, ↓
placements) JSON persistence) apply_heightmap_patches()
↓
TerrainChunk updates
Key Features
| Feature | Description |
|---|---|
| Polygon Zones | Arbitrary polygons define generation boundaries |
| Replica Mode | 1:1 fixed placement from .blend scene data |
| Inspired Mode | Procedural scatter using extracted ScatterConfig |
| Biome Presets | Pure biome-driven scatter without .blend data |
| Heightmap Injection | Rasterized terrain meshes applied as height patches |
| Boundary Blending | Smoothstep falloff prevents hard cutoffs at zone edges |
| Adaptive Scaling | Auto-adjusts density/scale when zone ≠ source area |
| Undo/Redo | Full command stack in blueprint panel |
| 3D Overlay | Wireframe zone projection in viewport |
Quick Start
#![allow(unused)]
fn main() {
use astraweave_terrain::blueprint_zone::*;
use astraweave_terrain::zone_scatter::*;
use std::path::Path;
// 1. Define a zone
let zone = BlueprintZone {
id: ZoneId(1),
name: "Pine Forest".into(),
vertices: vec![
[0.0, 0.0], [200.0, 0.0],
[200.0, 150.0], [0.0, 150.0],
],
source: ZoneSource::BlendScene {
pack_path: "assets/pine_forest.biomepack".into(),
placement_mode: PlacementMode::Replica,
},
priority: 0,
enabled: true,
};
// 2. Register it
let mut registry = ZoneRegistry::new();
registry.add_zone(zone.clone());
// 3. Generate scatter
let gen = ZoneScatterGenerator::new(256.0, 128);
let result = gen.generate_zone_scatter(&zone, &biome_pack)?;
println!("{} placements", result.placement_count());
println!("{} height modifications", result.modified_height_count());
// 4. Apply heightmap patches to terrain
let mut chunks = HashMap::new();
// ... populate with TerrainChunks ...
apply_heightmap_patches(&mut chunks, &[result]);
// 5. Persist zones
registry.save(Path::new("assets/zones.json"))?;
}
Zone Data Model
BlueprintZone
A BlueprintZone represents a polygon region with a vegetation/terrain source:
#![allow(unused)]
fn main() {
pub struct BlueprintZone {
pub id: ZoneId, // Unique identifier
pub name: String, // Display name
pub vertices: Vec<[f32; 2]>, // Polygon vertices (XZ plane)
pub source: ZoneSource, // What to generate
pub priority: u32, // Overlap resolution (higher wins)
pub enabled: bool, // Toggle generation
}
}
ZoneSource
#![allow(unused)]
fn main() {
pub enum ZoneSource {
BiomePreset(BiomeType), // Built-in biome scatter
BlendScene {
pack_path: String, // Path to .biomepack
placement_mode: PlacementMode, // Replica or Inspired
},
}
}
PlacementMode
| Mode | Description |
|---|---|
Replica | Exact positions from .blend scene, scaled by AdaptiveScaleParams |
Inspired | Procedural scatter using extracted density/distribution rules |
ZoneRegistry
#![allow(unused)]
fn main() {
let mut registry = ZoneRegistry::new();
// CRUD operations
registry.add_zone(zone);
registry.remove_zone(zone_id);
let zone = registry.get_zone(zone_id);
let zone_mut = registry.get_zone_mut(zone_id);
// Spatial queries
let zones = registry.zones_containing_point(x, z);
let overlaps = registry.zones_overlapping_rect(min_x, min_z, max_x, max_z);
// Persistence
registry.save(Path::new("zones.json"))?;
let loaded = ZoneRegistry::load(Path::new("zones.json"))?;
}
Zone-Scoped Generation
ZoneScatterGenerator
#![allow(unused)]
fn main() {
use astraweave_terrain::zone_scatter::*;
// Create generator with chunk size and heightmap resolution
let gen = ZoneScatterGenerator::new(256.0, 128);
// Generate for a single zone
let result: ZoneGenerationResult = gen.generate_zone_scatter(&zone, &biome_pack)?;
}
ZoneGenerationResult
#![allow(unused)]
fn main() {
pub struct ZoneGenerationResult {
pub placements: Vec<VegetationInstance>, // What to place
pub heightmap_patches: Vec<HeightmapPatch>, // Height modifications
}
// VegetationInstance fields:
// - position: Vec3
// - rotation: f32
// - scale: f32
// - vegetation_type: String
// - model_path: String
// - terrain_normal: Vec3
}
Multi-Zone Generation
#![allow(unused)]
fn main() {
// Generate scatter for all zones with overlap priority resolution
let results = generate_multi_zone_scatter(&zones, &gen, &biome_packs)?;
// Apply all heightmap patches at once
apply_heightmap_patches(&mut chunk_map, &results);
}
Adaptive Scaling
When a zone’s area differs from the source scene’s footprint, scaling parameters adjust automatically:
#![allow(unused)]
fn main() {
let params = AdaptiveScaleParams::compute(reference_area, zone_area);
}
| Parameter | Formula | Effect |
|---|---|---|
density_multiplier | $\sqrt{\frac{\text{zone}}{\text{ref}}}$ | Adjusts placement density |
scale_multiplier | $\left(\frac{\text{zone}}{\text{ref}}\right)^{0.25}$ | Adjusts object scale |
position_scale | $\sqrt{\frac{\text{zone}}{\text{ref}}}$ | Adjusts position spread |
Example: A zone 4× larger than the source scene would have:
density_multiplier= 2.0 (double the objects)scale_multiplier≈ 1.41 (slightly larger objects)position_scale= 2.0 (spread positions wider)
Boundary Blending
Smoothstep Falloff
Zone edges use smoothstep interpolation to prevent hard vegetation/height cutoffs:
#![allow(unused)]
fn main() {
// BlendMask provides per-point blending weights
let mask = BlendMask::new(resolution, world_bounds);
let weight = mask.sample(x, z); // 0.0 at edge → 1.0 at center
}
Manual Painting
The editor provides BrushMode::ZoneBlend for manually painting blend weights at zone boundaries, giving artists fine control over transition regions.
Heightmap Rasterization
Terrain meshes extracted from .blend files are rasterized into heightmaps:
#![allow(unused)]
fn main() {
use astraweave_blend::heightmap_raster::*;
let heightmap = rasterize_terrain_meshes(&terrain_meshes, 128)?;
// Query rasterized data
let height = heightmap.sample_bilinear(0.5, 0.5); // Normalized UV coords
let area = heightmap.footprint_area(); // m² world-space
// Fixed placements (exact object positions from scene)
let placements: Vec<FixedPlacement> = extract_fixed_placements(&scene_objects);
}
The rasterizer uses ray-triangle intersection with:
- Seam averaging for multi-tile terrain boundaries
- Hole filling via neighbor interpolation for missing samples
Editor Integration
Blueprint Panel
The BlueprintPanel (panel type: Blueprint) provides:
- 2D Canvas: Pan/zoom view for polygon zone drawing
- Tools: Select, DrawPolygon, MoveVertex, DeleteZone
- Zone Inspector: Name, source selection (biome preset or blend scene), placement mode toggle
- Undo/Redo: Full
BlueprintCommandstack with Ctrl+Z/Ctrl+Shift+Z - Persistence: Save/Load zones as
.zones.jsonfiles
Viewport Overlay
BlueprintOverlay projects zone polygons as colored wireframe outlines in the 3D viewport. Zone boundaries are rendered alongside component gizmos and brush cursors in the physics renderer debug line pass.
Asset Browser
The BlendAssetScanner adds .blend file discovery to the asset browser with:
- Automatic directory scanning for
.blendfiles - Decomposition status detection (checking for
manifest.json) - Quick actions: Import Blend Scene, Use as Zone Source
System Wiring
The editor’s update loop dispatches BlueprintAction events:
| Action | Handler | Effect |
|---|---|---|
GenerateZone | handle_generate_zone() | Runs ZoneScatterGenerator for one zone |
GenerateAll | Iterates all zones | Generates scatter for every enabled zone |
ClearGeneration | Clears results | Removes generated placements |
SaveZones | handle_save_zones() | Saves ZoneRegistry to JSON |
LoadZones | handle_load_zones() | Loads registry + syncs panel state |
After each action, sync_zone_overlay() pushes updated zone data to the viewport.
Test Coverage
| Suite | Count | Scope |
|---|---|---|
blueprint_zone unit | 24 | Zone model, registry, polygon math, persistence |
zone_scatter unit | 16 | Generation modes, blending, heightmap patches |
zone_scatter_e2e integration | 11 | Full pipeline, multi-zone, chunk spanning |
heightmap_raster unit | 11 | Rasterization, terrain bounds, seam stitching |
heightmap_raster_e2e integration | 10 | Multi-tile, bilinear sampling, edge cases |
biome_pack unit | 9 | BiomePack extension fields, detection |
blueprint_panel unit | 17 | Canvas, tools, undo/redo, actions |
blueprint_overlay unit | 7 | Wireframe generation, color mapping |
blend_scanner unit | 8 | Directory scanning, status detection |
| Total | 113+ |
See Also
- Terrain System - Core terrain generation and streaming
- Rendering - Renderer debug line pass for overlays
- Physics - DebugLine integration
Cinematics System
Status: Production Ready
Crate:astraweave-cinematics
Coverage: ~85%
The cinematics system provides a timeline-based sequencer for creating cutscenes, scripted events, and cinematic sequences with synchronized camera, animation, audio, and visual effects tracks.
Core Types
| Type | Description |
|---|---|
Time | Precise time representation (seconds/milliseconds) |
Track | Track variant (Camera, Animation, Audio, Fx) |
CameraKey | Camera keyframe with position, rotation, FOV |
Timeline | Collection of tracks with duration |
Sequencer | Playback engine with seek and step |
SequencerEvent | Events emitted during playback |
Quick Start
#![allow(unused)]
fn main() {
use astraweave_cinematics::{Time, Timeline, Track, CameraKey, Sequencer};
// Create a 10-second timeline
let mut timeline = Timeline::new("intro_cutscene", 10.0);
// Add camera keyframes
let keyframes = vec![
CameraKey::new(Time::zero(), [0.0, 5.0, -10.0], [0.0, 0.0, 0.0], 60.0),
CameraKey::new(Time::from_secs(5.0), [10.0, 8.0, -5.0], [0.0, 45.0, 0.0], 55.0),
CameraKey::new(Time::from_secs(10.0), [0.0, 3.0, 0.0], [0.0, 90.0, 0.0], 70.0),
];
timeline.add_camera_track(keyframes);
// Add audio
timeline.add_audio_track("music/intro.ogg", Time::zero(), 0.8);
// Add animation trigger
timeline.add_track(Track::animation(1, "character_wave", Time::from_secs(2.0)));
// Add FX
timeline.add_track(Track::fx("explosion", Time::from_secs(8.0), serde_json::json!({
"scale": 2.0,
"particles": 500
})));
// Playback
let mut sequencer = Sequencer::new();
let dt = 1.0 / 60.0; // 60 FPS
loop {
match sequencer.step(dt, &timeline) {
Ok(events) => {
for event in events {
handle_event(event);
}
}
Err(_) => break, // End of timeline
}
}
}
Architecture
Timeline Structure
Timeline "intro_cutscene" (10.0s)
├── Track::Camera
│ ├── Keyframe @ 0.0s (pos, rot, fov)
│ ├── Keyframe @ 5.0s
│ └── Keyframe @ 10.0s
├── Track::Audio "music/intro.ogg" @ 0.0s
├── Track::Animation target=1 "character_wave" @ 2.0s
└── Track::Fx "explosion" @ 8.0s
Playback Flow
┌───────────────────────────────────────────────────┐
│ Sequencer │
│ ┌─────────┐ ┌───────────┐ ┌─────────────┐ │
│ │ seek() │ → │ step(dt) │ → │ events │ │
│ └─────────┘ └───────────┘ └─────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ For each track, emit events where │ │
│ │ start_time > previous_t && start_time <= t │ │
│ └─────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────┘
Time API
The Time type provides precision time handling:
#![allow(unused)]
fn main() {
use astraweave_cinematics::Time;
// Construction
let t1 = Time::zero(); // 0.0s
let t2 = Time::from_secs(5.0); // 5.0s
let t3 = Time::from_millis(1500.0); // 1.5s
// Access
println!("Seconds: {}", t2.as_secs()); // 5.0
println!("Millis: {}", t2.as_millis()); // 5000.0
// Arithmetic
let sum = t2 + t3; // 6.5s
let diff = t2 - t3; // 3.5s
let added = t2.add_secs(2.5); // 7.5s
// Utilities
let clamped = t2.clamp(Time::zero(), Time::from_secs(4.0)); // 4.0s
let lerped = Time::zero().lerp(Time::from_secs(10.0), 0.3); // 3.0s
// Display
println!("{}", Time::from_secs(2.5)); // "2.50s"
println!("{}", Time::from_millis(500.0)); // "500ms"
}
Track Types
Camera Track
Controls camera position, rotation, and field of view:
#![allow(unused)]
fn main() {
use astraweave_cinematics::{Track, CameraKey, Time};
let track = Track::camera(vec![
CameraKey {
t: Time::zero(),
pos: [0.0, 5.0, -10.0],
rot: [0.0, 0.0, 0.0], // Euler angles (degrees)
fov: 60.0, // Field of view
},
CameraKey {
t: Time::from_secs(5.0),
pos: [10.0, 8.0, -5.0],
rot: [0.0, 45.0, 0.0],
fov: 55.0,
},
]);
assert!(track.is_camera());
assert_eq!(track.keyframe_count(), Some(2));
}
Animation Track
Triggers character/object animations:
#![allow(unused)]
fn main() {
let track = Track::animation(
42, // Target entity ID
"run_forward", // Animation clip name
Time::from_secs(1.5), // Start time
);
assert!(track.is_animation());
assert_eq!(track.start_time(), Some(Time::from_secs(1.5)));
}
Audio Track
Plays sound effects or music:
#![allow(unused)]
fn main() {
let track = Track::audio(
"sfx/explosion.wav", // Audio clip path
Time::from_secs(3.0), // Start time
0.8, // Volume (0.0 - 1.0)
);
assert!(track.is_audio());
}
FX Track
Triggers visual effects with parameters:
#![allow(unused)]
fn main() {
use serde_json::json;
let track = Track::fx(
"particle_burst", // Effect name
Time::from_secs(2.0), // Start time
json!({
"count": 100,
"color": [1.0, 0.5, 0.0],
"lifetime": 2.0
}),
);
assert!(track.is_fx());
}
Timeline API
Creating Timelines
#![allow(unused)]
fn main() {
use astraweave_cinematics::Timeline;
// Named timeline with duration
let mut timeline = Timeline::new("boss_intro", 30.0);
// Empty timeline
let empty = Timeline::empty();
assert!(empty.is_empty());
}
Building Timelines
#![allow(unused)]
fn main() {
let mut timeline = Timeline::new("action_sequence", 15.0);
// Add tracks
timeline.add_camera_track(camera_keyframes);
timeline.add_audio_track("music/action.ogg", Time::zero(), 1.0);
timeline.add_track(Track::animation(1, "attack", Time::from_secs(5.0)));
// Track counts
println!("Camera tracks: {}", timeline.camera_track_count());
println!("Audio tracks: {}", timeline.audio_track_count());
println!("Animation tracks: {}", timeline.animation_track_count());
println!("FX tracks: {}", timeline.fx_track_count());
println!("Total tracks: {}", timeline.track_count());
println!("Total keyframes: {}", timeline.total_keyframes());
}
Timeline Properties
#![allow(unused)]
fn main() {
let timeline = Timeline::new("example", 10.0);
println!("Name: {}", timeline.name);
println!("Duration: {}s", timeline.duration_secs());
println!("Empty: {}", timeline.is_empty());
// Display
println!("{}", timeline); // Timeline("example", duration=10.00s, 0 tracks)
}
Sequencer API
Basic Playback
#![allow(unused)]
fn main() {
use astraweave_cinematics::Sequencer;
let mut seq = Sequencer::new();
let dt = 1.0 / 60.0; // 60 FPS
// Step through timeline
loop {
match seq.step(dt, &timeline) {
Ok(events) => {
for event in events {
process_event(event);
}
}
Err(SeqError::Range(_)) => {
println!("Timeline complete");
break;
}
}
}
}
Seeking
#![allow(unused)]
fn main() {
let mut seq = Sequencer::new();
// Jump to specific time
seq.seek(Time::from_secs(5.0));
// Current time
println!("Current: {}", seq.t);
}
Event Handling
#![allow(unused)]
fn main() {
use astraweave_cinematics::SequencerEvent;
fn handle_event(event: SequencerEvent) {
match event {
SequencerEvent::CameraKey(key) => {
camera.set_position(key.pos);
camera.set_rotation(key.rot);
camera.set_fov(key.fov);
}
SequencerEvent::AnimStart { target, clip } => {
if let Some(entity) = world.get_entity(target) {
animator.play(entity, &clip);
}
}
SequencerEvent::AudioPlay { clip, volume } => {
audio.play(&clip, volume);
}
SequencerEvent::FxTrigger { name, params } => {
fx_system.trigger(&name, params);
}
}
}
}
Serialization
Timelines are fully serializable with Serde:
#![allow(unused)]
fn main() {
use astraweave_cinematics::Timeline;
// Save to JSON
let json = serde_json::to_string_pretty(&timeline)?;
std::fs::write("cutscene.json", &json)?;
// Load from JSON
let loaded: Timeline = serde_json::from_str(&json)?;
// Save to RON (common in game dev)
let ron = ron::to_string(&timeline)?;
}
Example JSON:
{
"name": "intro",
"duration": 10.0,
"tracks": [
{
"Camera": {
"keyframes": [
{ "t": 0.0, "pos": [0, 5, -10], "rot": [0, 0, 0], "fov": 60 },
{ "t": 5.0, "pos": [10, 8, -5], "rot": [0, 45, 0], "fov": 55 }
]
}
},
{
"Audio": {
"clip": "music/intro.ogg",
"start": 0.0,
"volume": 0.8
}
}
]
}
Integration Examples
With ECS
#![allow(unused)]
fn main() {
use astraweave_ecs::World;
use astraweave_cinematics::{Sequencer, Timeline};
fn cinematics_system(world: &mut World, timeline: &Timeline, seq: &mut Sequencer, dt: f32) {
if let Ok(events) = seq.step(dt, timeline) {
for event in events {
match event {
SequencerEvent::CameraKey(key) => {
// Update camera component
if let Some(camera) = world.query_mut::<CameraComponent>().next() {
camera.position = key.pos.into();
camera.rotation = key.rot.into();
camera.fov = key.fov;
}
}
SequencerEvent::AnimStart { target, clip } => {
// Trigger animation on entity
if let Some(anim) = world.get_component_mut::<Animator>(target) {
anim.play(&clip);
}
}
// ... handle other events
}
}
}
}
}
With Game State
#![allow(unused)]
fn main() {
enum GameState {
Playing,
Cutscene { timeline: Timeline, sequencer: Sequencer },
}
impl GameState {
fn update(&mut self, dt: f32) -> Option<SequencerEvent> {
match self {
GameState::Cutscene { timeline, sequencer } => {
match sequencer.step(dt, timeline) {
Ok(events) => {
for event in events {
// Handle events
}
None
}
Err(_) => {
// Cutscene complete, return to gameplay
*self = GameState::Playing;
None
}
}
}
_ => None,
}
}
}
}
Performance
| Operation | Latency | Notes |
|---|---|---|
Time creation | ~1 ns | Zero-cost abstraction |
step() per track | ~50 ns | Linear scan of events |
| Event emission | ~100 ns | Allocation per event |
| Full timeline step | ~500 ns | Typical 10-track timeline |
Optimization Tips
- Pre-sort tracks by start time for faster event lookup
- Reuse sequencers instead of creating new ones
- Batch event handling when multiple events fire simultaneously
- Use
seek()sparingly - stepping is more efficient
Common Patterns
Looping Cutscenes
#![allow(unused)]
fn main() {
let mut seq = Sequencer::new();
loop {
match seq.step(dt, &timeline) {
Ok(events) => handle_events(events),
Err(_) => {
// Loop back to start
seq.seek(Time::zero());
}
}
}
}
Skippable Cutscenes
#![allow(unused)]
fn main() {
fn update_cutscene(seq: &mut Sequencer, timeline: &Timeline, dt: f32, skip_pressed: bool) {
if skip_pressed {
// Jump to end
seq.seek(timeline.duration);
return;
}
if let Ok(events) = seq.step(dt, timeline) {
handle_events(events);
}
}
}
Branching Cutscenes
#![allow(unused)]
fn main() {
struct BranchingCutscene {
timelines: HashMap<String, Timeline>,
current: String,
sequencer: Sequencer,
}
impl BranchingCutscene {
fn switch_to(&mut self, name: &str) {
self.current = name.to_string();
self.sequencer.seek(Time::zero());
}
fn update(&mut self, dt: f32) -> Vec<SequencerEvent> {
let timeline = &self.timelines[&self.current];
self.sequencer.step(dt, timeline).unwrap_or_default()
}
}
}
See Also
- API Reference: Cinematics
- Audio System - Audio integration
- Animation System - Character animation
- Camera System - Camera controls
Building Your First Game
This guide walks you through building a complete game with AstraWeave, from project setup to a playable demo featuring AI companions, physics, and navigation.
Before starting, ensure you have:
- Rust 1.75+ installed
- AstraWeave cloned and building successfully
- Ollama running with a compatible model (see [Installation Guide](../getting-started/installation.md))
Project Overview
We’ll build “Companion Quest” - a small adventure where an AI companion helps the player navigate a dungeon, solve puzzles, and defeat enemies. This showcases:
- AI companion with LLM-powered dialogue
- Physics-based puzzles
- Navigation and pathfinding
- Combat with adaptive enemies
- Save/load functionality
Step 1: Project Setup
Create a New Example
Create a new directory in examples/:
mkdir -p examples/companion_quest/src
Create examples/companion_quest/Cargo.toml:
[package]
name = "companion_quest"
version = "0.1.0"
edition = "2021"
[dependencies]
astraweave-core = { path = "../../astraweave-core" }
astraweave-ecs = { path = "../../astraweave-ecs" }
astraweave-ai = { path = "../../astraweave-ai" }
astraweave-physics = { path = "../../astraweave-physics" }
astraweave-nav = { path = "../../astraweave-nav" }
astraweave-render = { path = "../../astraweave-render" }
astraweave-audio = { path = "../../astraweave-audio" }
astraweave-input = { path = "../../astraweave-input" }
astraweave-llm = { path = "../../astraweave-llm" }
astraweave-gameplay = { path = "../../astraweave-gameplay" }
tokio = { version = "1.0", features = ["rt-multi-thread", "macros"] }
Main Entry Point
Create examples/companion_quest/src/main.rs:
use astraweave_core::prelude::*;
use astraweave_ecs::prelude::*;
use astraweave_ai::prelude::*;
use astraweave_physics::prelude::*;
use astraweave_nav::prelude::*;
fn main() {
let mut world = World::new();
setup_world(&mut world);
setup_player(&mut world);
setup_companion(&mut world);
setup_dungeon(&mut world);
run_game_loop(&mut world);
}
fn setup_world(world: &mut World) {
world.insert_resource(PhysicsConfig::default());
world.insert_resource(NavigationConfig::default());
world.insert_resource(AiConfig {
tick_budget_ms: 8,
max_concurrent_plans: 4,
..Default::default()
});
}
Step 2: Creating the Player
The player entity needs transform, physics, and input components:
#![allow(unused)]
fn main() {
fn setup_player(world: &mut World) {
let player = world.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::capsule(0.5, 1.8),
Player {
health: 100.0,
max_health: 100.0,
stamina: 100.0,
},
InputReceiver::default(),
CharacterController {
speed: 5.0,
jump_force: 8.0,
..Default::default()
},
));
world.insert_resource(PlayerEntity(player));
}
#[derive(Component)]
struct Player {
health: f32,
max_health: f32,
stamina: f32,
}
}
Step 3: Creating the AI Companion
The companion uses AstraWeave’s AI-native architecture:
#![allow(unused)]
fn main() {
fn setup_companion(world: &mut World) {
let companion = world.spawn((
Transform::from_xyz(2.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::capsule(0.4, 1.6),
AiAgent {
personality: "helpful and curious".into(),
knowledge_base: vec![
"I am Spark, a magical companion".into(),
"I can help solve puzzles and fight enemies".into(),
],
},
PerceptionRadius(15.0),
NavAgent {
speed: 4.0,
acceleration: 10.0,
avoidance_radius: 0.5,
},
Companion {
following: true,
follow_distance: 3.0,
},
DialogueCapable::default(),
));
world.insert_resource(CompanionEntity(companion));
}
#[derive(Component)]
struct Companion {
following: bool,
follow_distance: f32,
}
}
Step 4: Building the Dungeon
Create a simple dungeon with rooms and corridors:
#![allow(unused)]
fn main() {
fn setup_dungeon(world: &mut World) {
let floor = world.spawn((
Transform::from_xyz(0.0, 0.0, 0.0),
RigidBody::Static,
Collider::cuboid(50.0, 0.5, 50.0),
Floor,
));
spawn_walls(world);
spawn_doors(world);
spawn_puzzles(world);
spawn_enemies(world);
generate_navmesh(world);
}
fn generate_navmesh(world: &mut World) {
let navmesh = NavMeshBuilder::new()
.cell_size(0.3)
.cell_height(0.2)
.agent_radius(0.5)
.agent_height(1.8)
.max_slope(45.0)
.build_from_world(world);
world.insert_resource(navmesh);
}
}
Step 5: The Game Loop
AstraWeave uses a fixed-tick simulation for determinism:
#![allow(unused)]
fn main() {
fn run_game_loop(world: &mut World) {
let mut scheduler = Scheduler::new();
scheduler.add_system(input_system);
scheduler.add_system(player_movement_system);
scheduler.add_system(ai_perception_system);
scheduler.add_system(ai_planning_system);
scheduler.add_system(companion_follow_system);
scheduler.add_system(navigation_system);
scheduler.add_system(physics_system);
scheduler.add_system(combat_system);
scheduler.add_system(dialogue_system);
scheduler.add_system(render_system);
let tick_rate = Duration::from_secs_f64(1.0 / 60.0);
let mut accumulator = Duration::ZERO;
let mut last_time = Instant::now();
loop {
let now = Instant::now();
let delta = now - last_time;
last_time = now;
accumulator += delta;
while accumulator >= tick_rate {
scheduler.run(world);
accumulator -= tick_rate;
}
if should_quit(world) {
break;
}
}
}
}
Step 6: AI Companion Behavior
The companion uses perception and planning:
#![allow(unused)]
fn main() {
fn companion_follow_system(
player_query: Query<&Transform, With<Player>>,
mut companion_query: Query<(&mut NavAgent, &Companion, &Transform)>,
navmesh: Res<NavMesh>,
) {
let player_pos = player_query.single().translation;
for (mut nav_agent, companion, transform) in companion_query.iter_mut() {
if !companion.following {
continue;
}
let distance = transform.translation.distance(player_pos);
if distance > companion.follow_distance {
let target = player_pos - (player_pos - transform.translation)
.normalize() * companion.follow_distance;
if let Some(path) = navmesh.find_path(transform.translation, target) {
nav_agent.set_path(path);
}
}
}
}
}
Step 7: Dialogue Integration
Enable LLM-powered dialogue with the companion:
#![allow(unused)]
fn main() {
fn dialogue_system(
mut dialogue_events: EventReader<DialogueEvent>,
mut llm_client: ResMut<LlmClient>,
companion_query: Query<&AiAgent, With<Companion>>,
mut dialogue_responses: EventWriter<DialogueResponse>,
) {
for event in dialogue_events.read() {
let agent = companion_query.single();
let prompt = format!(
"You are {}. The player says: '{}'. Respond in character.",
agent.personality,
event.message
);
match llm_client.generate_blocking(&prompt) {
Ok(response) => {
dialogue_responses.send(DialogueResponse {
speaker: "Spark".into(),
text: response,
});
}
Err(e) => {
dialogue_responses.send(DialogueResponse {
speaker: "Spark".into(),
text: "Hmm, I'm not sure what to say...".into(),
});
}
}
}
}
}
Step 8: Combat System
Add combat with tool validation:
#![allow(unused)]
fn main() {
fn combat_system(
mut combat_events: EventReader<CombatEvent>,
mut health_query: Query<&mut Health>,
tool_validator: Res<ToolValidator>,
) {
for event in combat_events.read() {
let validation = tool_validator.validate(&ToolCall {
tool: "attack".into(),
params: json!({
"attacker": event.attacker,
"target": event.target,
"damage": event.damage,
}),
});
match validation {
ToolResult::Success => {
if let Ok(mut health) = health_query.get_mut(event.target) {
health.current -= event.damage;
}
}
ToolResult::Blocked(reason) => {
println!("Attack blocked: {}", reason);
}
}
}
}
}
Step 9: Running Your Game
Build and run:
cargo run -p companion_quest --release
Expected Output
[INFO] AstraWeave v0.1.0 starting...
[INFO] Physics initialized: Rapier 0.17
[INFO] Navigation: NavMesh built (2,450 polygons)
[INFO] AI: Connected to Ollama (hermes2-pro)
[INFO] Companion "Spark" spawned at (2.0, 1.0, 0.0)
[INFO] Game loop started at 60 Hz
Complete Example
See the full working example at examples/companion_quest/ or explore these related examples:
| Example | Description |
|---|---|
hello_companion | Minimal AI companion demo |
adaptive_boss | Multi-phase adaptive boss fight |
quest_dialogue_demo | Dialogue and quest system |
combat_physics_demo | Physics-based combat |
unified_showcase | Full engine demonstration |
Next Steps
- AI Companions - Deep dive into companion AI
- Adaptive Bosses - Creating intelligent enemies
- Dialogue Systems - LLM-powered conversations
- Physics - Physics system details
Enable release mode (`--release`) for LLM inference. Debug builds can be 10-50x slower for AI operations.
AI Companions
AI companions are a signature feature of AstraWeave. Unlike scripted NPCs in traditional engines, companions in AstraWeave use LLM-powered planning to make intelligent decisions while respecting game rules through tool validation.
Companions cannot cheat - they perceive the world, plan actions, and execute through the same validated systems as players.
Companion Architecture
graph LR
subgraph Companion
Perception[Perception]
Memory[Memory]
Personality[Personality]
Planning[LLM Planning]
Actions[Action Queue]
end
World[World State] --> Perception
Perception --> Memory
Memory --> Planning
Personality --> Planning
Planning --> Actions
Actions --> World
Creating a Companion
Basic Companion Setup
#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
use astraweave_ecs::prelude::*;
fn spawn_companion(world: &mut World, name: &str, personality: &str) -> Entity {
world.spawn((
Name::new(name),
Transform::from_xyz(0.0, 0.0, 0.0),
AiAgent::new()
.with_personality(personality)
.with_perception_radius(20.0)
.with_memory_capacity(100),
CompanionBehavior {
follow_distance: 3.0,
engage_distance: 10.0,
flee_health_threshold: 0.2,
},
AvailableTools::new(vec![
Tool::move_to(),
Tool::follow(),
Tool::attack(),
Tool::defend(),
Tool::use_item(),
Tool::speak(),
Tool::investigate(),
]),
PerceptionState::default(),
NavAgent::default(),
DialogueCapable::default(),
Health::new(100.0),
))
}
}
Personality Configuration
Companions have distinct personalities that influence their decisions:
#![allow(unused)]
fn main() {
let warrior_companion = AiAgent::new()
.with_personality("brave and protective warrior who prioritizes the player's safety")
.with_traits(vec![
Trait::Brave,
Trait::Protective,
Trait::DirectCommunicator,
])
.with_knowledge(vec![
"I am a seasoned warrior".into(),
"I protect my allies at all costs".into(),
"I prefer melee combat".into(),
]);
let scholar_companion = AiAgent::new()
.with_personality("curious scholar who loves discovering secrets and solving puzzles")
.with_traits(vec![
Trait::Curious,
Trait::Analytical,
Trait::Cautious,
])
.with_knowledge(vec![
"I study ancient texts and artifacts".into(),
"I prefer avoiding combat when possible".into(),
"I excel at puzzle-solving".into(),
]);
}
Companion Behaviors
Following the Player
#![allow(unused)]
fn main() {
fn companion_follow_system(
player: Query<&Transform, With<Player>>,
mut companions: Query<(&mut NavAgent, &CompanionBehavior, &Transform), With<AiAgent>>,
navmesh: Res<NavMesh>,
) {
let player_pos = player.single().translation;
for (mut nav, behavior, transform) in companions.iter_mut() {
let distance = transform.translation.distance(player_pos);
if distance > behavior.follow_distance {
let target = calculate_follow_position(player_pos, transform.translation, behavior.follow_distance);
if let Some(path) = navmesh.find_path(transform.translation, target) {
nav.set_path(path);
}
} else {
nav.stop();
}
}
}
}
Combat Assistance
#![allow(unused)]
fn main() {
fn companion_combat_system(
mut companions: Query<(&AiAgent, &mut ActionQueue, &Transform, &Health)>,
enemies: Query<(Entity, &Transform, &Health), With<Enemy>>,
player: Query<&Transform, With<Player>>,
) {
let player_pos = player.single().translation;
for (agent, mut actions, companion_pos, health) in companions.iter_mut() {
if health.percentage() < agent.flee_threshold {
actions.push(Action::Flee);
continue;
}
let nearest_enemy = enemies.iter()
.filter(|(_, t, h)| h.current > 0.0)
.min_by_key(|(_, t, _)| {
(t.translation.distance(player_pos) * 100.0) as i32
});
if let Some((enemy_entity, enemy_pos, _)) = nearest_enemy {
let distance = companion_pos.translation.distance(enemy_pos.translation);
if distance < agent.engage_distance {
actions.push(Action::Attack(enemy_entity));
} else {
actions.push(Action::MoveTo(enemy_pos.translation));
}
}
}
}
}
Dialogue Interaction
#![allow(unused)]
fn main() {
fn companion_dialogue_system(
mut events: EventReader<DialogueRequest>,
mut companions: Query<(&AiAgent, &PerceptionState, &mut DialogueState)>,
llm: Res<LlmClient>,
mut responses: EventWriter<DialogueResponse>,
) {
for request in events.read() {
if let Ok((agent, perception, mut dialogue)) = companions.get_mut(request.target) {
let context = build_dialogue_context(agent, perception, &dialogue.history);
let prompt = format!(
"{}\n\nThe player says: '{}'\n\nRespond in character:",
context,
request.message
);
match llm.generate(&prompt) {
Ok(response) => {
dialogue.history.push(DialogueEntry {
speaker: "Player".into(),
text: request.message.clone(),
});
dialogue.history.push(DialogueEntry {
speaker: agent.name.clone(),
text: response.clone(),
});
responses.send(DialogueResponse {
speaker: agent.name.clone(),
text: response,
});
}
Err(_) => {
responses.send(DialogueResponse {
speaker: agent.name.clone(),
text: "Hmm, let me think about that...".into(),
});
}
}
}
}
}
}
Memory System
Companions remember past events and use them in decision-making:
#![allow(unused)]
fn main() {
#[derive(Component)]
struct CompanionMemory {
short_term: VecDeque<MemoryEntry>,
long_term: Vec<MemoryEntry>,
capacity: usize,
}
impl CompanionMemory {
fn remember(&mut self, event: MemoryEntry) {
self.short_term.push_back(event);
if self.short_term.len() > self.capacity {
if let Some(old) = self.short_term.pop_front() {
if old.importance >= 0.7 {
self.long_term.push(old);
}
}
}
}
fn recall(&self, query: &str, limit: usize) -> Vec<&MemoryEntry> {
self.long_term.iter()
.chain(self.short_term.iter())
.filter(|m| m.matches(query))
.take(limit)
.collect()
}
}
#[derive(Clone)]
struct MemoryEntry {
timestamp: GameTime,
event_type: EventType,
description: String,
importance: f32,
entities: Vec<Entity>,
location: Vec3,
}
}
Companion Commands
Players can issue commands to companions:
#![allow(unused)]
fn main() {
#[derive(Event)]
enum CompanionCommand {
Follow,
Stay,
Attack(Entity),
Investigate(Vec3),
UseAbility(AbilityId),
ToggleAggressive,
}
fn handle_companion_commands(
mut commands: EventReader<CompanionCommand>,
mut companions: Query<(&mut CompanionBehavior, &mut ActionQueue)>,
) {
for command in commands.read() {
for (mut behavior, mut actions) in companions.iter_mut() {
match command {
CompanionCommand::Follow => {
behavior.mode = CompanionMode::Follow;
actions.clear();
}
CompanionCommand::Stay => {
behavior.mode = CompanionMode::Stay;
actions.clear();
}
CompanionCommand::Attack(target) => {
actions.push(Action::Attack(*target));
}
CompanionCommand::Investigate(pos) => {
actions.push(Action::MoveTo(*pos));
actions.push(Action::Investigate);
}
CompanionCommand::UseAbility(ability) => {
actions.push(Action::UseAbility(*ability));
}
CompanionCommand::ToggleAggressive => {
behavior.aggressive = !behavior.aggressive;
}
}
}
}
}
}
Configuration
Companion Presets
#![allow(unused)]
fn main() {
pub fn create_healer_companion(world: &mut World) -> Entity {
spawn_companion(world, "Luna", "compassionate healer who prioritizes keeping allies alive")
.with(AvailableTools::new(vec![
Tool::heal(),
Tool::cure_status(),
Tool::buff(),
Tool::follow(),
Tool::flee(),
]))
.with(CompanionBehavior {
follow_distance: 5.0,
engage_distance: 15.0,
flee_health_threshold: 0.3,
mode: CompanionMode::Support,
})
}
pub fn create_tank_companion(world: &mut World) -> Entity {
spawn_companion(world, "Grimjaw", "fearless guardian who draws enemy attention")
.with(AvailableTools::new(vec![
Tool::taunt(),
Tool::block(),
Tool::attack(),
Tool::charge(),
Tool::protect_ally(),
]))
.with(CompanionBehavior {
follow_distance: 2.0,
engage_distance: 8.0,
flee_health_threshold: 0.1,
mode: CompanionMode::Aggressive,
})
}
}
Best Practices
- Limit perception radius for better performance
- Use plan caching to reduce LLM calls
- Batch companion updates in the scheduler
- Don't give companions tools they shouldn't have
- Always define fallback behaviors for LLM timeouts
- Test personality prompts for consistent behavior
See Also
- AI System - Core AI architecture
- Adaptive Bosses - Enemy AI patterns
- Dialogue Systems - Conversation implementation
- Tool Validation - How actions are validated
Adaptive Bosses
Adaptive bosses in AstraWeave learn from player behavior and adjust their strategies in real-time. Unlike scripted boss encounters, these AI-driven enemies create unique, memorable fights that evolve with each attempt.
Bosses use the same AI architecture as companions - perception, planning, and validated tools - but with combat-focused behaviors and learning capabilities.
Boss Architecture
graph TB
subgraph "Boss AI"
Perception[Player Analysis]
Memory[Pattern Memory]
Strategy[Strategy Selection]
Phase[Phase Management]
Actions[Action Execution]
end
Player[Player Behavior] --> Perception
Perception --> Memory
Memory --> Strategy
Strategy --> Phase
Phase --> Actions
Actions --> Player
Creating an Adaptive Boss
Basic Boss Setup
#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
use astraweave_gameplay::boss::*;
fn spawn_adaptive_boss(world: &mut World) -> Entity {
world.spawn((
Name::new("The Hollow Knight"),
Transform::from_xyz(0.0, 2.0, 0.0),
BossAi::new()
.with_phases(3)
.with_adaptation_rate(0.3)
.with_pattern_memory(50),
BossHealth {
current: 10000.0,
max: 10000.0,
phase_thresholds: vec![0.7, 0.4, 0.15],
},
AvailableTools::new(vec![
Tool::melee_combo(),
Tool::ranged_attack(),
Tool::area_attack(),
Tool::summon_minions(),
Tool::teleport(),
Tool::enrage(),
]),
PlayerAnalyzer::default(),
StrategySelector::default(),
RigidBody::Dynamic,
Collider::capsule(1.0, 4.0),
NavAgent::default(),
))
}
}
Phase Configuration
#![allow(unused)]
fn main() {
#[derive(Component)]
struct BossPhaseConfig {
phases: Vec<BossPhase>,
current_phase: usize,
}
struct BossPhase {
name: String,
health_threshold: f32,
available_attacks: Vec<AttackPattern>,
behavior_modifiers: BehaviorModifiers,
transition_animation: Option<AnimationId>,
}
let phase_config = BossPhaseConfig {
phases: vec![
BossPhase {
name: "Cautious".into(),
health_threshold: 1.0,
available_attacks: vec![
AttackPattern::SingleSlash,
AttackPattern::Thrust,
AttackPattern::Sidestep,
],
behavior_modifiers: BehaviorModifiers {
aggression: 0.3,
defense: 0.7,
patience: 0.8,
},
transition_animation: None,
},
BossPhase {
name: "Aggressive".into(),
health_threshold: 0.6,
available_attacks: vec![
AttackPattern::TripleCombo,
AttackPattern::SpinAttack,
AttackPattern::LeapSlam,
AttackPattern::Thrust,
],
behavior_modifiers: BehaviorModifiers {
aggression: 0.7,
defense: 0.3,
patience: 0.4,
},
transition_animation: Some(AnimationId::Enrage),
},
BossPhase {
name: "Desperate".into(),
health_threshold: 0.25,
available_attacks: vec![
AttackPattern::FuryCombo,
AttackPattern::ShadowClones,
AttackPattern::AreaDenial,
AttackPattern::GrabAttack,
],
behavior_modifiers: BehaviorModifiers {
aggression: 1.0,
defense: 0.1,
patience: 0.1,
},
transition_animation: Some(AnimationId::Transform),
},
],
current_phase: 0,
};
}
Player Analysis
Bosses track player behavior to counter their strategies:
#![allow(unused)]
fn main() {
#[derive(Component, Default)]
struct PlayerAnalyzer {
dodge_pattern: DodgePattern,
attack_timing: AttackTiming,
positioning_preference: PositioningStyle,
heal_threshold: f32,
aggression_level: f32,
pattern_history: VecDeque<PlayerAction>,
}
#[derive(Default)]
struct DodgePattern {
left_count: u32,
right_count: u32,
back_count: u32,
roll_timing: Vec<f32>,
}
fn analyze_player_system(
mut analyzers: Query<&mut PlayerAnalyzer, With<BossAi>>,
player_actions: EventReader<PlayerActionEvent>,
) {
for action in player_actions.read() {
for mut analyzer in analyzers.iter_mut() {
analyzer.pattern_history.push_back(action.clone());
if analyzer.pattern_history.len() > 100 {
analyzer.pattern_history.pop_front();
}
match action {
PlayerActionEvent::Dodge(direction) => {
match direction {
Direction::Left => analyzer.dodge_pattern.left_count += 1,
Direction::Right => analyzer.dodge_pattern.right_count += 1,
Direction::Back => analyzer.dodge_pattern.back_count += 1,
_ => {}
}
}
PlayerActionEvent::Attack(timing) => {
analyzer.attack_timing.record(*timing);
}
PlayerActionEvent::Heal => {
analyzer.heal_threshold = calculate_heal_threshold(&analyzer);
}
_ => {}
}
}
}
}
}
Strategy Adaptation
#![allow(unused)]
fn main() {
#[derive(Component)]
struct StrategySelector {
current_strategy: BossStrategy,
strategy_effectiveness: HashMap<BossStrategy, f32>,
adaptation_cooldown: Timer,
}
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
enum BossStrategy {
Aggressive,
Defensive,
Counter,
Pressure,
Bait,
Mixed,
}
fn adapt_strategy_system(
mut bosses: Query<(&PlayerAnalyzer, &mut StrategySelector, &BossHealth)>,
time: Res<Time>,
) {
for (analyzer, mut selector, health) in bosses.iter_mut() {
selector.adaptation_cooldown.tick(time.delta());
if selector.adaptation_cooldown.finished() {
let new_strategy = select_counter_strategy(analyzer);
if new_strategy != selector.current_strategy {
let old_effectiveness = selector.strategy_effectiveness
.get(&selector.current_strategy)
.copied()
.unwrap_or(0.5);
selector.strategy_effectiveness
.insert(selector.current_strategy, old_effectiveness * 0.9);
selector.current_strategy = new_strategy;
selector.adaptation_cooldown.reset();
}
}
}
}
fn select_counter_strategy(analyzer: &PlayerAnalyzer) -> BossStrategy {
if analyzer.aggression_level > 0.7 {
BossStrategy::Counter
} else if analyzer.dodge_pattern.is_predictable() {
BossStrategy::Bait
} else if analyzer.heal_threshold > 0.5 {
BossStrategy::Pressure
} else {
BossStrategy::Mixed
}
}
}
Attack Patterns
#![allow(unused)]
fn main() {
fn boss_attack_system(
mut bosses: Query<(
&BossAi,
&StrategySelector,
&PlayerAnalyzer,
&Transform,
&mut ActionQueue,
)>,
player: Query<&Transform, With<Player>>,
) {
let player_pos = player.single().translation;
for (boss, strategy, analyzer, transform, mut actions) in bosses.iter_mut() {
let distance = transform.translation.distance(player_pos);
let attack = match strategy.current_strategy {
BossStrategy::Aggressive => {
select_aggressive_attack(distance, boss.current_phase)
}
BossStrategy::Counter => {
if analyzer.is_player_attacking() {
Some(AttackPattern::Parry)
} else {
Some(AttackPattern::Wait)
}
}
BossStrategy::Bait => {
let predicted_dodge = analyzer.predict_dodge_direction();
Some(AttackPattern::DelayedStrike(predicted_dodge.opposite()))
}
BossStrategy::Pressure => {
Some(AttackPattern::RelentlessCombo)
}
_ => {
select_random_attack(boss.current_phase)
}
};
if let Some(pattern) = attack {
actions.push(Action::ExecutePattern(pattern));
}
}
}
}
Learning Between Attempts
Bosses can remember strategies across player deaths:
#![allow(unused)]
fn main() {
#[derive(Resource)]
struct BossMemory {
player_deaths: u32,
successful_attacks: HashMap<AttackPattern, u32>,
failed_attacks: HashMap<AttackPattern, u32>,
player_weaknesses: Vec<Weakness>,
}
impl BossMemory {
fn record_attack_result(&mut self, pattern: AttackPattern, hit: bool) {
if hit {
*self.successful_attacks.entry(pattern).or_default() += 1;
} else {
*self.failed_attacks.entry(pattern).or_default() += 1;
}
}
fn get_attack_priority(&self, pattern: AttackPattern) -> f32 {
let successes = self.successful_attacks.get(&pattern).copied().unwrap_or(0) as f32;
let failures = self.failed_attacks.get(&pattern).copied().unwrap_or(0) as f32;
if successes + failures < 3.0 {
return 0.5;
}
successes / (successes + failures)
}
}
}
Example: The Hollow Knight
A complete adaptive boss implementation:
#![allow(unused)]
fn main() {
pub fn spawn_hollow_knight(world: &mut World) -> Entity {
let boss = world.spawn((
Name::new("The Hollow Knight"),
Transform::from_xyz(0.0, 2.0, -20.0),
BossAi::new()
.with_phases(4)
.with_adaptation_rate(0.25),
BossHealth::new(15000.0)
.with_phases(vec![0.75, 0.5, 0.25]),
PlayerAnalyzer::default(),
StrategySelector::new(BossStrategy::Defensive),
AttackPatterns::new(vec![
("slash", AttackPattern::Slash { damage: 80.0, range: 3.0 }),
("thrust", AttackPattern::Thrust { damage: 100.0, range: 5.0 }),
("spin", AttackPattern::Spin { damage: 60.0, radius: 4.0 }),
("leap", AttackPattern::LeapSlam { damage: 120.0, radius: 6.0 }),
("shadow", AttackPattern::ShadowDash { damage: 50.0, distance: 10.0 }),
]),
RigidBody::Dynamic,
Collider::capsule(1.2, 3.5),
NavAgent::default(),
));
world.send_event(BossSpawnedEvent { boss });
boss
}
}
Best Practices
- Start with simpler patterns, add complexity in later phases
- Ensure all attacks are telegraphed and fair
- Test adaptation rates - too fast feels unfair, too slow feels scripted
- Provide recovery windows between attack chains
- Adaptation should challenge, not frustrate
- Preserve attack patterns that players can learn
- Don't punish the same mistake indefinitely
- Allow multiple valid strategies
See Also
- AI System - Core AI architecture
- AI Companions - Friendly AI patterns
- Combat System - Combat mechanics
Crafting & Combat Systems
AstraWeave provides a unified crafting and combat framework that integrates with the AI systems for dynamic, adaptive gameplay. This guide covers implementing both systems with physics integration, AI-driven balancing, and procedural content generation.
Architecture Overview
graph TB
subgraph Combat["Combat System"]
CS[Combat State] --> DM[Damage Model]
DM --> HR[Hit Resolution]
HR --> EF[Effects Pipeline]
EF --> FB[Feedback System]
end
subgraph Crafting["Crafting System"]
IV[Inventory] --> RC[Recipe Check]
RC --> CR[Craft Resolution]
CR --> QR[Quality Roll]
QR --> IT[Item Creation]
end
subgraph Integration["AI Integration"]
AI[AI Director] --> CB[Combat Balancing]
AI --> CG[Craft Guidance]
CB --> CS
CG --> RC
end
Physics[Physics Engine] --> HR
Physics --> EF
Combat System
Combat Components
Define the core components for combat entities:
#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use astraweave_physics::prelude::*;
#[derive(Component, Debug, Clone)]
pub struct CombatStats {
pub health: f32,
pub max_health: f32,
pub stamina: f32,
pub max_stamina: f32,
pub poise: f32,
pub max_poise: f32,
}
impl Default for CombatStats {
fn default() -> Self {
Self {
health: 100.0,
max_health: 100.0,
stamina: 100.0,
max_stamina: 100.0,
poise: 50.0,
max_poise: 50.0,
}
}
}
#[derive(Component, Debug, Clone)]
pub struct DamageResistances {
pub physical: f32,
pub fire: f32,
pub ice: f32,
pub lightning: f32,
pub poison: f32,
}
#[derive(Component, Debug, Clone)]
pub struct WeaponStats {
pub base_damage: f32,
pub damage_type: DamageType,
pub attack_speed: f32,
pub reach: f32,
pub stamina_cost: f32,
pub poise_damage: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum DamageType {
Physical,
Fire,
Ice,
Lightning,
Poison,
}
#[derive(Component, Debug)]
pub struct CombatState {
pub stance: CombatStance,
pub combo_count: u32,
pub combo_timer: f32,
pub stagger_timer: f32,
pub invincibility_frames: f32,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CombatStance {
Neutral,
Attacking,
Blocking,
Dodging,
Staggered,
Recovering,
}
}
Attack System
Implement the attack resolution with physics-based hit detection:
#![allow(unused)]
fn main() {
use astraweave_physics::collision::*;
#[derive(Debug, Clone)]
pub struct AttackEvent {
pub attacker: Entity,
pub weapon: Entity,
pub attack_type: AttackType,
pub direction: Vec3,
}
#[derive(Debug, Clone, Copy)]
pub enum AttackType {
Light,
Heavy,
Special,
Charged { charge_time: f32 },
}
pub fn attack_system(
mut commands: Commands,
mut attack_events: EventReader<AttackEvent>,
mut combat_query: Query<(&mut CombatState, &mut CombatStats, &Transform)>,
weapon_query: Query<&WeaponStats>,
physics: Res<PhysicsWorld>,
) {
for event in attack_events.iter() {
let Ok((mut state, mut stats, transform)) = combat_query.get_mut(event.attacker) else {
continue;
};
if stats.stamina < get_stamina_cost(&event.attack_type) {
continue;
}
let Ok(weapon) = weapon_query.get(event.weapon) else {
continue;
};
stats.stamina -= weapon.stamina_cost * get_attack_multiplier(&event.attack_type);
state.stance = CombatStance::Attacking;
let hitbox = create_attack_hitbox(transform, weapon, &event.attack_type);
commands.spawn((
hitbox,
AttackHitbox {
owner: event.attacker,
damage: calculate_attack_damage(weapon, &event.attack_type, state.combo_count),
damage_type: weapon.damage_type,
poise_damage: weapon.poise_damage,
knockback: event.direction * 5.0,
},
Lifetime { remaining: 0.2 },
));
state.combo_count = (state.combo_count + 1).min(5);
state.combo_timer = 1.5;
}
}
fn calculate_attack_damage(weapon: &WeaponStats, attack_type: &AttackType, combo: u32) -> f32 {
let base = weapon.base_damage;
let type_mult = match attack_type {
AttackType::Light => 1.0,
AttackType::Heavy => 1.8,
AttackType::Special => 2.5,
AttackType::Charged { charge_time } => 1.0 + (charge_time * 0.5).min(2.0),
};
let combo_mult = 1.0 + (combo as f32 * 0.1);
base * type_mult * combo_mult
}
fn create_attack_hitbox(
transform: &Transform,
weapon: &WeaponStats,
attack_type: &AttackType,
) -> Collider {
let size = match attack_type {
AttackType::Light => Vec3::new(weapon.reach * 0.8, 1.0, weapon.reach * 0.8),
AttackType::Heavy => Vec3::new(weapon.reach * 1.2, 1.5, weapon.reach * 1.2),
AttackType::Special => Vec3::new(weapon.reach * 2.0, 2.0, weapon.reach * 2.0),
AttackType::Charged { .. } => Vec3::new(weapon.reach * 1.5, 1.2, weapon.reach * 1.5),
};
Collider::cuboid(size.x, size.y, size.z)
.with_offset(transform.forward() * (weapon.reach * 0.5))
}
}
Damage Resolution
Process hits and apply damage with resistance calculations:
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct AttackHitbox {
pub owner: Entity,
pub damage: f32,
pub damage_type: DamageType,
pub poise_damage: f32,
pub knockback: Vec3,
}
pub fn damage_resolution_system(
mut commands: Commands,
hitbox_query: Query<(Entity, &AttackHitbox, &CollidingEntities)>,
mut target_query: Query<(
&mut CombatStats,
&mut CombatState,
&DamageResistances,
&mut Transform,
)>,
mut damage_events: EventWriter<DamageEvent>,
) {
for (hitbox_entity, hitbox, colliding) in hitbox_query.iter() {
for &target in colliding.iter() {
if target == hitbox.owner {
continue;
}
let Ok((mut stats, mut state, resistances, mut transform)) =
target_query.get_mut(target) else {
continue;
};
if state.invincibility_frames > 0.0 {
continue;
}
let resistance = get_resistance(resistances, hitbox.damage_type);
let final_damage = hitbox.damage * (1.0 - resistance);
stats.health = (stats.health - final_damage).max(0.0);
stats.poise -= hitbox.poise_damage;
if stats.poise <= 0.0 {
state.stance = CombatStance::Staggered;
state.stagger_timer = 1.5;
stats.poise = stats.max_poise * 0.5;
}
transform.translation += hitbox.knockback * 0.1;
damage_events.send(DamageEvent {
target,
attacker: hitbox.owner,
damage: final_damage,
damage_type: hitbox.damage_type,
was_critical: false,
});
state.invincibility_frames = 0.15;
}
commands.entity(hitbox_entity).despawn();
}
}
fn get_resistance(resistances: &DamageResistances, damage_type: DamageType) -> f32 {
match damage_type {
DamageType::Physical => resistances.physical,
DamageType::Fire => resistances.fire,
DamageType::Ice => resistances.ice,
DamageType::Lightning => resistances.lightning,
DamageType::Poison => resistances.poison,
}.clamp(0.0, 0.9)
}
}
Combat AI Integration
Leverage the AI system for dynamic combat behavior:
#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
#[derive(Component)]
pub struct CombatAI {
pub aggression: f32,
pub patience: f32,
pub preferred_range: f32,
pub combo_tendency: f32,
}
pub fn ai_combat_decision_system(
mut ai_query: Query<(
Entity,
&CombatAI,
&CombatStats,
&CombatState,
&Transform,
&mut AiPlanner,
)>,
target_query: Query<(&CombatStats, &CombatState, &Transform), Without<CombatAI>>,
mut attack_events: EventWriter<AttackEvent>,
) {
for (entity, ai, stats, state, transform, mut planner) in ai_query.iter_mut() {
if state.stance != CombatStance::Neutral {
continue;
}
let Some(target) = find_nearest_target(transform, &target_query) else {
continue;
};
let (target_stats, target_state, target_transform) = target_query.get(target).unwrap();
let distance = transform.translation.distance(target_transform.translation);
let direction = (target_transform.translation - transform.translation).normalize();
let decision = evaluate_combat_options(
ai,
stats,
target_stats,
target_state,
distance,
);
match decision {
CombatDecision::Attack(attack_type) => {
attack_events.send(AttackEvent {
attacker: entity,
weapon: entity,
attack_type,
direction,
});
}
CombatDecision::Reposition(target_distance) => {
let goal_pos = target_transform.translation - direction * target_distance;
planner.set_goal(AiGoal::MoveTo { position: goal_pos });
}
CombatDecision::Defend => {
planner.set_goal(AiGoal::Custom("block".into()));
}
CombatDecision::Wait => {}
}
}
}
#[derive(Debug)]
enum CombatDecision {
Attack(AttackType),
Reposition(f32),
Defend,
Wait,
}
fn evaluate_combat_options(
ai: &CombatAI,
stats: &CombatStats,
target_stats: &CombatStats,
target_state: &CombatState,
distance: f32,
) -> CombatDecision {
let health_ratio = stats.health / stats.max_health;
let stamina_ratio = stats.stamina / stats.max_stamina;
let target_health_ratio = target_stats.health / target_stats.max_health;
if target_state.stance == CombatStance::Staggered && distance < 3.0 {
return CombatDecision::Attack(AttackType::Heavy);
}
if health_ratio < 0.3 && ai.patience > 0.5 {
return CombatDecision::Reposition(ai.preferred_range * 1.5);
}
if distance > ai.preferred_range * 1.2 {
return CombatDecision::Reposition(ai.preferred_range);
}
if target_state.stance == CombatStance::Attacking && ai.patience > 0.7 {
return CombatDecision::Defend;
}
if stamina_ratio > 0.3 && distance < ai.preferred_range {
let attack_type = if ai.aggression > 0.7 && stamina_ratio > 0.6 {
AttackType::Heavy
} else {
AttackType::Light
};
return CombatDecision::Attack(attack_type);
}
CombatDecision::Wait
}
}
Crafting System
Inventory and Items
Define the item and inventory structures:
#![allow(unused)]
fn main() {
#[derive(Component, Debug, Clone)]
pub struct Item {
pub id: ItemId,
pub name: String,
pub item_type: ItemType,
pub rarity: ItemRarity,
pub stack_size: u32,
pub max_stack: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ItemId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ItemType {
Material,
Consumable,
Weapon,
Armor,
Accessory,
Tool,
QuestItem,
}
#[derive(Debug, Clone, Copy, PartialEq, Ord, PartialOrd, Eq)]
pub enum ItemRarity {
Common,
Uncommon,
Rare,
Epic,
Legendary,
}
#[derive(Component, Debug)]
pub struct Inventory {
pub slots: Vec<Option<InventorySlot>>,
pub max_slots: usize,
pub weight_limit: f32,
pub current_weight: f32,
}
#[derive(Debug, Clone)]
pub struct InventorySlot {
pub item: Item,
pub quantity: u32,
}
impl Inventory {
pub fn new(max_slots: usize, weight_limit: f32) -> Self {
Self {
slots: vec![None; max_slots],
max_slots,
weight_limit,
current_weight: 0.0,
}
}
pub fn add_item(&mut self, item: Item, quantity: u32) -> Result<(), InventoryError> {
for slot in self.slots.iter_mut() {
if let Some(ref mut existing) = slot {
if existing.item.id == item.id && existing.quantity < item.max_stack {
let space = item.max_stack - existing.quantity;
let add = quantity.min(space);
existing.quantity += add;
if add == quantity {
return Ok(());
}
}
}
}
for slot in self.slots.iter_mut() {
if slot.is_none() {
*slot = Some(InventorySlot {
item: item.clone(),
quantity: quantity.min(item.max_stack),
});
return Ok(());
}
}
Err(InventoryError::Full)
}
pub fn remove_item(&mut self, item_id: ItemId, quantity: u32) -> Result<u32, InventoryError> {
let mut remaining = quantity;
for slot in self.slots.iter_mut() {
if remaining == 0 {
break;
}
if let Some(ref mut existing) = slot {
if existing.item.id == item_id {
let remove = remaining.min(existing.quantity);
existing.quantity -= remove;
remaining -= remove;
if existing.quantity == 0 {
*slot = None;
}
}
}
}
if remaining > 0 {
Err(InventoryError::InsufficientItems)
} else {
Ok(quantity)
}
}
pub fn count_item(&self, item_id: ItemId) -> u32 {
self.slots
.iter()
.filter_map(|s| s.as_ref())
.filter(|s| s.item.id == item_id)
.map(|s| s.quantity)
.sum()
}
}
#[derive(Debug)]
pub enum InventoryError {
Full,
InsufficientItems,
WeightExceeded,
}
}
Recipe System
Define recipes and crafting requirements:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct Recipe {
pub id: RecipeId,
pub name: String,
pub ingredients: Vec<RecipeIngredient>,
pub outputs: Vec<RecipeOutput>,
pub crafting_time: f32,
pub required_station: Option<CraftingStation>,
pub required_skill: Option<(SkillType, u32)>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RecipeId(pub u64);
#[derive(Debug, Clone)]
pub struct RecipeIngredient {
pub item_id: ItemId,
pub quantity: u32,
pub consumed: bool,
}
#[derive(Debug, Clone)]
pub struct RecipeOutput {
pub item_id: ItemId,
pub base_quantity: u32,
pub quality_scaling: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CraftingStation {
Workbench,
Forge,
Alchemy,
Enchanting,
Cooking,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SkillType {
Smithing,
Alchemy,
Enchanting,
Cooking,
Woodworking,
}
#[derive(Resource)]
pub struct RecipeRegistry {
recipes: HashMap<RecipeId, Recipe>,
by_station: HashMap<Option<CraftingStation>, Vec<RecipeId>>,
by_output: HashMap<ItemId, Vec<RecipeId>>,
}
impl RecipeRegistry {
pub fn new() -> Self {
Self {
recipes: HashMap::new(),
by_station: HashMap::new(),
by_output: HashMap::new(),
}
}
pub fn register(&mut self, recipe: Recipe) {
let id = recipe.id;
self.by_station
.entry(recipe.required_station)
.or_default()
.push(id);
for output in &recipe.outputs {
self.by_output
.entry(output.item_id)
.or_default()
.push(id);
}
self.recipes.insert(id, recipe);
}
pub fn get(&self, id: RecipeId) -> Option<&Recipe> {
self.recipes.get(&id)
}
pub fn available_at_station(&self, station: Option<CraftingStation>) -> &[RecipeId] {
self.by_station.get(&station).map(|v| v.as_slice()).unwrap_or(&[])
}
pub fn recipes_for_item(&self, item_id: ItemId) -> &[RecipeId] {
self.by_output.get(&item_id).map(|v| v.as_slice()).unwrap_or(&[])
}
}
}
Crafting Execution
Process crafting with quality calculations:
#![allow(unused)]
fn main() {
#[derive(Debug)]
pub struct CraftRequest {
pub crafter: Entity,
pub recipe_id: RecipeId,
pub station: Option<Entity>,
}
#[derive(Debug)]
pub struct CraftResult {
pub success: bool,
pub items: Vec<(ItemId, u32, ItemRarity)>,
pub experience: u32,
}
pub fn crafting_system(
mut craft_requests: EventReader<CraftRequest>,
mut craft_results: EventWriter<CraftResult>,
registry: Res<RecipeRegistry>,
mut crafter_query: Query<(&mut Inventory, Option<&CrafterSkills>)>,
station_query: Query<&CraftingStationComponent>,
item_registry: Res<ItemRegistry>,
) {
for request in craft_requests.iter() {
let Some(recipe) = registry.get(request.recipe_id) else {
continue;
};
let Ok((mut inventory, skills)) = crafter_query.get_mut(request.crafter) else {
continue;
};
if let Some(station_entity) = request.station {
if let Ok(station) = station_query.get(station_entity) {
if Some(station.station_type) != recipe.required_station {
continue;
}
}
} else if recipe.required_station.is_some() {
continue;
}
if !can_craft(&inventory, recipe) {
continue;
}
if let Some((skill_type, level)) = recipe.required_skill {
let crafter_level = skills
.map(|s| s.get_level(skill_type))
.unwrap_or(0);
if crafter_level < level {
continue;
}
}
for ingredient in &recipe.ingredients {
if ingredient.consumed {
let _ = inventory.remove_item(ingredient.item_id, ingredient.quantity);
}
}
let quality = calculate_craft_quality(skills, recipe);
let rarity = quality_to_rarity(quality);
let mut crafted_items = Vec::new();
for output in &recipe.outputs {
let quantity = if output.quality_scaling {
(output.base_quantity as f32 * (1.0 + quality * 0.5)) as u32
} else {
output.base_quantity
};
if let Some(base_item) = item_registry.get(output.item_id) {
let mut item = base_item.clone();
item.rarity = rarity;
let _ = inventory.add_item(item, quantity);
crafted_items.push((output.item_id, quantity, rarity));
}
}
craft_results.send(CraftResult {
success: true,
items: crafted_items,
experience: calculate_craft_xp(recipe, quality),
});
}
}
fn can_craft(inventory: &Inventory, recipe: &Recipe) -> bool {
recipe.ingredients.iter().all(|ing| {
inventory.count_item(ing.item_id) >= ing.quantity
})
}
fn calculate_craft_quality(skills: Option<&CrafterSkills>, recipe: &Recipe) -> f32 {
let base_quality = 0.5;
let skill_bonus = if let (Some(skills), Some((skill_type, required))) = (skills, recipe.required_skill) {
let level = skills.get_level(skill_type) as f32;
let excess = (level - required as f32).max(0.0);
excess * 0.05
} else {
0.0
};
let random_factor = rand::random::<f32>() * 0.2;
(base_quality + skill_bonus + random_factor).clamp(0.0, 1.0)
}
fn quality_to_rarity(quality: f32) -> ItemRarity {
match quality {
q if q >= 0.95 => ItemRarity::Legendary,
q if q >= 0.8 => ItemRarity::Epic,
q if q >= 0.6 => ItemRarity::Rare,
q if q >= 0.4 => ItemRarity::Uncommon,
_ => ItemRarity::Common,
}
}
}
AI-Assisted Crafting
Use AI to suggest recipes and optimize crafting:
#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;
pub struct CraftingAdvisor {
llm: LlmClient,
}
impl CraftingAdvisor {
pub async fn suggest_recipes(
&self,
inventory: &Inventory,
goal: &str,
registry: &RecipeRegistry,
) -> Vec<RecipeSuggestion> {
let available_items: Vec<_> = inventory.slots
.iter()
.filter_map(|s| s.as_ref())
.map(|s| format!("{} x{}", s.item.name, s.quantity))
.collect();
let prompt = format!(
r#"Given these materials: {}
Player goal: {}
Suggest the best crafting recipes to achieve this goal.
Return as JSON array of recipe suggestions."#,
available_items.join(", "),
goal
);
let response = self.llm.complete(&prompt).await;
parse_suggestions(&response, registry)
}
pub async fn optimize_craft_order(
&self,
target_item: ItemId,
inventory: &Inventory,
registry: &RecipeRegistry,
) -> Vec<RecipeId> {
let mut order = Vec::new();
let mut simulated_inventory = inventory.clone();
self.build_craft_tree(target_item, &mut simulated_inventory, registry, &mut order);
order
}
fn build_craft_tree(
&self,
target: ItemId,
inventory: &mut Inventory,
registry: &RecipeRegistry,
order: &mut Vec<RecipeId>,
) {
let recipes = registry.recipes_for_item(target);
for &recipe_id in recipes {
if let Some(recipe) = registry.get(recipe_id) {
let mut can_craft = true;
for ingredient in &recipe.ingredients {
let have = inventory.count_item(ingredient.item_id);
if have < ingredient.quantity {
self.build_craft_tree(
ingredient.item_id,
inventory,
registry,
order,
);
if inventory.count_item(ingredient.item_id) < ingredient.quantity {
can_craft = false;
break;
}
}
}
if can_craft {
order.push(recipe_id);
return;
}
}
}
}
}
#[derive(Debug)]
pub struct RecipeSuggestion {
pub recipe_id: RecipeId,
pub reason: String,
pub priority: u32,
}
}
Combat-Crafting Integration
Weapon Modification
Allow runtime weapon customization:
#![allow(unused)]
fn main() {
#[derive(Component, Debug)]
pub struct ModifiableWeapon {
pub base: WeaponStats,
pub modifications: Vec<WeaponMod>,
pub max_mods: usize,
}
#[derive(Debug, Clone)]
pub struct WeaponMod {
pub mod_type: WeaponModType,
pub value: f32,
pub item_source: ItemId,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WeaponModType {
DamageBonus,
ElementalDamage(DamageType),
AttackSpeed,
CriticalChance,
CriticalDamage,
LifeSteal,
PoiseDamage,
}
impl ModifiableWeapon {
pub fn calculate_stats(&self) -> WeaponStats {
let mut stats = self.base.clone();
for modification in &self.modifications {
match modification.mod_type {
WeaponModType::DamageBonus => {
stats.base_damage *= 1.0 + modification.value;
}
WeaponModType::AttackSpeed => {
stats.attack_speed *= 1.0 + modification.value;
}
WeaponModType::PoiseDamage => {
stats.poise_damage *= 1.0 + modification.value;
}
_ => {}
}
}
stats
}
pub fn add_mod(&mut self, modification: WeaponMod) -> Result<(), &'static str> {
if self.modifications.len() >= self.max_mods {
return Err("Maximum modifications reached");
}
let same_type_count = self.modifications
.iter()
.filter(|m| std::mem::discriminant(&m.mod_type) == std::mem::discriminant(&modification.mod_type))
.count();
if same_type_count >= 2 {
return Err("Cannot stack more than 2 of the same mod type");
}
self.modifications.push(modification);
Ok(())
}
}
pub fn apply_weapon_mods_system(
mut weapons: Query<(&ModifiableWeapon, &mut WeaponStats), Changed<ModifiableWeapon>>,
) {
for (modifiable, mut stats) in weapons.iter_mut() {
*stats = modifiable.calculate_stats();
}
}
}
Combat Loot System
Generate loot based on combat performance:
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct LootTable {
pub entries: Vec<LootEntry>,
pub guaranteed: Vec<ItemId>,
}
#[derive(Debug, Clone)]
pub struct LootEntry {
pub item_id: ItemId,
pub weight: f32,
pub min_quantity: u32,
pub max_quantity: u32,
pub min_rarity: ItemRarity,
}
pub fn combat_loot_system(
mut death_events: EventReader<EntityDeathEvent>,
loot_query: Query<&LootTable>,
combat_log: Res<CombatLog>,
mut loot_events: EventWriter<LootDropEvent>,
) {
for event in death_events.iter() {
let Ok(loot_table) = loot_query.get(event.entity) else {
continue;
};
let performance = combat_log.get_performance(event.killer, event.entity);
let luck_bonus = calculate_luck_bonus(&performance);
let mut drops = Vec::new();
for item_id in &loot_table.guaranteed {
drops.push((*item_id, 1, ItemRarity::Common));
}
let roll_count = 1 + (luck_bonus * 2.0) as usize;
for _ in 0..roll_count {
if let Some(entry) = roll_loot(loot_table, luck_bonus) {
let quantity = rand::thread_rng()
.gen_range(entry.min_quantity..=entry.max_quantity);
let rarity = roll_rarity(entry.min_rarity, luck_bonus);
drops.push((entry.item_id, quantity, rarity));
}
}
loot_events.send(LootDropEvent {
position: event.position,
drops,
});
}
}
fn calculate_luck_bonus(performance: &CombatPerformance) -> f32 {
let mut bonus = 0.0;
if performance.no_damage_taken {
bonus += 0.3;
}
if performance.time_to_kill < 30.0 {
bonus += 0.2;
}
if performance.parries > 0 {
bonus += performance.parries as f32 * 0.05;
}
bonus.min(1.0)
}
}
Best Practices
1. **Balance Stamina Economy**: Ensure attacks have meaningful stamina costs to create decision points
2. **Poise Windows**: Use stagger states to create openings for both players and AI
3. **Readable Attacks**: Give clear visual tells before powerful attacks
4. **Combo Depth**: Reward skill with combo multipliers but keep basic attacks viable
- **Damage Sponges**: Avoid enemies that just have more health; vary resistances and mechanics
- **Inventory Overflow**: Always handle full inventory gracefully
- **Recipe Bloat**: Curate recipes to avoid overwhelming players
- **Mod Stacking**: Prevent multiplicative mod abuse
Related Documentation
- AI Companions - Combat companion behaviors
- Adaptive Bosses - Advanced boss combat AI
- Physics Integration - Combat physics
- Configuration Reference - Combat tuning parameters
Dialogue Systems
AstraWeave’s dialogue system combines traditional branching dialogue with LLM-powered dynamic conversations, enabling NPCs to engage in contextual, personality-driven interactions while maintaining narrative coherence.
Architecture Overview
graph TB
subgraph Input["Player Input"]
PI[Player Choice] --> DM
TI[Text Input] --> NLP[NLP Parser]
NLP --> DM
end
subgraph DialogueManager["Dialogue Manager"]
DM[Dialogue Controller] --> SC[Script Check]
SC -->|Scripted| SN[Script Node]
SC -->|Dynamic| LLM[LLM Handler]
SN --> VAL[Validator]
LLM --> VAL
VAL --> OUT[Output]
end
subgraph Context["Context System"]
MEM[Memory] --> DM
REL[Relationships] --> DM
QS[Quest State] --> DM
WS[World State] --> DM
end
OUT --> UI[Dialogue UI]
OUT --> AUD[Voice/Audio]
OUT --> EVT[Event Triggers]
Core Components
Dialogue Node System
Define structured dialogue with branching paths:
#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DialogueTree {
pub id: DialogueId,
pub nodes: HashMap<NodeId, DialogueNode>,
pub entry_node: NodeId,
pub metadata: DialogueMetadata,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DialogueId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct NodeId(pub u32);
#[derive(Debug, Clone)]
pub struct DialogueNode {
pub id: NodeId,
pub speaker: SpeakerId,
pub content: DialogueContent,
pub responses: Vec<DialogueResponse>,
pub conditions: Vec<DialogueCondition>,
pub effects: Vec<DialogueEffect>,
}
#[derive(Debug, Clone)]
pub enum DialogueContent {
Text(String),
Localized { key: String, fallback: String },
Dynamic { prompt_template: String },
Voice { text: String, audio_id: String },
}
#[derive(Debug, Clone)]
pub struct DialogueResponse {
pub text: String,
pub next_node: Option<NodeId>,
pub conditions: Vec<DialogueCondition>,
pub effects: Vec<DialogueEffect>,
pub tone: ResponseTone,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResponseTone {
Neutral,
Friendly,
Hostile,
Curious,
Sarcastic,
Romantic,
}
#[derive(Debug, Clone)]
pub enum DialogueCondition {
QuestState { quest_id: u64, state: String },
RelationshipMin { npc_id: u64, value: i32 },
HasItem { item_id: u64, quantity: u32 },
PlayerStat { stat: String, min: i32 },
Flag { name: String, value: bool },
TimeOfDay { start: u32, end: u32 },
Custom { script: String },
}
#[derive(Debug, Clone)]
pub enum DialogueEffect {
SetFlag { name: String, value: bool },
ModifyRelationship { npc_id: u64, delta: i32 },
GiveItem { item_id: u64, quantity: u32 },
TakeItem { item_id: u64, quantity: u32 },
StartQuest { quest_id: u64 },
AdvanceQuest { quest_id: u64, stage: String },
TriggerEvent { event_name: String },
PlayAnimation { animation: String },
}
}
Dialogue Controller
Manage dialogue state and flow:
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct DialogueController {
pub active_dialogue: Option<ActiveDialogue>,
pub history: Vec<DialogueHistoryEntry>,
pub context: DialogueContext,
}
#[derive(Debug)]
pub struct ActiveDialogue {
pub tree: DialogueTree,
pub current_node: NodeId,
pub participants: Vec<Entity>,
pub started_at: f64,
}
#[derive(Debug, Clone)]
pub struct DialogueHistoryEntry {
pub node_id: NodeId,
pub speaker: SpeakerId,
pub text: String,
pub timestamp: f64,
pub response_chosen: Option<usize>,
}
#[derive(Debug, Default)]
pub struct DialogueContext {
pub player_name: String,
pub location: String,
pub time_of_day: String,
pub recent_events: Vec<String>,
pub custom_vars: HashMap<String, String>,
}
impl DialogueController {
pub fn start_dialogue(&mut self, tree: DialogueTree, participants: Vec<Entity>, time: f64) {
self.active_dialogue = Some(ActiveDialogue {
current_node: tree.entry_node,
tree,
participants,
started_at: time,
});
}
pub fn get_current_node(&self) -> Option<&DialogueNode> {
self.active_dialogue.as_ref().and_then(|active| {
active.tree.nodes.get(&active.current_node)
})
}
pub fn select_response(&mut self, index: usize, time: f64) -> Option<Vec<DialogueEffect>> {
let active = self.active_dialogue.as_mut()?;
let node = active.tree.nodes.get(&active.current_node)?;
let response = node.responses.get(index)?;
self.history.push(DialogueHistoryEntry {
node_id: active.current_node,
speaker: node.speaker,
text: match &node.content {
DialogueContent::Text(t) => t.clone(),
DialogueContent::Localized { fallback, .. } => fallback.clone(),
DialogueContent::Dynamic { .. } => "[Dynamic]".into(),
DialogueContent::Voice { text, .. } => text.clone(),
},
timestamp: time,
response_chosen: Some(index),
});
if let Some(next) = response.next_node {
active.current_node = next;
Some(response.effects.clone())
} else {
let effects = response.effects.clone();
self.active_dialogue = None;
Some(effects)
}
}
pub fn end_dialogue(&mut self) {
self.active_dialogue = None;
}
}
}
Dialogue System
Process dialogue logic each frame:
#![allow(unused)]
fn main() {
pub fn dialogue_system(
mut dialogue_query: Query<&mut DialogueController>,
mut effect_events: EventWriter<DialogueEffectEvent>,
conditions: Res<ConditionEvaluator>,
time: Res<Time>,
) {
for mut controller in dialogue_query.iter_mut() {
let Some(active) = &controller.active_dialogue else {
continue;
};
let Some(node) = active.tree.nodes.get(&active.current_node) else {
controller.end_dialogue();
continue;
};
if !conditions.evaluate_all(&node.conditions) {
if let Some(fallback) = find_fallback_node(&active.tree, &conditions) {
if let Some(active) = controller.active_dialogue.as_mut() {
active.current_node = fallback;
}
} else {
controller.end_dialogue();
}
continue;
}
for effect in &node.effects {
effect_events.send(DialogueEffectEvent {
effect: effect.clone(),
timestamp: time.elapsed_seconds_f64(),
});
}
}
}
pub fn dialogue_effect_system(
mut effect_events: EventReader<DialogueEffectEvent>,
mut quest_events: EventWriter<QuestEvent>,
mut inventory_events: EventWriter<InventoryEvent>,
mut relationship_events: EventWriter<RelationshipEvent>,
mut flags: ResMut<GameFlags>,
) {
for event in effect_events.iter() {
match &event.effect {
DialogueEffect::SetFlag { name, value } => {
flags.set(name.clone(), *value);
}
DialogueEffect::ModifyRelationship { npc_id, delta } => {
relationship_events.send(RelationshipEvent::Modify {
npc_id: *npc_id,
delta: *delta,
});
}
DialogueEffect::GiveItem { item_id, quantity } => {
inventory_events.send(InventoryEvent::Add {
item_id: *item_id,
quantity: *quantity,
});
}
DialogueEffect::StartQuest { quest_id } => {
quest_events.send(QuestEvent::Start { quest_id: *quest_id });
}
DialogueEffect::AdvanceQuest { quest_id, stage } => {
quest_events.send(QuestEvent::Advance {
quest_id: *quest_id,
stage: stage.clone(),
});
}
_ => {}
}
}
}
}
LLM-Powered Dialogue
Dynamic Conversation Handler
Enable free-form AI conversations:
#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;
#[derive(Component)]
pub struct DynamicDialogue {
pub npc_persona: NpcPersona,
pub conversation_history: Vec<ConversationTurn>,
pub memory_context: Vec<String>,
pub allowed_topics: Vec<String>,
pub forbidden_topics: Vec<String>,
pub max_history: usize,
}
#[derive(Debug, Clone)]
pub struct NpcPersona {
pub name: String,
pub personality: String,
pub background: String,
pub speaking_style: String,
pub knowledge: Vec<String>,
pub goals: Vec<String>,
pub relationships: HashMap<String, String>,
}
#[derive(Debug, Clone)]
pub struct ConversationTurn {
pub role: ConversationRole,
pub content: String,
pub timestamp: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ConversationRole {
Player,
Npc,
System,
}
impl DynamicDialogue {
pub fn new(persona: NpcPersona) -> Self {
Self {
npc_persona: persona,
conversation_history: Vec::new(),
memory_context: Vec::new(),
allowed_topics: Vec::new(),
forbidden_topics: Vec::new(),
max_history: 20,
}
}
pub fn add_turn(&mut self, role: ConversationRole, content: String, time: f64) {
self.conversation_history.push(ConversationTurn {
role,
content,
timestamp: time,
});
if self.conversation_history.len() > self.max_history {
self.conversation_history.remove(0);
}
}
pub fn build_prompt(&self, player_input: &str, context: &DialogueContext) -> String {
let mut prompt = format!(
r#"You are {}, an NPC in a fantasy game.
Personality: {}
Background: {}
Speaking style: {}
Current situation:
- Location: {}
- Time: {}
- Recent events: {}
Knowledge you have:
{}
Your goals:
{}
"#,
self.npc_persona.name,
self.npc_persona.personality,
self.npc_persona.background,
self.npc_persona.speaking_style,
context.location,
context.time_of_day,
context.recent_events.join(", "),
self.npc_persona.knowledge.join("\n- "),
self.npc_persona.goals.join("\n- "),
);
if !self.memory_context.is_empty() {
prompt.push_str("Relevant memories:\n");
for memory in &self.memory_context {
prompt.push_str(&format!("- {}\n", memory));
}
prompt.push('\n');
}
prompt.push_str("Conversation so far:\n");
for turn in &self.conversation_history {
let speaker = match turn.role {
ConversationRole::Player => &context.player_name,
ConversationRole::Npc => &self.npc_persona.name,
ConversationRole::System => "[System]",
};
prompt.push_str(&format!("{}: {}\n", speaker, turn.content));
}
prompt.push_str(&format!("\n{}: {}\n", context.player_name, player_input));
prompt.push_str(&format!("\nRespond as {}. Stay in character. Keep response under 100 words.",
self.npc_persona.name));
prompt
}
}
}
LLM Dialogue System
Process dynamic dialogue requests:
#![allow(unused)]
fn main() {
pub struct LlmDialogueSystem {
llm: LlmClient,
validator: DialogueValidator,
}
impl LlmDialogueSystem {
pub async fn generate_response(
&self,
dialogue: &mut DynamicDialogue,
player_input: &str,
context: &DialogueContext,
) -> Result<String, DialogueError> {
dialogue.add_turn(
ConversationRole::Player,
player_input.to_string(),
0.0
);
let prompt = dialogue.build_prompt(player_input, context);
let response = self.llm.complete(&prompt).await
.map_err(|e| DialogueError::LlmError(e.to_string()))?;
let validated = self.validator.validate_response(
&response,
&dialogue.forbidden_topics,
&dialogue.npc_persona,
)?;
dialogue.add_turn(ConversationRole::Npc, validated.clone(), 0.0);
Ok(validated)
}
}
pub struct DialogueValidator {
blocked_patterns: Vec<regex::Regex>,
}
impl DialogueValidator {
pub fn validate_response(
&self,
response: &str,
forbidden_topics: &[String],
persona: &NpcPersona,
) -> Result<String, DialogueError> {
if response.len() > 500 {
return Err(DialogueError::ResponseTooLong);
}
for pattern in &self.blocked_patterns {
if pattern.is_match(response) {
return Err(DialogueError::BlockedContent);
}
}
for topic in forbidden_topics {
if response.to_lowercase().contains(&topic.to_lowercase()) {
return Err(DialogueError::ForbiddenTopic(topic.clone()));
}
}
Ok(response.trim().to_string())
}
}
#[derive(Debug)]
pub enum DialogueError {
LlmError(String),
ResponseTooLong,
BlockedContent,
ForbiddenTopic(String),
ValidationFailed,
}
}
Tool-Based Dialogue Actions
Allow NPCs to take actions during conversation:
#![allow(unused)]
fn main() {
use astraweave_ai::tools::*;
pub fn create_dialogue_tools() -> Vec<AiTool> {
vec![
AiTool {
name: "give_item".into(),
description: "Give an item to the player as a gift or quest reward".into(),
parameters: json!({
"type": "object",
"properties": {
"item_name": { "type": "string" },
"quantity": { "type": "integer", "minimum": 1 },
"reason": { "type": "string" }
},
"required": ["item_name", "quantity"]
}),
handler: Box::new(|params| {
let item = params["item_name"].as_str().unwrap();
let qty = params["quantity"].as_i64().unwrap() as u32;
ToolResult::Success(json!({
"action": "give_item",
"item": item,
"quantity": qty
}))
}),
},
AiTool {
name: "share_rumor".into(),
description: "Share a rumor or piece of information with the player".into(),
parameters: json!({
"type": "object",
"properties": {
"rumor_id": { "type": "string" },
"importance": { "type": "string", "enum": ["minor", "major", "critical"] }
},
"required": ["rumor_id"]
}),
handler: Box::new(|params| {
let rumor = params["rumor_id"].as_str().unwrap();
ToolResult::Success(json!({
"action": "share_rumor",
"rumor": rumor,
"unlocked": true
}))
}),
},
AiTool {
name: "offer_quest".into(),
description: "Offer a quest to the player".into(),
parameters: json!({
"type": "object",
"properties": {
"quest_id": { "type": "string" },
"urgency": { "type": "string", "enum": ["low", "medium", "high"] }
},
"required": ["quest_id"]
}),
handler: Box::new(|params| {
let quest = params["quest_id"].as_str().unwrap();
ToolResult::Success(json!({
"action": "offer_quest",
"quest": quest
}))
}),
},
AiTool {
name: "express_emotion".into(),
description: "Show an emotional reaction through animation".into(),
parameters: json!({
"type": "object",
"properties": {
"emotion": {
"type": "string",
"enum": ["happy", "sad", "angry", "surprised", "fearful", "disgusted"]
},
"intensity": { "type": "number", "minimum": 0, "maximum": 1 }
},
"required": ["emotion"]
}),
handler: Box::new(|params| {
let emotion = params["emotion"].as_str().unwrap();
let intensity = params.get("intensity")
.and_then(|v| v.as_f64())
.unwrap_or(0.5);
ToolResult::Success(json!({
"action": "express_emotion",
"emotion": emotion,
"intensity": intensity
}))
}),
},
]
}
}
Relationship System
Relationship Tracking
Track player-NPC relationships:
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct RelationshipTracker {
pub relationships: HashMap<Entity, Relationship>,
}
#[derive(Debug, Clone)]
pub struct Relationship {
pub affinity: i32,
pub trust: i32,
pub fear: i32,
pub respect: i32,
pub romantic_interest: i32,
pub interaction_count: u32,
pub last_interaction: f64,
pub memories: Vec<RelationshipMemory>,
}
#[derive(Debug, Clone)]
pub struct RelationshipMemory {
pub event: String,
pub impact: i32,
pub timestamp: f64,
}
impl Relationship {
pub fn new() -> Self {
Self {
affinity: 0,
trust: 0,
fear: 0,
respect: 0,
romantic_interest: 0,
interaction_count: 0,
last_interaction: 0.0,
memories: Vec::new(),
}
}
pub fn get_disposition(&self) -> Disposition {
let total = self.affinity + self.trust + self.respect - self.fear;
match total {
t if t >= 50 => Disposition::Friendly,
t if t >= 20 => Disposition::Warm,
t if t >= -20 => Disposition::Neutral,
t if t >= -50 => Disposition::Cool,
_ => Disposition::Hostile,
}
}
pub fn modify(&mut self, event: &str, changes: RelationshipChanges, time: f64) {
self.affinity = (self.affinity + changes.affinity).clamp(-100, 100);
self.trust = (self.trust + changes.trust).clamp(-100, 100);
self.fear = (self.fear + changes.fear).clamp(0, 100);
self.respect = (self.respect + changes.respect).clamp(-100, 100);
self.romantic_interest = (self.romantic_interest + changes.romantic).clamp(0, 100);
self.interaction_count += 1;
self.last_interaction = time;
let impact = changes.affinity + changes.trust + changes.respect;
if impact.abs() >= 5 {
self.memories.push(RelationshipMemory {
event: event.to_string(),
impact,
timestamp: time,
});
if self.memories.len() > 20 {
self.memories.sort_by(|a, b| b.impact.abs().cmp(&a.impact.abs()));
self.memories.truncate(10);
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Disposition {
Hostile,
Cool,
Neutral,
Warm,
Friendly,
}
#[derive(Debug, Clone, Default)]
pub struct RelationshipChanges {
pub affinity: i32,
pub trust: i32,
pub fear: i32,
pub respect: i32,
pub romantic: i32,
}
}
Relationship-Aware Dialogue
Modify dialogue based on relationship:
#![allow(unused)]
fn main() {
pub fn relationship_dialogue_modifier(
dialogue: &mut DynamicDialogue,
relationship: &Relationship,
) {
let disposition = relationship.get_disposition();
let style_modifier = match disposition {
Disposition::Hostile => "Be cold and dismissive. Keep responses short. Show distrust.",
Disposition::Cool => "Be polite but distant. Don't volunteer extra information.",
Disposition::Neutral => "Be professional and helpful when asked.",
Disposition::Warm => "Be friendly and open. Share more freely.",
Disposition::Friendly => "Be warm and trusting. Treat them as a close friend.",
};
dialogue.npc_persona.speaking_style.push_str(&format!("\n{}", style_modifier));
for memory in &relationship.memories {
dialogue.memory_context.push(format!(
"{} (impact: {:+})",
memory.event,
memory.impact
));
}
}
}
Voice Integration
Text-to-Speech Support
Generate voice output for dialogue:
#![allow(unused)]
fn main() {
use astraweave_audio::prelude::*;
#[derive(Component)]
pub struct VoiceDialogue {
pub voice_id: String,
pub pitch: f32,
pub speed: f32,
pub volume: f32,
}
pub struct VoiceSynthesizer {
tts_endpoint: String,
}
impl VoiceSynthesizer {
pub async fn synthesize(
&self,
text: &str,
voice: &VoiceDialogue,
) -> Result<AudioBuffer, VoiceError> {
let request = VoiceSynthRequest {
text: text.to_string(),
voice_id: voice.voice_id.clone(),
pitch: voice.pitch,
speed: voice.speed,
};
let response = reqwest::Client::new()
.post(&self.tts_endpoint)
.json(&request)
.send()
.await?
.bytes()
.await?;
Ok(AudioBuffer::from_bytes(&response))
}
}
pub fn dialogue_voice_system(
dialogue_query: Query<(&DialogueController, &VoiceDialogue)>,
synthesizer: Res<VoiceSynthesizer>,
mut audio_events: EventWriter<PlayAudioEvent>,
runtime: Res<TokioRuntime>,
) {
for (controller, voice) in dialogue_query.iter() {
if let Some(node) = controller.get_current_node() {
if let DialogueContent::Voice { text, audio_id } = &node.content {
if let Some(cached) = try_get_cached_audio(audio_id) {
audio_events.send(PlayAudioEvent {
buffer: cached,
volume: voice.volume,
spatial: false,
});
} else {
let text = text.clone();
let voice = voice.clone();
runtime.spawn(async move {
if let Ok(buffer) = synthesizer.synthesize(&text, &voice).await {
cache_audio(audio_id, buffer);
}
});
}
}
}
}
}
}
Dialogue UI
Dialogue Display Component
#![allow(unused)]
fn main() {
use astraweave_ui::prelude::*;
#[derive(Component)]
pub struct DialogueUI {
pub visible: bool,
pub speaker_name: String,
pub current_text: String,
pub responses: Vec<String>,
pub selected_response: usize,
pub text_progress: f32,
pub chars_per_second: f32,
}
impl DialogueUI {
pub fn show(&mut self, speaker: &str, text: &str, responses: Vec<String>) {
self.visible = true;
self.speaker_name = speaker.to_string();
self.current_text = text.to_string();
self.responses = responses;
self.selected_response = 0;
self.text_progress = 0.0;
}
pub fn get_visible_text(&self) -> &str {
let char_count = (self.current_text.len() as f32 * self.text_progress) as usize;
&self.current_text[..char_count.min(self.current_text.len())]
}
pub fn is_text_complete(&self) -> bool {
self.text_progress >= 1.0
}
pub fn skip_to_end(&mut self) {
self.text_progress = 1.0;
}
}
pub fn dialogue_ui_system(
mut ui_query: Query<&mut DialogueUI>,
dialogue_query: Query<&DialogueController>,
input: Res<Input>,
time: Res<Time>,
npc_query: Query<&NpcInfo>,
) {
for (mut ui, controller) in ui_query.iter_mut().zip(dialogue_query.iter()) {
if let Some(node) = controller.get_current_node() {
let speaker_name = npc_query
.get(node.speaker.0)
.map(|n| n.name.clone())
.unwrap_or_else(|_| "Unknown".to_string());
let text = match &node.content {
DialogueContent::Text(t) => t.clone(),
DialogueContent::Localized { fallback, .. } => fallback.clone(),
_ => String::new(),
};
let responses: Vec<String> = node.responses
.iter()
.map(|r| r.text.clone())
.collect();
if !ui.visible || ui.current_text != text {
ui.show(&speaker_name, &text, responses);
}
if !ui.is_text_complete() {
ui.text_progress += time.delta_seconds() * ui.chars_per_second
/ ui.current_text.len() as f32;
ui.text_progress = ui.text_progress.min(1.0);
}
if input.just_pressed(KeyCode::Space) || input.just_pressed(MouseButton::Left) {
if !ui.is_text_complete() {
ui.skip_to_end();
}
}
if ui.is_text_complete() {
if input.just_pressed(KeyCode::Up) {
ui.selected_response = ui.selected_response.saturating_sub(1);
}
if input.just_pressed(KeyCode::Down) {
ui.selected_response = (ui.selected_response + 1).min(ui.responses.len() - 1);
}
}
} else {
ui.visible = false;
}
}
}
}
Best Practices
1. **Blend Scripted and Dynamic**: Use scripted dialogue for critical story beats, dynamic for flavor
2. **Maintain Consistency**: Ensure LLM responses align with established NPC personality
3. **Respect Player Time**: Keep responses concise; players can ask for details
4. **Branch Meaningfully**: Only offer choices that have real consequences
- **Context Window Overflow**: Trim conversation history to prevent LLM context issues
- **Hallucination**: Validate LLM outputs against game state to prevent impossible claims
- **Tone Whiplash**: Ensure relationship state affects dialogue tone gradually
- **Dead Ends**: Always provide a way to exit or continue dialogue
Related Documentation
- AI Companions - Companion dialogue integration
- AI System Overview - LLM configuration
- Quest Systems - Dialogue-quest integration
- Audio Systems - Voice synthesis setup
Procedural Content Generation
AstraWeave’s PCG system combines traditional algorithmic generation with AI-driven content creation, enabling infinite variety while maintaining coherent, designer-guided output.
Architecture Overview
graph TB
subgraph Input["Generation Input"]
SEED[Seed/Parameters] --> GEN
RULES[Design Rules] --> GEN
CONTEXT[World Context] --> GEN
end
subgraph Generator["PCG Pipeline"]
GEN[Generator Core] --> ALGO[Algorithmic Pass]
ALGO --> AI[AI Enhancement]
AI --> VAL[Validation]
VAL --> POST[Post-Processing]
end
subgraph Output["Generated Content"]
POST --> TERRAIN[Terrain]
POST --> DUNGEONS[Dungeons]
POST --> ITEMS[Items]
POST --> QUESTS[Quests]
POST --> NPCS[NPCs]
end
VAL -->|Reject| GEN
Core PCG Framework
Generator Trait
Define the common interface for all generators:
#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use std::hash::{Hash, Hasher};
pub trait Generator<T> {
fn generate(&self, params: &GenerationParams) -> GenerationResult<T>;
fn validate(&self, output: &T, params: &GenerationParams) -> ValidationResult;
fn seed_from_hash<H: Hash>(&self, hashable: H) -> u64;
}
#[derive(Debug, Clone)]
pub struct GenerationParams {
pub seed: u64,
pub difficulty: f32,
pub density: f32,
pub theme: String,
pub constraints: Vec<GenerationConstraint>,
pub context: GenerationContext,
}
#[derive(Debug, Clone)]
pub struct GenerationContext {
pub world_position: Vec3,
pub biome: String,
pub player_level: u32,
pub story_stage: String,
pub nearby_content: Vec<String>,
}
#[derive(Debug, Clone)]
pub enum GenerationConstraint {
MinSize(f32),
MaxSize(f32),
MustContain(String),
MustNotContain(String),
ConnectTo(Vec3),
StyleMatch(String),
Custom { name: String, value: String },
}
pub type GenerationResult<T> = Result<T, GenerationError>;
#[derive(Debug)]
pub enum GenerationError {
InvalidParams(String),
ConstraintViolation(String),
MaxIterationsReached,
ValidationFailed(String),
}
#[derive(Debug)]
pub enum ValidationResult {
Valid,
Invalid(String),
NeedsAdjustment(Vec<String>),
}
}
Seeded Random Generator
Deterministic random number generation:
#![allow(unused)]
fn main() {
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
pub struct SeededRng {
rng: ChaCha8Rng,
initial_seed: u64,
}
impl SeededRng {
pub fn new(seed: u64) -> Self {
Self {
rng: ChaCha8Rng::seed_from_u64(seed),
initial_seed: seed,
}
}
pub fn from_position(x: i32, y: i32, z: i32, world_seed: u64) -> Self {
let position_hash = ((x as u64) << 42) ^ ((y as u64) << 21) ^ (z as u64);
Self::new(world_seed ^ position_hash)
}
pub fn next_float(&mut self) -> f32 {
self.rng.gen()
}
pub fn next_range(&mut self, min: i32, max: i32) -> i32 {
self.rng.gen_range(min..=max)
}
pub fn next_float_range(&mut self, min: f32, max: f32) -> f32 {
self.rng.gen_range(min..=max)
}
pub fn choose<T: Clone>(&mut self, options: &[T]) -> Option<T> {
if options.is_empty() {
None
} else {
let index = self.rng.gen_range(0..options.len());
Some(options[index].clone())
}
}
pub fn weighted_choose<T: Clone>(&mut self, options: &[(T, f32)]) -> Option<T> {
let total_weight: f32 = options.iter().map(|(_, w)| w).sum();
let mut roll = self.next_float() * total_weight;
for (item, weight) in options {
roll -= weight;
if roll <= 0.0 {
return Some(item.clone());
}
}
options.last().map(|(item, _)| item.clone())
}
pub fn fork(&mut self) -> Self {
Self::new(self.rng.gen())
}
}
}
Terrain Generation
Heightmap Generator
Generate terrain heightmaps with noise:
#![allow(unused)]
fn main() {
use noise::{NoiseFn, Perlin, Fbm, MultiFractal};
pub struct TerrainGenerator {
base_noise: Fbm<Perlin>,
detail_noise: Perlin,
erosion_passes: u32,
}
impl TerrainGenerator {
pub fn new(seed: u64) -> Self {
let mut fbm = Fbm::new(seed as u32);
fbm.octaves = 6;
fbm.frequency = 0.005;
fbm.lacunarity = 2.0;
fbm.persistence = 0.5;
Self {
base_noise: fbm,
detail_noise: Perlin::new(seed as u32 + 1),
erosion_passes: 50,
}
}
pub fn generate_chunk(&self, chunk_x: i32, chunk_z: i32, size: usize) -> Heightmap {
let mut heights = vec![vec![0.0f32; size]; size];
for z in 0..size {
for x in 0..size {
let world_x = (chunk_x * size as i32 + x as i32) as f64;
let world_z = (chunk_z * size as i32 + z as i32) as f64;
let base = self.base_noise.get([world_x, world_z]) as f32;
let detail = self.detail_noise.get([world_x * 0.1, world_z * 0.1]) as f32 * 0.1;
heights[z][x] = (base + detail + 1.0) * 0.5 * 256.0;
}
}
self.apply_erosion(&mut heights);
Heightmap {
data: heights,
chunk_x,
chunk_z,
size,
}
}
fn apply_erosion(&self, heights: &mut Vec<Vec<f32>>) {
let size = heights.len();
let mut water = vec![vec![0.0f32; size]; size];
let mut sediment = vec![vec![0.0f32; size]; size];
for _ in 0..self.erosion_passes {
for z in 1..size - 1 {
for x in 1..size - 1 {
let current = heights[z][x];
let neighbors = [
(heights[z - 1][x], 0, -1),
(heights[z + 1][x], 0, 1),
(heights[z][x - 1], -1, 0),
(heights[z][x + 1], 1, 0),
];
if let Some((lowest, dx, dz)) = neighbors
.iter()
.filter(|(h, _, _)| *h < current)
.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap())
{
let diff = current - lowest;
let transfer = diff * 0.1;
heights[z][x] -= transfer * 0.5;
heights[(z as i32 + dz) as usize][(x as i32 + dx) as usize] += transfer * 0.3;
}
}
}
}
}
}
#[derive(Debug)]
pub struct Heightmap {
pub data: Vec<Vec<f32>>,
pub chunk_x: i32,
pub chunk_z: i32,
pub size: usize,
}
impl Heightmap {
pub fn get_height(&self, x: usize, z: usize) -> f32 {
self.data.get(z).and_then(|row| row.get(x)).copied().unwrap_or(0.0)
}
pub fn get_normal(&self, x: usize, z: usize) -> Vec3 {
let h_l = self.get_height(x.saturating_sub(1), z);
let h_r = self.get_height((x + 1).min(self.size - 1), z);
let h_d = self.get_height(x, z.saturating_sub(1));
let h_u = self.get_height(x, (z + 1).min(self.size - 1));
Vec3::new(h_l - h_r, 2.0, h_d - h_u).normalize()
}
}
}
Biome Assignment
Assign biomes based on terrain properties:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Biome {
Ocean,
Beach,
Plains,
Forest,
Desert,
Tundra,
Mountains,
Swamp,
Jungle,
Volcanic,
}
pub struct BiomeGenerator {
temperature_noise: Perlin,
moisture_noise: Perlin,
}
impl BiomeGenerator {
pub fn new(seed: u64) -> Self {
Self {
temperature_noise: Perlin::new(seed as u32 + 100),
moisture_noise: Perlin::new(seed as u32 + 200),
}
}
pub fn get_biome(&self, x: f64, z: f64, height: f32) -> Biome {
if height < 10.0 {
return Biome::Ocean;
}
if height < 15.0 {
return Biome::Beach;
}
if height > 200.0 {
return Biome::Mountains;
}
let temp = (self.temperature_noise.get([x * 0.001, z * 0.001]) + 1.0) * 0.5;
let moisture = (self.moisture_noise.get([x * 0.001, z * 0.001]) + 1.0) * 0.5;
let temp = temp as f32 - (height - 50.0) * 0.002;
match (temp, moisture as f32) {
(t, _) if t < 0.2 => Biome::Tundra,
(t, m) if t > 0.8 && m < 0.3 => Biome::Desert,
(t, m) if t > 0.7 && m > 0.7 => Biome::Jungle,
(_, m) if m > 0.8 => Biome::Swamp,
(_, m) if m > 0.5 => Biome::Forest,
_ => Biome::Plains,
}
}
pub fn get_biome_properties(&self, biome: Biome) -> BiomeProperties {
match biome {
Biome::Ocean => BiomeProperties {
tree_density: 0.0,
grass_density: 0.0,
rock_density: 0.1,
enemy_level_mod: 0.8,
ambient_sound: "ocean".into(),
},
Biome::Forest => BiomeProperties {
tree_density: 0.7,
grass_density: 0.8,
rock_density: 0.2,
enemy_level_mod: 1.0,
ambient_sound: "forest".into(),
},
Biome::Desert => BiomeProperties {
tree_density: 0.02,
grass_density: 0.05,
rock_density: 0.4,
enemy_level_mod: 1.2,
ambient_sound: "desert".into(),
},
Biome::Mountains => BiomeProperties {
tree_density: 0.1,
grass_density: 0.2,
rock_density: 0.8,
enemy_level_mod: 1.5,
ambient_sound: "wind".into(),
},
_ => BiomeProperties::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct BiomeProperties {
pub tree_density: f32,
pub grass_density: f32,
pub rock_density: f32,
pub enemy_level_mod: f32,
pub ambient_sound: String,
}
}
Dungeon Generation
Room-Based Dungeon Generator
Generate dungeons using room placement:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct DungeonLayout {
pub rooms: Vec<Room>,
pub corridors: Vec<Corridor>,
pub entry_room: usize,
pub boss_room: usize,
pub width: i32,
pub height: i32,
}
#[derive(Debug, Clone)]
pub struct Room {
pub id: usize,
pub bounds: Rect,
pub room_type: RoomType,
pub connections: Vec<usize>,
pub content: RoomContent,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RoomType {
Entry,
Normal,
Treasure,
Boss,
Secret,
Shop,
Shrine,
}
#[derive(Debug, Clone)]
pub struct RoomContent {
pub enemies: Vec<EnemySpawn>,
pub items: Vec<ItemSpawn>,
pub props: Vec<PropSpawn>,
pub triggers: Vec<TriggerSpawn>,
}
#[derive(Debug, Clone)]
pub struct Corridor {
pub from_room: usize,
pub to_room: usize,
pub path: Vec<(i32, i32)>,
pub width: i32,
}
pub struct DungeonGenerator {
min_rooms: usize,
max_rooms: usize,
room_size_range: (i32, i32),
room_templates: Vec<RoomTemplate>,
}
impl DungeonGenerator {
pub fn generate(&self, params: &GenerationParams) -> GenerationResult<DungeonLayout> {
let mut rng = SeededRng::new(params.seed);
let room_count = rng.next_range(self.min_rooms as i32, self.max_rooms as i32) as usize;
let mut rooms = Vec::new();
let mut attempts = 0;
let max_attempts = room_count * 100;
while rooms.len() < room_count && attempts < max_attempts {
let width = rng.next_range(self.room_size_range.0, self.room_size_range.1);
let height = rng.next_range(self.room_size_range.0, self.room_size_range.1);
let x = rng.next_range(0, 100 - width);
let y = rng.next_range(0, 100 - height);
let bounds = Rect { x, y, width, height };
if !rooms.iter().any(|r: &Room| r.bounds.intersects_padded(&bounds, 2)) {
let room_type = if rooms.is_empty() {
RoomType::Entry
} else {
self.choose_room_type(&mut rng, rooms.len(), room_count)
};
rooms.push(Room {
id: rooms.len(),
bounds,
room_type,
connections: Vec::new(),
content: RoomContent::default(),
});
}
attempts += 1;
}
if rooms.len() < self.min_rooms {
return Err(GenerationError::MaxIterationsReached);
}
let corridors = self.connect_rooms(&mut rooms, &mut rng);
let boss_room = rooms.iter()
.enumerate()
.filter(|(_, r)| r.room_type != RoomType::Entry)
.max_by_key(|(_, r)| self.distance_from_entry(&rooms, r.id))
.map(|(i, _)| i)
.unwrap_or(rooms.len() - 1);
rooms[boss_room].room_type = RoomType::Boss;
for room in &mut rooms {
room.content = self.generate_room_content(room, params, &mut rng);
}
Ok(DungeonLayout {
rooms,
corridors,
entry_room: 0,
boss_room,
width: 100,
height: 100,
})
}
fn connect_rooms(&self, rooms: &mut [Room], rng: &mut SeededRng) -> Vec<Corridor> {
let mut corridors = Vec::new();
let mut connected = vec![false; rooms.len()];
connected[0] = true;
while connected.iter().any(|&c| !c) {
let mut best_pair = None;
let mut best_dist = f32::MAX;
for (i, room_a) in rooms.iter().enumerate() {
if !connected[i] {
continue;
}
for (j, room_b) in rooms.iter().enumerate() {
if connected[j] {
continue;
}
let dist = room_a.bounds.center_distance(&room_b.bounds);
if dist < best_dist {
best_dist = dist;
best_pair = Some((i, j));
}
}
}
if let Some((from, to)) = best_pair {
let path = self.create_corridor_path(
rooms[from].bounds.center(),
rooms[to].bounds.center(),
rng,
);
rooms[from].connections.push(to);
rooms[to].connections.push(from);
connected[to] = true;
corridors.push(Corridor {
from_room: from,
to_room: to,
path,
width: 2,
});
} else {
break;
}
}
corridors
}
fn create_corridor_path(
&self,
start: (i32, i32),
end: (i32, i32),
rng: &mut SeededRng,
) -> Vec<(i32, i32)> {
let mut path = Vec::new();
let (mut x, mut y) = start;
if rng.next_float() > 0.5 {
while x != end.0 {
path.push((x, y));
x += (end.0 - x).signum();
}
while y != end.1 {
path.push((x, y));
y += (end.1 - y).signum();
}
} else {
while y != end.1 {
path.push((x, y));
y += (end.1 - y).signum();
}
while x != end.0 {
path.push((x, y));
x += (end.0 - x).signum();
}
}
path.push(end);
path
}
fn choose_room_type(&self, rng: &mut SeededRng, current: usize, total: usize) -> RoomType {
let options = [
(RoomType::Normal, 10.0),
(RoomType::Treasure, 2.0),
(RoomType::Secret, 1.0),
(RoomType::Shop, 1.5),
(RoomType::Shrine, 1.0),
];
rng.weighted_choose(&options).unwrap_or(RoomType::Normal)
}
fn generate_room_content(
&self,
room: &Room,
params: &GenerationParams,
rng: &mut SeededRng,
) -> RoomContent {
let mut content = RoomContent::default();
match room.room_type {
RoomType::Entry => {}
RoomType::Normal => {
let enemy_count = rng.next_range(1, 4);
for _ in 0..enemy_count {
content.enemies.push(EnemySpawn {
position: room.bounds.random_point(rng),
enemy_type: "skeleton".into(),
level: params.context.player_level,
});
}
}
RoomType::Treasure => {
content.items.push(ItemSpawn {
position: room.bounds.center(),
loot_table: "treasure_chest".into(),
});
}
RoomType::Boss => {
content.enemies.push(EnemySpawn {
position: room.bounds.center(),
enemy_type: "boss".into(),
level: params.context.player_level + 5,
});
}
_ => {}
}
content
}
}
}
Item Generation
Procedural Item Generator
Generate items with random properties:
#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
pub struct GeneratedItem {
pub base_type: ItemBaseType,
pub name: String,
pub rarity: ItemRarity,
pub level: u32,
pub stats: ItemStats,
pub affixes: Vec<ItemAffix>,
pub special_ability: Option<SpecialAbility>,
}
#[derive(Debug, Clone)]
pub struct ItemStats {
pub damage: Option<(f32, f32)>,
pub armor: Option<f32>,
pub durability: f32,
pub weight: f32,
}
#[derive(Debug, Clone)]
pub struct ItemAffix {
pub affix_type: AffixType,
pub tier: u32,
pub value: f32,
}
#[derive(Debug, Clone, Copy)]
pub enum AffixType {
Strength,
Agility,
Intelligence,
Vitality,
FireDamage,
IceDamage,
LightningDamage,
LifeSteal,
CriticalChance,
CriticalDamage,
AttackSpeed,
MovementSpeed,
}
pub struct ItemGenerator {
name_generator: NameGenerator,
affix_pool: Vec<AffixTemplate>,
}
impl ItemGenerator {
pub fn generate(&self, params: &ItemGenParams, rng: &mut SeededRng) -> GeneratedItem {
let rarity = self.roll_rarity(params.base_rarity_chance, rng);
let affix_count = self.affix_count_for_rarity(rarity);
let base = ¶ms.base_type;
let mut stats = self.roll_base_stats(base, params.level, rng);
let mut affixes = Vec::new();
for _ in 0..affix_count {
if let Some(affix) = self.roll_affix(params.level, &affixes, rng) {
affixes.push(affix);
}
}
for affix in &affixes {
self.apply_affix_to_stats(&mut stats, affix);
}
let special = if rarity >= ItemRarity::Epic && rng.next_float() > 0.5 {
Some(self.generate_special_ability(params.level, rng))
} else {
None
};
let name = self.generate_name(base, &affixes, rarity, rng);
GeneratedItem {
base_type: base.clone(),
name,
rarity,
level: params.level,
stats,
affixes,
special_ability: special,
}
}
fn roll_rarity(&self, base_chance: f32, rng: &mut SeededRng) -> ItemRarity {
let roll = rng.next_float() * base_chance;
match roll {
r if r > 0.99 => ItemRarity::Legendary,
r if r > 0.95 => ItemRarity::Epic,
r if r > 0.85 => ItemRarity::Rare,
r if r > 0.65 => ItemRarity::Uncommon,
_ => ItemRarity::Common,
}
}
fn affix_count_for_rarity(&self, rarity: ItemRarity) -> usize {
match rarity {
ItemRarity::Common => 0,
ItemRarity::Uncommon => 1,
ItemRarity::Rare => 2,
ItemRarity::Epic => 3,
ItemRarity::Legendary => 4,
}
}
fn roll_affix(
&self,
level: u32,
existing: &[ItemAffix],
rng: &mut SeededRng,
) -> Option<ItemAffix> {
let available: Vec<_> = self.affix_pool
.iter()
.filter(|a| a.min_level <= level)
.filter(|a| !existing.iter().any(|e| e.affix_type == a.affix_type))
.collect();
if available.is_empty() {
return None;
}
let template = rng.choose(&available)?;
let tier = rng.next_range(1, template.max_tier as i32) as u32;
let value = template.base_value * (1.0 + tier as f32 * 0.25);
Some(ItemAffix {
affix_type: template.affix_type,
tier,
value,
})
}
fn generate_name(
&self,
base: &ItemBaseType,
affixes: &[ItemAffix],
rarity: ItemRarity,
rng: &mut SeededRng,
) -> String {
if rarity >= ItemRarity::Legendary {
return self.name_generator.generate_legendary_name(base, rng);
}
let prefix = affixes.first().map(|a| self.affix_to_prefix(a));
let suffix = affixes.get(1).map(|a| self.affix_to_suffix(a));
match (prefix, suffix) {
(Some(p), Some(s)) => format!("{} {} {}", p, base.display_name(), s),
(Some(p), None) => format!("{} {}", p, base.display_name()),
(None, Some(s)) => format!("{} {}", base.display_name(), s),
(None, None) => base.display_name().to_string(),
}
}
}
}
AI-Enhanced Generation
LLM-Powered Content Creation
Use AI for narrative content:
#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;
pub struct AiContentGenerator {
llm: LlmClient,
templates: ContentTemplates,
}
impl AiContentGenerator {
pub async fn generate_quest(
&self,
context: &QuestGenContext,
) -> Result<GeneratedQuest, GenerationError> {
let prompt = format!(
r#"Generate a side quest for a fantasy RPG.
Setting: {}
Player level: {}
Available NPCs: {}
Recent events: {}
Theme: {}
Generate a quest with:
1. Title (catchy, thematic)
2. Description (2-3 sentences)
3. Objectives (3-5 steps)
4. Rewards (appropriate for level)
5. Optional twist or complication
Return as JSON."#,
context.location,
context.player_level,
context.available_npcs.join(", "),
context.recent_events.join(", "),
context.theme
);
let response = self.llm.complete(&prompt).await
.map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
self.parse_quest_response(&response)
}
pub async fn generate_npc_backstory(
&self,
npc: &NpcGenContext,
) -> Result<NpcBackstory, GenerationError> {
let prompt = format!(
r#"Create a backstory for an NPC in a fantasy game.
Name: {}
Role: {}
Location: {}
Personality traits: {}
Generate:
1. Background (2-3 sentences)
2. Current motivation
3. Secret or hidden aspect
4. Connection to the world
5. Speech mannerism
Keep responses concise and game-appropriate."#,
npc.name,
npc.role,
npc.location,
npc.personality_traits.join(", ")
);
let response = self.llm.complete(&prompt).await
.map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
self.parse_npc_response(&response)
}
pub async fn generate_location_description(
&self,
location: &LocationGenContext,
) -> Result<LocationDescription, GenerationError> {
let prompt = format!(
r#"Describe a location for a fantasy game.
Type: {}
Biome: {}
Key features: {}
Mood: {}
Provide:
1. Name (evocative)
2. Short description (1 sentence for UI)
3. Full description (2-3 sentences for exploration)
4. Notable elements (3-5 items)
5. Ambient sounds suggestion"#,
location.location_type,
location.biome,
location.features.join(", "),
location.mood
);
let response = self.llm.complete(&prompt).await
.map_err(|e| GenerationError::InvalidParams(e.to_string()))?;
self.parse_location_response(&response)
}
}
#[derive(Debug)]
pub struct GeneratedQuest {
pub title: String,
pub description: String,
pub objectives: Vec<QuestObjective>,
pub rewards: Vec<QuestReward>,
pub twist: Option<String>,
}
#[derive(Debug)]
pub struct NpcBackstory {
pub background: String,
pub motivation: String,
pub secret: String,
pub world_connection: String,
pub speech_pattern: String,
}
}
Validation and Constraints
Content Validator
Ensure generated content meets requirements:
#![allow(unused)]
fn main() {
pub struct ContentValidator {
rules: Vec<ValidationRule>,
}
pub enum ValidationRule {
MinRooms(usize),
MaxDeadEnds(usize),
RequireRoomType(RoomType),
MaxDifficulty(f32),
Connectivity,
NoOverlap,
Custom(Box<dyn Fn(&DungeonLayout) -> ValidationResult>),
}
impl ContentValidator {
pub fn validate_dungeon(&self, dungeon: &DungeonLayout) -> ValidationResult {
for rule in &self.rules {
match rule {
ValidationRule::MinRooms(min) => {
if dungeon.rooms.len() < *min {
return ValidationResult::Invalid(
format!("Too few rooms: {} < {}", dungeon.rooms.len(), min)
);
}
}
ValidationRule::MaxDeadEnds(max) => {
let dead_ends = dungeon.rooms
.iter()
.filter(|r| r.connections.len() <= 1)
.count();
if dead_ends > *max {
return ValidationResult::Invalid(
format!("Too many dead ends: {} > {}", dead_ends, max)
);
}
}
ValidationRule::RequireRoomType(room_type) => {
if !dungeon.rooms.iter().any(|r| r.room_type == *room_type) {
return ValidationResult::Invalid(
format!("Missing required room type: {:?}", room_type)
);
}
}
ValidationRule::Connectivity => {
if !self.check_connectivity(dungeon) {
return ValidationResult::Invalid("Dungeon is not fully connected".into());
}
}
_ => {}
}
}
ValidationResult::Valid
}
fn check_connectivity(&self, dungeon: &DungeonLayout) -> bool {
if dungeon.rooms.is_empty() {
return true;
}
let mut visited = vec![false; dungeon.rooms.len()];
let mut stack = vec![0usize];
while let Some(current) = stack.pop() {
if visited[current] {
continue;
}
visited[current] = true;
for &connected in &dungeon.rooms[current].connections {
if !visited[connected] {
stack.push(connected);
}
}
}
visited.iter().all(|&v| v)
}
}
}
Best Practices
1. **Seed Everything**: Use deterministic RNG for reproducible generation
2. **Validate Early**: Reject invalid content before expensive operations
3. **Layer Generation**: Start coarse, refine with detail passes
4. **Cache Results**: Store expensive generation results for reuse
- **Infinite Loops**: Always limit generation attempts
- **Memory Bloat**: Stream large content; don't hold everything in memory
- **Sameness**: Vary parameters enough to feel unique
- **Performance**: Profile generation; move to background threads if slow
Related Documentation
- Terrain System - Terrain rendering integration
- Quest Systems - Generated quest execution
- AI System Overview - AI content enhancement
- Configuration Reference - PCG parameters
Save & Load Systems
Crates:
astraweave-persistence-ecs,astraweave-core
Status: Production Ready
AstraWeave provides deterministic save/load with full ECS state serialization, player profiles, and versioned migrations.
Quick Start
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveSystem, LoadSystem, SaveConfig};
use astraweave_ecs::World;
let config = SaveConfig {
path: "saves/",
compression: true,
encryption: false,
..Default::default()
};
let save_system = SaveSystem::new(config);
// Save world state
save_system.save(&world, "quicksave")?;
// Load world state
let loaded_world = save_system.load("quicksave")?;
}
Save System Architecture
Save File Structure
├── Header (version, timestamp, checksum)
├── World State
│ ├── Entities (sparse set serialization)
│ ├── Components (per-archetype)
│ └── Resources (global state)
├── Player Data
│ ├── Inventory
│ ├── Progress
│ └── Settings
└── Metadata
├── Screenshot (optional)
└── Play time
ECS Serialization
Component Registration
Components must be registered for serialization:
#![allow(unused)]
fn main() {
use astraweave_ecs::{Component, Serialize, Deserialize};
#[derive(Component, Serialize, Deserialize)]
struct Health {
current: f32,
max: f32,
}
#[derive(Component, Serialize, Deserialize)]
struct Position {
x: f32,
y: f32,
z: f32,
}
// Register with world
app.register_serializable::<Health>();
app.register_serializable::<Position>();
}
Selective Serialization
Mark components to skip during save:
#![allow(unused)]
fn main() {
#[derive(Component)]
#[component(skip_save)]
struct RenderCache {
// Regenerated at load time
}
#[derive(Component, Serialize, Deserialize)]
#[component(save_priority = "high")]
struct PlayerData {
// Saved first, loaded first
}
}
Save Slots
Slot Management
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveSlot, SlotManager};
let mut slots = SlotManager::new("saves/", 10); // 10 slots
// Get available slots
let available = slots.list()?;
for slot in &available {
println!("Slot {}: {} ({})", slot.index, slot.name, slot.timestamp);
}
// Save to slot
slots.save_to_slot(3, &world, "My Save")?;
// Load from slot
let world = slots.load_from_slot(3)?;
// Delete slot
slots.delete_slot(3)?;
}
Auto-Save
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::AutoSave;
let mut autosave = AutoSave::new(Duration::from_secs(300)); // 5 minutes
// In game loop
if autosave.should_save() {
slots.save_to_slot(0, &world, "Autosave")?;
autosave.mark_saved();
}
}
Player Profiles
Profile Management
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{PlayerProfile, ProfileManager};
let mut profiles = ProfileManager::new("profiles/")?;
// Create profile
let profile = PlayerProfile::new("Player1");
profiles.create(profile)?;
// Load profile
let profile = profiles.load("Player1")?;
// Update settings
profile.settings.volume = 0.8;
profile.settings.difficulty = Difficulty::Hard;
profiles.save(&profile)?;
}
Profile Contents
#![allow(unused)]
fn main() {
pub struct PlayerProfile {
pub name: String,
pub created: DateTime,
pub play_time: Duration,
pub settings: PlayerSettings,
pub achievements: Vec<Achievement>,
pub statistics: PlayerStats,
}
pub struct PlayerSettings {
pub volume: f32,
pub music_volume: f32,
pub sfx_volume: f32,
pub voice_volume: f32,
pub difficulty: Difficulty,
pub controls: ControlBindings,
pub graphics: GraphicsSettings,
}
}
Versioning & Migration
Version Handling
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::{SaveVersion, Migration};
// Current save version
const SAVE_VERSION: SaveVersion = SaveVersion::new(1, 2, 0);
// Load with migration
let world = save_system.load_with_migrations(
"old_save",
&[
Migration::new(
SaveVersion::new(1, 0, 0),
SaveVersion::new(1, 1, 0),
migrate_1_0_to_1_1,
),
Migration::new(
SaveVersion::new(1, 1, 0),
SaveVersion::new(1, 2, 0),
migrate_1_1_to_1_2,
),
],
)?;
fn migrate_1_0_to_1_1(data: &mut SaveData) -> Result<()> {
// Add new health.shield field
for entity in data.entities_with::<Health>() {
let health: &mut Health = data.get_mut(entity)?;
health.shield = 0.0; // New field
}
Ok(())
}
}
Corruption Recovery
Backup System
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::BackupConfig;
let config = SaveConfig {
backup: BackupConfig {
enabled: true,
max_backups: 3,
backup_on_save: true,
},
..Default::default()
};
// Backups created automatically
// saves/quicksave.sav
// saves/quicksave.sav.bak1
// saves/quicksave.sav.bak2
// saves/quicksave.sav.bak3
}
Integrity Verification
#![allow(unused)]
fn main() {
use astraweave_persistence_ecs::verify_save;
match verify_save("saves/quicksave.sav") {
Ok(info) => {
println!("Valid save: v{}", info.version);
}
Err(SaveError::Corrupted) => {
// Try backup
if let Ok(backup) = find_valid_backup("quicksave") {
restore_from_backup(backup)?;
}
}
Err(SaveError::VersionMismatch(v)) => {
println!("Save from future version: {}", v);
}
}
}
Deterministic Replay
AstraWeave’s deterministic ECS enables save/load that reproduces exact game states:
#![allow(unused)]
fn main() {
use astraweave_core::replay::{ReplayRecorder, ReplayPlayer};
// Record session
let mut recorder = ReplayRecorder::new();
recorder.start(&world);
// During gameplay
recorder.record_frame(&world, &inputs);
// Save replay
recorder.save("replay.bin")?;
// Play back (bit-identical to original)
let mut player = ReplayPlayer::load("replay.bin")?;
while let Some(frame) = player.next_frame() {
world.apply_inputs(frame.inputs);
world.step();
assert_eq!(world.checksum(), frame.checksum);
}
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| Quick save (1K entities) | ~50 ms | With compression |
| Quick load (1K entities) | ~30 ms | With decompression |
| Profile save | ~5 ms | Settings only |
| Checksum verify | ~2 ms | Per save file |
Optimization Tips
- Async saves - Don’t block gameplay
- Incremental saves - Only save changed data
- Compression - 60-80% size reduction
- Memory-mapped loading - Faster large worlds
#![allow(unused)]
fn main() {
// Async save
let handle = save_system.save_async(&world, "quicksave");
// Continue gameplay...
// Wait for completion
handle.await?;
}
See Also
Scripting with Rhai
Crates:
astraweave-scripting,astraweave-author
Status: Production Ready (Sandboxed)
AstraWeave provides secure Rhai scripting for modding, behavior authoring, and runtime customization with full sandboxing.
Quick Start
// scripts/my_behavior.rhai
// Called when entity spawns
fn on_spawn(entity) {
log("Entity spawned: " + entity.id);
entity.set_health(100);
entity.play_animation("idle");
}
// Called every frame
fn on_update(entity, dt) {
let player = find_nearest("Player");
if distance(entity.position, player.position) < 10.0 {
entity.look_at(player);
entity.move_towards(player, dt * 5.0);
}
}
// Called when taking damage
fn on_damage(entity, amount, source) {
if entity.health - amount <= 0 {
entity.play_animation("death");
spawn_particles("blood", entity.position, 20);
}
}
Script Engine Setup
#![allow(unused)]
fn main() {
use astraweave_scripting::{ScriptEngine, ScriptConfig};
let config = ScriptConfig {
max_operations: 100_000, // Operation limit per call
timeout_ms: 10, // Max execution time
memory_limit_kb: 1024, // Memory limit
allow_network: false, // Network access
allow_filesystem: false, // File access
};
let mut engine = ScriptEngine::new(config);
// Register scripts
engine.load_script("behaviors/enemy.rhai")?;
engine.load_script("behaviors/companion.rhai")?;
// Execute
engine.call("on_spawn", (entity,))?;
}
Available APIs
Entity API
// Position & Movement
entity.position // Get Vec3
entity.set_position(x, y, z) // Set position
entity.move_towards(target, speed)
entity.look_at(target)
entity.rotate(yaw, pitch, roll)
// Health & Combat
entity.health // Current health
entity.max_health // Maximum health
entity.set_health(amount)
entity.damage(amount)
entity.heal(amount)
entity.is_dead()
// Animation
entity.play_animation(name)
entity.stop_animation()
entity.is_animating()
// Components
entity.has_component("Physics")
entity.get_tag("faction")
entity.set_tag("faction", "enemy")
World API
// Entity queries
let player = find_nearest("Player");
let enemies = find_all("Enemy");
let nearby = find_in_radius(position, radius);
// Spawning
let entity = spawn("enemy_type", position);
destroy(entity);
// Raycasting
let hit = raycast(origin, direction, max_distance);
if hit.success {
log("Hit: " + hit.entity.name + " at " + hit.point);
}
// Time
let time = game_time();
let dt = delta_time();
Math API
// Vectors
let v = vec3(1.0, 2.0, 3.0);
let dist = distance(a, b);
let dir = normalize(v);
let dot = dot_product(a, b);
let cross = cross_product(a, b);
// Interpolation
let result = lerp(a, b, t);
let smooth = smoothstep(a, b, t);
// Random
let r = random(); // 0.0 - 1.0
let ri = random_range(1, 10); // 1 - 10
let choice = random_choice(array);
Audio API
// Sound playback
play_sound("sfx/explosion.ogg");
play_sound_at("sfx/growl.ogg", position);
play_music("music/combat.ogg", fade_in_seconds);
stop_music(fade_out_seconds);
set_volume("sfx", 0.8);
Particle API
spawn_particles("fire", position, count);
spawn_particles_at_entity("smoke", entity, count);
stop_particles(particle_system);
Behavior Components
Attaching Scripts to Entities
#![allow(unused)]
fn main() {
use astraweave_scripting::ScriptComponent;
commands.spawn((
Transform::default(),
Health::new(100),
ScriptComponent::new("behaviors/enemy.rhai"),
));
}
Callbacks
| Callback | When Called | Parameters |
|---|---|---|
on_spawn | Entity created | (entity) |
on_update | Every frame | (entity, dt) |
on_destroy | Entity removed | (entity) |
on_collision | Physics collision | (entity, other, contact) |
on_trigger | Trigger overlap | (entity, other, entered) |
on_damage | Taking damage | (entity, amount, source) |
on_death | Health reaches 0 | (entity, killer) |
on_interact | Player interaction | (entity, player) |
Security Sandboxing
Operation Limits
// This will be terminated after 100,000 operations
fn infinite_loop(entity) {
while true {
// Will hit operation limit
}
}
Memory Limits
fn memory_hog(entity) {
let huge_array = [];
for i in 0..1000000 {
huge_array.push(i); // Will hit memory limit
}
}
Blocked Operations
Scripts cannot:
- Access filesystem
- Open network connections
- Call system commands
- Modify engine internals
- Access other scripts’ data
Hot Reloading
Scripts can be modified at runtime:
#![allow(unused)]
fn main() {
// Enable hot reload
engine.enable_hot_reload(true);
// In game loop
engine.check_for_changes()?; // Reloads modified scripts
}
Development workflow:
- Run game
- Edit
.rhaifiles - Changes apply immediately
- No restart needed
Custom Functions
Exposing Rust to Scripts
#![allow(unused)]
fn main() {
use astraweave_scripting::ScriptEngine;
engine.register_fn("custom_ability", |entity: Entity, power: f64| {
// Rust implementation
let damage = power * 2.0;
apply_area_damage(entity.position(), damage, 5.0);
});
}
Using in Scripts
fn on_special_attack(entity) {
custom_ability(entity, 50.0);
play_sound("sfx/special.ogg");
spawn_particles("explosion", entity.position, 100);
}
State Machines
Defining States
// AI state machine
let state = "idle";
fn on_update(entity, dt) {
switch state {
"idle" => {
if see_player() {
state = "chase";
}
}
"chase" => {
let player = find_nearest("Player");
entity.move_towards(player, dt * 8.0);
if distance(entity.position, player.position) < 2.0 {
state = "attack";
}
}
"attack" => {
entity.attack();
state = "cooldown";
set_timer("attack_cooldown", 1.0);
}
"cooldown" => {
if timer_expired("attack_cooldown") {
state = "chase";
}
}
}
}
Debugging
Logging
log("Debug message");
log_warn("Warning message");
log_error("Error message");
Console Commands
#![allow(unused)]
fn main() {
// Register console command
engine.register_command("spawn_enemy", |args| {
let count = args.get(0).unwrap_or(1);
for _ in 0..count {
spawn("enemy", random_position());
}
});
}
Script Inspector
The editor provides:
- Breakpoints
- Variable inspection
- Call stack
- Performance profiling
Performance
| Operation | Latency | Notes |
|---|---|---|
| Script call | ~5 µs | Per function call |
| Entity API | ~100 ns | Property access |
| Script load | ~1 ms | Parse + compile |
| Hot reload | ~2 ms | Per file |
Optimization Tips
- Cache lookups - Don’t find entities every frame
- Limit scope - Smaller scripts are faster
- Use events - Don’t poll in on_update
- Batch operations - Group similar calls
// Bad: Finding player every frame
fn on_update(entity, dt) {
let player = find_nearest("Player"); // Expensive!
}
// Good: Cache reference
let cached_player = null;
fn on_spawn(entity) {
cached_player = find_nearest("Player");
}
fn on_update(entity, dt) {
if cached_player != null {
entity.look_at(cached_player);
}
}
See Also
Working Examples
AstraWeave includes over 20 examples demonstrating different aspects of the engine. This page focuses on the working examples that you can build and run to learn the engine.
Note: AstraWeave is under active development. Some examples have compilation issues due to API evolution. This page focuses on examples that are confirmed to work.
Core AI Examples
These examples demonstrate the AI-native architecture:
Hello Companion ✅
Location: examples/hello_companion
Status: ✅ Working (expected panic)
The simplest example of AI perception, planning, and validation.
cargo run -p hello_companion --release
What it demonstrates:
- AI perception system capturing world state
- LLM-based planning generating intents
- Tool validation system (demonstrates failure case)
- Fixed-tick simulation loop
Expected behavior: Shows AI plan generation, then panics with “LosBlocked” error. This demonstrates that the AI cannot perform invalid actions.
Adaptive Boss ✅
Location: examples/adaptive_boss
Status: ✅ Working
Multi-phase boss with Director AI that adapts tactics based on player behavior.
cargo run -p adaptive_boss --release
What it demonstrates:
- BossDirector for dynamic encounter planning
- Budget-constrained AI decisions
- Telegraph system for attack warnings
- Phase-based combat behavior
Companion Profile ✅
Location: examples/companion_profile
Status: ✅ Working
Demonstrates persistent AI profiles that learn and adapt.
cargo run -p companion_profile --release
What it demonstrates:
- AI profile serialization/deserialization
- Learning from player interactions
- Personality trait adjustment
- Long-term memory systems
Core Engine Examples
These examples showcase fundamental engine systems:
Fluids Demo ✅
Location: examples/fluids_demo
Status: ✅ Working
Interactive fluid simulation with PCISPH physics and multiple scenarios.
cargo run -p fluids_demo --release
What it demonstrates:
- Real-time particle-based fluid simulation
- Multiple scenarios (laboratory, ocean, waterfall, splash)
- Interactive particle spawning (click to add water)
- LOD optimization for performance scaling
- egui debug panel with live parameters
Unified Showcase ✅
Location: examples/unified_showcase
Status: ✅ Working
Comprehensive rendering demo with shadows, terrain, GLTF models, and skybox.
cargo run -p unified_showcase --release
What it demonstrates:
- Shadow mapping with 2048×2048 depth textures
- GLTF model loading with materials
- Procedural terrain with multi-texture blending
- HDR skybox rendering
- 4× MSAA antialiasing
Physics Demo 3D ✅
Location: examples/physics_demo3d
Status: ✅ Working
Demonstrates the Rapier3D physics integration with character controllers, destructibles, and environmental forces.
cargo run -p physics_demo3d --release
What it demonstrates:
- 3D physics simulation with Rapier3D
- Character controller with slope handling
- Destructible objects that shatter
- Water buoyancy and wind forces
- Collision layer filtering
Navmesh Demo ✅
Location: examples/navmesh_demo
Status: ✅ Working
Shows navigation mesh baking and A* pathfinding.
cargo run -p navmesh_demo --release
What it demonstrates:
- NavMesh baking from triangle geometry
- Slope-based walkability filtering
- A* pathfinding with path visualization
- Agent radius margin calculation
Audio Spatial Demo ✅
Location: examples/audio_spatial_demo
Status: ✅ Working
Spatial audio system with 3D positioning and music crossfading.
cargo run -p audio_spatial_demo --release
What it demonstrates:
- 3D positional audio (left/center/right beeps)
- Music playback with crossfading
- Listener tracking tied to camera
- Volume bus control (master/music/SFX)
Networking Examples
These examples show multiplayer and IPC capabilities:
IPC Loopback ✅
Location: examples/ipc_loopback
Status: ✅ Should work
Demonstrates inter-process communication for AI models.
cargo run -p ipc_loopback --release
What it demonstrates:
- Local/cloud AI model switching
- Process isolation for AI
- IPC message passing
- AI model hot-swapping
Coop Server/Client ✅
Location: examples/coop_server, examples/coop_client
Status: ✅ Working
Basic multiplayer client-server architecture.
# Terminal 1
cargo run -p coop_server --release
# Terminal 2
cargo run -p coop_client --release
What it demonstrates:
- Server-authoritative validation
- Intent-based networking
- AI agent synchronization
- Anti-cheat through determinism
Tool and Planning Examples
These examples focus on AI planning and tool usage:
LLM Tool Call ✅
Location: examples/llm_toolcall
Status: ✅ Working
Direct demonstration of LLM tool calling.
cargo run -p llm_toolcall --release
What it demonstrates:
- LLM integration
- Tool definition and usage
- Structured AI responses
- Planning validation
Phase Director ✅
Location: examples/phase_director
Status: ✅ Working
Complex AI director managing multiple phases.
cargo run -p phase_director --release
What it demonstrates:
- Multi-phase AI behavior
- Director pattern implementation
- State machine management
- Complex AI coordination
Development Examples
These examples help with engine development:
Debug Overlay ❌
Location: examples/debug_overlay
Status: ❌ Has compilation issues (egui API)
Debug UI overlay for development.
Known issues: egui API mismatches with current version.
Persona Loader ✅
Location: examples/persona_loader
Status: ✅ Working
Loading and managing AI personas from files.
cargo run -p persona_loader --release
What it demonstrates:
- AI persona definition files
- Dynamic persona loading
- Personality trait configuration
- Behavioral parameter tuning
Known Compilation Issues
Some examples have known issues due to API evolution:
Graphics Examples ❌
- visual_3d: winit API mismatches
- ui_controls_demo: egui API compatibility issues
- debug_overlay: egui API changes
Authoring Examples ❌
- rhai_authoring: Depends on broken astraweave-author crate
- Issues with rhai sync/send traits
Complex Demos ❌
- npc_town_demo: Multiple API mismatches
- weaving_playground: Dependency issues
- cutscene_render_demo: Graphics API issues
Testing Examples
To verify your installation is working:
Minimal Test Sequence
# 1. Build core components
cargo build -p astraweave-core -p astraweave-ai -p hello_companion
# 2. Run the basic example
cargo run -p hello_companion --release
# 3. Run unit tests
cargo test -p astraweave-input
Debugging Build Issues
If examples fail to compile:
- Check Rust version:
rustc --versionshould matchrust-toolchain.toml - Update dependencies:
cargo update - Clean build:
cargo clean && cargo build - Check system dependencies: Ensure graphics and audio libraries are installed
Reporting Issues
If you find compilation issues with examples marked as working:
- Check your platform and Rust version
- Ensure all system dependencies are installed
- Try a clean build
- Report the issue with full error output
Building Your Own Examples
When creating new examples:
Minimal Example Structure
// examples/my_example/src/main.rs
use astraweave_core::*;
use astraweave_ai::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize the engine
let mut world = World::new();
// Add your systems
world.add_system(my_custom_system);
// Run the simulation
world.run()?;
Ok(())
}
Cargo.toml Template
[package]
name = "my_example"
version.workspace = true
edition.workspace = true
[dependencies]
astraweave-core = { path = "../../astraweave-core" }
astraweave-ai = { path = "../../astraweave-ai" }
anyhow.workspace = true
Next Steps
- Start Simple: Begin with Hello Companion
- Learn Architecture: Read AI-Native Design
- Build Something: Follow Building Your First Game
- Contribute: Help fix broken examples in Contributing Guide
The working examples are your best introduction to AstraWeave’s capabilities. Start with hello_companion and work your way up to more complex scenarios.
Hello Companion Walkthrough
The hello_companion example is the perfect introduction to AstraWeave’s AI-native architecture. This walkthrough explains every step of what happens when you run this example and why it’s designed this way.
Running the Example
cargo run -p hello_companion --release
Expected Output
[INFO] Initializing AstraWeave Engine...
[INFO] Creating world with ECS...
[INFO] Spawning AI companion entity
[INFO] Starting simulation loop at 60Hz
[INFO] Tick 1: Capturing perception snapshot
[INFO] AI perception: 1 entities visible, 0 audio events
[INFO] Sending perception to AI planning layer
[INFO] AI generated plan: MoveTo { target: Vec3(10.0, 0.0, 5.0), urgency: 0.7 }
[INFO] Validating movement tool usage...
[ERROR] Tool validation failed: LosBlocked - No clear line of sight to target
thread 'main' panicked at examples/hello_companion/src/main.rs:42:5:
called `Result::unwrap()` on an `Err` value: ToolValidationError(LosBlocked)
This panic is intentional! It demonstrates AstraWeave’s core principle: AI agents cannot perform actions that violate the game world’s constraints.
Code Walkthrough
Let’s examine the source code to understand each step:
1. Engine Initialization
// examples/hello_companion/src/main.rs
use astraweave_core::*;
use astraweave_ai::*;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize logging
env_logger::init();
// Create the ECS world
let mut world = World::new();
// Configure the engine systems
world.add_plugin(CorePlugin)
.add_plugin(AIPlugin)
.add_plugin(PhysicsPlugin);
What’s happening:
- Sets up the Entity-Component-System (ECS) world
- Registers core systems for AI, physics, and simulation
- Configures 60Hz fixed-tick simulation
2. Spawning the AI Companion
#![allow(unused)]
fn main() {
// Spawn an AI companion entity
let companion = world.spawn()
.insert(Position(Vec3::new(0.0, 0.0, 0.0)))
.insert(AIAgent {
perception_range: 10.0,
planning_interval: Duration::from_millis(500),
ai_model: AIModel::Local("companion-7b".to_string()),
})
.insert(MovementCapability {
max_speed: 5.0,
acceleration: 2.0,
})
.id();
info!("Spawned AI companion with ID: {:?}", companion);
}
What’s happening:
- Creates a new entity in the ECS world
- Adds position component (where the companion is)
- Adds AI agent component (makes it intelligent)
- Adds movement capability (what it can do)
3. The Simulation Loop
#![allow(unused)]
fn main() {
// Run simulation for a few ticks
for tick in 0..5 {
info!("Starting tick {}", tick);
// This is where the AI magic happens
world.step(Duration::from_nanos(16_666_667)); // 1/60 second
// Small delay so we can see the output
std::thread::sleep(Duration::from_millis(100));
}
}
What’s happening:
- Runs exactly 5 simulation ticks
- Each tick advances the world by exactly 1/60th of a second
- The deterministic timing ensures consistent behavior
4. The AI Pipeline (Inside world.step())
During each world.step() call, several systems run in sequence:
A. Perception System
#![allow(unused)]
fn main() {
fn ai_perception_system(
mut query: Query<(&Position, &AIAgent, &mut PerceptionState)>,
world_query: Query<&Position>,
) {
for (pos, agent, mut perception) in query.iter_mut() {
// Gather what the AI can see
let mut visible_entities = Vec::new();
for other_pos in world_query.iter() {
let distance = pos.0.distance(other_pos.0);
if distance <= agent.perception_range {
visible_entities.push(EntityData {
position: other_pos.0,
distance,
entity_type: "unknown".to_string(),
});
}
}
// Create perception snapshot
perception.last_snapshot = Some(PerceptionSnapshot {
timestamp: world.current_tick(),
agent_id: entity,
visible_entities,
audio_events: vec![], // None in this simple example
world_state: WorldState::default(),
});
}
}
}
B. AI Planning System
#![allow(unused)]
fn main() {
fn ai_planning_system(
mut query: Query<(&AIAgent, &PerceptionState, &mut PlanningState)>,
ai_service: Res<AIService>,
) {
for (agent, perception, mut planning) in query.iter_mut() {
if let Some(snapshot) = &perception.last_snapshot {
// Send to AI model for planning
let plan_request = PlanningRequest {
perception: snapshot.clone(),
agent_profile: agent.clone(),
available_tools: vec!["MovementTool", "InteractionTool"],
};
// This is where the LLM generates a plan
let plan = ai_service.generate_plan(plan_request)?;
info!("AI generated plan: {:?}", plan.intent);
planning.current_plan = Some(plan);
}
}
}
}
C. Tool Validation System
#![allow(unused)]
fn main() {
fn tool_validation_system(
mut query: Query<(&PlanningState, &mut ActionState)>,
tool_registry: Res<ToolRegistry>,
world: &World,
) {
for (planning, mut action) in query.iter_mut() {
if let Some(plan) = &planning.current_plan {
for tool_usage in &plan.tools {
// This is where the validation happens
let validation_result = tool_registry
.get_tool(&tool_usage.tool_name)
.unwrap()
.validate(world, tool_usage);
match validation_result {
ValidationResult::Valid => {
info!("Tool validation passed: {}", tool_usage.tool_name);
action.pending_actions.push(tool_usage.clone());
}
ValidationResult::Blocked(reason) => {
error!("Tool validation failed: {:?}", reason);
// This causes the panic in hello_companion
return Err(ToolValidationError::from(reason));
}
}
}
}
}
}
}
Why Does It Panic?
The panic occurs because the AI tries to move to a position but there’s no clear line of sight. Here’s what happens:
1. AI Perception
- The companion perceives its current position (0, 0, 0)
- It detects no obstacles in its perception range
2. AI Planning
- The AI decides it wants to move to position (10, 0, 5)
- This seems reasonable based on its limited perception
3. Tool Validation
- The MovementTool.validate() method checks line of sight
- There’s an invisible obstacle blocking the path
- Validation fails with
LosBlockederror
4. Engine Authority
- The engine refuses to execute the invalid action
- This maintains world integrity and prevents AI cheating
Key Learning Points
1. AI Cannot Cheat
The AI doesn’t have perfect information about the world. It can only act based on what it perceives, and all actions must be validated by the engine.
2. Deterministic Behavior
Run the example multiple times - you’ll get the same result every time. This determinism is crucial for:
- Reliable testing
- Networking (same simulation on all clients)
- Debugging AI behavior
3. Tool-Based Architecture
The AI doesn’t directly move entities or change the world. It can only request actions through validated tools:
- MovementTool (for movement)
- InteractionTool (for object interaction)
- CombatTool (for attacks)
- CommunicationTool (for dialogue)
4. Perception vs Reality
The AI’s perception is limited and may not match reality. This creates interesting emergent behavior as AI agents must:
- Explore to gather information
- Make decisions with incomplete data
- Adapt when actions fail
Modifying the Example
Make It Not Panic
To see successful AI behavior, modify the world setup:
#![allow(unused)]
fn main() {
// Remove the obstacle that blocks line of sight
world.remove_obstacle(Vec3::new(5.0, 0.0, 2.5));
// Or give the AI perfect perception
ai_agent.perception_range = f32::INFINITY;
}
Add More Interesting Behavior
#![allow(unused)]
fn main() {
// Add a target for the AI to find
world.spawn()
.insert(Position(Vec3::new(15.0, 0.0, 0.0)))
.insert(InteractableItem {
item_type: "treasure_chest".to_string(),
value: 100,
});
// The AI will now try to navigate to and interact with the chest
}
Enable Logging for More Detail
RUST_LOG=debug cargo run -p hello_companion --release
This shows detailed information about:
- ECS system execution order
- AI model input/output
- Tool validation steps
- World state changes
Architectural Insights
Fixed-Tick Simulation
The 60Hz fixed timestep ensures:
- Physics determinism
- Consistent AI decision making
- Reliable networking
- Predictable performance
ECS Benefits
The Entity-Component-System architecture provides:
- Cache-friendly performance
- Clear separation of concerns
- Easy parallel system execution
- Modular, testable code
AI Validation Pipeline
The perception → planning → validation → execution pipeline ensures:
- No AI cheating
- Consistent game rules
- Emergent behavior from constraints
- Easy debugging and testing
Next Steps
After understanding hello_companion:
- Explore More Examples: Try Adaptive Boss for complex AI
- Learn Architecture: Read AI-Native Design
- Build Your Own: Follow Building Your First Game
- Dive Deeper: Study Core AI Systems
Common Questions
Q: Why does it panic instead of just logging the error?
A: The panic demonstrates that validation failures are serious. In a real game, you’d handle this gracefully, but the example uses panic to make the validation concept crystal clear.
Q: Can I make the AI smarter to avoid this error?
A: Yes! You can:
- Improve the AI’s perception system
- Give it better pathfinding tools
- Add obstacle detection to its planning
- Implement learning from failed actions
Q: Is this really how a game AI should work?
A: For AI-native games, yes! This approach:
- Prevents AI cheating
- Creates emergent behavior
- Works in multiplayer
- Enables complex AI interactions
The “failed action = learning opportunity” approach leads to much more interesting AI behavior than scripted sequences.
The hello_companion example may be simple, but it demonstrates the fundamental principles that enable AstraWeave’s AI-native gameplay. Every complex AI behavior in the engine builds on these same validation patterns.
Adaptive Boss Walkthrough
The adaptive_boss example demonstrates AstraWeave’s Director System - an AI-driven game master that orchestrates boss encounters with dynamic phase transitions, telegraphed attacks, and budget-constrained tactical decisions.
Running the Example
cargo run -p adaptive_boss --release
For the full Veilweaver boss experience (requires veilweaver_slice feature):
cargo run -p adaptive_boss --release --features veilweaver_slice
What It Demonstrates
- BossDirector: AI that plans boss encounter actions
- Phase-based combat: Boss behavior changes based on situation
- Director budget: Limited resources for spawns, traps, and terrain edits
- Telegraph system: Visual/audio warnings before powerful attacks
- OathboundWardenDirector: Full Veilweaver boss with Fate-weaving mechanics
Expected Output
Warden phase: Stalking
Telegraphs: ["Thread gathering at position (5, 3)"]
Director plan: {
"spawn": [{"enemy": "shade", "pos": [4, 1]}],
"traps": [{"type": "thread_snare", "pos": [6, 2]}],
"terrain": []
}
Remaining budget: traps=1, terrain_edits=2, spawns=1
Code Walkthrough
1. Arena Setup
#![allow(unused)]
fn main() {
let mut w = World::new();
// Create the combat arena: player, companion, and boss
let player = w.spawn("Player", IVec2 { x: 2, y: 2 }, Team { id: 0 }, 100, 0);
let comp = w.spawn("Comp", IVec2 { x: 3, y: 2 }, Team { id: 1 }, 80, 30);
let boss = w.spawn("Boss", IVec2 { x: 14, y: 2 }, Team { id: 2 }, 400, 0);
}
The world contains three entities:
- Player (HP: 100): The human-controlled character
- Companion (HP: 80, Ammo: 30): AI ally
- Boss (HP: 400): The adversary with high HP
2. World Snapshot
#![allow(unused)]
fn main() {
let snap = WorldSnapshot {
t: w.t,
player: PlayerState { hp: 100, pos: w.pos_of(player).unwrap(), ... },
me: CompanionState { ammo: 30, morale: 0.8, pos: w.pos_of(comp).unwrap(), ... },
enemies: vec![EnemyState { id: boss, hp: 400, cover: "high", ... }],
pois: vec![],
obstacles: vec![],
objective: Some("defeat_boss".into()),
};
}
The Director receives a perception snapshot containing:
- Current game time
- Player state (health, position, stance)
- Companion state (ammo, morale, cooldowns)
- Enemy states (the boss from its perspective)
3. Director Budget
#![allow(unused)]
fn main() {
let mut budget = DirectorBudget {
traps: 2, // Can place 2 traps
terrain_edits: 2, // Can modify terrain twice
spawns: 2, // Can summon 2 minions
};
}
The budget prevents the Director from overwhelming the player. Each action consumes budget:
- Spawns: Summoning minions costs 1 spawn point
- Traps: Placing hazards costs 1 trap point
- Terrain edits: Blocking paths or creating cover costs 1 edit point
4. Director Planning (Basic)
#![allow(unused)]
fn main() {
#[cfg(not(feature = "veilweaver_slice"))]
{
let director = BossDirector;
let plan = director.plan(&snap, &budget);
apply_director_plan(&mut w, &mut budget, &plan, &mut log);
}
}
The basic BossDirector analyzes the world state and generates a plan within budget.
5. Veilweaver Director (Advanced)
#![allow(unused)]
fn main() {
#[cfg(feature = "veilweaver_slice")]
{
let mut director = OathboundWardenDirector::new();
let directive = director.step(&snap, &budget);
println!("Warden phase: {:?}", directive.phase);
if !directive.telegraphs.is_empty() {
println!("Telegraphs: {:?}", directive.telegraphs);
}
apply_director_plan(&mut w, &mut budget, &directive.plan, &mut log);
}
}
The Oathbound Warden has distinct phases:
- Stalking: Observing, placing traps, gathering threads
- Weaving: Manipulating fate threads, buffing/debuffing
- Severing: Aggressive attacks, breaking player connections
- Unraveling: Desperate phase when low HP
Director System Architecture
┌────────────────────────────────────────────────────────┐
│ BossDirector │
├────────────────────────────────────────────────────────┤
│ Input: WorldSnapshot + DirectorBudget │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐ │
│ │ Analyze │ → │ Plan │ → │ Apply within │ │
│ │ Threat │ │ Response │ │ Budget │ │
│ └──────────┘ └──────────┘ └──────────────────┘ │
│ │
│ Output: DirectorPlan { spawns, traps, terrain } │
└────────────────────────────────────────────────────────┘
Telegraph System
Telegraphs provide fair warning to players before powerful attacks:
#![allow(unused)]
fn main() {
if !directive.telegraphs.is_empty() {
for telegraph in &directive.telegraphs {
// Display visual/audio warning
// e.g., "Thread gathering at position (5, 3)"
}
}
}
Telegraph types:
- Thread gathering: Fate-weaving attack incoming
- Ground tremor: Area attack warning
- Shadow forming: Minion spawn location
- Pattern shift: Phase transition imminent
Key Concepts
Budget-Constrained AI
Unlike traditional boss AI that can spam abilities, the Director operates under resource constraints. This creates:
- Strategic decisions: Trade-offs between aggression and defense
- Fair encounters: Players can anticipate resource depletion
- Dynamic difficulty: Budget recharges over time
Phase-Based Behavior
The boss doesn’t just cycle through attacks. Phases emerge from:
- HP thresholds: Different behavior at 75%, 50%, 25% HP
- Player behavior: Adapts to aggressive vs defensive playstyles
- Companion effectiveness: Responds to ally threat level
Related Examples
- Hello Companion - Basic AI perception and planning
- Fluids Demo - Physics simulation
- Physics Demo - Rapier3D integration
Troubleshooting
Missing veilweaver_slice feature
The advanced Warden Director requires the Veilweaver game data:
cargo run -p adaptive_boss --release --features veilweaver_slice
Empty plan output
If the Director returns an empty plan, the budget may already be exhausted or the boss doesn’t see any targets.
Source Location
- Example:
examples/adaptive_boss/src/main.rs - Director:
astraweave-director/src/lib.rs - Warden:
astraweave-director/src/oathbound_warden.rs(feature-gated)
Physics Demo Walkthrough
The physics_demo3d example showcases AstraWeave’s Rapier3D-based physics system with character controllers, destructible objects, water buoyancy, wind forces, and real-time rendering.
Running the Example
cargo run -p physics_demo3d --release
What It Demonstrates
- Character controller: Physics-driven movement with climb detection
- Destructible objects: Boxes that break under force
- Water simulation: Buoyancy and drag in water volumes
- Wind forces: Environmental wind affecting dynamic objects
- Collision layers: Filtering which objects collide
- Real-time rendering: wgpu-based visualization
Controls
Camera
| Key | Action |
|---|---|
W/A/S/D | Move camera |
Space | Camera up |
Shift | Camera down |
| Right-click + drag | Look around |
Esc | Exit |
Character
| Key | Action |
|---|---|
I/J/K/L | Move character (forward/left/back/right) |
C | Attempt climb (hold near climbable surface) |
Physics
| Key | Action |
|---|---|
F | Spawn dynamic box at (0, 4, 0) |
N | Spawn destructible box |
M | Break last destructible |
B | Spawn ragdoll placeholder |
T | Toggle wind on/off |
G | Toggle water volume on/off |
Expected Behavior
When you run the demo:
- Initial scene: Ground plane, wall, character (green), water volume, destructible box
- Press
J/L: Character slides left/right with physics - Press
F: Box drops and bounces - Press
T: Wind pushes boxes in the wind direction - Press
G: Water volume toggles, affecting buoyancy - Press
M: Destructible box shatters into fragments
Code Walkthrough
1. Physics World Setup
#![allow(unused)]
fn main() {
let mut phys = PhysicsWorld::new(vec3(0.0, -9.81, 0.0));
let _ground = phys.create_ground_plane(vec3(100.0, 0.0, 100.0), 1.0);
}
- Creates a physics world with Earth gravity (9.81 m/s²)
- Adds a ground plane at Y=0
2. Static Geometry
#![allow(unused)]
fn main() {
let _wall = phys.add_static_trimesh(
&[
vec3(5.0, 0.0, 0.0),
vec3(5.0, 3.0, 0.0),
// ... vertices
],
&[[0, 1, 2], [3, 2, 1]], // Triangle indices
Layers::CHARACTER | Layers::DEFAULT,
);
}
Static meshes (walls, floors) don’t move but participate in collisions. The Layers flags control which objects can collide.
3. Character Controller
#![allow(unused)]
fn main() {
let char_id = phys.add_character(
vec3(-2.0, 1.0, 0.0), // Start position
vec3(0.4, 0.9, 0.4), // Capsule half-extents
);
}
The character controller:
- Handles walking on slopes (up to 50° by default)
- Provides ground detection and step climbing
- Prevents tunneling through walls
4. Character Movement
#![allow(unused)]
fn main() {
// In the game loop:
let desired = vec3(self.move_dir.x, 0.0, self.move_dir.z);
self.phys.control_character(self.char_id, desired, dt, self.climb_try);
}
The controller converts desired velocity into physics-respecting movement.
5. Destructible Objects
#![allow(unused)]
fn main() {
let id = phys.add_destructible_box(
vec3(-1.0, 1.0, 2.0), // Position
vec3(0.4, 0.4, 0.4), // Half-size
3.0, // Mass (kg)
50.0, // Break force threshold
12.0, // Fragment mass
);
}
When break force is exceeded:
#![allow(unused)]
fn main() {
self.phys.break_destructible(id); // Shatters into fragments
}
6. Water Volume
#![allow(unused)]
fn main() {
phys.add_water_aabb(
vec3(-2.0, 0.0, -2.0), // Min corner
vec3(2.0, 1.2, 2.0), // Max corner
1000.0, // Water density (kg/m³)
0.8, // Drag coefficient
);
}
Objects inside the volume experience:
- Buoyancy: Upward force proportional to submerged volume
- Drag: Velocity-dependent resistance
7. Wind Forces
#![allow(unused)]
fn main() {
self.phys.set_wind(
vec3(1.0, 0.0, 0.2).normalize(), // Direction
8.0, // Strength
);
}
Wind applies continuous force to dynamic bodies.
8. Rendering Loop
#![allow(unused)]
fn main() {
fn about_to_wait(&mut self, _event_loop: &ActiveEventLoop) {
// Update physics
self.phys.step();
// Sync render instances from physics transforms
self.instances.clear();
for (handle, _body) in self.phys.bodies.iter() {
if let Some(m) = self.phys.body_transform(id) {
self.instances.push(Instance { transform: m, ... });
}
}
// Render
renderer.update_instances(&self.instances);
renderer.render()?;
}
}
Physics Architecture
┌─────────────────────────────────────────────────────────┐
│ PhysicsWorld │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Rigid Bodies│ │ Colliders │ │ Constraints │ │
│ │ (dynamic) │ │ (shapes) │ │ (joints) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Character │ │ Water │ │ Wind/Forces │ │
│ │ Controller │ │ Volumes │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ │
│ Backend: Rapier3D 0.22 │
└─────────────────────────────────────────────────────────┘
Collision Layers
AstraWeave uses bitflag layers to control collisions:
#![allow(unused)]
fn main() {
pub struct Layers: u32 {
const DEFAULT = 0b0001; // General objects
const CHARACTER = 0b0010; // Player/NPC capsules
const PROJECTILE= 0b0100; // Bullets, arrows
const TRIGGER = 0b1000; // Non-solid triggers
}
}
Objects only collide if their layer masks overlap.
Performance Notes
The physics demo runs at 60 FPS with:
- ~50 dynamic bodies
- Character controller
- Water volume intersection tests
- Wind force application
For larger simulations, consider:
- Reducing simulation substeps
- Using simpler collision shapes
- Spatial partitioning for many bodies
Related Examples
- Fluids Demo - Particle-based fluid simulation
- Navmesh Demo - Pathfinding on terrain
- Unified Showcase - Rendering pipeline
Troubleshooting
Character falls through floor
Ensure the ground plane is created before spawning the character.
Objects don’t collide
Check that collision layers overlap between the objects.
Low framerate
Reduce the number of dynamic bodies or increase the physics timestep.
Source Location
- Example:
examples/physics_demo3d/src/main.rs(301 lines) - Physics:
astraweave-physics/src/lib.rs - Character Controller:
astraweave-physics/src/character_controller.rs
Navmesh Demo Walkthrough
The navmesh_demo example demonstrates AstraWeave’s navigation mesh system - baking walkable surfaces from geometry and finding optimal paths with slope constraints.
Running the Example
cargo run -p navmesh_demo --release
What It Demonstrates
- NavMesh baking: Converting triangle soup into walkable navigation data
- Slope filtering: Excluding steep surfaces from navigation
- A pathfinding*: Finding optimal routes across the mesh
- Path visualization: Rendering waypoints in 3D space
Controls
| Key | Action |
|---|---|
W/A/S/D | Move camera |
Space | Camera up |
Shift | Camera down |
| Right-click + drag | Look around |
Esc | Exit |
Expected Behavior
When you run the demo:
- Yellow spheres: Triangle centers of the navigation mesh
- Green spheres: Path waypoints from start to goal
- Scene geometry: Flat area + ramp + elevated plateau
The path flows from bottom-left (-3.5, 0, -3.5) up the ramp to the plateau (11.5, 0.8, 0).
Code Walkthrough
1. Define Walkable Geometry
#![allow(unused)]
fn main() {
let tris = vec![
// Main floor (two triangles forming a quad)
tri(vec3(-4.0, 0.0, -4.0), vec3(4.0, 0.0, -4.0), vec3(4.0, 0.0, 4.0)),
tri(vec3(-4.0, 0.0, -4.0), vec3(4.0, 0.0, 4.0), vec3(-4.0, 0.0, 4.0)),
// Ramp going up (two triangles)
tri(vec3(4.0, 0.0, -1.0), vec3(8.0, 0.8, -1.0), vec3(8.0, 0.8, 1.0)),
tri(vec3(4.0, 0.0, -1.0), vec3(8.0, 0.8, 1.0), vec3(4.0, 0.0, 1.0)),
// Elevated plateau (two triangles)
tri(vec3(8.0, 0.8, -1.0), vec3(12.0, 0.8, -1.0), vec3(12.0, 0.8, 1.0)),
tri(vec3(8.0, 0.8, -1.0), vec3(12.0, 0.8, 1.0), vec3(8.0, 0.8, 1.0)),
];
}
The geometry describes:
- Floor: 8×8 meter flat area at Y=0
- Ramp: Inclined surface from Y=0 to Y=0.8
- Plateau: Elevated area at Y=0.8
2. Bake the NavMesh
#![allow(unused)]
fn main() {
let nav = NavMesh::bake(
&tris, // Triangle geometry
0.4, // Agent radius (meters)
50.0, // Max walkable slope (degrees)
);
}
The baking process:
- Voxelizes triangles into a 3D grid
- Filters steep surfaces (>50° rejected)
- Shrinks walkable area by agent radius (0.4m)
- Generates navigation polygon data
3. Find a Path
#![allow(unused)]
fn main() {
let start = vec3(-3.5, 0.0, -3.5); // Bottom-left of floor
let goal = vec3(11.5, 0.8, 0.0); // Center of plateau
let path = nav.find_path(start, goal);
}
The find_path function:
- Projects start/goal onto nearest navmesh triangle
- Runs A* across triangle adjacency graph
- Returns waypoint list (
Vec<Vec3>)
4. Visualize Results
#![allow(unused)]
fn main() {
// Show triangle centers (yellow)
for t in &nav.tris {
instances.push(Instance::from_pos_scale_color(
t.center + vec3(0.0, 0.05, 0.0), // Slightly above surface
vec3(0.1, 0.1, 0.1), // Small sphere
[0.7, 0.7, 0.3, 1.0], // Yellow
));
}
// Show path waypoints (green)
for p in &path {
instances.push(Instance::from_pos_scale_color(
*p + vec3(0.0, 0.08, 0.0),
vec3(0.12, 0.12, 0.12),
[0.2, 1.0, 0.4, 1.0], // Green
));
}
}
NavMesh Architecture
┌─────────────────────────────────────────────────────────┐
│ NavMesh │
├─────────────────────────────────────────────────────────┤
│ Input: Vec<Triangle> │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Voxelization │→ │ Slope Filter │→ │ Region Build │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ Output: tris: Vec<NavTri>, adjacency: HashMap │
├─────────────────────────────────────────────────────────┤
│ find_path(start, goal) → Vec\<Vec3\> │
│ ├─ Project points to mesh │
│ ├─ A* search on triangle graph │
│ └─ String-pull path smoothing │
└─────────────────────────────────────────────────────────┘
Slope Filtering
The max_slope parameter (50° in this example) determines walkability:
| Surface Angle | Walkable? (50° max) |
|---|---|
| 0° (flat) | ✅ Yes |
| 30° (gentle) | ✅ Yes |
| 50° (steep) | ✅ Yes (boundary) |
| 60° (cliff) | ❌ No |
| 90° (wall) | ❌ No |
Steep surfaces are excluded from the navigation graph.
Agent Radius
The agent_radius (0.4m) shrinks walkable areas:
Before shrinking: After shrinking:
┌────────────────┐ ┌────────────────┐
│ │ │ ┌────────┐ │
│ Walkable │ → │ │Walkable│ │
│ │ │ └────────┘ │
└────────────────┘ └────────────────┘
↑ 0.4m margin
This prevents agents from clipping through walls.
Key Types
Triangle
#![allow(unused)]
fn main() {
pub struct Triangle {
pub a: Vec3, // First vertex
pub b: Vec3, // Second vertex
pub c: Vec3, // Third vertex
}
}
NavTri (Internal)
#![allow(unused)]
fn main() {
pub struct NavTri {
pub center: Vec3, // Triangle centroid
pub normal: Vec3, // Surface normal
pub neighbors: Vec<usize>, // Adjacent triangle indices
}
}
Performance Notes
NavMesh baking is an offline process:
- Bake time: ~10ms for simple geometry, seconds for complex levels
- Runtime pathfinding: <1ms for typical paths
- Memory: ~100 bytes per navigation triangle
For large worlds, consider:
- Pre-baking navmeshes at build time
- Hierarchical navigation (coarse + fine meshes)
- Path caching for frequently-used routes
Related Examples
- Physics Demo - Rapier3D integration
- Hello Companion - AI using navigation
- Unified Showcase - Rendering pipeline
Troubleshooting
Path returns empty
- Check that start/goal are near the navmesh surface
- Verify the mesh is connected (no isolated islands)
Agent walks through walls
- Increase
agent_radiusparameter - Ensure wall geometry is included in navmesh input
Steep ramps not walkable
- Increase
max_slopeangle (up to 89°)
Source Location
- Example:
examples/navmesh_demo/src/main.rs(196 lines) - NavMesh:
astraweave-nav/src/navmesh.rs - Pathfinding:
astraweave-nav/src/pathfinder.rs
Audio Spatial Demo Walkthrough
The audio_spatial_demo example demonstrates AstraWeave’s 3D spatial audio system - positional sound effects, music playback with crossfading, and listener tracking tied to the camera.
Running the Example
cargo run -p audio_spatial_demo --release
Note: This demo requires audio files in
assets/audio/. If files are missing, you’ll see error messages but the demo will still run for visualization.
What It Demonstrates
- 3D positional audio: Sounds panning based on listener position
- Procedural beeps: Generated tones at specified frequencies
- Music playback: Looped background music
- Crossfading: Smooth transitions between music tracks
- Listener tracking: Audio follows camera position/orientation
Controls
| Key | Action |
|---|---|
W/A/S/D | Move camera (and listener) |
Space | Camera up |
Shift | Camera down |
| Right-click + drag | Look around |
1 | Play beep at center (880 Hz) |
2 | Play beep at left (-3, 1, 0) at 660 Hz |
3 | Play beep at right (+3, 1, 0) at 440 Hz |
M | Switch music track (crossfade) |
Esc | Exit |
Expected Behavior
- Launch: Background music plays (if
assets/audio/bgm.oggexists) - Press 1: High-pitched beep plays from center
- Press 2: Medium-pitched beep plays from left (pans left in headphones)
- Press 3: Low-pitched beep plays from right (pans right)
- Move camera with W/A/S/D: Audio panning shifts as listener moves
- Press M: Music crossfades to alternate track
Code Walkthrough
1. Audio Engine Setup
#![allow(unused)]
fn main() {
let mut audio = AudioEngine::new()?;
audio.set_master_volume(1.0);
// Try to play looped background music
let _ = audio.play_music(
MusicTrack {
path: "assets/audio/bgm.ogg".into(),
looped: true,
},
1.0, // Volume
);
}
The AudioEngine:
- Initializes the audio device (rodio backend)
- Manages multiple simultaneous sounds
- Controls master/music/SFX volume buses
2. 3D Positional Beeps
#![allow(unused)]
fn main() {
// Center beep (880 Hz, 0.25s duration, 0.5 volume)
self.audio.play_sfx_3d_beep(
100, // Emitter ID
vec3(0.0, 1.0, 0.0), // Position (center)
880.0, // Frequency (Hz)
0.25, // Duration (seconds)
0.5, // Volume
);
// Left beep (660 Hz)
self.audio.play_sfx_3d_beep(
101,
vec3(-3.0, 1.0, 0.0), // Left of center
660.0,
0.25,
0.5,
);
// Right beep (440 Hz)
self.audio.play_sfx_3d_beep(
102,
vec3(3.0, 1.0, 0.0), // Right of center
440.0,
0.25,
0.5,
);
}
The emitter ID allows tracking/stopping specific sounds.
3. Music Crossfading
#![allow(unused)]
fn main() {
self.audio.play_music(
MusicTrack {
path: "assets/audio/bgm_alt.ogg".into(),
looped: true,
},
1.25, // Crossfade duration (seconds)
);
}
When switching tracks:
- Current music fades out over 1.25 seconds
- New music fades in simultaneously
- No audio gap or jarring transition
4. Listener Updates
#![allow(unused)]
fn main() {
// Calculate forward direction from camera yaw/pitch
let forward = glam::Quat::from_euler(
glam::EulerRot::YXZ,
self.camera.yaw,
self.camera.pitch,
0.0,
) * vec3(0.0, 0.0, -1.0);
// Update audio listener pose
self.audio.update_listener(ListenerPose {
position: self.camera.position,
forward,
up: vec3(0.0, 1.0, 0.0),
});
// Advance audio processing
self.audio.tick(dt);
}
The listener pose determines:
- Position: Where sounds are heard from
- Forward: Which direction is “front”
- Up: Which direction is “up” (for proper panning)
Audio Architecture
┌─────────────────────────────────────────────────────────┐
│ AudioEngine │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────┐ │
│ │ Music Bus │ │ SFX Bus │ │ Voice Bus │ │
│ │ (looped) │ │ (one-shot) │ │ (dialogue) │ │
│ └─────────────┘ └─────────────┘ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ Spatial Mixer │ ← ListenerPose │
│ │ (3D panning) │ │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ Master Volume │ │
│ └─────────────────┘ │
│ ↓ │
│ ┌─────────────────┐ │
│ │ rodio Output │ │
│ └─────────────────┘ │
│ │
│ Backend: rodio 0.17 │
└─────────────────────────────────────────────────────────┘
ListenerPose
#![allow(unused)]
fn main() {
pub struct ListenerPose {
pub position: Vec3, // World-space position
pub forward: Vec3, // Look direction (normalized)
pub up: Vec3, // Up vector (typically Y-up)
}
}
The audio system uses this to compute:
- Attenuation: Sounds get quieter with distance
- Panning: Left/right based on angle to listener
- Doppler (optional): Pitch shift for moving sources
Volume Buses
#![allow(unused)]
fn main() {
// Individual bus control
audio.set_master_volume(1.0); // Overall volume
audio.set_music_volume(0.8); // Background music
audio.set_sfx_volume(1.0); // Sound effects
audio.set_voice_volume(1.0); // Dialogue/narration
}
Supported Formats
AstraWeave audio supports:
- OGG Vorbis (recommended for music)
- WAV (uncompressed, good for short SFX)
- MP3 (widely compatible)
- FLAC (lossless, large files)
Performance Notes
- Simultaneous sounds: Up to 64 by default
- Streaming: Large files (>1MB) stream from disk
- CPU usage: <1% for typical game audio
- Memory: ~10KB per short sound, streaming for long tracks
Related Examples
- Physics Demo - 3D environment
- Unified Showcase - Visual rendering
- Hello Companion - AI with audio cues
Troubleshooting
No audio output
- Check system audio settings
- Verify audio device is connected
- Try different audio files
Crackling/popping
- Increase audio buffer size
- Reduce simultaneous sound count
- Check for CPU throttling
Sounds don’t pan correctly
- Verify
update_listener()is called each frame - Check that forward/up vectors are normalized
- Ensure sound positions are in world space
Missing audio files
The demo will print errors if assets/audio/bgm.ogg is missing. Create the directory and add audio files, or the demo will run silently.
Source Location
- Example:
examples/audio_spatial_demo/src/main.rs(215 lines) - Audio Engine:
astraweave-audio/src/engine.rs - Spatial Mixer:
astraweave-audio/src/spatial.rs
Fluids Demo
Interactive demonstration of AstraWeave’s PCISPH-based fluid simulation system with real-time rendering, multiple scenarios, and egui-powered controls.
cargo run -p fluids_demo --release
Overview
The fluids demo showcases:
- Real-time particle-based fluid simulation using PCISPH solver
- Multiple simulation scenarios (laboratory, ocean, waterfall, splash)
- Interactive controls for spawning particles and adjusting parameters
- Advanced rendering with caustics, foam, and refraction effects
- LOD optimization for performance scaling
Quick Start
# Run the demo
cargo run -p fluids_demo --release
# Controls:
# WASD - Move camera
# Mouse - Rotate camera
# LMB - Spawn particles at cursor
# RMB - Apply force to particles
# Tab - Switch scenario
# F3 - Toggle debug panel
Architecture
graph TD
A[FluidSystem] --> B[PCISPH Solver]
A --> C[FluidRenderer]
A --> D[ScenarioManager]
B --> E[Particle Grid]
B --> F[Pressure Solve]
B --> G[Viscosity]
C --> H[GPU Rendering]
C --> I[Caustics]
C --> J[Foam Effects]
D --> K[Laboratory]
D --> L[Ocean]
D --> M[Waterfall]
D --> N[Splash]
Scenarios
Laboratory Scenario
Controlled environment for testing fluid behavior:
- Bounded container simulation
- Particle spawning controls
- Parameter visualization
- Ideal for learning system behavior
Ocean Scenario
Large-scale ocean wave simulation:
- Skybox environment (HDR-based)
- Wave dynamics
- Foam generation
- Infinite horizon effect
Waterfall Scenario
Vertical fluid flow demonstration:
- Gravity-driven particle flow
- Splash effects at base
- Continuous particle recycling
Splash Scenario
High-energy impact simulation:
- Droplet spawning
- Surface tension effects
- Spray particle generation
Configuration
Fluid System Parameters
The demo exposes all major fluid parameters through the debug panel:
#![allow(unused)]
fn main() {
// From the demo - actual parameter configuration
fluid_system.smoothing_radius = 0.5; // SPH kernel radius
fluid_system.target_density = 1.0; // Rest density
fluid_system.pressure_multiplier = 100.0; // Stiffness
fluid_system.viscosity = 0.01; // Fluid thickness
fluid_system.gravity = -9.81; // Gravity strength
fluid_system.cell_size = 1.2; // Spatial hash cell size
fluid_system.grid_width = 64; // Grid dimensions
fluid_system.grid_height = 64;
fluid_system.grid_depth = 64;
}
Quality Presets
The demo includes 4 quality levels (selectable in UI):
| Preset | Particles | Cell Size | Target FPS |
|---|---|---|---|
| Low | 5,000 | 2.0 | 60+ |
| Medium | 10,000 | 1.5 | 60 |
| High | 20,000 | 1.2 | 45-60 |
| Ultra | 50,000 | 1.0 | 30-45 |
Interactive Features
Particle Spawning
Left-click in the viewport to spawn particles:
#![allow(unused)]
fn main() {
// Demo implementation - ray-plane intersection for spawn position
fn spawn_particles_at_cursor(&mut self) {
let (origin, dir) = self.screen_to_world_ray(
self.mouse_pos[0],
self.mouse_pos[1]
);
// Intersect with Y=5 plane (fluid center height)
if let Some(hit_pos) = self.ray_plane_intersection(origin, dir, 5.0) {
let positions: Vec<[f32; 3]> = (0..count)
.map(|i| {
let angle = (i as f32 / count as f32) * TAU;
let radius = (i as f32 * 0.1).sin() * 0.5;
[hit_pos.x + angle.cos() * radius,
hit_pos.y + 0.5,
hit_pos.z + angle.sin() * radius]
})
.collect();
self.fluid_system.spawn_particles(&queue, &positions, &velocities, colors);
}
}
}
Force Application
Right-click and drag to apply forces:
- Drag direction determines force vector
- Force magnitude scales with drag distance
- Affects all particles in a configurable radius
Debug Panel
Press F3 to toggle the debug panel (egui-based):
- Real-time particle count
- FPS / frame time graph
- Simulation step timing
- LOD level indicator
- Parameter sliders
LOD System
The demo uses FluidLodManager and FluidOptimizationController for dynamic quality scaling:
#![allow(unused)]
fn main() {
// LOD configuration from demo
let lod_config = FluidLodConfig {
target_fps: 60.0,
min_particles: 1000,
max_particles: 50000,
lod_levels: vec![
LodLevel { distance: 10.0, particle_scale: 1.0 },
LodLevel { distance: 25.0, particle_scale: 0.5 },
LodLevel { distance: 50.0, particle_scale: 0.25 },
],
};
// Controller maintains target FPS
optimization_controller.update(last_frame_time_ms);
let suggested_count = optimization_controller.suggested_particle_count();
}
Rendering Pipeline
GPU-Accelerated Rendering
The demo uses wgpu 25 for all rendering:
#![allow(unused)]
fn main() {
// Render pass structure
let fluid_renderer = FluidRenderer::new(&device, width, height, format);
// Each frame:
// 1. Render skybox to scene texture (if ocean scenario)
// 2. Render fluid particles with refraction
// 3. Apply caustics and foam effects
// 4. Composite to swapchain
// 5. Render egui overlay
}
Shader Features
The demo includes multiple WGSL shaders:
fluid.wgsl- Main particle renderingocean.wgsl- Ocean surface simulationskybox.wgsl- HDR environment mappingglass.wgsl- Refraction effects
Performance Tips
Achieving 60 FPS
- Reduce particle count - Start with Medium preset
- Increase cell size - Fewer neighbor lookups
- Use LOD - Enable optimization controller
- Disable foam - Reduces particle overhead
Profiling
Enable Tracy profiling:
cargo run -p fluids_demo --release --features profiling
Key spans to watch:
fluid_system.step()- Physics simulationfluid_renderer.render()- GPU draw callsegui.render()- UI overhead
Dependencies
[dependencies]
astraweave-physics = { path = "../../astraweave-physics" }
astraweave-fluids = { path = "../../astraweave-fluids" }
winit = { workspace = true }
wgpu = { workspace = true }
glam = { workspace = true }
egui = { workspace = true }
egui-wgpu = { workspace = true }
egui-winit = { workspace = true }
Related Documentation
- Fluids System Guide - Core fluids architecture
- Fluids API Reference - Complete API documentation
- Performance Optimization - General optimization strategies
Troubleshooting
Low FPS
Symptom: Frame rate drops below 30 FPS
Solutions:
- Switch to Low quality preset
- Reduce
spawn_burst_sizein debug panel - Close other GPU-intensive applications
- Check GPU driver is up to date
No Particles Visible
Symptom: Scene renders but no fluid particles
Solutions:
- Check scenario is initialized (Tab to switch)
- Click LMB to spawn particles manually
- Verify camera position (reset with R key)
Skybox Not Loading
Symptom: Ocean scenario shows black sky
Cause: Missing HDR file at assets/hdri/polyhaven/kloppenheim_02_puresky_2k.hdr
Solution: Download from Poly Haven or disable skybox feature
wgpu Adapter Errors
Symptom: “No suitable adapter found”
Solutions:
- Update GPU drivers
- Try different backend:
WGPU_BACKEND=vulkan cargo run -p fluids_demo - Ensure system meets minimum requirements (DX12 or Vulkan 1.1 capable GPU)
Unified Showcase
A comprehensive rendering demonstration combining shadows, terrain, GLTF models, and skybox rendering in a self-contained wgpu application.
cargo run -p unified_showcase --release
Overview
The unified showcase demonstrates:
- Shadow mapping with 2048×2048 depth textures
- GLTF model loading with materials and transforms
- Procedural terrain with multi-texture blending
- HDR skybox rendering
- 4× MSAA antialiasing
- First-person camera controls
This is a standalone renderer example that doesn’t depend on engine render crates—ideal for learning wgpu patterns.
Quick Start
# Run the showcase
cargo run -p unified_showcase --release
# Controls:
# WASD - Move camera
# Mouse - Look around (click to capture)
# Escape - Release mouse
# Space - Move up
# Shift - Move down
Features
Shadow Mapping
Real-time shadow casting with PCF (Percentage Closer Filtering):
#![allow(unused)]
fn main() {
// Shadow texture configuration
let shadow_texture = device.create_texture(&wgpu::TextureDescriptor {
label: Some("Shadow Texture"),
size: wgpu::Extent3d {
width: 2048,
height: 2048,
depth_or_array_layers: 1,
},
format: wgpu::TextureFormat::Depth32Float,
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
...
});
// Comparison sampler for PCF
let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
compare: Some(wgpu::CompareFunction::Less),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
...
});
}
GLTF Model Loading
The demo includes a custom GLTF loader for scene objects:
#![allow(unused)]
fn main() {
// Load and render GLTF models
mod gltf_loader;
// Material indices for different model parts
pine_bark_mat: usize,
pine_leaves_mat: usize,
tower_wood_mat: usize,
tower_stone_mat: usize,
}
Supported GLTF features:
- Mesh geometry (positions, normals, UVs)
- Base color textures
- Transform hierarchies
- Multiple materials per model
Terrain Rendering
Procedural terrain with height-based texture blending:
- Grass texture - Low elevation areas
- Rock texture - Steep slopes
- Snow texture - High elevation peaks
- Normal mapping - Surface detail
- Triplanar projection - Seamless texturing on steep surfaces
// terrain.wgsl - height-based texture blending
fn blend_terrain_textures(height: f32, slope: f32) -> vec4<f32> {
let grass_weight = smoothstep(0.0, 0.3, 1.0 - slope);
let rock_weight = smoothstep(0.2, 0.5, slope);
let snow_weight = smoothstep(0.7, 1.0, height);
return grass * grass_weight + rock * rock_weight + snow * snow_weight;
}
Skybox
HDR-based environment rendering:
#![allow(unused)]
fn main() {
// Skybox pipeline - renders at depth 1.0 (far plane)
let sky_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
depth_stencil: Some(wgpu::DepthStencilState {
depth_write_enabled: false,
depth_compare: wgpu::CompareFunction::LessEqual,
...
}),
...
});
}
MSAA
4× multisampling for smooth edges:
#![allow(unused)]
fn main() {
multisample: wgpu::MultisampleState {
count: 4,
mask: !0,
alpha_to_coverage_enabled: false,
}
}
Architecture
graph TD
A[ShowcaseApp] --> B[Render Pipelines]
A --> C[Scene Data]
A --> D[Camera System]
B --> E[Main Pipeline]
B --> F[Sky Pipeline]
B --> G[Terrain Pipeline]
B --> H[Shadow Pipeline]
C --> I[Meshes]
C --> J[Materials]
C --> K[Scene Objects]
E --> L[MSAA Texture]
L --> M[Resolve to Swapchain]
Render Pass Order
- Shadow Pass - Render scene from light perspective
- Skybox Pass - Render environment (no depth write)
- Terrain Pass - Render ground with shadows
- Object Pass - Render GLTF models with shadows
- MSAA Resolve - Resolve 4× samples to swapchain
Pipeline Details
Main Render Pipeline
#![allow(unused)]
fn main() {
let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
layout: Some(&render_pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Vertex::desc()],
},
fragment: Some(wgpu::FragmentState {
module: &shader,
entry_point: Some("fs_main"),
targets: &[Some(wgpu::ColorTargetState {
format: config.format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
}),
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleList,
front_face: wgpu::FrontFace::Ccw,
cull_mode: Some(wgpu::Face::Back),
},
depth_stencil: Some(wgpu::DepthStencilState {
format: wgpu::TextureFormat::Depth32Float,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
}),
multisample: wgpu::MultisampleState { count: 4, ... },
});
}
Bind Group Layouts
The showcase uses 4 main bind group layouts:
| Slot | Name | Contents |
|---|---|---|
| 0 | Camera | View-projection matrix, camera position |
| 1 | Light | Light view-projection, position, color, shadow map |
| 2 | Material | Albedo texture, sampler |
| 3 | Model | Per-object transform matrix |
Vertex Format
#![allow(unused)]
fn main() {
#[repr(C)]
struct Vertex {
position: [f32; 3],
normal: [f32; 3],
uv: [f32; 2],
color: [f32; 4],
tangent: [f32; 4],
}
}
Shaders
Main Shader (shader_v2.wgsl)
@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
var out: VertexOutput;
let world_pos = model.model * vec4<f32>(in.position, 1.0);
out.clip_position = camera.view_proj * world_pos;
out.world_position = world_pos.xyz;
out.world_normal = normalize((model.model * vec4<f32>(in.normal, 0.0)).xyz);
out.uv = in.uv;
out.shadow_position = light.view_proj * world_pos;
return out;
}
@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
let shadow = calculate_shadow(in.shadow_position);
let albedo = textureSample(t_diffuse, s_diffuse, in.uv);
let lighting = calculate_lighting(in.world_normal, light.position, shadow);
return vec4<f32>(albedo.rgb * lighting, albedo.a);
}
Shadow Shader
Minimal shader for depth-only rendering:
@vertex
fn vs_main(in: VertexInput) -> @builtin(position) vec4<f32> {
return light.view_proj * model.model * vec4<f32>(in.position, 1.0);
}
Dependencies
[dependencies]
winit = { workspace = true }
wgpu = { workspace = true }
glam = { workspace = true }
pollster = { workspace = true }
bytemuck = { workspace = true }
image = { version = "0.25", features = ["png", "jpeg", "hdr", "webp"] }
gltf = { version = "1.4", features = ["utils", "KHR_materials_unlit"] }
Note: This example is self-contained and doesn’t use astraweave-render crates. It’s designed as a clean reference implementation.
Asset Requirements
The demo expects assets in the following locations:
assets/
├── models/
│ ├── pine_tree.glb
│ └── watch_tower.glb
├── textures/
│ ├── grass_albedo.png
│ ├── rock_albedo.png
│ └── snow_albedo.png
└── hdri/
└── polyhaven/
└── kloppenheim_02_puresky_2k.hdr
Performance
Typical performance on mid-range hardware:
| Metric | Value |
|---|---|
| Shadow Resolution | 2048×2048 |
| MSAA | 4× |
| Draw Calls | 10-50 (scene dependent) |
| Frame Time | 8-12 ms (60-80+ FPS) |
| VRAM | ~200-500 MB |
Optimization Opportunities
For production use, consider:
- Cascaded Shadow Maps - Multiple shadow cascades for large scenes
- Instanced Rendering - Batch similar objects
- Frustum Culling - Skip off-screen objects
- LOD System - Distance-based mesh simplification
Learning Value
This example teaches:
- wgpu Pipeline Creation - Complete setup from device to swapchain
- Bind Group Management - Proper resource binding patterns
- Shadow Mapping - Light-space rendering and PCF sampling
- GLTF Integration - Loading and rendering real 3D assets
- Multi-Pass Rendering - Coordinating multiple render passes
- MSAA Setup - Multisampling with resolve
Related Documentation
- Rendering System Guide - Engine rendering architecture
- Render API Reference - Complete rendering API
- Performance Optimization - General optimization
Troubleshooting
Black Screen
Symptom: Window opens but shows only black
Solutions:
- Check GPU supports wgpu backends (Vulkan/DX12/Metal)
- Update graphics drivers
- Enable logging:
RUST_LOG=info cargo run -p unified_showcase
Missing Textures
Symptom: Models render as solid colors
Cause: Asset files not found
Solution: Ensure assets/ directory contains required textures
Slow Performance
Symptom: FPS below 30
Solutions:
- Reduce shadow map resolution (edit source)
- Disable MSAA (change count to 1)
- Close other GPU applications
- Try different wgpu backend:
WGPU_BACKEND=dx12
GLTF Load Errors
Symptom: Panic on startup with GLTF error
Solutions:
- Verify GLTF files exist in
assets/models/ - Check GLTF files are valid (test in Blender)
- Ensure GLTF uses supported features only
Troubleshooting Examples
This guide helps resolve common issues when running AstraWeave examples.
Working Examples
The following examples are verified to build and run:
| Example | Command | Notes |
|---|---|---|
| hello_companion | cargo run -p hello_companion --release | Expected panic (demonstrates tool validation) |
| fluids_demo | cargo run -p fluids_demo --release | Interactive PCISPH fluid simulation |
| unified_showcase | cargo run -p unified_showcase --release | wgpu rendering with shadows, terrain |
| physics_demo3d | cargo run -p physics_demo3d --release | Rapier3D physics with character controller |
| navmesh_demo | cargo run -p navmesh_demo --release | NavMesh baking and A* pathfinding |
| audio_spatial_demo | cargo run -p audio_spatial_demo --release | 3D spatial audio |
| adaptive_boss | cargo run -p adaptive_boss --release | Director AI for boss encounters |
| ipc_loopback | cargo run -p ipc_loopback --release | IPC communication test |
Known Compilation Issues
Some examples have compilation issues due to API evolution:
| Example | Issue | Status |
|---|---|---|
| debug_overlay | egui 0.32 API changes | Needs update |
| ui_controls_demo | egui/winit version mismatches | Needs update |
| visual_3d | winit 0.30 migration | Needs update |
| rhai_authoring | Rhai sync/send traits | Needs refactor |
| npc_town_demo | Multiple API mismatches | Needs update |
Build Aliases
Use these workspace aliases for safer builds:
cargo build-core
cargo check-all
cargo clippy-all
Common Errors
“No suitable graphics adapter”
thread 'main' panicked at 'No suitable graphics adapter found'
Solution: Update graphics drivers or use software rendering:
export WGPU_BACKEND=gl
cargo run -p hello_companion
“AI model not found”
Error: Could not load AI model
Solution: Examples use mock AI by default. This panic is expected when no LLM is configured.
Vulkan Not Found (Linux)
sudo apt-get install mesa-vulkan-drivers vulkan-tools
Missing Build Tools (Windows)
Install Visual Studio with C++ build tools.
Getting Help
Benchmark Dashboard
Performance Benchmarks
All benchmarks executed via Criterion.rs statistical benchmarking. Results sourced from the Master Benchmark Report v5.55 (January 2026). Every measurement can be reproduced with documented commands.
Key validation results — January 2026
Engine has 85% headroom at p99 for 1,000 entities at 60 FPS. All core systems operate well under their per-frame budgets.
| System | Benchmark | Target | Actual | Margin |
|---|---|---|---|---|
| ECS | Entity spawn (100) | <50µs | 15.0µs | 70% under |
| ECS | Entity spawn (1000) | <500µs | 106.7µs | 79% under |
| AI | GOAP planning (full) | <10µs | 286ns | 97% under |
| AI | GOAP planning (cache) | <1µs | 9.8ns | 99% under |
| Frame | p50 @ 1k entities | <16.67ms | 1.27ms | 92% under |
| Frame | p99 @ 1k entities | <16.67ms | 2.42ms | 85% under |
Operations achieving sub-nanosecond and sub-10ns latency.
These benchmarks represent AstraWeave's highest-performing operations, many achieving sub-nanosecond latency.
Operations under 1 ns
| Operation | Latency | Throughput |
|---|---|---|
| Multi-Agent Per-Agent | 12-20 ps | 50-83 trillion/sec |
| Nav Sliver Triangles | 99-104 ps | 10 billion/sec |
| Multi-Agent Per-Plan | 0.29-0.31 ns | 3.2-3.4 billion/sec |
| Pan Mode Switching | 418 ps | — |
| State Transitions | 0.49-0.51 ns | — |
| Emotion Blending | 0.55 ns | — |
| Multi-Agent Feedback | 0.73-0.76 ns | 1.3 billion/sec |
| MSAA Resize 720p | 582-645 ps | — |
| UI Settings Nav | 696 ps | — |
| Clear Frame | 0.72 ns | — |
| Weather Attenuation | 730-783 ps | 22.8 billion/frame |
| Room Overlap Check | 571-629 ps | — |
| Frustum AABB Inside | 889-915 ps | — |
| GPU Budget Check | 890 ps-1.05 ns | 17 billion/frame |
Operations under 10 ns
| Operation | Latency | Notes |
|---|---|---|
| SparseSet Lookup (1k) | 1.56 ns | 37× faster than BTreeMap |
| SIMD Movement | 1.73 ns | 2.26× faster than naive |
| Quat Multiply | 1.34 ns | glam SIMD-optimized |
| Quat Slerp | 2.10 ns | Rotation interpolation |
| Context Switching | 2.38 ns | 7M switches/frame |
| GOAP (no enemies) | 3.46-3.56 ns | Idle detection FREE |
| Component Deserialize | 3.50 ns | Postcard ECS |
| Physics Stage | 3.63 ns | 7,580× vs perception |
| RAG Engine Creation | 4.61 ns | Zero-cost abstraction |
| Mat4 Multiply | 4.28 ns | glam SIMD matrix |
| GOAP (close) | 4.68-5.11 ns | Tactical decision |
| GOAP (far) | 7.04-7.86 ns | Strategic decision |
| SparseSet Insert | 9.9 ns | 13× faster than BTreeMap |
Engine subsystem benchmarks
Each subsystem is benchmarked independently with Criterion.rs. Results include budget analysis against the 16.67ms frame budget.
Entity-Component-System
| Benchmark | Result | Budget |
|---|---|---|
| Spawn empty (10k) | 645µs | Excellent |
| Spawn + Position (10k) | 5.6ms | Production |
| Despawn empty (10k) | 287µs | Fixed |
| Despawn + comp (10k) | 2.5ms | 68% faster |
| Iteration (10k) | 273µs | Excellent |
| Archetype trans (10k) | 5.6ms | Within budget |
Planning and orchestration
| Benchmark | Result | Notes |
|---|---|---|
| GOAP (cache hit) | 9.8 ns | 99% under |
| GOAP (cache miss) | 286 ns | 97% under |
| GOAP next (idle) | 3.5 ns | 4.7B ops/frame |
| GOAP next (close) | 5.1 ns | 3.5B ops/frame |
| GOAP next (far) | 7.9 ns | 2.4B ops/frame |
| Multi-agent (10) | 1.34-1.39µs | 66-68% faster |
| Arbiter GOAP | 101.7 ns | 982× faster |
| Arbiter LLM | 575 ns | 86× faster |
| Mode transition | 221.9 ns | 45× faster |
Simulation and collision
| Benchmark | Result | Notes |
|---|---|---|
| Character move | 43.8-52.0 ns | 12-26% faster |
| Rigid body lookup | 14.8-15.4 ns | 10× vs character |
| Raycast (empty) | 26.3-31.5 ns | 8-23% faster |
| Rigid body batch | 47µs | Excellent |
| Spatial hash | 99.96% fewer | Grid optimization |
SPH simulation (A+ grade)
| Benchmark | Result | Notes |
|---|---|---|
| Particles (1K-10K) | 5.3-110µs | 100-322 Melem/s |
| Spatial hashing | 163µs-5.6ms | 38-62% improved |
| SPH kernels (100K) | 171-223µs | poly6/spiky |
| Density/pressure | 3.5-10.5ms | — |
| Sim step (1K) | 1.8-3.0ms | — |
| Multi-step | 450-500µs | 45-57% faster |
| GPU data prep | 0.9-2.6 ns | Sub-nanosecond |
wgpu pipeline benchmarks
| Category | Benchmark | Result |
|---|---|---|
| Culling | AABB inside | 889-915 ps |
| Culling | Contains point | 951 ps-1.01 ns |
| MSAA | Mode check | 795-842 ps |
| MSAA | Resize 720p | 582-645 ps |
| Camera | View matrix | 4.42-5.36 ns |
| Camera | Toggle mode | 1.72-2.29 ns |
| Instancing | Savings calc | 1.43-1.52 ns |
| Weather | Particle | 1.95-2.04 ns |
| Weather | Attenuation | 730-783 ps |
Interpolation and pathfinding
| Benchmark | Result | Notes |
|---|---|---|
| vec3_lerp | 1.69-1.83 ns | 57% faster |
| quat_to_rotation | 1.63-1.73 ns | 36% faster |
| Tween update | 22.1 ns | — |
| Spring update | 14.2 ns | 1.6× vs tween |
| Sliver triangles | 99-104 ps | Sub-nanosecond |
| Impossible paths | 3.7-24.9µs | Fast-fail |
| Maze stress | 1.6-108µs | — |
| Pathfind short | 7.5µs | Excellent |
Target: 60 FPS = 16.67ms per frame
Budget breakdown at 1,000 entities shows the engine using only 14.5% of the available frame time at p99, leaving 85% headroom for gameplay logic and rendering.
| System | Time | Budget % | Status |
|---|---|---|---|
| ECS Core | 85 µs | 0.51% | ✅ |
| AI (500 agents) | 471 µs | 2.83% | ✅ |
| Physics (100 bodies) | 47 µs | 0.28% | ✅ |
| Core loop (5k) | 529 µs | 3.17% | ✅ |
| p50 Total | 1.27 ms | 7.6% | ✅ |
| p99 Total | 2.42 ms | 14.5% | ✅ |
| Headroom | 14.25 ms | 85% | ✅ |
| Entity Count | p99 Estimate | Feasibility |
|---|---|---|
| 1,000 | 2.42 ms | ✅ 85% headroom |
| 5,000 | ~8-10 ms | ✅ 40-50% headroom |
| 10,000 | ~15-18 ms | ⚠️ Near budget |
| 20,000+ | >30 ms | ❌ Requires 30 FPS |
ECS time at scale
| Entity Count | ECS Time | Budget Used |
|---|---|---|
| 1,000 | ~85µs | 0.51% |
| 5,000 | ~529µs | 3.17% |
| 10,000 | ~1ms | ~6% |
Running benchmarks
Every benchmark can be reproduced with a single command. Criterion.rs provides confidence intervals and statistical rigor.
Full suite
# Run all Criterion benchmarks
cargo bench --workspace
# Run with odyssey automation (captures logs)
./scripts/benchmark_odyssey.ps1 \
-OutDir benchmark_results/$(Get-Date -Format 'yyyy-MM-dd')
Per-crate benchmarks
# ECS benchmarks
cargo bench -p astraweave-ecs
# AI benchmarks
cargo bench -p astraweave-ai
# Physics benchmarks
cargo bench -p astraweave-physics
# Render benchmarks
cargo bench -p astraweave-render
Generating HTML reports
# Open Criterion HTML report
cargo bench -p astraweave-ecs -- --save-baseline main
# Reports at: target/criterion/*/report/index.html
Benchmarks as verification artifacts, not marketing numbers.
See also
- MethodologyHow benchmarks are measured
- Optimization guidePerformance improvement techniques
- Performance budgetsFrame budget allocation
- Master benchmark reportComplete raw data
Benchmark Methodology
This document describes how AstraWeave performance measurements are collected, validated, and reported.
Measurement Philosophy
“Prove it, don’t hype it.”
Every performance claim in AstraWeave documentation:
- Has a command that reproduces it
- Captures raw logs for auditing
- Uses statistical analysis for reliability
- Is validated against real workloads, not synthetic benchmarks
Tools & Infrastructure
Criterion.rs
All microbenchmarks use Criterion.rs for statistical rigor.
Why Criterion:
- Statistical analysis with confidence intervals
- Outlier detection and filtering
- Baseline comparison (catch regressions)
- HTML report generation
Location: target/criterion/**/base/estimates.json
Odyssey Runner
For full-suite benchmarking, use the automation script:
./scripts/benchmark_odyssey.ps1 -OutDir benchmark_results/$(Get-Date -Format 'yyyy-MM-dd')
Outputs:
environment.txt- OS/CPU/RAM, rustc/cargo version, git SHApackages_with_benches.txt- Inventory of benchmarked cratesrun_order.txt- Execution orderbench_<package>.log- Raw benchmark output per craterun_results.json- Success/fail status
Statistical Practices
Confidence Intervals
Criterion provides 95% confidence intervals for all measurements. We report:
- Point estimate: The measured mean
- Lower bound: 95% CI lower
- Upper bound: 95% CI upper
Example: 1.34 ns [1.33 ns, 1.35 ns] means the true mean is 95% likely within that range.
Warm-Up & Iterations
Default Criterion settings:
- Warm-up: 3 seconds (eliminates cold-start artifacts)
- Measurement: 5 seconds minimum
- Sample size: 100 samples minimum
Outlier Handling
Criterion automatically detects and reports outliers:
- Mild outliers: 1.5× IQR
- Severe outliers: 3× IQR
Outliers are flagged in reports but included in analysis (not discarded).
Benchmark Categories
1. Microbenchmarks
Single-operation measurements (e.g., “how long does vec3_lerp take?”).
Location: crates/*/benches/*.rs
Example:
#![allow(unused)]
fn main() {
fn bench_vec3_lerp(c: &mut Criterion) {
let a = Vec3::new(0.0, 0.0, 0.0);
let b = Vec3::new(1.0, 1.0, 1.0);
c.bench_function("vec3_lerp", |bencher| {
bencher.iter(|| a.lerp(b, 0.5))
});
}
}
2. Adversarial Benchmarks
Stress tests for edge cases and worst-case scenarios.
Categories (22 sections):
- Gameplay adversarial (massive damage, rapid hits)
- Input adversarial (input storms, frame clear)
- Math adversarial (IEEE-754 edge cases: infinity, NaN, denormals)
- Navigation adversarial (sliver triangles, impossible paths)
- Security adversarial (script sandboxing, anti-cheat)
- And 17 more…
Purpose: Ensure production stability under extreme conditions.
3. Integration Benchmarks
End-to-end measurements of complete systems.
Example: “Full game loop with 5,000 entities”
#![allow(unused)]
fn main() {
fn bench_full_game_loop(c: &mut Criterion) {
let mut world = setup_world_with_entities(5000);
c.bench_function("full_game_loop/5000_entities", |bencher| {
bencher.iter(|| world.tick(1.0 / 60.0))
});
}
}
4. Scalability Benchmarks
Measure performance across varying input sizes.
Example: Entity spawn at 10, 100, 1000, 10000 entities.
#![allow(unused)]
fn main() {
fn bench_entity_spawn(c: &mut Criterion) {
let mut group = c.benchmark_group("entity_spawn");
for size in [10, 100, 1000, 10000] {
group.bench_with_input(
BenchmarkId::from_parameter(size),
&size,
|b, &size| b.iter(|| spawn_entities(size))
);
}
group.finish();
}
}
Environment Standardization
Hardware Requirements
Benchmark machines should document:
- CPU: Model, cores, clock speed
- RAM: Size, speed
- OS: Windows/Linux/macOS version
- Rust:
rustc --version - Profile: Always
--release
Isolation Practices
For reliable measurements:
- Close unnecessary applications
- Disable turbo boost (optional, for consistency)
- Run multiple times to verify reproducibility
- Use
cargo bench -- --noplotto skip HTML generation (faster)
Reporting Standards
Master Benchmark Report
All benchmark results are consolidated in:
docs/masters/MASTER_BENCHMARK_REPORT.md
Update triggers:
- Any benchmark changes >10%
- New benchmarks added
- Performance regressions discovered
Version Tracking
Each report version documents:
- Version number (e.g., v5.55)
- Date of measurement
- Key changes since last version
- Critical fixes applied
Regression Detection
Baseline Comparison
# Save current as baseline
cargo bench -p astraweave-ecs -- --save-baseline main
# Compare against baseline
cargo bench -p astraweave-ecs -- --baseline main
CI Integration
GitHub Actions workflow (benchmark.yml) runs benchmarks on:
- Pull requests (compare against main)
- Nightly builds (detect gradual regressions)
Alert Thresholds
| Change | Action |
|---|---|
| < 5% | Normal variance, no action |
| 5-10% | Flag for review |
| 10-20% | Investigate root cause |
| > 20% | Block merge, fix required |
Coverage Methodology
Test coverage is measured using cargo-llvm-cov:
# Generate coverage report
cargo llvm-cov --workspace --html
# View report
open target/llvm-cov/html/index.html
Coverage by Tier
| Tier | Crates | Target | Actual |
|---|---|---|---|
| Tier 1 (Critical) | ecs, core, ai, render | 80% | 75.3% |
| Tier 2 (Important) | physics, nav, gameplay | 75% | 72.6% |
| Tier 3 (Supporting) | audio, scene, terrain | 70% | 71.8% |
| Tier 4 (Specialized) | fluids, llm, prompts | 65% | 71.5% |
Per-Crate Coverage (verified January 2026)
| Crate | Coverage | Status |
|---|---|---|
| astraweave-ecs | 83.2% | ✅ |
| astraweave-core | 79.1% | ✅ |
| astraweave-ai | 71.3% | ✅ |
| astraweave-render | 67.4% | ✅ |
| astraweave-physics | 76.8% | ✅ |
| astraweave-fluids | 94.2% | ✅ A+ |
| astraweave-nav | 72.1% | ✅ |
| astraweave-gameplay | 68.9% | ✅ |
| astraweave-terrain | 71.5% | ✅ |
| astraweave-audio | 69.2% | ✅ |
| astraweave-scene | 74.6% | ✅ |
| astraweave-llm | 58.3% | ⚠️ Beta |
Reproducing Results
Quick Verification
# Verify ECS benchmarks match documentation
cargo bench -p astraweave-ecs -- entity_spawn/empty/10000
# Expected: ~645µs (±10%)
Full Reproduction
- Clone repository at documented commit
- Run
./scripts/benchmark_odyssey.ps1 - Compare
benchmark_results/*/against documented values - Variance > 20% indicates environment difference
See Also
- Benchmarks - Performance data
- Optimization Guide - Improvement techniques
- Performance Budgets - Frame budget allocation
- Master Report - Complete raw data
Optimization Guide
This guide covers performance optimization techniques for AstraWeave games.
General Principles
Measure First
Never optimize without profiling.
Before optimizing:
- Run benchmarks to establish baseline
- Profile with Tracy or
cargo flamegraph - Identify actual bottlenecks
- Optimize the measured slow path
Amdahl’s Law
Only parallelize work that benefits from it:
- Sequential overhead limits speedup
- 0.15-22.4% of work is parallelizable in typical games
- Maximum theoretical speedup: ~1.24× in many cases
ECS Optimization
Batching Over Scattering
#![allow(unused)]
fn main() {
// ❌ Slow: Scattered access
for entity in entities {
let pos = world.get_mut::<Position>(entity);
pos.x += 1.0;
}
// ✅ Fast: Batch access (3-5× faster)
let mut positions: Vec<_> = world.query::<&mut Position>().iter_mut().collect();
for pos in &mut positions {
pos.x += 1.0;
}
}
Why: Archetype lookup is O(log n) per access. Batching amortizes this cost.
Component Layout
- Keep frequently-accessed components small
- Group components that are accessed together
- Use
u32overu64when possible (cache efficiency)
Entity Spawning
#![allow(unused)]
fn main() {
// ❌ Slow: One-by-one spawning
for _ in 0..1000 {
world.spawn((Position::default(), Velocity::default()));
}
// ✅ Fast: Batch spawning
world.spawn_batch((0..1000).map(|_| (Position::default(), Velocity::default())));
}
AI Optimization
GOAP Caching
GOAP cache hit: 9.8 ns vs cache miss: 286 ns (29× faster)
#![allow(unused)]
fn main() {
// Enable GOAP caching
let planner = GoapPlanner::new()
.with_cache_size(1000) // Cache 1000 plans
.with_cache_ttl(Duration::from_secs(5));
}
Agent Update Staggering
#![allow(unused)]
fn main() {
// Update 1/10th of agents per frame (10-frame rotation)
for (i, agent) in agents.iter_mut().enumerate() {
if i % 10 == frame_count % 10 {
agent.update(&snapshot);
}
}
}
AIArbiter Cooldown
#![allow(unused)]
fn main() {
// Reduce LLM request frequency for better performance
let arbiter = AIArbiter::new(executor, goap, bt)
.with_llm_cooldown(15.0); // 15 seconds between requests
}
Physics Optimization
Spatial Hashing
AstraWeave’s spatial hash reduces collision checks by 99.96%:
#![allow(unused)]
fn main() {
// Automatic with default physics setup
let physics = PhysicsWorld::new()
.with_spatial_hash(cell_size: 2.0); // Tune cell size to object density
}
Tuning: Cell size should be ~2× average object radius.
Rigid Body Batching
#![allow(unused)]
fn main() {
// Batch physics updates (47µs for 100 bodies)
physics.step_batch(&mut bodies, dt);
}
Sleep Optimization
#![allow(unused)]
fn main() {
// Enable sleeping for static objects
body.enable_sleeping(linear_threshold: 0.01, angular_threshold: 0.01);
}
Rendering Optimization
Frustum Culling
Frustum check: 889-915 ps (essentially free)
#![allow(unused)]
fn main() {
// Automatic in renderer
renderer.enable_frustum_culling(true);
}
LOD Selection
#![allow(unused)]
fn main() {
// Configure LOD distances
mesh.set_lod_distances(&[10.0, 50.0, 100.0, 500.0]);
}
Instanced Rendering
Instance overhead: 1.43-1.52 ns per calculation
#![allow(unused)]
fn main() {
// Batch identical meshes
let instances = entities
.filter(|e| e.mesh_id == mesh_id)
.map(|e| e.transform)
.collect();
renderer.draw_instanced(mesh, &instances);
}
Memory Optimization
SparseSet vs BTreeMap
| Operation | SparseSet | BTreeMap | Winner |
|---|---|---|---|
| Lookup (1000) | 1.56 ns | 59 µs | 37× SparseSet |
| Insert (1000) | 9.9 ns | 129 ns | 13× SparseSet |
AstraWeave uses SparseSet for entity storage—architecture decision validated.
Component Pooling
#![allow(unused)]
fn main() {
// Reuse allocations
let mut pool = ComponentPool::<Particle>::new(10000);
for _ in 0..10000 {
let particle = pool.allocate();
// Use particle...
pool.deallocate(particle);
}
}
Profiling Tools
Tracy Integration
#![allow(unused)]
fn main() {
// Add Tracy spans to hot paths
#[cfg(feature = "profiling")]
astraweave_profiling::span!("AI::Update");
}
Criterion Benchmarks
# Profile specific benchmark
cargo bench -p astraweave-ecs -- entity_spawn/empty/10000 --profile-time 10
# Generate flamegraph
cargo flamegraph -- --bench entity_bench
Frame Timing
#![allow(unused)]
fn main() {
// Log frame timing
let start = Instant::now();
// ... game loop ...
let frame_time = start.elapsed();
if frame_time > Duration::from_millis(16) {
warn!("Frame budget exceeded: {:?}", frame_time);
}
}
Common Pitfalls
1. Over-Parallelization
Problem: Adding Rayon to small workloads
Why: Thread pool overhead (~50-100 µs) exceeds benefit for <5 ms work
Solution: Only parallelize workloads >5 ms
2. Allocations in Hot Paths
Problem: Vec::new() in per-frame code
Solution: Pre-allocate and reuse
#![allow(unused)]
fn main() {
// ❌ Bad: Allocates every frame
fn update(&mut self) {
let temp = Vec::new(); // Allocation!
// ...
}
// ✅ Good: Reuse allocation
fn update(&mut self) {
self.temp_buffer.clear(); // No allocation
// ...
}
}
3. Debug vs Release
Problem: Testing performance in debug mode
Why: Debug can be 10-100× slower
Solution: Always benchmark with --release
cargo bench -p astraweave-ecs # Automatically uses release
See Also
- Benchmarks - Current performance data
- Methodology - How we measure
- Performance Budgets - Frame allocation
Performance Budgets
This document defines frame time allocations for AstraWeave games targeting 60 FPS.
Target Specification
| Metric | Value | Notes |
|---|---|---|
| Target FPS | 60 | Standard for action games |
| Frame Budget | 16.67 ms | 1000ms ÷ 60 |
| Headroom Target | 20% | 3.33 ms reserved for spikes |
| Available Budget | 13.34 ms | 80% of frame |
Budget Allocation
Recommended Distribution
| System | Budget (ms) | Percentage | Notes |
|---|---|---|---|
| Game Logic | 2.0 | 12% | ECS, AI, gameplay |
| Physics | 2.5 | 15% | Simulation, collision |
| Rendering | 6.0 | 36% | Draw calls, GPU sync |
| Audio | 0.5 | 3% | Spatial audio, mixing |
| Networking | 1.0 | 6% | Sync, prediction |
| UI | 0.5 | 3% | HUD, menus |
| I/O | 0.5 | 3% | Streaming, saves |
| Headroom | 3.67 | 22% | Spike absorption |
| Total | 16.67 | 100% | — |
Visual Breakdown
Frame Budget: 16.67ms
├── Game Logic ████░░░░░░░░░░░░░░░░ 2.0ms (12%)
├── Physics █████░░░░░░░░░░░░░░░ 2.5ms (15%)
├── Rendering ████████████░░░░░░░░ 6.0ms (36%)
├── Audio █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
├── Networking ██░░░░░░░░░░░░░░░░░░ 1.0ms (6%)
├── UI █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
├── I/O █░░░░░░░░░░░░░░░░░░░ 0.5ms (3%)
└── Headroom ███████░░░░░░░░░░░░░ 3.67ms (22%)
Game Logic Budget (2.0 ms)
Sub-Allocation
| Component | Budget (µs) | Notes |
|---|---|---|
| ECS core (1k entities) | 85 | 0.51% of frame |
| AI updates (500 agents) | 471 | GOAP + arbiter |
| Gameplay systems | 500 | Combat, quests, dialogue |
| Script execution | 300 | Rhai scripts |
| Event processing | 200 | Input, triggers |
| Total | 1,556 | 22% headroom within budget |
Scaling Guidelines
| Entity Count | ECS Budget | AI Agents | Feasibility |
|---|---|---|---|
| 1,000 | 85 µs | 500 | ✅ Comfortable |
| 5,000 | 529 µs | 1,000 | ✅ Within budget |
| 10,000 | 1 ms | 2,000 | ⚠️ Near limit |
| 20,000+ | >2 ms | — | ❌ Consider 30 FPS |
Physics Budget (2.5 ms)
Sub-Allocation
| Component | Budget (µs) | Notes |
|---|---|---|
| Rigid body simulation | 500 | 100 bodies |
| Character controllers | 200 | Player + NPCs |
| Collision detection | 800 | Spatial hash accelerated |
| Raycasts | 300 | AI, weapons, cameras |
| Triggers/sensors | 200 | Zone detection |
| Total | 2,000 | 20% headroom |
Collision Budget
With spatial hashing (99.96% check reduction):
| Objects | Naive | With Hash | Savings |
|---|---|---|---|
| 100 | 4,950 checks | 2 checks | 99.96% |
| 1,000 | 499,500 checks | ~20 checks | 99.996% |
| 10,000 | 49.9M checks | ~200 checks | 99.9996% |
Rendering Budget (6.0 ms)
Sub-Allocation
| Stage | Budget (ms) | Notes |
|---|---|---|
| Culling | 0.5 | Frustum, occlusion |
| Shadow passes | 1.0 | CSM, spot shadows |
| G-Buffer | 1.5 | Deferred geometry |
| Lighting | 1.0 | Clustered, IBL |
| Post-processing | 1.0 | Bloom, SSAO, TAA |
| UI overlay | 0.5 | HUD, debug |
| GPU sync | 0.5 | Fence waiting |
| Total | 6.0 | — |
Draw Call Budget
| Category | Calls | Notes |
|---|---|---|
| Opaque geometry | 500 | With instancing |
| Transparent | 100 | Sorted, no batching |
| Shadows | 200 | Per cascade |
| UI | 50 | Batched |
| Total | 850 | Target maximum |
Audio Budget (0.5 ms)
Sub-Allocation
| Component | Budget (µs) | Notes |
|---|---|---|
| Voice mixing | 200 | 4-bus mixer |
| Spatial positioning | 150 | 3D audio |
| DSP effects | 100 | Reverb, filters |
| Stream decode | 50 | Music, ambient |
| Total | 500 | — |
Monitoring Budget Compliance
Runtime Checks
#![allow(unused)]
fn main() {
use std::time::Instant;
struct FrameBudget {
logic_budget: Duration,
physics_budget: Duration,
render_budget: Duration,
}
impl FrameBudget {
fn check(&self, phase: &str, elapsed: Duration, budget: Duration) {
if elapsed > budget {
warn!(
"{} exceeded budget: {:?} > {:?} ({}%)",
phase,
elapsed,
budget,
(elapsed.as_micros() * 100 / budget.as_micros())
);
}
}
}
}
Tracy Zones
#![allow(unused)]
fn main() {
#[cfg(feature = "profiling")]
{
astraweave_profiling::zone!("GameLogic");
// ... game logic ...
}
}
30 FPS Budget (Alternative)
For complex simulations or lower-end hardware:
| Metric | Value |
|---|---|
| Frame Budget | 33.33 ms |
| Headroom | 6.67 ms (20%) |
| Available | 26.66 ms |
When to use 30 FPS:
- Open worlds with >20k entities
- Complex physics simulations
- Lower-end hardware targets
- Cinematic experiences
Budget Violations
Severity Levels
| Violation | Duration | Response |
|---|---|---|
| Minor | 1-2 frames | Log, continue |
| Moderate | 3-10 frames | Reduce quality |
| Severe | >10 frames | Emergency LOD |
Automatic Quality Scaling
#![allow(unused)]
fn main() {
// Reduce quality on budget overrun
if frame_time > Duration::from_millis(18) {
quality.reduce_shadow_resolution();
}
if frame_time > Duration::from_millis(20) {
quality.reduce_draw_distance();
}
if frame_time > Duration::from_millis(25) {
quality.disable_post_processing();
}
}
See Also
- Benchmarks - Actual measured performance
- Methodology - How we measure
- Optimization Guide - Performance techniques
Veilweaver: Threads of Eternity
Reference Implementation for AstraWeave AI-Native Game Engine
Veilweaver is a complete AI-native Action RPG that serves as AstraWeave’s flagship reference implementation. It demonstrates the full capabilities of the engine in a production game context.
Overview
Veilweaver showcases:
- AI Companions with persistent memory and adaptive behavior
- Adaptive Boss Battles that learn from player strategies
- Dynamic World Events driven by AI directors
- Emergent Narrative through AI-orchestrated storytelling
- Procedural Content integrated with hand-crafted design
Current Status
Veilweaver is developed in a separate repository and is currently in active development.
The Veilweaver documentation is being migrated to this documentation site. For the latest information:
- GitHub Repository: Games-VEILWEAVER
- Design Documents: Located in
docs/archive/projects/veilweaver/
Key Features Demonstrated
AI Companion System
Veilweaver’s companions use AstraWeave’s full AI stack:
- Perception Bus: Companions observe the game world in real-time
- GOAP Planning: Goal-oriented action planning for complex behaviors
- Memory System: Companions remember interactions and adapt
- LLM Integration: Natural language dialogue and reasoning
Weaving System
The signature gameplay mechanic demonstrates:
- Tool Sandbox: AI-validated player abilities
- Deterministic Simulation: Consistent weaving effects
- Physics Integration: Weave-affected environments
World Design
Three biome zones showcase terrain systems:
- Loomspire Sanctum - Tutorial and hub area
- Echo Grove - Forest exploration zone
- Fractured Cliffs - Vertical traversal challenges
Getting Started with Veilweaver
# Clone the Veilweaver repository
git clone https://github.com/lazyxeon/Games-VEILWEAVER.git
# Build and run
cd Games-VEILWEAVER
cargo run --release
Additional Documentation
For detailed Veilweaver documentation, see the Games-VEILWEAVER repository:
- Game mechanics guide
- AI integration deep-dive
- World design principles
- Asset creation workflow
- Modding support guide
See Also
Contributing to AstraWeave
Thank you for your interest in contributing to AstraWeave! This guide will help you get started with contributing to the project.
Table of Contents
- Code of Conduct
- Getting Started
- How to Contribute
- Development Setup
- Coding Standards
- Testing Requirements
- Documentation Requirements
- Code Review Process
- Community
Code of Conduct
We are committed to providing a welcoming and inclusive environment for all contributors. Please be respectful and constructive in all interactions.
Getting Started
Before you start contributing, please:
- Read the Building from Source guide
- Familiarize yourself with the Code Style Guide
- Review the Testing Guide
- Check existing Issues and Pull Requests
How to Contribute
Reporting Bugs
When reporting bugs, please include:
- Clear title: Describe the issue concisely
- Environment: OS, Rust version, GPU model
- Steps to reproduce: Detailed steps to trigger the bug
- Expected behavior: What should happen
- Actual behavior: What actually happens
- Logs: Relevant error messages or stack traces
**Environment:**
- OS: Windows 11
- Rust: 1.75.0
- GPU: NVIDIA RTX 4080
**Steps to Reproduce:**
1. Create a new companion with perception
2. Add emotion system
3. Run the simulation
**Expected:** Companion should respond to stimuli
**Actual:** Panic in emotion processing thread
**Logs:**
\`\`\`
thread 'emotion' panicked at 'index out of bounds'
\`\`\`
Suggesting Features
For feature requests, please:
- Check if the feature already exists or is planned
- Describe the use case and problem it solves
- Provide examples of how it would work
- Consider implementation complexity and alternatives
Submitting Pull Requests
-
Fork the repository and create a new branch:
git checkout -b feature/my-awesome-feature -
Make your changes following our coding standards
-
Write or update tests to cover your changes
-
Run the full test suite:
cargo test --all-features cargo clippy --all-targets --all-features cargo fmt --check -
Update documentation if needed
-
Commit with clear messages:
git commit -m "feat(ai): Add emotion blending system" -
Push and create a Pull Request:
git push origin feature/my-awesome-feature
Keep PRs focused on a single feature or fix. Large PRs are harder to review and merge.
Development Setup
Prerequisites
- Rust 1.75.0 or later
- Git
- A supported GPU with Vulkan/DirectX 12/Metal
Initial Setup
# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
# Build the project
cargo build
# Run tests
cargo test
# Run examples
cargo run --example basic_companion
Development Workflow
# Create a new branch
git checkout -b feature/my-feature
# Make changes and test frequently
cargo test
# Run clippy for linting
cargo clippy --all-targets --all-features
# Format code
cargo fmt
# Commit changes
git commit -m "feat: Add feature description"
Coding Standards
General Principles
- Clarity over cleverness: Write code that is easy to understand
- Performance matters: AstraWeave is a high-performance engine
- Safety first: Prefer safe abstractions over unsafe code
- Documentation: Document all public APIs
Rust Conventions
#![allow(unused)]
fn main() {
// Good: Clear naming and documentation
/// Calculates emotion intensity based on stimuli strength.
///
/// # Arguments
/// * `stimulus` - The input stimulus value (0.0 to 1.0)
/// * `sensitivity` - Sensitivity multiplier (default: 1.0)
///
/// # Returns
/// Emotion intensity clamped to [0.0, 1.0]
pub fn calculate_emotion_intensity(stimulus: f32, sensitivity: f32) -> f32 {
(stimulus * sensitivity).clamp(0.0, 1.0)
}
// Bad: Unclear naming and no documentation
pub fn calc(s: f32, m: f32) -> f32 {
(s * m).min(1.0).max(0.0)
}
}
Naming Conventions
- Types:
PascalCase(e.g.,EmotionSystem,CompanionState) - Functions:
snake_case(e.g.,process_emotion,update_behavior) - Constants:
SCREAMING_SNAKE_CASE(e.g.,MAX_COMPANIONS,DEFAULT_TICK_RATE) - Modules:
snake_case(e.g.,emotion_system,companion_ai)
Error Handling
#![allow(unused)]
fn main() {
// Good: Use Result for recoverable errors
pub fn load_companion_config(path: &Path) -> Result<CompanionConfig, ConfigError> {
let contents = std::fs::read_to_string(path)
.map_err(|e| ConfigError::FileRead(path.to_path_buf(), e))?;
serde_json::from_str(&contents)
.map_err(|e| ConfigError::Parse(e))
}
// Good: Use panic only for programming errors
pub fn get_emotion(&self, index: usize) -> &Emotion {
assert!(index < self.emotions.len(), "Emotion index out of bounds");
&self.emotions[index]
}
}
Testing Requirements
All contributions must include appropriate tests:
Unit Tests
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emotion_intensity_clamping() {
assert_eq!(calculate_emotion_intensity(0.5, 1.0), 0.5);
assert_eq!(calculate_emotion_intensity(1.5, 1.0), 1.0);
assert_eq!(calculate_emotion_intensity(-0.5, 1.0), 0.0);
}
#[test]
fn test_emotion_blending() {
let joy = Emotion::new("joy", 0.8);
let calm = Emotion::new("calm", 0.6);
let blended = blend_emotions(&joy, &calm, 0.5);
assert_eq!(blended.intensity, 0.7);
}
}
}
Integration Tests
Place integration tests in tests/ directory:
#![allow(unused)]
fn main() {
// tests/companion_integration.rs
use astraweave_ai::*;
#[test]
fn test_companion_lifecycle() {
let mut companion = CompanionBuilder::new()
.with_perception()
.with_emotion()
.build();
companion.update(0.016);
assert!(companion.is_active());
}
}
Benchmarks
For performance-critical code, add benchmarks:
#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, black_box};
fn benchmark_emotion_processing(c: &mut Criterion) {
c.bench_function("process_100_emotions", |b| {
let mut system = EmotionSystem::new();
b.iter(|| {
for _ in 0..100 {
system.process(black_box(0.016));
}
});
});
}
criterion_group!(benches, benchmark_emotion_processing);
criterion_main!(benches);
}
Coverage Requirements
- New features should have >80% code coverage
- Bug fixes must include a regression test
- Run coverage locally:
cargo tarpaulin --all-features --workspace --timeout 300
PRs with insufficient test coverage will not be merged.
Documentation Requirements
Public API Documentation
All public items must have documentation:
#![allow(unused)]
fn main() {
/// Represents an AI companion with emotion and behavior systems.
///
/// A companion can perceive its environment, process emotions, and
/// exhibit behaviors based on its internal state.
///
/// # Examples
///
/// ```
/// use astraweave_ai::Companion;
///
/// let companion = Companion::new("Buddy");
/// companion.update(0.016);
/// ```
pub struct Companion {
// ...
}
}
Module Documentation
Add module-level documentation in lib.rs or mod.rs:
#![allow(unused)]
fn main() {
//! # Emotion System
//!
//! This module provides the emotion processing system for AI companions.
//! Emotions are processed based on stimuli and decay over time.
//!
//! ## Example
//!
//! ```
//! use astraweave_ai::emotion::EmotionSystem;
//!
//! let mut system = EmotionSystem::new();
//! system.add_emotion("joy", 0.8);
//! system.update(0.016);
//! ```
}
Changelog Updates
Update CHANGELOG.md for significant changes:
## [Unreleased]
### Added
- Emotion blending system for smoother transitions
- New `CompanionBuilder` for easier companion creation
### Changed
- Improved perception system performance by 30%
### Fixed
- Fixed emotion decay rate calculation
Code Review Process
What Reviewers Look For
- Correctness: Does the code work as intended?
- Tests: Are there adequate tests?
- Documentation: Is the code well-documented?
- Style: Does it follow our coding standards?
- Performance: Are there any performance concerns?
- Safety: Are there any unsafe patterns?
Review Timeline
- Initial review: Within 48 hours
- Follow-up reviews: Within 24 hours
- Merging: After approval from at least one maintainer
Addressing Feedback
Respond to all review comments, even if just to acknowledge them.
- Make requested changes in new commits
- Don’t force-push during review (makes tracking changes hard)
- Mark conversations as resolved when addressed
- Ask questions if feedback is unclear
Approval Process
- All CI checks must pass
- At least one maintainer approval required
- No unresolved review comments
- Up-to-date with the main branch
Community
Getting Help
- Discord: Join our Discord server
- Discussions: Use GitHub Discussions
- Issues: Search existing issues before creating new ones
Stay Updated
- Watch the repository for updates
- Follow our blog
- Subscribe to release notifications
Thank you for contributing to AstraWeave! Your efforts help make AI-native gaming better for everyone.
Building from Source
This guide covers how to build AstraWeave from source on all supported platforms.
Table of Contents
Prerequisites
Required Software
Rust Toolchain
AstraWeave requires Rust 1.75.0 or later.
# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Or on Windows, download from: https://rustup.rs/
# Verify installation
rustc --version
cargo --version
Keep Rust updated for the latest performance improvements and bug fixes:
\`\`\`bash
rustup update
\`\`\`
Git
# Linux (Debian/Ubuntu)
sudo apt install git
# macOS
brew install git
# Windows
# Download from https://git-scm.com/
GPU Requirements
AstraWeave requires a modern GPU with support for:
- Vulkan 1.2+ (Linux, Windows)
- DirectX 12 (Windows)
- Metal (macOS)
Supported GPUs:
- NVIDIA: GTX 1060 or newer
- AMD: RX 580 or newer
- Intel: Arc series or newer integrated graphics
Older GPUs may work but are not officially supported and may have reduced performance.
Platform-Specific Instructions
Windows
System Dependencies
-
Visual Studio Build Tools (required for linking):
- Download Visual Studio 2022 Build Tools
- Select “Desktop development with C++” workload
- Minimum components:
- MSVC v143 build tools
- Windows 10/11 SDK
-
Vulkan SDK (optional, for Vulkan backend):
- Download from LunarG
- Install with default options
Building on Windows
# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
# Build (release mode recommended)
cargo build --release
# Run tests
cargo test --release
# Run an example
cargo run --release --example basic_companion
Windows Defender may flag the first build as suspicious. This is normal for newly compiled executables.
Windows-Specific Issues
Long Path Support: Enable long paths if you encounter path length errors:
# Run as Administrator
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" `
-Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
Linux
System Dependencies
Debian/Ubuntu:
sudo apt update
sudo apt install -y \
build-essential \
pkg-config \
libx11-dev \
libxcursor-dev \
libxrandr-dev \
libxi-dev \
libasound2-dev \
libudev-dev \
vulkan-tools \
libvulkan-dev
Fedora/RHEL:
sudo dnf install -y \
gcc gcc-c++ \
pkg-config \
libX11-devel \
libXcursor-devel \
libXrandr-devel \
libXi-devel \
alsa-lib-devel \
systemd-devel \
vulkan-tools \
vulkan-loader-devel
Arch Linux:
sudo pacman -S --needed \
base-devel \
libx11 \
libxcursor \
libxrandr \
libxi \
alsa-lib \
vulkan-tools \
vulkan-headers
Building on Linux
# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
# Build
cargo build --release
# Run tests
cargo test --release
# Run an example
cargo run --release --example basic_companion
GPU Drivers
NVIDIA:
# Ubuntu
sudo ubuntu-drivers autoinstall
# Or manually install latest driver
sudo apt install nvidia-driver-535
AMD:
# Mesa drivers (usually pre-installed)
sudo apt install mesa-vulkan-drivers
Intel:
sudo apt install mesa-vulkan-drivers intel-media-va-driver
Verify Vulkan support:
vulkaninfo | grep "deviceName"
macOS
System Dependencies
-
Xcode Command Line Tools:
xcode-select --install -
Homebrew (recommended):
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Building on macOS
# Clone the repository
git clone https://github.com/verdentlabs/astraweave.git
cd astraweave
# Build
cargo build --release
# Run tests
cargo test --release
# Run an example
cargo run --release --example basic_companion
macOS uses Metal as the graphics backend. Ensure your system is macOS 10.15+ with Metal-compatible GPU.
Apple Silicon (M1/M2/M3)
AstraWeave fully supports Apple Silicon:
# Verify you're using the ARM toolchain
rustc --version --verbose | grep host
# Should show: aarch64-apple-darwin
# Build optimized for Apple Silicon
RUSTFLAGS="-C target-cpu=native" cargo build --release
Build Configurations
Release vs Debug
Debug build (fast compilation, slow runtime):
cargo build
# Binary: target/debug/
Release build (slow compilation, fast runtime):
cargo build --release
# Binary: target/release/
Always use release builds for performance testing and gameplay. Debug builds can be 10-100x slower.
Feature Flags
AstraWeave uses Cargo features for optional functionality:
# Build with all features
cargo build --all-features
# Build with specific features
cargo build --features "ai,physics,fluids"
# Build without default features
cargo build --no-default-features --features "minimal"
Available features:
ai- AI companion systems (default)physics- Physics simulationfluids- Fluid dynamicseditor- Level editor toolsnetworking- Multiplayer supportserialization- Save/load support
Optimization Profiles
Custom profiles are defined in Cargo.toml:
[profile.release]
opt-level = 3
lto = "thin"
codegen-units = 1
[profile.dev]
opt-level = 1
[profile.bench]
inherits = "release"
lto = "fat"
Build with specific profile:
cargo build --profile bench
Cross-Compilation
Targeting Different Platforms
# Install target
rustup target add x86_64-pc-windows-gnu
# Build for target
cargo build --target x86_64-pc-windows-gnu --release
Common targets:
x86_64-pc-windows-msvc- Windows (MSVC)x86_64-unknown-linux-gnu- Linuxx86_64-apple-darwin- macOS (Intel)aarch64-apple-darwin- macOS (Apple Silicon)
Troubleshooting
Common Build Errors
“linker not found”
Windows: Install Visual Studio Build Tools
Linux:
sudo apt install build-essential
macOS:
xcode-select --install
“could not find native library ‘vulkan’”
Windows: Install Vulkan SDK
Linux:
sudo apt install libvulkan-dev
macOS: Not needed (uses Metal)
Out of Memory During Build
Reduce parallel compilation:
cargo build --release -j 2
Or increase swap space on Linux:
sudo fallocate -l 8G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
“failed to run custom build command”
Clean the build cache:
cargo clean
cargo build --release
Performance Issues
Slow Compilation
Use the faster linker:
Linux (lld):
sudo apt install lld
export RUSTFLAGS="-C link-arg=-fuse-ld=lld"
cargo build --release
macOS (zld):
brew install michaeleisel/zld/zld
export RUSTFLAGS="-C link-arg=-fuse-ld=/usr/local/bin/zld"
cargo build --release
Windows (lld-link, included with MSVC):
$env:RUSTFLAGS="-C link-arg=/DEBUG:NONE"
cargo build --release
Enable sccache for cached builds:
cargo install sccache
export RUSTC_WRAPPER=sccache
Runtime Performance
Verify release mode:
# Should show /release/ in path
cargo run --release -- --version
Check GPU usage:
# Windows
nvidia-smi
# Linux
watch -n 1 nvidia-smi
Debugging Build Issues
Enable verbose output:
cargo build --release --verbose
Check dependencies:
cargo tree
Update dependencies:
cargo update
Next Steps
After successfully building AstraWeave:
- Run the test suite: See Testing Guide
- Try the examples:
cargo run --release --example <name> - Read the tutorials: Getting Started
- Join the community: Discord
Congratulations! You've successfully built AstraWeave from source. You're ready to start developing AI-native games.
Testing Guide
This guide covers testing practices for AstraWeave, including unit tests, integration tests, benchmarks, and coverage analysis.
Table of Contents
Running Tests
Quick Start
# Run all tests
cargo test
# Run tests in release mode (faster for compute-heavy tests)
cargo test --release
# Run tests for a specific package
cargo test -p astraweave-ai
# Run tests with all features enabled
cargo test --all-features
Verbose Output
# Show println! output from tests
cargo test -- --nocapture
# Show one test per line
cargo test -- --test-threads=1 --nocapture
# Run specific test
cargo test test_emotion_processing -- --exact
Filtering Tests
# Run tests matching a pattern
cargo test emotion
# Run only unit tests (exclude integration tests)
cargo test --lib
# Run only integration tests
cargo test --test '*'
# Run only documentation tests
cargo test --doc
Use `cargo test --help` and `cargo test -- --help` to see all available options.
Writing Unit Tests
Basic Unit Tests
Place unit tests in the same file as the code they test:
#![allow(unused)]
fn main() {
pub fn calculate_emotion_decay(initial: f32, decay_rate: f32, dt: f32) -> f32 {
initial * (1.0 - decay_rate * dt).max(0.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emotion_decay_basic() {
let result = calculate_emotion_decay(1.0, 0.5, 0.1);
assert_eq!(result, 0.95);
}
#[test]
fn test_emotion_decay_never_negative() {
let result = calculate_emotion_decay(1.0, 2.0, 1.0);
assert_eq!(result, 0.0);
}
}
}
Testing with Floating Point
Use approximate comparisons for floating-point values:
#![allow(unused)]
fn main() {
#[test]
fn test_emotion_blending() {
let result = blend_emotions(0.8, 0.6, 0.5);
assert!((result - 0.7).abs() < 1e-6, "Expected ~0.7, got {}", result);
}
// Or use the approx crate
use approx::assert_relative_eq;
#[test]
fn test_emotion_blending_approx() {
let result = blend_emotions(0.8, 0.6, 0.5);
assert_relative_eq!(result, 0.7, epsilon = 1e-6);
}
}
Testing Errors
Test error conditions explicitly:
#![allow(unused)]
fn main() {
#[test]
fn test_invalid_companion_id() {
let world = CompanionWorld::new();
let result = world.get_companion(CompanionId(999));
assert!(result.is_err());
assert!(matches!(result, Err(CompanionError::NotFound(_))));
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panic_on_invalid_index() {
let emotions = vec![Emotion::new("joy", 0.8)];
let _ = emotions[10]; // Should panic
}
}
Testing with Resources
Use setup and teardown for resource management:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn setup_test_config() -> (CompanionConfig, PathBuf) {
let temp_dir = std::env::temp_dir().join("astraweave_test");
fs::create_dir_all(&temp_dir).unwrap();
let config_path = temp_dir.join("config.json");
let config = CompanionConfig::default();
(config, config_path)
}
fn teardown_test_config(path: &Path) {
let _ = fs::remove_file(path);
}
#[test]
fn test_config_save_load() {
let (config, path) = setup_test_config();
config.save(&path).unwrap();
let loaded = CompanionConfig::load(&path).unwrap();
assert_eq!(config, loaded);
teardown_test_config(&path);
}
}
}
Parameterized Tests
Test multiple cases efficiently:
#![allow(unused)]
fn main() {
#[test]
fn test_emotion_clamping() {
let test_cases = vec![
(0.5, 0.5), // Normal case
(1.5, 1.0), // Above max
(-0.5, 0.0), // Below min
(0.0, 0.0), // Edge case: zero
(1.0, 1.0), // Edge case: max
];
for (input, expected) in test_cases {
let result = clamp_emotion(input);
assert_eq!(result, expected, "Failed for input {}", input);
}
}
}
Testing Async Code
For async functions, use tokio::test or similar:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
use tokio::test;
#[tokio::test]
async fn test_async_companion_update() {
let mut companion = Companion::new("TestBot");
companion.update_async(0.016).await;
assert!(companion.is_initialized());
}
#[tokio::test]
async fn test_concurrent_updates() {
let companion = Arc::new(Mutex::new(Companion::new("Concurrent")));
let handles: Vec<_> = (0..10)
.map(|_| {
let c = Arc::clone(&companion);
tokio::spawn(async move {
c.lock().await.update_async(0.016).await;
})
})
.collect();
for handle in handles {
handle.await.unwrap();
}
}
}
}
Integration Tests
Integration Test Structure
Integration tests live in the tests/ directory:
astraweave-ai/
├── src/
│ └── lib.rs
├── tests/
│ ├── companion_lifecycle.rs
│ ├── emotion_integration.rs
│ └── perception_behavior.rs
└── Cargo.toml
Example Integration Test
#![allow(unused)]
fn main() {
// tests/companion_lifecycle.rs
use astraweave_ai::*;
#[test]
fn test_complete_companion_lifecycle() {
// Creation
let mut companion = CompanionBuilder::new()
.with_name("IntegrationBot")
.with_perception()
.with_emotion()
.with_behavior()
.build();
assert!(!companion.is_initialized());
// Initialization
companion.initialize();
assert!(companion.is_initialized());
// Update cycle
for _ in 0..100 {
companion.update(0.016);
}
// Verify state
assert!(companion.get_emotion("joy").is_some());
assert!(companion.perception_active());
// Shutdown
companion.shutdown();
assert!(!companion.is_active());
}
#[test]
fn test_companion_persistence() {
let temp_path = std::env::temp_dir().join("companion_save.json");
// Create and save
let original = CompanionBuilder::new()
.with_name("Persistent")
.build();
original.save(&temp_path).unwrap();
// Load and verify
let loaded = Companion::load(&temp_path).unwrap();
assert_eq!(original.name(), loaded.name());
std::fs::remove_file(&temp_path).unwrap();
}
}
Integration Test Helpers
Create test utilities in tests/common/mod.rs:
#![allow(unused)]
fn main() {
// tests/common/mod.rs
use astraweave_ai::*;
pub fn create_test_companion() -> Companion {
CompanionBuilder::new()
.with_name("TestCompanion")
.with_perception()
.with_emotion()
.build()
}
pub fn create_test_world() -> CompanionWorld {
let mut world = CompanionWorld::new();
for i in 0..5 {
world.add_companion(create_test_companion());
}
world
}
pub fn assert_emotion_in_range(emotion: &Emotion, min: f32, max: f32) {
assert!(
emotion.intensity >= min && emotion.intensity <= max,
"Emotion {} out of range [{}, {}]",
emotion.intensity,
min,
max
);
}
}
#![allow(unused)]
fn main() {
// tests/world_simulation.rs
mod common;
use common::*;
#[test]
fn test_world_simulation() {
let mut world = create_test_world();
for _ in 0..100 {
world.update(0.016);
}
assert_eq!(world.companion_count(), 5);
}
}
Benchmarks
Setting Up Benchmarks
Benchmarks use the criterion crate. Add to Cargo.toml:
[dev-dependencies]
criterion = "0.5"
[[bench]]
name = "emotion_benchmarks"
harness = false
Writing Benchmarks
#![allow(unused)]
fn main() {
// benches/emotion_benchmarks.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
use astraweave_ai::*;
fn benchmark_emotion_update(c: &mut Criterion) {
let mut system = EmotionSystem::new();
c.bench_function("emotion_update_single", |b| {
b.iter(|| {
system.update(black_box(0.016));
});
});
}
fn benchmark_emotion_blending(c: &mut Criterion) {
let mut group = c.benchmark_group("emotion_blending");
for count in [10, 100, 1000] {
group.bench_with_input(
BenchmarkId::from_parameter(count),
&count,
|b, &count| {
let emotions: Vec<_> = (0..count)
.map(|i| Emotion::new(&format!("emotion_{}", i), 0.5))
.collect();
b.iter(|| {
blend_emotion_array(black_box(&emotions))
});
},
);
}
group.finish();
}
fn benchmark_companion_update(c: &mut Criterion) {
let mut companion = CompanionBuilder::new()
.with_perception()
.with_emotion()
.with_behavior()
.build();
c.bench_function("companion_full_update", |b| {
b.iter(|| {
companion.update(black_box(0.016));
});
});
}
criterion_group!(
benches,
benchmark_emotion_update,
benchmark_emotion_blending,
benchmark_companion_update
);
criterion_main!(benches);
}
Running Benchmarks
# Run all benchmarks
cargo bench
# Run specific benchmark
cargo bench emotion_update
# Save baseline for comparison
cargo bench -- --save-baseline main
# Compare against baseline
cargo bench -- --baseline main
# Generate detailed reports
cargo bench -- --verbose
Benchmark results are saved in `target/criterion/`. You can view HTML reports by opening `target/criterion/report/index.html`.
Benchmark Best Practices
#![allow(unused)]
fn main() {
// Good: Use black_box to prevent optimization
use std::hint::black_box;
c.bench_function("optimized_safe", |b| {
b.iter(|| {
expensive_function(black_box(input))
});
});
// Good: Separate setup from measurement
c.bench_function("with_setup", |b| {
let data = create_large_dataset();
b.iter(|| {
process_dataset(black_box(&data))
});
});
// Good: Use iter_batched for setup per iteration
use criterion::BatchSize;
c.bench_function("setup_per_iter", |b| {
b.iter_batched(
|| create_test_data(),
|data| process_data(data),
BatchSize::SmallInput,
);
});
}
Code Coverage
Installing Coverage Tools
# Install tarpaulin (Linux only)
cargo install cargo-tarpaulin
# Or use llvm-cov (cross-platform)
cargo install cargo-llvm-cov
Running Coverage Analysis
Using tarpaulin (Linux):
# Generate coverage report
cargo tarpaulin --all-features --workspace --timeout 300 --out Html
# View report
firefox tarpaulin-report.html
Using llvm-cov (cross-platform):
# Install llvm-tools
rustup component add llvm-tools-preview
# Generate coverage
cargo llvm-cov --all-features --workspace --html
# View report
open target/llvm-cov/html/index.html
Coverage in CI/CD
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
- name: Install tarpaulin
run: cargo install cargo-tarpaulin
- name: Generate coverage
run: cargo tarpaulin --all-features --workspace --timeout 300 --out Xml
- name: Upload to codecov
uses: codecov/codecov-action@v3
with:
files: cobertura.xml
Coverage Goals
- Overall: Aim for >80% code coverage
- Critical paths: AI logic, physics should be >90%
- Edge cases: All error paths should be tested
- Public APIs: 100% coverage of public interfaces
High coverage doesn't guarantee quality tests. Focus on meaningful test cases, not just hitting lines.
Testing Best Practices
Test Organization
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
// Group related tests
mod emotion_tests {
use super::*;
#[test]
fn test_creation() { /* ... */ }
#[test]
fn test_update() { /* ... */ }
}
mod perception_tests {
use super::*;
#[test]
fn test_stimulus_detection() { /* ... */ }
}
}
}
Clear Test Names
#![allow(unused)]
fn main() {
// Good: Descriptive names
#[test]
fn test_emotion_decay_clamps_to_zero() { }
#[test]
fn test_companion_initialization_fails_without_required_components() { }
// Bad: Unclear names
#[test]
fn test1() { }
#[test]
fn test_emotion() { }
}
Arrange-Act-Assert Pattern
#![allow(unused)]
fn main() {
#[test]
fn test_companion_emotion_response() {
// Arrange
let mut companion = create_test_companion();
let stimulus = Stimulus::new(StimulusType::Positive, 0.8);
// Act
companion.process_stimulus(stimulus);
companion.update(0.016);
// Assert
let joy = companion.get_emotion("joy").unwrap();
assert!(joy.intensity > 0.5);
}
}
Test Independence
#![allow(unused)]
fn main() {
// Good: Each test is independent
#[test]
fn test_a() {
let mut system = EmotionSystem::new();
system.add_emotion("joy", 0.5);
assert_eq!(system.emotion_count(), 1);
}
#[test]
fn test_b() {
let mut system = EmotionSystem::new(); // Fresh instance
system.add_emotion("calm", 0.8);
assert_eq!(system.emotion_count(), 1);
}
}
Documentation in Tests
#![allow(unused)]
fn main() {
/// Tests that emotion decay never produces negative values,
/// even with extreme decay rates or large time deltas.
#[test]
fn test_emotion_decay_bounds() {
let test_cases = vec![
(1.0, 10.0, 1.0), // Extreme decay
(0.5, 1.0, 100.0), // Large time delta
];
for (initial, rate, dt) in test_cases {
let result = calculate_emotion_decay(initial, rate, dt);
assert!(result >= 0.0, "Decay produced negative value: {}", result);
}
}
}
Well-tested code is reliable code. Invest time in comprehensive tests to catch bugs early and enable confident refactoring.
Code Style Guide
This guide defines the coding standards and style conventions for AstraWeave. Consistent code style improves readability and maintainability.
Table of Contents
- Rust Conventions
- Naming Conventions
- Code Organization
- Documentation Style
- Error Handling
- Performance Guidelines
- Clippy and Formatting
Rust Conventions
General Principles
- Idiomatic Rust: Write code that follows Rust idioms and patterns
- Safety First: Prefer safe abstractions; document unsafe code thoroughly
- Zero-Cost Abstractions: Don’t sacrifice performance for convenience
- Explicit over Implicit: Make behavior clear and predictable
Code Layout
#![allow(unused)]
fn main() {
// Good: Clear structure with proper spacing
pub struct Companion {
id: CompanionId,
name: String,
emotion_system: EmotionSystem,
perception: PerceptionSystem,
}
impl Companion {
pub fn new(name: impl Into<String>) -> Self {
Self {
id: CompanionId::generate(),
name: name.into(),
emotion_system: EmotionSystem::default(),
perception: PerceptionSystem::default(),
}
}
pub fn update(&mut self, dt: f32) {
self.perception.update(dt);
self.emotion_system.update(dt);
}
}
// Bad: Cramped, hard to read
pub struct Companion{id:CompanionId,name:String}
impl Companion{pub fn new(n:String)->Self{Self{id:CompanionId::generate(),name:n}}}
}
Line Length
- Maximum line length: 100 characters
- Break long lines at logical points
#![allow(unused)]
fn main() {
// Good: Broken at logical points
let companion = CompanionBuilder::new()
.with_name("Alice")
.with_perception()
.with_emotion()
.with_behavior()
.build();
// Bad: Too long
let companion = CompanionBuilder::new().with_name("Alice").with_perception().with_emotion().with_behavior().build();
}
Imports
Group and sort imports:
#![allow(unused)]
fn main() {
// 1. Standard library
use std::collections::HashMap;
use std::sync::Arc;
// 2. External crates (alphabetically)
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
// 3. Internal crates
use astraweave_ai::*;
use astraweave_physics::*;
// 4. Local modules
use crate::emotion::*;
use crate::perception::Stimulus;
// 5. Type aliases and trait imports
use super::CompanionId;
}
Use `cargo fmt` to automatically organize imports.
Naming Conventions
Types
PascalCase for types, structs, enums, and traits:
#![allow(unused)]
fn main() {
// Good
pub struct CompanionState { }
pub enum EmotionType { }
pub trait BehaviorController { }
// Bad
pub struct companion_state { }
pub enum emotion_type { }
pub trait behavior_controller { }
}
Functions and Methods
snake_case for functions and methods:
#![allow(unused)]
fn main() {
// Good
pub fn calculate_emotion_intensity(stimulus: f32) -> f32 { }
pub fn process_visual_stimulus(&mut self, data: &[u8]) { }
// Bad
pub fn CalculateEmotionIntensity(stimulus: f32) -> f32 { }
pub fn ProcessVisualStimulus(&mut self, data: &[u8]) { }
}
Variables
snake_case for variables and parameters:
#![allow(unused)]
fn main() {
// Good
let companion_id = CompanionId::new();
let emotion_decay_rate = 0.95;
// Bad
let CompanionID = CompanionId::new();
let EmotionDecayRate = 0.95;
}
Constants
SCREAMING_SNAKE_CASE for constants:
#![allow(unused)]
fn main() {
// Good
pub const MAX_COMPANIONS: usize = 1000;
pub const DEFAULT_TICK_RATE: f32 = 60.0;
const PI: f32 = std::f32::consts::PI;
// Bad
pub const max_companions: usize = 1000;
pub const DefaultTickRate: f32 = 60.0;
}
Modules
snake_case for module names:
#![allow(unused)]
fn main() {
// Good
mod emotion_system;
mod companion_ai;
mod perception_processing;
// Bad
mod EmotionSystem;
mod CompanionAI;
mod PerceptionProcessing;
}
Acronyms
Treat acronyms as words in PascalCase:
#![allow(unused)]
fn main() {
// Good
struct HttpClient;
struct XmlParser;
struct AiCompanion;
// Bad (but acceptable for well-known 2-letter acronyms)
struct AICompanion; // Acceptable
struct XMLParser; // Avoid
// Definitely bad
struct HTTPClient;
}
Code Organization
Module Structure
Organize code logically by functionality:
astraweave-ai/
├── src/
│ ├── lib.rs // Public API exports
│ ├── companion/
│ │ ├── mod.rs // Module root
│ │ ├── builder.rs // CompanionBuilder
│ │ ├── state.rs // CompanionState
│ │ └── lifecycle.rs // Lifecycle management
│ ├── emotion/
│ │ ├── mod.rs
│ │ ├── system.rs // EmotionSystem
│ │ ├── types.rs // Emotion types
│ │ └── blending.rs // Emotion blending
│ └── perception/
│ ├── mod.rs
│ ├── visual.rs // Visual perception
│ └── auditory.rs // Auditory perception
File Organization
Structure within a file:
#![allow(unused)]
fn main() {
// 1. Module documentation
//! # Emotion System
//!
//! This module implements the emotion processing system.
// 2. Imports (grouped as shown earlier)
use std::collections::HashMap;
use bevy::prelude::*;
// 3. Type definitions
pub struct EmotionSystem {
emotions: HashMap<String, Emotion>,
}
// 4. Implementation blocks
impl EmotionSystem {
pub fn new() -> Self {
Self {
emotions: HashMap::new(),
}
}
}
// 5. Trait implementations
impl Default for EmotionSystem {
fn default() -> Self {
Self::new()
}
}
// 6. Tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_emotion_system_creation() {
let system = EmotionSystem::new();
assert!(system.emotions.is_empty());
}
}
}
Public vs Private
Be intentional about visibility:
#![allow(unused)]
fn main() {
// Good: Clear public API
pub struct Companion {
// Public fields only when necessary
pub id: CompanionId,
// Private implementation details
emotion_system: EmotionSystem,
internal_state: State,
}
impl Companion {
// Public constructor
pub fn new(name: String) -> Self { }
// Public methods for API
pub fn update(&mut self, dt: f32) { }
// Private helper methods
fn process_internal_state(&mut self) { }
}
// Bad: Everything public
pub struct Companion {
pub id: CompanionId,
pub emotion_system: EmotionSystem,
pub internal_state: State,
}
}
Documentation Style
Module Documentation
#![allow(unused)]
fn main() {
//! # Emotion System
//!
//! This module provides the core emotion processing system for AI companions.
//! Emotions are represented as continuous values that decay over time and
//! blend based on stimuli.
//!
//! ## Example
//!
//! ```
//! use astraweave_ai::emotion::EmotionSystem;
//!
//! let mut system = EmotionSystem::new();
//! system.add_emotion("joy", 0.8);
//! system.update(0.016);
//! ```
}
Type Documentation
#![allow(unused)]
fn main() {
/// Represents an AI companion with emotion and behavior systems.
///
/// A companion processes stimuli through its perception system,
/// updates its emotional state, and exhibits behaviors based on
/// its internal state and environment.
///
/// # Examples
///
/// ```
/// use astraweave_ai::Companion;
///
/// let mut companion = Companion::new("Buddy");
/// companion.update(0.016);
///
/// if let Some(emotion) = companion.get_emotion("joy") {
/// println!("Joy level: {}", emotion.intensity);
/// }
/// ```
///
/// # Performance
///
/// Companion updates are O(n) where n is the number of active
/// emotions and behaviors.
pub struct Companion {
// ...
}
}
Function Documentation
#![allow(unused)]
fn main() {
/// Calculates the blended emotion intensity from two emotions.
///
/// Uses linear interpolation to blend two emotion intensities
/// based on a blend factor.
///
/// # Arguments
///
/// * `emotion_a` - First emotion intensity (0.0 to 1.0)
/// * `emotion_b` - Second emotion intensity (0.0 to 1.0)
/// * `blend_factor` - Blend weight (0.0 = all A, 1.0 = all B)
///
/// # Returns
///
/// Blended emotion intensity clamped to [0.0, 1.0]
///
/// # Examples
///
/// ```
/// use astraweave_ai::emotion::blend_emotions;
///
/// let joy = 0.8;
/// let calm = 0.6;
/// let blended = blend_emotions(joy, calm, 0.5);
/// assert_eq!(blended, 0.7);
/// ```
///
/// # Panics
///
/// Panics if `blend_factor` is not in range [0.0, 1.0] in debug builds.
pub fn blend_emotions(emotion_a: f32, emotion_b: f32, blend_factor: f32) -> f32 {
debug_assert!(
(0.0..=1.0).contains(&blend_factor),
"blend_factor must be in [0.0, 1.0]"
);
(emotion_a * (1.0 - blend_factor) + emotion_b * blend_factor).clamp(0.0, 1.0)
}
}
Documentation Sections
Use these sections in order:
- Summary: One-line description
- Detailed description: Multi-paragraph explanation
- Arguments: Parameter descriptions
- Returns: Return value description
- Examples: Code examples
- Errors: Possible errors (for
Result) - Panics: Panic conditions
- Safety: Safety invariants (for
unsafe) - Performance: Performance characteristics
Error Handling
Use Result for Recoverable Errors
#![allow(unused)]
fn main() {
// Good: Use Result for expected errors
pub fn load_companion(path: &Path) -> Result<Companion, LoadError> {
let data = std::fs::read_to_string(path)
.map_err(|e| LoadError::FileRead(path.to_path_buf(), e))?;
serde_json::from_str(&data)
.map_err(LoadError::Parse)
}
// Bad: Panic on expected errors
pub fn load_companion(path: &Path) -> Companion {
let data = std::fs::read_to_string(path).unwrap(); // Don't do this
serde_json::from_str(&data).unwrap()
}
}
Define Custom Error Types
#![allow(unused)]
fn main() {
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CompanionError {
#[error("Companion not found: {0}")]
NotFound(CompanionId),
#[error("Invalid companion state: {0}")]
InvalidState(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(#[from] serde_json::Error),
}
}
Provide Context
#![allow(unused)]
fn main() {
use anyhow::{Context, Result};
pub fn save_companion(&self, path: &Path) -> Result<()> {
let json = serde_json::to_string_pretty(self)
.context("Failed to serialize companion")?;
std::fs::write(path, json)
.with_context(|| format!("Failed to write to {}", path.display()))?;
Ok(())
}
}
Performance Guidelines
Prefer Iteration over Indexing
#![allow(unused)]
fn main() {
// Good: Iterator-based
fn sum_emotions(emotions: &[Emotion]) -> f32 {
emotions.iter().map(|e| e.intensity).sum()
}
// Less efficient: Index-based
fn sum_emotions_indexed(emotions: &[Emotion]) -> f32 {
let mut sum = 0.0;
for i in 0..emotions.len() {
sum += emotions[i].intensity;
}
sum
}
}
Avoid Unnecessary Allocations
#![allow(unused)]
fn main() {
// Good: Reuse buffer
pub struct EmotionProcessor {
buffer: Vec<f32>,
}
impl EmotionProcessor {
pub fn process(&mut self, emotions: &[Emotion]) {
self.buffer.clear();
self.buffer.extend(emotions.iter().map(|e| e.intensity));
// Process buffer...
}
}
// Bad: Allocate every time
pub fn process(emotions: &[Emotion]) -> Vec<f32> {
emotions.iter().map(|e| e.intensity).collect() // New allocation
}
}
Use References When Possible
#![allow(unused)]
fn main() {
// Good: Borrow instead of clone
pub fn analyze_emotion(&self, emotion: &Emotion) -> Analysis {
Analysis {
intensity: emotion.intensity,
category: emotion.category.clone(), // Only clone when necessary
}
}
// Bad: Unnecessary clone
pub fn analyze_emotion(&self, emotion: Emotion) -> Analysis {
// Took ownership, forcing caller to clone
Analysis {
intensity: emotion.intensity,
category: emotion.category,
}
}
}
Document Performance Characteristics
#![allow(unused)]
fn main() {
/// Finds a companion by ID.
///
/// # Performance
///
/// O(1) average case using HashMap lookup.
pub fn find_companion(&self, id: CompanionId) -> Option<&Companion> {
self.companions.get(&id)
}
/// Sorts companions by emotion intensity.
///
/// # Performance
///
/// O(n log n) where n is the number of companions.
/// Consider using `find_max_emotion` for single maximum lookup.
pub fn sort_by_emotion(&mut self, emotion_name: &str) {
self.companions.sort_by(|a, b| {
a.get_emotion(emotion_name)
.cmp(&b.get_emotion(emotion_name))
});
}
}
Clippy and Formatting
Running Clippy
# Run clippy with all features
cargo clippy --all-targets --all-features
# Fix clippy warnings automatically (where possible)
cargo clippy --fix --all-targets --all-features
# Deny all warnings (for CI)
cargo clippy --all-targets --all-features -- -D warnings
Common Clippy Lints
Enable project-wide lints in lib.rs:
#![allow(unused)]
#![warn(
fn main() {
clippy::all,
clippy::pedantic,
clippy::cargo,
missing_docs,
rust_2018_idioms,
)]
// Allow specific lints when justified
#![allow(
clippy::module_name_repetitions, // CompanionBuilder in companion module is fine
clippy::must_use_candidate, // Not all pure functions need #[must_use]
)]
}
Code Formatting
# Format all code
cargo fmt
# Check formatting without applying
cargo fmt -- --check
# Format with custom config
cargo fmt -- --config max_width=100
rustfmt Configuration
Create rustfmt.toml:
max_width = 100
hard_tabs = false
tab_spaces = 4
newline_style = "Unix"
use_small_heuristics = "Default"
reorder_imports = true
reorder_modules = true
remove_nested_parens = true
edition = "2021"
Pre-commit Hook
Create .git/hooks/pre-commit:
#!/bin/sh
# Run formatter
cargo fmt -- --check
if [ $? -ne 0 ]; then
echo "Code is not formatted. Run 'cargo fmt' first."
exit 1
fi
# Run clippy
cargo clippy --all-targets --all-features -- -D warnings
if [ $? -ne 0 ]; then
echo "Clippy found issues. Fix them before committing."
exit 1
fi
exit 0
Consistent style makes code easier to read, review, and maintain. Use automated tools to enforce standards.
Adding New Features
This guide walks you through the process of contributing new features to AstraWeave, from initial proposal through implementation, testing, and documentation.
Feature Development Workflow
graph LR
subgraph Proposal["1. Proposal"]
RFC[RFC Discussion] --> DESIGN[Design Doc]
DESIGN --> APPROVAL[Team Review]
end
subgraph Implementation["2. Implementation"]
APPROVAL --> BRANCH[Feature Branch]
BRANCH --> CODE[Code + Tests]
CODE --> DOCS[Documentation]
end
subgraph Review["3. Review"]
DOCS --> PR[Pull Request]
PR --> CI[CI Validation]
CI --> REVIEW[Code Review]
REVIEW --> MERGE[Merge]
end
Phase 1: Proposal
Feature Request Format
Before implementing a significant feature, create a discussion or RFC:
## Feature: [Name]
### Summary
One paragraph describing the feature.
### Motivation
- Why is this needed?
- What problem does it solve?
- Who benefits?
### Design Overview
- High-level approach
- Key components affected
- API surface changes
### Alternatives Considered
- Option A: ...
- Option B: ...
- Why chosen approach is better
### Breaking Changes
- None / List any breaking changes
### Implementation Plan
1. Step 1
2. Step 2
3. ...
Design Document
For complex features, create a design document in docs/design/:
# Design: [Feature Name]
## Goals
- Primary objective
- Secondary objectives
## Non-Goals
- Explicitly out of scope
## Architecture
[Mermaid diagram or description]
## API Design
```rust
// Public API examples
Migration Path
How existing code adapts to the new feature.
Testing Strategy
How the feature will be tested.
Timeline
Estimated implementation phases.
## Phase 2: Implementation
### Branch Setup
```bash
git checkout main
git pull origin main
git checkout -b feature/my-feature-name
Project Structure
Understand where your feature belongs:
astraweave/
├── astraweave-ecs/ # Core ECS (rare modifications)
├── astraweave-ai/ # AI systems
├── astraweave-render/ # Rendering
├── astraweave-physics/ # Physics
├── astraweave-gameplay/ # Gameplay systems
├── astraweave-*/ # Other subsystems
└── examples/ # Feature examples
Creating a New Crate
For substantial features, create a new crate:
cargo new --lib astraweave-myfeature
Cargo.toml template:
[package]
name = "astraweave-myfeature"
version = "0.1.0"
edition = "2021"
license = "MIT OR Apache-2.0"
description = "Brief description"
repository = "https://github.com/astraweave/astraweave"
keywords = ["gamedev", "astraweave"]
[dependencies]
astraweave-ecs = { path = "../astraweave-ecs" }
[dev-dependencies]
criterion = "0.5"
[features]
default = []
[[bench]]
name = "my_benchmark"
harness = false
Adding to Workspace
Update root Cargo.toml:
[workspace]
members = [
# ... existing crates
"astraweave-myfeature",
]
Module Structure
Follow the standard module layout:
astraweave-myfeature/
├── src/
│ ├── lib.rs # Public API, re-exports
│ ├── prelude.rs # Common imports
│ ├── component.rs # ECS components
│ ├── system.rs # ECS systems
│ ├── resource.rs # ECS resources
│ └── internal/ # Private implementation
│ └── mod.rs
├── tests/
│ └── integration.rs # Integration tests
├── benches/
│ └── benchmark.rs # Performance benchmarks
└── Cargo.toml
Implementation Guidelines
Public API Design
#![allow(unused)]
fn main() {
pub mod prelude {
pub use crate::MyComponent;
pub use crate::MyResource;
pub use crate::my_system;
}
#[derive(Component, Debug, Clone)]
pub struct MyComponent {
pub visible_field: f32,
}
impl MyComponent {
pub fn new(value: f32) -> Self {
Self { visible_field: value }
}
pub fn with_option(mut self, option: bool) -> Self {
self
}
}
#[derive(Resource, Default)]
pub struct MyResource {
config: MyConfig,
}
pub fn my_system(
query: Query<&MyComponent>,
resource: Res<MyResource>,
) {
for component in query.iter() {
// Implementation
}
}
}
Error Handling
Define clear error types:
#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum MyFeatureError {
#[error("Invalid configuration: {0}")]
InvalidConfig(String),
#[error("Resource not found: {0}")]
NotFound(String),
#[error("Operation failed: {0}")]
OperationFailed(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, MyFeatureError>;
}
Feature Flags
Use feature flags for optional functionality:
[features]
default = ["standard"]
standard = []
advanced = ["dep:optional-crate"]
serde = ["dep:serde"]
[dependencies]
optional-crate = { version = "1.0", optional = true }
serde = { version = "1.0", features = ["derive"], optional = true }
#![allow(unused)]
fn main() {
#[cfg(feature = "advanced")]
pub mod advanced {
pub fn advanced_function() {
// Only available with "advanced" feature
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for MyComponent {
// Serialization implementation
}
}
Phase 3: Testing
Unit Tests
Include tests alongside implementation:
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_creation() {
let component = MyComponent::new(42.0);
assert_eq!(component.visible_field, 42.0);
}
#[test]
fn test_builder_pattern() {
let component = MyComponent::new(1.0)
.with_option(true);
// Assertions
}
#[test]
#[should_panic(expected = "Invalid value")]
fn test_invalid_input() {
MyComponent::new(-1.0);
}
}
}
Integration Tests
Create tests/integration.rs:
#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
use astraweave_myfeature::prelude::*;
#[test]
fn test_system_integration() {
let mut world = World::new();
world.spawn(MyComponent::new(10.0));
world.insert_resource(MyResource::default());
let mut schedule = Schedule::default();
schedule.add_system(my_system);
schedule.run(&mut world);
// Verify results
}
#[test]
fn test_full_workflow() {
// Test complete feature workflow
}
}
Benchmarks
Create benches/benchmark.rs:
#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
use astraweave_myfeature::*;
fn benchmark_creation(c: &mut Criterion) {
c.bench_function("component_create", |b| {
b.iter(|| MyComponent::new(42.0))
});
}
fn benchmark_system(c: &mut Criterion) {
let mut group = c.benchmark_group("system_throughput");
for count in [100, 1000, 10000].iter() {
group.bench_with_input(
BenchmarkId::from_parameter(count),
count,
|b, &count| {
let mut world = setup_world(count);
let mut schedule = setup_schedule();
b.iter(|| schedule.run(&mut world));
},
);
}
group.finish();
}
criterion_group!(benches, benchmark_creation, benchmark_system);
criterion_main!(benches);
}
Run benchmarks:
cargo bench -p astraweave-myfeature
Phase 4: Documentation
API Documentation
Document all public items:
#![allow(unused)]
fn main() {
/// A component that tracks feature state.
///
/// # Examples
///
/// ```rust
/// use astraweave_myfeature::MyComponent;
///
/// let component = MyComponent::new(42.0);
/// assert_eq!(component.visible_field, 42.0);
/// ```
///
/// # Panics
///
/// Panics if `value` is negative.
#[derive(Component)]
pub struct MyComponent {
/// The primary value tracked by this component.
pub visible_field: f32,
}
/// Creates a new component with the given value.
///
/// # Arguments
///
/// * `value` - The initial value (must be non-negative)
///
/// # Returns
///
/// A new `MyComponent` instance.
///
/// # Examples
///
/// ```rust
/// let component = MyComponent::new(10.0);
/// ```
impl MyComponent {
pub fn new(value: f32) -> Self {
assert!(value >= 0.0, "Invalid value");
Self { visible_field: value }
}
}
}
User Guide
Add documentation to docs/src/:
# My Feature
Brief introduction explaining what the feature does.
## Quick Start
```rust
use astraweave_myfeature::prelude::*;
// Minimal example
Concepts
Explain key concepts and terminology.
Usage
Basic Usage
Step-by-step instructions.
Advanced Usage
More complex scenarios.
Configuration
Available options and settings.
Best Practices
Tips for effective use.
Related
### Update SUMMARY.md
Add your documentation to `docs/src/SUMMARY.md`:
```markdown
- [Core Systems](core-systems/index.md)
- [My Feature](core-systems/myfeature.md) # Add here
Phase 5: Pull Request
Pre-Submit Checklist
Before opening a PR:
cargo fmt --all
cargo clippy --all-targets --all-features -- -D warnings
cargo test --all
cargo doc --no-deps --all-features
PR Template
## Summary
Brief description of changes.
## Changes
- Added `MyComponent` for tracking X
- Implemented `my_system` for processing Y
- Added benchmarks for performance validation
## Testing
- [x] Unit tests added
- [x] Integration tests added
- [x] Benchmarks added
- [x] Manual testing completed
## Documentation
- [x] API docs complete
- [x] User guide added
- [x] SUMMARY.md updated
## Breaking Changes
None / List any breaking changes.
## Related Issues
Fixes #123
Related to #456
CI Requirements
Your PR must pass:
- Format check:
cargo fmt --check - Lint check:
cargo clippy - Test suite:
cargo test --all - Doc generation:
cargo doc - Benchmark regression: No significant slowdowns
Code Review
Expect reviewers to check:
- API design and ergonomics
- Performance implications
- Test coverage
- Documentation quality
- Breaking change management
Best Practices
1. **Start Small**: Begin with minimal viable feature, iterate
2. **Design for Extension**: Make it easy to add functionality later
3. **Follow Conventions**: Match existing code style and patterns
4. **Test Edge Cases**: Cover error conditions and boundaries
5. **Document Intent**: Explain why, not just what
- **Scope Creep**: Resist adding "just one more thing"
- **Missing Tests**: Every public function needs tests
- **Breaking Changes**: Avoid unless absolutely necessary
- **Performance Regression**: Always benchmark critical paths
Example: Adding a New System
Here’s a complete example of adding a health regeneration system:
#![allow(unused)]
fn main() {
// astraweave-gameplay/src/health_regen.rs
use astraweave_ecs::prelude::*;
/// Configuration for health regeneration.
#[derive(Resource)]
pub struct HealthRegenConfig {
/// Base regeneration rate (HP per second).
pub base_rate: f32,
/// Delay before regeneration starts after damage.
pub regen_delay: f32,
}
impl Default for HealthRegenConfig {
fn default() -> Self {
Self {
base_rate: 5.0,
regen_delay: 3.0,
}
}
}
/// Enables health regeneration on an entity.
#[derive(Component)]
pub struct HealthRegen {
/// Multiplier applied to base regeneration rate.
pub rate_multiplier: f32,
/// Time since last damage taken.
pub time_since_damage: f32,
}
impl Default for HealthRegen {
fn default() -> Self {
Self {
rate_multiplier: 1.0,
time_since_damage: 0.0,
}
}
}
/// System that regenerates health over time.
pub fn health_regen_system(
config: Res<HealthRegenConfig>,
time: Res<Time>,
mut query: Query<(&mut Health, &mut HealthRegen)>,
) {
let dt = time.delta_seconds();
for (mut health, mut regen) in query.iter_mut() {
regen.time_since_damage += dt;
if regen.time_since_damage >= config.regen_delay {
let regen_amount = config.base_rate * regen.rate_multiplier * dt;
health.current = (health.current + regen_amount).min(health.max);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_regen_after_delay() {
// Test implementation
}
}
}
Related Documentation
- Contributing Guide - General contribution guidelines
- Code Style - Coding standards
- Testing Guide - Testing practices
- Building - Build instructions
Performance Optimization
This guide covers profiling, benchmarking, and optimization techniques for AstraWeave games and the engine itself.
Performance Philosophy
AstraWeave targets consistent 60+ FPS gameplay with AI-native features. Key principles:
- Measure First: Always profile before optimizing
- Budget Time: Allocate frame time across systems
- Batch Operations: Minimize per-entity overhead
- Cache Strategically: Trade memory for speed where appropriate
- Offload to Threads: Parallelize independent work
Frame Budget
At 60 FPS, each frame has ~16.67ms. Recommended allocation:
| System | Budget | Notes |
|---|---|---|
| Game Logic | 2-3ms | ECS systems, gameplay |
| AI | 2-4ms | Perception, planning, behaviors |
| Physics | 2-3ms | Collision, dynamics |
| Rendering | 6-8ms | Draw calls, GPU submission |
| Audio | 0.5-1ms | Mixing, spatial |
| Buffer | 1-2ms | Headroom for spikes |
Profiling Tools
Tracy Integration
AstraWeave integrates with Tracy for real-time profiling:
[dependencies]
astraweave-profiling = { version = "0.1", features = ["tracy"] }
#![allow(unused)]
fn main() {
use astraweave_profiling::*;
fn my_system(query: Query<&MyComponent>) {
profile_scope!("my_system");
for component in query.iter() {
profile_scope!("process_entity");
// Work
}
}
}
Run with Tracy:
cargo run --release --features tracy
Built-in Profiler
Enable the debug overlay:
#![allow(unused)]
fn main() {
use astraweave_profiling::prelude::*;
app.add_plugin(ProfilingPlugin::default());
}
Press F3 in-game to toggle the performance overlay showing:
- Frame time graph
- System timing breakdown
- Memory usage
- Draw call count
CPU Profiling
For detailed CPU analysis:
# Linux (perf)
perf record -g cargo run --release
perf report
# Windows (ETW)
cargo build --release
# Use Windows Performance Analyzer
# macOS (Instruments)
cargo instruments --release -t time
GPU Profiling
# NVIDIA Nsight
cargo run --release
# Attach Nsight Graphics
# RenderDoc
cargo run --release --features renderdoc
# Press F12 to capture frame
Optimization Techniques
ECS Optimization
Query Optimization
#![allow(unused)]
fn main() {
// Bad: Iterating all entities
fn slow_system(query: Query<&Transform>) {
for transform in query.iter() {
// Processes all entities with Transform
}
}
// Good: Filter to relevant entities
fn fast_system(
query: Query<&Transform, (With<Enemy>, Without<Dead>)>,
) {
for transform in query.iter() {
// Only active enemies
}
}
}
Parallel Iteration
#![allow(unused)]
fn main() {
use rayon::prelude::*;
fn parallel_system(query: Query<&mut Transform>) {
query.par_iter_mut().for_each(|mut transform| {
// Thread-safe processing
transform.translation.y += 0.1;
});
}
}
Change Detection
#![allow(unused)]
fn main() {
fn efficient_update(
query: Query<&MyComponent, Changed<MyComponent>>,
) {
for component in query.iter() {
// Only processes recently changed entities
}
}
}
Archetypes
Group components that are commonly accessed together:
#![allow(unused)]
fn main() {
// Good: Components often queried together
#[derive(Bundle)]
struct EnemyBundle {
transform: Transform,
health: Health,
ai: AiAgent,
collider: Collider,
}
// Avoid: Rarely used components on common entities
struct RarelyUsedData { /* ... */ }
}
Memory Optimization
Component Size
#![allow(unused)]
fn main() {
// Bad: Large component
#[derive(Component)]
struct LargeComponent {
data: [f32; 1000], // 4KB per entity
name: String,
}
// Good: Split into data and reference
#[derive(Component)]
struct SmallComponent {
data_handle: Handle<LargeData>, // 8 bytes
flags: u8,
}
}
Object Pools
#![allow(unused)]
fn main() {
use astraweave_ecs::pool::*;
#[derive(Resource)]
struct BulletPool {
pool: EntityPool<BulletBundle>,
}
impl BulletPool {
fn spawn(&mut self, commands: &mut Commands) -> Entity {
self.pool.get_or_spawn(commands, || BulletBundle::default())
}
fn despawn(&mut self, entity: Entity) {
self.pool.return_entity(entity);
}
}
}
Arena Allocation
For temporary allocations:
#![allow(unused)]
fn main() {
use bumpalo::Bump;
fn batch_process(entities: &[Entity]) {
let arena = Bump::new();
let temp_data: &mut [Vec3] = arena.alloc_slice_fill_default(entities.len());
// Work with temp_data
// Arena automatically freed at scope end
}
}
Rendering Optimization
Batching
#![allow(unused)]
fn main() {
// Enable instanced rendering for repeated meshes
#[derive(Component)]
struct InstancedMesh {
mesh: Handle<Mesh>,
material: Handle<Material>,
instances: Vec<Transform>,
}
}
Level of Detail
#![allow(unused)]
fn main() {
#[derive(Component)]
struct LodGroup {
distances: [f32; 3],
meshes: [Handle<Mesh>; 3],
}
fn lod_system(
camera: Query<&Transform, With<Camera>>,
mut lod_query: Query<(&Transform, &LodGroup, &mut Handle<Mesh>)>,
) {
let camera_pos = camera.single().translation;
for (transform, lod, mut mesh) in lod_query.iter_mut() {
let distance = transform.translation.distance(camera_pos);
let lod_level = if distance < lod.distances[0] { 0 }
else if distance < lod.distances[1] { 1 }
else { 2 };
*mesh = lod.meshes[lod_level].clone();
}
}
}
Culling
#![allow(unused)]
fn main() {
#[derive(Component)]
struct Visibility {
pub visible: bool,
pub render_layers: u32,
}
fn frustum_culling_system(
camera: Query<(&Camera, &Transform)>,
mut renderables: Query<(&Transform, &Aabb, &mut Visibility)>,
) {
let (camera, cam_transform) = camera.single();
let frustum = camera.compute_frustum(cam_transform);
for (transform, aabb, mut visibility) in renderables.iter_mut() {
let world_aabb = aabb.transformed(transform);
visibility.visible = frustum.intersects_aabb(&world_aabb);
}
}
}
AI Optimization
Tick Budgeting
#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct AiBudget {
pub max_ms_per_frame: f32,
pub agents_processed: usize,
}
fn budgeted_ai_system(
mut budget: ResMut<AiBudget>,
mut agents: Query<&mut AiAgent>,
time: Res<Time>,
) {
let start = std::time::Instant::now();
budget.agents_processed = 0;
for mut agent in agents.iter_mut() {
if start.elapsed().as_secs_f32() * 1000.0 > budget.max_ms_per_frame {
break;
}
agent.tick();
budget.agents_processed += 1;
}
}
}
LOD for AI
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct AiLod {
pub distance_from_player: f32,
pub update_frequency: u32,
pub frames_since_update: u32,
}
fn ai_lod_system(
player: Query<&Transform, With<Player>>,
mut ai_query: Query<(&Transform, &mut AiLod, &mut AiAgent)>,
) {
let player_pos = player.single().translation;
for (transform, mut lod, mut agent) in ai_query.iter_mut() {
lod.distance_from_player = transform.translation.distance(player_pos);
lod.update_frequency = match lod.distance_from_player {
d if d < 20.0 => 1, // Every frame
d if d < 50.0 => 2, // Every 2 frames
d if d < 100.0 => 5, // Every 5 frames
_ => 10, // Every 10 frames
};
lod.frames_since_update += 1;
if lod.frames_since_update >= lod.update_frequency {
lod.frames_since_update = 0;
agent.tick();
}
}
}
}
LLM Caching
#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct LlmCache {
cache: LruCache<u64, String>,
hit_count: u64,
miss_count: u64,
}
impl LlmCache {
pub fn get_or_generate<F>(
&mut self,
prompt: &str,
generator: F,
) -> &str
where
F: FnOnce() -> String,
{
let hash = hash_prompt(prompt);
if !self.cache.contains(&hash) {
self.miss_count += 1;
let response = generator();
self.cache.put(hash, response);
} else {
self.hit_count += 1;
}
self.cache.get(&hash).unwrap()
}
}
}
Physics Optimization
Broad Phase
#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct PhysicsConfig {
pub broad_phase: BroadPhaseType,
pub substeps: u32,
pub velocity_iterations: u32,
}
pub enum BroadPhaseType {
BruteForce, // < 100 entities
SpatialHash, // 100-1000 entities
BvhTree, // > 1000 entities
}
}
Sleeping
#![allow(unused)]
fn main() {
#[derive(Component)]
pub struct RigidBody {
pub sleeping: bool,
pub sleep_threshold: f32,
pub sleep_timer: f32,
}
fn sleep_system(mut bodies: Query<(&Velocity, &mut RigidBody)>) {
for (velocity, mut body) in bodies.iter_mut() {
if velocity.linear.length_squared() < body.sleep_threshold {
body.sleep_timer += delta;
if body.sleep_timer > 0.5 {
body.sleeping = true;
}
} else {
body.sleep_timer = 0.0;
body.sleeping = false;
}
}
}
}
Benchmarking
Criterion Benchmarks
#![allow(unused)]
fn main() {
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId};
fn bench_ecs_iteration(c: &mut Criterion) {
let mut group = c.benchmark_group("ecs_iteration");
for entity_count in [1000, 10000, 100000] {
let world = create_world_with_entities(entity_count);
group.bench_with_input(
BenchmarkId::new("query", entity_count),
&world,
|b, world| {
b.iter(|| {
let mut count = 0;
for _ in world.query::<&Transform>().iter() {
count += 1;
}
count
});
},
);
}
group.finish();
}
criterion_group!(benches, bench_ecs_iteration);
criterion_main!(benches);
}
Performance Regression Testing
# .github/workflows/bench.yml
name: Benchmarks
on: [push, pull_request]
jobs:
benchmark:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run benchmarks
run: cargo bench --all -- --save-baseline new
- name: Compare with main
run: |
git fetch origin main
cargo bench --all -- --baseline main --save-baseline new
Common Performance Issues
Issue: Frame Spikes
Symptoms: Occasional stutters, inconsistent frame times
Causes:
- GC in scripting
- Asset loading on main thread
- Large allocations
Solutions:
#![allow(unused)]
fn main() {
// Pre-warm asset loading
fn warmup_system(asset_server: Res<AssetServer>) {
asset_server.load::<Mesh>("meshes/common.gltf");
asset_server.load::<Texture>("textures/atlas.png");
}
// Use streaming for large assets
let handle = asset_server.load_async::<LargeAsset>("path").await;
}
Issue: High CPU Usage
Symptoms: High CPU, low GPU utilization
Causes:
- Inefficient queries
- Too many systems
- Excessive allocations
Solutions:
#![allow(unused)]
fn main() {
// Combine related systems
fn combined_system(
mut query: Query<(&mut Transform, &Velocity, &mut Health)>,
) {
for (mut transform, velocity, mut health) in query.iter_mut() {
transform.translation += velocity.0;
health.regen();
}
}
}
Issue: Memory Growth
Symptoms: Increasing memory over time
Causes:
- Entity leaks
- Cache growth
- Asset retention
Solutions:
#![allow(unused)]
fn main() {
// Periodic cleanup
fn cleanup_system(
mut commands: Commands,
dead_entities: Query<Entity, With<Dead>>,
time: Res<Time>,
mut cleanup_timer: ResMut<CleanupTimer>,
) {
cleanup_timer.tick(time.delta());
if cleanup_timer.just_finished() {
for entity in dead_entities.iter() {
commands.entity(entity).despawn_recursive();
}
}
}
}
Best Practices
1. **Profile in Release**: Debug builds are 10-50x slower
2. **Measure Realistically**: Test with actual content, not empty scenes
3. **Test on Target Hardware**: Don't only test on dev machines
4. **Budget Early**: Set performance targets before development
5. **Automate Testing**: Catch regressions in CI
- **Premature Optimization**: Don't optimize without profiling data
- **Micro-benchmarks**: Real-world performance may differ
- **Ignoring Memory**: CPU speed means nothing if you're thrashing cache
- **Single-threaded Thinking**: Utilize all cores
Related Documentation
- Configuration - Performance-related settings
- Building - Release build optimization
- Testing - Performance test strategies
- Best Practices - General best practices
API Reference
Documentation Accuracy: All exports listed below are verified against actual
lib.rsfiles as of v0.4.0. Coverage percentages sourced from Master Coverage Report.
Browse the Rust API documentation for all AstraWeave crates.
Quick Links
| Crate | Description | Coverage | Status |
|---|---|---|---|
| astraweave-ecs | Entity Component System implementation | 83.2% | Production |
| astraweave-core | Core ECS, simulation, and world systems | 79.1% | Production |
| astraweave-ai | AI orchestration and planning | 71.3% | Production |
| astraweave-render | wgpu 25-based renderer | 67.4% | Production |
| astraweave-physics | Rapier3D 0.22 integration | 76.8% | Production |
| astraweave-fluids | SPH fluid simulation | 94.2% | Production |
| astraweave-nav | Navigation and pathfinding | 72.1% | Production |
| astraweave-gameplay | Combat physics, damage | 68.9% | Production |
| astraweave-terrain | Voxel terrain generation | 71.5% | Production |
| astraweave-llm | LLM integration (Qwen3-8B) | 58.3% | Beta |
| astraweave-behavior | Behavior trees, utility AI | 74.2% | Production |
Core Engine Crates
astraweave-ecs (83.2% coverage)
High-performance Entity Component System:
| Export | Description |
|---|---|
World | Main container for all ECS data |
Entity | Lightweight entity handles with generational indices |
App | Application builder and runner |
Schedule | System scheduling and ordering |
Component | Trait for component data |
Resource | Singleton data storage |
Query | Efficient component access patterns |
CommandBuffer | Deferred entity operations |
Events | Event queues and readers |
#![allow(unused)]
fn main() {
use astraweave_ecs::{World, Entity, App, Schedule, Component, Resource, Query, CommandBuffer};
}
astraweave-core (79.1% coverage)
Core engine systems and AI infrastructure:
| Module | Description |
|---|---|
capture_replay | Deterministic frame recording/playback |
perception | AI world observation and filtering |
schema | WorldSnapshot, PlanIntent, ActionStep |
sim | Game state management |
tool_sandbox | Secure AI action execution |
tool_vocabulary | 37-tool AI action vocabulary |
validation | Input/output validation |
world | World state management |
#![allow(unused)]
fn main() {
use astraweave_core::{
schema::{WorldSnapshot, PlanIntent, ActionStep},
capture_replay::CaptureReplay,
perception::PerceptionFilter,
};
}
Generate rustdoc: cargo doc -p astraweave-core --no-deps --open
AI & Behavior Crates
astraweave-ai (71.3% coverage)
AI orchestration layer with GOAP+LLM hybrid support:
| Module | Feature Gate | Description |
|---|---|---|
core_loop | — | Perception-Reasoning-Planning-Action loop |
orchestrator | — | AI coordination trait |
tool_sandbox | — | Tool validation and sandboxing |
async_task | llm_orchestrator | Async LLM task wrapper |
llm_executor | llm_orchestrator | LLM plan generation |
goap | llm_orchestrator | Goal-oriented action planning |
AIArbiter | llm_orchestrator | GOAP+LLM hybrid (101.7ns control) |
#![allow(unused)]
fn main() {
use astraweave_ai::{core_loop, orchestrator::Orchestrator};
// With llm_orchestrator feature:
#[cfg(feature = "llm_orchestrator")]
use astraweave_ai::{AIArbiter, goap::GoapOrchestrator};
}
View Full Documentation → | Arbiter Guide →
astraweave-behavior (74.2% coverage)
Behavior systems:
- BehaviorGraph - Hierarchical behavior nodes (BehaviorNode enum)
- BehaviorContext - Shared context for tick execution
- BehaviorStatus - Success/Failure/Running states
- UtilityAI - Score-based decision making
#![allow(unused)]
fn main() {
use astraweave_behavior::{BehaviorGraph, BehaviorNode, BehaviorContext, BehaviorStatus};
}
Generate rustdoc: cargo doc -p astraweave-behavior --no-deps --open
astraweave-llm (58.3% coverage)
LLM integration (Qwen3-8B via Ollama):
| Export | Description |
|---|---|
LlmOrchestrator | Provider-agnostic LLM coordination |
FallbackChain | 4-tier fallback (Full LLM → Simplified → Heuristic → Emergency) |
OllamaClient | Ollama API client |
#![allow(unused)]
fn main() {
use astraweave_llm::{LlmOrchestrator, FallbackChain};
}
Generate rustdoc: cargo doc -p astraweave-llm --no-deps --open
Rendering & Graphics Crates
astraweave-render (67.4% coverage)
wgpu 25.0.2 based rendering (40+ modules):
| Category | Key Modules |
|---|---|
| Pipeline | camera, clustered, deferred, forward, renderer |
| Animation | animation, animation_blending, skeleton, skinning_gpu |
| Lighting | light, shadow, ibl_manager, post_fx_shader |
| Geometry | mesh, culling, lod_generator, instancing |
| Materials | material, texture_array, vertex_compression |
| Effects | water, volumetric, particle_system |
#![allow(unused)]
fn main() {
use astraweave_render::{
camera::Camera,
material::Material,
mesh::Mesh,
renderer::Renderer,
};
}
Physics & Navigation Crates
astraweave-physics (76.8% coverage)
Rapier3D 0.22 integration:
| Module | Performance | Description |
|---|---|---|
rigid_body | — | Physics bodies and dynamics |
collider | — | Collision shapes and detection |
character_controller | 114ns/move | Player movement |
spatial_hash | 99.96% fewer checks | Broad-phase acceleration |
async_scheduler | — | Parallel physics stepping |
#![allow(unused)]
fn main() {
use astraweave_physics::{
rigid_body::RigidBody,
collider::Collider,
character_controller::CharacterController,
};
}
astraweave-fluids (94.2% coverage, 2,404 tests)
SPH fluid simulation (A+ grade):
| Module | Description |
|---|---|
solver | SPH pressure/viscosity solver |
surface_tension | Surface tension forces |
boundary | Domain boundary handling |
spatial_hash | Neighbor lookup acceleration |
#![allow(unused)]
fn main() {
use astraweave_fluids::{FluidWorld, Particle, FluidConfig};
}
View Full Documentation → | Fluids Guide →
astraweave-nav (72.1% coverage)
Navigation and pathfinding:
| Module | Description |
|---|---|
navmesh | Navigation mesh generation |
pathfinding | A* and hierarchical planning |
portal_graph | Room-to-room navigation |
agent | Navigation agent component |
#![allow(unused)]
fn main() {
use astraweave_nav::{Navmesh, PathQuery, Agent};
}
Gameplay Crates
astraweave-gameplay (68.9% coverage)
Combat and game mechanics:
| Module | Description |
|---|---|
combat_physics | Raycast attacks, parry, iframes |
damage_system | Damage calculation |
ability_system | Ability cooldowns and effects |
#![allow(unused)]
fn main() {
use astraweave_gameplay::combat_physics::perform_attack_sweep;
}
Generate rustdoc: cargo doc -p astraweave-gameplay --no-deps --open
astraweave-terrain (71.5% coverage)
Procedural terrain generation:
| Module | Description |
|---|---|
voxel_mesh | Marching cubes (256 configurations) |
biome | Biome distribution and blending |
chunk | Terrain chunking (15.06ms/chunk) |
#![allow(unused)]
fn main() {
use astraweave_terrain::{VoxelMesh, Biome, TerrainChunk};
}
Generate rustdoc: cargo doc -p astraweave-terrain --no-deps --open | Terrain Guide →
Infrastructure Crates
astraweave-audio (69.2% coverage)
Spatial audio with rodio:
| Module | Description |
|---|---|
audio_engine | 4-bus mixer system (master, music, SFX, voice) |
spatial | 3D audio positioning |
crossfade | Music transitions |
#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, SpatialAudio};
}
astraweave-scene (74.6% coverage)
World streaming and partitioning:
| Module | Description |
|---|---|
streaming | Async cell loading |
partition | World partitioning |
#![allow(unused)]
fn main() {
use astraweave_scene::{WorldCell, CellLoader};
}
Generate rustdoc: cargo doc -p astraweave-scene --no-deps --open
Building API Docs Locally
# Generate all API documentation
cargo doc --workspace --no-deps --open
# Generate for specific crate with all features
cargo doc --package astraweave-ai --all-features --no-deps --open
# Generate with private items (for internal development)
cargo doc --package astraweave-core --document-private-items --no-deps --open
Coverage by Tier
| Tier | Crates | Avg Coverage | Status |
|---|---|---|---|
| Tier 1 (Critical) | ecs, core, ai, render | 75.3% | ✅ Production |
| Tier 2 (Important) | physics, nav, gameplay | 72.6% | ✅ Production |
| Tier 3 (Supporting) | audio, scene, terrain | 71.8% | ✅ Production |
| Tier 4 (Specialized) | fluids, llm, prompts | 71.5% | ✅ Production |
See Master Coverage Report for detailed breakdown.
See Also
- Crate Overview - High-level crate descriptions
- Architecture - System design
- Examples - Working code examples
- Benchmarks - Performance data
ECS API Reference
Crate:
astraweave-ecs
Coverage: 94.26%
Tests: 1,200+
The Entity Component System is the foundation of AstraWeave, providing deterministic, cache-friendly entity management with archetype-based storage.
Quick Links
- rustdoc (when published)
- Source
- ECS Core Concepts
Core Types
World
The central container for all entities, components, and resources.
#![allow(unused)]
fn main() {
use astraweave_ecs::World;
let mut world = World::new();
// Spawn entities
let entity = world.spawn((Position::default(), Velocity::default()));
// Access resources
world.insert_resource(GameTime::default());
let time = world.resource::<GameTime>();
}
Key Methods:
new()→World- Create empty worldspawn(bundle)→Entity- Create entity with componentsdespawn(entity)- Remove entityinsert_resource<R>(resource)- Add singleton resourceresource<R>()→&R- Get resource referencequery<Q>()→Query<Q>- Create component query
Entity
Lightweight 64-bit identifier (32-bit index + 32-bit generation).
#![allow(unused)]
fn main() {
use astraweave_ecs::Entity;
// Entities are created via World::spawn()
let entity = world.spawn(MyBundle::default());
// Check if entity exists
if world.contains(entity) {
// Entity is alive
}
// Create from raw (unsafe, for FFI)
let raw = Entity::from_raw(42, 1);
}
Properties:
- 8 bytes memory footprint
- Copy, Clone, Hash, Eq
- Safe to store in collections
- Generation prevents use-after-free
App
Application builder with plugin support.
#![allow(unused)]
fn main() {
use astraweave_ecs::App;
App::new()
.add_plugin(DefaultPlugins)
.add_startup_system(setup)
.add_system(update)
.run();
}
Key Methods:
add_plugin<P>(plugin)- Add functionality bundleadd_startup_system(system)- Run once at startadd_system(system)- Add to main loopadd_system_to_stage(stage, system)- Add to specific stagerun()- Start main loop
Schedule
System execution scheduler with parallel support.
#![allow(unused)]
fn main() {
use astraweave_ecs::{Schedule, SystemStage};
let mut schedule = Schedule::new();
schedule.add_system_to_stage(SystemStage::Update, my_system);
schedule.run(&mut world);
}
System Stages (in execution order):
First- Pre-frame setupPreUpdate- Input processingUpdate- Main game logicPostUpdate- Physics, AILast- Rendering prep
Component
Trait for data attached to entities (auto-implemented for 'static + Send + Sync).
#![allow(unused)]
fn main() {
use astraweave_ecs::Component;
#[derive(Component, Default)]
struct Position {
x: f32,
y: f32,
z: f32,
}
#[derive(Component)]
struct Velocity(Vec3);
#[derive(Component)]
struct Name(String);
}
Requirements:
'staticlifetimeSend + Sync(for parallel systems)- Prefer small, focused components
Resource
Singleton data shared across systems.
#![allow(unused)]
fn main() {
use astraweave_ecs::Resource;
#[derive(Resource, Default)]
struct GameTime {
elapsed: f32,
delta: f32,
}
#[derive(Resource)]
struct Settings {
volume: f32,
difficulty: Difficulty,
}
// Access in systems
fn update_time(mut time: ResMut<GameTime>) {
time.elapsed += time.delta;
}
}
Query
Efficient iteration over component combinations.
#![allow(unused)]
fn main() {
use astraweave_ecs::Query;
// Read-only query
fn read_system(query: Query<&Position>) {
for pos in query.iter() {
println!("Position: {:?}", pos);
}
}
// Mutable query
fn write_system(mut query: Query<&mut Position>) {
for mut pos in query.iter_mut() {
pos.x += 1.0;
}
}
// Multiple components
fn complex_query(query: Query<(&Position, &Velocity, Option<&Name>)>) {
for (pos, vel, name) in query.iter() {
// name is Option<&Name>
}
}
// Filters
fn filtered_query(query: Query<&Position, With<Player>>) {
// Only entities with Player component
}
}
Filter Types:
With<T>- Entity must have TWithout<T>- Entity must not have TOr<(A, B)>- Entity has A or BAdded<T>- T was just addedChanged<T>- T was modified
CommandBuffer
Deferred entity/component modifications (thread-safe).
#![allow(unused)]
fn main() {
use astraweave_ecs::CommandBuffer;
fn spawn_system(mut commands: Commands) {
// Spawn entity with components
commands.spawn((Position::default(), Velocity::default()));
// Modify existing entity
commands.entity(some_entity)
.insert(Health(100))
.remove::<Poisoned>();
// Despawn
commands.entity(dead_entity).despawn();
}
}
Key Methods:
spawn(bundle)→EntityCommands- Queue entity spawnentity(entity)→EntityCommands- Get entity commandsinsert_resource<R>(resource)- Queue resource insertremove_resource<R>()- Queue resource removal
Events
Type-safe event channel for system communication.
#![allow(unused)]
fn main() {
use astraweave_ecs::{Events, EventReader, EventWriter};
struct DamageEvent {
target: Entity,
amount: f32,
}
fn damage_sender(mut events: EventWriter<DamageEvent>) {
events.send(DamageEvent { target: enemy, amount: 50.0 });
}
fn damage_receiver(mut events: EventReader<DamageEvent>) {
for event in events.iter() {
println!("Entity {:?} took {} damage", event.target, event.amount);
}
}
}
Event Lifecycle:
- Events sent via
EventWriter - Events read via
EventReader - Events cleared at end of frame
- Double-buffered for reliable delivery
Archetype Storage
AstraWeave uses archetype-based storage for optimal cache performance:
Archetype A: [Position, Velocity]
┌──────────┬──────────┐
│ Position │ Velocity │
├──────────┼──────────┤
│ (1,2,3) │ (0,1,0) │ Entity 1
│ (4,5,6) │ (1,0,0) │ Entity 2
│ (7,8,9) │ (0,0,1) │ Entity 3
└──────────┴──────────┘
Archetype B: [Position, Velocity, Health]
┌──────────┬──────────┬────────┐
│ Position │ Velocity │ Health │
├──────────┼──────────┼────────┤
│ (0,0,0) │ (0,0,0) │ 100 │ Entity 4
└──────────┴──────────┴────────┘
Benefits:
- Cache-friendly iteration
- No pointer chasing
- Parallel-safe by design
Performance
| Operation | Latency | Notes |
|---|---|---|
| World creation | 25.8 ns | Empty world |
| Entity spawn | 420 ns | With components |
| Component access | <1 ns | Direct archetype access |
| Query iteration | ~2 ns/entity | Cache-optimal |
Feature Flags
| Feature | Description | Default |
|---|---|---|
parallel | Parallel system execution | ✅ |
tracing | Tracy profiling integration | ❌ |
serde | Serialization support | ❌ |
[dependencies]
astraweave-ecs = { version = "0.4", features = ["serde"] }
See Also
AI API Reference
Crate:
astraweave-ai
Coverage: ~75%
Tests: 400+
The AI system provides the core perception-reasoning-planning-action loop, orchestration, and tool validation for AI-native game development.
Quick Links
- rustdoc (when published)
- Source
- AI Core Concepts
- Arbiter System
Core Modules
core_loop
The fundamental perception → reasoning → planning → action cycle.
#![allow(unused)]
fn main() {
use astraweave_ai::core_loop::{CoreLoop, WorldSnapshot, PlanIntent, ActionStep};
// Build perception
let snapshot = WorldSnapshot {
t: game_time,
player: player_state,
me: companion_state,
enemies: detected_enemies,
pois: points_of_interest,
obstacles: obstacle_positions,
objective: current_objective,
};
// Execute AI cycle
let plan = orchestrator.plan(&mut world, &snapshot)?;
// Execute first action
if let Some(action) = plan.steps.first() {
execute_action(action);
}
}
Key Types:
WorldSnapshot- Filtered world state for AI perceptionPlanIntent- Validated action sequence from AIActionStep- Individual executable action
orchestrator
Trait-based AI planning abstraction.
#![allow(unused)]
fn main() {
use astraweave_ai::orchestrator::Orchestrator;
pub trait Orchestrator: Send + Sync {
fn plan(&self, world: &mut World, snap: &WorldSnapshot) -> Result<PlanIntent>;
}
// Implementations provided:
// - RuleOrchestrator (classical if-then rules)
// - GoapOrchestrator (goal-oriented planning)
// - LlmOrchestrator (LLM-based reasoning, feature-gated)
// - HybridOrchestrator (GOAP + LLM)
}
tool_sandbox
Secure action validation preventing impossible/cheating actions.
#![allow(unused)]
fn main() {
use astraweave_ai::tool_sandbox::{ToolSandbox, ToolResult};
let sandbox = ToolSandbox::new();
// Validate action before execution
match sandbox.validate(&action, &world_state) {
ToolResult::Valid => execute(action),
ToolResult::Invalid(reason) => {
log::warn!("AI attempted invalid action: {}", reason);
}
}
}
Validation Examples:
- Movement range limits
- Line-of-sight requirements
- Resource availability checks
- Cooldown enforcement
Feature-Gated Types
AIArbiter (requires llm_orchestrator)
Hybrid GOAP+LLM system with zero user-facing latency.
#![allow(unused)]
fn main() {
#[cfg(feature = "llm_orchestrator")]
use astraweave_ai::{AIArbiter, LlmExecutor, GoapOrchestrator, RuleOrchestrator};
let mut arbiter = AIArbiter::new(llm_executor, goap, bt);
// Game loop - returns instantly (101.7 ns)
let action = arbiter.update(&snapshot);
}
Performance: 101.7 ns GOAP control, 575 ns LLM polling
See Arbiter System for full documentation.
goap
Goal-Oriented Action Planning implementation.
#![allow(unused)]
fn main() {
use astraweave_ai::goap::{GoapPlanner, Goal, Action, WorldState};
let mut planner = GoapPlanner::new();
// Define goals
planner.add_goal(Goal::new("kill_enemy")
.with_precondition("enemy_visible", true)
.with_effect("enemy_dead", true));
// Define actions
planner.add_action(Action::new("attack")
.with_precondition("has_weapon", true)
.with_precondition("in_range", true)
.with_effect("enemy_dead", true)
.with_cost(1.0));
// Plan
let plan = planner.plan(¤t_state, &goal_state)?;
}
veilweaver
Integration for the Veilweaver game mechanics (fate-weaving).
#![allow(unused)]
fn main() {
use astraweave_ai::veilweaver::{FateThread, Prophecy, ThreadWeaver};
let mut weaver = ThreadWeaver::new();
let thread = weaver.create_thread(prophecy);
// Weave fate during gameplay
weaver.weave(&mut world, thread)?;
}
WorldSnapshot
The AI’s view of the world (filtered for perception).
#![allow(unused)]
fn main() {
pub struct WorldSnapshot {
pub t: f32, // Current game time
pub player: PlayerState, // Player information
pub me: CompanionState, // This AI's state
pub enemies: Vec<EnemyState>, // Detected enemies
pub pois: Vec<Poi>, // Points of interest
pub obstacles: Vec<IVec2>, // Obstacle positions
pub objective: Option<String>, // Current objective
}
pub struct CompanionState {
pub pos: IVec2, // Position
pub ammo: i32, // Ammunition
pub cooldowns: BTreeMap<String, f32>, // Ability cooldowns
pub morale: f32, // Morale level
}
}
PlanIntent
Validated action sequence from AI reasoning.
#![allow(unused)]
fn main() {
pub struct PlanIntent {
pub plan_id: String, // Unique plan identifier
pub steps: Vec<ActionStep>, // Ordered actions
}
pub enum ActionStep {
MoveTo { x: i32, y: i32 },
Attack { target: u32, stance: String },
TakeCover { position: Option<(i32, i32)> },
UseAbility { ability: String, target: Option<u32> },
Wait { duration: f32 },
Interact { object: u32 },
}
}
Orchestrator Implementations
| Orchestrator | Latency | Use Case |
|---|---|---|
RuleOrchestrator | ~100 ns | Simple if-then logic |
GoapOrchestrator | 3-50 µs | Goal-oriented planning |
BehaviorTreeOrchestrator | ~200 ns | Behavior trees |
UtilityOrchestrator | ~500 ns | Utility-based scoring |
LlmOrchestrator | 13-21s | Deep reasoning (async) |
HybridOrchestrator | ~100 ns | GOAP + async LLM |
Feature Flags
| Feature | Description | Default |
|---|---|---|
llm_orchestrator | LLM-based planning | ❌ |
goap | Goal-oriented planning | ✅ |
behavior_tree | BT integration | ✅ |
utility | Utility AI | ❌ |
[dependencies]
astraweave-ai = { version = "0.4", features = ["llm_orchestrator"] }
Performance
| Operation | Latency | Notes |
|---|---|---|
| WorldSnapshot build | ~500 ns | Perception gathering |
| GOAP planning | 3-50 µs | Depends on action space |
| Tool validation | ~100 ns | Per action |
| Full AI cycle | ~5 µs | Typical case |
See Also
Physics API Reference
Crate:
astraweave-physics
Coverage: ~82%
Tests: 657+
Physics simulation built on Rapier3D with character controllers, vehicles, destruction, and deterministic replay support.
Quick Links
- rustdoc (when published)
- Source
- Physics System Guide
Core Types
PhysicsWorld
Central physics simulation container.
#![allow(unused)]
fn main() {
use astraweave_physics::{PhysicsWorld, RigidBodyDesc, ColliderDesc};
let mut physics = PhysicsWorld::new();
// Create rigid body
let body = physics.create_rigid_body(RigidBodyDesc {
position: Vec3::new(0.0, 5.0, 0.0),
body_type: RigidBodyType::Dynamic,
mass: 1.0,
..Default::default()
});
// Add collider
physics.add_collider(body, ColliderDesc::sphere(0.5));
// Step simulation
physics.step(1.0 / 60.0);
}
CharacterController
Physics-based character movement with slopes, stairs, and ground detection.
#![allow(unused)]
fn main() {
use astraweave_physics::CharacterController;
let mut controller = CharacterController::new(CharacterConfig {
height: 1.8,
radius: 0.3,
step_height: 0.3,
max_slope: 45.0,
..Default::default()
});
// Move character
let movement = controller.move_character(
&physics,
desired_velocity,
delta_time,
);
// Check ground state
if controller.is_grounded() {
// Can jump
}
}
Vehicle
Vehicle physics with tire model and suspension.
#![allow(unused)]
fn main() {
use astraweave_physics::vehicle::{Vehicle, VehicleConfig, WheelConfig};
let vehicle = Vehicle::new(VehicleConfig {
chassis_mass: 1500.0,
wheels: vec![
WheelConfig { position: Vec3::new(-0.8, 0.0, 1.5), ..Default::default() },
WheelConfig { position: Vec3::new(0.8, 0.0, 1.5), ..Default::default() },
WheelConfig { position: Vec3::new(-0.8, 0.0, -1.5), ..Default::default() },
WheelConfig { position: Vec3::new(0.8, 0.0, -1.5), ..Default::default() },
],
..Default::default()
});
// Apply input
vehicle.set_steering(steering_angle);
vehicle.set_throttle(throttle);
vehicle.set_brake(brake);
}
SpatialHash
Efficient broad-phase collision detection.
#![allow(unused)]
fn main() {
use astraweave_physics::SpatialHash;
let mut spatial = SpatialHash::new(10.0); // Cell size
// Insert objects
spatial.insert(entity_a, aabb_a);
spatial.insert(entity_b, aabb_b);
// Query nearby objects
let nearby = spatial.query_sphere(position, radius);
// Query potential collisions
let pairs = spatial.get_collision_pairs();
}
Performance: 99.96% collision check reduction vs brute force
Ragdoll
Ragdoll physics with joint constraints.
#![allow(unused)]
fn main() {
use astraweave_physics::ragdoll::{Ragdoll, RagdollConfig, BoneConfig};
let ragdoll = Ragdoll::new(RagdollConfig {
bones: vec![
BoneConfig { name: "spine", parent: None, .. },
BoneConfig { name: "head", parent: Some(0), .. },
BoneConfig { name: "arm_l", parent: Some(0), .. },
// ...
],
..Default::default()
});
// Activate ragdoll on character death
ragdoll.activate(&mut physics, character_pose);
}
Cloth
Cloth simulation with wind and collision.
#![allow(unused)]
fn main() {
use astraweave_physics::cloth::{Cloth, ClothConfig};
let cloth = Cloth::new(ClothConfig {
width: 10,
height: 10,
particle_mass: 0.1,
stiffness: 1000.0,
damping: 0.1,
..Default::default()
});
// Pin corners
cloth.pin_particle(0, 0);
cloth.pin_particle(9, 0);
// Apply wind
cloth.apply_force(wind_force);
}
Destruction
Destructible objects with fracturing.
#![allow(unused)]
fn main() {
use astraweave_physics::destruction::{Destructible, FractureConfig};
let destructible = Destructible::new(FractureConfig {
health: 100.0,
fracture_threshold: 50.0,
piece_count: 8,
..Default::default()
});
// Apply damage
let fragments = destructible.apply_damage(damage, hit_point)?;
// Spawn fragment entities
for fragment in fragments {
commands.spawn(fragment);
}
}
Projectile
Projectile physics with penetration and ricochet.
#![allow(unused)]
fn main() {
use astraweave_physics::projectile::{Projectile, ProjectileConfig};
let projectile = Projectile::new(ProjectileConfig {
velocity: Vec3::new(0.0, 0.0, 500.0),
mass: 0.01,
drag: 0.1,
gravity_scale: 1.0,
penetration_power: 50.0,
..Default::default()
});
// Step projectile
let hit = projectile.step(&physics, delta_time);
if let Some(hit_info) = hit {
// Handle impact
}
}
Collision Detection
Raycasting
#![allow(unused)]
fn main() {
use astraweave_physics::RaycastResult;
// Single ray
if let Some(hit) = physics.raycast(origin, direction, max_distance) {
println!("Hit at {:?}, normal: {:?}", hit.point, hit.normal);
}
// Ray with filter
let hit = physics.raycast_filtered(origin, direction, max_distance, |entity| {
!entity.has::<Ghost>()
});
}
Shape Casts
#![allow(unused)]
fn main() {
// Sphere cast
let hit = physics.spherecast(origin, radius, direction, max_distance);
// Box cast
let hit = physics.boxcast(origin, half_extents, direction, max_distance);
}
Overlap Queries
#![allow(unused)]
fn main() {
// Get all entities in sphere
let entities = physics.overlap_sphere(center, radius);
// Get all entities in box
let entities = physics.overlap_box(center, half_extents, rotation);
}
Collision Shapes
| Shape | Constructor | Use Case |
|---|---|---|
| Sphere | ColliderDesc::sphere(radius) | Characters, projectiles |
| Box | ColliderDesc::cuboid(hx, hy, hz) | Crates, buildings |
| Capsule | ColliderDesc::capsule(half_height, radius) | Characters |
| Cylinder | ColliderDesc::cylinder(half_height, radius) | Pillars |
| Convex Hull | ColliderDesc::convex_hull(points) | Complex objects |
| Trimesh | ColliderDesc::trimesh(vertices, indices) | Static geometry |
| Heightfield | ColliderDesc::heightfield(heights, scale) | Terrain |
Collision Groups
#![allow(unused)]
fn main() {
use astraweave_physics::{CollisionGroups, Group};
// Define groups
const PLAYER: Group = Group::GROUP_1;
const ENEMY: Group = Group::GROUP_2;
const PROJECTILE: Group = Group::GROUP_3;
const ENVIRONMENT: Group = Group::GROUP_4;
// Player collides with enemies and environment
let player_groups = CollisionGroups::new(
PLAYER,
ENEMY | ENVIRONMENT,
);
// Projectile collides with everything
let projectile_groups = CollisionGroups::new(
PROJECTILE,
PLAYER | ENEMY | ENVIRONMENT,
);
}
Determinism
AstraWeave physics is fully deterministic for replay and networking:
#![allow(unused)]
fn main() {
use astraweave_physics::PhysicsWorld;
// Same seed = same results
let mut physics_a = PhysicsWorld::with_seed(12345);
let mut physics_b = PhysicsWorld::with_seed(12345);
// Run simulation
for _ in 0..1000 {
physics_a.step(1.0 / 60.0);
physics_b.step(1.0 / 60.0);
}
// Results are bit-identical
assert_eq!(physics_a.checksum(), physics_b.checksum());
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| Rigid body step | 2.97 µs | Single body |
| Full physics tick | 6.52 µs | 100 bodies |
| Character move | 114 ns | Controller step |
| Raycast | ~500 ns | Typical scene |
| Spatial hash query | ~50 ns | Per cell |
Feature Flags
| Feature | Description | Default |
|---|---|---|
debug-render | Physics visualization | ❌ |
simd | SIMD acceleration | ✅ |
parallel | Parallel simulation | ✅ |
serialize | State serialization | ❌ |
See Also
Fluids API Reference
Crate:
astraweave-fluids
Coverage: ~91%
Tests: 2,404
Production-ready SPH fluid simulation with real-time rendering, terrain integration, and editor support.
Quick Links
- rustdoc (when published)
- Source
- Fluids System Guide
Core Types
WaterEffectsManager
Central manager for all water visual effects.
#![allow(unused)]
fn main() {
use astraweave_fluids::{WaterEffectsManager, WaterQualityPreset};
let mut manager = WaterEffectsManager::new(WaterQualityPreset::High);
// Configure effects
manager.enable_caustics(true);
manager.enable_god_rays(true);
manager.enable_foam(true);
// Update each frame
manager.update(delta_time, &water_state);
// Render
manager.render(&mut render_context);
}
WaterQualityPreset
Quality presets for different hardware targets.
#![allow(unused)]
fn main() {
use astraweave_fluids::WaterQualityPreset;
// Available presets
let ultra = WaterQualityPreset::Ultra; // 64 rays, 1024 particles
let high = WaterQualityPreset::High; // 32 rays, 512 particles
let medium = WaterQualityPreset::Medium; // 16 rays, 256 particles
let low = WaterQualityPreset::Low; // 8 rays, 128 particles
// Apply preset
let manager = WaterEffectsManager::new(high);
}
PcisphSystem
Predictive-Corrective Incompressible SPH solver.
#![allow(unused)]
fn main() {
use astraweave_fluids::pcisph_system::{PcisphSystem, PcisphConfig};
let config = PcisphConfig {
particle_count: 10000,
particle_radius: 0.1,
rest_density: 1000.0,
viscosity: 0.01,
surface_tension: 0.0728,
..Default::default()
};
let mut solver = PcisphSystem::new(config);
// Add particles
solver.spawn_particles(position, count);
// Step simulation
solver.step(delta_time);
// Get particle data for rendering
let positions = solver.positions();
let velocities = solver.velocities();
}
UnifiedSolver
Unified particle solver handling fluids, foam, and spray.
#![allow(unused)]
fn main() {
use astraweave_fluids::unified_solver::{UnifiedSolver, ParticleType};
let mut solver = UnifiedSolver::new(config);
// Spawn different particle types
solver.spawn(ParticleType::Water, position, count);
solver.spawn(ParticleType::Foam, position, count);
solver.spawn(ParticleType::Spray, position, count);
// Step all particles together
solver.step(delta_time);
}
Visual Effects
Caustics
Underwater light caustic patterns.
#![allow(unused)]
fn main() {
use astraweave_fluids::caustics::{CausticRenderer, CausticConfig};
let config = CausticConfig {
resolution: 512,
intensity: 1.0,
speed: 0.5,
scale: 10.0,
..Default::default()
};
let mut caustics = CausticRenderer::new(config);
caustics.update(time, water_surface);
// Apply to underwater objects
caustics.apply_to_surface(&mut material);
}
God Rays
Volumetric underwater light shafts.
#![allow(unused)]
fn main() {
use astraweave_fluids::god_rays::{GodRayRenderer, GodRayConfig};
let config = GodRayConfig {
ray_count: 32,
ray_length: 50.0,
intensity: 0.8,
decay: 0.95,
..Default::default()
};
let mut god_rays = GodRayRenderer::new(config);
god_rays.render(sun_direction, water_surface);
}
Foam
Surface foam simulation.
#![allow(unused)]
fn main() {
use astraweave_fluids::foam::{FoamSystem, FoamConfig};
let config = FoamConfig {
spawn_threshold: 2.0, // Velocity threshold
lifetime: 3.0, // Seconds
size: 0.1,
..Default::default()
};
let mut foam = FoamSystem::new(config);
foam.update(delta_time, &water_particles);
foam.render(&mut render_context);
}
Terrain Integration
WaterTerrainIntegration
Seamless water-terrain interaction.
#![allow(unused)]
fn main() {
use astraweave_fluids::terrain_integration::{WaterTerrainIntegration, WaterBodyConfig};
let integration = WaterTerrainIntegration::new();
// Define water body
let lake = WaterBodyConfig {
center: Vec3::new(100.0, 50.0, 100.0),
radius: 50.0,
depth: 10.0,
flow_direction: Vec3::ZERO,
..Default::default()
};
// Query water at position
let water_info = integration.sample_water(position, &terrain);
if water_info.is_submerged {
apply_buoyancy(water_info.depth);
}
}
Building Integration
Water interaction with structures.
#![allow(unused)]
fn main() {
use astraweave_fluids::building::{WaterBuildingIntegration, FloodConfig};
let mut building_water = WaterBuildingIntegration::new();
// Configure flooding
building_water.set_flood_config(FloodConfig {
water_level: 45.0,
rise_rate: 0.1,
..Default::default()
});
// Update with building geometry
building_water.update(&buildings, delta_time);
// Query flood state
let flood = building_water.get_flood_level(building_id);
}
Volume Grid
GpuVolumeGrid
GPU-accelerated 3D volume grid for water simulation.
#![allow(unused)]
fn main() {
use astraweave_fluids::gpu_volume::{GpuVolumeGrid, VolumeConfig};
let config = VolumeConfig {
resolution: [128, 64, 128],
cell_size: 0.5,
..Default::default()
};
let mut volume = GpuVolumeGrid::new(&device, config);
// Update from particles
volume.splat_particles(&particles);
// Read density
let density = volume.sample(position);
}
Editor Support
Undo/Redo
Full undo/redo support for editor integration.
#![allow(unused)]
fn main() {
use astraweave_fluids::editor::{WaterEditor, EditorCommand};
let mut editor = WaterEditor::new();
// Make changes
editor.execute(EditorCommand::SetWaterLevel(50.0));
editor.execute(EditorCommand::AddEmitter(emitter));
// Undo
editor.undo();
// Redo
editor.redo();
}
Validation
Real-time validation of water configurations.
#![allow(unused)]
fn main() {
use astraweave_fluids::editor::validation::{validate_water_body, ValidationResult};
let result = validate_water_body(&config);
match result {
ValidationResult::Valid => { /* OK */ }
ValidationResult::Warning(msg) => {
ui.show_warning(&msg);
}
ValidationResult::Error(msg) => {
ui.show_error(&msg);
return;
}
}
}
Serialization
Save/Load
Full water state serialization.
#![allow(unused)]
fn main() {
use astraweave_fluids::serialization::{save_water_state, load_water_state};
// Save
let data = save_water_state(&manager)?;
std::fs::write("water.bin", &data)?;
// Load
let data = std::fs::read("water.bin")?;
let manager = load_water_state(&data)?;
}
LOD System
Adaptive Quality
Distance-based quality adjustment.
#![allow(unused)]
fn main() {
use astraweave_fluids::lod::{WaterLod, LodConfig};
let lod = WaterLod::new(LodConfig {
lod0_distance: 50.0, // Full quality
lod1_distance: 100.0, // Medium
lod2_distance: 200.0, // Low
..Default::default()
});
// Update based on camera
lod.update(camera_position);
// Get quality for water body
let quality = lod.get_quality(water_position);
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| PCISPH step (10K particles) | ~2 ms | GPU accelerated |
| Caustic update | ~0.5 ms | 512×512 texture |
| God ray render | ~0.8 ms | 32 rays |
| Foam update | ~0.3 ms | 1K particles |
| Volume splat | ~0.2 ms | 128³ grid |
Feature Flags
| Feature | Description | Default |
|---|---|---|
gpu | GPU acceleration | ✅ |
editor | Editor integration | ❌ |
serialize | State serialization | ✅ |
simd | SIMD optimization | ✅ |
See Also
Render API Reference
Crate:
astraweave-render
Coverage: ~68%
Tests: 300+
Modern wgpu-based rendering with PBR materials, shadows, post-processing, GPU skinning, and mesh optimization.
Quick Links
- rustdoc (when published)
- Source
- Rendering Guide
Core Types
Renderer
Central rendering coordinator.
#![allow(unused)]
fn main() {
use astraweave_render::{Renderer, RendererConfig};
let config = RendererConfig {
width: 1920,
height: 1080,
vsync: true,
msaa_samples: 4,
..Default::default()
};
let mut renderer = Renderer::new(&window, config).await?;
// Render frame
renderer.begin_frame();
renderer.draw_scene(&scene);
renderer.end_frame();
}
MeshRegistry
GPU mesh resource management.
#![allow(unused)]
fn main() {
use astraweave_render::MeshRegistry;
let mut registry = MeshRegistry::new(&device);
// Register mesh
let mesh_id = registry.register(MeshData {
vertices: vertex_data,
indices: index_data,
bounds: aabb,
});
// Draw mesh
renderer.draw_mesh(mesh_id, &transform, &material);
}
MaterialManager
PBR material system with hot reloading.
#![allow(unused)]
fn main() {
use astraweave_render::{MaterialManager, PbrMaterial};
let mut materials = MaterialManager::new(&device);
// Create material
let material = PbrMaterial {
albedo: Color::rgb(0.8, 0.2, 0.2),
metallic: 0.0,
roughness: 0.5,
normal_map: Some(normal_texture),
..Default::default()
};
let material_id = materials.register(material);
}
IblManager
Image-Based Lighting for environment reflections.
#![allow(unused)]
fn main() {
use astraweave_render::IblManager;
let mut ibl = IblManager::new(&device);
// Load environment map
ibl.load_hdr("environment.hdr").await?;
// Bind to shader
ibl.bind(&mut render_pass);
}
GPU Skinning
SkinnedVertex
Vertex format with bone weights.
#![allow(unused)]
fn main() {
use astraweave_render::skinning_gpu::{SkinnedVertex, SkinnedMesh};
let vertex = SkinnedVertex {
position: [0.0, 1.0, 0.0],
normal: [0.0, 1.0, 0.0],
uv: [0.5, 0.5],
bone_indices: [0, 1, 0, 0], // Up to 4 bones
bone_weights: [0.7, 0.3, 0.0, 0.0],
};
let skinned_mesh = SkinnedMesh::new(&device, vertices, indices);
}
BoneMatrixBuffer
GPU bone matrix upload.
#![allow(unused)]
fn main() {
use astraweave_render::skinning_gpu::BoneMatrixBuffer;
let mut bone_buffer = BoneMatrixBuffer::new(&device, max_bones);
// Update bone matrices each frame
bone_buffer.update(&queue, &bone_matrices);
// Bind for skinning
bone_buffer.bind(&mut render_pass);
}
Mesh Optimization
Vertex Compression
37% memory reduction via octahedral normals and half-float UVs.
#![allow(unused)]
fn main() {
use astraweave_render::vertex_compression::{compress_vertices, CompressedVertex};
let compressed = compress_vertices(&vertices);
// 32 bytes → 20 bytes per vertex
assert!(std::mem::size_of::<CompressedVertex>() < std::mem::size_of::<Vertex>());
}
LOD Generation
Automatic level-of-detail mesh generation.
#![allow(unused)]
fn main() {
use astraweave_render::lod_generator::{generate_lods, LodConfig};
let config = LodConfig {
target_ratios: vec![1.0, 0.5, 0.25, 0.1],
min_triangles: 100,
..Default::default()
};
let lods = generate_lods(&mesh, config);
// lods[0] = original
// lods[1] = 50% triangles
// lods[2] = 25% triangles
// lods[3] = 10% triangles
}
Instancing
GPU instanced rendering for repeated objects.
#![allow(unused)]
fn main() {
use astraweave_render::instancing::{InstanceBuffer, InstanceData};
let mut instances = InstanceBuffer::new(&device, max_instances);
// Update instance data
let instance_data: Vec<InstanceData> = trees.iter()
.map(|t| InstanceData {
transform: t.transform.to_cols_array_2d(),
color: t.color.into(),
})
.collect();
instances.update(&queue, &instance_data);
// Draw all instances in one call
renderer.draw_instanced(mesh_id, &instances);
}
Shadows
CascadedShadowMaps
Multi-cascade shadow mapping for large scenes.
#![allow(unused)]
fn main() {
use astraweave_render::shadows::{CascadedShadowMaps, CsmConfig};
let config = CsmConfig {
cascade_count: 4,
resolution: 2048,
split_lambda: 0.5,
..Default::default()
};
let mut csm = CascadedShadowMaps::new(&device, config);
// Update cascades
csm.update(&camera, &sun_direction);
// Render shadow maps
for cascade in csm.cascades() {
renderer.render_shadow_pass(cascade, &shadow_casters);
}
}
Post-Processing
PostFxPipeline
Post-processing effect chain.
#![allow(unused)]
fn main() {
use astraweave_render::post_fx::{PostFxPipeline, PostFxConfig};
let config = PostFxConfig {
bloom_enabled: true,
bloom_intensity: 0.5,
tonemapping: Tonemapping::Aces,
exposure: 1.0,
..Default::default()
};
let mut post_fx = PostFxPipeline::new(&device, config);
// Apply post-processing
post_fx.process(&mut render_context, &scene_texture);
}
Available Effects:
- Bloom
- Tonemapping (ACES, Reinhard, Filmic)
- Color grading
- Vignette
- Film grain
- SSAO (optional)
Camera
Camera3D
3D camera with multiple projection modes.
#![allow(unused)]
fn main() {
use astraweave_render::camera::{Camera3D, Projection};
let camera = Camera3D {
position: Vec3::new(0.0, 5.0, -10.0),
target: Vec3::ZERO,
up: Vec3::Y,
projection: Projection::Perspective {
fov: 60.0_f32.to_radians(),
aspect: 16.0 / 9.0,
near: 0.1,
far: 1000.0,
},
};
let view_proj = camera.view_projection_matrix();
}
Debug Rendering
DebugDraw
Immediate-mode debug visualization.
#![allow(unused)]
fn main() {
use astraweave_render::debug_draw::DebugDraw;
let mut debug = DebugDraw::new(&device);
// Draw primitives
debug.line(start, end, Color::RED);
debug.sphere(center, radius, Color::GREEN);
debug.box3d(&aabb, Color::BLUE);
debug.arrow(origin, direction, Color::YELLOW);
// Submit
debug.flush(&mut render_pass);
}
Render Graph
RenderGraph
Declarative render pipeline.
#![allow(unused)]
fn main() {
use astraweave_render::graph::{RenderGraph, RenderPass};
let mut graph = RenderGraph::new();
// Define passes
graph.add_pass(RenderPass::new("shadow")
.with_output("shadow_map", Format::Depth32Float));
graph.add_pass(RenderPass::new("gbuffer")
.with_output("albedo", Format::Rgba8Unorm)
.with_output("normal", Format::Rgba16Float)
.with_output("depth", Format::Depth32Float));
graph.add_pass(RenderPass::new("lighting")
.with_input("albedo")
.with_input("normal")
.with_input("shadow_map")
.with_output("hdr_color", Format::Rgba16Float));
// Execute
graph.execute(&mut renderer);
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| Mesh draw | 21 ns | Per instance |
| Vertex compression | 21 ns | Per vertex |
| Instancing overhead | 2 ns | Per instance |
| Shadow pass (2K) | ~1 ms | Per cascade |
| Post-FX (full) | ~2 ms | All effects |
Feature Flags
| Feature | Description | Default |
|---|---|---|
pbr | PBR materials | ✅ |
shadows | Shadow mapping | ✅ |
post-fx | Post-processing | ✅ |
debug-render | Debug visualization | ❌ |
hot-reload | Shader hot reload | ❌ |
See Also
Navigation API Reference
Crate:
astraweave-nav
Coverage: ~78%
Tests: 200+
Navmesh-based pathfinding with A*, portal graphs, and dynamic obstacle avoidance.
Quick Links
- rustdoc (when published)
- Source
- Navigation Guide
Core Types
NavMesh
Navigation mesh for pathfinding.
#![allow(unused)]
fn main() {
use astraweave_nav::{NavMesh, NavMeshConfig};
let config = NavMeshConfig {
cell_size: 0.3,
cell_height: 0.2,
agent_height: 2.0,
agent_radius: 0.6,
max_slope: 45.0,
..Default::default()
};
// Build from geometry
let navmesh = NavMesh::build(&vertices, &indices, config)?;
// Query path
let path = navmesh.find_path(start, goal)?;
}
NavAgent
Agent with steering behaviors.
#![allow(unused)]
fn main() {
use astraweave_nav::{NavAgent, AgentConfig};
let mut agent = NavAgent::new(AgentConfig {
speed: 5.0,
acceleration: 10.0,
radius: 0.5,
..Default::default()
});
// Set destination
agent.set_destination(target_position);
// Update each frame
agent.update(delta_time, &navmesh);
// Get movement vector
let velocity = agent.velocity();
}
PortalGraph
Hierarchical pathfinding for large worlds.
#![allow(unused)]
fn main() {
use astraweave_nav::PortalGraph;
let mut graph = PortalGraph::new();
// Add regions
graph.add_region("forest", forest_navmesh);
graph.add_region("castle", castle_navmesh);
// Connect regions via portals
graph.add_portal("forest", "castle", portal_polygon);
// Find cross-region path
let path = graph.find_path(start, goal)?;
}
ObstacleAvoidance
Dynamic obstacle avoidance using velocity obstacles.
#![allow(unused)]
fn main() {
use astraweave_nav::ObstacleAvoidance;
let mut avoidance = ObstacleAvoidance::new();
// Add moving obstacles
for other in nearby_agents {
avoidance.add_obstacle(other.position, other.velocity, other.radius);
}
// Compute safe velocity
let safe_velocity = avoidance.compute(
agent.position,
agent.desired_velocity,
agent.radius,
delta_time,
);
}
Pathfinding
A* Search
#![allow(unused)]
fn main() {
use astraweave_nav::astar::{astar_search, PathResult};
let result = astar_search(&navmesh, start, goal);
match result {
PathResult::Found(path) => {
for waypoint in path {
// Follow path
}
}
PathResult::Partial(path) => {
// Closest reachable point
}
PathResult::NotFound => {
// No path exists
}
}
}
Path Smoothing
#![allow(unused)]
fn main() {
use astraweave_nav::path_smoothing::smooth_path;
let raw_path = navmesh.find_path(start, goal)?;
let smooth = smooth_path(&raw_path, &navmesh);
// Smooth path has fewer waypoints
assert!(smooth.len() <= raw_path.len());
}
Crowd Simulation
CrowdManager
Large-scale agent coordination.
#![allow(unused)]
fn main() {
use astraweave_nav::crowd::{CrowdManager, CrowdAgent};
let mut crowd = CrowdManager::new(&navmesh, max_agents);
// Add agents
let agent_id = crowd.add_agent(CrowdAgent {
position: spawn_point,
target: destination,
speed: 5.0,
..Default::default()
});
// Update all agents
crowd.update(delta_time);
// Get agent state
let agent = crowd.get_agent(agent_id);
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| A* pathfind | 50-500 µs | Depends on distance |
| NavMesh query | ~1 µs | Point location |
| Agent update | ~100 ns | Per agent |
| Crowd update (100) | ~50 µs | All agents |
See Also
Audio API Reference
Crate:
astraweave-audio
Coverage: ~72%
Tests: 150+
Spatial audio with mixer buses, crossfading, reverb zones, and audio occlusion.
Quick Links
- rustdoc (when published)
- Source
- Audio Guide
Core Types
AudioEngine
Central audio system with mixer.
#![allow(unused)]
fn main() {
use astraweave_audio::{AudioEngine, AudioConfig};
let config = AudioConfig {
sample_rate: 48000,
buffer_size: 512,
..Default::default()
};
let mut audio = AudioEngine::new(config)?;
// Play sound
audio.play("sfx/explosion.ogg", PlayConfig::default());
// Update each frame
audio.update(listener_position, listener_orientation);
}
AudioMixer
4-bus mixing system.
#![allow(unused)]
fn main() {
use astraweave_audio::mixer::{AudioMixer, Bus};
let mut mixer = AudioMixer::new();
// Configure bus volumes
mixer.set_volume(Bus::Master, 1.0);
mixer.set_volume(Bus::Music, 0.8);
mixer.set_volume(Bus::Sfx, 1.0);
mixer.set_volume(Bus::Voice, 1.0);
// Mute bus
mixer.set_muted(Bus::Music, true);
}
SpatialSource
3D positioned audio source.
#![allow(unused)]
fn main() {
use astraweave_audio::SpatialSource;
let mut source = SpatialSource::new("ambient/wind.ogg");
source.set_position(Vec3::new(10.0, 0.0, 5.0));
source.set_looping(true);
source.set_min_distance(1.0);
source.set_max_distance(50.0);
source.play();
}
MusicLayer
Dynamic music with layers and crossfading.
#![allow(unused)]
fn main() {
use astraweave_audio::music::{MusicPlayer, MusicLayer};
let mut music = MusicPlayer::new();
// Add layers
music.add_layer(MusicLayer::new("music/ambient.ogg"));
music.add_layer(MusicLayer::new("music/combat.ogg"));
music.add_layer(MusicLayer::new("music/boss.ogg"));
// Crossfade between layers
music.crossfade_to(1, Duration::from_secs(2)); // Combat music
}
ReverbZone
Environment-based reverb.
#![allow(unused)]
fn main() {
use astraweave_audio::reverb::{ReverbZone, ReverbPreset};
let zone = ReverbZone::new(ReverbPreset::Cave)
.with_bounds(aabb)
.with_blend_distance(5.0);
// Add to audio system
audio.add_reverb_zone(zone);
}
Presets: Small, Medium, Large, Hall, Cave, Outdoor, Underwater
AudioOcclusion
Raycast-based sound occlusion.
#![allow(unused)]
fn main() {
use astraweave_audio::occlusion::AudioOcclusion;
let mut occlusion = AudioOcclusion::new(&physics);
// Update occlusion for all sources
occlusion.update(listener_position, &sources);
// Apply to source
let factor = occlusion.get_factor(source_id);
source.set_volume(base_volume * factor);
}
Performance
| Operation | Latency | Notes |
|---|---|---|
| Source play | ~100 µs | Initial setup |
| Mixer update | ~50 µs | Per frame |
| Spatial update | ~1 µs | Per source |
| Occlusion raycast | ~5 µs | Per source |
See Also
Crate Reference
AstraWeave is organized as a workspace of interconnected crates, each providing focused functionality. This reference documents the crate hierarchy, dependencies, and public APIs.
Workspace Overview
graph TB
subgraph Core["Core Layer"]
ECS[astraweave-ecs]
MATH[astraweave-math]
SDK[astraweave-sdk]
end
subgraph AI["AI Layer"]
AI_CORE[astraweave-ai]
LLM[astraweave-llm]
MEMORY[astraweave-memory]
PERSONA[astraweave-persona]
BEHAVIOR[astraweave-behavior]
end
subgraph Rendering["Rendering Layer"]
RENDER[astraweave-render]
MATERIALS[astraweave-materials]
ASSET[astraweave-asset]
UI[astraweave-ui]
end
subgraph Simulation["Simulation Layer"]
PHYSICS[astraweave-physics]
NAV[astraweave-nav]
AUDIO[astraweave-audio]
end
subgraph Gameplay["Gameplay Layer"]
GAMEPLAY[astraweave-gameplay]
DIALOGUE[astraweave-dialogue]
QUESTS[astraweave-quests]
PCG[astraweave-pcg]
end
SDK --> ECS
SDK --> AI_CORE
SDK --> RENDER
SDK --> PHYSICS
AI_CORE --> ECS
AI_CORE --> LLM
AI_CORE --> MEMORY
RENDER --> ECS
RENDER --> ASSET
RENDER --> MATERIALS
GAMEPLAY --> ECS
GAMEPLAY --> AI_CORE
GAMEPLAY --> PHYSICS
Core Crates
astraweave-ecs
The foundation Entity Component System providing deterministic, high-performance entity management.
[dependencies]
astraweave-ecs = "0.1"
Key Types:
| Type | Description |
|---|---|
World | Container for all entities, components, and resources |
Entity | Lightweight identifier for game objects |
Component | Data attached to entities (derive macro available) |
Resource | Singleton data shared across systems |
Query | Efficient iteration over component combinations |
Commands | Deferred entity/component modifications |
Example:
#![allow(unused)]
fn main() {
use astraweave_ecs::prelude::*;
#[derive(Component)]
struct Position(Vec3);
#[derive(Component)]
struct Velocity(Vec3);
fn movement_system(mut query: Query<(&mut Position, &Velocity)>) {
for (mut pos, vel) in query.iter_mut() {
pos.0 += vel.0;
}
}
}
Features:
parallel- Enable parallel system execution (default)tracing- Add performance tracing instrumentationserde- Serialization support for components
astraweave-math
Mathematics library optimized for game development with SIMD acceleration.
[dependencies]
astraweave-math = "0.1"
Key Types:
| Type | Description |
|---|---|
Vec2, Vec3, Vec4 | Vector types with operator overloading |
Mat3, Mat4 | Matrix types for transforms |
Quat | Quaternion for rotations |
Transform | Position + rotation + scale |
Aabb | Axis-aligned bounding box |
Ray | Ray for intersection tests |
Example:
#![allow(unused)]
fn main() {
use astraweave_math::prelude::*;
let pos = Vec3::new(1.0, 2.0, 3.0);
let rotation = Quat::from_axis_angle(Vec3::Y, 45.0_f32.to_radians());
let transform = Transform::from_translation(pos).with_rotation(rotation);
let world_pos = transform.transform_point(Vec3::ZERO);
}
astraweave-sdk
High-level SDK that re-exports commonly used types and provides convenience APIs.
[dependencies]
astraweave-sdk = "0.1"
Re-exports:
#![allow(unused)]
fn main() {
pub use astraweave_ecs::prelude::*;
pub use astraweave_ai::prelude::*;
pub use astraweave_render::prelude::*;
pub use astraweave_physics::prelude::*;
pub use astraweave_audio::prelude::*;
pub use astraweave_input::prelude::*;
}
App Builder:
use astraweave_sdk::prelude::*;
fn main() {
App::new()
.add_plugin(DefaultPlugins)
.add_startup_system(setup)
.add_system(game_logic)
.run();
}
AI Crates
astraweave-ai
Core AI framework with perception, planning, and behavior systems.
[dependencies]
astraweave-ai = "0.1"
Key Types:
| Type | Description |
|---|---|
AiAgent | AI-controlled entity component |
PerceptionBus | Sensory input aggregation |
Planner | Goal-oriented action planning |
BehaviorTree | Behavior tree execution |
Blackboard | Shared AI state storage |
AiTool | Tool definition for LLM agents |
Example:
#![allow(unused)]
fn main() {
use astraweave_ai::prelude::*;
let mut agent = AiAgent::new()
.with_perception_radius(50.0)
.with_tick_budget_ms(8);
agent.add_goal(AiGoal::Patrol {
waypoints: vec![point_a, point_b, point_c]
});
}
Features:
llm- Enable LLM integration (requiresastraweave-llm)goap- Goal-Oriented Action Planningutility- Utility AI scoring system
astraweave-llm
LLM integration for AI agents with tool calling and validation.
[dependencies]
astraweave-llm = "0.1"
Key Types:
| Type | Description |
|---|---|
LlmClient | HTTP client for LLM endpoints |
LlmConfig | Configuration for model and endpoint |
ToolCall | Structured tool invocation from LLM |
ToolResult | Validated tool execution result |
PromptBuilder | Fluent prompt construction |
Example:
#![allow(unused)]
fn main() {
use astraweave_llm::prelude::*;
let config = LlmConfig {
endpoint: "http://localhost:11434".into(),
model: "hermes2-pro-mistral".into(),
temperature: 0.7,
max_tokens: 256,
};
let client = LlmClient::new(config);
let response = client.complete("What should I do next?").await?;
}
Supported Backends:
- Ollama (local)
- OpenAI-compatible APIs
- Custom endpoints
astraweave-memory
Memory systems for AI agents including short-term, long-term, and episodic memory.
[dependencies]
astraweave-memory = "0.1"
Key Types:
| Type | Description |
|---|---|
MemoryStore | Central memory management |
ShortTermMemory | Recent observations with decay |
LongTermMemory | Persistent important memories |
EpisodicMemory | Event sequences and narratives |
MemoryQuery | Semantic memory retrieval |
astraweave-behavior
Behavior tree implementation with visual editor support.
[dependencies]
astraweave-behavior = "0.1"
Node Types:
| Category | Nodes |
|---|---|
| Composite | Sequence, Selector, Parallel, RandomSelector |
| Decorator | Inverter, Repeater, Succeeder, UntilFail |
| Leaf | Action, Condition, Wait, SubTree |
Example:
#![allow(unused)]
fn main() {
use astraweave_behavior::prelude::*;
let tree = BehaviorTree::new(
Selector::new(vec![
Sequence::new(vec![
Condition::new("has_target"),
Action::new("attack_target"),
]).into(),
Action::new("patrol").into(),
])
);
}
Rendering Crates
astraweave-render
GPU rendering with Vulkan/DX12/Metal backends via wgpu.
[dependencies]
astraweave-render = "0.1"
Key Types:
| Type | Description |
|---|---|
Renderer | Main rendering context |
RenderPass | Configurable render pass |
Mesh | Vertex/index buffer pair |
Material | Surface properties and shaders |
Camera | View and projection configuration |
Light | Point, directional, spot lights |
Features:
pbr- Physically-based rendering (default)shadows- Shadow mapping with CSMpost-process- Bloom, SSAO, tone mappingskeletal- Skeletal animation
astraweave-materials
PBR material system with shader graph support.
[dependencies]
astraweave-materials = "0.1"
Material Properties:
| Property | Type | Description |
|---|---|---|
albedo | Color or Texture | Base color |
metallic | f32 or Texture | Metallic factor (0-1) |
roughness | f32 or Texture | Surface roughness (0-1) |
normal | Texture | Normal map |
emission | Color | Emissive color |
ao | Texture | Ambient occlusion |
astraweave-asset
Asset loading, caching, and hot-reloading.
[dependencies]
astraweave-asset = "0.1"
Key Types:
| Type | Description |
|---|---|
AssetServer | Async asset loading |
Handle<T> | Reference-counted asset handle |
AssetLoader | Custom loader trait |
AssetEvent | Load/unload notifications |
Supported Formats:
- Meshes: glTF 2.0, OBJ, FBX
- Textures: PNG, JPEG, KTX2, DDS
- Audio: WAV, OGG, MP3
- Fonts: TTF, OTF
astraweave-ui
Immediate-mode UI with retained state for game interfaces.
[dependencies]
astraweave-ui = "0.1"
Key Types:
| Type | Description |
|---|---|
UiContext | UI state and input handling |
Widget | Base widget trait |
Layout | Flexbox-style layout |
Style | Visual styling properties |
Built-in Widgets:
Button,Label,TextInputSlider,Checkbox,RadioGroupPanel,ScrollView,ModalProgressBar,Tooltip
Simulation Crates
astraweave-physics
3D physics with Rapier backend.
[dependencies]
astraweave-physics = "0.1"
Key Types:
| Type | Description |
|---|---|
RigidBody | Dynamic, kinematic, or static body |
Collider | Collision shape |
PhysicsWorld | Physics simulation context |
RayCast | Ray intersection queries |
Joint | Constraints between bodies |
Collider Shapes:
Ball,Cuboid,Capsule,CylinderConvexHull,TriMesh,HeightFieldCompound(multiple shapes)
astraweave-nav
Navigation mesh and pathfinding.
[dependencies]
astraweave-nav = "0.1"
Key Types:
| Type | Description |
|---|---|
NavMesh | Navigation mesh geometry |
NavAgent | Pathfinding agent component |
PathQuery | Path computation request |
NavObstacle | Dynamic obstacle |
Features:
- A* pathfinding with string pulling
- Dynamic obstacle avoidance
- Off-mesh links for jumps/ladders
- Hierarchical pathfinding for large worlds
astraweave-audio
Spatial audio with multiple backends.
[dependencies]
astraweave-audio = "0.1"
Key Types:
| Type | Description |
|---|---|
AudioSource | Positional audio emitter |
AudioListener | Spatial audio receiver |
AudioClip | Loaded audio data |
Mixer | Audio mixing and effects |
Features:
- 3D spatial audio with HRTF
- Reverb zones
- Audio occlusion
- Streaming for music
Gameplay Crates
astraweave-gameplay
High-level gameplay systems and components.
[dependencies]
astraweave-gameplay = "0.1"
Systems:
- Combat and damage
- Inventory management
- Status effects
- Interactable objects
- Save/load integration
astraweave-dialogue
Dialogue tree and conversation systems.
[dependencies]
astraweave-dialogue = "0.1"
Key Types:
| Type | Description |
|---|---|
DialogueTree | Branching conversation graph |
DialogueNode | Single dialogue entry |
DialogueController | Runtime dialogue state |
DynamicDialogue | LLM-powered conversations |
astraweave-quests
Quest tracking and objective systems.
[dependencies]
astraweave-quests = "0.1"
Key Types:
| Type | Description |
|---|---|
Quest | Quest definition |
QuestLog | Player’s active quests |
Objective | Quest goal/task |
QuestEvent | Quest state changes |
astraweave-pcg
Procedural content generation framework.
[dependencies]
astraweave-pcg = "0.1"
Generators:
- Terrain heightmaps with erosion
- Dungeon layouts
- Item properties
- NPC backstories (AI-enhanced)
- Quest generation (AI-enhanced)
Tool Crates
aw_editor
Visual editor for scenes, behavior trees, and materials.
cargo run -p aw_editor
Features:
- Scene hierarchy view
- Component inspector
- Behavior tree editor
- Material graph editor
- Asset browser
aw_asset_cli
Command-line asset processing.
cargo run -p aw_asset_cli -- --help
Commands:
import- Convert assets to engine formatpack- Create asset bundlesvalidate- Check asset integrityoptimize- Compress and optimize assets
aw_debug
Runtime debugging tools.
[dependencies]
aw_debug = "0.1"
Features:
- Entity inspector overlay
- Performance graphs
- Physics debug visualization
- AI state visualization
- Console commands
Feature Flags Summary
| Crate | Feature | Description |
|---|---|---|
astraweave-ecs | parallel | Parallel system execution |
astraweave-ecs | tracing | Performance instrumentation |
astraweave-ai | llm | LLM integration |
astraweave-ai | goap | Goal-oriented planning |
astraweave-render | pbr | PBR materials |
astraweave-render | shadows | Shadow mapping |
astraweave-physics | debug-render | Physics visualization |
astraweave-audio | spatial | 3D audio |
Dependency Graph
graph LR
subgraph External["External Dependencies"]
wgpu[wgpu]
rapier[rapier3d]
tokio[tokio]
serde[serde]
end
ECS[astraweave-ecs] --> serde
RENDER[astraweave-render] --> wgpu
RENDER --> ECS
PHYSICS[astraweave-physics] --> rapier
PHYSICS --> ECS
LLM[astraweave-llm] --> tokio
AI[astraweave-ai] --> ECS
AI --> LLM
Related Documentation
- Getting Started - Initial setup
- Configuration Reference - Runtime configuration
- Building from Source - Build instructions
- Contributing - Development guidelines
Configuration
This reference documents all configuration options for AstraWeave projects.
Project Configuration
Cargo.toml
Standard Rust project configuration with AstraWeave dependencies:
[package]
name = "my_game"
version = "0.1.0"
edition = "2021"
[dependencies]
astraweave-core = "0.1"
astraweave-ecs = "0.1"
astraweave-ai = "0.1"
astraweave-physics = "0.1"
astraweave-render = "0.1"
astraweave-audio = "0.1"
astraweave-input = "0.1"
[features]
default = ["graphics"]
graphics = ["astraweave-render/wgpu"]
headless = []
profiling = ["astraweave-core/tracy"]
[profile.release]
lto = true
codegen-units = 1
opt-level = 3
Runtime Configuration
Engine Configuration
Configure the engine at startup:
#![allow(unused)]
fn main() {
use astraweave_core::config::*;
let config = EngineConfig {
tick_rate: TickRate::Fixed(60),
window: WindowConfig {
title: "My Game".into(),
width: 1920,
height: 1080,
vsync: true,
fullscreen: false,
},
graphics: GraphicsConfig {
backend: GraphicsBackend::Auto,
msaa_samples: 4,
shadow_quality: ShadowQuality::High,
max_lights: 128,
},
physics: PhysicsConfig {
gravity: Vec3::new(0.0, -9.81, 0.0),
substeps: 4,
solver_iterations: 8,
},
audio: AudioConfig {
sample_rate: 48000,
channels: 2,
buffer_size: 1024,
},
ai: AiConfig {
tick_budget_ms: 8,
max_concurrent_plans: 4,
ollama_endpoint: "http://localhost:11434".into(),
default_model: "hermes2-pro-mistral".into(),
},
};
Engine::run(config, |world| {
// Game setup
});
}
Configuration File (TOML)
Load configuration from file:
# config.toml
[engine]
tick_rate = 60
[window]
title = "My Game"
width = 1920
height = 1080
vsync = true
fullscreen = false
[graphics]
backend = "vulkan"
msaa_samples = 4
shadow_quality = "high"
max_lights = 128
hdr = true
bloom = true
[physics]
gravity = [0.0, -9.81, 0.0]
substeps = 4
solver_iterations = 8
ccd_enabled = true
[audio]
sample_rate = 48000
channels = 2
buffer_size = 1024
master_volume = 1.0
[ai]
tick_budget_ms = 8
max_concurrent_plans = 4
ollama_endpoint = "http://localhost:11434"
default_model = "hermes2-pro-mistral"
fallback_enabled = true
[navigation]
cell_size = 0.3
cell_height = 0.2
agent_radius = 0.5
agent_height = 1.8
max_slope = 45.0
[networking]
tick_rate = 30
interpolation_delay_ms = 100
max_clients = 32
Load in code:
#![allow(unused)]
fn main() {
use astraweave_core::config::EngineConfig;
let config = EngineConfig::from_file("config.toml")?;
}
System-Specific Configuration
Graphics Configuration
#![allow(unused)]
fn main() {
let graphics = GraphicsConfig {
backend: GraphicsBackend::Vulkan,
msaa_samples: 4,
anisotropic_filtering: 16,
shadow_quality: ShadowQuality::High,
shadow_cascade_count: 4,
shadow_map_size: 2048,
max_lights: 128,
clustered_lighting: true,
cluster_dimensions: [16, 9, 24],
hdr: true,
bloom: true,
bloom_intensity: 0.5,
ambient_occlusion: AmbientOcclusion::Ssao,
vsync: VsyncMode::Enabled,
frame_limit: None,
};
}
Physics Configuration
#![allow(unused)]
fn main() {
let physics = PhysicsConfig {
gravity: Vec3::new(0.0, -9.81, 0.0),
substeps: 4,
solver_iterations: 8,
ccd_enabled: true,
ccd_max_substeps: 4,
broad_phase: BroadPhase::SweepAndPrune,
contact_skin: 0.01,
sleep_threshold_linear: 0.1,
sleep_threshold_angular: 0.05,
debug_render: false,
};
}
AI Configuration
#![allow(unused)]
fn main() {
let ai = AiConfig {
tick_budget_ms: 8,
max_concurrent_plans: 4,
plan_queue_size: 16,
ollama_endpoint: "http://localhost:11434".into(),
default_model: "hermes2-pro-mistral".into(),
temperature: 0.7,
max_tokens: 256,
request_timeout_ms: 100,
plan_cache_enabled: true,
plan_cache_duration_ms: 2000,
fallback_enabled: true,
context_window_size: 4096,
tool_validation_strict: true,
};
}
Audio Configuration
#![allow(unused)]
fn main() {
let audio = AudioConfig {
sample_rate: 48000,
channels: 2,
buffer_size: 1024,
master_volume: 1.0,
music_volume: 0.8,
sfx_volume: 1.0,
voice_volume: 1.0,
max_simultaneous_sounds: 32,
spatial_audio: true,
hrtf_enabled: true,
reverb_enabled: true,
distance_model: DistanceModel::InverseSquare,
rolloff_factor: 1.0,
reference_distance: 1.0,
max_distance: 100.0,
};
}
Navigation Configuration
#![allow(unused)]
fn main() {
let navigation = NavigationConfig {
cell_size: 0.3,
cell_height: 0.2,
agent_radius: 0.5,
agent_height: 1.8,
agent_max_climb: 0.4,
agent_max_slope: 45.0,
region_min_size: 8,
region_merge_size: 20,
edge_max_len: 12.0,
edge_max_error: 1.3,
verts_per_poly: 6,
detail_sample_distance: 6.0,
detail_sample_max_error: 1.0,
tile_size: 64,
pathfinder: PathfinderConfig {
max_iterations: 2048,
heuristic_scale: 1.0,
},
crowd: CrowdConfig {
max_agents: 128,
avoidance_quality: AvoidanceQuality::High,
},
};
}
Input Configuration
#![allow(unused)]
fn main() {
let input = InputConfig {
deadzone: 0.15,
mouse_sensitivity: 1.0,
mouse_invert_y: false,
mouse_smoothing: true,
gamepad_enabled: true,
gamepad_vibration: true,
action_repeat_delay_ms: 500,
action_repeat_rate_ms: 50,
};
}
Environment Variables
Override configuration with environment variables:
| Variable | Description | Default |
|---|---|---|
ASTRAWEAVE_LOG | Log level (trace, debug, info, warn, error) | info |
ASTRAWEAVE_GRAPHICS_BACKEND | Graphics backend (vulkan, dx12, metal) | Auto |
ASTRAWEAVE_OLLAMA_ENDPOINT | Ollama API endpoint | http://localhost:11434 |
ASTRAWEAVE_OLLAMA_MODEL | Default LLM model | hermes2-pro-mistral |
ASTRAWEAVE_TICK_RATE | Simulation tick rate | 60 |
ASTRAWEAVE_HEADLESS | Run without graphics | false |
Example:
ASTRAWEAVE_LOG=debug ASTRAWEAVE_HEADLESS=true cargo run -p my_game
Platform-Specific Configuration
Windows
[target.'cfg(windows)'.graphics]
backend = "dx12"
Linux
[target.'cfg(target_os = "linux")'.graphics]
backend = "vulkan"
macOS
[target.'cfg(target_os = "macos")'.graphics]
backend = "metal"
Debug Configuration
#![allow(unused)]
fn main() {
#[cfg(debug_assertions)]
let debug_config = DebugConfig {
show_fps: true,
show_entity_count: true,
physics_debug_draw: true,
navmesh_debug_draw: false,
ai_debug_overlay: true,
memory_stats: true,
profiler_enabled: true,
};
}
See Also
CLI Tools
AstraWeave provides command-line tools for development, asset processing, debugging, and deployment.
Overview
| Tool | Purpose |
|---|---|
aw_build | Build and package games |
aw_asset_cli | Asset processing and optimization |
aw_debug | Runtime debugging and inspection |
aw_editor | Visual editor (GUI) |
aw_headless | Headless server for testing |
aw_save_cli | Save file management |
aw_build
Build and package games for distribution.
Installation
cargo install --path tools/aw_build
Usage
# Build for current platform
aw_build
# Build release version
aw_build --release
# Build for specific platform
aw_build --target windows
aw_build --target linux
aw_build --target macos
aw_build --target web
# Full package with assets
aw_build --release --package
# Custom output directory
aw_build --release --output dist/
Configuration
Create aw_build.toml in your project root:
[package]
name = "my-game"
version = "1.0.0"
author = "Developer Name"
description = "An awesome game"
[build]
features = ["release-optimizations"]
target_dir = "target"
[assets]
source = "assets"
compress = true
strip_unused = true
[platforms.windows]
icon = "assets/icons/game.ico"
installer = true
[platforms.linux]
appimage = true
[platforms.macos]
bundle = true
icon = "assets/icons/game.icns"
[platforms.web]
wasm_opt = true
Build Profiles
# Debug build (fast compile, slow runtime)
aw_build --profile debug
# Release build (slow compile, optimized)
aw_build --profile release
# Distribution build (maximum optimization)
aw_build --profile dist
aw_asset_cli
Process and optimize game assets.
Installation
cargo install --path tools/aw_asset_cli
Commands
Import Assets
# Import a single asset
aw_asset_cli import models/character.fbx
# Import with options
aw_asset_cli import models/character.fbx --format gltf --optimize
# Batch import directory
aw_asset_cli import-dir raw_assets/ --recursive
Optimize Assets
# Optimize textures
aw_asset_cli optimize textures/ --compress --generate-mipmaps
# Optimize meshes
aw_asset_cli optimize-mesh models/ --simplify 0.5
# Full optimization pass
aw_asset_cli optimize-all assets/
Pack Assets
# Create asset bundle
aw_asset_cli pack assets/ --output game.pak
# Pack with compression
aw_asset_cli pack assets/ --output game.pak --compression zstd
# Pack specific types
aw_asset_cli pack assets/textures/ --output textures.pak --type texture
Validate Assets
# Check asset integrity
aw_asset_cli validate assets/
# Check specific asset
aw_asset_cli validate models/character.gltf --verbose
# Validate bundle
aw_asset_cli validate-bundle game.pak
Texture Processing
# Convert to KTX2 format
aw_asset_cli texture convert input.png --format ktx2
# Generate mipmaps
aw_asset_cli texture mipmaps input.png
# Compress textures
aw_asset_cli texture compress textures/ --format bc7
# Create texture atlas
aw_asset_cli texture atlas sprites/*.png --output atlas.png --json atlas.json
Mesh Processing
# Simplify mesh
aw_asset_cli mesh simplify model.gltf --ratio 0.5 --output model_lod1.gltf
# Generate LODs
aw_asset_cli mesh lod model.gltf --levels 3
# Optimize vertex cache
aw_asset_cli mesh optimize model.gltf --vertex-cache --output optimized.gltf
aw_debug
Runtime debugging and inspection tools.
Installation
cargo install --path tools/aw_debug
Attach to Running Game
# Connect to local game
aw_debug attach
# Connect to specific port
aw_debug attach --port 9999
# Connect to remote game
aw_debug attach --host 192.168.1.100 --port 9999
Commands (Interactive Mode)
> help # Show available commands
> entities # List all entities
> entity 42 # Inspect entity 42
> components Player # Find entities with Player component
> resource GameState # Inspect resource
> systems # List running systems
> perf # Show performance stats
> mem # Show memory usage
> spawn Enemy 10.0 0.0 5.0 # Spawn entity at position
> despawn 42 # Despawn entity
> pause # Pause game
> resume # Resume game
> step # Step one frame
> quit # Disconnect
Scripted Commands
# Run commands from file
aw_debug attach --script debug_commands.txt
# Run single command
aw_debug attach --exec "entities | grep Player"
# Dump state to file
aw_debug attach --exec "dump world.json"
Profiling
# Start profiling
aw_debug attach --profile
# Capture Tracy trace
aw_debug attach --tracy --output trace.tracy
# Memory profiling
aw_debug attach --memory-profile --output memory.json
aw_editor
Visual editor for scene creation and game development.
Installation
cargo install --path tools/aw_editor
Launch
# Open editor
aw_editor
# Open specific project
aw_editor --project path/to/project
# Open specific scene
aw_editor --project myproject --scene levels/main.scene
Features
- Scene hierarchy view
- Component inspector
- Asset browser
- Behavior tree editor
- Material editor
- Play-in-editor
Keyboard Shortcuts
| Shortcut | Action |
|---|---|
| Ctrl+S | Save scene |
| Ctrl+Shift+S | Save all |
| Ctrl+Z | Undo |
| Ctrl+Shift+Z | Redo |
| F5 | Play |
| Shift+F5 | Stop |
| F | Focus selected |
| Delete | Delete selected |
aw_headless
Run games without rendering for testing and CI.
Installation
cargo install --path tools/aw_headless
Usage
# Run game headlessly
aw_headless --project path/to/game
# Run for specific duration
aw_headless --project game --duration 60
# Run specific test scene
aw_headless --project game --scene test_level.scene
# Run with custom config
aw_headless --project game --config test_config.toml
Testing
# Run integration tests
aw_headless --project game --test integration
# Run performance benchmark
aw_headless --project game --benchmark --output results.json
# Run AI behavior tests
aw_headless --project game --test ai-behaviors --verbose
CI Integration
# GitHub Actions example
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --release
- name: Headless Tests
run: aw_headless --project . --test all --timeout 300
aw_save_cli
Manage game save files.
Installation
cargo install --path tools/aw_save_cli
Commands
# List saves
aw_save_cli list
# Inspect save
aw_save_cli inspect save_001.dat
# Validate save
aw_save_cli validate save_001.dat
# Export to JSON
aw_save_cli export save_001.dat --format json --output save.json
# Import from JSON
aw_save_cli import save.json --output save_new.dat
# Migrate save to new version
aw_save_cli migrate save_001.dat --to-version 2
Save File Locations
| Platform | Default Location |
|---|---|
| Windows | %APPDATA%/GameName/saves/ |
| Linux | ~/.local/share/GameName/saves/ |
| macOS | ~/Library/Application Support/GameName/saves/ |
Common Workflows
Development Build
# Quick iteration
cargo build
cargo run
# With asset hot-reload
ASTRAWEAVE_HOT_RELOAD=1 cargo run
Release Build
# Build optimized release
aw_build --release
# Process assets
aw_asset_cli optimize-all assets/
# Package for distribution
aw_build --release --package
CI/CD Pipeline
# Install tools
cargo install --path tools/aw_build
cargo install --path tools/aw_asset_cli
cargo install --path tools/aw_headless
# Build
aw_build --release
# Test
aw_headless --test all
# Package
aw_build --release --package --output artifacts/
Environment Variables
| Variable | Description |
|---|---|
ASTRAWEAVE_LOG | Log level (trace, debug, info, warn, error) |
ASTRAWEAVE_ASSETS | Asset directory path |
ASTRAWEAVE_CONFIG | Config file path |
ASTRAWEAVE_HOT_RELOAD | Enable hot-reload (1/0) |
ASTRAWEAVE_DEBUG_PORT | Debug server port |
Related Documentation
- Building - Build instructions
- Configuration - Configuration options
- Testing - Testing guide
- Crates Reference - API documentation
Platform Support
AstraWeave targets multiple platforms with varying levels of support and feature availability.
Support Tiers
| Tier | Definition | Guarantees |
|---|---|---|
| Tier 1 | Fully supported, tested in CI | Full feature support, regular testing, priority bug fixes |
| Tier 2 | Supported, community tested | Most features work, issues addressed on best-effort basis |
| Tier 3 | Experimental | May work, no guarantees, community-maintained |
Platform Matrix
Desktop Platforms
| Platform | Tier | Rendering | AI/LLM | Audio | Networking |
|---|---|---|---|---|---|
| Windows 10/11 (x64) | Tier 1 | Vulkan, DX12 | Full | Full | Full |
| Linux (x64) | Tier 1 | Vulkan | Full | Full | Full |
| macOS (x64) | Tier 2 | Metal | Full | Full | Full |
| macOS (Apple Silicon) | Tier 2 | Metal | Full | Full | Full |
Web Platforms
| Platform | Tier | Rendering | AI/LLM | Audio | Networking |
|---|---|---|---|---|---|
| WebAssembly | Tier 2 | WebGPU, WebGL2 | Remote only | Web Audio | WebSocket |
Console Platforms
| Platform | Tier | Notes |
|---|---|---|
| PlayStation 5 | Planned | Post-1.0 |
| Xbox Series X/S | Planned | Post-1.0 |
| Nintendo Switch | Planned | Post-1.0 |
Mobile Platforms
| Platform | Tier | Notes |
|---|---|---|
| Android | Tier 3 | Experimental |
| iOS | Tier 3 | Experimental |
System Requirements
Minimum Requirements
| Component | Windows | Linux | macOS |
|---|---|---|---|
| OS | Windows 10 (1909+) | Ubuntu 20.04+ / Fedora 34+ | macOS 11+ |
| CPU | x64, 4 cores | x64, 4 cores | x64 / Apple M1+ |
| RAM | 8 GB | 8 GB | 8 GB |
| GPU | Vulkan 1.2 / DX12 | Vulkan 1.2 | Metal 2 |
| Storage | 2 GB | 2 GB | 2 GB |
Recommended Requirements
| Component | Specification |
|---|---|
| CPU | 8+ cores, modern (2020+) |
| RAM | 16 GB+ |
| GPU | RTX 2060 / RX 5700 / M1 Pro+ |
| Storage | SSD, 10 GB+ |
For AI/LLM Features
| Feature | Additional Requirements |
|---|---|
| Local LLM (Ollama) | 16 GB+ RAM, fast SSD |
| GPU-accelerated LLM | NVIDIA GPU with 8 GB+ VRAM, CUDA |
| Large context windows | 32 GB+ RAM |
Graphics API Support
Vulkan (Windows, Linux)
Primary rendering backend for Windows and Linux.
[dependencies]
astraweave-render = { version = "0.1", features = ["vulkan"] }
Requirements:
- Vulkan 1.2+
- Up-to-date GPU drivers
Tested GPUs:
- NVIDIA: GTX 1000+, RTX series
- AMD: RX 400+, RX 5000+, RX 6000+, RX 7000+
- Intel: UHD 600+, Arc series
DirectX 12 (Windows)
Alternative backend for Windows.
[dependencies]
astraweave-render = { version = "0.1", features = ["dx12"] }
Requirements:
- Windows 10 1909+
- DirectX 12 capable GPU
Metal (macOS)
Primary backend for macOS.
[dependencies]
astraweave-render = { version = "0.1", features = ["metal"] }
Requirements:
- macOS 11 (Big Sur)+
- Metal 2 capable GPU
WebGPU (Web)
Modern web rendering API.
[dependencies]
astraweave-render = { version = "0.1", features = ["webgpu"] }
Requirements:
- Chrome 113+ / Firefox 115+ / Safari 17+
- WebGPU enabled
WebGL2 (Web Fallback)
Fallback for browsers without WebGPU.
[dependencies]
astraweave-render = { version = "0.1", features = ["webgl2"] }
Limitations:
- Reduced shader capabilities
- Lower texture limits
- No compute shaders
Feature Availability by Platform
Core Features
| Feature | Windows | Linux | macOS | Web |
|---|---|---|---|---|
| ECS | Yes | Yes | Yes | Yes |
| Physics | Yes | Yes | Yes | Yes |
| Audio | Yes | Yes | Yes | Yes |
| Input | Yes | Yes | Yes | Partial |
| Networking | Yes | Yes | Yes | WebSocket |
Rendering Features
| Feature | Windows | Linux | macOS | Web |
|---|---|---|---|---|
| PBR Materials | Yes | Yes | Yes | Yes |
| Shadow Mapping | Yes | Yes | Yes | Limited |
| Post-Processing | Yes | Yes | Yes | Limited |
| Skeletal Animation | Yes | Yes | Yes | Yes |
| Compute Shaders | Yes | Yes | Yes | WebGPU only |
AI Features
| Feature | Windows | Linux | macOS | Web |
|---|---|---|---|---|
| Behavior Trees | Yes | Yes | Yes | Yes |
| GOAP | Yes | Yes | Yes | Yes |
| Local LLM (Ollama) | Yes | Yes | Yes | No |
| Remote LLM | Yes | Yes | Yes | Yes |
| GPU LLM (CUDA) | Yes | Limited | No | No |
Building for Platforms
Windows
# Native build
cargo build --release
# Cross-compile from Linux
cargo build --release --target x86_64-pc-windows-gnu
Linux
# Native build
cargo build --release
# With specific features
cargo build --release --features "vulkan,llm"
macOS
# Native build
cargo build --release
# Universal binary (x64 + ARM)
cargo build --release --target x86_64-apple-darwin
cargo build --release --target aarch64-apple-darwin
lipo -create -output libgame.dylib \
target/x86_64-apple-darwin/release/libgame.dylib \
target/aarch64-apple-darwin/release/libgame.dylib
WebAssembly
# Install wasm-pack
cargo install wasm-pack
# Build for web
wasm-pack build --target web --release
# Optimize WASM
wasm-opt -O3 -o game_opt.wasm game.wasm
Platform-Specific Configuration
Windows Configuration
# astraweave.toml
[platform.windows]
renderer = "dx12" # or "vulkan"
vsync = true
fullscreen_mode = "borderless"
[platform.windows.audio]
backend = "wasapi"
Linux Configuration
[platform.linux]
renderer = "vulkan"
vsync = true
wayland = true # Enable Wayland support
[platform.linux.audio]
backend = "pulseaudio" # or "alsa", "pipewire"
macOS Configuration
[platform.macos]
renderer = "metal"
retina = true
native_fullscreen = true
[platform.macos.audio]
backend = "coreaudio"
Web Configuration
[platform.web]
renderer = "webgpu" # or "webgl2"
canvas_id = "game-canvas"
[platform.web.audio]
backend = "webaudio"
Known Limitations
Windows
- DX12 may require Windows Update for latest features
- Some anti-virus software may flag debug builds
Linux
- Wayland compositor compatibility varies
- Audio backend may need manual selection
macOS
- Notarization required for distribution
- Some features limited without Rosetta on Apple Silicon
Web
- No local file system access
- LLM requires remote server
- Thread support limited (SharedArrayBuffer)
- Binary size considerations
Troubleshooting Platform Issues
Windows: DX12 Errors
Error: Failed to create DX12 device
Solution: Update GPU drivers and Windows.
Linux: Vulkan Not Found
Error: Failed to load Vulkan library
Solution:
# Ubuntu/Debian
sudo apt install vulkan-tools mesa-vulkan-drivers
# Fedora
sudo dnf install vulkan-loader vulkan-tools
macOS: Metal Errors
Error: Metal device not found
Solution: Ensure running on macOS 11+ with Metal-capable GPU.
Web: WebGPU Not Supported
Error: WebGPU not available
Solution: Enable WebGPU in browser settings or use WebGL2 fallback:
[platform.web]
renderer = "webgl2"
Platform Testing
CI/CD Matrix
# GitHub Actions example
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: windows-latest
target: x86_64-pc-windows-msvc
- os: macos-latest
target: x86_64-apple-darwin
Platform-Specific Tests
#![allow(unused)]
fn main() {
#[cfg(target_os = "windows")]
#[test]
fn test_windows_specific() {
// Windows-only test
}
#[cfg(target_os = "linux")]
#[test]
fn test_linux_specific() {
// Linux-only test
}
#[cfg(target_arch = "wasm32")]
#[test]
fn test_wasm_specific() {
// WebAssembly-only test
}
}
Related Documentation
- Configuration - Platform configuration options
- Building - Build instructions
- Troubleshooting - General troubleshooting
- CLI Tools - Command-line tools
Glossary
A comprehensive glossary of terms used throughout the AstraWeave documentation.
A
- Action
- The final stage of the AI loop where an agent executes planned behaviors through validated tool calls.
- Admonish
- An mdBook preprocessor for creating callout blocks (warnings, notes, tips).
- Agent
- An AI-controlled entity in the game world with perception, planning, and action capabilities.
- Arbiter
- The AI system responsible for validating tool calls and ensuring agents can only perform sanctioned actions within game rules.
- Archetype
- In ECS, a unique combination of component types. Entities with the same components share an archetype for cache-efficient storage.
- Asset Pipeline
- The content processing workflow that converts raw assets (textures, models, audio) into engine-optimized formats.
B
- Batch Executor
- A system for grouping multiple LLM inference requests to improve throughput.
- Behavior Tree
- A hierarchical AI decision-making structure where nodes represent conditions, actions, and control flow.
- Blackboard
- A shared data structure for AI agents to read and write state information.
- Biome
- A terrain zone with distinct environmental characteristics (forest, desert, etc.).
C
- Character Controller
- A physics component that handles player/NPC movement with collision detection.
- Clustered Lighting
- A rendering technique that divides the view frustum into clusters for efficient many-light rendering.
- Component
- In ECS, a piece of data attached to an entity. Components contain only data, not behavior.
- Core Loop
- The fundamental AI cycle: Perception -> Reasoning -> Planning -> Action.
D
- Deterministic Simulation
- A simulation where given the same inputs, the outputs are always identical. Essential for replay and networking.
- Director
- An AI system that orchestrates game pacing, difficulty, and narrative events.
E
- ECS (Entity Component System)
- A data-oriented architecture pattern where entities are IDs, components are data, and systems are logic operating on components.
- Entity
- In ECS, a unique identifier (typically an integer with a generation counter) that components are attached to.
- Episode
- In memory systems, a recorded sequence of events that can be recalled for learning.
F
- Fixed Timestep
- A simulation update rate that runs at a constant interval (e.g., 60Hz) regardless of frame rate.
- Frame
- One complete update cycle of the game, including input, simulation, and rendering.
G
- GOAP (Goal-Oriented Action Planning)
- An AI planning algorithm that finds optimal action sequences to achieve goals by searching through possible world states.
- Generational Index
- An entity ID with a generation counter to detect use-after-free bugs when entities are recycled.
H
- HDRI (High Dynamic Range Image)
- An image format storing extended brightness values, commonly used for environment lighting.
I
- IBL (Image-Based Lighting)
- A technique using environment maps to provide realistic ambient lighting.
K
- KTX2
- A texture container format supporting GPU compression and mipmaps.
L
- LLM (Large Language Model)
- AI models (like GPT, Llama, Phi) used for natural language understanding and generation.
- LOD (Level of Detail)
- Rendering optimization that uses simpler geometry for distant objects.
M
- Mermaid
- A JavaScript-based diagramming tool integrated into mdBook for rendering flowcharts and diagrams.
- Mipmap
- Pre-calculated, progressively smaller versions of a texture for efficient rendering at different distances.
- MRA
- Metallic-Roughness-AO texture packing format used in PBR materials.
N
- Nanite
- Unreal Engine 5’s virtualized geometry system. AstraWeave implements similar techniques for GPU-driven LOD.
- A polygon mesh representing walkable areas for AI pathfinding.
O
- Orchestrator
- The AI component that coordinates multiple agents and manages their interactions.
P
- PBR (Physically Based Rendering)
- A rendering approach that simulates realistic light behavior using physical principles.
- Perception
- The first stage of the AI loop where agents observe the game world state.
- Planning
- The AI stage where goals are decomposed into action sequences.
- Preprocessor
- An mdBook tool that transforms source files before rendering.
Q
- Query
- In ECS, a request for entities matching specific component patterns.
R
- RAG (Retrieval-Augmented Generation)
- A technique combining information retrieval with LLM generation for context-aware responses.
- Ragdoll
- A physics-based character simulation for realistic death/knockback animations.
- Rapier
- The Rust physics engine used by AstraWeave.
- Rhai
- An embedded scripting language for Rust, used in AstraWeave for game logic.
S
- Sandbox
- An isolated execution environment for AI tool calls, preventing unauthorized actions.
- SIMD (Single Instruction Multiple Data)
- CPU instructions processing multiple data points simultaneously for performance.
- Spatial Hash
- A data structure for efficient spatial queries (collision detection, neighbor finding).
- System
- In ECS, logic that operates on entities with specific component combinations.
T
- Tick
- One fixed timestep update of the simulation.
- Tool
- In AI systems, an action an agent can take, validated by the Arbiter.
- Tool Vocabulary
- The set of actions available to AI agents, each with schema and validation rules.
U
- Utility AI
- An AI approach where actions are scored by utility functions and the highest-scoring action is selected.
V
- Voxel
- A 3D pixel, used in terrain systems for destructible/modifiable environments.
W
- WGPU
- A Rust graphics library providing a cross-platform abstraction over Vulkan, Metal, DX12, and WebGPU.
- WGSL (WebGPU Shading Language)
- The shader language used with WGPU.
- World
- The container for all entities, components, and resources in the engine.
See Also
FAQ
Frequently asked questions about AstraWeave, the AI-native game engine.
General
What is AstraWeave?
AstraWeave is an AI-native game engine built in Rust. Unlike traditional engines that add AI as an afterthought, AstraWeave places intelligent agents at the core of its architecture. AI companions, enemies, and NPCs use the same validated game systems as players, ensuring fair and emergent gameplay.
How is AstraWeave different from Unity or Unreal?
| Feature | AstraWeave | Unity/Unreal |
|---|---|---|
| AI Architecture | AI-first, tool-validated | AI as addon/plugin |
| LLM Integration | Native, first-class | Third-party plugins |
| Determinism | Guaranteed by design | Optional, complex |
| Language | Rust (safe, fast) | C#/C++ |
| Multiplayer AI | Same validation for AI & players | Separate systems |
Is AstraWeave production-ready?
AstraWeave is in active development (pre-1.0). Core systems are functional and tested, but APIs may change. We recommend it for:
- Research and prototyping
- Indie game development
- Learning AI game development
- Contributing to an open-source engine
What platforms does AstraWeave support?
- Windows: Full support (primary development platform)
- Linux: Full support
- macOS: Supported (Apple Silicon native)
- WebAssembly: Experimental (rendering only)
- Consoles: Planned for post-1.0
AI System
Which LLMs does AstraWeave support?
AstraWeave uses Ollama for local LLM inference. Recommended models:
| Model | Use Case | VRAM Required |
|---|---|---|
hermes2-pro-mistral | Tool calling, dialogue | 8GB |
phi3:mini | Lightweight inference | 4GB |
llama3:8b | General purpose | 8GB |
mistral:7b | Fast inference | 8GB |
Can I use cloud LLMs like GPT-4 or Claude?
Not currently. AstraWeave is designed for local inference to ensure:
- Deterministic behavior
- Low latency (8ms budget per tick)
- Privacy (no data leaves the machine)
- Offline gameplay
Cloud support may be added post-1.0 for specific use cases.
How does AI validation work?
All AI actions go through the Tool Validation System:
#![allow(unused)]
fn main() {
let tool_call = ToolCall {
tool: "move_to",
params: json!({ "target": [10.0, 0.0, 5.0] }),
};
match validator.validate(&tool_call, &world_state) {
ToolResult::Success(action) => execute(action),
ToolResult::Blocked(reason) => handle_failure(reason),
}
}
This prevents AI from cheating - it can only perform actions that would be valid for a player.
Why does my AI companion not respond?
Common causes:
- Ollama not running: Start with
ollama serve - Model not pulled: Run
ollama pull hermes2-pro-mistral - Port conflict: Check Ollama is on port 11434
- Timeout: AI has an 8ms budget per tick; complex queries may timeout
Performance
What are the minimum system requirements?
| Component | Minimum | Recommended |
|---|---|---|
| CPU | 4 cores, 2.5 GHz | 8 cores, 3.5 GHz |
| RAM | 8 GB | 16 GB |
| GPU | GTX 1060 / RX 580 | RTX 3070 / RX 6800 |
| VRAM | 4 GB | 8 GB (for LLM) |
| Storage | 10 GB | 50 GB SSD |
Why is my game running slowly?
- Debug builds: Always use
--releasefor playable performance - LLM inference: Check Ollama is GPU-accelerated
- Physics overhead: Reduce collision complexity
- AI agents: Limit concurrent planning operations
How do I profile my game?
AstraWeave integrates with Tracy profiler:
cargo run -p your_game --release --features profiling
See Performance Optimization for details.
ECS Architecture
Why does AstraWeave use its own ECS?
AstraWeave’s ECS is designed for:
- Deterministic iteration: Same order every tick
- Generational entities: Safe entity references
- AI integration: Perception and planning systems
- Networking: State synchronization
It’s inspired by Bevy but optimized for AI-native gameplay.
How do I create entities?
#![allow(unused)]
fn main() {
let entity = world.spawn((
Transform::from_xyz(0.0, 1.0, 0.0),
RigidBody::Dynamic,
Collider::sphere(0.5),
Health { current: 100.0, max: 100.0 },
));
}
How do I query entities?
#![allow(unused)]
fn main() {
fn damage_system(
mut query: Query<(&mut Health, &Transform), With<Enemy>>,
) {
for (mut health, transform) in query.iter_mut() {
if in_danger_zone(transform) {
health.current -= 10.0;
}
}
}
}
Building and Development
How do I build from source?
git clone https://github.com/lazyxeon/AstraWeave-AI-Native-Gaming-Engine
cd AstraWeave-AI-Native-Gaming-Engine
cargo build --release
Why is the first build so slow?
AstraWeave has many dependencies. First builds compile:
- wgpu (graphics)
- rapier (physics)
- rodio (audio)
- tokio (async runtime)
Subsequent builds are much faster due to incremental compilation.
How do I run the examples?
cargo run -p hello_companion --release
cargo run -p physics_demo3d --release
cargo run -p unified_showcase --release
How do I contribute?
See Contributing Guide. In brief:
- Fork the repository
- Create a feature branch
- Make changes with tests
- Submit a pull request
Troubleshooting
“LosBlocked” error in hello_companion
This is expected behavior! The example demonstrates that AI cannot perform invalid actions. The companion tries to move but line-of-sight is blocked, proving the validation system works.
Shader compilation errors
Ensure your GPU driver is up to date. WGPU requires:
- Vulkan 1.1+ on Windows/Linux
- Metal on macOS
- WebGPU on browsers
“Entity not found” panic
This usually means an entity was despawned while still referenced. Use generational entities:
#![allow(unused)]
fn main() {
if world.get::<Health>(entity).is_some() {
// Entity still exists
}
}
Audio not playing
Check audio device availability and ensure the audio example works:
cargo run -p audio_spatial_demo --release
Licensing
What license is AstraWeave under?
AstraWeave uses a dual license:
- Apache 2.0: For most use cases
- MIT: Alternative option
Commercial use is permitted under both licenses.
Can I use AstraWeave for commercial games?
Yes! Both Apache 2.0 and MIT licenses permit commercial use. Attribution is appreciated but not required.
Getting Help
Where can I get help?
- GitHub Issues: Bug reports and feature requests
- GitHub Discussions: Questions and community chat
- Documentation: This site
- Examples:
examples/directory in the repository
How do I report a bug?
Open a GitHub issue with:
- AstraWeave version
- Operating system
- Steps to reproduce
- Expected vs actual behavior
- Error messages or logs
Performance Tips
Quick reference for optimizing your AstraWeave games. For in-depth performance optimization, see the Performance Guide.
Quick Wins
Build Configuration
Always use release builds for performance testing:
cargo run --release -p your_game
Common Optimizations
| Issue | Solution |
|---|---|
| Low FPS | Use release builds, reduce entity count |
| High memory | Limit AI memory buffers, use asset streaming |
| AI lag | Increase planning interval, reduce perception range |
| Render stutter | Enable frustum culling, use LODs |
AI Performance
#![allow(unused)]
fn main() {
ai_config.planning_interval = Duration::from_millis(500);
ai_config.perception_range = 15.0;
ai_config.max_concurrent_llm_requests = 2;
}
ECS Batching
Process entities in batches:
#![allow(unused)]
fn main() {
fn process_entities(query: Query<(&Transform, &mut Velocity)>) {
query.par_iter_mut().for_each(|(transform, mut velocity)| {
velocity.0 = calculate_velocity(transform);
});
}
}
Performance Targets
| Metric | Target | Measurement |
|---|---|---|
| Frame time | < 16.67ms | 60 FPS |
| AI tick | < 5ms | Planning + execution |
| Physics step | < 4ms | Collision + dynamics |
| Render | < 8ms | Draw calls + GPU |
Profiling Tools
- Tracy: Real-time frame profiler
- Cargo flamegraph: CPU profiling
- RenderDoc: GPU debugging
cargo install cargo-flamegraph
cargo flamegraph --release -p your_game
See Also
- Performance Guide - Complete optimization guide
- Best Practices - Architecture patterns
- Configuration - Performance settings
- Troubleshooting - Common issues
Best Practices
This guide covers recommended practices for building production-quality games with AstraWeave, covering architecture, performance, AI integration, and maintainability.
Project Architecture
Modular Design
Organize game code into focused modules:
my_game/
├── src/
│ ├── main.rs # Entry point
│ ├── lib.rs # Game library root
│ ├── player/ # Player systems
│ │ ├── mod.rs
│ │ ├── controller.rs
│ │ └── inventory.rs
│ ├── enemy/ # Enemy systems
│ │ ├── mod.rs
│ │ ├── ai.rs
│ │ └── spawner.rs
│ ├── world/ # World management
│ │ ├── mod.rs
│ │ ├── terrain.rs
│ │ └── weather.rs
│ └── ui/ # User interface
│ ├── mod.rs
│ ├── hud.rs
│ └── menus.rs
├── assets/
├── config/
└── Cargo.toml
Plugin Pattern
Encapsulate features as plugins:
pub struct PlayerPlugin;
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app
.init_resource::<PlayerConfig>()
.add_systems(Startup, spawn_player)
.add_systems(Update, (
player_movement,
player_combat,
player_inventory,
).chain());
}
}
pub struct EnemyPlugin;
impl Plugin for EnemyPlugin {
fn build(&self, app: &mut App) {
app
.add_systems(Update, (
enemy_ai_tick,
enemy_spawning,
enemy_cleanup,
));
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PlayerPlugin)
.add_plugins(EnemyPlugin)
.run();
}
Dependency Injection
Use resources for configuration and shared state:
#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct GameConfig {
pub difficulty: Difficulty,
pub render_distance: f32,
pub ai_budget_ms: f32,
}
#[derive(Resource)]
pub struct GameState {
pub current_level: String,
pub player_score: u64,
pub elapsed_time: f32,
}
fn game_system(
config: Res<GameConfig>,
mut state: ResMut<GameState>,
) {
// Access shared configuration and state
}
}
ECS Best Practices
Component Design
Do:
#![allow(unused)]
fn main() {
// Small, focused components
#[derive(Component)]
struct Health(f32);
#[derive(Component)]
struct MaxHealth(f32);
#[derive(Component)]
struct Damage(f32);
// Use marker components
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Enemy;
#[derive(Component)]
struct Hostile;
}
Don’t:
#![allow(unused)]
fn main() {
// Avoid monolithic components
#[derive(Component)]
struct Entity {
health: f32,
max_health: f32,
damage: f32,
position: Vec3,
rotation: Quat,
velocity: Vec3,
ai_state: AiState,
inventory: Vec<Item>,
// ... more fields
}
}
System Organization
Group related systems with clear ordering:
#![allow(unused)]
fn main() {
app.add_systems(Update, (
// Input phase
read_player_input,
// AI phase (can run in parallel)
(enemy_perception, companion_perception).run_if(should_run_ai),
enemy_decision,
companion_decision,
// Movement phase
apply_movement,
resolve_collisions,
// Combat phase
process_attacks,
apply_damage,
check_deaths,
// Cleanup phase
despawn_dead_entities,
).chain());
}
Query Best Practices
#![allow(unused)]
fn main() {
// Good: Specific queries with filters
fn enemy_update(
enemies: Query<(&mut Transform, &mut AiAgent), (With<Enemy>, Without<Dead>)>,
) {
for (mut transform, mut ai) in enemies.iter_mut() {
// Only active enemies
}
}
// Good: Read-only where possible
fn render_system(
renderables: Query<(&Transform, &Mesh, &Material)>,
) {
for (transform, mesh, material) in renderables.iter() {
// Read-only access allows parallelism
}
}
// Good: Use change detection
fn on_health_changed(
query: Query<(Entity, &Health), Changed<Health>>,
) {
for (entity, health) in query.iter() {
// Only runs when health actually changes
}
}
}
AI Integration
LLM Usage Guidelines
When to use LLM:
- Dynamic dialogue and conversation
- Procedural narrative generation
- Player behavior analysis
- Creative content generation
When NOT to use LLM:
- Real-time combat decisions (too slow)
- Deterministic game logic
- Anything requiring < 100ms response
AI Architecture
#![allow(unused)]
fn main() {
// Layer AI appropriately
pub struct AiStack {
// Fast: Behavior trees for immediate reactions
pub behavior_tree: BehaviorTree,
// Medium: GOAP for tactical planning
pub planner: GoalPlanner,
// Slow: LLM for strategic/narrative decisions
pub llm_client: Option<LlmClient>,
}
impl AiStack {
pub fn update(&mut self, context: &AiContext) {
// Always run fast layer
self.behavior_tree.tick(context);
// Run planner when needed
if self.needs_replan(context) {
self.planner.plan(context);
}
// Async LLM calls for non-critical decisions
if self.should_consult_llm(context) {
self.queue_llm_request(context);
}
}
}
}
Tool Validation
Always validate LLM tool calls:
#![allow(unused)]
fn main() {
pub fn validate_tool_call(call: &ToolCall, context: &GameContext) -> ToolResult {
match call.name.as_str() {
"move_to" => {
let target: Vec3 = call.parse_param("target")?;
// Validate target is reachable
if !context.navmesh.is_reachable(context.position, target) {
return ToolResult::Error("Target unreachable".into());
}
// Validate target is within range
let distance = context.position.distance(target);
if distance > context.max_move_distance {
return ToolResult::Error("Target too far".into());
}
ToolResult::Success(json!({ "path_found": true }))
}
"attack" => {
let target_id: u64 = call.parse_param("target")?;
// Validate target exists and is attackable
if !context.can_attack(target_id) {
return ToolResult::Error("Cannot attack target".into());
}
ToolResult::Success(json!({ "attack_initiated": true }))
}
_ => ToolResult::Error("Unknown tool".into()),
}
}
}
Performance
Frame Budget Management
#![allow(unused)]
fn main() {
#[derive(Resource)]
pub struct FrameBudget {
pub target_fps: f32,
pub ai_budget_pct: f32,
pub physics_budget_pct: f32,
pub render_budget_pct: f32,
}
impl FrameBudget {
pub fn frame_time_ms(&self) -> f32 {
1000.0 / self.target_fps
}
pub fn ai_budget_ms(&self) -> f32 {
self.frame_time_ms() * self.ai_budget_pct
}
}
}
Async Operations
#![allow(unused)]
fn main() {
// Load assets asynchronously
fn load_level_async(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut loading_state: ResMut<LoadingState>,
) {
if loading_state.started {
return;
}
loading_state.handles.push(asset_server.load_async::<Scene>("levels/level1.scn"));
loading_state.started = true;
}
// Check loading progress
fn check_loading(
asset_server: Res<AssetServer>,
loading_state: Res<LoadingState>,
mut next_state: ResMut<NextState<GameState>>,
) {
if loading_state.handles.iter().all(|h| asset_server.is_loaded(h)) {
next_state.set(GameState::Playing);
}
}
}
Memory Management
#![allow(unused)]
fn main() {
// Use object pools for frequent spawn/despawn
#[derive(Resource)]
pub struct ProjectilePool {
available: Vec<Entity>,
active: HashSet<Entity>,
}
impl ProjectilePool {
pub fn get(&mut self, commands: &mut Commands) -> Entity {
if let Some(entity) = self.available.pop() {
commands.entity(entity).insert(Visible);
self.active.insert(entity);
entity
} else {
let entity = commands.spawn(ProjectileBundle::default()).id();
self.active.insert(entity);
entity
}
}
pub fn return_entity(&mut self, entity: Entity, commands: &mut Commands) {
commands.entity(entity).remove::<Visible>();
self.active.remove(&entity);
self.available.push(entity);
}
}
}
Error Handling
Graceful Degradation
#![allow(unused)]
fn main() {
pub fn ai_system_with_fallback(
mut agents: Query<&mut AiAgent>,
llm: Option<Res<LlmClient>>,
) {
for mut agent in agents.iter_mut() {
let decision = if let Some(ref llm) = llm {
// Try LLM-based decision
match llm.try_decide(&agent.context) {
Ok(decision) => decision,
Err(e) => {
warn!("LLM failed: {}, using fallback", e);
agent.fallback_decision()
}
}
} else {
// No LLM available, use behavior tree
agent.behavior_tree.tick()
};
agent.execute(decision);
}
}
}
Structured Errors
#![allow(unused)]
fn main() {
#[derive(Debug, thiserror::Error)]
pub enum GameError {
#[error("Asset not found: {0}")]
AssetNotFound(String),
#[error("Invalid game state: {0}")]
InvalidState(String),
#[error("Network error: {0}")]
Network(#[from] NetworkError),
#[error("AI error: {0}")]
Ai(#[from] AiError),
}
pub type GameResult<T> = Result<T, GameError>;
}
Testing
Unit Testing Systems
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_damage_system() {
let mut world = World::new();
let entity = world.spawn((Health(100.0), DamageQueue::default())).id();
world.get_mut::<DamageQueue>(entity).unwrap().push(Damage(30.0));
let mut schedule = Schedule::default();
schedule.add_systems(apply_damage_system);
schedule.run(&mut world);
assert_eq!(world.get::<Health>(entity).unwrap().0, 70.0);
}
}
}
Integration Testing
#![allow(unused)]
fn main() {
#[test]
fn test_combat_flow() {
let mut app = App::new();
app.add_plugins(MinimalPlugins);
app.add_plugins(CombatPlugin);
app.world.spawn((
Player,
Health(100.0),
Transform::default(),
));
app.world.spawn((
Enemy,
Health(50.0),
Transform::from_xyz(5.0, 0.0, 0.0),
));
// Simulate multiple frames
for _ in 0..100 {
app.update();
}
// Verify expected state
}
}
Debugging
Logging
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, trace, instrument};
#[instrument(skip(query))]
fn my_system(query: Query<(&Transform, &Health)>) {
for (transform, health) in query.iter() {
trace!("Processing entity at {:?}", transform.translation);
if health.0 < 10.0 {
warn!("Entity at low health: {}", health.0);
}
}
}
}
Debug Visualization
#![allow(unused)]
fn main() {
fn debug_draw_system(
mut gizmos: Gizmos,
agents: Query<(&Transform, &AiAgent)>,
) {
#[cfg(debug_assertions)]
for (transform, agent) in agents.iter() {
// Draw perception radius
gizmos.circle(transform.translation, Vec3::Y, agent.perception_radius, Color::YELLOW);
// Draw path
if let Some(path) = &agent.current_path {
for window in path.windows(2) {
gizmos.line(window[0], window[1], Color::GREEN);
}
}
}
}
}
Security
Input Validation
#![allow(unused)]
fn main() {
pub fn validate_player_input(input: &PlayerInput) -> Result<(), InputError> {
// Validate movement speed
if input.movement.length() > MAX_MOVEMENT_SPEED {
return Err(InputError::InvalidMovement);
}
// Validate action cooldowns
if input.action.is_some() && !can_perform_action() {
return Err(InputError::ActionOnCooldown);
}
Ok(())
}
}
Save Data Integrity
#![allow(unused)]
fn main() {
use sha2::{Sha256, Digest};
pub fn save_game(state: &GameState) -> Result<(), SaveError> {
let data = bincode::serialize(state)?;
let hash = Sha256::digest(&data);
let save = SaveFile {
version: SAVE_VERSION,
data,
checksum: hash.to_vec(),
};
std::fs::write("save.dat", bincode::serialize(&save)?)?;
Ok(())
}
pub fn load_game() -> Result<GameState, LoadError> {
let bytes = std::fs::read("save.dat")?;
let save: SaveFile = bincode::deserialize(&bytes)?;
// Verify checksum
let hash = Sha256::digest(&save.data);
if hash.as_slice() != save.checksum {
return Err(LoadError::CorruptedSave);
}
Ok(bincode::deserialize(&save.data)?)
}
}
Summary Checklist
- [ ] All systems have error handling
- [ ] Performance profiled on target hardware
- [ ] AI fallbacks tested
- [ ] Save/load tested extensively
- [ ] Memory usage profiled
- [ ] Input validation complete
- [ ] Logging appropriate for production
- [ ] All assets properly loaded
- [ ] Multiplayer desync handled (if applicable)
Related Documentation
- Performance Optimization - Detailed performance guide
- Configuration Reference - Configuration options
- Testing Guide - Testing strategies
- Design Patterns - Common patterns
Design Patterns
This guide covers common design patterns used in AstraWeave game development, with practical examples and when to apply each pattern.
Entity Patterns
Marker Components
Use empty components to categorize entities:
#![allow(unused)]
fn main() {
#[derive(Component)]
struct Player;
#[derive(Component)]
struct Enemy;
#[derive(Component)]
struct NPC;
#[derive(Component)]
struct Interactable;
// Query with markers
fn player_system(query: Query<&Transform, With<Player>>) {
for transform in query.iter() {
// Only players
}
}
fn enemy_system(query: Query<&Transform, (With<Enemy>, Without<Dead>)>) {
for transform in query.iter() {
// Only living enemies
}
}
}
Component Bundles
Group related components for entity archetypes:
#![allow(unused)]
fn main() {
#[derive(Bundle)]
struct CharacterBundle {
transform: Transform,
health: Health,
velocity: Velocity,
collider: Collider,
}
#[derive(Bundle)]
struct PlayerBundle {
character: CharacterBundle,
player: Player,
inventory: Inventory,
controller: PlayerController,
}
#[derive(Bundle)]
struct EnemyBundle {
character: CharacterBundle,
enemy: Enemy,
ai: AiAgent,
loot_table: LootTable,
}
// Spawn with bundles
fn spawn_player(mut commands: Commands) {
commands.spawn(PlayerBundle {
character: CharacterBundle {
transform: Transform::default(),
health: Health(100.0),
velocity: Velocity::default(),
collider: Collider::capsule(0.5, 1.8),
},
player: Player,
inventory: Inventory::new(20),
controller: PlayerController::default(),
});
}
}
Entity References
Store references between entities:
#![allow(unused)]
fn main() {
#[derive(Component)]
struct Parent(Entity);
#[derive(Component)]
struct Children(Vec<Entity>);
#[derive(Component)]
struct Target(Option<Entity>);
#[derive(Component)]
struct Owner(Entity);
fn targeting_system(
mut agents: Query<(&Transform, &mut Target), With<Enemy>>,
targets: Query<(Entity, &Transform), With<Player>>,
) {
for (agent_transform, mut target) in agents.iter_mut() {
let nearest = targets
.iter()
.min_by_key(|(_, t)| {
OrderedFloat(t.translation.distance(agent_transform.translation))
})
.map(|(e, _)| e);
target.0 = nearest;
}
}
}
State Patterns
State Machine
Implement entity state machines:
#![allow(unused)]
fn main() {
#[derive(Component, Default)]
enum CharacterState {
#[default]
Idle,
Walking,
Running,
Jumping,
Attacking,
Stunned { duration: f32 },
Dead,
}
fn state_machine_system(
time: Res<Time>,
mut query: Query<(&mut CharacterState, &Velocity, &Health)>,
) {
let dt = time.delta_seconds();
for (mut state, velocity, health) in query.iter_mut() {
// Check for death
if health.0 <= 0.0 && !matches!(*state, CharacterState::Dead) {
*state = CharacterState::Dead;
continue;
}
// State transitions
*state = match &*state {
CharacterState::Stunned { duration } => {
let remaining = duration - dt;
if remaining <= 0.0 {
CharacterState::Idle
} else {
CharacterState::Stunned { duration: remaining }
}
}
CharacterState::Dead => CharacterState::Dead,
_ => {
if velocity.0.length() > 5.0 {
CharacterState::Running
} else if velocity.0.length() > 0.1 {
CharacterState::Walking
} else {
CharacterState::Idle
}
}
};
}
}
}
Global Game State
Use app states for high-level game flow:
#![allow(unused)]
fn main() {
#[derive(States, Debug, Clone, PartialEq, Eq, Hash, Default)]
enum GameState {
#[default]
Loading,
MainMenu,
Playing,
Paused,
GameOver,
}
fn setup_states(app: &mut App) {
app
.add_state::<GameState>()
.add_systems(OnEnter(GameState::Loading), start_loading)
.add_systems(OnEnter(GameState::MainMenu), show_menu)
.add_systems(OnExit(GameState::MainMenu), hide_menu)
.add_systems(Update, game_logic.run_if(in_state(GameState::Playing)))
.add_systems(OnEnter(GameState::Paused), show_pause_menu)
.add_systems(OnEnter(GameState::GameOver), show_game_over);
}
}
Resource Patterns
Configuration Resources
Store runtime configuration:
#![allow(unused)]
fn main() {
#[derive(Resource, Default)]
struct GameSettings {
master_volume: f32,
music_volume: f32,
sfx_volume: f32,
mouse_sensitivity: f32,
difficulty: Difficulty,
}
#[derive(Resource)]
struct LevelConfig {
spawn_rate: f32,
max_enemies: usize,
boss_health_multiplier: f32,
}
fn load_settings(mut settings: ResMut<GameSettings>) {
if let Ok(data) = std::fs::read_to_string("settings.toml") {
if let Ok(loaded) = toml::from_str(&data) {
*settings = loaded;
}
}
}
}
Service Resources
Encapsulate complex functionality:
#![allow(unused)]
fn main() {
#[derive(Resource)]
struct AudioManager {
music_channel: AudioChannel,
sfx_channels: Vec<AudioChannel>,
ambient_channel: AudioChannel,
}
impl AudioManager {
fn play_music(&mut self, track: &str, fade_in: f32) {
self.music_channel.fade_in(track, fade_in);
}
fn play_sfx(&mut self, sound: &str, position: Vec3) {
if let Some(channel) = self.sfx_channels.iter_mut().find(|c| c.is_available()) {
channel.play_spatial(sound, position);
}
}
}
#[derive(Resource)]
struct DialogueManager {
active_dialogue: Option<DialogueTree>,
history: Vec<DialogueEntry>,
}
}
Event Patterns
Game Events
Use events for decoupled communication:
#![allow(unused)]
fn main() {
#[derive(Event)]
struct DamageEvent {
target: Entity,
amount: f32,
damage_type: DamageType,
source: Option<Entity>,
}
#[derive(Event)]
struct DeathEvent {
entity: Entity,
killer: Option<Entity>,
position: Vec3,
}
#[derive(Event)]
struct ItemPickupEvent {
player: Entity,
item: ItemId,
quantity: u32,
}
fn damage_system(
mut damage_events: EventReader<DamageEvent>,
mut death_events: EventWriter<DeathEvent>,
mut query: Query<(&mut Health, &Transform)>,
) {
for event in damage_events.iter() {
if let Ok((mut health, transform)) = query.get_mut(event.target) {
health.0 -= event.amount;
if health.0 <= 0.0 {
death_events.send(DeathEvent {
entity: event.target,
killer: event.source,
position: transform.translation,
});
}
}
}
}
fn death_system(
mut commands: Commands,
mut death_events: EventReader<DeathEvent>,
loot_query: Query<&LootTable>,
) {
for event in death_events.iter() {
// Spawn loot
if let Ok(loot) = loot_query.get(event.entity) {
spawn_loot(&mut commands, loot, event.position);
}
// Play death effects
commands.spawn(ParticleEffect::death(event.position));
// Mark entity for removal
commands.entity(event.entity).insert(Dead);
}
}
}
Event Chains
Chain events for complex interactions:
#![allow(unused)]
fn main() {
// Event flow: Attack -> Hit -> Damage -> Death -> Loot -> XP
#[derive(Event)]
struct AttackEvent { attacker: Entity, weapon: Entity }
#[derive(Event)]
struct HitEvent { attacker: Entity, target: Entity, damage: f32 }
#[derive(Event)]
struct XpGainEvent { player: Entity, amount: u32 }
fn attack_resolution(
mut attacks: EventReader<AttackEvent>,
mut hits: EventWriter<HitEvent>,
weapons: Query<&Weapon>,
transforms: Query<&Transform>,
) {
for attack in attacks.iter() {
if let Ok(weapon) = weapons.get(attack.weapon) {
// Perform hit detection
for (entity, hit_point) in detect_hits(attack.attacker, weapon, &transforms) {
hits.send(HitEvent {
attacker: attack.attacker,
target: entity,
damage: weapon.damage,
});
}
}
}
}
}
AI Patterns
Behavior Tree
Structure AI decisions hierarchically:
#![allow(unused)]
fn main() {
pub enum BtNode {
Selector(Vec<BtNode>),
Sequence(Vec<BtNode>),
Condition(Box<dyn Fn(&AiContext) -> bool>),
Action(Box<dyn Fn(&mut AiContext) -> BtStatus>),
}
#[derive(Clone, Copy, PartialEq)]
pub enum BtStatus {
Success,
Failure,
Running,
}
fn create_enemy_bt() -> BtNode {
BtNode::Selector(vec![
// Priority 1: Flee if low health
BtNode::Sequence(vec![
BtNode::Condition(Box::new(|ctx| ctx.health_percent() < 0.2)),
BtNode::Action(Box::new(|ctx| ctx.flee())),
]),
// Priority 2: Attack if in range
BtNode::Sequence(vec![
BtNode::Condition(Box::new(|ctx| ctx.has_target())),
BtNode::Condition(Box::new(|ctx| ctx.in_attack_range())),
BtNode::Action(Box::new(|ctx| ctx.attack())),
]),
// Priority 3: Chase target
BtNode::Sequence(vec![
BtNode::Condition(Box::new(|ctx| ctx.has_target())),
BtNode::Action(Box::new(|ctx| ctx.chase_target())),
]),
// Priority 4: Patrol
BtNode::Action(Box::new(|ctx| ctx.patrol())),
])
}
}
Blackboard
Share data between AI systems:
#![allow(unused)]
fn main() {
#[derive(Component, Default)]
pub struct Blackboard {
values: HashMap<String, BlackboardValue>,
}
#[derive(Clone)]
pub enum BlackboardValue {
Bool(bool),
Float(f32),
Int(i32),
Entity(Entity),
Vec3(Vec3),
String(String),
}
impl Blackboard {
pub fn set<K: Into<String>>(&mut self, key: K, value: BlackboardValue) {
self.values.insert(key.into(), value);
}
pub fn get(&self, key: &str) -> Option<&BlackboardValue> {
self.values.get(key)
}
pub fn get_entity(&self, key: &str) -> Option<Entity> {
match self.get(key) {
Some(BlackboardValue::Entity(e)) => Some(*e),
_ => None,
}
}
}
fn perception_system(
mut agents: Query<(&Transform, &Perception, &mut Blackboard)>,
targets: Query<(Entity, &Transform), With<Player>>,
) {
for (transform, perception, mut blackboard) in agents.iter_mut() {
let nearest = find_nearest_in_range(transform, &targets, perception.radius);
if let Some((entity, distance)) = nearest {
blackboard.set("target", BlackboardValue::Entity(entity));
blackboard.set("target_distance", BlackboardValue::Float(distance));
blackboard.set("has_target", BlackboardValue::Bool(true));
} else {
blackboard.set("has_target", BlackboardValue::Bool(false));
}
}
}
}
System Patterns
System Sets
Organize systems with explicit ordering:
#![allow(unused)]
fn main() {
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum GameSystems {
Input,
Ai,
Movement,
Combat,
Effects,
Cleanup,
}
fn configure_systems(app: &mut App) {
app.configure_sets(Update, (
GameSystems::Input,
GameSystems::Ai,
GameSystems::Movement,
GameSystems::Combat,
GameSystems::Effects,
GameSystems::Cleanup,
).chain());
app
.add_systems(Update, read_input.in_set(GameSystems::Input))
.add_systems(Update, (
ai_perception,
ai_planning,
ai_execution,
).chain().in_set(GameSystems::Ai))
.add_systems(Update, (
apply_velocity,
resolve_collisions,
).chain().in_set(GameSystems::Movement));
}
}
Run Conditions
Control when systems execute:
#![allow(unused)]
fn main() {
fn run_if_playing(state: Res<State<GameState>>) -> bool {
*state.get() == GameState::Playing
}
fn run_if_has_enemies(enemies: Query<(), With<Enemy>>) -> bool {
!enemies.is_empty()
}
fn run_every_n_frames(mut counter: Local<u32>) -> bool {
*counter = (*counter + 1) % 5;
*counter == 0
}
app
.add_systems(Update, game_logic.run_if(run_if_playing))
.add_systems(Update, enemy_ai.run_if(run_if_has_enemies))
.add_systems(Update, expensive_system.run_if(run_every_n_frames));
}
Data Patterns
Flyweight
Share immutable data across entities:
#![allow(unused)]
fn main() {
#[derive(Resource)]
struct WeaponDatabase {
weapons: HashMap<WeaponId, WeaponData>,
}
#[derive(Clone)]
struct WeaponData {
name: String,
base_damage: f32,
attack_speed: f32,
mesh: Handle<Mesh>,
icon: Handle<Texture>,
}
#[derive(Component)]
struct Weapon {
id: WeaponId,
level: u32,
durability: f32,
}
impl Weapon {
fn get_data<'a>(&self, db: &'a WeaponDatabase) -> &'a WeaponData {
db.weapons.get(&self.id).expect("Invalid weapon ID")
}
fn calculate_damage(&self, db: &WeaponDatabase) -> f32 {
let data = self.get_data(db);
data.base_damage * (1.0 + self.level as f32 * 0.1)
}
}
}
Observer
React to component changes:
#![allow(unused)]
fn main() {
fn on_health_changed(
query: Query<(Entity, &Health, &MaxHealth), Changed<Health>>,
mut damage_flash: Query<&mut DamageFlash>,
) {
for (entity, health, max_health) in query.iter() {
let health_percent = health.0 / max_health.0;
// Trigger damage flash
if let Ok(mut flash) = damage_flash.get_mut(entity) {
flash.trigger();
}
// Update health bar
if health_percent < 0.25 {
// Critical health effects
}
}
}
fn on_entity_added(
query: Query<Entity, Added<Enemy>>,
mut enemy_count: ResMut<EnemyCount>,
) {
for _entity in query.iter() {
enemy_count.0 += 1;
}
}
}
Command Pattern
Deferred Commands
Queue operations for later execution:
#![allow(unused)]
fn main() {
#[derive(Resource, Default)]
struct CommandQueue {
commands: Vec<GameCommand>,
}
enum GameCommand {
SpawnEnemy { position: Vec3, enemy_type: EnemyType },
DealDamage { target: Entity, amount: f32 },
GiveItem { player: Entity, item: ItemId },
TriggerEvent { event_name: String },
}
fn queue_command(queue: &mut CommandQueue, command: GameCommand) {
queue.commands.push(command);
}
fn execute_commands(
mut commands: Commands,
mut queue: ResMut<CommandQueue>,
mut damage_events: EventWriter<DamageEvent>,
) {
for command in queue.commands.drain(..) {
match command {
GameCommand::SpawnEnemy { position, enemy_type } => {
commands.spawn(EnemyBundle::new(enemy_type, position));
}
GameCommand::DealDamage { target, amount } => {
damage_events.send(DamageEvent { target, amount, .. });
}
// Handle other commands
_ => {}
}
}
}
}
Summary
| Pattern | Use Case |
|---|---|
| Marker Components | Entity categorization |
| Bundles | Entity archetypes |
| State Machine | Entity behavior |
| App States | Game flow |
| Events | Decoupled communication |
| Behavior Tree | AI decisions |
| Blackboard | AI data sharing |
| System Sets | Execution ordering |
| Run Conditions | Conditional execution |
| Flyweight | Shared data |
| Observer | Change reactions |
| Command Queue | Deferred execution |
Related Documentation
- Best Practices - General guidelines
- ECS Architecture - ECS fundamentals
- AI System - AI patterns in depth
- Performance - Pattern performance
Troubleshooting
This guide helps you solve common issues when working with AstraWeave. Issues are organized by category with specific solutions.
Build Issues
Rust Toolchain Problems
Error: “rustc version mismatch”
error: rustc version doesn't match the expected version
Solution:
# Remove existing toolchain and reinstall
rustup toolchain uninstall stable
rustup toolchain install 1.89.0
rustup default 1.89.0
# Verify version
rustc --version # Should show 1.89.0
Error: “rust-toolchain.toml not respected”
Solution:
# Force toolchain installation
rustup toolchain install 1.89.0
rustup override set 1.89.0
# Clean and rebuild
cargo clean
cargo build -p astraweave-core
Dependency Issues
Error: “linker cc not found”
Linux Solution:
sudo apt-get install build-essential
macOS Solution:
xcode-select --install
Windows Solution: Install Visual Studio with C++ build tools.
Error: “failed to find required package”
error: could not find `wgpu` in the registry
Solution:
# Update Cargo registry
cargo update
# If still failing, clear cache
rm -rf ~/.cargo/registry
cargo update
Graphics Dependencies
Error: “Vulkan not found”
Linux Solution:
# Ubuntu/Debian
sudo apt-get install mesa-vulkan-drivers vulkan-tools
# Arch Linux
sudo pacman -S vulkan-devel mesa
# Fedora
sudo dnf install vulkan-devel mesa-dri-drivers
# Verify Vulkan
vulkaninfo | head -20
Windows Solution: Update your graphics drivers from manufacturer website:
- NVIDIA: Download latest drivers
- AMD: Download Adrenalin drivers
- Intel: Download latest graphics drivers
macOS Solution: Vulkan support requires MoltenVK:
brew install molten-vk
Error: “wgpu adapter not found”
thread 'main' panicked at 'No suitable graphics adapter found'
Solutions:
-
Check GPU compatibility:
# Linux: Check Vulkan support vulkaninfo # Should show at least one device -
Force software rendering:
export WGPU_BACKEND=gl cargo run -p hello_companion -
Update graphics drivers
Audio Dependencies
Error: “ALSA lib errors” (Linux)
ALSA lib pcm_dmix.c:1089:(snd_pcm_dmix_open) unable to open slave
Solution:
# Install audio libraries
sudo apt-get install libasound2-dev libpulse-dev
# Check audio devices
aplay -l
# If no devices, check PulseAudio
pulseaudio --check
Error: “No audio output device found”
Linux Solution:
# Restart audio services
systemctl --user restart pulseaudio
Windows Solution: Check that Windows Audio service is running in Services.
macOS Solution: Usually works out of the box. Check System Preferences > Sound.
Example Compilation Issues
Error: “examples fail to compile”
Many examples have known compilation issues. Use only the working examples:
Working Examples:
cargo build -p hello_companion # ✅ Works (expected panic)
cargo build -p ipc_loopback # ✅ Should work
cargo test -p astraweave-input # ✅ Tests pass
Known Broken Examples:
# ❌ These have compilation issues:
# cargo build -p debug_toolkit_demo # egui/winit/renderer API mismatches
# cargo build -p aw_editor # eframe/glutin sync/send trait issues
# cargo build -p aw_debug # eframe API mismatches
# cargo build -p visual_3d # clippy deny-level errors
# cargo build -p navmesh_demo # clippy deny-level errors (approx_constant)
# cargo build -p physics_demo3d # clippy deny-level errors (approx_constant)
Workaround: Focus on the working core components and use the provided aliases:
cargo build-core # Build core components only
cargo check-all # Check workspace (excluding problematic crates)
cargo clippy-all # Run clippy on working crates
Runtime Issues
Graphics Issues
Error: “Validation error in wgpu”
wgpu validation error: Buffer usage VERTEX | COPY_DST is not valid
Solutions:
- Update graphics drivers
- Use older wgpu backend:
export WGPU_BACKEND=vulkan # or gl, metal, dx12 - Reduce graphics settings in your code
Error: “Surface creation failed”
Error creating surface: SurfaceError(OutOfMemory)
Solutions:
- Reduce window size:
#![allow(unused)] fn main() { // In your window configuration .with_inner_size(winit::dpi::LogicalSize::new(800, 600)) } - Lower graphics quality settings
- Check available VRAM:
# Linux nvidia-smi # for NVIDIA radeontop # for AMD
AI Model Issues
Error: “AI model not found”
Error: Could not load AI model 'companion-7b'
Solutions:
-
Use mock AI for testing:
#![allow(unused)] fn main() { // In your configuration ai_agent.ai_model = AIModel::Mock; } -
Download required models:
# Models not included in repository # Use mock or implement your own model loader -
Configure model path:
#![allow(unused)] fn main() { ai_agent.ai_model = AIModel::Local("path/to/your/model".to_string()); }
Performance Issues
Issue: “Low FPS / Stuttering”
Diagnosis:
# Always use release builds for performance testing
cargo run -p hello_companion --release
# Check if running in debug mode
cargo run -p hello_companion # This is debug mode - will be slow
Solutions:
-
Always use release builds:
cargo build --release cargo run --release -p your_example -
Check system resources:
# Linux htop # Monitor GPU usage nvidia-smi # NVIDIA radeontop # AMD -
Reduce AI complexity:
#![allow(unused)] fn main() { // Lower AI planning frequency ai_agent.planning_interval = Duration::from_millis(1000); // Instead of 500 // Reduce perception range ai_agent.perception_range = 5.0; // Instead of 10.0 }
Issue: “High memory usage”
Diagnosis:
# Check memory usage
cargo run --release -p hello_companion &
ps aux | grep hello_companion
Solutions:
-
Limit AI memory:
#![allow(unused)] fn main() { ai_memory.max_episodic_memories = 100; ai_memory.max_working_memory = 10; } -
Use memory profiling:
# Install valgrind (Linux) sudo apt-get install valgrind valgrind --tool=massif cargo run --release -p hello_companion
Network Issues
Error: “Connection refused” (multiplayer examples)
Error: Connection refused (os error 111)
Solutions:
-
Check if server is running:
# Terminal 1 - Start server first cargo run -p coop_server --release # Terminal 2 - Then client cargo run -p coop_client --release -
Check firewall settings:
# Linux: Check if port is open sudo ufw status # Allow port if needed sudo ufw allow 8080 -
Use localhost:
#![allow(unused)] fn main() { // Make sure client connects to localhost let server_addr = "127.0.0.1:8080"; }
Development Issues
IDE Problems
Issue: “rust-analyzer not working”
Solution:
# Restart rust-analyzer
# In VS Code: Ctrl+Shift+P > "Rust Analyzer: Restart Server"
# Or reinstall
rustup component add rust-analyzer
Issue: “Slow code completion”
Solutions:
- Exclude target directory from indexing
- Reduce project scope:
// In VS Code settings.json { "rust-analyzer.cargo.allFeatures": false, "rust-analyzer.checkOnSave.allFeatures": false }
Testing Issues
Error: “Tests hanging”
cargo test -p astraweave-input
# Hangs indefinitely
Solutions:
-
Run with timeout:
timeout 30s cargo test -p astraweave-input -
Run single test:
cargo test -p astraweave-input test_input_system -
Use single-threaded execution:
cargo test -p astraweave-input -- --test-threads=1
Error: “Test failures due to timing”
thread 'ai_planning_test' panicked at 'assertion failed: plan.is_some()'
Solution: Tests involving AI may have timing dependencies:
#![allow(unused)]
fn main() {
// Add delays in tests
#[test]
fn ai_planning_test() {
let mut world = create_test_world();
world.step(); // Let one frame pass
std::thread::sleep(Duration::from_millis(100)); // Give AI time to plan
let plan = world.get_ai_plan();
assert!(plan.is_some());
}
}
Platform-Specific Issues
Linux Issues
Issue: “Wayland compatibility”
Some features may not work correctly on Wayland:
# Force X11 if needed
export WAYLAND_DISPLAY=""
export DISPLAY=:0
# Or force Wayland if X11 is causing issues
export DISPLAY=""
Issue: “Audio permission denied”
# Add user to audio group
sudo usermod -a -G audio $USER
# Restart session or reboot
macOS Issues
Issue: “Code signing errors”
error: codesign failed with exit code 1
Solution:
# For development, disable code signing
export MACOSX_DEPLOYMENT_TARGET=11.0
# Or sign manually
codesign --force --deep --sign - target/release/hello_companion
Issue: “Metal validation errors”
Use software rendering if Metal causes issues:
export WGPU_BACKEND=gl
cargo run -p hello_companion --release
Windows Issues
Issue: “MSVC runtime missing”
Install Microsoft Visual C++ Redistributable:
- Download from Microsoft’s website
- Or install Visual Studio with C++ tools
Issue: “Antivirus blocking execution”
Add exclusions for:
- Project directory
%USERPROFILE%\.cargotarget\directory
Issue: “Path too long errors”
# Enable long paths in Windows
# Run as Administrator in PowerShell:
New-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Value 1 -PropertyType DWORD -Force
Getting Help
Before Asking for Help
- Check this troubleshooting guide
- Verify your setup:
rustc --version # Should be 1.89.0 cargo --version - Try with minimal example:
cargo run -p hello_companion --release - Check system dependencies
Information to Include
When reporting issues, include:
-
System information:
# Linux uname -a lsb_release -a # macOS sw_vers # Windows systeminfo -
Rust version:
rustc --version cargo --version -
Graphics information:
# Linux lspci | grep VGA vulkaninfo | head -20 # Windows dxdiag # macOS system_profiler SPDisplaysDataType -
Full error output:
# Include full error with backtrace RUST_BACKTRACE=full cargo run -p hello_companion 2>&1 | tee error.log -
Steps to reproduce
Community Resources
- GitHub Issues: For bug reports and feature requests
- Discussions: For questions and general help
- Matrix/Discord: For real-time community support (if available)
Known Limitations
Current Development State
AstraWeave is under active development. Known limitations:
- Many examples don’t compile due to API evolution
- Limited AI model integration - mostly uses mock AI
- Graphics API compatibility - some newer GPU features not supported
- Documentation gaps - some advanced features lack documentation
Workarounds
- Focus on working examples (hello_companion, core components)
- Use mock AI for learning the architecture
- Stick to stable APIs in core crates
- Contribute fixes for broken examples
If you’re still having issues after trying these solutions, please create an issue on GitHub with the requested information. The community is here to help!
Community
Welcome to the AstraWeave community! This page lists resources for connecting with other developers, getting help, and contributing to the project.
Official Channels
GitHub
The primary home for AstraWeave development:
| Resource | Description |
|---|---|
| Repository | Source code, issues, and pull requests |
| Discussions | Q&A, ideas, and general discussion |
| Issues | Bug reports and feature requests |
| Projects | Development roadmap and tracking |
Discord
Join our Discord server for real-time chat:
- #general - Introduce yourself and chat with the community
- #help - Get help with AstraWeave questions
- #showcase - Share your projects and creations
- #ai-discussion - Discuss AI/LLM integration topics
- #development - Engine development discussion
- #announcements - Official news and updates
Social Media
- Twitter/X: @AstraWeaveEngine - News and updates
- YouTube: AstraWeave - Tutorials and devlogs
- Reddit: r/astraweave - Community discussions
Getting Help
Before Asking
-
Search existing resources:
-
Prepare your question:
- Describe what you’re trying to do
- Show what you’ve tried
- Include relevant code snippets
- Share error messages
Where to Ask
| Question Type | Best Channel |
|---|---|
| Quick questions | Discord #help |
| Detailed technical questions | GitHub Discussions |
| Bug reports | GitHub Issues |
| Feature requests | GitHub Issues or Discussions |
| Security issues | security@astraweave.dev (private) |
How to Ask Good Questions
## What I'm trying to do
I want to create an AI companion that follows the player and helps in combat.
## What I've tried
```rust
// My current code
fn companion_follow(
companion: Query<&mut Transform, With<Companion>>,
player: Query<&Transform, With<Player>>,
) {
// This doesn't work as expected
}
What happens
The companion doesn’t move at all.
What I expected
The companion should move toward the player.
Environment
- AstraWeave 0.1.0
- Windows 11
## Contributing
### Ways to Contribute
Everyone can contribute, regardless of experience level:
| Contribution | Description |
|--------------|-------------|
| **Bug Reports** | Found a bug? Report it on GitHub Issues |
| **Documentation** | Improve or add documentation |
| **Code** | Fix bugs or implement features |
| **Examples** | Create example projects |
| **Testing** | Test pre-release versions |
| **Translations** | Help translate documentation |
| **Community** | Help others in Discord/Discussions |
### Getting Started
1. **Read the contributing guide**: [Contributing](../dev/contributing.md)
2. **Set up your development environment**: [Building](../dev/building.md)
3. **Find a good first issue**:
- Look for `good first issue` labels on GitHub
- Check the `help wanted` label for more challenging tasks
### Code Contribution Workflow
```bash
# 1. Fork and clone
git clone https://github.com/YOUR_USERNAME/astraweave.git
cd astraweave
# 2. Create a branch
git checkout -b feature/my-feature
# 3. Make changes
# ... edit files ...
# 4. Run tests
cargo test --all
# 5. Commit
git commit -am "feat: add my feature"
# 6. Push and create PR
git push origin feature/my-feature
Pull Request Guidelines
- Keep PRs focused: One feature or fix per PR
- Write tests: Cover new functionality with tests
- Update docs: Add documentation for new features
- Follow style: Match existing code style
- Be patient: Reviews take time
Showcase
Share your AstraWeave projects!
How to Submit
-
Discord: Post in #showcase with:
- Project name
- Brief description
- Screenshots or video
- Link (if public)
-
GitHub Discussions: Create a post in the “Show and Tell” category
Featured Projects
Projects using AstraWeave:
| Project | Description |
|---|---|
| hello_companion | Official demo showcasing AI companion systems |
| adaptive-boss | Boss AI that learns and adapts to player strategies |
Have a project to share? Post in #showcase on Discord or open a Discussion on GitHub!
Events
Community Events
- Monthly Showcase: First Friday of each month
- AI Game Jam: Quarterly jam focusing on AI-native games
- Office Hours: Weekly Q&A with maintainers
Conferences
AstraWeave may be presented at:
- GDC (Game Developers Conference)
- RustConf
- Various game development meetups
Governance
Project Leadership
| Role | Responsibility |
|---|---|
| Core Team | Major decisions, releases, roadmap |
| Maintainers | Code review, issue triage |
| Contributors | Feature development, bug fixes |
Decision Making
- Minor changes: Maintainer approval
- Major changes: Core team discussion
- Breaking changes: RFC process with community input
Code of Conduct
We follow a Code of Conduct to ensure a welcoming community:
- Be respectful and inclusive
- Focus on constructive feedback
- Help others learn
- No harassment or discrimination
Full Code of Conduct: CODE_OF_CONDUCT.md
Learning Resources
Official Resources
- Documentation - Complete documentation
- Examples - Working code examples
- API Reference - Generated API docs
Community Resources
- Tutorials: Community-created tutorials (see Discord pinned messages)
- Templates: Starter templates for common project types
- Libraries: Community extensions and plugins
Recommended Learning Path
- Getting Started: Installation
- First Project: Building Your First Game
- Core Concepts: Architecture Overview
- AI Features: AI System
- Advanced Topics: Explore specific system documentation
Staying Updated
Release Notes
- Check GitHub Releases
- Watch the repository for release notifications
Changelog
Each release includes a detailed changelog covering:
- New features
- Bug fixes
- Breaking changes
- Migration guides
Roadmap
See Roadmap for upcoming features and long-term plans.
Sponsorship
Supporting Development
If AstraWeave helps you, consider supporting its development:
- GitHub Sponsors: Support individual maintainers
- Corporate Sponsorship: Contact sponsorship@astraweave.dev
Sponsors
Thank you to our sponsors! Interested in sponsoring AstraWeave? Contact sponsorship@astraweave.dev to learn about sponsorship tiers and benefits.
Contact
| Purpose | Contact |
|---|---|
| General inquiries | hello@astraweave.dev |
| Security issues | security@astraweave.dev |
| Sponsorship | sponsorship@astraweave.dev |
| Press | press@astraweave.dev |
Related Documentation
- Contributing - Contribution guidelines
- Code Style - Coding standards
- Roadmap - Future plans
- FAQ - Frequently asked questions
Roadmap
This document outlines the development roadmap for AstraWeave, including current status, planned features, and long-term vision.
Current Status
Version: 0.1.0 (Alpha)
AstraWeave is currently in active development. The core systems are functional but APIs may change before 1.0.
Stability Matrix
| System | Status | API Stability |
|---|---|---|
| ECS | Stable | High |
| Rendering | Beta | Medium |
| Physics | Stable | High |
| AI/LLM | Beta | Medium |
| Audio | Stable | High |
| Networking | Alpha | Low |
| Editor | Alpha | Low |
Recently Completed Milestones
| Milestone | Date | Tests | Highlights |
|---|---|---|---|
| Blueprint Zone Editor | Mar 2026 | 125+ | Polygon zone editor, Replica/Inspired placement, heightmap injection, 3D overlay |
| Blend Import Pipeline | Feb 2026 | 97 | .blend scene decomposition, BiomePack bridge, texture processing, editor panel |
| Veilweaver Vertical Slice | Feb 2026 | 320 | 5-phase game slice, dialogue, cinematics, boss HUD, determinism validation |
| Miri Memory Safety | Feb 2026 | 977 | 4 crates validated, zero undefined behavior |
| Fluids System (A+) | Jan 2026 | 4,907 | SPH/FLIP simulation, comprehensive test suite |
| AI-Orchestrated Terrain | Dec 2025 | 2,536 | TerrainSolver, NavMesh rebaking, terrain persistence |
| Rendering Pipeline | Dec 2025 | 806+ | PBR/IBL, CSM shadows, headless support, post-processing |
Near-Term (Q1-Q2 2026)
v0.2.0 - Polish Release
Focus: Stability and developer experience.
-
API Stabilization
- Finalize core ECS APIs
- Stabilize component derive macros
- Lock down resource patterns
-
Documentation
- Complete API documentation
- Add more tutorials
- Video walkthroughs
-
Tooling
- Improved editor prototype
- Better debugging tools
- Asset pipeline improvements
-
Performance
- Rendering optimization
- AI tick budgeting improvements
- Memory usage reduction
v0.3.0 - AI Enhancement
Focus: Advanced AI capabilities.
-
LLM Improvements
- Multi-provider support (OpenAI, Anthropic, local)
- Streaming responses
- Function calling standardization
- Context window management
-
Behavior Systems
- Visual behavior tree editor
- GOAP improvements
- Utility AI integration
-
Memory Systems
- Improved episodic memory
- Semantic search with embeddings
- Memory persistence
-
AI Characters
- Personality templates
- Dynamic relationship modeling
- Emotion simulation
Mid-Term (Q3-Q4 2026)
v0.4.0 - Content Creation
Focus: Procedural content and world building.
-
PCG Framework
- Dungeon generation improvements
- Terrain generation enhancements
- AI-assisted content creation
-
World Streaming
- Large world support
- Seamless level streaming
- Background loading
-
Quest System
- Dynamic quest generation
- Branching narratives
- AI-driven story adaptation
v0.5.0 - Editor & Tools
Focus: Complete development environment.
-
Visual Editor
- Scene editing
- Prefab system
- Play-in-editor
-
Asset Pipeline
- Automated texture processing
- Model optimization
- Audio processing
-
Debugging
- In-game console
- Entity inspector
- Performance profiler
-
Scripting
- Rhai scripting integration
- Hot-reload for scripts
- Visual scripting (prototype)
Long-Term (2027+)
v1.0.0 - Production Ready
Focus: Stability for production games.
-
API Freeze
- Stable, documented APIs
- Semantic versioning
- Deprecation policy
-
Platform Support
- Windows, Linux, macOS (native)
- WebAssembly
- Console support (planned)
-
Performance Guarantees
- Documented performance characteristics
- Benchmark suite
- Regression testing
-
Security
- Security audit
- Sandboxed scripting
- Save file validation
Future Vision
Features being considered for post-1.0:
- Console Ports: PlayStation, Xbox, Nintendo Switch support
- VR/AR: Virtual and augmented reality support
- Cloud Gaming: Streaming and cloud save integration
- Multiplayer: Advanced networking and matchmaking
- Marketplace: Asset and plugin marketplace
- AI Cloud: Optional cloud-based AI processing
- Mobile: iOS and Android support
Feature Requests
How to Request Features
- Check existing requests: Search GitHub Issues
- Create a discussion: Use GitHub Discussions for initial feedback
- Submit formal request: Create an issue with the
feature-requestlabel
Prioritization Criteria
Features are prioritized based on:
| Factor | Weight |
|---|---|
| Community interest | High |
| Development effort | Medium |
| Strategic alignment | High |
| Maintainability | Medium |
| Performance impact | Medium |
Most Requested Features
Current top community requests:
- Visual scripting
- More platform support
- VR integration
- Advanced networking
- Asset marketplace
Deprecation Policy
Pre-1.0
During alpha/beta:
- APIs may change with each minor version
- Breaking changes documented in changelog
- Migration guides provided for significant changes
Post-1.0
After 1.0 release:
- Deprecated features marked with
#[deprecated] - 2 minor versions before removal
- Clear migration documentation
Release Schedule
Versioning
We follow Semantic Versioning:
- MAJOR: Breaking changes
- MINOR: New features, backward compatible
- PATCH: Bug fixes
Release Cadence
| Phase | Cadence |
|---|---|
| Alpha | As needed |
| Beta | Monthly |
| Stable | Quarterly |
Support Policy
| Version | Support Duration |
|---|---|
| Current | Full support |
| Previous minor | Security fixes |
| Older | Community only |
Contributing to Roadmap
How to Help
- Implement Features: Check
help wantedissues - Provide Feedback: Test pre-release versions
- Write Documentation: Help document new features
- Create Examples: Build showcase projects
RFC Process
For major changes:
- Create RFC in Discussions
- Gather community feedback
- Core team review
- Implementation (if approved)
Roadmap Updates
This roadmap is updated:
- After each major release
- Quarterly for minor adjustments
- As needed for significant changes
Milestones
Completed Milestones
| Milestone | Date | Highlights |
|---|---|---|
| Initial Release | 2025 Q4 | Core ECS, basic rendering |
| AI Integration | 2025 Q4 | LLM support, behavior trees |
Upcoming Milestones
| Milestone | Target | Goals |
|---|---|---|
| Beta Release | 2026 Q2 | Stable APIs, documentation |
| Editor Preview | 2026 Q3 | Basic visual editor |
| 1.0 Release | 2027 Q1 | Production ready |
Risk Factors
Technical Risks
| Risk | Mitigation |
|---|---|
| LLM API changes | Abstraction layer, multiple providers |
| Performance challenges | Continuous benchmarking, optimization focus |
| Platform compatibility | Early testing on all platforms |
Project Risks
| Risk | Mitigation |
|---|---|
| Scope creep | Clear prioritization, phased releases |
| Maintainer burnout | Community building, shared ownership |
| Funding | Exploring sustainability options |
Community Involvement
The roadmap reflects community input:
- Surveys: Periodic community surveys
- Voting: Feature request voting on GitHub
- Discussions: Open planning discussions
- Contributors: Active contributors help shape direction
Related Documentation
- Contributing - How to contribute
- Community - Join the community
- FAQ - Frequently asked questions
- Best Practices - Current best practices