This commit is contained in:
2023-09-18 21:23:49 +02:00
parent e11ba22c93
commit 7f1f2cc6ab
9 changed files with 1158 additions and 770 deletions

1633
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,15 +4,15 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
itertools = "0.10.5" itertools = "0.11.0"
rand = "0.8.5" rand = "0.8.5"
bevy_editor_pls = "0.3.0" bevy_editor_pls = { git = "https://github.com/jakobhellermann/bevy_editor_pls" }
bevy_tweening = "0.7.0" bevy_tweening = "0.8.0"
leafwing-input-manager = "0.9.0" leafwing-input-manager = "0.10.0"
bevy_asset_loader = "0.15.0" bevy_asset_loader = "0.17.0"
[dependencies.bevy] [dependencies.bevy]
version = "0.10" version = "0.11"
features = ["wayland", "wav"] features = ["wayland", "wav"]
# [profile.dev.package."*"] # [profile.dev.package."*"]

View File

@@ -3,7 +3,7 @@ mod tile_light;
use crate::grid; use crate::grid;
use bevy::{ use bevy::{
prelude::*, prelude::*,
reflect::TypeUuid, reflect::{TypeUuid, TypePath},
render::{ render::{
render_resource::{encase, AsBindGroup, OwnedBindingResource, ShaderRef, ShaderType}, render_resource::{encase, AsBindGroup, OwnedBindingResource, ShaderRef, ShaderType},
renderer::RenderQueue, renderer::RenderQueue,
@@ -19,18 +19,18 @@ pub struct CanvasPlugin;
impl Plugin for CanvasPlugin { impl Plugin for CanvasPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_plugin(Material2dPlugin::<CanvasMaterial>::default()) app.add_plugins(Material2dPlugin::<CanvasMaterial>::default())
.add_startup_system(spawn_canvas); .add_systems(Startup, spawn_canvas);
app.sub_app_mut(RenderApp) app.sub_app_mut(RenderApp)
.add_system(extract_tile_lights.in_schedule(ExtractSchedule)) .add_systems(ExtractSchedule, extract_tile_lights)
.add_system(prepare_canvas_material.in_set(RenderSet::Prepare)); .add_systems(Update, prepare_canvas_material.in_set(RenderSet::Prepare));
} }
} }
const CANVAS_COLOR: Color = Color::rgb(0.05, 0.05, 0.05); const CANVAS_COLOR: Color = Color::rgb(0.05, 0.05, 0.05);
#[derive(AsBindGroup, TypeUuid, Clone)] #[derive(AsBindGroup, TypeUuid, TypePath, Clone)]
#[uuid = "24f83f6e-e52d-41a6-bf1d-0e46e57a4995"] #[uuid = "24f83f6e-e52d-41a6-bf1d-0e46e57a4995"]
struct CanvasMaterial { struct CanvasMaterial {
#[uniform(0)] #[uniform(0)]

View File

@@ -12,13 +12,19 @@ pub struct FruitPlugin;
impl Plugin for FruitPlugin { impl Plugin for FruitPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_event::<EatenEvent>() app.add_event::<EatenEvent>()
.add_startup_system(spawn_fruit_system) .add_systems(Startup, spawn_fruit_system)
.add_system(eat_fruit_system.in_schedule(CoreSchedule::FixedUpdate)) .add_systems(FixedUpdate, eat_fruit_system)
.add_system(despawn_fruit_system) .add_systems(
.add_system(spawn_fruit_system.run_if(eaten_event_sent)); Update,
(
despawn_fruit_system,
spawn_fruit_system.run_if(eaten_event_sent),
),
);
} }
} }
#[derive(Event)]
pub struct EatenEvent(Option<Entity>); pub struct EatenEvent(Option<Entity>);
#[derive(Component)] #[derive(Component)]

View File

