From ec83b94fcfc7a25e90669546119e7138aa5c8985 Mon Sep 17 00:00:00 2001 From: Mubelotix Date: Thu, 4 Jan 2024 20:38:30 +0100 Subject: [PATCH] Add zombie AI --- minecraft-positions/src/lib.rs | 4 + .../src/entities/monsters/zombies.rs | 171 +++++++++++++----- minecraft-server/src/world/ecs.rs | 4 +- minecraft-server/src/world/mod.rs | 2 +- 4 files changed, 129 insertions(+), 52 deletions(-) diff --git a/minecraft-positions/src/lib.rs b/minecraft-positions/src/lib.rs index e67cc314..e3b603ae 100644 --- a/minecraft-positions/src/lib.rs +++ b/minecraft-positions/src/lib.rs @@ -112,6 +112,10 @@ impl Position { cz: (self.z.floor() as i32).div_euclid(16), } } + + pub fn distance(&self, other: &Position) -> f64 { + ((self.x - other.x).powi(2) + (self.y - other.y).powi(2) + (self.z - other.z).powi(2)).sqrt() + } } impl std::ops::Add for Position { diff --git a/minecraft-server/src/entities/monsters/zombies.rs b/minecraft-server/src/entities/monsters/zombies.rs index 1b1ddcfd..95b86f10 100644 --- a/minecraft-server/src/entities/monsters/zombies.rs +++ b/minecraft-server/src/entities/monsters/zombies.rs @@ -13,7 +13,10 @@ pub struct Zombie { pub is_becoming_drowned: bool, } -const ZOMBIE_SPEED: f64 = 0.2; // Arbitrary value +// TODO: Attributes should be stored in a nicer way +// https://minecraft.wiki/w/Attribute +const ZOMBIE_BASE_FOLLOW_RANGE: f64 = 35.0; +const ZOMBIE_BASE_MOVEMENT_SPEED: f64 = 0.23; pub struct ZombieTask { newton_task: NewtonTask, @@ -30,59 +33,129 @@ impl ZombieTask { }) } + /// Sets the target to the closest player in range. + /// + /// Returns the position of the zombie and the position of the target as an optimization, just so that we don't have to get them again. + async fn acquire_target(&mut self, h: &Handler) -> Option<(Position, Position)> { + // Get the range of chunks to search + let self_position = h.observe(|e| e.get_entity().position.clone()).await?; + let mut lowest = self_position.clone(); + lowest.x -= ZOMBIE_BASE_FOLLOW_RANGE.floor(); + lowest.z -= ZOMBIE_BASE_FOLLOW_RANGE.floor(); + let mut highest = self_position.clone(); + highest.x += ZOMBIE_BASE_FOLLOW_RANGE.ceil(); + highest.z += ZOMBIE_BASE_FOLLOW_RANGE.ceil(); + let lowest_chunk = lowest.chunk_column(); + let highest_chunk = highest.chunk_column(); + + // List all players in area + let mut player_positions = HashMap::new(); + for cx in lowest_chunk.cx..=highest_chunk.cx { + for cz in lowest_chunk.cz..=highest_chunk.cz { + let chunk_position = ChunkColumnPosition { cx, cz }; + h.world.observe_entities(chunk_position, |entity, eid| -> Option<()> { + TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { + player_positions.insert(eid, player.get_entity().position.clone()); + }); + None + }).await; + } + } + + // Return if no players are found + if player_positions.is_empty() { + return None; + } + + // Get their distances + let mut player_distances = Vec::with_capacity(player_positions.len()); + for (eid, position) in &player_positions { + player_distances.push((*eid, position.distance(&self_position))); + } + player_distances.sort_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap()); + + // Get the closest player that's in range + let (target_eid, target_dist) = player_distances[0]; + if target_dist > ZOMBIE_BASE_FOLLOW_RANGE as f64 { + return None; + } + self.target = Some(target_eid); + + // TODO: ensure there is a line of sight + + player_positions.remove(&target_eid).map(|target_position| (self_position, target_position)) + } + + /// Returns the position of the target if any. + async fn get_target_position(&self, h: &Handler) -> Option { + let target_eid = self.target?; + h.world.observe_entity(target_eid, |entity| { + TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { + player.position.clone() + }) + }).await.flatten() + } + + /// Returns the position of the zombie. + async fn get_self_position(&self, h: &Handler) -> Option { + h.observe(|e| e.get_entity().position.clone()).await + } + + /// Returns the movement towards the target that can be applied without colliding with the world. + async fn get_movement(&self, h: &Handler, self_position: &Position, target_position: &Position) -> Translation { + // Create a movement vector + let mut translation = Translation { + x: target_position.x - self_position.x, + y: target_position.y - self_position.y, + z: target_position.z - self_position.z, + }; + if translation.norm() > ZOMBIE_BASE_MOVEMENT_SPEED { + translation.set_norm(ZOMBIE_BASE_MOVEMENT_SPEED); + } + + // Create a collision shape + let collision_shape = CollisionShape { + x1: self_position.x - 0.5, + y1: self_position.y, + z1: self_position.z - 0.5, + x2: self_position.x + 0.5, + y2: self_position.y + 1.95, + z2: self_position.z + 0.5, + }; + + // Restrict the movement considering world collisions + h.world.try_move(&collision_shape, &translation).await + } + pub async fn tick(&mut self, h: Handler, entity_change_set: &EntityChangeSet) { + // Acquire target if none + let mut positions = None; + if self.target.is_none() { + positions = self.acquire_target(&h).await; + } + + // Get target position if not already acquired + if positions.is_none() { + let target_position = self.get_target_position(&h).await; + let self_position = self.get_self_position(&h).await; + positions = match (target_position, self_position) { + (Some(target_position), Some(self_position)) => Some((self_position, target_position)), + _ => return, + }; + } + + // Get the movement to apply + if let Some((self_position, target_position)) = positions { + let movement = self.get_movement(&h, &self_position, &target_position).await; + h.mutate(|e| { + e.get_entity_mut().position += movement; + }).await; + } + self.newton_task.tick(h.into(), entity_change_set).await; } } -// pub async fn zombie_ai_task(h: Handler, mut server_msg_rcvr: BroadcastReceiver) where AnyEntity: TryAsEntityRef { -// loop { -// sleep_ticks(&mut server_msg_rcvr, 1).await; - -// let mut self_position = h.observe(|e| e.get_entity().position.clone()).await.unwrap(); -// let chunk = self_position.chunk_column(); -// let player_positions = h.world.observe_entities(chunk, |entity| { -// let network_entity = entity.to_network().unwrap(); -// TryAsEntityRef::::try_as_entity_ref(entity).map(|player| { -// (player.get_entity().position.clone(), network_entity) -// }) -// }).await; - -// let Some((target_position, network_entity)) = player_positions.get(0) else { sleep_ticks(&mut server_msg_rcvr, 100).await; continue }; -// let target_object = CollisionShape { -// x1: target_position.x - network_entity.width() as f64 / 2.0, -// y1: target_position.y, -// z1: target_position.z - network_entity.width() as f64 / 2.0, -// x2: target_position.x + network_entity.width() as f64 / 2.0, -// y2: target_position.y + network_entity.height() as f64, -// z2: target_position.z + network_entity.width() as f64 / 2.0, -// }; - -// for _ in 0..50 { -// let mut translation = Translation { -// x: target_position.x - self_position.x, -// y: target_position.y - self_position.y, -// z: target_position.z - self_position.z, -// }; -// translation.set_norm(ZOMBIE_SPEED); - -// let authorized_translation = h.world.try_move(&target_object, &translation).await; - -// let new_pos = h.mutate(|e| { -// e.get_entity_mut().position += authorized_translation; -// (e.get_entity().position.clone(), EntityChanges::position()) -// }).await; -// self_position = match new_pos { -// Some(pos) => pos, -// None => break, -// }; - -// sleep_ticks(&mut server_msg_rcvr, 1).await; // TODO: do while -// } - -// } -// } - #[derive(Default)] #[MinecraftEntity( ancestors { Zombie, Monster, PathfinderMob, Mob, LivingEntity, Entity }, diff --git a/minecraft-server/src/world/ecs.rs b/minecraft-server/src/world/ecs.rs index af281e82..489defee 100644 --- a/minecraft-server/src/world/ecs.rs +++ b/minecraft-server/src/world/ecs.rs @@ -35,14 +35,14 @@ impl Entities { /// Observe entities in a chunk through a closure /// That closure will be applied to each entity, and the results will be returned in a vector - pub(super) async fn observe_entities(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity) -> Option) -> Vec { + pub(super) async fn observe_entities(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity, Eid) -> Option) -> Vec { let entities = self.entities.read().await; let chunks = self.chunks.read().await; let Some(eids) = chunks.get(&chunk) else {return Vec::new()}; let mut results = Vec::with_capacity(eids.len()); for eid in eids { if let Some(entity) = entities.get(eid) { - if let Some(r) = observer(entity) { + if let Some(r) = observer(entity, *eid) { results.push(r); } } diff --git a/minecraft-server/src/world/mod.rs b/minecraft-server/src/world/mod.rs index c96a501d..b5554b7c 100644 --- a/minecraft-server/src/world/mod.rs +++ b/minecraft-server/src/world/mod.rs @@ -108,7 +108,7 @@ impl World { self.entities.observe_entity(eid, observer).await } - pub async fn observe_entities(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity) -> Option) -> Vec { + pub async fn observe_entities(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity, Eid) -> Option) -> Vec { self.entities.observe_entities(chunk, observer).await }