Ending the game of Snake can happen one of three ways:
- Snake hits a wall
- Snake hits itself
- All tiles are full of Snake (Win condition).
In lib.rs
we can use an enum to represent each of these.
#[derive(PartialEq, Eq, Debug)]
enum GameOverReason {
HitWall,
HitSnake,
Win,
}
In tick
, we’ll need access to the board to detect whether the snake has hit the wall or not, so bring Board
into scope and set up the query_board
. Inside of the system, we can get access to the board using query_board.single()
.
Then we can use the match on the input direction to do hit detection against the walls.
hit_wall
is going to be an Option<GameOverReason>
. If we hit a wall we’ll have a game over, if not then we’ll have None
.
I’ve also put use controls::Direction::*;
at the top of this file so that we can use Up
and the other variants rather than typing out the full module paths.
pub fn tick(
mut commands: Commands,
mut snake: ResMut<Snake>,
positions: Query<(Entity, &Position)>,
input: Res<controls::Direction>,
query_food: Query<(Entity, &Position), With<Food>>,
mut food_events: EventWriter<NewFoodEvent>,
query_board: Query<&Board>,
) {
let board = query_board.single();
let mut next_position = snake.segments[0].clone();
let hit_wall = match *input {
Up if next_position.y == board.size - 1 => Some(GameOverReason::HitWall),
Up => {
next_position.y += 1;
None
}
Down if next_position.y == 0 => Some(GameOverReason::HitWall),
Down => {
next_position.y -= 1;
None
}
Right if next_position.x == board.size - 1 => Some(GameOverReason::HitWall),
Right => {
next_position.x += 1;
None
}
Left if next_position.x == 0 => Some(GameOverReason::HitWall),
Left => {
next_position.x -= 1;
None
}
};
...
}
We changed out matches here to use a Rust feature called Guards.
A Guard is a check we can do in the left hand of the match to see if we should use this arm of the expression.
Zooming in on Up
specifically, if the input direction is up, the first part of the first match matches. We then can immediately use an if statement to check to see if the y position of the next snake segment would be outside of the bounds of the board grid.
If the if expression is true, then we evaluate the expression on the right hand side.
If the if expression is false, then we fall through to the next match, which in this case is just Up
and therefore would catch the match and return None
.
Up if next_position.y == board.size - 1 => Some(GameOverReason::HitWall),
Up => {
next_position.y += 1;
None
}
We do this guard for each direction, Up against the top of the board, Right
against the right side, etc.
After this match, we’ll either have the next segment ready to go or we’ll have a game over indicator.
After checking the walls we can check if the snake hit itself. This is a bit simpler as we can use .contains
to see if the next position is in the snake segments.
// did the snake hit itself?
let hit_self =
if snake.segments.contains(&next_position) {
Some(GameOverReason::HitSnake)
} else {
None
};
and of course, the win condition. If the number of snake segments equals the number of tiles in the board then the snake has nowhere else to go and has won the round.
let has_won = if snake.segments.len()
== board.size as usize * board.size as usize
{
Some(GameOverReason::Win)
} else {
None
};
We can then move the rest of our code in this function into a match expression.
hit_wall.or(hit_self).or(has_won)
uses the or function on the Option
type to combine all of the potential game over option values and if any of them are a Some
value, it will be the value we’re matching on. Otherwise it will be None
.
If we have any of the game over variants, we transition into the menu state which ends the game. Note that NextState
is part of the iyes_loopless::prelude::*
.
Otherwise we continue with the same code we had before.
match hit_wall.or(hit_self).or(has_won) {
Some(GameOverReason::HitWall)
| Some(GameOverReason::HitSnake)
| Some(GameOverReason::Win) => {
commands.insert_resource(NextState(
GameState::Menu,
));
}
None => {
snake.segments.push_front(next_position);
commands.add({
SpawnSnakeSegment {
position: next_position,
}
});
// remove old snake segment, unless snake just
// ate food
let is_food = query_food
.iter()
.find(|(_, pos)| &&next_position == pos);
match is_food {
Some((entity, _)) => {
commands
.entity(entity)
.despawn_recursive();
food_events.send(NewFoodEvent);
}
None => {
let old_tail =
snake.segments.pop_back().unwrap();
if let Some((entity, _)) = positions
.iter()
.find(|(_, pos)| pos == &&old_tail)
{
commands
.entity(entity)
.despawn_recursive();
}
}
}
}
}
and now the game can end multiple ways!