Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

AspectTraditional ApproachAstraWeave AI-Native
Decision MakingScripted state machinesLLM-based planning
World KnowledgeOmniscient accessPerception-limited
Action ExecutionDirect world manipulationTool-validated actions
Behavior AdaptationManual script updatesAutomatic learning
MultiplayerSeparate AI/player codeUnified validation
DebuggingComplex state inspectionNatural language reasoning
PerformancePredictable overheadVariable AI complexity
EmergenceLimited by scriptsUnbounded 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.