From feeccc2ff3c2317e56b6a87afc2a448b856dfec2 Mon Sep 17 00:00:00 2001 From: Joshua Barretto Date: Sat, 25 Jan 2020 18:49:47 +0000 Subject: [PATCH] Improved patrol idling --- common/src/comp/agent.rs | 33 ++++++- common/src/comp/mod.rs | 2 +- common/src/path.rs | 28 +++++- common/src/sys/agent.rs | 180 +++++++++++++++++++++++++++++++++++++-- server/src/cmd.rs | 12 +-- server/src/test_world.rs | 2 +- 6 files changed, 234 insertions(+), 23 deletions(-) diff --git a/common/src/comp/agent.rs b/common/src/comp/agent.rs index 940de56b0f..156f6105ab 100644 --- a/common/src/comp/agent.rs +++ b/common/src/comp/agent.rs @@ -25,11 +25,9 @@ impl Component for Alignment { #[derive(Clone, Debug, Default)] pub struct Agent { - pub chaser: Chaser, - pub target: Option<(EcsEntity, f64)>, pub owner: Option, pub patrol_origin: Option>, - pub wander_pos: Option>, + pub activity: Activity, } impl Agent { @@ -47,3 +45,32 @@ impl Agent { impl Component for Agent { type Storage = IDVStorage; } + +#[derive(Clone, Debug)] +pub enum Activity { + Idle(Option>, Chaser), + Follow(EcsEntity, Chaser), + Attack(EcsEntity, Chaser, f64), +} + +impl Activity { + pub fn is_follow(&self) -> bool { + match self { + Activity::Follow(_, _) => true, + _ => false, + } + } + + pub fn is_attack(&self) -> bool { + match self { + Activity::Attack(_, _, _) => true, + _ => false, + } + } +} + +impl Default for Activity { + fn default() -> Self { + Activity::Idle(None, Chaser::default()) + } +} diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index dc67dbf03f..16d9bf7a72 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -1,5 +1,5 @@ mod admin; -mod agent; +pub mod agent; mod body; mod character_state; mod controller; diff --git a/common/src/path.rs b/common/src/path.rs index c02fd840a5..f75c8c77d4 100644 --- a/common/src/path.rs +++ b/common/src/path.rs @@ -171,7 +171,7 @@ where }; let get_walkable_z = |pos| { let mut z_incr = 0; - for i in 0..32 { + for _ in 0..32 { let test_pos = pos + Vec3::unit_z() * z_incr; if is_walkable(&test_pos) { return Some(test_pos); @@ -192,7 +192,7 @@ where let heuristic = |pos: &Vec3| (pos.distance_squared(end) as f32).sqrt(); let neighbors = |pos: &Vec3| { let pos = *pos; - const dirs: [Vec3; 17] = [ + const DIRS: [Vec3; 17] = [ Vec3::new(0, 1, 0), // Forward Vec3::new(0, 1, 1), // Forward upward Vec3::new(0, 1, 2), // Forward Upwardx2 @@ -212,9 +212,31 @@ where Vec3::new(0, 0, -1), // Downwards ]; - dirs.iter() + let walkable = [ + is_walkable(&(pos + Vec3::new(1, 0, 0))), + is_walkable(&(pos + Vec3::new(-1, 0, 0))), + is_walkable(&(pos + Vec3::new(0, 1, 0))), + is_walkable(&(pos + Vec3::new(0, -1, 0))), + ]; + + const DIAGONALS: [(Vec3, [usize; 2]); 4] = [ + (Vec3::new(1, 1, 0), [0, 2]), + (Vec3::new(-1, 1, 0), [1, 2]), + (Vec3::new(1, -1, 0), [0, 3]), + (Vec3::new(-1, -1, 0), [1, 3]), + ]; + + DIRS.iter() .map(move |dir| pos + dir) .filter(move |pos| is_walkable(pos)) + .chain( + DIAGONALS + .iter() + .filter(move |(dir, [a, b])| { + is_walkable(&(pos + *dir)) && walkable[*a] && walkable[*b] + }) + .map(move |(dir, _)| pos + *dir), + ) }; let transition = |_: &Vec3, _: &Vec3| 1.0; let satisfied = |pos: &Vec3| pos == &end; diff --git a/common/src/sys/agent.rs b/common/src/sys/agent.rs index 095abdf6dd..3d58f5fa9e 100644 --- a/common/src/sys/agent.rs +++ b/common/src/sys/agent.rs @@ -1,6 +1,7 @@ use crate::terrain::TerrainGrid; use crate::{ - comp::{self, Agent, Alignment, CharacterState, Controller, MountState, Pos, Stats}, + comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Pos, Stats}, + path::Chaser, state::Time, sync::UidAllocator, }; @@ -20,7 +21,6 @@ impl<'a> System<'a> for Sys { Entities<'a>, ReadStorage<'a, Pos>, ReadStorage<'a, Stats>, - ReadStorage<'a, CharacterState>, ReadExpect<'a, TerrainGrid>, ReadStorage<'a, Alignment>, WriteStorage<'a, Agent>, @@ -36,7 +36,6 @@ impl<'a> System<'a> for Sys { entities, positions, stats, - character_states, terrain, alignments, mut agents, @@ -72,7 +71,165 @@ impl<'a> System<'a> for Sys { let mut inputs = &mut controller.inputs; - const PET_DIST: f32 = 12.0; + const AVG_FOLLOW_DIST: f32 = 6.0; + const MAX_FOLLOW_DIST: f32 = 12.0; + const MAX_CHASE_DIST: f32 = 24.0; + const SIGHT_DIST: f32 = 20.0; + const MIN_ATTACK_DIST: f32 = 3.25; + const PATROL_DIST: f32 = 32.0; + + let mut do_idle = false; + + match &mut agent.activity { + Activity::Idle(wander_pos, chaser) => { + if let Some(patrol_origin) = agent.patrol_origin { + if thread_rng().gen::() < 0.002 { + *wander_pos = + if thread_rng().gen::() < 0.5 { + Some(patrol_origin.map(|e| { + e + thread_rng().gen_range(-1.0, 1.0) * PATROL_DIST + })) + } else { + None + }; + } + + if let Some(wander_pos) = wander_pos { + if let Some(bearing) = chaser.chase(&*terrain, pos.0, *wander_pos, 2.0) + { + inputs.move_dir = + Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); + inputs.jump.set_state(bearing.z > 1.0); + } else { + *wander_pos = None; + } + } + } + + // Sometimes try searching for new targets + if thread_rng().gen::() < 0.025 { + // Search for new targets + let entities = (&entities, &positions, &stats, alignments.maybe()) + .join() + .filter(|(e, e_pos, e_stats, e_alignment)| { + (e_pos.0 - pos.0).magnitude() < SIGHT_DIST + && *e != entity + && !e_stats.is_dead + && alignment + .and_then(|a| e_alignment.map(|b| a.hostile_towards(*b))) + .unwrap_or(false) + }) + .map(|(e, _, _, _)| e) + .collect::>(); + + if let Some(target) = (&entities).choose(&mut thread_rng()).cloned() { + agent.activity = Activity::Attack(target, Chaser::default(), time.0); + } + } + } + Activity::Follow(target, chaser) => { + if let (Some(tgt_pos), _tgt_stats) = + (positions.get(*target), stats.get(*target)) + { + let dist = pos.0.distance(tgt_pos.0); + // Follow, or return to idle + if dist > AVG_FOLLOW_DIST { + if let Some(bearing) = + chaser.chase(&*terrain, pos.0, tgt_pos.0, AVG_FOLLOW_DIST) + { + inputs.move_dir = + Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); + inputs.jump.set_state(bearing.z > 1.0); + } + } else { + do_idle = true; + } + } else { + do_idle = true; + } + } + Activity::Attack(target, chaser, _) => { + if let (Some(tgt_pos), _tgt_stats) = + (positions.get(*target), stats.get(*target)) + { + let dist = pos.0.distance(tgt_pos.0); + if dist < MIN_ATTACK_DIST { + // Close-range attack + inputs.look_dir = tgt_pos.0 - pos.0; + inputs.move_dir = Vec2::from(tgt_pos.0 - pos.0) + .try_normalized() + .unwrap_or(Vec2::zero()) + * 0.01; + inputs.primary.set_state(true); + } else if dist < MAX_CHASE_DIST { + // Long-range chase + if let Some(bearing) = chaser.chase(&*terrain, pos.0, tgt_pos.0, 1.25) { + inputs.move_dir = + Vec2::from(bearing).try_normalized().unwrap_or(Vec2::zero()); + inputs.jump.set_state(bearing.z > 1.0); + } + } else { + do_idle = true; + } + } else { + do_idle = true; + } + } + } + + if do_idle { + agent.activity = Activity::Idle(None, Chaser::default()); + } + + // --- Activity overrides (in reverse order of priority: most important goes last!) --- + + // Attack a target that's attacking us + if let Some(stats) = stats.get(entity) { + // Only if the attack was recent + if stats.health.last_change.0 < 5.0 { + if let comp::HealthSource::Attack { by } = stats.health.last_change.1.cause { + if !agent.activity.is_attack() { + if let Some(attacker) = uid_allocator.retrieve_entity_internal(by.id()) + { + agent.activity = + Activity::Attack(attacker, Chaser::default(), time.0); + } + } + } + } + } + + // Follow owner if we're too far, or if they're under attack + if let Some(owner) = agent.owner { + if let Some(owner_pos) = positions.get(owner) { + let dist = pos.0.distance(owner_pos.0); + if dist > MAX_FOLLOW_DIST && !agent.activity.is_follow() { + agent.activity = Activity::Follow(owner, Chaser::default()); + } + + // Attack owner's attacker + if let Some(owner_stats) = stats.get(owner) { + if owner_stats.health.last_change.0 < 5.0 { + if let comp::HealthSource::Attack { by } = + owner_stats.health.last_change.1.cause + { + if !agent.activity.is_attack() { + if let Some(attacker) = + uid_allocator.retrieve_entity_internal(by.id()) + { + agent.activity = + Activity::Attack(attacker, Chaser::default(), time.0); + } + } + } + } + } + } + } + + /* + const PET_DIST: f32 = 6.0; + const MAX_PET_DIST: f32 = 16.0; const PATROL_DIST: f32 = 32.0; const SIGHT_DIST: f32 = 24.0; const MIN_ATTACK_DIST: f32 = 3.25; @@ -99,12 +256,15 @@ impl<'a> System<'a> for Sys { } else { choose_target = true; } + } else { + choose_target = thread_rng().gen::() < 0.05; } // Return to owner if let Some(owner) = agent.owner { if let Some(tgt_pos) = positions.get(owner) { - if pos.0.distance(tgt_pos.0) > PET_DIST { + let dist = pos.0.distance(tgt_pos.0); + if dist > MAX_PET_DIST || (dist > PET_DIST && agent.target.is_none()) { // Follow owner chase_tgt = Some((tgt_pos.0, 6.0, false)); } else if agent.target.is_none() { @@ -118,8 +278,6 @@ impl<'a> System<'a> for Sys { // Return to patrol origin chase_tgt = Some((patrol_origin, 64.0, false)); } - } else { - choose_target = thread_rng().gen::() < 0.05; } // Attack a target that's attacking us @@ -156,8 +314,9 @@ impl<'a> System<'a> for Sys { } // Update target when attack begins - if let Some(tgt) = new_target { - agent.target = Some((tgt, time.0)); + match agent.target { + Some((tgt, time)) if Some(tgt) == new_target => {}, + _ => agent.target = new_target.map(|tgt| (tgt, time.0)) } // Chase target @@ -193,6 +352,7 @@ impl<'a> System<'a> for Sys { } // Choose new wander position + /* if agent.wander_pos.is_none() || thread_rng().gen::() < 0.005 { agent.wander_pos = if thread_rng().gen::() < 0.5 { let max_dist = if agent.owner.is_some() { @@ -210,7 +370,9 @@ impl<'a> System<'a> for Sys { None }; } + */ } + */ debug_assert!(inputs.move_dir.map(|e| !e.is_nan()).reduce_and()); debug_assert!(inputs.look_dir.map(|e| !e.is_nan()).reduce_and()); diff --git a/server/src/cmd.rs b/server/src/cmd.rs index 495b674ae4..3f6a01d88e 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -465,14 +465,14 @@ fn handle_spawn(server: &mut Server, entity: EcsEntity, args: String, action: &C .unwrap_or(1) .min(10); - let agent = if let comp::Alignment::Npc = alignment { - comp::Agent::default().with_pet(entity) - } else { - comp::Agent::default() - }; - match server.state.read_component_cloned::(entity) { Some(pos) => { + let agent = if let comp::Alignment::Npc = alignment { + comp::Agent::default().with_pet(entity) + } else { + comp::Agent::default().with_patrol_origin(pos.0) + }; + for _ in 0..amount { let vel = Vec3::new( rand::thread_rng().gen_range(-2.0, 3.0), diff --git a/server/src/test_world.rs b/server/src/test_world.rs index 8e562f80ae..1234d4fe38 100644 --- a/server/src/test_world.rs +++ b/server/src/test_world.rs @@ -32,7 +32,7 @@ impl World { let mut supplement = ChunkSupplement::default(); - if chunk_pos.map(|e| e % 3 == 0).reduce_and() { + if chunk_pos.map(|e| e % 8 == 0).reduce_and() { supplement = supplement.with_entity(EntityInfo { pos: Vec3::::from(chunk_pos.map(|e| e as f32 * 32.0)) + Vec3::unit_z() * 256.0, kind: EntityKind::Waypoint,