mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'holychowders/agent_awareness2' into 'master'
Rework internal representation of agent awareness See merge request veloren/veloren!3661
This commit is contained in:
commit
1173c8257f
@ -18,7 +18,6 @@ use super::{dialogue::Subject, Pos};
|
|||||||
|
|
||||||
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
|
pub const DEFAULT_INTERACTION_TIME: f32 = 3.0;
|
||||||
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
pub const TRADE_INTERACTION_TIME: f32 = 300.0;
|
||||||
const AWARENESS_DECREMENT_CONSTANT: f32 = 2.1;
|
|
||||||
const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0;
|
const SECONDS_BEFORE_FORGET_SOUNDS: f64 = 180.0;
|
||||||
|
|
||||||
//intentionally very few concurrent action state variables are allowed. This is
|
//intentionally very few concurrent action state variables are allowed. This is
|
||||||
@ -529,12 +528,77 @@ pub struct Agent {
|
|||||||
pub timer: Timer,
|
pub timer: Timer,
|
||||||
pub bearing: Vec2<f32>,
|
pub bearing: Vec2<f32>,
|
||||||
pub sounds_heard: Vec<Sound>,
|
pub sounds_heard: Vec<Sound>,
|
||||||
pub awareness: f32,
|
|
||||||
pub position_pid_controller: Option<PidController<fn(Vec3<f32>, Vec3<f32>) -> f32, 16>>,
|
pub position_pid_controller: Option<PidController<fn(Vec3<f32>, Vec3<f32>) -> f32, 16>>,
|
||||||
/// Position from which to flee. Intended to be the agent's position plus a
|
/// Position from which to flee. Intended to be the agent's position plus a
|
||||||
/// random position offset, to be used when a random flee direction is
|
/// random position offset, to be used when a random flee direction is
|
||||||
/// required and reset each time the flee timer is reset.
|
/// required and reset each time the flee timer is reset.
|
||||||
pub flee_from_pos: Option<Pos>,
|
pub flee_from_pos: Option<Pos>,
|
||||||
|
pub awareness: Awareness,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Always clamped between `0.0` and `1.0`.
|
||||||
|
pub struct Awareness {
|
||||||
|
level: f32,
|
||||||
|
reached: bool,
|
||||||
|
}
|
||||||
|
impl fmt::Display for Awareness {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{:.2}", self.level) }
|
||||||
|
}
|
||||||
|
impl Awareness {
|
||||||
|
const ALERT: f32 = 1.0;
|
||||||
|
const HIGH: f32 = 0.6;
|
||||||
|
const LOW: f32 = 0.1;
|
||||||
|
const MEDIUM: f32 = 0.3;
|
||||||
|
const UNAWARE: f32 = 0.0;
|
||||||
|
|
||||||
|
pub fn new(level: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
level: level.clamp(Self::UNAWARE, Self::ALERT),
|
||||||
|
reached: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level of awareness as a decimal.
|
||||||
|
pub fn level(&self) -> f32 { self.level }
|
||||||
|
|
||||||
|
/// The level of awareness in English. To see if awareness has been fully
|
||||||
|
/// reached, use `self.reached()`.
|
||||||
|
pub fn state(&self) -> AwarenessState {
|
||||||
|
if self.level == Self::ALERT {
|
||||||
|
AwarenessState::Alert
|
||||||
|
} else if self.level.is_between(Self::HIGH, Self::ALERT) {
|
||||||
|
AwarenessState::High
|
||||||
|
} else if self.level.is_between(Self::MEDIUM, Self::HIGH) {
|
||||||
|
AwarenessState::Medium
|
||||||
|
} else if self.level.is_between(Self::LOW, Self::MEDIUM) {
|
||||||
|
AwarenessState::Low
|
||||||
|
} else {
|
||||||
|
AwarenessState::Unaware
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Awareness was reached at some point and has not been reset.
|
||||||
|
pub fn reached(&self) -> bool { self.reached }
|
||||||
|
|
||||||
|
pub fn change_by(&mut self, amount: f32) {
|
||||||
|
self.level = (self.level + amount).clamp(Self::UNAWARE, Self::ALERT);
|
||||||
|
|
||||||
|
if self.state() == AwarenessState::Alert {
|
||||||
|
self.reached = true;
|
||||||
|
} else if self.state() == AwarenessState::Unaware {
|
||||||
|
self.reached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialOrd, PartialEq, Eq)]
|
||||||
|
pub enum AwarenessState {
|
||||||
|
Unaware = 0,
|
||||||
|
Low = 1,
|
||||||
|
Medium = 2,
|
||||||
|
High = 3,
|
||||||
|
Alert = 4,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// State persistence object for the behavior tree
|
/// State persistence object for the behavior tree
|
||||||
@ -565,9 +629,9 @@ impl Agent {
|
|||||||
timer: Timer::default(),
|
timer: Timer::default(),
|
||||||
bearing: Vec2::zero(),
|
bearing: Vec2::zero(),
|
||||||
sounds_heard: Vec::new(),
|
sounds_heard: Vec::new(),
|
||||||
awareness: 0.0,
|
|
||||||
position_pid_controller: None,
|
position_pid_controller: None,
|
||||||
flee_from_pos: None,
|
flee_from_pos: None,
|
||||||
|
awareness: Awareness::new(0.0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -618,34 +682,6 @@ impl Agent {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrement_awareness(&mut self, dt: f32) {
|
|
||||||
let mut decrement = dt * AWARENESS_DECREMENT_CONSTANT;
|
|
||||||
let awareness = self.awareness;
|
|
||||||
|
|
||||||
let too_high = awareness >= 100.0;
|
|
||||||
let high = awareness >= 50.0;
|
|
||||||
let medium = awareness >= 30.0;
|
|
||||||
let low = awareness > 15.0;
|
|
||||||
let positive = awareness >= 0.0;
|
|
||||||
let negative = awareness < 0.0;
|
|
||||||
|
|
||||||
if too_high {
|
|
||||||
decrement *= 3.0;
|
|
||||||
} else if high {
|
|
||||||
decrement *= 1.0;
|
|
||||||
} else if medium {
|
|
||||||
decrement *= 2.5;
|
|
||||||
} else if low {
|
|
||||||
decrement *= 0.70;
|
|
||||||
} else if positive {
|
|
||||||
decrement *= 0.5;
|
|
||||||
} else if negative {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.awareness -= decrement;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn forget_old_sounds(&mut self, time: f64) {
|
pub fn forget_old_sounds(&mut self, time: f64) {
|
||||||
if !self.sounds_heard.is_empty() {
|
if !self.sounds_heard.is_empty() {
|
||||||
// Keep (retain) only newer sounds
|
// Keep (retain) only newer sounds
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
consts::{
|
consts::{
|
||||||
AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST,
|
AVG_FOLLOW_DIST, DEFAULT_ATTACK_RANGE, IDLE_HEALING_ITEM_THRESHOLD, PARTIAL_PATH_DIST,
|
||||||
SEPARATION_BIAS, SEPARATION_DIST,
|
SEPARATION_BIAS, SEPARATION_DIST, STD_AWARENESS_DECAY_RATE,
|
||||||
},
|
},
|
||||||
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
|
data::{AgentData, AttackData, Path, ReadData, Tactic, TargetData},
|
||||||
util::{
|
util::{
|
||||||
@ -166,6 +166,11 @@ impl<'a> AgentData<'a> {
|
|||||||
enum ActionTimers {
|
enum ActionTimers {
|
||||||
TimerIdle = 0,
|
TimerIdle = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
agent
|
||||||
|
.awareness
|
||||||
|
.change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
|
||||||
|
|
||||||
// Light lanterns at night
|
// Light lanterns at night
|
||||||
// TODO Add a method to turn on NPC lanterns underground
|
// TODO Add a method to turn on NPC lanterns underground
|
||||||
let lantern_equipped = self
|
let lantern_equipped = self
|
||||||
@ -696,13 +701,8 @@ impl<'a> AgentData<'a> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let can_sense_directly_near =
|
|
||||||
{ |e_pos: &Pos| e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) };
|
|
||||||
|
|
||||||
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
|
let is_detected = |entity: EcsEntity, e_pos: &Pos| {
|
||||||
let chance = thread_rng().gen_bool(0.3);
|
self.can_sense_directly_near(e_pos)
|
||||||
|
|
||||||
(can_sense_directly_near(e_pos) && chance)
|
|
||||||
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
|
|| self.can_see_entity(agent, controller, entity, e_pos, read_data)
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1306,9 +1306,6 @@ impl<'a> AgentData<'a> {
|
|||||||
.map_or(false, |stats| stats.name == *"Guard".to_string());
|
.map_or(false, |stats| stats.name == *"Guard".to_string());
|
||||||
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
|
let follows_threatening_sounds = has_enemy_alignment || is_village_guard;
|
||||||
|
|
||||||
// TODO: Awareness currently doesn't influence anything.
|
|
||||||
//agent.awareness += 0.5 * sound.vol;
|
|
||||||
|
|
||||||
if sound_was_threatening && is_close {
|
if sound_was_threatening && is_close {
|
||||||
if !self.below_flee_health(agent) && follows_threatening_sounds {
|
if !self.below_flee_health(agent) && follows_threatening_sounds {
|
||||||
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
self.follow(agent, controller, &read_data.terrain, &sound_pos);
|
||||||
@ -1517,7 +1514,7 @@ impl<'a> AgentData<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_see_entity(
|
pub fn can_see_entity(
|
||||||
&self,
|
&self,
|
||||||
agent: &Agent,
|
agent: &Agent,
|
||||||
controller: &Controller,
|
controller: &Controller,
|
||||||
@ -1550,6 +1547,11 @@ impl<'a> AgentData<'a> {
|
|||||||
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
|
&& entities_have_line_of_sight(self.pos, self.body, other_pos, other_body, read_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn can_sense_directly_near(&self, e_pos: &Pos) -> bool {
|
||||||
|
let chance = thread_rng().gen_bool(0.3);
|
||||||
|
e_pos.0.distance_squared(self.pos.0) < 5_f32.powi(2) && chance
|
||||||
|
}
|
||||||
|
|
||||||
pub fn menacing(
|
pub fn menacing(
|
||||||
&self,
|
&self,
|
||||||
agent: &mut Agent,
|
agent: &mut Agent,
|
||||||
|
@ -14,4 +14,4 @@ pub const RETARGETING_THRESHOLD_SECONDS: f64 = 10.0;
|
|||||||
pub const HEALING_ITEM_THRESHOLD: f32 = 0.5;
|
pub const HEALING_ITEM_THRESHOLD: f32 = 0.5;
|
||||||
pub const IDLE_HEALING_ITEM_THRESHOLD: f32 = 0.999;
|
pub const IDLE_HEALING_ITEM_THRESHOLD: f32 = 0.999;
|
||||||
pub const DEFAULT_ATTACK_RANGE: f32 = 2.0;
|
pub const DEFAULT_ATTACK_RANGE: f32 = 2.0;
|
||||||
pub const AWARENESS_INVESTIGATE_THRESHOLD: f32 = 1.0;
|
pub const STD_AWARENESS_DECAY_RATE: f32 = -0.05;
|
||||||
|
@ -203,6 +203,7 @@ impl<'a> System<'a> for Sys {
|
|||||||
msm: &read_data.msm,
|
msm: &read_data.msm,
|
||||||
poise: read_data.poises.get(entity),
|
poise: read_data.poises.get(entity),
|
||||||
};
|
};
|
||||||
|
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
// Behavior tree
|
// Behavior tree
|
||||||
///////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////
|
||||||
|
@ -2,7 +2,8 @@ use crate::rtsim::Entity as RtSimEntity;
|
|||||||
use common::{
|
use common::{
|
||||||
comp::{
|
comp::{
|
||||||
agent::{
|
agent::{
|
||||||
AgentEvent, Target, TimerAction, DEFAULT_INTERACTION_TIME, TRADE_INTERACTION_TIME,
|
AgentEvent, AwarenessState, Target, TimerAction, DEFAULT_INTERACTION_TIME,
|
||||||
|
TRADE_INTERACTION_TIME,
|
||||||
},
|
},
|
||||||
Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction,
|
Agent, Alignment, BehaviorCapability, BehaviorState, Body, BuffKind, ControlAction,
|
||||||
ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind,
|
ControlEvent, Controller, InputKind, InventoryEvent, Pos, UtteranceKind,
|
||||||
@ -11,7 +12,6 @@ use common::{
|
|||||||
path::TraversalConfig,
|
path::TraversalConfig,
|
||||||
};
|
};
|
||||||
use rand::{prelude::ThreadRng, thread_rng, Rng};
|
use rand::{prelude::ThreadRng, thread_rng, Rng};
|
||||||
use server_agent::consts::NORMAL_FLEE_DIR_DIST;
|
|
||||||
use specs::{
|
use specs::{
|
||||||
saveload::{Marker, MarkerAllocator},
|
saveload::{Marker, MarkerAllocator},
|
||||||
Entity as EcsEntity,
|
Entity as EcsEntity,
|
||||||
@ -27,7 +27,8 @@ use self::interaction::{
|
|||||||
use super::{
|
use super::{
|
||||||
consts::{
|
consts::{
|
||||||
DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FOLLOW_DIST,
|
DAMAGE_MEMORY_DURATION, FLEE_DURATION, HEALING_ITEM_THRESHOLD, MAX_FOLLOW_DIST,
|
||||||
NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
|
NORMAL_FLEE_DIR_DIST, NPC_PICKUP_RANGE, RETARGETING_THRESHOLD_SECONDS,
|
||||||
|
STD_AWARENESS_DECAY_RATE,
|
||||||
},
|
},
|
||||||
data::{AgentData, ReadData, TargetData},
|
data::{AgentData, ReadData, TargetData},
|
||||||
util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing},
|
util::{get_entity_by_id, is_dead, is_dead_or_invulnerable, is_invulnerable, stop_pursuing},
|
||||||
@ -100,7 +101,8 @@ impl BehaviorTree {
|
|||||||
Self {
|
Self {
|
||||||
tree: vec![
|
tree: vec![
|
||||||
untarget_if_dead,
|
untarget_if_dead,
|
||||||
do_hostile_tree_if_hostile,
|
update_target_awareness,
|
||||||
|
do_hostile_tree_if_hostile_and_aware,
|
||||||
do_pet_tree_if_owned,
|
do_pet_tree_if_owned,
|
||||||
do_pickup_loot,
|
do_pickup_loot,
|
||||||
do_idle_tree,
|
do_idle_tree,
|
||||||
@ -247,6 +249,8 @@ fn target_if_attacked(bdata: &mut BehaviorData) -> bool {
|
|||||||
.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
.push_event(ControlEvent::Utterance(UtteranceKind::Angry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bdata.agent.awareness.change_by(1.0);
|
||||||
|
|
||||||
// Determine whether the new target should be a priority
|
// Determine whether the new target should be a priority
|
||||||
// over the old one (i.e: because it's either close or
|
// over the old one (i.e: because it's either close or
|
||||||
// because they attacked us).
|
// because they attacked us).
|
||||||
@ -316,10 +320,13 @@ fn untarget_if_dead(bdata: &mut BehaviorData) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If target is hostile, do the hostile tree and stop the current BehaviorTree
|
/// If target is hostile and agent is aware of target, do the hostile tree and
|
||||||
fn do_hostile_tree_if_hostile(bdata: &mut BehaviorData) -> bool {
|
/// stop the current BehaviorTree
|
||||||
|
fn do_hostile_tree_if_hostile_and_aware(bdata: &mut BehaviorData) -> bool {
|
||||||
|
let alert = bdata.agent.awareness.reached();
|
||||||
|
|
||||||
if let Some(Target { hostile, .. }) = bdata.agent.target {
|
if let Some(Target { hostile, .. }) = bdata.agent.target {
|
||||||
if hostile {
|
if alert && hostile {
|
||||||
BehaviorTree::hostile().run(bdata);
|
BehaviorTree::hostile().run(bdata);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -514,6 +521,41 @@ fn hurt_utterance(bdata: &mut BehaviorData) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_target_awareness(bdata: &mut BehaviorData) -> bool {
|
||||||
|
let BehaviorData {
|
||||||
|
agent,
|
||||||
|
agent_data,
|
||||||
|
read_data,
|
||||||
|
controller,
|
||||||
|
..
|
||||||
|
} = bdata;
|
||||||
|
|
||||||
|
let target = agent.target.map(|t| t.target);
|
||||||
|
let tgt_pos = target.and_then(|t| read_data.positions.get(t));
|
||||||
|
|
||||||
|
if let (Some(target), Some(tgt_pos)) = (target, tgt_pos) {
|
||||||
|
if agent_data.can_see_entity(agent, controller, target, tgt_pos, read_data) {
|
||||||
|
agent.awareness.change_by(1.75 * read_data.dt.0);
|
||||||
|
} else if agent_data.can_sense_directly_near(tgt_pos) {
|
||||||
|
agent.awareness.change_by(0.25);
|
||||||
|
} else {
|
||||||
|
agent
|
||||||
|
.awareness
|
||||||
|
.change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
agent
|
||||||
|
.awareness
|
||||||
|
.change_by(STD_AWARENESS_DECAY_RATE * read_data.dt.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if bdata.agent.awareness.state() == AwarenessState::Unaware {
|
||||||
|
bdata.agent.target = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
fn do_combat(bdata: &mut BehaviorData) -> bool {
|
fn do_combat(bdata: &mut BehaviorData) -> bool {
|
||||||
let BehaviorData {
|
let BehaviorData {
|
||||||
agent,
|
agent,
|
||||||
|
Loading…
Reference in New Issue
Block a user