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"
[dependencies]
itertools = "0.10.5"
itertools = "0.11.0"
rand = "0.8.5"
bevy_editor_pls = "0.3.0"
bevy_tweening = "0.7.0"
leafwing-input-manager = "0.9.0"
bevy_asset_loader = "0.15.0"
bevy_editor_pls = { git = "https://github.com/jakobhellermann/bevy_editor_pls" }
bevy_tweening = "0.8.0"
leafwing-input-manager = "0.10.0"
bevy_asset_loader = "0.17.0"
[dependencies.bevy]
version = "0.10"
version = "0.11"
features = ["wayland", "wav"]
# [profile.dev.package."*"]

View File

@@ -3,7 +3,7 @@ mod tile_light;
use crate::grid;
use bevy::{
prelude::*,
reflect::TypeUuid,
reflect::{TypeUuid, TypePath},
render::{
render_resource::{encase, AsBindGroup, OwnedBindingResource, ShaderRef, ShaderType},
renderer::RenderQueue,
@@ -19,18 +19,18 @@ pub struct CanvasPlugin;
impl Plugin for CanvasPlugin {
fn build(&self, app: &mut App) {
app.add_plugin(Material2dPlugin::<CanvasMaterial>::default())
.add_startup_system(spawn_canvas);
app.add_plugins(Material2dPlugin::<CanvasMaterial>::default())
.add_systems(Startup, spawn_canvas);
app.sub_app_mut(RenderApp)
.add_system(extract_tile_lights.in_schedule(ExtractSchedule))
.add_system(prepare_canvas_material.in_set(RenderSet::Prepare));
.add_systems(ExtractSchedule, extract_tile_lights)
.add_systems(Update, prepare_canvas_material.in_set(RenderSet::Prepare));
}
}
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"]
struct CanvasMaterial {
#[uniform(0)]

View File

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

View File

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

View File

@@ -3,7 +3,7 @@ mod bulge;
mod direction;
mod movement;
use crate::{audio, fruit, grid, GameState};
use crate::{audio, fruit, grid, level::LevelState, AppState};
use bevy::prelude::*;
use direction::Direction;
use itertools::Itertools;
@@ -20,55 +20,71 @@ pub struct SnakePlugin;
impl Plugin for SnakePlugin {
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>()
.insert_resource(bulge::PropagationTimer::default())
.add_startup_system(setup_snake_system)
.add_systems(
OnEnter(AppState::InGame(LevelState::Begin)),
setup_snake_system,
)
.add_systems(
FixedUpdate,
(
segments_movement_system
.in_set(SystemSet::Movement)
.run_if(in_state(GameState::InGame))
.run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(SystemSet::DirectionFlush),
direction::apply_direction_system
.run_if(in_state(GameState::InGame))
.run_if(in_state(AppState::InGame(LevelState::InGame)))
.before(SystemSet::DirectionFlush),
apply_system_buffers
apply_deferred
.in_set(SystemSet::DirectionFlush)
.run_if(in_state(GameState::InGame)),
.run_if(in_state(AppState::InGame(LevelState::InGame))),
collision_system
.in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame))
.run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(SystemSet::DirectionFlush),
game_over_system
.in_set(SystemSet::CollisionDetection)
.run_if(in_state(GameState::InGame))
.run_if(in_state(AppState::InGame(LevelState::InGame)))
.after(collision_system)
.run_if(about_to_collide),
)
.in_schedule(CoreSchedule::FixedUpdate),
),
)
.add_systems((
grid_transform_system.run_if(in_state(GameState::InGame)),
direction::start_game_system.run_if(in_state(GameState::Begin)),
direction::change_direction_system,
bulge::add_bulge_system,
bulge::propagate_bulge_system,
bulge::animate_bulge_system,
add_tail_system,
eaten_event_system,
))
.add_systems((
collision_sound_system
.run_if(in_state(GameState::InGame))
.run_if(about_to_collide),
blip_sound_system.run_if(fruit::eaten_event_sent),
));
.add_systems(
Update,
(
grid_transform_system.run_if(in_state(AppState::InGame(LevelState::InGame))),
direction::start_game_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::propagate_bulge_system,
bulge::animate_bulge_system,
add_tail_system,
eaten_event_system,
),
)
.add_systems(
Update,
(
collision_sound_system
.run_if(in_state(AppState::InGame(LevelState::InGame)))
.run_if(about_to_collide),
blip_sound_system.run_if(fruit::eaten_event_sent),
),
);
#[cfg(debug_assertions)]
app.add_system(debug::add_tail);
app.add_systems(Update, debug::add_tail);
}
}
@@ -91,8 +107,6 @@ struct SnakeBundle {
snake: Snake,
collision: Collision,
segments: Segments,
#[bundle]
spatial_bundle: SpatialBundle,
}
@@ -101,6 +115,7 @@ struct Collision {
about_to_collide: bool,
}
#[derive(Event)]
pub struct AddTailEvent;
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
}
fn game_over_system(mut next_state: ResMut<NextState<GameState>>) {
next_state.set(GameState::End);
fn game_over_system(mut next_state: ResMut<NextState<AppState>>) {
next_state.set(AppState::InGame(LevelState::End));
}
fn collision_sound_system(audio: Res<Audio>, audio_assets: Res<audio::Assets>) {
audio.play(audio_assets.collision.clone());
fn collision_sound_system(mut commands: Commands, audio_assets: Res<audio::Assets>) {
commands.spawn(AudioBundle {
source: audio_assets.collision.clone(),
..Default::default()
});
}
fn blip_sound_system(
audio: Res<Audio>,
mut commands: Commands,
audio_assets: Res<audio::Assets>,
mut eaten_event_reader: EventReader<fruit::EatenEvent>,
) {
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 crate::{audio, GameState};
use crate::{audio, level::LevelState, AppState};
use bevy::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 {
Up,
Down,
@@ -13,35 +13,34 @@ pub(super) enum Direction {
#[derive(Component, Copy, Clone, Debug, PartialEq, Eq)]
#[component(storage = "SparseSet")]
pub(super) struct NewDirection(Direction);
pub(super) struct NextDirection(Direction);
pub(super) fn start_game_system(
query: Query<&ActionState<Direction>>,
mut next_state: ResMut<NextState<GameState>>,
mut next_state: ResMut<NextState<AppState>>,
) {
let action_state = query.single();
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(
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() {
commands
.entity(snake)
.insert(new_direction.0)
.remove::<NewDirection>();
.remove::<NextDirection>();
}
}
pub(super) fn change_direction_system(
mut commands: Commands,
mut query: Query<(Entity, &ActionState<Direction>, Option<&Direction>), With<Snake>>,
audio: Res<Audio>,
audio_assets: Res<audio::Assets>,
) {
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));
audio.play(audio_assets.tick.clone());
commands.entity(snake).insert(NextDirection(new_direction));
commands.spawn(AudioBundle {
source: audio_assets.tick.clone(),
..Default::default()
});
}
}

View File

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