Add Entity AI

This commit is contained in:
Alexander Medvedev
2025-01-04 18:37:59 +01:00
parent 0fbc730818
commit f2cfad06bd
11 changed files with 281 additions and 71 deletions

View File

@@ -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

View File

@@ -42,6 +42,17 @@ impl<T: Math + Copy> Vector3<T> {
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<T: Math + Copy + Float> Vector3<T> {

View File

@@ -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<Option<Arc<Player>>>,
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;
}
}
}

View File

@@ -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);
}

View File

@@ -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<LivingEntity>,
pub goals: Mutex<Vec<(Arc<dyn Goal>, 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<f64>,
world: &Arc<World>,
) -> (Arc<MobEntity>, 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<T: Goal + 'static>(&self, goal: T) {
self.goals.lock().await.push((Arc::new(goal), false));
}
}

View File

@@ -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<f64>,
world: &Arc<World>,
) -> (Arc<MobEntity>, 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)
}
}

View File

@@ -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<World>,
position: Vector3<f64>,
entity_type: EntityType,
standing_eye_height: f32,
bounding_box: AtomicCell<BoundingBox>,
bounding_box_size: AtomicCell<BoundingBoxSize>,
) -> 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<f64>) {
pub async fn look_at(&self, target: Vector3<f64>) {
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<f64>, yaw: f32, pitch: f32) {

View File

@@ -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)),

View File

@@ -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;

View File

@@ -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<f64>,
world: &Arc<World>,
) -> (Arc<MobEntity>, 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<LivingEntity>`: A reference to the newly created living entity.
/// - `Arc<World>`: 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<f64>,
entity_type: EntityType,
) -> (Arc<LivingEntity>, Arc<World>, Uuid) {
world: &Arc<World>,
) -> (Arc<LivingEntity>, 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(

View File

@@ -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<Level>,
/// A map of active players within the world, keyed by their unique UUID.
pub current_players: Arc<Mutex<HashMap<uuid::Uuid, Arc<Player>>>>,
/// A map of active mob entities within the world, keyed by their unique UUID.
pub current_living_mobs: Arc<Mutex<HashMap<uuid::Uuid, Arc<MobEntity>>>>,
/// The world's scoreboard, used for tracking scores, objectives, and display information.
pub scoreboard: Mutex<Scoreboard>,
/// 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<LevelTime>,
/// 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<Mutex<HashMap<uuid::Uuid, Arc<LivingEntity>>>>,
// 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<Arc<LivingEntity>> {
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<f64>,
radius: u16,
radius: f64,
) -> HashMap<uuid::Uuid, Arc<Player>> {
let radius_squared = (f64::from(radius)).powi(2);
let radius_squared = 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();
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()
}
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());
}
}
found_players
pub async fn get_closest_player(&self, pos: Vector3<f64>, radius: f64) -> Option<Arc<Player>> {
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<LivingEntity>` reference to the living entity object.
pub async fn add_living_entity(&self, uuid: uuid::Uuid, living_entity: Arc<LivingEntity>) {
let mut current_living_entities = self.current_living_entities.lock().await;
pub async fn add_mob_entity(&self, uuid: uuid::Uuid, living_entity: Arc<MobEntity>) {
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<LivingEntity>, world: Arc<Self>) {
let mut current_living_entities = world.current_living_entities.lock().await.clone();
pub async fn remove_mob_entity(self: Arc<Self>, living_entity: Arc<LivingEntity>) {
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;
});
}