@@ -2,6 +2,7 @@ use crate::{
canvas::CanvasPlugin, canvas::CanvasPlugin,
fruit::FruitPlugin, fruit::FruitPlugin,
grid::{SEGMENT_SIZE, SIZE}, grid::{SEGMENT_SIZE, SIZE},
level::{LevelPlugin, LevelState},
snake::SnakePlugin, snake::SnakePlugin,
ui::UiPlugin, ui::UiPlugin,
}; };
@@ -17,6 +18,7 @@ mod audio;
mod canvas; mod canvas;
mod fruit; mod fruit;
mod grid; mod grid;
mod level;
mod snake; mod snake;
mod ui; mod ui;
@@ -25,23 +27,70 @@ const WINDOW_WIDTH: f32 = WINDOW_HEIGHT * ASPECT_RATIO;
const WINDOW_HEIGHT: f32 = 720.; const WINDOW_HEIGHT: f32 = 720.;
const TICK_PERIOD: Duration = Duration::from_millis(125); const TICK_PERIOD: Duration = Duration::from_millis(125);
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default, States)] #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
enum GameState { enum AppState {
#[default] MainMenu,
Begin, InGame(LevelState),
InGame, }
End,
#[derive(Default)]
struct AppStateIter {
current: Option<AppState>,
}
impl Iterator for AppStateIter {
type Item = AppState;
fn next(&mut self) -> Option<Self::Item> {
let next_state = match self.current {
None => Some(AppState::MainMenu),
Some(AppState::MainMenu) => Some(AppState::InGame(LevelState::Begin)),
Some(AppState::InGame(level_state)) => match level_state {
LevelState::Begin => Some(AppState::InGame(LevelState::InGame)),
LevelState::InGame => Some(AppState::InGame(LevelState::End)),
LevelState::End => None,
},
};
if let Some(next_state) = next_state {
self.current = Some(next_state);
}
next_state
}
}
impl Default for AppState {
fn default() -> Self {
if cfg!(debug_assertions) {
Self::InGame(LevelState::Begin)
} else {
Self::MainMenu
}
}
}
impl States for AppState {
type Iter = AppStateIter;
fn variants() -> Self::Iter {
AppStateIter::default()
}
} }
#[derive(Resource)] #[derive(Resource)]
struct Score(u32); struct Score(u32);
#[derive(Resource)]
struct Level(u32);
fn main() { fn main() {
let mut app = App::new(); let mut app = App::new();
app.insert_resource(ClearColor(Color::BLACK)) app.insert_resource(ClearColor(Color::BLACK))
.insert_resource(FixedTime::new(TICK_PERIOD)) .insert_resource(FixedTime::new(TICK_PERIOD))
.insert_resource(Score(0)) .insert_resource(Score(0))
.insert_resource(Level(0))
.add_plugins(DefaultPlugins.set(WindowPlugin { .add_plugins(DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window { primary_window: Some(Window {
title: "Bevy-Snake".into(), title: "Bevy-Snake".into(),
@@ -52,26 +101,28 @@ fn main() {
..Default::default() ..Default::default()
})) }))
.init_collection::<audio::Assets>() .init_collection::<audio::Assets>()
.add_state::<GameState>() .add_state::<AppState>()
.add_plugin(TweeningPlugin) .add_plugins((
.add_plugin(SnakePlugin) TweeningPlugin,
.add_plugin(FruitPlugin) SnakePlugin,
.add_plugin(CanvasPlugin) FruitPlugin,
.add_plugin(UiPlugin) CanvasPlugin,
.add_startup_system(setup_system) UiPlugin,
.add_system(update_score_system); LevelPlugin,
))
.add_systems(Startup, setup_camera_system)
.add_systems(Update, update_score_system);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
app.add_plugin(EditorPlugin) app.add_plugins(EditorPlugin::new())
.add_system(bevy::window::close_on_esc) .add_systems(Update, (bevy::window::close_on_esc, pause_system));
.add_system(pause_system);
} }
app.run(); app.run();
} }
fn setup_system(mut commands: Commands) { fn setup_camera_system(mut commands: Commands) {
let grid_dimensions = f32::from(SIZE) * SEGMENT_SIZE * 1.2; let grid_dimensions = f32::from(SIZE) * SEGMENT_SIZE * 1.2;
commands commands

View File

@@ -3,7 +3,7 @@ mod bulge;
mod direction; mod direction;
mod movement; mod movement;
use crate::{audio, fruit, grid, GameState}; use crate::{audio, fruit, grid, level::LevelState, AppState};
use bevy::prelude::*; use bevy::prelude::*;
use direction::Direction; use direction::Direction;
use itertools::Itertools; use itertools::Itertools;
@@ -20,55 +20,71 @@ pub struct SnakePlugin;
impl Plugin for SnakePlugin { impl Plugin for SnakePlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.configure_set(SystemSet::Movement.after(SystemSet::CollisionDetection)); app.configure_set(
Update,
SystemSet::Movement.after(SystemSet::CollisionDetection),
);
app.add_plugin(InputManagerPlugin::<direction::Direction>::default()) app.add_plugins(InputManagerPlugin::<direction::Direction>::default())
.add_event::<AddTailEvent>() .add_event::<AddTailEvent>()
.insert_resource(bulge::PropagationTimer::default()) .insert_resource(bulge::PropagationTimer::default())
.add_startup_system(setup_snake_system)
.add_systems( .add_systems(
OnEnter(AppState::InGame(LevelState::Begin)),
setup_snake_system,
)
.add_systems(
FixedUpdate,
( (
segments_movement_system segments_movement_system
.in_set(SystemSet::Movement) .in_set(SystemSet::Movement)
.run_if(in_state(GameState::InGame)) .run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(SystemSet::DirectionFlush), .after(SystemSet::DirectionFlush),
direction::apply_direction_system direction::apply_direction_system
.run_if(in_state(GameState::InGame)) .run_if(in_state(AppState::InGame(LevelState::InGame)))
.before(SystemSet::DirectionFlush), .before(SystemSet::DirectionFlush),
apply_system_buffers apply_deferred
.in_set(SystemSet::DirectionFlush) .in_set(SystemSet::DirectionFlush)
.run_if(in_state(GameState::InGame)), .run_if(in_state(AppState::InGame(LevelState::InGame))),
collision_system collision_system
.in_set(SystemSet::CollisionDetection) .in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame)) .run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(SystemSet::DirectionFlush), .after(SystemSet::DirectionFlush),
game_over_system game_over_system
.in_set(SystemSet::CollisionDetection) .in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame)) .run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(collision_system) .after(collision_system)
.run_if(about_to_collide), .run_if(about_to_collide),
),
) )
.in_schedule(CoreSchedule::FixedUpdate), .add_systems(
) Update,
.add_systems(( (
grid_transform_system.run_if(in_state(GameState::InGame)), grid_transform_system.run_if(in_state(AppState::InGame(LevelState::InGame))),
direction::start_game_system.run_if(in_state(GameState::Begin)), direction::start_game_system
direction::change_direction_system, .run_if(in_state(AppState::InGame(LevelState::Begin))),
direction::change_direction_system.run_if(
in_state(AppState::InGame(LevelState::Begin))
.or_else(in_state(AppState::InGame(LevelState::InGame))),
),
bulge::add_bulge_system, bulge::add_bulge_system,
bulge::propagate_bulge_system, bulge::propagate_bulge_system,
bulge::animate_bulge_system, bulge::animate_bulge_system,
add_tail_system, add_tail_system,
eaten_event_system, eaten_event_system,
)) ),
.add_systems(( )
.add_systems(
Update,
(
collision_sound_system collision_sound_system
.run_if(in_state(GameState::InGame)) .run_if(in_state(AppState::InGame(LevelState::InGame)))
.run_if(about_to_collide), .run_if(about_to_collide),
blip_sound_system.run_if(fruit::eaten_event_sent), blip_sound_system.run_if(fruit::eaten_event_sent),
)); ),
);
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
app.add_system(debug::add_tail); app.add_systems(Update, debug::add_tail);
} }
} }
@@ -91,8 +107,6 @@ struct SnakeBundle {
snake: Snake, snake: Snake,
collision: Collision, collision: Collision,
segments: Segments, segments: Segments,
#[bundle]
spatial_bundle: SpatialBundle, spatial_bundle: SpatialBundle,
} }
@@ -101,6 +115,7 @@ struct Collision {
about_to_collide: bool, about_to_collide: bool,
} }
#[derive(Event)]
pub struct AddTailEvent; pub struct AddTailEvent;
fn create_snake_segment( fn create_snake_segment(
@@ -246,21 +261,27 @@ fn about_to_collide(query: Query<&Collision, With<Snake>>) -> bool {
query.get_single().unwrap().about_to_collide query.get_single().unwrap().about_to_collide
} }
fn game_over_system(mut next_state: ResMut<NextState<GameState>>) { fn game_over_system(mut next_state: ResMut<NextState<AppState>>) {
next_state.set(GameState::End); next_state.set(AppState::InGame(LevelState::End));
} }
fn collision_sound_system(audio: Res<Audio>, audio_assets: Res<audio::Assets>) { fn collision_sound_system(mut commands: Commands, audio_assets: Res<audio::Assets>) {
audio.play(audio_assets.collision.clone()); commands.spawn(AudioBundle {
source: audio_assets.collision.clone(),
..Default::default()
});
} }
fn blip_sound_system( fn blip_sound_system(
audio: Res<Audio>, mut commands: Commands,
audio_assets: Res<audio::Assets>, audio_assets: Res<audio::Assets>,
mut eaten_event_reader: EventReader<fruit::EatenEvent>, mut eaten_event_reader: EventReader<fruit::EatenEvent>,
) { ) {
for _ in eaten_event_reader.iter() { for _ in eaten_event_reader.iter() {
audio.play(audio_assets.blip.clone()); commands.spawn(AudioBundle {
source: audio_assets.blip.clone(),
..Default::default()
});
} }
} }

View File

@@ -1,9 +1,9 @@
use super::Snake; use super::Snake;
use crate::{audio, GameState}; use crate::{audio, level::LevelState, AppState};
use bevy::prelude::*; use bevy::prelude::*;
use leafwing_input_manager::prelude::*; use leafwing_input_manager::prelude::*;
#[derive(Actionlike, Component, Copy, Clone, Debug, PartialEq, Eq)] #[derive(Actionlike, Reflect, Component, Copy, Clone, Debug, PartialEq, Eq)]
pub(super) enum Direction { pub(super) enum Direction {
Up, Up,
Down, Down,
@@ -13,35 +13,34 @@ pub(super) enum Direction {
#[derive(Component, Copy, Clone, Debug, PartialEq, Eq)] #[derive(Component, Copy, Clone, Debug, PartialEq, Eq)]
#[component(storage = "SparseSet")] #[component(storage = "SparseSet")]
pub(super) struct NewDirection(Direction); pub(super) struct NextDirection(Direction);
pub(super) fn start_game_system( pub(super) fn start_game_system(
query: Query<&ActionState<Direction>>, query: Query<&ActionState<Direction>>,
mut next_state: ResMut<NextState<GameState>>, mut next_state: ResMut<NextState<AppState>>,
) { ) {
let action_state = query.single(); let action_state = query.single();
if !action_state.get_just_pressed().is_empty() { if !action_state.get_just_pressed().is_empty() {
next_state.set(GameState::InGame); next_state.set(AppState::InGame(LevelState::InGame));
} }
} }
pub(super) fn apply_direction_system( pub(super) fn apply_direction_system(
mut commands: Commands, mut commands: Commands,
mut query: Query<(Entity, &NewDirection), With<Snake>>, mut query: Query<(Entity, &NextDirection), With<Snake>>,
) { ) {
for (snake, new_direction) in query.iter_mut() { for (snake, new_direction) in query.iter_mut() {
commands commands
.entity(snake) .entity(snake)
.insert(new_direction.0) .insert(new_direction.0)
.remove::<NewDirection>(); .remove::<NextDirection>();
} }
} }
pub(super) fn change_direction_system( pub(super) fn change_direction_system(
mut commands: Commands, mut commands: Commands,
mut query: Query<(Entity, &ActionState<Direction>, Option<&Direction>), With<Snake>>, mut query: Query<(Entity, &ActionState<Direction>, Option<&Direction>), With<Snake>>,
audio: Res<Audio>,
audio_assets: Res<audio::Assets>, audio_assets: Res<audio::Assets>,
) { ) {
let (snake, action_state, direction) = query.single_mut(); let (snake, action_state, direction) = query.single_mut();
@@ -62,7 +61,10 @@ pub(super) fn change_direction_system(
} }
} }
commands.entity(snake).insert(NewDirection(new_direction)); commands.entity(snake).insert(NextDirection(new_direction));
audio.play(audio_assets.tick.clone()); commands.spawn(AudioBundle {
source: audio_assets.tick.clone(),
..Default::default()
});
} }
} }

