marcusbuffett / bevy_snake Goto Github PK
View Code? Open in Web Editor NEWClone of the snake game, with Bevy
Home Page: https://mbuffett.com/posts/bevy-snake-tutorial/
Clone of the snake game, with Bevy
Home Page: https://mbuffett.com/posts/bevy-snake-tutorial/
.add_system(game_over.system())
isn't mentioned anywhere on https://mbuffett.com/posts/bevy-snake-tutorial/ , but it's necessary to get the game over system working
You may wish to add a note in the tutorial that the order of the systems matter.
I myself put the put the snake_eat and snake_growth before the other systems and it the snake does not grow when the snake ate food
Hi
Thank you for your great post. in Adding a tail part, you define the SnakeSegments
wrong and it cause to error in Making the tail follow the snake:
#[derive(Default)]
struct SnakeSegments(Vec<Entity>);
But I found that the code in Github is true and don't cause to error:
#[derive(Default, Deref, DerefMut)]
struct SnakeSegments(Vec<Entity>);
Please update the post.
Thanks!
Thanks for this tutorial! It was a great intro to bevy (and ECS).
While playing, I noticed that sometimes my keyboard input is missed when I'm typing quickly. I think this is because input is only checked when the movement timer fires.
I found some example input handling code and thought I would try this instead--basically, it looks like I'd be keeping the most-recently-pressed key in the program state. So I created a resource for this:
pub struct InputState {
pub keys: EventReader<KeyboardInput>,
pub direction: SnakeDirection, // I renamed Direction to SnakeDirection due to a potential conflict from bevy's prelude
}
impl Default for InputState {
fn default() -> Self {
Self {
keys: EventReader::<KeyboardInput>::default(),
direction: SnakeDirection::Up,
}
}
}
And a system:
pub fn input(
mut state: ResMut<InputState>,
ev_keys: Res<Events<KeyboardInput>>,
) {
for ev in state.keys.iter(&ev_keys) {
if ev.state.is_pressed() {
let key_code = ev.key_code.unwrap();
state.direction = match key_code {
KeyCode::Left => Some(SnakeDirection::Left),
KeyCode::Right => Some(SnakeDirection::Right),
KeyCode::Up => Some(SnakeDirection::Up),
KeyCode::Down => Some(SnakeDirection::Down),
_ => None,
}
.unwrap_or(state.direction);
}
}
}
And changed the movement system's signature:
pub fn snake_movement(
input: Res<InputState>,
snake_timer: ResMut<SnakeMoveTimer>,
segments: ResMut<SnakeSegments>,
mut last_tail_position: ResMut<LastTailPosition>,
mut game_over_events: ResMut<Events<GameOverEvent>>,
mut heads: Query<(Entity, &mut SnakeHead)>,
mut positions: Query<&mut Position>,
) {
if !snake_timer.0.finished {
return;
}
for (head_entity, mut head) in heads.iter_mut() {
let mut head_pos = positions.get_mut(head_entity).unwrap();
let dir = input.direction;
if dir != head.direction.opposite() {
head.direction = dir;
}
// ...
With the new code above, the snake doesn't drop input as much, but it still misses some. This feels like I'm on the right track, anyway.
If you don't mind me asking:
Anyway, I realize your time is valuable and I don't expect a response. Thanks again for this tutorial.
Your website is down, it takes me to an nginx page :)
fn spawn_segment(
mut commands: Commands,
material: &Handle<ColorMaterial>,
position: Position,
) -> Entity {
commands
.spawn_bundle(SpriteBundle {
material: material.clone(), <-- here(on line 119)
..Default::default()
})
.insert(SnakeSegment)
.insert(position)
.insert(Size::square(0.65))
.id()
}
mismatched types [E0308]expected Handle<ColorMaterial>
, found &Handle<ColorMaterial>
no more detial, i just copy main.rs to CLion and got this error.
For adding startup systems the Tutorial does this:
.add_startup_system(setup.system())
.add_startup_stage("game_setup", SystemStage::single(spawn_snake.system()))
The usage of add_startup_stage
+ SystemStage::single
regularly causes questions on the Bevy discord why this is necessary here.
I would suggest to use this instead:
.add_startup_system(setup.system())
.add_startup_system_to_stage(StartupStage::PostStartup, spawn_snake.system())
The explanation below the code block can then be
- The reason we need a new stage instead of just calling add_startup_system again,
- is that we need to use the material that gets inserted in the setup function.
+ The reason we need to specify a stage instead of just calling add_startup_system again,
+ is that we need to use the material that gets inserted in the setup function, commands are only processed at the end of a stage.
error[E0432]: unresolved import bevy::core::FixedTimestep
error[E0277]: the trait bound SnakeSegments: Resource
is not satisfied
error[E0277]: the trait bound LastTailPosition: Resource
is not satisfied
error[E0277]: the trait bound SnakeSegments: Resource
is not satisfied
error[E0277]: the trait bound SnakeSegments: Resource
is not satisfied
error[E0277]: the trait bound LastTailPosition: Resource
is not satisfied
error[E0277]: the trait bound SnakeSegments: Resource
is not satisfied
error[E0433]: failed to resolve: use of undeclared type OrthographicCameraBundle
here is the fix with a correct program close:
use bevy::prelude::*;
use bevy::time::FixedTimestep;
use rand::prelude::random;
const SNAKE_HEAD_COLOR: Color = Color::rgb(0.7, 0.7, 0.7);
const FOOD_COLOR: Color = Color::rgb(1.0, 0.0, 1.0);
const SNAKE_SEGMENT_COLOR: Color = Color::rgb(0.3, 0.3, 0.3);
const ARENA_HEIGHT: u32 = 10;
const ARENA_WIDTH: u32 = 10;
#[derive(Component, Clone, Copy, PartialEq, Eq)]
struct Position {
x: i32,
y: i32,
}
#[derive(Component)]
struct Size {
width: f32,
height: f32,
}
impl Size {
pub fn square(x: f32) -> Self {
Self {
width: x,
height: x,
}
}
}
#[derive(Component)]
struct SnakeHead {
direction: Direction,
}
struct GameOverEvent;
struct GrowthEvent;
#[derive(Resource, Default)]
struct LastTailPosition(Option);
#[derive(Component)]
struct SnakeSegment;
#[derive(Resource, Default, Deref, DerefMut)]
struct SnakeSegments(Vec);
#[derive(Component)]
struct Food;
#[derive(PartialEq, Copy, Clone)]
enum Direction {
Left,
Up,
Right,
Down,
}
impl Direction {
fn opposite(self) -> Self {
match self {
Self::Left => Self::Right,
Self::Right => Self::Left,
Self::Up => Self::Down,
Self::Down => Self::Up,
}
}
}
fn setup_camera(mut commands: Commands) {
commands.spawn(Camera2dBundle::default());
}
fn spawn_snake(mut commands: Commands, mut segments: ResMut) {
*segments = SnakeSegments(vec![
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_HEAD_COLOR,
..default()
},
..default()
})
.insert(SnakeHead {
direction: Direction::Up,
})
.insert(SnakeSegment)
.insert(Position { x: 3, y: 3 })
.insert(Size::square(0.8))
.id(),
spawn_segment(commands, Position { x: 3, y: 2 }),
]);
}
fn spawn_segment(mut commands: Commands, position: Position) -> Entity {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: SNAKE_SEGMENT_COLOR,
..default()
},
..default()
})
.insert(SnakeSegment)
.insert(position)
.insert(Size::square(0.65))
.id()
}
fn snake_movement(
mut last_tail_position: ResMut,
mut game_over_writer: EventWriter,
segments: ResMut,
mut heads: Query<(Entity, &SnakeHead)>,
mut positions: Query<&mut Position>,
) {
if let Some((head_entity, head)) = heads.iter_mut().next() {
let segment_positions = segments
.iter()
.map(|e| *positions.get_mut(*e).unwrap())
.collect::<Vec>();
let mut head_pos = positions.get_mut(head_entity).unwrap();
match &head.direction {
Direction::Left => {
head_pos.x -= 1;
}
Direction::Right => {
head_pos.x += 1;
}
Direction::Up => {
head_pos.y += 1;
}
Direction::Down => {
head_pos.y -= 1;
}
};
if head_pos.x < 0
|| head_pos.y < 0
|| head_pos.x as u32 >= ARENA_WIDTH
|| head_pos.y as u32 >= ARENA_HEIGHT
{
game_over_writer.send(GameOverEvent);
}
if segment_positions.contains(&head_pos) {
game_over_writer.send(GameOverEvent);
}
segment_positions
.iter()
.zip(segments.iter().skip(1))
.for_each(|(pos, segment)| {
*positions.get_mut(*segment).unwrap() = *pos;
});
*last_tail_position = LastTailPosition(Some(*segment_positions.last().unwrap()));
}
}
fn snake_movement_input(keyboard_input: Res<Input>, mut heads: Query<&mut SnakeHead>) {
if let Some(mut head) = heads.iter_mut().next() {
let dir: Direction = if keyboard_input.pressed(KeyCode::Left) {
Direction::Left
} else if keyboard_input.pressed(KeyCode::Down) {
Direction::Down
} else if keyboard_input.pressed(KeyCode::Up) {
Direction::Up
} else if keyboard_input.pressed(KeyCode::Right) {
Direction::Right
} else {
head.direction
};
if dir != head.direction.opposite() {
head.direction = dir;
}
}
}
fn game_over(
mut commands: Commands,
mut reader: EventReader,
segments_res: ResMut,
food: Query<Entity, With>,
segments: Query<Entity, With>,
) {
if reader.iter().next().is_some() {
for ent in food.iter().chain(segments.iter()) {
commands.entity(ent).despawn();
}
spawn_snake(commands, segments_res);
}
}
fn snake_eating(
mut commands: Commands,
mut growth_writer: EventWriter,
food_positions: Query<(Entity, &Position), With>,
head_positions: Query<&Position, With>,
) {
for head_pos in head_positions.iter() {
for (ent, food_pos) in food_positions.iter() {
if food_pos == head_pos {
commands.entity(ent).despawn();
growth_writer.send(GrowthEvent);
}
}
}
}
fn snake_growth(
commands: Commands,
last_tail_position: Res,
mut segments: ResMut,
mut growth_reader: EventReader,
) {
if growth_reader.iter().next().is_some() {
segments.push(spawn_segment(commands, last_tail_position.0.unwrap()));
}
}
fn size_scaling(windows: Res, mut q: Query<(&Size, &mut Transform)>) {
if let Some(window) = windows.get_primary() {
for (sprite_size, mut transform) in q.iter_mut() {
transform.scale = Vec3::new(
sprite_size.width / ARENA_WIDTH as f32 * window.width(),
sprite_size.height / ARENA_HEIGHT as f32 * window.height(),
1.0,
);
}
}
}
fn position_translation(windows: Res, mut q: Query<(&Position, &mut Transform)>) {
fn convert(pos: f32, bound_window: f32, bound_game: f32) -> f32 {
let tile_size = bound_window / bound_game;
pos / bound_game * bound_window - (bound_window / 2.) + (tile_size / 2.)
}
if let Some(window) = windows.get_primary() {
for (pos, mut transform) in q.iter_mut() {
transform.translation = Vec3::new(
convert(pos.x as f32, window.width(), ARENA_WIDTH as f32),
convert(pos.y as f32, window.height(), ARENA_HEIGHT as f32),
0.0,
);
}
}
}
fn food_spawner(mut commands: Commands) {
commands
.spawn(SpriteBundle {
sprite: Sprite {
color: FOOD_COLOR,
..default()
},
..default()
})
.insert(Food)
.insert(Position {
x: (random::() * ARENA_WIDTH as f32) as i32,
y: (random::() * ARENA_HEIGHT as f32) as i32,
})
.insert(Size::square(0.8));
}
fn main() {
App::new()
.insert_resource(ClearColor(Color::rgb(0.04, 0.04, 0.04)))
.add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor {
title: "Snake!".to_string(),
width: 500.,
height: 300.,
..default()
},
..default()
}))
.add_startup_system(setup_camera)
.add_startup_system(spawn_snake)
.insert_resource(SnakeSegments::default())
.insert_resource(LastTailPosition::default())
.add_event::()
.add_system(snake_movement_input.before(snake_movement))
.add_event::()
.add_system_set(
SystemSet::new()
.with_run_criteria(FixedTimestep::step(0.150))
.with_system(snake_movement)
.with_system(snake_eating.after(snake_movement))
.with_system(snake_growth.after(snake_eating)),
)
.add_system(game_over.after(snake_movement))
.add_system_set(
SystemSet::new()
.with_run_criteria(FixedTimestep::step(1.0))
.with_system(food_spawner),
)
.add_system_set_to_stage(
CoreStage::PostUpdate,
SystemSet::new()
.with_system(position_translation)
.with_system(size_scaling),
)
.add_system(bevy::window::close_on_esc)
.run();
}
It is possible to trick opposite direction check. To do this you need quickly change direction to something different than current and then switch it back, before the timer.finished
condition is met.
I've played around it a bit, but have no idea how to fix it.
Is there any chance to overcome this issue?
Hi there,
since the repo technically has no licence, I believe everything you wrote is under copyright - with certain exceptions like forking or contributing via PRs and issues, according to GitHub's OpenSource Guides and their TOS.
I'm following your tutorial but re-building a lot because the bevy versions differ, so I don't know whether we're far enough apart that it matters or not, but would you mind adding a licence making it explicit? :)
Best,
Lexi
(I'm aware the account hasn't been active for a while, just trying nonetheless!)
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.