Melhorando a Cadência do Movimento

O atual movimento da cobra está ligado aos comandos do teclado diferentemente do snale game que a cobra se movimenta independente dos comandos do teclado e a cada x segundos, em vez de a cada frame. Para podermos fazer com que a cobra se movimente independente dos comandos do teclado, precisamos de uma forma de armazenar a direção que ela está se movimentando, além de evitar que a cobra vá para a direção oposta. Assim, precisamos criar o enum que armazena a direção e criar uma função que indica a direção oposta.

// components.rs
#[test]
fn opposite_direction() {
    assert_eq!(Direction::Up.opposite(), Direction::Down);
    assert_eq!(Direction::Down.opposite(), Direction::Up);
    assert_eq!(Direction::Right.opposite(), Direction::Left);
    assert_eq!(Direction::Left.opposite(), Direction::Right);
}

// ...
#[derive(PartialEq, Debug, Copy, Clone)]
pub enum Direction {
    Left,
    Up,
    Right,
    Down,
}

impl Direction {
    pub fn opposite(self) -> Self {
        match self {
            Self::Left => Self::Right,
            Self::Right => Self::Left,
            Self::Up => Self::Down,
            Self::Down => Self::Up,
        }
    }
}

Em uma etapa inicial do desenvolvimento, eu adicionaria Direction como um componente na entidade cobra, porém, a medida que a cobra ficar maior, vai ser difícil sincronizar a direção de cada elemento. Sabendo disso, vamos fazer um pouco de overengineering, e colocar Direction como elemento do componente snake::Head. Além disso, definimos a direção padrão como Direction::Up utilizando a trait Default:

// snake.rs

#[derive(Component)]
pub struct Head {
    direction: Direction
}

impl Default for Head {
    fn default() -> Self {
        Self { direction: Direction::Up }
    }
}

pub fn spawn_system(mut commands: Commands) {
    commands
        .spawn_bundle(SpriteBundle {
            sprite: Sprite {
                color: SNAKE_HEAD_COLOR,
                ..default()
            },
            transform: Transform {
                scale: Vec3::new(10.0, 10.0, 10.0),
                ..default()
            },
            ..default()
        })
        .insert(Head::default() ) // <--
        .insert(Position { x: 3, y: 3 })
        .insert(Size::square(0.8));
}

Separando o movimento em duas etapas

Agora que nossa cobra possui uma direção armazenada na cabeça podemos mudar seu sistema de movimento para que seja executado a cada 0.15 segundos, fazemos isso da mesma forma que fizemos com o sistema de geração de comidas:

fn main() {
    App::new()
        // ...
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(1.0))
                .with_system(food::spawn_system),
        )
        .add_system_set(
            SystemSet::new()
                .with_run_criteria(FixedTimestep::step(0.150)) // <--
                .with_system(snake::movement_system), // <--
        ) 
        .run();
}

Nosso sistema de movimento está atrelado à translação da cobra em uma posição para cada tecla que apertarmos e agora sabemos que o sistema de movimento deve ser independente do sistema de direção, que vamos chamar de snake::movement_input_system. Então precisamos que o sistema de input/direção aconteça antes do sistema de movimento, e, ainda, precisamos garantir que o sistema de movimento aconteça a cada 0.15 segundos. Para solucionar este problema, vamos adicionar uma função especial para sistemas before. Para utilizar está função, precisamos adicionar o sistema de input que vamos criar e indicar que ele deve ocorrer antes do sistema de movimento com .add_system(snake::movement_input_system.before(snake::movement_system)), adicione ao App::new() na função main. Agora vamos para o sistema movement_input_system.

Primeira coisa que devemos fazer é alterar os testes para considerar que o snake::movement_system recebe uma query com Position e snake::Head, além de considerar que o movimento é feito com base no enum Direction contido dentro de snake::Head:

