From f2cfad06bd1ef3ade442e65890634291601ac366 Mon Sep 17 00:00:00 2001 From: Alexander Medvedev Date: Sat, 4 Jan 2025 18:37:59 +0100 Subject: [PATCH] Add Entity AI --- README.md | 2 +- pumpkin-core/src/math/vector3.rs | 11 +++ pumpkin/src/entity/ai/goal/look_at_entity.rs | 55 ++++++++++++ pumpkin/src/entity/ai/goal/mod.rs | 13 ++- pumpkin/src/entity/mob/mod.rs | 53 ++++++++++++ pumpkin/src/entity/mob/zombie.rs | 24 ++++++ pumpkin/src/entity/mod.rs | 31 +++++-- pumpkin/src/entity/player.rs | 1 + pumpkin/src/net/packet/play.rs | 21 +++-- pumpkin/src/server/mod.rs | 50 +++++++---- pumpkin/src/world/mod.rs | 91 ++++++++++++-------- 11 files changed, 281 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index 7b807ac8a..4dad73857 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ and customizable experience. It prioritizes performance and player enjoyment whi - [x] Players - [x] Mobs - [x] Animals - - [ ] Entity AI + - [x] Entity AI - [ ] Boss - Server - [ ] Plugins diff --git a/pumpkin-core/src/math/vector3.rs b/pumpkin-core/src/math/vector3.rs index 53d87826a..553e07f61 100644 --- a/pumpkin-core/src/math/vector3.rs +++ b/pumpkin-core/src/math/vector3.rs @@ -42,6 +42,17 @@ impl Vector3 { z: self.z * z, } } + + pub fn squared_distance_to_vec(&self, other: Self) -> T { + self.squared_distance_to(other.x, other.y, other.z) + } + + pub fn squared_distance_to(&self, x: T, y: T, z: T) -> T { + let delta_x = self.x - x; + let delta_y = self.y - y; + let delta_z = self.z - z; + delta_x * delta_x + delta_y * delta_y + delta_z * delta_z + } } impl Vector3 { diff --git a/pumpkin/src/entity/ai/goal/look_at_entity.rs b/pumpkin/src/entity/ai/goal/look_at_entity.rs index 8b1378917..ab045b8c0 100644 --- a/pumpkin/src/entity/ai/goal/look_at_entity.rs +++ b/pumpkin/src/entity/ai/goal/look_at_entity.rs @@ -1 +1,56 @@ +use std::sync::Arc; +use async_trait::async_trait; +use tokio::sync::Mutex; + +use crate::entity::{mob::MobEntity, player::Player}; + +use super::Goal; + +pub struct LookAtEntityGoal { + // TODO: make this an entity + target: Mutex>>, + range: f64, +} + +impl LookAtEntityGoal { + #[must_use] + pub fn new(range: f64) -> Self { + Self { + target: Mutex::new(None), + range, + } + } +} + +#[async_trait] +impl Goal for LookAtEntityGoal { + async fn can_start(&self, mob: &crate::entity::mob::MobEntity) -> bool { + // TODO: make this an entity + let mut target = self.target.lock().await; + + *target = mob + .living_entity + .entity + .world + .get_closest_player(mob.living_entity.entity.pos.load(), self.range) + .await; + target.is_some() + } + + async fn should_continue(&self, mob: &MobEntity) -> bool { + if let Some(target) = self.target.lock().await.as_ref() { + let mob_pos = mob.living_entity.entity.pos.load(); + let target_pos = target.living_entity.entity.pos.load(); + return mob_pos.squared_distance_to_vec(target_pos) <= (self.range * self.range); + } + false + } + + async fn tick(&self, mob: &MobEntity) { + if let Some(target) = self.target.lock().await.as_ref() { + let target_pos = target.living_entity.entity.pos.load(); + mob.living_entity.entity.look_at(target_pos).await; + } + } +} diff --git a/pumpkin/src/entity/ai/goal/mod.rs b/pumpkin/src/entity/ai/goal/mod.rs index e7847ddff..9405603ed 100644 --- a/pumpkin/src/entity/ai/goal/mod.rs +++ b/pumpkin/src/entity/ai/goal/mod.rs @@ -1,10 +1,15 @@ +use async_trait::async_trait; + +use crate::entity::mob::MobEntity; + pub mod look_at_entity; -pub trait Goal { +#[async_trait] +pub trait Goal: Send + Sync { /// How Should the Goal initially start? - fn can_start(&self) -> bool; + async fn can_start(&self, mob: &MobEntity) -> bool; /// When its started, How it should Continue to run - fn should_continue() -> bool; + async fn should_continue(&self, mob: &MobEntity) -> bool; /// If the Goal is running, this gets called every tick - fn tick(&self); + async fn tick(&self, mob: &MobEntity); } diff --git a/pumpkin/src/entity/mob/mod.rs b/pumpkin/src/entity/mob/mod.rs index e95568045..798b53932 100644 --- a/pumpkin/src/entity/mob/mod.rs +++ b/pumpkin/src/entity/mob/mod.rs @@ -1 +1,54 @@ +use std::sync::Arc; + +use pumpkin_core::math::vector3::Vector3; +use pumpkin_entity::entity_type::EntityType; +use tokio::sync::Mutex; +use uuid::Uuid; +use zombie::Zombie; + +use crate::{server::Server, world::World}; + +use super::{ai::goal::Goal, living::LivingEntity}; + pub mod zombie; + +pub struct MobEntity { + pub living_entity: Arc, + pub goals: Mutex, bool)>>, +} + +impl MobEntity { + pub async fn tick(&self) { + let mut goals = self.goals.lock().await; + for (goal, running) in goals.iter_mut() { + if *running { + if goal.should_continue(self).await { + goal.tick(self).await; + } else { + *running = false; + } + } else { + *running = goal.can_start(self).await; + } + } + } +} + +pub async fn from_type( + entity_type: EntityType, + server: &Server, + position: Vector3, + world: &Arc, +) -> (Arc, Uuid) { + match entity_type { + EntityType::Zombie => Zombie::make(server, position, world).await, + // TODO + _ => server.add_mob_entity(entity_type, position, world).await, + } +} + +impl MobEntity { + pub async fn goal(&self, goal: T) { + self.goals.lock().await.push((Arc::new(goal), false)); + } +} diff --git a/pumpkin/src/entity/mob/zombie.rs b/pumpkin/src/entity/mob/zombie.rs index 8b1378917..44443a9a1 100644 --- a/pumpkin/src/entity/mob/zombie.rs +++ b/pumpkin/src/entity/mob/zombie.rs @@ -1 +1,25 @@ +use std::sync::Arc; +use pumpkin_core::math::vector3::Vector3; +use pumpkin_entity::entity_type::EntityType; +use uuid::Uuid; + +use crate::{entity::ai::goal::look_at_entity::LookAtEntityGoal, server::Server, world::World}; + +use super::MobEntity; + +pub struct Zombie; + +impl Zombie { + pub async fn make( + server: &Server, + position: Vector3, + world: &Arc, + ) -> (Arc, Uuid) { + let (zombie_entity, uuid) = server + .add_mob_entity(EntityType::Zombie, position, world) + .await; + zombie_entity.goal(LookAtEntityGoal::new(8.0)).await; + (zombie_entity, uuid) + } +} diff --git a/pumpkin/src/entity/mod.rs b/pumpkin/src/entity/mod.rs index b982adf4c..861adcaac 100644 --- a/pumpkin/src/entity/mod.rs +++ b/pumpkin/src/entity/mod.rs @@ -12,7 +12,7 @@ use pumpkin_core::math::{ }; use pumpkin_entity::{entity_type::EntityType, pose::EntityPose, EntityId}; use pumpkin_protocol::{ - client::play::{CSetEntityMetadata, CTeleportEntity, Metadata}, + client::play::{CHeadRot, CSetEntityMetadata, CTeleportEntity, CUpdateEntityRot, Metadata}, codec::var_int::VarInt, }; @@ -67,23 +67,29 @@ pub struct Entity { } impl Entity { + #[allow(clippy::too_many_arguments)] pub fn new( entity_id: EntityId, entity_uuid: uuid::Uuid, world: Arc, + position: Vector3, entity_type: EntityType, standing_eye_height: f32, bounding_box: AtomicCell, bounding_box_size: AtomicCell, ) -> Self { + let floor_x = position.x.floor() as i32; + let floor_y = position.y.floor() as i32; + let floor_z = position.z.floor() as i32; + Self { entity_id, entity_uuid, entity_type, on_ground: AtomicBool::new(false), - pos: AtomicCell::new(Vector3::new(0.0, 0.0, 0.0)), - block_pos: AtomicCell::new(WorldPosition(Vector3::new(0, 0, 0))), - chunk_pos: AtomicCell::new(Vector2::new(0, 0)), + pos: AtomicCell::new(position), + block_pos: AtomicCell::new(WorldPosition(Vector3::new(floor_x, floor_y, floor_z))), + chunk_pos: AtomicCell::new(Vector2::new(floor_x, floor_z)), sneaking: AtomicBool::new(false), world, // TODO: Load this from previous instance @@ -141,7 +147,7 @@ impl Entity { } /// Changes this entity's pitch and yaw to look at target - pub fn look_at(&self, target: Vector3) { + pub async fn look_at(&self, target: Vector3) { let position = self.pos.load(); let delta = target.sub(&position); let root = delta.x.hypot(delta.z); @@ -149,6 +155,21 @@ impl Entity { let yaw = wrap_degrees((delta.z.atan2(delta.x) as f32 * 180.0 / f32::consts::PI) - 90.0); self.pitch.store(pitch); self.yaw.store(yaw); + + // send packet + let yaw = (yaw * 256.0 / 360.0).rem_euclid(256.0); + let pitch = (pitch * 256.0 / 360.0).rem_euclid(256.0); + self.world + .broadcast_packet_all(&CUpdateEntityRot::new( + self.entity_id.into(), + yaw as u8, + pitch as u8, + self.on_ground.load(std::sync::atomic::Ordering::Relaxed), + )) + .await; + self.world + .broadcast_packet_all(&CHeadRot::new(self.entity_id.into(), yaw as u8)) + .await; } pub async fn teleport(&self, position: Vector3, yaw: f32, pitch: f32) { diff --git a/pumpkin/src/entity/player.rs b/pumpkin/src/entity/player.rs index 9bd67bb77..fd5d8619f 100644 --- a/pumpkin/src/entity/player.rs +++ b/pumpkin/src/entity/player.rs @@ -158,6 +158,7 @@ impl Player { entity_id, player_uuid, world, + Vector3::new(0.0, 0.0, 0.0), EntityType::Player, 1.62, AtomicCell::new(BoundingBox::new_default(&bounding_box_size)), diff --git a/pumpkin/src/net/packet/play.rs b/pumpkin/src/net/packet/play.rs index 7f402ce9e..b638e48be 100644 --- a/pumpkin/src/net/packet/play.rs +++ b/pumpkin/src/net/packet/play.rs @@ -2,8 +2,8 @@ use std::num::NonZeroU8; use std::sync::Arc; use crate::block::block_manager::BlockActionResult; +use crate::entity::mob; use crate::net::PlayerConfig; -use crate::world::World; use crate::{ command::CommandSender, entity::player::{ChatMode, Hand, Player}, @@ -664,7 +664,7 @@ impl Player { return; } entity_victim.kill().await; - World::remove_living_entity(entity_victim, world.clone()).await; + world.clone().remove_mob_entity(entity_victim).await; // TODO: block entities should be checked here (signs) } else { log::error!( @@ -1006,19 +1006,24 @@ impl Player { if let Some(spawn_item_name) = get_entity_id(&item_t) { let head_yaw = 10.0; let world_pos = WorldPosition(location.0 + face.to_offset()); + let pos = Vector3::new( + f64::from(world_pos.0.x), + f64::from(world_pos.0.y), + f64::from(world_pos.0.z), + ); // TODO: this should not be hardcoded - let (mob, _world, uuid) = server.add_living_entity(EntityType::Chicken).await; + let (mob, uuid) = mob::from_type(EntityType::Zombie, server, pos, self.world()).await; let opposite_yaw = self.living_entity.entity.yaw.load() + 180.0; server .broadcast_packet_all(&CSpawnEntity::new( - VarInt(mob.entity.entity_id), + VarInt(mob.living_entity.entity.entity_id), uuid, VarInt((*spawn_item_name).into()), - f64::from(world_pos.0.x) + f64::from(cursor_pos.x), - f64::from(world_pos.0.y), - f64::from(world_pos.0.z) + f64::from(cursor_pos.z), + pos.x + f64::from(cursor_pos.x), + pos.y, + pos.z + f64::from(cursor_pos.z), 10.0, head_yaw, opposite_yaw, @@ -1075,7 +1080,7 @@ impl Player { let block_bounding_box = BoundingBox::from_block(&world_pos); let mut intersects = false; - for player in world.get_nearby_players(entity.pos.load(), 20).await { + for player in world.get_nearby_players(entity.pos.load(), 20.0).await { let bounding_box = player.1.living_entity.entity.bounding_box.load(); if bounding_box.intersects(&block_bounding_box) { intersects = true; diff --git a/pumpkin/src/server/mod.rs b/pumpkin/src/server/mod.rs index 18db57e05..28cba6b4f 100644 --- a/pumpkin/src/server/mod.rs +++ b/pumpkin/src/server/mod.rs @@ -5,6 +5,7 @@ use pumpkin_config::BASIC_CONFIG; use pumpkin_core::math::boundingbox::{BoundingBox, BoundingBoxSize}; use pumpkin_core::math::position::WorldPosition; use pumpkin_core::math::vector2::Vector2; +use pumpkin_core::math::vector3::Vector3; use pumpkin_core::GameMode; use pumpkin_entity::entity_type::EntityType; use pumpkin_entity::EntityId; @@ -32,6 +33,7 @@ use uuid::Uuid; use crate::block::block_manager::BlockManager; use crate::block::default_block_manager; use crate::entity::living::LivingEntity; +use crate::entity::mob::MobEntity; use crate::entity::Entity; use crate::net::EncryptionError; use crate::world::custom_bossbar::CustomBossbars; @@ -194,7 +196,22 @@ impl Server { } } - /// Adds a new living entity to the server. + pub async fn add_mob_entity( + &self, + entity_type: EntityType, + position: Vector3, + world: &Arc, + ) -> (Arc, Uuid) { + let (living_entity, uuid) = self.add_living_entity(position, entity_type, world); + + let mob = Arc::new(MobEntity { + living_entity, + goals: Mutex::new(vec![]), + }); + world.add_mob_entity(uuid, mob.clone()).await; + (mob, uuid) + } + /// Adds a new living entity to the server. This does not Spawn the entity /// /// # Returns /// @@ -203,27 +220,25 @@ impl Server { /// - `Arc`: A reference to the newly created living entity. /// - `Arc`: A reference to the world that the living entity was added to. /// - `Uuid`: The uuid of the newly created living entity to be used to send to the client. - pub async fn add_living_entity( + fn add_living_entity( &self, + position: Vector3, entity_type: EntityType, - ) -> (Arc, Arc, Uuid) { + world: &Arc, + ) -> (Arc, Uuid) { let entity_id = self.new_entity_id(); - // TODO: select current - let world = &self.worlds[0]; // TODO: this should be resolved to a integer using a macro when calling this function - let bounding_box_size: BoundingBoxSize; - if let Some(entity) = get_entity_by_id(entity_type.clone() as u16) { - bounding_box_size = BoundingBoxSize { - width: f64::from(entity.dimension[0]), - height: f64::from(entity.dimension[1]), - }; - } else { - bounding_box_size = BoundingBoxSize { + let bounding_box_size = get_entity_by_id(entity_type.clone() as u16).map_or( + BoundingBoxSize { width: 0.6, height: 1.8, - }; - } + }, + |entity| BoundingBoxSize { + width: f64::from(entity.dimension[0]), + height: f64::from(entity.dimension[1]), + }, + ); // TODO: standing eye height should be per mob let new_uuid = uuid::Uuid::new_v4(); @@ -231,15 +246,14 @@ impl Server { entity_id, new_uuid, world.clone(), + position, entity_type, 1.62, AtomicCell::new(BoundingBox::new_default(&bounding_box_size)), AtomicCell::new(bounding_box_size), ))); - world.add_living_entity(new_uuid, mob.clone()).await; - - (mob, world.clone(), new_uuid) + (mob, new_uuid) } pub async fn try_get_container( diff --git a/pumpkin/src/world/mod.rs b/pumpkin/src/world/mod.rs index c11d37153..fef2d1943 100644 --- a/pumpkin/src/world/mod.rs +++ b/pumpkin/src/world/mod.rs @@ -5,7 +5,7 @@ pub mod player_chunker; use crate::{ command::client_cmd_suggestions, - entity::{living::LivingEntity, player::Player, Entity}, + entity::{living::LivingEntity, mob::MobEntity, player::Player, Entity}, error::PumpkinError, server::Server, }; @@ -94,6 +94,8 @@ pub struct World { pub level: Arc, /// A map of active players within the world, keyed by their unique UUID. pub current_players: Arc>>>, + /// A map of active mob entities within the world, keyed by their unique UUID. + pub current_living_mobs: Arc>>>, /// The world's scoreboard, used for tracking scores, objectives, and display information. pub scoreboard: Mutex, /// The world's worldborder, defining the playable area and controlling its expansion or contraction. @@ -102,8 +104,6 @@ pub struct World { pub level_time: Mutex, /// The type of dimension the world is in pub dimension_type: DimensionType, - /// A map of active entities within the world, keyed by their unique UUID. - pub current_living_entities: Arc>>>, // TODO: entities } @@ -113,11 +113,11 @@ impl World { Self { level: Arc::new(level), current_players: Arc::new(Mutex::new(HashMap::new())), + current_living_mobs: Arc::new(Mutex::new(HashMap::new())), scoreboard: Mutex::new(Scoreboard::new()), worldborder: Mutex::new(Worldborder::new(0.0, 0.0, 29_999_984.0, 0, 0, 0)), level_time: Mutex::new(LevelTime::new()), dimension_type, - current_living_entities: Arc::new(Mutex::new(HashMap::new())), } } @@ -198,16 +198,21 @@ impl World { pub async fn tick(&self) { // world ticks - let mut level_time = self.level_time.lock().await; - level_time.tick_time(); - if level_time.world_age % 20 == 0 { - level_time.send_time(self).await; + { + let mut level_time = self.level_time.lock().await; + level_time.tick_time(); + if level_time.world_age % 20 == 0 { + level_time.send_time(self).await; + } } // player ticks - let current_players = self.current_players.lock().await; - for player in current_players.values() { + for player in self.current_players.lock().await.values() { player.tick().await; } + // entites tick + for entity in self.current_living_mobs.lock().await.values() { + entity.tick().await; + } } /// Gets the y position of the first non air block from the top down @@ -609,7 +614,8 @@ impl World { /// Gets a Living Entity by entity id pub async fn get_living_entity_by_entityid(&self, id: EntityId) -> Option> { - for living_entity in self.current_living_entities.lock().await.values() { + for mob_entity in self.current_living_mobs.lock().await.values() { + let living_entity = &mob_entity.living_entity; if living_entity.entity_id() == id { return Some(living_entity.clone()); } @@ -682,27 +688,42 @@ impl World { pub async fn get_nearby_players( &self, pos: Vector3, - radius: u16, + radius: f64, ) -> HashMap> { - let radius_squared = (f64::from(radius)).powi(2); - - let mut found_players = HashMap::new(); - for player in self.current_players.lock().await.iter() { - let player_pos = player.1.living_entity.entity.pos.load(); + let radius_squared = radius.powi(2); - let diff = Vector3::new( - player_pos.x - pos.x, - player_pos.y - pos.y, - player_pos.z - pos.z, - ); - - let distance_squared = diff.x.powi(2) + diff.y.powi(2) + diff.z.powi(2); - if distance_squared <= radius_squared { - found_players.insert(*player.0, player.1.clone()); - } - } + self.current_players + .lock() + .await + .iter() + .filter_map(|(id, player)| { + let player_pos = player.living_entity.entity.pos.load(); + (player_pos.squared_distance_to_vec(pos) <= radius_squared) + .then(|| (*id, player.clone())) + }) + .collect() + } - found_players + pub async fn get_closest_player(&self, pos: Vector3, radius: f64) -> Option> { + let players = self.get_nearby_players(pos, radius).await; + players + .iter() + .min_by(|a, b| { + a.1.living_entity + .entity + .pos + .load() + .squared_distance_to_vec(pos) + .partial_cmp( + &b.1.living_entity + .entity + .pos + .load() + .squared_distance_to_vec(pos), + ) + .unwrap() + }) + .map(|p| p.1.clone()) } /// Adds a player to the world and broadcasts a join message if enabled. @@ -780,19 +801,19 @@ impl World { /// /// * `uuid`: The unique UUID of the living entity to add. /// * `living_entity`: A `Arc` reference to the living entity object. - pub async fn add_living_entity(&self, uuid: uuid::Uuid, living_entity: Arc) { - let mut current_living_entities = self.current_living_entities.lock().await; + pub async fn add_mob_entity(&self, uuid: uuid::Uuid, living_entity: Arc) { + let mut current_living_entities = self.current_living_mobs.lock().await; current_living_entities.insert(uuid, living_entity); } - pub async fn remove_living_entity(living_entity: Arc, world: Arc) { - let mut current_living_entities = world.current_living_entities.lock().await.clone(); + pub async fn remove_mob_entity(self: Arc, living_entity: Arc) { + let mut current_living_entities = self.current_living_mobs.lock().await.clone(); + current_living_entities.remove(&living_entity.entity.entity_uuid); // TODO: does this work with collisions? living_entity.entity.set_pose(EntityPose::Dying).await; tokio::spawn(async move { tokio::time::sleep(tokio::time::Duration::from_millis(2000)).await; - world.remove_entity(&living_entity.entity).await; - current_living_entities.remove(&living_entity.entity.entity_uuid); + self.remove_entity(&living_entity.entity).await; }); }