-
-
Notifications
You must be signed in to change notification settings - Fork 5
Features/pathfinding #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
9e64813
2e6b22a
ab4ff64
4f62469
a079fb7
51f78ab
bbe3dac
6165d49
0c22cc0
ad6a495
ba672c9
13eb519
80009f2
ed9a26c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| use std::sync::LazyLock; | ||
|
|
||
| use crate::block_data::BlockData; | ||
| use crate::block_state_id::{BlockStateId, ID2BLOCK}; | ||
|
|
||
| /// Precomputed solidity for all block states. | ||
| /// A block is solid if it has a full collision box that entities cannot walk through. | ||
| /// Indexed by `BlockStateId::raw()`. | ||
| static SOLID_BLOCKS: LazyLock<Vec<bool>> = | ||
| LazyLock::new(|| ID2BLOCK.iter().map(compute_solid).collect()); | ||
|
|
||
| /// Returns whether a block is solid (has a full collision box). | ||
| /// | ||
| /// This is the single source of truth for solidity, used by both the collision | ||
| /// system and the pathfinding system. | ||
| #[inline] | ||
| pub fn is_solid(id: BlockStateId) -> bool { | ||
| SOLID_BLOCKS[id.raw() as usize] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Blockstateids aren't checked to be valid, can we return false instead of panicking |
||
| } | ||
|
|
||
| /// Determine whether a block data entry represents a solid block. | ||
| fn compute_solid(data: &BlockData) -> bool { | ||
| let name = data.name.trim_start_matches("minecraft:"); | ||
|
|
||
| // Air variants are never solid | ||
| if name.ends_with("air") { | ||
| return false; | ||
| } | ||
|
|
||
| // Liquids | ||
| if matches!(name, "water" | "lava" | "bubble_column") { | ||
| return false; | ||
| } | ||
|
|
||
| // Fire | ||
| if matches!(name, "fire" | "soul_fire") { | ||
| return false; | ||
| } | ||
| if name.ends_with("_campfire") { | ||
| // Campfires are solid blocks you can stand on, but lit ones deal damage | ||
| return true; | ||
| } | ||
|
|
||
| // Doors, fence gates, trapdoors: solid only when closed | ||
| if name.ends_with("_door") || name.ends_with("_fence_gate") || name.ends_with("_trapdoor") { | ||
| let open = data | ||
| .properties | ||
| .as_ref() | ||
| .and_then(|p| p.get("open")) | ||
| .is_some_and(|v| v == "true"); | ||
| return !open; | ||
| } | ||
|
|
||
| // Non-solid vegetation and decorations | ||
| if is_non_solid_decoration(name) { | ||
| return false; | ||
| } | ||
|
|
||
| // Default: solid | ||
| true | ||
| } | ||
|
|
||
| /// Returns true for blocks that have no collision box (decorative, vegetation, etc.) | ||
| fn is_non_solid_decoration(name: &str) -> bool { | ||
| if matches!( | ||
| name, | ||
| "grass" | ||
| | "short_grass" | ||
| | "tall_grass" | ||
| | "fern" | ||
| | "large_fern" | ||
| | "dead_bush" | ||
| | "snow" | ||
| | "string" | ||
| | "nether_portal" | ||
| | "spore_blossom" | ||
| | "glow_lichen" | ||
| | "dandelion" | ||
| | "poppy" | ||
| | "blue_orchid" | ||
| | "allium" | ||
| | "azure_bluet" | ||
| | "oxeye_daisy" | ||
| | "cornflower" | ||
| | "lily_of_the_valley" | ||
| | "wither_rose" | ||
| | "sunflower" | ||
| | "lilac" | ||
| | "rose_bush" | ||
| | "peony" | ||
| | "torchflower" | ||
| | "pitcher_plant" | ||
| | "pitcher_pod" | ||
| | "sweet_berry_bush" | ||
| | "cobweb" | ||
| | "powder_snow" | ||
| | "redstone_wire" | ||
| | "rail" | ||
| | "powered_rail" | ||
| | "detector_rail" | ||
| | "activator_rail" | ||
| | "tripwire" | ||
| | "tripwire_hook" | ||
| | "structure_void" | ||
| ) { | ||
| return true; | ||
| } | ||
|
|
||
| name.ends_with("_button") | ||
| || name.ends_with("_pressure_plate") | ||
| || name.ends_with("_sign") | ||
| || name.ends_with("_banner") | ||
| || name.ends_with("_carpet") | ||
| || name.ends_with("_torch") | ||
| || name.ends_with("_sapling") | ||
| || name.ends_with("_mushroom") | ||
| || name.ends_with("_flower") | ||
| || name.ends_with("_vine") | ||
| || name.ends_with("_roots") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,10 @@ | ||
| use bevy_ecs::prelude::Schedule; | ||
| use bevy_ecs::schedule::IntoScheduleConfigs; | ||
|
|
||
| mod pig; | ||
|
|
||
| pub fn register_mob_systems(schedule: &mut Schedule) { | ||
| schedule.add_systems(pig::tick_pig); | ||
| schedule.add_systems(pathfinding::tick_pathfinder); | ||
| schedule.add_systems(pig::tick_pig.after(pathfinding::tick_pathfinder)); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can we its weird, but this would actually run the pathfinder twice cos the |
||
| schedule.add_systems(pig::tick_pig_particles); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,138 @@ | ||
| use bevy_ecs::prelude::{Query, With}; | ||
| use bevy_ecs::prelude::*; | ||
| use bevy_math::Vec3A; | ||
| use pathfinding::{Pathfinder, pos_to_block}; | ||
| use temper_components::player::grounded::OnGround; | ||
| use temper_components::player::player_identity::PlayerIdentity; | ||
| use temper_components::player::position::Position; | ||
| use temper_components::player::velocity::Velocity; | ||
| use temper_entities::markers::entity_types::Pig; | ||
| use temper_messages::particle::SendParticle; | ||
| use temper_particles::ParticleType; | ||
|
|
||
| /// Pig walk speed in blocks per tick. | ||
| const PIG_WALK_SPEED: f32 = 0.1; | ||
|
|
||
| /// Jump impulse matching Minecraft's standard jump velocity (blocks/tick). | ||
| const JUMP_IMPULSE: f32 = 0.42; | ||
|
|
||
| /// How often to update the pathfinding target (ticks). | ||
| const REPATH_INTERVAL: u32 = 40; | ||
|
|
||
| /// Per-pig AI state. | ||
| #[derive(Component, Default)] | ||
| pub struct PigAI { | ||
| repath_cooldown: u32, | ||
| } | ||
|
|
||
| type PigQuery<'a> = ( | ||
| Entity, | ||
| &'a Position, | ||
| &'a mut Velocity, | ||
| &'a OnGround, | ||
| Option<&'a mut PigAI>, | ||
| Option<&'a mut Pathfinder>, | ||
| ); | ||
|
|
||
| #[expect(unused_variables)] | ||
| pub fn tick_pig( | ||
|
Tonguechaude marked this conversation as resolved.
|
||
| query: Query<&Position, With<Pig>>, | ||
| mut commands: Commands, | ||
| mut pigs: Query<PigQuery, With<Pig>>, | ||
| players: Query<&Position, With<PlayerIdentity>>, | ||
| ) { | ||
| for (entity, pig_pos, mut velocity, grounded, ai_opt, pf_opt) in pigs.iter_mut() { | ||
| let (Some(mut ai), Some(mut pathfinder)) = (ai_opt, pf_opt) else { | ||
| commands | ||
| .entity(entity) | ||
| .insert((PigAI::default(), Pathfinder::default())); | ||
| continue; | ||
| }; | ||
|
|
||
| ai.repath_cooldown = ai.repath_cooldown.saturating_sub(1); | ||
|
|
||
| // Repath when the cooldown expires OR when the pig has followed a path | ||
| // to its end (but not when pathfinding simply failed to find a route). | ||
| let path_reached_end = | ||
| !pathfinder.has_path() && !pathfinder.path.is_empty() && !pathfinder.is_searching(); | ||
|
|
||
| if ai.repath_cooldown == 0 || path_reached_end { | ||
| pathfinder.target = players | ||
| .iter() | ||
| .min_by(|a, b| { | ||
| pig_pos | ||
| .coords | ||
| .distance_squared(a.coords) | ||
| .total_cmp(&pig_pos.coords.distance_squared(b.coords)) | ||
| }) | ||
| .map(pos_to_block); | ||
| pathfinder.request_repath(); | ||
| ai.repath_cooldown = REPATH_INTERVAL; | ||
| } | ||
|
|
||
| let current_block = pos_to_block(pig_pos); | ||
|
|
||
| // Advance waypoint when the pig reaches it (same X/Z block). | ||
| if let Some(wp) = pathfinder.current_waypoint() | ||
| && wp.pos.x == current_block.pos.x | ||
| && wp.pos.z == current_block.pos.z | ||
| { | ||
| pathfinder.advance_waypoint(); | ||
| } | ||
|
|
||
| let Some(next) = pathfinder.current_waypoint() else { | ||
| stop(&mut velocity); | ||
| continue; | ||
| }; | ||
|
|
||
| // Jump if the next waypoint is 1 block above and the pig is on the ground. | ||
| if next.pos.y > current_block.pos.y && grounded.0 { | ||
| velocity.vec.y = JUMP_IMPULSE; | ||
| } | ||
|
|
||
| // Steer horizontally toward the center of the next waypoint block. | ||
| let dx = (next.pos.x as f64 + 0.5 - pig_pos.x) as f32; | ||
| let dz = (next.pos.z as f64 + 0.5 - pig_pos.z) as f32; | ||
| let len = (dx * dx + dz * dz).sqrt(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| if len > 0.1 { | ||
| velocity.vec.x = (dx / len) * PIG_WALK_SPEED; | ||
| velocity.vec.z = (dz / len) * PIG_WALK_SPEED; | ||
| } else { | ||
| velocity.vec.x = 0.0; | ||
| velocity.vec.z = 0.0; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn tick_pig_particles( | ||
| pigs: Query<(Entity, &Position), With<Pig>>, | ||
| players: Query<&Position, With<PlayerIdentity>>, | ||
| mut msgs: MessageWriter<SendParticle>, | ||
| ) { | ||
| for pos in pigs.iter() { | ||
| for player_pos in players.iter() { | ||
| let distance_sq = player_pos.as_vec3a().distance_squared(pos.1.as_vec3a()); | ||
| if distance_sq > 16.0 * 256.0 { | ||
| continue; | ||
| } | ||
| let steps = temper_utils::maths::step::step_between( | ||
| pos.1.as_vec3a(), | ||
| player_pos.coords.as_vec3a(), | ||
| 0.5, | ||
| ); | ||
| for step_pos in steps.iter().take(32) { | ||
| let particle_message = SendParticle { | ||
| particle_type: ParticleType::EndRod, | ||
| position: *step_pos, | ||
| offset: Vec3A::new(0.0, 0.0, 0.0), | ||
| speed: 0.0, | ||
| count: 1, | ||
| }; | ||
| msgs.write(particle_message); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn stop(velocity: &mut Velocity) { | ||
| velocity.vec.x = 0.0; | ||
| velocity.vec.z = 0.0; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| [package] | ||
| name = "pathfinding" | ||
| version = "0.1.0" | ||
| edition = "2024" | ||
|
|
||
| [dependencies] | ||
| temper-core = { workspace = true } | ||
| temper-world = { workspace = true } | ||
| temper-components = { workspace = true } | ||
| bevy_ecs = { workspace = true } | ||
| temper-entities = { workspace = true } | ||
| temper-state = { workspace = true } | ||
| rustc-hash = "2" | ||
| arrayvec = "0.7" | ||
|
|
||
| [dev-dependencies] | ||
| temper-state = { workspace = true } | ||
| temper-macros = { workspace = true } | ||
| temper-data = { workspace = true } | ||
| tempfile = { workspace = true } | ||
|
|
||
| [lints] | ||
| workspace = true |
Uh oh!
There was an error while loading. Please reload this page.