Skip to content
This repository has been archived by the owner on Jul 1, 2024. It is now read-only.

Commit

Permalink
Add zombie AI
Browse files Browse the repository at this point in the history
  • Loading branch information
Mubelotix committed Jan 4, 2024
1 parent 3f0b69b commit ec83b94
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 52 deletions.
4 changes: 4 additions & 0 deletions minecraft-positions/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Position> for Position {
Expand Down
171 changes: 122 additions & 49 deletions minecraft-server/src/entities/monsters/zombies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Zombie>) -> 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::<Player>::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<Zombie>) -> Option<Position> {
let target_eid = self.target?;
h.world.observe_entity(target_eid, |entity| {
TryAsEntityRef::<Entity>::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<Zombie>) -> Option<Position> {
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<Zombie>, 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<Zombie>, 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<T: EntityDescendant + ZombieDescendant>(h: Handler<T>, mut server_msg_rcvr: BroadcastReceiver<ServerMessage>) where AnyEntity: TryAsEntityRef<T> {
// 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::<Player>::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 },
Expand Down
4 changes: 2 additions & 2 deletions minecraft-server/src/world/ecs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity) -> Option<R>) -> Vec<R> {
pub(super) async fn observe_entities<R>(&self, chunk: ChunkColumnPosition, mut observer: impl FnMut(&AnyEntity, Eid) -> Option<R>) -> Vec<R> {
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);
}
}
Expand Down
2 changes: 1 addition & 1 deletion minecraft-server/src/world/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ impl World {
self.entities.observe_entity(eid, observer).await
}

pub async fn observe_entities<R>(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity) -> Option<R>) -> Vec<R> {
pub async fn observe_entities<R>(&self, chunk: ChunkColumnPosition, observer: impl FnMut(&AnyEntity, Eid) -> Option<R>) -> Vec<R> {
self.entities.observe_entities(chunk, observer).await
}

Expand Down

0 comments on commit ec83b94

Please sign in to comment.