Skip to content

Commit cdb0f48

Browse files
committed
perf: optimize trigger area avatar scene detection
- Cache avatar scene info in AvatarTriggerInfo struct to avoid per-frame metadata queries - Batch metadata queries across all trigger areas (query each avatar once, not per-area) - Only query metadata when cache indicates potential state change: - If cache says avatar in scene: query to verify they haven't left - If cache says avatar not in scene: query to check if they joined - Skip query entirely when cache matches expected state
1 parent 7a73e4c commit cdb0f48

1 file changed

Lines changed: 178 additions & 120 deletions

File tree

lib/src/scene_runner/components/trigger_area.rs

Lines changed: 178 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,15 @@ fn handle_body_monitor_event(
225225
// TriggerAreaInstance and TriggerAreaState
226226
// ============================================================================
227227

228+
/// Cached info about an avatar inside a trigger area
229+
#[derive(Debug, Clone)]
230+
pub struct AvatarTriggerInfo {
231+
/// Godot instance_id of the avatar's TriggerDetector node
232+
pub instance_id: i64,
233+
/// Last known scene ID for this avatar (cached to avoid per-frame metadata queries)
234+
pub last_known_scene: Option<i32>,
235+
}
236+
228237
/// State for a single trigger area instance
229238
#[derive(Debug)]
230239
pub struct TriggerAreaInstance {
@@ -235,9 +244,8 @@ pub struct TriggerAreaInstance {
235244
/// Set of entities for which we've sent ENTER event (logical state)
236245
/// An entity can be in entities_inside but not entities_entered if it's not active in the scene
237246
pub entities_entered: HashSet<SceneEntityId>,
238-
/// Instance IDs for avatars inside this trigger area (used to query current scene metadata)
239-
/// Maps entity_id -> Godot instance_id of their TriggerDetector node
240-
pub avatar_instance_ids: HashMap<SceneEntityId, i64>,
247+
/// Info for avatars inside this trigger area (used to track scene changes)
248+
pub avatar_info: HashMap<SceneEntityId, AvatarTriggerInfo>,
241249
pub mesh_type: TriggerAreaMeshType,
242250
pub collision_mask: u32,
243251
}
@@ -353,8 +361,7 @@ fn get_avatar_current_scene(instance_id: i64) -> Option<i32> {
353361
}
354362

355363
/// Sync entity states: handle entities becoming active/inactive
356-
/// This generates synthetic ENTER events for entities that entered physically before becoming active
357-
/// and EXIT events for entities that became inactive while inside
364+
/// Uses cached scene info and batches metadata queries for efficiency
358365
fn sync_entity_states(scene: &mut Scene, current_parcel_scene_id: &SceneId) {
359366
// Only parcel scenes need syncing - global scenes always receive events
360367
if !matches!(scene.scene_type, SceneType::Parcel) {
@@ -364,147 +371,183 @@ fn sync_entity_states(scene: &mut Scene, current_parcel_scene_id: &SceneId) {
364371
let tick_number = scene.tick_number;
365372
let scene_pos = scene.godot_dcl_scene.root_node_3d.get_global_position();
366373
let this_scene_id = scene.scene_id.0;
367-
368-
// Check if local player is active in this scene
369374
let player_active = scene.scene_id == *current_parcel_scene_id;
370375

371-
// Collect entities that need state transitions
372-
let mut enter_events = Vec::new();
373-
let mut exit_events = Vec::new();
376+
// Step 1: Collect all unique avatars that need metadata queries (batch to avoid duplicate queries)
377+
let mut avatars_to_query: HashMap<SceneEntityId, i64> = HashMap::new();
378+
379+
for (_trigger_entity, instance) in scene.trigger_areas.instances.iter() {
380+
if (instance.collision_mask & CL_PLAYER) == 0 {
381+
continue;
382+
}
383+
384+
// Avatars inside but not entered - need query if cache says not in this scene
385+
for collider_entity in instance.entities_inside.iter() {
386+
if instance.entities_entered.contains(collider_entity) {
387+
continue;
388+
}
389+
if *collider_entity == SceneEntityId::PLAYER {
390+
continue;
391+
}
392+
if let Some(info) = instance.avatar_info.get(collider_entity) {
393+
if info.last_known_scene != Some(this_scene_id) {
394+
avatars_to_query.insert(*collider_entity, info.instance_id);
395+
}
396+
}
397+
}
398+
399+
// Avatars entered - need query if cache says in this scene (to verify still true)
400+
for collider_entity in instance.entities_entered.iter() {
401+
if *collider_entity == SceneEntityId::PLAYER {
402+
continue;
403+
}
404+
if let Some(info) = instance.avatar_info.get(collider_entity) {
405+
if info.last_known_scene == Some(this_scene_id) {
406+
avatars_to_query.insert(*collider_entity, info.instance_id);
407+
}
408+
}
409+
}
410+
}
411+
412+
// Step 2: Batch query all avatar scenes at once
413+
let queried_scenes: HashMap<SceneEntityId, Option<i32>> = avatars_to_query
414+
.into_iter()
415+
.map(|(entity, instance_id)| (entity, get_avatar_current_scene(instance_id)))
416+
.collect();
417+
418+
// Step 3: Determine state transitions using cached + queried values
419+
struct StateTransition {
420+
trigger_entity: SceneEntityId,
421+
collider_entity: SceneEntityId,
422+
is_enter: bool,
423+
new_scene: Option<i32>,
424+
}
425+
426+
let mut transitions = Vec::new();
374427

375428
for (trigger_entity, instance) in scene.trigger_areas.instances.iter() {
376-
// Only check trigger areas that can detect players/avatars
377429
if (instance.collision_mask & CL_PLAYER) == 0 {
378430
continue;
379431
}
380432

381-
// Check for entities that need synthetic ENTER (physically inside but not entered)
433+
// Check avatars physically inside but not yet "entered"
382434
for collider_entity in instance.entities_inside.iter() {
383435
if instance.entities_entered.contains(collider_entity) {
384-
continue; // Already entered, skip
436+
continue;
385437
}
386438

387-
// Entity is physically inside but hasn't had ENTER sent
388-
let is_now_active = if *collider_entity == SceneEntityId::PLAYER {
389-
// Local player: check current_parcel_scene_id
390-
player_active
391-
} else if let Some(&instance_id) = instance.avatar_instance_ids.get(collider_entity) {
392-
// Remote avatar: query their current scene from metadata
393-
get_avatar_current_scene(instance_id)
394-
.map(|scene| scene == this_scene_id)
395-
.unwrap_or(false)
439+
let Some(info) = instance.avatar_info.get(collider_entity) else {
440+
continue;
441+
};
442+
443+
let (is_now_active, current_scene) = if *collider_entity == SceneEntityId::PLAYER {
444+
(player_active, Some(current_parcel_scene_id.0))
396445
} else {
397-
false
446+
let cached_in_scene = info.last_known_scene == Some(this_scene_id);
447+
if cached_in_scene {
448+
(true, info.last_known_scene)
449+
} else {
450+
// Use batched query result
451+
let current = queried_scenes.get(collider_entity).copied().flatten();
452+
(current == Some(this_scene_id), current)
453+
}
398454
};
399455

400456
if is_now_active {
401-
let trigger_transform = scene
402-
.godot_dcl_scene
403-
.get_node_or_null_3d(trigger_entity)
404-
.map(|n| n.get_global_transform())
405-
.unwrap_or(Transform3D::IDENTITY);
406-
407-
let collider_transform = scene
408-
.godot_dcl_scene
409-
.get_node_or_null_3d(collider_entity)
410-
.map(|n| n.get_global_transform())
411-
.unwrap_or(Transform3D::IDENTITY);
412-
413-
enter_events.push((
414-
*trigger_entity,
415-
*collider_entity,
416-
trigger_transform,
417-
collider_transform,
418-
));
457+
transitions.push(StateTransition {
458+
trigger_entity: *trigger_entity,
459+
collider_entity: *collider_entity,
460+
is_enter: true,
461+
new_scene: current_scene,
462+
});
419463
}
420464
}
421465

422-
// Check for entities that need EXIT (entered but no longer active)
466+
// Check avatars that "entered" but may no longer be active
423467
for collider_entity in instance.entities_entered.iter() {
424-
let is_still_active = if *collider_entity == SceneEntityId::PLAYER {
425-
// Local player: check if they're still in this scene
426-
player_active
427-
} else if let Some(&instance_id) = instance.avatar_instance_ids.get(collider_entity) {
428-
// Remote avatar: query their current scene from metadata
429-
get_avatar_current_scene(instance_id)
430-
.map(|scene| scene == this_scene_id)
431-
.unwrap_or(false)
468+
let Some(info) = instance.avatar_info.get(collider_entity) else {
469+
transitions.push(StateTransition {
470+
trigger_entity: *trigger_entity,
471+
collider_entity: *collider_entity,
472+
is_enter: false,
473+
new_scene: None,
474+
});
475+
continue;
476+
};
477+
478+
let (is_still_active, current_scene) = if *collider_entity == SceneEntityId::PLAYER {
479+
(player_active, Some(current_parcel_scene_id.0))
432480
} else {
433-
// No instance_id tracked - entity might have been removed, treat as inactive
434-
false
481+
let cached_in_scene = info.last_known_scene == Some(this_scene_id);
482+
if cached_in_scene {
483+
// Use batched query result
484+
let current = queried_scenes.get(collider_entity).copied().flatten();
485+
(current == Some(this_scene_id), current)
486+
} else {
487+
(false, info.last_known_scene)
488+
}
435489
};
436490

437491
if !is_still_active {
438-
let trigger_transform = scene
439-
.godot_dcl_scene
440-
.get_node_or_null_3d(trigger_entity)
441-
.map(|n| n.get_global_transform())
442-
.unwrap_or(Transform3D::IDENTITY);
443-
444-
let collider_transform = scene
445-
.godot_dcl_scene
446-
.get_node_or_null_3d(collider_entity)
447-
.map(|n| n.get_global_transform())
448-
.unwrap_or(Transform3D::IDENTITY);
449-
450-
exit_events.push((
451-
*trigger_entity,
452-
*collider_entity,
453-
trigger_transform,
454-
collider_transform,
455-
));
492+
transitions.push(StateTransition {
493+
trigger_entity: *trigger_entity,
494+
collider_entity: *collider_entity,
495+
is_enter: false,
496+
new_scene: current_scene,
497+
});
456498
}
457499
}
458500
}
459501

460-
// Process synthetic ENTER events
461-
for (trigger_entity, collider_entity, trigger_transform, collider_transform) in enter_events {
462-
if let Some(instance) = scene.trigger_areas.instances.get_mut(&trigger_entity) {
463-
instance.entities_entered.insert(collider_entity);
464-
465-
let result = build_trigger_result(
466-
&trigger_entity,
467-
&collider_entity,
468-
TriggerAreaEventType::TaetEnter,
469-
tick_number,
470-
collider_transform,
471-
trigger_transform,
472-
scene_pos,
473-
CL_PLAYER,
474-
);
475-
scene.trigger_area_results.push((trigger_entity, result));
502+
// Step 4: Apply transitions
503+
for t in transitions {
504+
let Some(instance) = scene.trigger_areas.instances.get_mut(&t.trigger_entity) else {
505+
continue;
506+
};
476507

477-
tracing::debug!(
478-
"[TriggerArea] SYNC_ENTER trigger={:?}, collider={:?} (entity became active)",
479-
trigger_entity,
480-
collider_entity
481-
);
508+
// Update cached scene info
509+
if let Some(info) = instance.avatar_info.get_mut(&t.collider_entity) {
510+
info.last_known_scene = t.new_scene;
482511
}
483-
}
484512

485-
// Process EXIT events for entities that became inactive
486-
for (trigger_entity, collider_entity, trigger_transform, collider_transform) in exit_events {
487-
if let Some(instance) = scene.trigger_areas.instances.get_mut(&trigger_entity) {
488-
instance.entities_entered.remove(&collider_entity);
513+
let trigger_transform = scene
514+
.godot_dcl_scene
515+
.get_node_or_null_3d(&t.trigger_entity)
516+
.map(|n| n.get_global_transform())
517+
.unwrap_or(Transform3D::IDENTITY);
489518

490-
let result = build_trigger_result(
491-
&trigger_entity,
492-
&collider_entity,
493-
TriggerAreaEventType::TaetExit,
494-
tick_number,
495-
collider_transform,
496-
trigger_transform,
497-
scene_pos,
498-
CL_PLAYER,
499-
);
500-
scene.trigger_area_results.push((trigger_entity, result));
519+
let collider_transform = scene
520+
.godot_dcl_scene
521+
.get_node_or_null_3d(&t.collider_entity)
522+
.map(|n| n.get_global_transform())
523+
.unwrap_or(Transform3D::IDENTITY);
501524

502-
tracing::debug!(
503-
"[TriggerArea] SYNC_EXIT trigger={:?}, collider={:?} (entity became inactive)",
504-
trigger_entity,
505-
collider_entity
506-
);
507-
}
525+
let event_type = if t.is_enter {
526+
instance.entities_entered.insert(t.collider_entity);
527+
TriggerAreaEventType::TaetEnter
528+
} else {
529+
instance.entities_entered.remove(&t.collider_entity);
530+
TriggerAreaEventType::TaetExit
531+
};
532+
533+
let result = build_trigger_result(
534+
&t.trigger_entity,
535+
&t.collider_entity,
536+
event_type,
537+
tick_number,
538+
collider_transform,
539+
trigger_transform,
540+
scene_pos,
541+
CL_PLAYER,
542+
);
543+
scene.trigger_area_results.push((t.trigger_entity, result));
544+
545+
tracing::debug!(
546+
"[TriggerArea] SYNC_{} trigger={:?}, collider={:?}",
547+
if t.is_enter { "ENTER" } else { "EXIT" },
548+
t.trigger_entity,
549+
t.collider_entity
550+
);
508551
}
509552
}
510553

@@ -602,17 +645,32 @@ fn process_callback_events(scene: &mut Scene, current_parcel_scene_id: &SceneId)
602645
// Always update physical state (entities_inside)
603646
if event.is_enter {
604647
instance.entities_inside.insert(event.collider_entity);
605-
// Store instance_id for avatars so we can query their current scene later
648+
// Store avatar info with their current scene (already computed in first pass)
606649
if is_avatar {
607-
instance
608-
.avatar_instance_ids
609-
.insert(event.collider_entity, event.instance_id);
650+
instance.avatar_info.insert(
651+
event.collider_entity,
652+
AvatarTriggerInfo {
653+
instance_id: event.instance_id,
654+
last_known_scene: if event.collider_entity == SceneEntityId::PLAYER {
655+
// For local player, use current_parcel_scene_id
656+
Some(current_parcel_scene_id.0)
657+
} else {
658+
// For remote avatars, we got this from metadata in first pass
659+
// Reconstruct from entity_active check
660+
if event.entity_active {
661+
Some(this_scene_id.0)
662+
} else {
663+
None // Not in this scene
664+
}
665+
},
666+
},
667+
);
610668
}
611669
} else {
612670
instance.entities_inside.remove(&event.collider_entity);
613-
// Remove instance_id tracking for avatars
671+
// Remove avatar info tracking
614672
if is_avatar {
615-
instance.avatar_instance_ids.remove(&event.collider_entity);
673+
instance.avatar_info.remove(&event.collider_entity);
616674
}
617675
// If entity exits physically, also remove from entered state
618676
if instance.entities_entered.remove(&event.collider_entity) {
@@ -832,7 +890,7 @@ fn create_or_update_trigger_area(
832890
shape_rid,
833891
entities_inside: HashSet::new(),
834892
entities_entered: HashSet::new(),
835-
avatar_instance_ids: HashMap::new(),
893+
avatar_info: HashMap::new(),
836894
mesh_type,
837895
collision_mask,
838896
},

0 commit comments

Comments
 (0)