// snake.rs
#[test]
    fn snake_starts_moviment_up() { // <-- novo teste
        // Setup app
        let mut app = App::new();

        // Add startup system
        app.add_startup_system(spawn_system);

        // Run systems
        app.update();

        let mut query = app.world.query::<&Head>();
        let head = query.iter(&app.world).next().unwrap();
        assert_eq!(head.direction, Direction::Up);
    }

    #[test]
    fn snake_head_has_moved_up() {
        // Setup
        let mut app = App::new();
        let default_position = Position { x: 3, y: 4 };

        // Add systems
        app.add_startup_system(spawn_system)
            .add_system(movement_system)
            .add_system(movement_input_system.before(movement_system)); // <--

        // Add input resource
        let mut input = Input::<KeyCode>::default();
        input.press(KeyCode::W);
        app.insert_resource(input);

        // Run systems
        app.update();

        let mut query = app.world.query::<(&Head, &Position)>();
        query.iter(&app.world).for_each(|(head, position)| {
            assert_eq!(&default_position, position);
            assert_eq!(head.direction, Direction::Up); // <-- novo assert
            
        })
    }

    #[test]
    fn snake_head_moves_up_and_right() {
        // Setup
        let mut app = App::new();
        let up_position = Position { x: 3, y: 4 };

        // Add systems
        app.add_startup_system(spawn_system)
            .add_system(movement_system)
            .add_system(movement_input_system.before(movement_system)); // <--

        // Move Up
        let mut input = Input::<KeyCode>::default();
        input.press(KeyCode::W);
        app.insert_resource(input);
        app.update();

        let mut query = app.world.query::<(&Head, &Position)>();
        query.iter(&app.world).for_each(|(_head, position)| {
            assert_eq!(position, &up_position);
        });

        let up_right_position = Position { x: 4, y: 4 };

        // Move Right
        let mut input = Input::<KeyCode>::default();
        input.press(KeyCode::D);
        app.insert_resource(input);
        app.update();

        let mut query = app.world.query::<(&Head, &Position)>();
        query.iter(&app.world).for_each(|(head, position)| {
            assert_eq!(&up_right_position, position);
            assert_eq!(head.direction, Direction::Right);
        })
    }

    #[test]
    fn snake_head_moves_down_and_left() {
        // Setup
        let mut app = App::new();
        let down_left_position = Position { x: 2, y: 2 };

        // Add systems
        app.add_startup_system(spawn_system)
            .add_system(movement_system)
            .add_system(movement_input_system.before(movement_system)); // <--

        // Move Left
        let mut input = Input::<KeyCode>::default();
        input.press(KeyCode::A);
        app.insert_resource(input);
        app.update();

         // Move down
         let mut input = Input::<KeyCode>::default();
         input.press(KeyCode::S);
         app.insert_resource(input);
         app.update();

        // Assert
        let mut query = app.world.query::<(&Head, &Position)>();
        query.iter(&app.world).for_each(|(head, position)| {
            assert_eq!(&down_left_position, position);
            assert_eq!(head.direction, Direction::Down);
        })
    }

    #[test]
    fn snake_cannot_start_moving_down() { // <-- novo teste
         // Setup
         let mut app = App::new();
         let down_left_position = Position { x: 3, y: 4 };
 
         // Add systems
         app.add_startup_system(spawn_system)
             .add_system(movement_system)
             .add_system(movement_input_system.before(movement_system));
 
          // Move down
          let mut input = Input::<KeyCode>::default();
          input.press(KeyCode::S);
          app.insert_resource(input);
          app.update();
 
         // Assert
         let mut query = app.world.query::<(&Head, &Position)>();
         query.iter(&app.world).for_each(|(_head, position)| {
             assert_eq!(&down_left_position, position);
         })
    }

Novos testes: snake_starts_moviment_up que checa se a cobra inicia seu movimento para cima. snake_cannot_start_moving_down checa que não é possível se movimentar na direção oposta.

Com estes testes sabemos que o movement system agora deve receber uma query com a posição mutável e com a Head para obtermos a direção. Com base na direção, movemos a posição da cabeça:

pub fn movement_system(mut heads: Query<(&mut Position, &Head)>) {
    if let Some((mut pos, head)) = heads.iter_mut().next() {
        match &head.direction {
            Direction::Left => {
                pos.x -= 1;
            }
            Direction::Right => {
                pos.x += 1;
            }
            Direction::Up => {
                pos.y += 1;
            }
            Direction::Down => {
                pos.y -= 1;
            }
        };
    }
}

Já o movement_input_system recebe uma leitura de teclado (KeyCode) e muda a direção com base nesta leitura do teclado e evita mudanças na direção oposta:

pub fn movement_input_system(
    keyboard_input: Res<Input<KeyCode>>, 
    mut heads: Query<&mut Head>) {
    if let Some(mut head) = heads.iter_mut().next() {
        let dir: Direction = if keyboard_input.pressed(KeyCode::A) {
            Direction::Left
        } else if keyboard_input.pressed(KeyCode::S) {
            Direction::Down
        } else if keyboard_input.pressed(KeyCode::W) {
            Direction::Up
        } else if keyboard_input.pressed(KeyCode::D) {
            Direction::Right
        } else {
            head.direction
        };
        if dir != head.direction.opposite() {
            head.direction = dir;
        }
    }
}

Ao executarmos cargo run podemos ver a cobra se movimentando sozinha e obedecendo o sistema de direção. Próximo passo é adicionarmos o rabo.