Skip to content

Commit 94b0dd4

Browse files
stainluclaude
andcommitted
Wire post-process passes, audio, animation, and AI behavior trees
Four previously unconnected subsystems now execute at runtime in the MOBA demo: 1. Post-process: DoF and Motion Blur passes instantiated on Renderer, executed between TAA and the PostProcessStack. TAA enabled in dota_client. High quality preset already enables motion blur + DoF. 2. Audio: AudioEngine (Kira 0.12) initialized with bus settings. audio_update_system_mut() called each frame for spatial positioning, bus volume sync, and playback lifecycle management. 3. Animation: euca-animation added to euca-game. AnimationLibrary and AnimationEventLibrary resources inserted. animation_evaluate_system() called each frame for state machine transitions, pose blending, montage playback, and IK solving. 4. AI Behavior Trees: euca-ai added to euca-gameplay. New bt_perception system populates blackboards with self_position, nearest_enemy, enemy_distance. behavior_tree_system ticks all BT executors. bt_moveto_system bridges MoveTo actions to ECS velocity. Runs alongside existing ai_system (both coexist — AiGoal vs BT entities). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff3dbe3 commit 94b0dd4

8 files changed

Lines changed: 254 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/euca-game/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,10 @@ euca-net = { path = "../euca-net" }
2020
euca-input = { path = "../euca-input" }
2121
euca-core = { path = "../euca-core" }
2222
euca-gameplay = { path = "../euca-gameplay" }
23+
euca-ai = { path = "../euca-ai" }
2324
euca-agent = { path = "../euca-agent" }
2425
euca-audio = { path = "../euca-audio" }
26+
euca-animation = { path = "../euca-animation" }
2527
euca-nav = { path = "../euca-nav" }
2628
euca-asset = { path = "../euca-asset" }
2729
euca-terrain = { path = "../euca-terrain", features = ["level-render"] }

