A Brief Refactor
Up to this point we've been tossing everything in lib.rs. This is very convenient. Heck, we could keep doing this forever and have our entire game in one file! Please don't do this. This step is technically optional, but highly recommended if you're following this tutorial all the way through.
Separate Config
This is all a matter of taste, of course, but I have found it convenient to put all the consts we're using as config values into their own config.rs, making them all pub in the process.
pub const PLAYER_MAX_VELOCITY: f32 = 750.;
pub const PLAYER_ACCELERATION: f32 = 9000.;
pub const PLAYER_DECELERATION: f32 = 12000.;
pub const ZERO_VELOCITY_THRESHOLD: f32 = 0.1;
pub const ENEMY_MAX_VELOCITY: f32 = 300.;
pub const ENEMY_ACCELERATION: f32 = 1000.;
pub const ENEMY_DECELERATION: f32 = 5000.;
pub const ENEMY_CHASE_DISTANCE: f32 = 1000.;
pub const ENEMY_STOP_DISTANCE: f32 = 200.;
pub const GRID_DOT_SIZE: f32 = 7.5;
pub const GRID_DOT_SIDES: u32 = 5;
pub const GRID_DOT_SPACING: f32 = 25.;
pub const GRID_ROWS: u32 = 200;
pub const GRID_COLS: u32 = 200;
pub const GRID_DOT_ALPHA_1: f32 = 0.1;
pub const GRID_DOT_ALPHA_2: f32 = 0.2;
pub const GRID_DOT_COLOR_SPACING: u32 = 5;
Then in lib.rs we can replace that all with
mod config;
use crate::config::*;
Separate Spawn Functions
All of our spawn functions currently take no queries, meaning there's nothing special about them being systems. So instead, we can take those out into another file and just have a single system spawn them in the order we desire. It's possible down the line we may not find this is enough control, but for now it'll help simplify things a lot. We'l need to import the components we want to reference, and the config values. Of course, we'll also have to make them pub as well.
use engine::prelude::*;
use crate::config::*;
use crate::{Acceleration, Enemy, Player, Velocity};
pub fn spawn_grid_dots() {
let grid_width = (GRID_COLS - 1) as f32 * GRID_DOT_SPACING;
let grid_height = (GRID_ROWS - 1) as f32 * GRID_DOT_SPACING;
let start_x = -grid_width / 2.0;
let start_y = -grid_height / 2.0;
for row in 0..=GRID_ROWS {
for col in 0..=GRID_COLS {
let x = start_x + ((col) as f32 * GRID_DOT_SPACING);
let y = start_y + ((row) as f32 * GRID_DOT_SPACING);
let transform = &Transform {
position: Vec2::new(x, y),
scale: Vec2::splat(1.0),
..Default::default()
};
let is_grid_line =
row % GRID_DOT_ALPHA_SPACING == 0 || col % GRID_DOT_ALPHA_SPACING == 0;
let base_alpha = if is_grid_line {
GRID_DOT_ALPHA_2
} else {
GRID_DOT_ALPHA_1
};
let color = &Color::new(1.0, 1.0, 1.0, base_alpha);
let circle_render = &CircleRender {
num_sides: GRID_DOT_SIDES,
size: Vec2::splat(GRID_DOT_SIZE),
visible: true,
};
Engine::spawn(bundle!(transform, color, circle_render));
}
}
}
pub fn spawn_player() {
let transform = &Transform {
position: Vec2::new(1000., 0.),
scale: Vec2::splat(1.5),
..Default::default()
};
let color = &Color::new(0.96, 0.36, 0.9, 1.);
let circle_render = &CircleRender {
num_sides: 20,
..Default::default()
};
let velocity = &Velocity {
max_velocity: PLAYER_MAX_VELOCITY,
..Default::default()
};
let acceleration = &Acceleration {
acceleration_rate: PLAYER_ACCELERATION,
deceleration_rate: PLAYER_DECELERATION,
..Default::default()
};
Engine::spawn(bundle!(
transform,
color,
circle_render,
&Player,
velocity,
acceleration
));
}
pub fn spawn_camera() {
let camera = &Camera {
aspect: CameraAspect::FixedVirtualHeight { height: 1500. },
clear_color: Color::from_hex_str("#222").unwrap(),
..Default::default()
};
Engine::spawn(bundle!(&Transform::default(), camera));
}
pub 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.7, 0.7, 1.);
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
));
}
Then back in lib.rs we can replace all our spawning stuff with just...
mod spawn_systems;
use crate::spawn_systems::*;
#[system_once]
fn spawn_everything(){
spawn_grid_dots();
spawn_camera();
spawn_player();
spawn_enemy();
}
Separate the Movement
The last big block of code is our movement logic. Separating this out is more difficult but worth doing, I believe, though our lib.rs is already starting to look quite svelte.
Setting up movement_systems.rs is basically the same steps as we did with our spawn_systems.
use crate::config::*;
use crate::{Acceleration, Enemy, Player, PlayerInput, Velocity};
use engine::prelude::*;
pub fn update_player_acceleration(
player_input: Ref<PlayerInput>,
mut player_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Player>)>,
) {
let Some((mut acceleration, velocity, _)) = player_query.get_mut(0) else {
return;
};
acceleration.target_velocity = player_input.move_axes * velocity.max_velocity;
}
pub 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);
}
}
});
}
pub fn apply_acceleration(
mut velocities_to_adjust: Query<(Ref<Acceleration>, Mut<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
velocities_to_adjust.for_each(|(acceleration, mut velocity)| {
let velocity_delta = acceleration.target_velocity - velocity.current_velocity;
let is_decelerating = acceleration.target_velocity == Vec2::ZERO
|| acceleration.target_velocity.dot(velocity.current_velocity) < 0.0;
let rate = if is_decelerating {
acceleration.deceleration_rate
} else {
acceleration.acceleration_rate
};
let max_change = rate * frame_constants.delta_time;
let velocity_change = velocity_delta.clamp_length_max(max_change);
velocity.current_velocity += velocity_change;
if acceleration.target_velocity.x.abs() < ZERO_VELOCITY_THRESHOLD
&& velocity.current_velocity.x.abs() < ZERO_VELOCITY_THRESHOLD
{
velocity.current_velocity.x = 0.;
}
if acceleration.target_velocity.y.abs() < ZERO_VELOCITY_THRESHOLD
&& velocity.current_velocity.y.abs() < ZERO_VELOCITY_THRESHOLD
{
velocity.current_velocity.y = 0.;
}
velocity.current_velocity = velocity
.current_velocity
.clamp_length_max(velocity.max_velocity);
});
}
pub fn apply_velocity(
mut entities_to_move: Query<(Mut<Transform>, Ref<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
entities_to_move.for_each(|(mut transform, velocity)| {
transform.position += velocity.current_velocity * frame_constants.delta_time;
});
}
pub fn update_camera_position(
mut camera_query: Query<(Mut<Transform>, Ref<Camera>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
let Some((player_transform, _)) = player_query.get(0) else {
return;
};
let Some((mut camera_transform, _)) = camera_query.get_mut(0) else {
return;
};
camera_transform.position = player_transform.position;
}
However, in lib.rs, because we are using Queries, we still need to make those in properly tagged systems to work. Luckily you can take those results and immediately pass them to the function. In the future hopefully this won't be as necessary, but it's not that much code to repeat at least.
mod movement_systems;
use crate::movement_systems::*;
#[system]
fn update_player_acceleration_system(
player_input: Ref<PlayerInput>,
player_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Player>)>,
) {
update_player_acceleration(player_input, player_query);
}
#[system]
fn update_enemy_acceleration_system(
enemy_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Transform>, Ref<Enemy>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_enemy_acceleration(enemy_query, player_query);
}
#[system]
fn apply_acceleration_system(
velocities_to_adjust: Query<(Ref<Acceleration>, Mut<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_acceleration(velocities_to_adjust, frame_constants);
}
#[system]
fn apply_velocity_system(
entities_to_move: Query<(Mut<Transform>, Ref<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_velocity(entities_to_move, frame_constants);
}
#[system]
fn update_camera_position_system(
camera_query: Query<(Mut<Transform>, Ref<Camera>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_camera_position(camera_query, player_query);
}
Final lib.rs
Altogether now our lib.rs:
use engine::prelude::*;
mod ffi;
mod config;
mod spawn_systems;
mod movement_systems;
use crate::spawn_systems::*;
use crate::config::*;
use crate::movement_systems::*;
#[derive(Default)]
#[resource]
struct PlayerInput {
move_axes: Vec2,
}
#[component]
struct Player;
#[component]
struct Enemy;
#[derive(Default)]
#[component]
struct Velocity {
current_velocity: Vec2,
max_velocity: f32,
}
#[derive(Default)]
#[component]
struct Acceleration {
acceleration_rate: f32,
deceleration_rate: f32,
target_velocity: Vec2,
}
#[system_once]
fn spawn_everything(){
spawn_grid_dots();
spawn_camera();
spawn_player();
spawn_enemy();
}
#[system]
fn sync_player_movement_to_config_consts(
mut player: Query<(Mut<Acceleration>, Mut<Velocity>, Ref<Player>)>,
) {
let Some((mut acceleration, mut velocity, _)) = player.get_mut(0) else {
return;
};
velocity.max_velocity = PLAYER_MAX_VELOCITY;
acceleration.acceleration_rate = PLAYER_ACCELERATION;
acceleration.deceleration_rate = PLAYER_DECELERATION;
}
#[system]
fn update_player_input(mut player_input: Mut<PlayerInput>, input: Ref<InputState>) {
let key_to_f32 = |key_code| {
if input.keys[key_code].pressed() {
1.0
} else {
0.0
}
};
player_input.move_axes = Vec2::new(
key_to_f32(KeyCode::KeyD) - key_to_f32(KeyCode::KeyA),
key_to_f32(KeyCode::KeyW) - key_to_f32(KeyCode::KeyS),
)
.clamp_length_max(1.);
}
#[system]
fn update_player_acceleration_system(
player_input: Ref<PlayerInput>,
player_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Player>)>,
) {
update_player_acceleration(player_input, player_query);
}
#[system]
fn update_enemy_acceleration_system(
enemy_query: Query<(Mut<Acceleration>, Ref<Velocity>, Ref<Transform>, Ref<Enemy>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_enemy_acceleration(enemy_query, player_query);
}
#[system]
fn apply_acceleration_system(
velocities_to_adjust: Query<(Ref<Acceleration>, Mut<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_acceleration(velocities_to_adjust, frame_constants);
}
#[system]
fn apply_velocity_system(
entities_to_move: Query<(Mut<Transform>, Ref<Velocity>)>,
frame_constants: Ref<FrameConstants>,
) {
apply_velocity(entities_to_move, frame_constants);
}
#[system]
fn update_camera_position_system(
camera_query: Query<(Mut<Transform>, Ref<Camera>)>,
player_query: Query<(Ref<Transform>, Ref<Player>)>,
) {
update_camera_position(camera_query, player_query);
}
After all this your src folder should have ffi.rs, lib.rs, config.rs, spawn_systems.rs, and movement_systems.rs.
We have some really cool things to do with our simple grid on our next two side quests, if you'll join me on this effort.
If you prefer to just forge along with gameplay basics, go ahead and add aiming to your game instead! I'm not your boss.