View File

@@ -1,14 +1,12 @@
use bevy::prelude::*; use bevy::prelude::*;
mod score; mod status;
pub struct UiPlugin; pub struct UiPlugin;
impl Plugin for UiPlugin { impl Plugin for UiPlugin {
fn build(&self, app: &mut App) { fn build(&self, app: &mut App) {
app.add_systems(( app.add_systems(Startup, status::spawn_status_text)
score::spawn_score_text.on_startup(), .add_systems(Update, status::update_status_text);
score::update_score_text,
));
} }
} }

View File

@@ -1,45 +0,0 @@
use crate::Score;
use bevy::prelude::*;
#[derive(Component)]
pub(super) struct ScoreText;
pub(super) fn spawn_score_text(mut commands: Commands, asset_server: Res<AssetServer>) {
let font = asset_server.load("fonts/Audiowide-Regular.ttf");
commands.spawn((
ScoreText,
TextBundle::from_sections([
TextSection::new(
"Score: ",
TextStyle {
font: font.clone(),
font_size: 32.0,
color: Color::WHITE,
},
),
TextSection::from_style(TextStyle {
font: font,
font_size: 32.0,
color: Color::WHITE,
}),
])
.with_style(Style {
position_type: PositionType::Absolute,
position: UiRect {
left: Val::Px(10.0),
bottom: Val::Px(10.0),
..Default::default()
},
..Default::default()
}),
));
}
pub(super) fn update_score_text(mut query: Query<&mut Text, With<ScoreText>>, score: Res<Score>) {
let score = score.0;
for mut text in query.iter_mut() {
text.sections[1].value = format!("{score}");
}
}