Files
bevy-snake/src/snake.rs

303 lines
9.3 KiB
Rust

mod appearance;
mod bulge;
mod direction;
mod movement;
use crate::{audio, fruit, grid, GameState};
use bevy::prelude::*;
use direction::Direction;
use itertools::Itertools;
use leafwing_input_manager::prelude::*;
#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
enum SystemSet {
Movement,
DirectionFlush,
CollisionDetection,
}
pub struct SnakePlugin;
impl Plugin for SnakePlugin {
fn build(&self, app: &mut App) {
app.configure_set(SystemSet::Movement.after(SystemSet::CollisionDetection));
app.add_plugin(InputManagerPlugin::<direction::Direction>::default())
.add_event::<AddTailEvent>()
.insert_resource(bulge::PropagationTimer::default())
.add_startup_system(setup_snake_system)
.add_system(
segments_movement_system
.in_schedule(CoreSchedule::FixedUpdate)
.in_set(SystemSet::Movement)
.run_if(in_state(GameState::InGame))
.after(SystemSet::DirectionFlush),
)
.add_system(
direction::apply_direction_system
.in_schedule(CoreSchedule::FixedUpdate)
.run_if(in_state(GameState::InGame))
.before(SystemSet::DirectionFlush),
)
.add_system(
apply_system_buffers
.in_schedule(CoreSchedule::FixedUpdate)
.in_set(SystemSet::DirectionFlush)
.run_if(in_state(GameState::InGame)),
)
.add_system(
collision_system
.in_schedule(CoreSchedule::FixedUpdate)
.in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame))
.after(SystemSet::DirectionFlush),
)
.add_system(
game_over_system
.in_schedule(CoreSchedule::FixedUpdate)
.in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame))
.after(collision_system)
.run_if(about_to_collide),
)
.add_system(
collision_sound_system
.run_if(in_state(GameState::InGame))
.run_if(about_to_collide),
)
.add_system(
blip_sound_system
.run_if(fruit::eaten_event_sent),
)
.add_system(direction::start_game_system.run_if(in_state(GameState::Begin)))
.add_system(direction::change_direction_system)
.add_system(grid_transform_system.run_if(in_state(GameState::InGame)))
.add_system(add_tail_system)
.add_system(bulge::add_bulge_system)
.add_system(bulge::propagate_bulge_system)
.add_system(bulge::animate_bulge_system)
.add_system(eaten_event_system);
#[cfg(debug_assertions)]
app.add_system(debug::add_tail);
}
}
pub const Z_HEIGHT: f32 = 10.;
#[derive(Component, Debug)]
struct SnakeSegment;
#[derive(Component, Debug)]
pub struct SnakeHead;
#[derive(Component, Debug)]
struct Segments(Vec<Entity>);
#[derive(Component, Debug)]
struct Snake;
#[derive(Bundle)]
struct SnakeBundle {
snake: Snake,
collision: Collision,
segments: Segments,
#[bundle]
spatial_bundle: SpatialBundle,
}
#[derive(Component, Default, Debug)]
struct Collision {
about_to_collide: bool,
}
pub struct AddTailEvent;
fn create_snake_segment(
commands: &mut Commands,
grid_position: grid::Coordinate,
segment_number: u32,
) -> Entity {
let mut color = Color::RED;
color *= 0.99f32.powi(segment_number as _);
commands
.spawn(SpriteBundle {
sprite: Sprite {
color,
custom_size: Some(Vec2::splat(grid::SEGMENT_SIZE) * 0.9),
..Default::default()
},
transform: Transform::from_translation(Vec2::from(grid_position).extend(Z_HEIGHT)),
..Default::default()
})
.insert(SnakeSegment)
.insert(grid_position)
.insert(crate::canvas::EmissiveTile { intensity: 1.0 })
.id()
}
fn setup_snake_system(mut commands: Commands) {
let snake_head =
create_snake_segment(&mut commands, grid::Coordinate::splat(grid::SIZE / 2), 0);
commands
.entity(snake_head)
.insert(Name::new("SnakeHead"))
.insert(SnakeHead);
commands
.spawn(SnakeBundle {
snake: Snake,
collision: Default::default(),
segments: Segments(vec![snake_head]),
spatial_bundle: Default::default(),
})
.insert(Name::new("Snake"))
.insert(InputManagerBundle::<Direction> {
input_map: InputMap::new([
(KeyCode::Up, Direction::Up),
(KeyCode::Down, Direction::Down),
(KeyCode::Left, Direction::Left),
(KeyCode::Right, Direction::Right),
]),
..Default::default()
})
.add_child(snake_head);
}
fn eaten_event_system(
mut eaten_event_reader: EventReader<fruit::EatenEvent>,
mut tail_event_writer: EventWriter<AddTailEvent>,
) {
eaten_event_reader
.iter()
.for_each(|_| tail_event_writer.send(AddTailEvent));
}
fn add_tail_system(
mut commands: Commands,
mut snake_query: Query<(Entity, &mut Segments), With<Snake>>,
mut tail_event_reader: EventReader<AddTailEvent>,
) {
for _ in tail_event_reader.iter() {
let (snake_entity, mut segments) = snake_query.single_mut();
let segment = create_snake_segment(
&mut commands,
grid::Coordinate::splat(grid::Index::MIN / 2),
segments.0.len() as _,
);
segments.0.push(segment);
commands
.entity(segment)
.insert(Name::new(format!("Segment {}", segments.0.len())));
commands.entity(snake_entity).add_child(segment);
}
}
fn grid_transform_system(
mut query: Query<(&mut Transform, &grid::Coordinate), With<SnakeSegment>>,
) {
for (mut transform, grid_coordinate) in query.iter_mut() {
*transform = Transform::from_translation(Vec2::from(grid_coordinate).extend(Z_HEIGHT));
}
}
fn segments_movement_system(
mut segment_query: Query<&mut grid::Coordinate, With<SnakeSegment>>,
snake_query: Query<(&Collision, &Direction, &Segments), With<Snake>>,
) {
for (collision, direction, segments) in snake_query.iter() {
if collision.about_to_collide {
continue;
}
// Move other elements
for (&segment, &previous_segment) in segments.0.iter().rev().tuple_windows() {
let previous_coordinate = *segment_query.get(previous_segment).unwrap();
let mut coordinate = segment_query.get_mut(segment).unwrap();
*coordinate = previous_coordinate;
}
// Move head
let head = *segments.0.first().expect("Snake has always a head");
let mut head_coordinate = segment_query.get_mut(head).unwrap();
*head_coordinate = next_grid_coordinate(&head_coordinate, direction);
}
}
fn collision_system(
segment_query: Query<&grid::Coordinate, (With<SnakeSegment>, Without<SnakeHead>)>,
head_query: Query<(&grid::Coordinate, &Parent), With<SnakeHead>>,
mut snake_query: Query<(&Direction, &mut Collision), With<Snake>>,
) {
for (head_coordinate, parent) in head_query.iter() {
let (direction, mut collision) = snake_query
.get_mut(parent.get())
.expect("Head must be child of Snake");
let next_head_coordinate = next_grid_coordinate(head_coordinate, direction);
let hit_border = !next_head_coordinate.in_bounds();
let hit_snake = !segment_query
.iter()
.all(|&coordinate| coordinate != next_head_coordinate);
collision.about_to_collide = hit_border || hit_snake;
}
}
fn about_to_collide(query: Query<&Collision, With<Snake>>) -> bool {
query.get_single().unwrap().about_to_collide
}
fn game_over_system(mut next_state: ResMut<NextState<GameState>>) {
next_state.set(GameState::End);
}
fn collision_sound_system(audio: Res<Audio>, audio_assets: Res<audio::Assets>) {
audio.play(audio_assets.collision.clone());
}
fn blip_sound_system(
audio: Res<Audio>,
audio_assets: Res<audio::Assets>,
mut eaten_event_reader: EventReader<fruit::EatenEvent>,
) {
for _ in eaten_event_reader.iter() {
audio.play(audio_assets.blip.clone());
}
}
fn next_grid_coordinate(
current_coordinate: &grid::Coordinate,
direction: &Direction,
) -> grid::Coordinate {
match direction {
Direction::Up => *current_coordinate + grid::Coordinate(0, 1),
Direction::Down => *current_coordinate + grid::Coordinate(0, -1),
Direction::Left => *current_coordinate + grid::Coordinate(-1, 0),
Direction::Right => *current_coordinate + grid::Coordinate(1, 0),
}
}
#[cfg(debug_assertions)]
mod debug {
use super::*;
pub(super) fn add_tail(
keypress: Res<Input<KeyCode>>,
mut tail_event_writer: EventWriter<AddTailEvent>,
) {
if keypress.just_pressed(KeyCode::Space) {
tail_event_writer.send(AddTailEvent);
}
}
}