diff --git a/Cargo.toml b/Cargo.toml index 0de4a40..ee61dec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,8 +15,6 @@ opt-level = 3 [profile.dev] opt-level = 1 - - # The profile that 'cargo dist' will build with [profile.dist] inherits = "release" diff --git a/src/sheep.rs b/src/sheep.rs index f72fa58..e31e907 100644 --- a/src/sheep.rs +++ b/src/sheep.rs @@ -1,12 +1,12 @@ use std::f32::consts::PI; use bevy::prelude::*; -use rand::Rng; +use rand::{Rng, rngs::ThreadRng}; use crate::{ get_sprite_rotation, physics::{Velocity, WalkController}, - player::{Bark, DOG_SPEED}, + player::{Bark, DOG_SPEED, Dog}, safe_area::SafeArea, sprite_material::create_plane_mesh, test_level::LevelSize, @@ -21,8 +21,15 @@ const RANDOM_WALK_RANGE: f32 = 5.0; const RANDOM_WALK_ACCEPT_RADIUS: f32 = 0.5; const RANDOM_WALK_SPEED_MULTIPLIER: f32 = 0.2; -const IDLE_FEEDING_TIME: f32 = 1.0; -const IDLE_FEEDING_TIME_RANGE: f32 = 0.5; +//IDLE FEEDING must be large enough so that player can see sheep and react for escapes +const IDLE_FEEDING_TIME: f32 = 5.0; +const IDLE_FEEDING_TIME_RANGE: f32 = 1.5; + +const MOVE_IN_DIST: f32 = 11.0; +const MOVE_OUT_DIST: f32 = 10.0; + +const SCARE_RADIUS: f32 = 5.0; +const SCARE_MAX_DIST: f32 = 20.0; pub struct SheepPlugin; @@ -36,14 +43,17 @@ impl Plugin for SheepPlugin { //random walk app.add_event::() - .add_systems(Update, (init_random_walk, random_walk_system)); + .add_systems(Update, (init_random_walk, goto_system)); //idle feeding app.add_systems(Update, idle_feeding_system); // Move to safearea app.add_event::() - .add_systems(Update, (init_safeareawalk_walk, walk_to_safe_zone_system)); + .add_systems(Update, (init_safeareawalk_walk, )); + + app.add_event::() + .add_systems(Update, init_escape); app.register_type::() .register_type::() @@ -52,9 +62,7 @@ impl Plugin for SheepPlugin { } #[derive(Default, PartialEq, Debug, Clone, Component, Reflect)] -pub struct Sheep { - time: f32, -} +pub struct Sheep; #[derive(Default, PartialEq, Debug, Clone, Component, Reflect)] #[reflect(Component, Default)] @@ -66,11 +74,11 @@ pub struct IsScared { #[reflect(Component, Default)] pub enum Decision { #[default] - Idle, + Idle, //set sheep state to waiting next decision. Not just be idle. For stending we will use Feed or deleted IdleFeeding. We need to have transition states to move to next decision. Addition set of moves will be seen as sheep has plan to move and will be nicely waiting Feed, RandomWalk, MoveToSafeArea, - MoveOutSafeArea, + Escape, Scared, //Mark that sheep will not be able to another decision } @@ -84,12 +92,16 @@ impl Default for StateChance { fn default() -> Self { let mut res = Self { //set weights + //its weights are relative + //must be all between 0 and 1 + //normalization will be done automatically + + //For testing i made all weights 1 so all next states are equally likely to be chosen next_state: vec![ - (0.35, Decision::Idle), - (0.5, Decision::Feed), //zero values for unimplemented things - (0.1, Decision::RandomWalk), - (0.7, Decision::MoveToSafeArea), - (1.0, Decision::MoveOutSafeArea), + (1.0, Decision::Feed), //zero values for unimplemented things + (1.0, Decision::RandomWalk), + (1.0, Decision::MoveToSafeArea), + (0.5, Decision::Escape), ], }; res.normalize(); @@ -107,7 +119,19 @@ impl StateChance { for (w, _) in &mut self.next_state { *w /= sum; } - self.next_state.sort_by(|a, b| a.0.total_cmp(&b.0)); + } + + //I separated next decision selection to function + fn select_next(&self, rng : &mut ThreadRng) -> Decision { + let mut sum = 0.0; //This decisicion selection is based on weights, not prop graph. Just for testing and more stable behavior. Dont change please + let p = rng.gen_range(0.0..1.0); + for (w, d) in &self.next_state { + sum += *w; + if p < sum { + return *d; + } + } + return Decision::Idle; } } @@ -127,9 +151,8 @@ pub fn scared_sheeps( commands .entity(sheep.0) .insert(scare) - .remove::() .remove::() - .remove::(); + .remove::(); *sheep.3 = Decision::Scared; } } @@ -148,12 +171,7 @@ pub struct SafeAreaWalk { } #[derive(Component)] -pub struct RandomWalk { - pub target: Vec3, -} - -#[derive(Component)] -pub struct SafeAreaTarget { +pub struct GoTo { pub target: Vec3, } @@ -169,7 +187,7 @@ fn init_random_walk( let r = rand.gen_range(0.0..RANDOM_WALK_RANGE); let angle = rand.gen_range(0.0..PI * 2.0); - commands.entity(ev.e).insert(RandomWalk { + commands.entity(ev.e).insert(GoTo { target: t.translation + Vec3::new(angle.cos() * r, 0.0, angle.sin() * r), }); } @@ -177,41 +195,20 @@ fn init_random_walk( event_reader.clear(); } -fn init_safeareawalk_walk( - mut commands: Commands, - mut event_reader: EventReader, - poses: Query<&Transform, With>, - safeareas: Query<&SafeArea>, - level_size: Res, -) { - let Ok(safearea) = safeareas.get_single() else { - return; - }; - - for ev in event_reader.read() { - if poses.get_component::(ev.e).is_ok() { - commands.entity(ev.e).insert(SafeAreaTarget { - target: safearea.get_random_point_inside(level_size.0), - }); - } - } - event_reader.clear(); -} - -fn random_walk_system( +fn goto_system( mut commands: Commands, - mut random_walks: Query<( + mut goto_query: Query<( Entity, &mut Transform, &mut WalkController, &mut Decision, - &RandomWalk, + &GoTo, )>, ) { - for (e, t, mut v, mut dec, rw) in &mut random_walks.iter_mut() { + for (e, t, mut v, mut dec, rw) in &mut goto_query.iter_mut() { if t.translation.distance(rw.target) < RANDOM_WALK_ACCEPT_RADIUS { v.target_velocity = Vec3::ZERO; - commands.entity(e).remove::(); + commands.entity(e).remove::(); *dec = Decision::Idle; } else { v.target_velocity = (rw.target - t.translation).normalize() @@ -221,27 +218,29 @@ fn random_walk_system( } } -fn walk_to_safe_zone_system( + + +fn init_safeareawalk_walk( mut commands: Commands, - mut safe_walks: Query<( - Entity, - &mut Transform, - &mut WalkController, - &mut Decision, - &SafeAreaTarget, - )>, + mut event_reader: EventReader, + poses: Query<&Transform, With>, + safeareas: Query<&SafeArea>, + level_size: Res, ) { - for (e, t, mut v, mut dec, rw) in &mut safe_walks.iter_mut() { - if t.translation.distance(rw.target) < RANDOM_WALK_ACCEPT_RADIUS { - v.target_velocity = Vec3::ZERO; - commands.entity(e).remove::(); - *dec = Decision::Idle; - } else { - v.target_velocity = (rw.target - t.translation).normalize() - * SHEEP_SPEED - * RANDOM_WALK_SPEED_MULTIPLIER; + let Ok(safearea) = safeareas.get_single() else { + return; + }; + + for ev in event_reader.read() { + if let Ok(t) = poses.get_component::(ev.e) { + let inside_point = safearea.get_random_point_inside(level_size.0 / 3.0); + let dir = (inside_point - t.translation).normalize_or_zero(); + commands.entity(ev.e).insert(GoTo { + target: t.translation + dir * MOVE_IN_DIST, // move to near center, so move will be safe, opposite to RandomWalk or Move out safe zone + }); } } + event_reader.clear(); } pub fn sheep_state( @@ -251,19 +250,12 @@ pub fn sheep_state( mut sheeps: Query<(Entity, &mut Decision, &mut Sheep), Without>, mut init_random_walk: EventWriter, mut init_safe_walk: EventWriter, + mut init_escape_walk: EventWriter, ) { let mut rand = rand::thread_rng(); for (e, mut dec, mut sheep) in &mut sheeps.iter_mut() { - sheep.time += time.delta_seconds(); - if *dec == Decision::Idle && sheep.time > 1.5 * IDLE_FEEDING_TIME { - sheep.time = 0.; - let p = rand.gen_range(0.0..1.0); - let next_dec = state_matrix - .next_state - .iter() - .find(|state| state.0 > p) - .map(|s| s.1) - .unwrap_or_default(); + if *dec == Decision::Idle { + let next_dec = state_matrix.select_next(&mut rand); *dec = next_dec; @@ -283,9 +275,9 @@ pub fn sheep_state( Decision::MoveToSafeArea => { init_safe_walk.send(SafeAreaWalk { e }); } - Decision::MoveOutSafeArea => { + Decision::Escape => { // For now this seems ok - init_random_walk.send(InitRandomWalk { e }); + init_escape_walk.send(EscapeWalk { e }); } Decision::Scared => { *dec = Decision::Idle; @@ -295,18 +287,92 @@ pub fn sheep_state( } } +#[derive(Event)] +pub struct EscapeWalk { + pub e: Entity, +} + +pub fn init_escape( + mut commands: Commands, + mut event_reader: EventReader, + poses: Query<&Transform, With>, + safe_zones: Query<&SafeArea>, +) { + for ev in event_reader.read() { + if let Ok(t) = poses.get_component::(ev.e) { + //find nearest safe zone + let mut nearest = None; + let mut nearest_dist = f32::MAX; + + for sa in safe_zones.iter() { + let dist = t.translation.distance(sa.get_center()); + if dist < nearest_dist { + nearest = Some(sa); + nearest_dist = dist; + } + } + + if let Some(sa) = nearest { + let dir = (t.translation - sa.get_center()).normalize_or_zero(); + info!("escape {:?}", t.translation); + commands.entity(ev.e).insert(GoTo { + target: t.translation + dir * MOVE_OUT_DIST, + }); + } + } + } + event_reader.clear(); +} + pub fn update_scared_sheeps( mut commands: Commands, time: Res