Wrong Turn at the Arcane Convention
I had just finished reading the Unofficial Bevy Cheat Book in December of 2023 because I was interested in trying the Bevy game engine at the time. I then saw a public discord message by Tantan looking for people to join him in a game jam using bevy and decided send a request.
I got accepted and worked on the game for ~3 days along with Tantan and another programmer called Mini. I worked on the camera movement code, refactored the project sturcture multiple times, created a code based prefab system for creating and spawning entities, and I made approximately 90% of all the AI systems in the game.
The AI system works by adding components to create unique AI behaviors. An example of this is the fire and mage/human enemies in the game. The Fire enemy uses ChasePlayer while the mage/human uses Archor which both control how they approach the player.
The AI system works by feeding data along in a chain with some components being swapable or having different starting/ending points in the imaginary chain. Example: Perception -> (ChasePlayer/Archer) -> MovementDirection.
// Code snippet of fire enemies AI components
Name::new("enemy - fire element"),
Perception::new(90.0, 110.0),
MovementDirection::default(),
AttackInput::default(),
FireElement {
timer: Timer::from_seconds(rng.gen_range(3..10) as f32, TimerMode::Once),
attack_type_toggle: true,
},
ChasePlayer,
MeleeAttack {
range: 20.0,
timer: Timer::from_seconds(0.1, TimerMode::Once),
strength: 1_000.0,
},
Roam {
idle_chance: 0.0005,
change_direction_chance: 0.001,
}
// Code snippet of human enemies AI components
Name::new("enemy - skilled mage"),
Knockback::new(0.9),
Perception::new(120.0, 160.0),
MovementDirection::default(),
Archer {
range: 110.0,
dead_zone: 10.0,
},
AttackInput::default(),
Roam {
idle_chance: 0.0005,
change_direction_chance: 0.001,
},
Weapons::default(),
WeaponRange(115.0),
// AI Code from chase_player.rs
use crate::*;
use bevy::prelude::*;
#[derive(Debug, Component)]
pub struct ChasePlayer;
pub fn chase_player_when_perceived(
mut entities: Query<
(&mut MovementDirection, &Perception, &Transform),
(With<EntityCore<Enemy>>, With<ChasePlayer>),
>,
player: Query<&Transform, With<EntityCore<Player>>>,
) {
if let Ok(player) = player.get_single() {
for (mut move_direction, perception, transform) in entities.iter_mut() {
if perception.is_tracking {
let direction: Vec2 = player.translation.xy() - transform.translation.xy();
**move_direction = direction.normalize_or_zero();
}
}
};
}
// AI Code from archer.rs
use crate::*;
use bevy::prelude::*;
/// Archer takes a number representing how close they'll move to the player.
/// If the player gets to close, they'll move away in the opposite direction.
#[derive(Debug, Component)]
pub struct Archer {
// How close it will allow the player to get before moving away.
pub range: f32,
// The tolerance amount added to the range where the AI won't bother moving toward or away from the player.
// A value greater than 0 is require so entities don't move back and forth at the FPS rate.
pub dead_zone: f32,
}
pub fn archer_movement(
mut entities: Query<
(&mut MovementDirection, &Archer, &Perception, &Transform),
With<EntityCore<Enemy>>,
>,
player: Query<&Transform, With<EntityCore<Player>>>,
) {
if let Ok(player) = player.get_single() {
for (mut move_direction, archer, perception, transform) in entities.iter_mut() {
let distance: f32 = player.translation.xy().distance(transform.translation.xy());
let direction: Vec2 =
(player.translation.xy() - transform.translation.xy()).normalize_or_zero();
if perception.is_tracking && distance > (archer.range + archer.dead_zone) {
**move_direction = direction;
} else if perception.is_tracking && distance < archer.range {
**move_direction = direction * -1.0;
} else if perception.is_tracking {
// Since the other conditions didn't run, we assume this is the dead zone.
**move_direction = Vec2::ZERO;
}
}
};
}