Skip to content

Commit 890ae6d

Browse files
committed
feat : add very basic AI for pig
1 parent a4791e2 commit 890ae6d

File tree

10 files changed

+344
-18
lines changed

10 files changed

+344
-18
lines changed

src/bin/src/systems/entities/entity_movement.rs

Lines changed: 108 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ use bevy_ecs::prelude::*;
22
use ferrumc_core::transform::grounded::OnGround;
33
use ferrumc_core::transform::position::Position;
44
use ferrumc_entities::components::*;
5+
use ferrumc_state::GlobalStateResource;
56

67
const GRAVITY: f64 = -0.08; // Blocks per tick^2
78
const TERMINAL_VELOCITY: f64 = -3.92; // Max fall speed
89

910
/// System that apply basic physics to entity
1011
pub fn entity_physics_system(
1112
mut query: Query<(&mut Position, &mut Velocity, &OnGround), With<EntityType>>,
13+
state: Res<GlobalStateResource>,
1214
) {
1315
for (mut pos, mut vel, on_ground) in query.iter_mut() {
1416
// Apply gravity if not on ground
@@ -21,20 +23,121 @@ pub fn entity_physics_system(
2123
}
2224
}
2325

24-
pos.x += vel.x;
25-
pos.y += vel.y;
26-
pos.z += vel.z;
26+
// Try to move in all three axes, checking collision at the final position
27+
let new_x = pos.x + vel.x;
28+
let new_y = pos.y + vel.y;
29+
let new_z = pos.z + vel.z;
30+
31+
// Check collision at the new position (considering all movement)
32+
if !check_collision(&state.0, new_x, new_y, new_z) {
33+
// No collision, move freely
34+
pos.x = new_x;
35+
pos.y = new_y;
36+
pos.z = new_z;
37+
} else {
38+
// Collision detected, try each axis separately
39+
40+
// Try Y movement first (jumping/falling)
41+
if !check_collision(&state.0, pos.x, new_y, pos.z) {
42+
pos.y = new_y;
43+
} else {
44+
vel.y = 0.0;
45+
}
46+
47+
// Try X movement with updated Y position
48+
if !check_collision(&state.0, new_x, pos.y, pos.z) {
49+
pos.x = new_x;
50+
} else {
51+
vel.x = 0.0;
52+
}
53+
54+
// Try Z movement with updated X and Y positions
55+
if !check_collision(&state.0, pos.x, pos.y, new_z) {
56+
pos.z = new_z;
57+
} else {
58+
vel.z = 0.0;
59+
}
60+
}
2761

2862
if on_ground.0 {
29-
vel.x *= 0.6;
30-
vel.z *= 0.6;
63+
// Less friction on ground for better movement (was 0.6)
64+
vel.x *= 0.85;
65+
vel.z *= 0.85;
3166
} else {
3267
vel.x *= 0.98;
3368
vel.z *= 0.98;
3469
}
3570
}
3671
}
3772

