Sidequest 1: Basic Enemy AI
Now that we have our Velocity and Acceleration components, it's very easy to make our enemy do something. Of course, we want to specify they're an enemy, so we add a new tag component for it and update our enemy to spawn with it.
const ENEMY_MAX_VELOCITY: f32 = 100.;
const ENEMY_ACCELERATION: f32 = 1000.;
const ENEMY_DECELERATION: f32 = 1500.;
#[component]
struct Enemy;
#[system_once]
fn spawn_enemy() {
let transform = &Transform {
position: Vec2::new(-110., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.2, 0.2, 0.9, 0.9);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
let velocity = &Velocity {
max_velocity: ENEMY_MAX_VELOCITY,
..Default::default()
};
let acceleration = &Acceleration {
acceleration_rate: ENEMY_ACCELERATION,
deceleration_rate: ENEMY_DECELERATION,
..Default::default()
};
Engine::spawn(bundle!(
transform,
color,
circle_render,
&Enemy,
velocity,
acceleration
));
}
Now like any good enemy, ours should try to reach the player if they're within a certain range, and give up if the player gets too far away. But like many a good enemy, ours is shy and doesn't want to get too close to the object of their fixation, the player.
const ENEMY_CHASE_DISTANCE: f32 = 1000.;
const ENEMY_STOP_DISTANCE: f32 = 200.;
#[system]
fn update_enemy_acceleration(
mut enemy_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Transform>, Ref<Enemy>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
enemy_query.for_each(|(mut acceleration, velocity, transform, _)| {
let to_player = player_transform.position - transform.position;
let distance = to_player.length();
if distance > ENEMY_CHASE_DISTANCE {
acceleration.target_velocity = Vec2::ZERO;
} else {
let ideal_position =
player_transform.position - to_player.normalize() * ENEMY_STOP_DISTANCE;
let to_ideal = ideal_position - transform.position;
let distance_to_ideal = to_ideal.length();
if distance_to_ideal < 20.0 {
acceleration.target_velocity = Vec2::ZERO;
} else {
let speed_factor = if distance_to_ideal < 50.0 { 0.5 } else { 1.0 };
acceleration.target_velocity =
to_ideal.normalize_or_zero() * (velocity.max_velocity * speed_factor);
}
}
});
}
There's some magic numbers in there that could/should be consts, but this is just a little toy for now anyway. Now when you run the game, your enemy will actually interact with you! That means we've actually made a game for the first time!
We can celebrate by doing more homework now: adding a camera.