crates/euca-gameplay/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ euca-math = { path = "../euca-math" }
1414
euca-physics = { path = "../euca-physics" }
1515
euca-render = { path = "../euca-render" }
1616
euca-scene = { path = "../euca-scene" }
17+
euca-ai = { path = "../euca-ai" }
1718
serde = { version = "1", features = ["derive"] }
1819
serde_json = "1"
1920
bincode = "1"
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//! Behavior tree action handlers — bridge BT intent to ECS mutations.
2+
//!
3+
//! The [`euca_ai::BtAction::MoveTo`] action signals intent without moving the
4+
//! entity. This system reads the blackboard target and applies velocity,
5+
//! mirroring the logic in [`crate::ai::ai_system`].
6+
7+
use euca_ai::BehaviorTreeExecutor;
8+
use euca_ecs::{Entity, Query, World};
9+
use euca_math::{Quat, Vec3};
10+
use euca_physics::Velocity;
11+
use euca_scene::LocalTransform;
12+
13+
/// Default movement speed for BT-controlled entities.
14+
const BT_MOVE_SPEED: f32 = 4.0;
15+
16+
/// Apply movement for entities whose behavior tree has a `MoveTo` action running.
17+
///
18+
/// Reads `"enemy_position"` or `"patrol_target"` from the blackboard (whichever
19+
/// the active MoveTo references) and steers the entity toward it.
20+
pub fn bt_moveto_system(world: &mut World) {
21+
let updates: Vec<(Entity, Vec3)> = {
22+
let query = Query::<(Entity, &BehaviorTreeExecutor, &LocalTransform)>::new(world);
23+
query
24+
.iter()
25+
.filter_map(|(e, exec, lt)| {
26+
// Check common target keys in priority order.
27+
let target_pos = exec
28+
.blackboard
29+
.get_vec3("enemy_position")
30+
.or_else(|| exec.blackboard.get_vec3("patrol_target"))?;
31+
let self_pos = lt.0.translation;
32+
let delta = target_pos - self_pos;
33+
let horizontal = Vec3::new(delta.x, 0.0, delta.z);
34+
if horizontal.length() < 0.3 {
35+
return None; // Close enough
36+
}
37+
Some((e, horizontal.normalize()))
38+
})
39+
.collect()
40+
};
41+
42+
for (entity, dir) in updates {
43+
if let Some(vel) = world.get_mut::<Velocity>(entity) {
44+
vel.linear.x = dir.x * BT_MOVE_SPEED;
45+
vel.linear.z = dir.z * BT_MOVE_SPEED;
46+
}
47+
if let Some(lt) = world.get_mut::<LocalTransform>(entity) {
48+
let target_angle = dir.x.atan2(dir.z);
49+
lt.0.rotation = Quat::from_axis_angle(Vec3::Y, target_angle);
50+
}
51+
}
52+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Perception system for behavior tree AI entities.
2+
//!
3+
//! Runs before [`euca_ai::behavior_tree_system`] to populate each entity's
4+
//! blackboard with sensory data: own position, nearest enemy, distance, etc.
5+
6+
use euca_ai::{BehaviorTreeExecutor, BlackboardValue};
7+
use euca_ecs::{Entity, Query, World};
8+
use euca_math::Vec3;
9+
use euca_scene::GlobalTransform;
10+
11+
use crate::health::{Dead, Health};
12+
use crate::teams::Team;
13+
14+
/// Populate behavior tree blackboards with perception data.
15+
///
16+
/// For each entity with a [`BehaviorTreeExecutor`], writes:
17+
/// - `"self_position"` — own world position (Vec3)
18+
/// - `"alive"` — whether the entity is alive (Bool)
19+
/// - `"nearest_enemy"` — entity ID of closest living enemy (Entity)
20+
/// - `"enemy_position"` — world position of nearest enemy (Vec3)
21+
/// - `"enemy_distance"` — distance to nearest enemy (Float)
22+
pub fn bt_perception_system(world: &mut World) {
23+
// Collect all living combatants for enemy scanning.
24+
let combatants: Vec<(Entity, Vec3, u8)> = {
25+
let query = Query::<(Entity, &GlobalTransform, &Team)>::new(world);
26+
query
27+
.iter()
28+
.filter(|(e, _, _)| world.get::<Dead>(*e).is_none())
29+
.filter(|(e, _, _)| world.get::<Health>(*e).is_some())
30+
.map(|(e, gt, t)| (e, gt.0.translation, t.0))
31+
.collect()
32+
};
33+
34+
// Collect BT entities to update.
35+
let bt_entities: Vec<Entity> = {
36+
let query = Query::<(Entity, &BehaviorTreeExecutor)>::new(world);
37+
query.iter().map(|(e, _)| e).collect()
38+
};
39+
40+
for entity in bt_entities {
41+
let (self_pos, self_team, is_alive) = {
42+
let pos = world
43+
.get::<GlobalTransform>(entity)
44+
.map(|gt| gt.0.translation)
45+
.unwrap_or(Vec3::ZERO);
46+
let team = world.get::<Team>(entity).map(|t| t.0).unwrap_or(0);
47+
let alive = world.get::<Dead>(entity).is_none();
48+
(pos, team, alive)
49+
};
50+
51+
let executor = match world.get_mut::<BehaviorTreeExecutor>(entity) {
52+
Some(exec) => exec,
53+
None => continue,
54+
};
55+
56+
let bb = &mut executor.blackboard;
57+
bb.set("self_position", BlackboardValue::Vec3(self_pos));
58+
bb.set("alive", BlackboardValue::Bool(is_alive));
59+
60+
// Find nearest living enemy.
61+
let mut nearest_dist = f32::MAX;
62+
let mut nearest_entity = None;
63+
let mut nearest_pos = Vec3::ZERO;
64+
65+
for &(other, other_pos, other_team) in &combatants {
66+
if other == entity || other_team == self_team {
67+
continue;
68+
}
69+
let dist = (other_pos - self_pos).length();
70+
if dist < nearest_dist {
71+
nearest_dist = dist;
72+
nearest_entity = Some(other);
73+
nearest_pos = other_pos;
74+
}
75+
}
76+
77+
if let Some(enemy) = nearest_entity {
78+
bb.set("nearest_enemy", BlackboardValue::Entity(enemy));
79+
bb.set("enemy_position", BlackboardValue::Vec3(nearest_pos));
80+
bb.set("enemy_distance", BlackboardValue::Float(nearest_dist));
81+
} else {
82+
bb.remove("nearest_enemy");
83+
bb.remove("enemy_position");
84+
bb.remove("enemy_distance");
85+
}
86+
}
87+
}

crates/euca-gameplay/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ pub mod ai;
1818
pub mod assertions;
1919
/// Dota 2 hero attribute system — STR/AGI/INT with per-level growth and stat conversions.
2020
pub mod attributes;
21+
/// Behavior tree action handlers (MoveTo → velocity).
22+
pub mod bt_actions;
23+
/// Perception system for behavior tree AI entities.
24+
pub mod bt_perception;
2125
/// Dota 2 tower and building system — types, backdoor, fortification, aggro, bounties.
2226
pub mod building;
2327
/// ECS systems for buildings — backdoor protection, fortification, barracks death.
@@ -91,6 +95,8 @@ pub use assertions::{
9195
AssertCondition, AssertResult, Assertion, CompareOp, EntityFilter, EvaluationReport, Severity,
9296
evaluate_assertions, parse_entity_filter,
9397
};
98+
pub use bt_actions::bt_moveto_system;
99+
pub use bt_perception::bt_perception_system;
94100
pub use combat::{
95101
AttackStyle, AutoCombat, CurrentTarget, EntityRole, MarchDirection, Projectile,
96102
auto_combat_system, projectile_system,

crates/euca-render/src/renderer.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,10 @@ pub struct Renderer<D: euca_rhi::RenderDevice = euca_rhi::wgpu_backend::WgpuDevi
372372
prev_depth_dims: (u32, u32),
373373
/// TAA resolve pass (temporal anti-aliasing).
374374
taa_pass: crate::taa::TaaPass<D>,
375+
/// Depth-of-field pass (CoC computation + gather blur).
376+
dof_pass: crate::dof::DofPass<D>,
377+
/// Motion blur pass (tile max velocity + directional blur).
378+
motion_blur_pass: crate::motion_blur::MotionBlurPass<D>,
375379
/// Frame counter for TAA jitter sequence.
376380
frame_count: u32,
377381
/// Interpolated SH probe coefficients for indirect lighting (set by caller).
@@ -1107,6 +1111,8 @@ impl<D: RenderDevice> Renderer<D> {
11071111
prev_depth_buffer: Vec::new(),
11081112
prev_depth_dims: (0, 0),
11091113
taa_pass: crate::taa::TaaPass::new(rhi, surface_w, surface_h),
1114+
dof_pass: crate::dof::DofPass::new(rhi, surface_w, surface_h),
1115+
motion_blur_pass: crate::motion_blur::MotionBlurPass::new(rhi, surface_w, surface_h),
11101116
frame_count: 0,
11111117
probe_sh: [[0.0; 4]; 9],
11121118
probe_enabled: false,
@@ -1458,6 +1464,8 @@ impl<D: RenderDevice> Renderer<D> {
14581464
fog_pass.resize(&**gpu, fw, fh);
14591465
}
14601466
self.taa_pass.resize(rhi, w, h);
1467+
self.dof_pass.resize(rhi, w, h);
1468+
self.motion_blur_pass.resize(rhi, w, h);
14611469
self.velocity_textures.resize(rhi, w, h);
14621470
}
14631471

@@ -2458,6 +2466,70 @@ impl<D: RenderDevice> Renderer<D> {
24582466
}
24592467
}
24602468

2469+
// Motion blur: per-pixel directional blur from velocity buffer.
2470+
if self.post_process_settings.motion_blur.enabled {
2471+
self.motion_blur_pass.execute(
2472+
rhi,
2473+
encoder,
2474+
self.post_process_stack.ping_view(),
2475+
&self.velocity_textures.velocity_view,
2476+
&self.post_process_settings.motion_blur,
2477+
);
2478+
let (copy_w, copy_h) = rhi.surface_size();
2479+
rhi.copy_texture_to_texture(
2480+
encoder,
2481+
&euca_rhi::TexelCopyTextureInfo {
2482+
texture: self.motion_blur_pass.output_texture(),
2483+
mip_level: 0,
2484+
origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 },
2485+
aspect: euca_rhi::TextureAspect::All,
2486+
},
2487+
&euca_rhi::TexelCopyTextureInfo {
2488+
texture: self.post_process_stack.ping_texture(),
2489+
mip_level: 0,
2490+
origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 },
2491+
aspect: euca_rhi::TextureAspect::All,
2492+
},
2493+
euca_rhi::Extent3d {
2494+
width: copy_w,
2495+
height: copy_h,
2496+
depth_or_array_layers: 1,
2497+
},
2498+
);
2499+
}
2500+
2501+
// Depth of field: CoC computation + variable-radius gather blur.
2502+
if self.post_process_settings.dof.enabled {
2503+
self.dof_pass.execute(
2504+
rhi,
2505+
encoder,
2506+
self.post_process_stack.ping_view(),
2507+
&self.post_process_stack.depth_resolve_view,
2508+
&self.post_process_settings.dof,
2509+
);
2510+
let (copy_w, copy_h) = rhi.surface_size();
2511+
rhi.copy_texture_to_texture(
2512+
encoder,
2513+
&euca_rhi::TexelCopyTextureInfo {
2514+
texture: self.dof_pass.output_texture(),
2515+
mip_level: 0,
2516+
origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 },
2517+
aspect: euca_rhi::TextureAspect::All,
2518+
},
2519+
&euca_rhi::TexelCopyTextureInfo {
2520+
texture: self.post_process_stack.ping_texture(),
2521+
mip_level: 0,
2522+
origin: euca_rhi::Origin3d { x: 0, y: 0, z: 0 },
2523+
aspect: euca_rhi::TextureAspect::All,
2524+
},
2525+
euca_rhi::Extent3d {
2526+
width: copy_w,
2527+
height: copy_h,
2528+
depth_or_array_layers: 1,
2529+
},
2530+
);
2531+
}
2532+
24612533
self.frame_count = self.frame_count.wrapping_add(1);
24622534

