WIP
This commit is contained in:
1633
Cargo.lock
generated
1633
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
12
Cargo.toml
12
Cargo.toml
@@ -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."*"]
|
||||
|
||||
@@ -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)]
|
||||
|
||||
14
src/fruit.rs
14
src/fruit.rs
@@ -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)]
|
||||
|
||||
87
src/main.rs
87
src/main.rs
@@ -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
|
||||
|
||||
95
src/snake.rs
95
src/snake.rs
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user