Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Hitting walls and other game over events
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](http://lib.rs)` we can use an enum to represent each of these. ```rust 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. ```rust 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`. ```rust 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. ```rust // 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. ```rust 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](https://doc.rust-lang.org/std/option/enum.Option.html#method.or) 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. ```rust 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!
- Loading branch information