73+
/// Check if the entity would collide with a block at the given position
74+
fn check_collision(state: &ferrumc_state::GlobalState, x: f64, y: f64, z: f64) -> bool {
75+
// Pig hitbox is approximately 0.9 x 0.9 x 0.9 blocks
76+
let half_width = 0.45;
77+
let height = 0.9;
78+
79+
// Check corners of the bounding box at feet and head level
80+
let check_positions = [
81+
// Feet level - 4 corners
82+
(
83+
(x - half_width).floor() as i32,
84+
y.floor() as i32,
85+
(z - half_width).floor() as i32,
86+
),
87+
(
88+
(x + half_width).floor() as i32,
89+
y.floor() as i32,
90+
(z - half_width).floor() as i32,
91+
),
92+
(
93+
(x - half_width).floor() as i32,
94+
y.floor() as i32,
95+
(z + half_width).floor() as i32,
96+
),
97+
(
98+
(x + half_width).floor() as i32,
99+
y.floor() as i32,
100+
(z + half_width).floor() as i32,
101+
),
102+
// Head level - 4 corners
103+
(
104+
(x - half_width).floor() as i32,
105+
(y + height).floor() as i32,
106+
(z - half_width).floor() as i32,
107+
),
108+
(
109+
(x + half_width).floor() as i32,
110+
(y + height).floor() as i32,
111+
(z - half_width).floor() as i32,
112+
),
113+
(
114+
(x - half_width).floor() as i32,
115+
(y + height).floor() as i32,
116+
(z + half_width).floor() as i32,
117+
),
118+
(
119+
(x + half_width).floor() as i32,
120+
(y + height).floor() as i32,
121+
(z + half_width).floor() as i32,
122+
),
123+
];
124+
125+
for (check_x, check_y, check_z) in check_positions {
126+
if let Ok(block_state) =
127+
state
128+
.world
129+
.get_block_and_fetch(check_x, check_y, check_z, "overworld")
130+
{
131+
// If there's a solid block, collision detected
132+
if block_state.0 != 0 {
133+
return true;
134+
}
135+
}
136+
}
137+
138+
false
139+
}
140+
38141
pub fn entity_age_system(mut query: Query<&mut Age, With<EntityType>>) {
39142
for mut age in query.iter_mut() {
40143
age.tick();
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use bevy_ecs::prelude::*;
2+
use ferrumc_core::identity::player_identity::PlayerIdentity;
3+
use ferrumc_core::transform::grounded::OnGround;
4+
use ferrumc_core::transform::position::Position;
5+
use ferrumc_core::transform::rotation::Rotation;
6+
use ferrumc_entities::components::*;
7+
use ferrumc_net::connection::StreamWriter;
8+
use ferrumc_net::packets::outgoing::update_entity_position_and_rotation::UpdateEntityPositionAndRotationPacket;
9+
use ferrumc_net_codec::net_types::var_int::VarInt;
10+
use tracing::error;
11+
12+
/// System that syncs entity movement to players
13+
pub fn entity_movement_sync_system(
14+
mut entities: Query<
15+
(
16+
&EntityId,
17+
&Position,
18+
&Rotation,
19+
&OnGround,
20+
&SyncedToPlayers,
21+
&mut LastSyncedPosition,
22+
),
23+
(With<EntityType>, Without<PlayerIdentity>),
24+
>,
25+
players: Query<&StreamWriter, With<PlayerIdentity>>,
26+
) {
27+
for (entity_id, pos, rot, on_ground, synced, mut last_pos) in entities.iter_mut() {
28+
// Only sync if entity has moved
29+
if !last_pos.has_moved(pos) {
30+
continue;
31+
}
32+
33+
let delta = last_pos.delta_to(pos);
34+
35+
// Send update to all players who have this entity spawned
36+
for player_entity in &synced.player_entities {
37+
if let Ok(stream_writer) = players.get(*player_entity) {
38+
let packet = UpdateEntityPositionAndRotationPacket {
39+
entity_id: VarInt::new(entity_id.to_network_id()),
40+
delta_x: delta.0,
41+
delta_y: delta.1,
42+
delta_z: delta.2,
43+
yaw: ferrumc_net_codec::net_types::angle::NetAngle::from_degrees(
44+
rot.yaw as f64,
45+
),
46+
pitch: ferrumc_net_codec::net_types::angle::NetAngle::from_degrees(
47+
rot.pitch as f64,
48+
),
49+
on_ground: on_ground.0,
50+
};
51+
52+
if let Err(e) = stream_writer.send_packet(packet) {
53+
error!("Failed to send entity movement packet: {:?}", e);
54+
}
55+
}
56+
}
57+
58+
// Update last synced position
59+
*last_pos = LastSyncedPosition::from_position(pos);
60+
}
61+
}

src/bin/src/systems/entities/entity_spawner.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ pub fn entity_spawner_system(
1818
event
1919
.entity_type
2020
.spawn(&mut commands, entity_id, &event.position);
21-
info!("Spawned {:?} at {:?}", event.entity_type, event.position);
21+
info!(
22+
"Spawned {:?} with ID {} at ({:.2}, {:.2}, {:.2})",
23+
event.entity_type, entity_id, event.position.x, event.position.y, event.position.z
24+
);
2225
}
2326
}
2427

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,100 @@
11
use bevy_ecs::prelude::*;
2+
use ferrumc_core::transform::grounded::OnGround;
3+
use ferrumc_core::transform::position::Position;
4+
use ferrumc_core::transform::rotation::Rotation;
5+
use ferrumc_entities::components::Velocity;
26
use ferrumc_entities::types::passive::pig_data::PigData;
37
use ferrumc_entities::GameEntity;
48
use ferrumc_state::GlobalStateResource;
9+
use rand::Rng;
510

611
/// System that ticks pig entities to update their AI/behavior
712
pub fn pig_tick_system(
8-
mut pigs: Query<&mut PigData>,
13+
mut pigs: Query<(
14+
&mut PigData,
15+
&mut Velocity,
16+
&mut Rotation,
17+
&Position,
18+
&OnGround,
19+
)>,
920
state: Res<GlobalStateResource>,
1021
mut commands: Commands,
1122
) {
12-
for mut pig_data in pigs.iter_mut() {
23+
for (mut pig_data, mut velocity, mut rotation, position, on_ground) in pigs.iter_mut() {
24+
// Call the entity's tick method for entity-specific behavior
1325
pig_data.tick(&state.0, &mut commands);
26+
27+
// Basic AI: Random wandering when on ground
28+
if on_ground.0 {
29+
let mut rng = rand::rng();
30+
31+
// Check for obstacle first - if blocked, try to jump or change direction
32+
if should_avoid_obstacle(&state.0, position, velocity.x, velocity.z) {
33+
// 50% chance to try jumping over obstacle, 50% chance to turn around
34+
if rng.random_bool(0.5) && velocity.y.abs() < 0.01 {
35+
velocity.y = 0.42; // Jump to try to get over obstacle
36+
} else {
37+
// Pick a new random direction when hitting a wall
38+
let angle = rng.random_range(0.0..std::f64::consts::TAU);
39+
velocity.x = angle.cos() * 0.25;
40+
velocity.z = angle.sin() * 0.25;
41+
rotation.yaw = (-velocity.x.atan2(velocity.z).to_degrees()) as f32;
42+
}
43+
} else {
44+
// Only 1% chance to change direction when not blocked (less rotation)
45+
if rng.random_bool(0.01) {
46+
let angle = rng.random_range(0.0..std::f64::consts::TAU);
47+
velocity.x = angle.cos() * 0.25;
48+
velocity.z = angle.sin() * 0.25;
49+
rotation.yaw = (-velocity.x.atan2(velocity.z).to_degrees()) as f32;
50+
}
51+
}
52+
}
1453
}
1554
}
55+
56+
/// Check if there's an obstacle in the movement direction
57+
fn should_avoid_obstacle(
58+
state: &ferrumc_state::GlobalState,
59+
pos: &Position,
60+
vel_x: f64,
61+
vel_z: f64,
62+
) -> bool {
63+
// Pig hitbox is approximately 0.9 x 0.9 x 0.9 blocks
64+
// Check multiple points around the entity's bounding box
65+
66+
let check_distance = 0.6; // Look slightly ahead
67+
let next_x = pos.x + vel_x.signum() * check_distance;
68+
let next_z = pos.z + vel_z.signum() * check_distance;
69+
70+
// Check at feet level and head level (entity is ~0.9 blocks tall)
71+
let check_positions = [
72+
// Feet level
73+
(
74+
next_x.floor() as i32,
75+
pos.y.floor() as i32,
76+
next_z.floor() as i32,
77+
),
78+
// Head level
79+
(
80+
next_x.floor() as i32,
81+
(pos.y + 0.5).floor() as i32,
82+
next_z.floor() as i32,
83+
),
84+
];
85+
86+
for (check_x, check_y, check_z) in check_positions {
87+
if let Ok(block_state) =
88+
state
89+
.world
90+
.get_block_and_fetch(check_x, check_y, check_z, "overworld")
91+
{
92+
// If there's a solid block, obstacle detected
93+
if block_state.0 != 0 {
94+
return true;
95+
}
96+
}
97+
}
98+
99+
false
100+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
use bevy_ecs::prelude::*;
2+
use ferrumc_core::transform::grounded::OnGround;
3+
use ferrumc_core::transform::position::Position;
4+
use ferrumc_entities::EntityType;
5+
use ferrumc_state::GlobalStateResource;
6+
7+
/// System that checks if entities are on the ground
8+
/// Updates the OnGround component based on the block below the entity
9+
pub fn ground_check_system(
10+
mut query: Query<(&Position, &mut OnGround), With<EntityType>>,
11+
state: Res<GlobalStateResource>,
12+
) {
13+
for (pos, mut on_ground) in query.iter_mut() {
14+
let block_x = pos.x.floor() as i32;
15+
let block_y = (pos.y - 0.1).floor() as i32; // Slightly below feet
16+
let block_z = pos.z.floor() as i32;
17+
18+
match state
19+
.0
20+
.world
21+
.get_block_and_fetch(block_x, block_y, block_z, "overworld")
22+
{
23+
Ok(block_state) => {
24+
// Block ID 0 is air, anything else is solid
25+
// TODO: Check for specific non-solid blocks (water, lava, tall grass, etc.)
26+
on_ground.0 = block_state.0 != 0;
27+
}
28+
Err(_) => {
29+
// Chunk not loaded, assume in air
30+
on_ground.0 = false;
31+
}
32+
}
33+
}
34+
}
Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
pub mod entity_movement;
2+
pub mod entity_movement_sync;
23
pub mod entity_spawner;
34
pub mod entity_sync;
45
pub mod entity_tick;
6+
pub mod ground_check;
57
pub mod spawn_command_processor;
68

79
use bevy_ecs::schedule::Schedule;
@@ -11,9 +13,11 @@ pub fn register_entity_systems(schedule: &mut Schedule) {
1113
schedule.add_systems((
1214
spawn_command_processor::spawn_command_processor_system, // Process spawn commands from /spawnpig
1315
entity_spawner::entity_spawner_system,
14-
entity_tick::pig_tick_system, // Tick AI/behavior for pigs
15-
entity_movement::entity_physics_system,
16+
ground_check::ground_check_system, // Check if entities are on ground
17+
entity_tick::pig_tick_system, // Tick AI/behavior for pigs
18+
entity_movement::entity_physics_system, // Apply physics (gravity, movement)
1619
entity_movement::entity_age_system,
17-
entity_sync::entity_sync_system,
20+
entity_sync::entity_sync_system, // Sync new entities to clients
21+
entity_movement_sync::entity_movement_sync_system, // Sync entity movement to clients
1822
));
1923
}

0 commit comments

Comments
 (0)