GithubHelp home page GithubHelp logo

marcusbuffett / bevy_snake Goto Github PK

View Code? Open in Web Editor NEW
90.0 3.0 22.0 142 KB

Clone of the snake game, with Bevy

Home Page: https://mbuffett.com/posts/bevy-snake-tutorial/

Rust 100.00%
bevy rust gamedev

bevy_snake's People

Contributors

marcusbuffett avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

bevy_snake's Issues

responsiveness

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:

  1. Why might it still be skipping some keyboard input?
  2. How could I debug this?

Anyway, I realize your time is valuable and I don't expect a response. Thanks again for this tutorial.

with bevy 0.9.1 there are 8 errors + crash with closed the window

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();
}

thanks for writing the post! it's extremely solid :D

This isn't the right place, but I wanted to point out that your code blocks have their overflow hidden on the bevy tutorial post:

image

thanks for writing the post! I especially appreciate the "see the diff" asides that let me know whether I'm on track with your tutorial!

technically no licence

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!)

System Order

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

Problem in SnakeSegments struct

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!

More idiomatic startup setup

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.

There is a possibility to ignore opposite direction check

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?

mismatched types: &Handle<ColorMaterial>

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.