24632535
// Post-processing via the modular stack.

examples/dota_client.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1155,10 +1155,31 @@ impl DotaClientApp {
11551155
let mut pps = euca_render::RenderQuality::High.to_settings();
11561156
pps.ssao_enabled = false;
11571157
pps.ssr_enabled = true;
1158+
pps.taa_enabled = true;
11581159
// Focal distance matches the MOBA camera height (~40 world units).
11591160
pps.dof.focus_distance = 40.0;
11601161
world.insert_resource(pps);
11611162

1163+
// Audio engine: spatial audio, bus mixing, reverb zones.
1164+
match euca_audio::AudioEngine::new() {
1165+
Ok(engine) => {
1166+
world.insert_resource(engine);
1167+
world.insert_resource(euca_audio::AudioBusSettings::default());
1168+
world.insert_resource(euca_audio::AudioSettings::default());
1169+
log::info!("Audio engine initialized");
1170+
}
1171+
Err(e) => {
1172+
log::warn!("Audio engine failed to initialize: {e}");
1173+
}
1174+
}
1175+
1176+
// Animation: state machines, blending, montages, IK, root motion.
1177+
world.insert_resource(euca_asset::AnimationLibrary {
1178+
clips: Vec::new(),
1179+
skeletons: Vec::new(),
1180+
});
1181+
world.insert_resource(euca_animation::AnimationEventLibrary::new());
1182+
11621183
// Register items and heroes
11631184
world.insert_resource(define_items());
11641185
world.insert_resource(define_heroes());
@@ -1572,6 +1593,10 @@ impl DotaClientApp {
15721593
euca_gameplay::projectile_system(&mut self.world, dt);
15731594
euca_gameplay::trigger_system(&mut self.world);
15741595
euca_gameplay::ai_system(&mut self.world, dt);
1596+
// Behavior tree AI: perception → tick → action execution.
1597+
euca_gameplay::bt_perception_system(&mut self.world);
1598+
euca_ai::behavior_tree_system(&mut self.world, dt);
1599+
euca_gameplay::bt_moveto_system(&mut self.world);
15751600
euca_gameplay::tower_aggro_system(&mut self.world);
15761601
euca_gameplay::auto_combat_system(&mut self.world, dt);
15771602
euca_gameplay::neutral_camp_system(&mut self.world, dt);
@@ -1715,6 +1740,12 @@ impl DotaClientApp {
17151740
euca_agent::routes::drain_pending_mesh_uploads(&mut self.world, renderer, gpu);
17161741
}
17171742

1743+
// Animation: evaluate state machines, blend poses, sample clips.
1744+
euca_animation::animation_evaluate_system(&mut self.world, dt);
1745+
1746+
// Audio: update spatial positioning, bus volumes, and playback state.
1747+
euca_audio::audio_update_system_mut(&mut self.world, dt);
1748+
17181749
// ── Render ──────────────────────────────────────────────────────
17191750

17201751
let gpu = self.gpu.as_ref().unwrap();

0 commit comments

Comments
 (0)