Collision and Gizmos
There are three entities that the player can collider with:
- The top pipe
- The bottom pipe
- The gap in between the pipes
If the player colliders with a pipe, their game is over and should reset.
If the player collides with the gap entity, it should despawn and the player should earn a point.
Flappy Bird is a game with fairly lightweight collision needs. Each of the pipes, gaps, and player entities can be represented by a box or a circle. For boxes we’ll use Aabb2d and for circles we’ll use BoundingCircle. Both of these types are regular structs that can be created for very little cost on-the-fly every frame.
An Aabb2d has a new function that accepts an x/y position in the world and the “half-size” of the box. A “half-size” is exactly what it says, half the size of the side of the box it refers to. That leaves us with roughly this logic for creating new Aabb2ds.
Aabb2d::new(
Vec2::new(x, y),
sprite.custom_size.unwrap() / 2.,
);
The BoundingCircle is the same, but accepts a radius in the second argument.
BoundingCircle::new(
Vec2::new(x, y),
PLAYER_SIZE / 2.,
);
GlobalTransform
We’ve been working with Transform up until now, but there’s something worth knowing about how it works. A Transform represents a local position offset from the entity’s parent’s position. A GlobalTransform represents the entity’s position in global space. At the end of every frame, Transforms are accumulated through the tree of entities that exist, and those values are stored in the GlobalTransform.
We want this global position value to do our collision detection, but since the GlobalTransform is only updated at the end of the frame we have two choices:
- Live with being a frame behind
- Accept the performance cost of updating any entity’s
GlobalTransformright when we need it.
We’re building Flappy Bird, so the cost of computing the GlobalTransform again is acceptable and that’s what we’ll do. That is done using the TransformHelper SystemParam and its compute_global_transform method.
Computing Collisions
With the right positions and the constructed collider types, we can take advantage of the BoundingCircle and Aabb2d implementations of the IntersectsVolume trait. Concretely this means we can call .intersects on any two relevant values.
With all that background we can see the system that will compute our collisions. There’s quite a few new topics here! So take some time to read it through and we’ll continue talking about it line by line.
fn check_collisions(
mut commands: Commands,
player: Single<(&Sprite, Entity), With<Player>>,
pipe_segments: Query<
(&Sprite, Entity),
Or<(With<PipeTop>, With<PipeBottom>)>,
>,
pipe_gaps: Query<(&Sprite, Entity), With<PointsGate>>,
mut gizmos: Gizmos,
transform_helper: TransformHelper,
) -> Result<()> {
let player_transform = transform_helper
.compute_global_transform(player.1)?;
let player_collider = BoundingCircle::new(
player_transform.translation().xy(),
PLAYER_SIZE / 2.,
);
gizmos.circle_2d(
player_transform.translation().xy(),
PLAYER_SIZE / 2.,
RED_400,
);
for (sprite, entity) in &pipe_segments {
let pipe_transform = transform_helper
.compute_global_transform(entity)?;
let pipe_collider = Aabb2d::new(
pipe_transform.translation().xy(),
sprite.custom_size.unwrap() / 2.,
);
gizmos.rect_2d(
pipe_transform.translation().xy(),
sprite.custom_size.unwrap(),
RED_400,
);
if player_collider.intersects(&pipe_collider) {
commands.trigger(EndGame);
}
}
for (sprite, entity) in &pipe_gaps {
let gap_transform = transform_helper
.compute_global_transform(entity)?;
let gap_collider = Aabb2d::new(
gap_transform.translation().xy(),
sprite.custom_size.unwrap() / 2.,
);
gizmos.rect_2d(
gap_transform.translation().xy(),
sprite.custom_size.unwrap().xy(),
RED_400,
);
if player_collider.intersects(&gap_collider) {
info!("score a point!");
commands.entity(entity).despawn();
}
}
Ok(())
}
Digging into Collisions
We’ve seen Commands and Single before so Or is the first new item. Or with the Withs inside it will match any Entity that has either PipeTop or PipeBottom. All pipes collide the same to us, so we use this query to group them all together.
fn check_collisions(
mut commands: Commands,
player: Single<(&Sprite, Entity), With<Player>>,
pipe_segments: Query<
(&Sprite, Entity),
Or<(With<PipeTop>, With<PipeBottom>)>,
>,
pipe_gaps: Query<(&Sprite, Entity), With<PointsGate>>,
mut gizmos: Gizmos,
transform_helper: TransformHelper,
) -> Result<()> {
Gizmos
Gizmos are also new! Given that we’re working with what are effectively “invisible colliders”, it can be useful to visualize where the colliders would be. This is where Gizmos come in. Every frame, if we call the gizmo functions a shape will draw on the screen that frame.
Gizmos can be turned on and off using a bit of configuration. For example, this bit of code added to our startup system will control whether gizmos render or not. Gizmos work like this because you can create your own Gizmo groups for specific purposes and enable/disable them as a group.
fn startup(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut config_store: ResMut<GizmoConfigStore>,
) {
let (config, _) = config_store
.config_mut::<DefaultGizmoConfigGroup>();
config.enabled = false;
...
}
TransformHelper
The TransformHelper is how we’ll force the GlobalTransforms to be up to date.
The pattern for all of the colliders is the same.
- Get the up-to-date
GlobalTransform - Build the collider struct
- Draw the Gizmo to show the collider
let player_transform = transform_helper
.compute_global_transform(player.1)?;
let player_collider = BoundingCircle::new(
player_transform.translation().xy(),
PLAYER_SIZE / 2.,
);
gizmos.circle_2d(
player_transform.translation().xy(),
PLAYER_SIZE / 2.,
RED_400,
);
Running the Application
With our check_collisions system, we need to add it to FixedUpdate alongside our other systems with one major caveat: The check_collisions system needs to run after gravity if we want our gizmos to draw the effects on the Transforms that gravity has every frame.
To order our systems we take the easy path and use .chain which ensures that all systems in a tuple run in order.
Note that we also have a host of new items in scope for our collisions.
use bevy::color::palettes::tailwind::RED_400;
use bevy::{
camera::ScalingMode,
math::bounding::{
Aabb2d, BoundingCircle, IntersectsVolume,
},
prelude::*,
};
use flappy_bird::*;
fn main() -> AppExit {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(PipePlugin)
.add_systems(Startup, startup)
.add_systems(
FixedUpdate,
(
gravity,
check_in_bounds,
check_collisions,
)
.chain(),
)
.add_systems(Update, controls)
.add_observer(respawn_on_endgame)
.run()
}