Add ant sprite animation + improve random walk
Ants now randomize the velocity derivative instead of the velocity directly
This commit is contained in:
BIN
assets/sprites/ant_walk_anim.png
Normal file
BIN
assets/sprites/ant_walk_anim.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 716 KiB |
@ -1,5 +1,11 @@
|
|||||||
use super::common::{Position, RandomizedVelocity};
|
use super::common::{
|
||||||
|
AnimationIndices, AnimationTimer, Position, RandomizedVelocityChange, Velocity,
|
||||||
|
VelocityChangeTimer,
|
||||||
|
};
|
||||||
|
use crate::{ANT_ANIMATION_SPEED, VELOCITY_CHANGE_PERIOD, VELOCITY_CHANGE_SCALE};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
/// This bundle represents the ant entity.
|
/// This bundle represents the ant entity.
|
||||||
/// The ``state`` determines the ant's behavior, it stores its current ``position``
|
/// The ``state`` determines the ant's behavior, it stores its current ``position``
|
||||||
@ -13,7 +19,14 @@ use bevy::prelude::*;
|
|||||||
pub struct AntBundle {
|
pub struct AntBundle {
|
||||||
pub state: AntState,
|
pub state: AntState,
|
||||||
pub position: Position,
|
pub position: Position,
|
||||||
pub velocity: RandomizedVelocity,
|
pub velocity: Velocity,
|
||||||
|
pub velocity_change: RandomizedVelocityChange,
|
||||||
|
// TODO: This timer is only needed once
|
||||||
|
pub velocity_change_timer: VelocityChangeTimer,
|
||||||
|
pub texture_atlas: TextureAtlas,
|
||||||
|
pub animation_indices: AnimationIndices,
|
||||||
|
// TODO: This timer is only needed once
|
||||||
|
pub animation_timer: AnimationTimer,
|
||||||
|
|
||||||
// These are not components, but other bundles of components. Those can be nested.
|
// These are not components, but other bundles of components. Those can be nested.
|
||||||
pub sprite: SpriteBundle,
|
pub sprite: SpriteBundle,
|
||||||
@ -42,17 +55,37 @@ pub enum AntState {
|
|||||||
// The "impl" keyword allows us to implement functions in a struct's namespace, i.e. "methods"
|
// The "impl" keyword allows us to implement functions in a struct's namespace, i.e. "methods"
|
||||||
impl AntBundle {
|
impl AntBundle {
|
||||||
/// Instantiate a new ``AntBundle`` with a color and a starting position.
|
/// Instantiate a new ``AntBundle`` with a color and a starting position.
|
||||||
pub fn new(position: Vec2, color: Color) -> Self {
|
pub fn new(
|
||||||
|
position: Vec2,
|
||||||
|
texture: Handle<Image>,
|
||||||
|
texture_atlas: TextureAtlas,
|
||||||
|
animation_indices: AnimationIndices,
|
||||||
|
scale: Vec2,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
state: AntState::Searching,
|
state: AntState::Searching,
|
||||||
position: Position(position),
|
position: Position(position),
|
||||||
velocity: RandomizedVelocity(Vec2::ZERO),
|
velocity: Velocity(rand::thread_rng().gen_range(-PI..PI)),
|
||||||
|
velocity_change: RandomizedVelocityChange(
|
||||||
|
rand::thread_rng().gen_range(-1.0..1.0) * VELOCITY_CHANGE_SCALE,
|
||||||
|
),
|
||||||
|
velocity_change_timer: VelocityChangeTimer(Timer::from_seconds(
|
||||||
|
VELOCITY_CHANGE_PERIOD,
|
||||||
|
TimerMode::Repeating,
|
||||||
|
)),
|
||||||
|
texture_atlas,
|
||||||
|
animation_indices,
|
||||||
|
animation_timer: AnimationTimer(Timer::from_seconds(
|
||||||
|
ANT_ANIMATION_SPEED,
|
||||||
|
TimerMode::Repeating,
|
||||||
|
)),
|
||||||
sprite: SpriteBundle {
|
sprite: SpriteBundle {
|
||||||
transform: Transform {
|
transform: Transform {
|
||||||
scale: Vec3::new(20., 20., 20.),
|
translation: position.extend(0.),
|
||||||
|
scale: scale.extend(1.),
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
sprite: Sprite { color, ..default() },
|
texture,
|
||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,19 @@ use bevy::prelude::*;
|
|||||||
pub struct Position(pub Vec2);
|
pub struct Position(pub Vec2);
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct Velocity(pub Vec2);
|
pub struct Velocity(pub f32);
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct RandomizedVelocity(pub Vec2);
|
pub struct RandomizedVelocityChange(pub f32);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct AnimationIndices {
|
||||||
|
pub first: usize,
|
||||||
|
pub last: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct AnimationTimer(pub Timer);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct VelocityChangeTimer(pub Timer);
|
||||||
|
27
src/main.rs
27
src/main.rs
@ -4,24 +4,33 @@ mod systems;
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use std::f32::consts::PI;
|
use std::f32::consts::PI;
|
||||||
use systems::{
|
use systems::{
|
||||||
random_walk::{random_walk_system, randomized_velocity_system, wall_avoidance_system},
|
ant::{
|
||||||
|
animation_system, position_update_system, randomized_velocity_change_system,
|
||||||
|
randomized_velocity_system, wall_avoidance_system,
|
||||||
|
},
|
||||||
startup::{hello_ants_system, setup_system},
|
startup::{hello_ants_system, setup_system},
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: Use Settings Resource
|
// TODO: Use Settings Resource
|
||||||
const MIN_POSITION: Vec2 = Vec2::ZERO;
|
const MIN_POSITION: Vec2 = Vec2::splat(-450.);
|
||||||
const MAX_POSITION: Vec2 = Vec2::new(500., 500.);
|
const MAX_POSITION: Vec2 = Vec2::splat(450.);
|
||||||
|
|
||||||
const ANT_COUNT: u32 = 25;
|
const ANT_COUNT: u32 = 200;
|
||||||
const ANT_SPEED: f32 = 0.75;
|
const ANT_SPEED: f32 = 0.5;
|
||||||
const RANDOM_WALK_CONE: f32 = PI / 180. * 20.;
|
const ANT_ANIMATION_SPEED: f32 = 1. / 62.;
|
||||||
|
const ANT_SCALE: f32 = 0.15;
|
||||||
|
|
||||||
|
const VELOCITY_CHANGE_SCALE: f32 = PI / 180. * 1.; // Single degree
|
||||||
|
const VELOCITY_CHANGE_MAX: f32 = PI / 180. * 0.5;
|
||||||
|
const VELOCITY_CHANGE_PERIOD: f32 = 0.5;
|
||||||
|
|
||||||
/// The app's entrypoint.
|
/// The app's entrypoint.
|
||||||
fn main() {
|
fn main() {
|
||||||
let mut app = App::new();
|
let mut app = App::new();
|
||||||
|
|
||||||
// The DefaultPlugins contain the "Window" plugin.
|
// The DefaultPlugins contain the "Window" plugin.
|
||||||
app.add_plugins(DefaultPlugins);
|
// ImagePlugin::default_nearest() is supposed to prevent blurry sprites.
|
||||||
|
app.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()));
|
||||||
|
|
||||||
// Sets the color used to clear the screen, i.e. the background color.
|
// Sets the color used to clear the screen, i.e. the background color.
|
||||||
app.insert_resource(ClearColor(Color::srgb(0.9, 0.9, 0.9)));
|
app.insert_resource(ClearColor(Color::srgb(0.9, 0.9, 0.9)));
|
||||||
@ -38,9 +47,11 @@ fn main() {
|
|||||||
app.add_systems(
|
app.add_systems(
|
||||||
FixedUpdate,
|
FixedUpdate,
|
||||||
(
|
(
|
||||||
|
randomized_velocity_change_system,
|
||||||
randomized_velocity_system,
|
randomized_velocity_system,
|
||||||
wall_avoidance_system,
|
wall_avoidance_system,
|
||||||
random_walk_system,
|
position_update_system,
|
||||||
|
animation_system,
|
||||||
)
|
)
|
||||||
.chain(),
|
.chain(),
|
||||||
);
|
);
|
||||||
|
96
src/systems/ant.rs
Normal file
96
src/systems/ant.rs
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
use crate::{
|
||||||
|
components::common::{
|
||||||
|
AnimationIndices, AnimationTimer, Position, RandomizedVelocityChange, Velocity,
|
||||||
|
VelocityChangeTimer,
|
||||||
|
},
|
||||||
|
ANT_SPEED, MAX_POSITION, MIN_POSITION, VELOCITY_CHANGE_MAX, VELOCITY_CHANGE_SCALE,
|
||||||
|
};
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use rand::Rng;
|
||||||
|
use std::f32::consts::PI;
|
||||||
|
|
||||||
|
/// Randomizes the ``RandomizeVelocityChange`` components.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn randomized_velocity_change_system(
|
||||||
|
mut query: Query<(&mut RandomizedVelocityChange, &mut VelocityChangeTimer)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (mut velocity_change, mut timer) in &mut query {
|
||||||
|
timer.0.tick(time.delta());
|
||||||
|
|
||||||
|
if timer.0.just_finished() {
|
||||||
|
velocity_change.0 += rand::thread_rng().gen_range(-1.0..1.0) * VELOCITY_CHANGE_SCALE;
|
||||||
|
velocity_change.0 = velocity_change
|
||||||
|
.0
|
||||||
|
.clamp(-VELOCITY_CHANGE_MAX, VELOCITY_CHANGE_MAX);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the ``Velocity`` components for entities that have a ``RandomizedVelocityChange``.
|
||||||
|
pub fn randomized_velocity_system(
|
||||||
|
mut query: Query<(&mut Transform, &mut Velocity, &RandomizedVelocityChange)>,
|
||||||
|
) {
|
||||||
|
for (mut transform, mut velocity, velocity_change) in &mut query {
|
||||||
|
velocity.0 += velocity_change.0;
|
||||||
|
transform.rotation = Quat::from_euler(EulerRot::XYZ, 0., 0., velocity.0 - PI / 2.);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates ``Position`` and ``Transform`` components of entities that also have
|
||||||
|
/// the ``RandomizedVelocity`` component, like an ``Ant``.
|
||||||
|
pub fn position_update_system(
|
||||||
|
// We query for each entity that has a Position, Transform and Velocity component.
|
||||||
|
// The Transform component comes from the SpriteBundle.
|
||||||
|
mut query: Query<(&mut Position, &mut Transform, &Velocity)>,
|
||||||
|
) {
|
||||||
|
for (mut position, mut transform, velocity) in &mut query {
|
||||||
|
let new_position: Vec2 = position.0 + Vec2::from_angle(velocity.0).normalize() * ANT_SPEED;
|
||||||
|
|
||||||
|
transform.translation = new_position.clamp(MIN_POSITION, MAX_POSITION).extend(0.);
|
||||||
|
position.0 = new_position.clamp(MIN_POSITION, MAX_POSITION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets an ant's velocity perpendicular to a wall, if touched.
|
||||||
|
pub fn wall_avoidance_system(mut query: Query<(&Position, &mut Velocity)>) {
|
||||||
|
let touches_left_wall = |position: Vec2| -> bool { position.x <= MIN_POSITION.x };
|
||||||
|
let touches_right_wall = |position: Vec2| -> bool { position.x >= MAX_POSITION.x };
|
||||||
|
let touches_bottom_wall = |position: Vec2| -> bool { position.y <= MIN_POSITION.y };
|
||||||
|
let touches_top_wall = |position: Vec2| -> bool { position.y >= MAX_POSITION.y };
|
||||||
|
|
||||||
|
for (position, mut velocity) in &mut query {
|
||||||
|
let mut new_velocity: f32 = velocity.0;
|
||||||
|
|
||||||
|
if touches_left_wall(position.0) {
|
||||||
|
new_velocity = 0.;
|
||||||
|
} else if touches_right_wall(position.0) {
|
||||||
|
new_velocity = PI;
|
||||||
|
} else if touches_bottom_wall(position.0) {
|
||||||
|
new_velocity = PI / 2.;
|
||||||
|
} else if touches_top_wall(position.0) {
|
||||||
|
new_velocity = 3. * PI / 2.;
|
||||||
|
}
|
||||||
|
|
||||||
|
velocity.0 = new_velocity;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Animates the ants by incrementing their ``TextureAtlas`` indices, based on a timer.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn animation_system(
|
||||||
|
mut query: Query<(&AnimationIndices, &mut AnimationTimer, &mut TextureAtlas)>,
|
||||||
|
time: Res<Time>,
|
||||||
|
) {
|
||||||
|
for (indices, mut timer, mut atlas) in &mut query {
|
||||||
|
timer.0.tick(time.delta());
|
||||||
|
|
||||||
|
if timer.0.just_finished() {
|
||||||
|
atlas.index = if atlas.index == indices.last {
|
||||||
|
indices.first
|
||||||
|
} else {
|
||||||
|
atlas.index + 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,2 +1,2 @@
|
|||||||
pub mod random_walk;
|
pub mod ant;
|
||||||
pub mod startup;
|
pub mod startup;
|
||||||
|
@ -1,65 +0,0 @@
|
|||||||
use crate::{
|
|
||||||
components::common::{Position, RandomizedVelocity},
|
|
||||||
ANT_SPEED, MAX_POSITION, MIN_POSITION, RANDOM_WALK_CONE,
|
|
||||||
};
|
|
||||||
use bevy::prelude::*;
|
|
||||||
|
|
||||||
/// Randomizes the ``RandomizedVelocity`` components of entities that have them.
|
|
||||||
#[allow(clippy::needless_pass_by_value)] // I can't specify &Res<Time>, so pass by value
|
|
||||||
pub fn randomized_velocity_system(mut query: Query<&mut RandomizedVelocity>) {
|
|
||||||
for mut velocity in &mut query {
|
|
||||||
let mut new_velocity: Vec2 = Vec2::from_angle(
|
|
||||||
// We first multiply with CONE to get the angle-delta to [0, CONE].
|
|
||||||
// Then, we subtract CONE/2 to bring the angle-delta to [-CONE/2, CONE/2].
|
|
||||||
// Lastly, we add the angle-delta to the current angle.
|
|
||||||
// This should vary the direction in a front-facing CONE-degree cone.
|
|
||||||
rand::random::<f32>().mul_add(
|
|
||||||
RANDOM_WALK_CONE,
|
|
||||||
velocity.0.to_angle() - RANDOM_WALK_CONE / 2.,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
new_velocity = new_velocity.normalize() * ANT_SPEED;
|
|
||||||
|
|
||||||
velocity.0 = new_velocity;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates ``Position`` and ``Transform`` components of entities that also have
|
|
||||||
/// the ``RandomizedVelocity`` component, like an ``Ant``.
|
|
||||||
#[allow(clippy::module_name_repetitions)]
|
|
||||||
pub fn random_walk_system(
|
|
||||||
// We query for each entity that has a Position, Transform and Velocity component.
|
|
||||||
// The Transform component comes from the SpriteBundle.
|
|
||||||
mut query: Query<(&mut Position, &mut Transform, &RandomizedVelocity)>,
|
|
||||||
) {
|
|
||||||
for (mut position, mut transform, velocity) in &mut query {
|
|
||||||
let new_position: Vec2 = position.0 + velocity.0;
|
|
||||||
|
|
||||||
transform.translation = new_position.clamp(MIN_POSITION, MAX_POSITION).extend(0.);
|
|
||||||
position.0 = new_position.clamp(MIN_POSITION, MAX_POSITION);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets an ant's velocity perpendicular to a wall, if touched.
|
|
||||||
pub fn wall_avoidance_system(mut query: Query<(&Position, &mut RandomizedVelocity)>) {
|
|
||||||
let touches_left_wall = |position: Vec2| -> bool { position.x <= 0. };
|
|
||||||
let touches_right_wall = |position: Vec2| -> bool { position.x >= MAX_POSITION.x };
|
|
||||||
let touches_bottom_wall = |position: Vec2| -> bool { position.y <= 0. };
|
|
||||||
let touches_top_wall = |position: Vec2| -> bool { position.y >= MAX_POSITION.y };
|
|
||||||
|
|
||||||
for (position, mut velocity) in &mut query {
|
|
||||||
let mut new_velocity: Vec2 = velocity.0;
|
|
||||||
|
|
||||||
if touches_left_wall(position.0) {
|
|
||||||
new_velocity = Vec2::new(1., 0.);
|
|
||||||
} else if touches_right_wall(position.0) {
|
|
||||||
new_velocity = Vec2::new(-1., 0.);
|
|
||||||
} else if touches_bottom_wall(position.0) {
|
|
||||||
new_velocity = Vec2::new(0., 1.);
|
|
||||||
} else if touches_top_wall(position.0) {
|
|
||||||
new_velocity = Vec2::new(0., -1.);
|
|
||||||
}
|
|
||||||
|
|
||||||
velocity.0 = new_velocity;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,5 +1,9 @@
|
|||||||
use crate::{components::ant::AntBundle, ANT_COUNT};
|
use crate::{
|
||||||
|
components::{ant::AntBundle, common::AnimationIndices},
|
||||||
|
ANT_COUNT, ANT_SCALE, MAX_POSITION,
|
||||||
|
};
|
||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
/// Signal that the app has started by printing a message to the terminal.
|
/// Signal that the app has started by printing a message to the terminal.
|
||||||
pub fn hello_ants_system() {
|
pub fn hello_ants_system() {
|
||||||
@ -7,14 +11,36 @@ pub fn hello_ants_system() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Prepares the environment before the Update schedule by spawning required entities.
|
/// Prepares the environment before the Update schedule by spawning required entities.
|
||||||
pub fn setup_system(mut commands: Commands) {
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub fn setup_system(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
|
||||||
|
) {
|
||||||
// Using the default camera, world space coordinates correspond 1:1 with screen pixels.
|
// Using the default camera, world space coordinates correspond 1:1 with screen pixels.
|
||||||
// The point (0, 0) is in the center of the screen.
|
// The point (0, 0) is in the center of the screen.
|
||||||
commands.spawn(Camera2dBundle::default());
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
|
||||||
// Spawn cute ants
|
// Spawn cute ants
|
||||||
|
// https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs
|
||||||
|
// TODO: Some weird cloning here
|
||||||
|
let texture: Handle<Image> = asset_server.load("sprites/ant_walk_anim.png");
|
||||||
|
let layout = TextureAtlasLayout::from_grid(UVec2::new(202, 248), 8, 8, None, None);
|
||||||
|
let texture_atlas_layout = texture_atlas_layouts.add(layout);
|
||||||
for _ in 0..ANT_COUNT {
|
for _ in 0..ANT_COUNT {
|
||||||
commands.spawn(AntBundle::new(Vec2::new(0., 0.), Color::srgb(1., 0., 0.)));
|
let animation_indices = AnimationIndices { first: 0, last: 61 };
|
||||||
|
let texture_atlas = TextureAtlas {
|
||||||
|
layout: texture_atlas_layout.clone(),
|
||||||
|
index: rand::thread_rng().gen_range(animation_indices.first..=animation_indices.last),
|
||||||
|
};
|
||||||
|
|
||||||
|
commands.spawn(AntBundle::new(
|
||||||
|
Vec2::ZERO,
|
||||||
|
texture.clone(),
|
||||||
|
texture_atlas,
|
||||||
|
animation_indices,
|
||||||
|
Vec2::splat(ANT_SCALE),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
println!("Spawned {ANT_COUNT} cute ants 😍");
|
println!("Spawned {ANT_COUNT} cute ants 😍");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user