Adicionando um Rabo a Cobra

O rabo da cobra é uma parte um pouco mais complexa, pois para cada segmento é preciso saber o próximo segmento e para onde cada segmento esta se movendo. Assim, a forma mais simples de resolver este problema é adicionando todos os segmentos do rabo da cobra em um Vec e manter eles em um recurso ordenado em vez de entidades e componentes simples. Essa mudança nos garante que quando atualizarmos a posição de um segmento, atualizamos seu valor com relação ao segmento anterior. Isso pode ser feito iterando sobre todos os segmentos em pares com a função windows que nos permite acessar o elemento anterior e o atual da lista. Outro elemento importante é que precisamos definir uma cor para o rabo da cobra, define que seria um tom de rosa com:

// snake.rs
const SNAKE_SEGMENT_COLOR: Color = Color::rgb(0.8, 0.0, 0.8);

Agora que definimos uma cor, podemos começar a pensar no primeiro teste. O teste mais simples para este caso parece ser verificar se a entidade relacionada aos segmentos da cobra possui 2 segmento inicialmente:

// snake.rs
#[test]
fn entity_snake_has_two_segments() {
    // Setup app
    let mut app = App::new();

    // Adicionar sistema de spawn e recurso com segmentos
    app
        .insert_resource(Segments::default())
        .add_startup_system(spawn_system);

    // Executar sistema
    app.update();

    // Buscar todas entidades com componente `Segment`
    let mut query = app.world.query_filtered::<Entity, With<Segment>>();
    assert_eq!(query.iter(&app.world).count(), 2);
}

Executando esse teste vemos que precisamos adicionar o recurso Segments ao nosso código, um vetor de entidades como falamos anteriormente:

// snake.rs
#[derive(Default, Deref, DerefMut)]
pub struct Segments(Vec<Entity>);

Com isso, precisamos reescrever nosso snake::spawn_system para utilizar Segments. Como é um startup system, será executado para iniciar entidades do jogo:

pub fn spawn_system(mut commands: Commands, mut segments: ResMut<Segments>) {
    *segments = Segments(vec![
        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(Segment)
            .insert(Position { x: 3, y: 3 })
            .insert(Size::square(0.8))
            .id(),
    ]);
}

Neste novo spawn_system recebemos o recurso Segments como um recurso mutável mut segments: ResMut<Segments> e definimos segments através de uma dereferência mutável, #[derive(.., DerefMut)], reasignando o Segment::default(), que equivale a um vetor vazio, ao nosso recem adicionado componente. Outra mudança que precisamos fazer é adicionar o recurso Segments ao nosso startup do app na main.rs e em todos testes de snake.rs:

// main.rs

fn main() {
    App::new()
        .insert_resource(WindowDescriptor {
            title: "Snake Game".to_string(),
            width: 500.0,
            height: 500.0,
            ..default()
        })
        .insert_resource(snake::Segments::default()) // <-- adicionar
        .add_startup_system(snake::spawn_system)
        // ...
        .run();
}

// snake.rs
#[test]
fn entity_has_snake_head() {
    let mut app = App::new();

    app
        .insert_resource(Segments::default()) // <-- adicionar em todos testes
        .add_startup_system(spawn_system);

    app.update();

    let mut query = app.world.query_filtered::<Entity, With<Head>>();
    assert_eq!(query.iter(&app.world).count(), 1);
}

Agora, executando nossos testes percebemos que o novo teste, entity_snake_has_two_segments, falha por possuir somente uma entidade. Para isso, precisamos criar um novo sistema que adiciona um novo Segment em posição específica e retorna uma Entity id para adicionarmos no recurso Segments. Este sistema que cria um novo segmento é bastante semelhante ao segmento que instancia a cobra em si, spawn_system, sua maior diferença é o fato de que passamos uma posição, Position, como argumento para o segmento e que o tamanho do quadrado é menor, com coloração diferente.

pub fn spawn_segment_system(mut commands: Commands, position: Position) -> Entity {
    commands
        .spawn_bundle(SpriteBundle {
            sprite: Sprite {
                color: SNAKE_SEGMENT_COLOR,
                ..default()
            },
            transform: Transform {
                    scale: Vec3::new(10.0, 10.0, 10.0),
                    ..default()
                },
            ..default()
        })
        .insert(Segment)
        .insert(position)
        .insert(Size::square(0.65))
        .id()
}

Com este novo sistema, podemos adicionar um segmento extra no nosso snake::spawn_system, que armazenará os segmentos cabeça e o recem criado como entidades em Segments:

pub fn spawn_system(mut commands: Commands, mut segments: ResMut<Segments>) {
    *segments = Segments(vec![
        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(Segment)
            .insert(Position { x: 3, y: 3 })
            .insert(Size::square(0.8))
            .id(),
            spawn_segment_system(commands, Position { x: 3, y: 2 }), // <-- novo segmento
    ]);
}

Ao executarmos os teste agora, vemos que todos passam e podemos focar no próximo passo, fazer os segmentos se moverem seguindo a cabeça.

Fazendo o rabo seguir a cabeça

Anteriormente já falamos que os segmentos da cobra estão armazenados em um Vec e podemos iterar sobre pares destes elementos utilizando a função windows, agora nos falta entender como criar um teste para verificar se um segmento assume a posição de seu antecessor ao andar, mantendo a direção de movimento. Assim, nosso novo teste considera que a cobra é inicializada na posição x = 3 e y = 3, e seu segmento extra em x = 3 e y = 2. A primeira parte do teste define as novas posições da cabeça, Head, e do segment, Segment, com let new_position_head_right = Position { x: 4, y: 3 }; e let new_position_segment_right = Position { x: 3, y: 3 };, ou seja, Head se move para x = 4 e y = 3 e segment para x = 3 e y = 3 ao apertarmos a tecla D e executarmos um novo update:

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

    // Adiciona os systemas
    app.insert_resource(Segments::default())
        .add_startup_system(spawn_system)
        .add_system(movement_system)
        .add_system(movement_input_system.before(movement_system));

    // adiciona resource apertando a tecla D, movimento para direita
    let mut input = Input::<KeyCode>::default();
    input.press(KeyCode::D);
    app.insert_resource(input);

    // executa sistemas
    app.update();

    let mut query = app.world.query::<(&Head, &Position)>();
    query.iter(&app.world).for_each(|(head, position)| {
        // garante que nova posição da cabeçá é esperada:
        assert_eq!(&new_position_head_right, position); 
        // garante que nova direção é para direita:
        assert_eq!(head.direction, Direction::Right);
    });

    let mut query = app.world.query::<(&Segment, &Position, Without<Head>)>();
    query.iter(&app.world).for_each(|(_segment, position, _)| {
        // garante que nova posição do segmento é esperada:
        assert_eq!(&new_position_segment_right, position);
    });
    // ...
}

Por descargo de consciência podemos adicionar mais uma parte ao teste que muda a direção de movimento da cobra para cima:

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

    // Adiciona os systemas
    app.insert_resource(Segments::default())
        .add_startup_system(spawn_system)
        .add_system(movement_system)
        .add_system(movement_input_system.before(movement_system));

    // adiciona resource apertando a tecla D, movimento para direita
    let mut input = Input::<KeyCode>::default();
    input.press(KeyCode::D);
    app.insert_resource(input);

    // executa sistemas
    app.update();

    let mut query = app.world.query::<(&Head, &Position)>();
    query.iter(&app.world).for_each(|(head, position)| {
        // garante que nova posição da cabeça é esperada:
        assert_eq!(&new_position_head_right, position); 
        // garante que nova direção é para direita:
        assert_eq!(head.direction, Direction::Right);
    });

    let mut query = app.world.query::<(&Segment, &Position, Without<Head>)>();
    query.iter(&app.world).for_each(|(_segment, position, _)| {
        // garante que nova posição do segmento é esperada:
        assert_eq!(&new_position_segment_right, position);
    });

    // NOVAS POSIÇÕES ESPERADAS
    let new_position_head_up = Position { x: 4, y: 4 }; // <--
    let new_position_segment_up = Position { x: 4, y: 3 }; // <--

    // adiciona resource apertando a tecla W, movimento para cima
    let mut input = Input::<KeyCode>::default();
    input.press(KeyCode::W); // <--
    app.insert_resource(input);

    // executa sistemas de novo
    app.update();

    let mut query = app.world.query::<(&Head, &Position)>();
    query.iter(&app.world).for_each(|(head, position)| {
        // garante que nova posição da cabeça é esperada:
        assert_eq!(&new_position_head_up, position);
        // garante que nova direção da cabeça é esperada:
        assert_eq!(head.direction, Direction::Up); 
    });

    let mut query = app.world.query::<(&Segment, &Position, Without<Head>)>();
    query.iter(&app.world).for_each(|(_segment, position, _)| {
        // garante que nova posição do segmento é esperada:
        assert_eq!(&new_position_segment_up, position);
    })
}

Ao exercutarmos este código veremos que o assert assert_eq!(&new_position_segment_right, position); falha indicando que o segmento não se moveu, mas a cabeça se moveu. Como estamos falando de movimento, sabemos que precisamos alterar o sistema movement_system para incluir uma iteração sobre os elementos de Segments. Assim a primeira alteração é adicionar o recurso segments nos argumentos do sistema, segments: ResMut<Segments>. Depois disso vamos precisar extrair duas queries diferentes para posição, Position, e cabeça, Head, já que agora nem todas posições terão cabeças, mas todas posições terão Segment, fazemos isso com as queries mut heads: Query<(Entity, &Head)> e mut positions: Query<(Entity, &Segment, &mut Position)>. Precisamos de Segment, pois Food também possui Position e queremos evitar iterar por elementos desnecessarios em um ECS.

O código que vamos adicionar ao movement_system é simples, basicamente iteramos por segments dois a dois elementos por vez e adicionamos a posição do elementos anterior, entity[0], na posição do elemento posterior, entity[1]. Algo como:

(*segments).windows(2).for_each(|entity| {
    if let Ok((_, _segment, mut position)) = positions.get_mut(entity[1]) {
        if let Ok((_, _, mut new_position)) = positions.get_mut(entity[0]) {
            *position = new_position.clone();
        }
    };
});

Infelizmente, esse código em particular não compila devido ao fato de acessarmos positions como referência mutável duas vezes, inverter uma referência imutável e outra mutável também não funcionaria devida a uma regra do borrow checker do Rust que impede que um mesmo valor seja acesso como mutável e imutável no mesmo bloco. O mesmo acontece com duas referências diferentes mutáveis no mesmo bloco, o compilador não teria garantia que ambas não mutaria o mesmo valor ao mesmo tempo. Uma solução para este problema é adicionar um clone de positions, mas podemos fazer este clone ser mais inteligente, transformando ele em um HashMap na qual a chave é a entidade e a position é o valor, para depois acessarmos este clone de positions de forma imutável:

pub fn movement_system(
    segments: ResMut<Segments>,
    mut heads: Query<(Entity, &Head)>,
    mut positions: Query<(Entity, &Segment, &mut Position)>,
) {
    // Criar um hashmap clonado de positions com `Entity => Position`
    let positions_clone: HashMap<Entity, Position> = positions
        .iter()
        .map(|(entity, _segment, position)| (entity, position.clone()))
        .collect();
    // Acessar a cabeça (única existente por hora)
    if let Some((id, head)) = heads.iter_mut().next() {
        // Iterar sobre segments 2 a 2
        (*segments).windows(2).for_each(|entity| {
            // Acessar a posição da `entity[1]` em positions
            if let Ok((_, _segment, mut position)) = positions.get_mut(entity[1]) {
                // Acessar a posição da `entity[0]` em positions_clone
                if let Some(new_position) = positions_clone.get(&entity[0]) {
                    // Substituir position por new_position
                    *position = new_position.clone();
                }
            };
        });

        // mesmo código de antes para mover a cabeça
        let _ = positions.get_mut(id).map(|(_, _segment, mut pos)| {
            match &head.direction {
                Direction::Left => {
                    pos.x -= 1;
                }
                Direction::Right => {
                    pos.x += 1;
                }
                Direction::Up => {
                    pos.y += 1;
                }
                Direction::Down => {
                    pos.y -= 1;
                }
            };
        });
    }
}

Agora sim, nosso teste passa e se executarmos cargo run vemos que o segmento rosa sempre segue a cabeça. Próximo passo é entender como criar um sistema para expandir a cobra ao comer.

Alimentando a cobra

Faz algum tempo que nossa cobra está rodeada de comida, mas não pode comer, por isso chegou a hora de elaborarmos um teste que vai garatir que nossa cobra possa comer e que ela vai crescer ao comer. Nosso teste vai partir de um setup um pouco mais complexo, pois agora precisamos registrar um evento de crescimento, GrowthEvenet, precisamos registrar a última posição do vetor de segmentos, LastTailPosition, o nosso antigo sistema de spawn de comida, crate::food::spawn_system, e um system set organizado o que acontece primeiro. Algo como o seguinte bloco:

app.insert_resource(Segments::default())
    .insert_resource(LastTailPosition::default())
    .add_event::<GrowthEvent>()
    .add_startup_system(spawn_system)
    .add_system(crate::food::spawn_system)
    .add_system_set(
        SystemSet::new()
            .with_system(movement_system)
            .with_system(eating_system.after(movement_system))
            .with_system(growth_system.after(eating_system))
    );

Depois disso teremos dois updates, primeiro update cria o contexto de mundo e nos permite verificar que uma comida foi spawnada e que a cobra possui dois segmentos:

app.update();

let mut query = app.world.query::<(&Segment, &Position)>();
assert_eq!(query.iter(&app.world).count(), 2);
let mut query = app.world.query::<(&Food, &Position)>();
assert_eq!(query.iter(&app.world).count(), 1);

Por último, executamos mais uma vez o update e verificamos se a cobra possui 3 segmentos agora:

app.update();

let mut query = app.world.query::<(&Segment, &Position)>();
assert_eq!(query.iter(&app.world).count(), 3);

Infelizmente, um teste somento com isso não nos garantiria sucesso, já que o crate::food::spawn_system gera uma posição aleatória dentro do grid, para isso precisamos modificar a função crate::food::spawn_system para termos controle da posição que a comida vai surgir. Fazemos isso adicionado uma macro de compilação exclusiva de teste, cfg!:

// food.rs
#[allow(clippy::cast_possible_wrap)]
pub fn spawn_system(mut commands: Commands) {
    commands
        .spawn_bundle(SpriteBundle {
            sprite: Sprite {
                color: FOOD_COLOR,
                ..default()
            },
            ..default()
        })
        .insert(Food)
        .insert(Position {
            x: if cfg!(test) { 3 } else { (random::<u16>() % GRID_WIDTH) as i16 }, // <--
            y: if cfg!(test) { 5 } else { (random::<u16>() % GRID_HEIGHT) as i16 }, // <--
        })
        .insert(Size::square(0.65));
}

Agora sim, nosso teste poderá fazer sentido ao compilar:

// snake.rs
#[test]
fn snake_grows_when_eating() {
    // Setup
    let mut app = App::new();

    // sistemas
    app.insert_resource(Segments::default())
        .insert_resource(LastTailPosition::default())
        .add_event::<GrowthEvent>()
        .add_startup_system(spawn_system)
        .add_system(crate::food::spawn_system)
        .add_system_set(
            SystemSet::new()
                .with_system(movement_system)
                .with_system(eating_system.after(movement_system))
                .with_system(growth_system.after(eating_system))
        );

    // update de configuração
    app.update();

    let mut query = app.world.query::<(&Segment, &Position)>();
    assert_eq!(query.iter(&app.world).count(), 2);
    let mut query = app.world.query::<(&Food, &Position)>();
    assert_eq!(query.iter(&app.world).count(), 1);

    // update de execução
    app.update();

    let mut query = app.world.query::<(&Segment, &Position)>();
    assert_eq!(query.iter(&app.world).count(), 3);
}

Pronto, agora podemos ir jantar, mas, infelizmente, nossa cobra ainda não. Para isso, começamos com o snake::eating_system. O objetivo de snake::eating_system é simples, iterar sobre todas as entidades com componente comida, Food, e ver se a posição delas, Position, é igual a Position das entidades com Head. Caso, a firmação anterior seja verdade, removemos, despawn, a entidade Food e lançamos no sistema um evento para crescer a cobra, GrowthEvent, na sua última posição, pub struct LastTailPosition(Option<Position>). Um detalhe importante, é que para publicar um evento, EventWriter, precisamos registrar esse evento no App, com add_event::<GrowthEvent>(), o mesmo vale para lermos os eventos, EventReader.

pub struct GrowthEvent;

#[derive(Default)]
pub struct LastTailPosition(Option<Position>);

pub fn eating_system(
    mut commands: Commands,
    mut growth_writer: EventWriter<GrowthEvent>,
    food_positions: Query<(Entity, &Position), With<Food>>,
    head_positions: Query<&Position, With<Head>>,
) {
    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);
            }
        }
    }
}

Na função anterior, recebemos um EventWriter do tipo GrowthEvent, que vai publicar quaisquer eventos necessários, uma query com todas as entidades e posições de comidas, Query<(Entity, &Position), With<Food>> e uma query com a posição das cabeças, no caso, apenas uma, Query<&Position, With<Head>>. Iteramos por tudo e checamos se as posições são iguais para então removermos a entidade associada a comida e publicarmos um evento de crescimento. Próximo passo é lermos o evento com EventReader<GrowthEvent> e adicionarmos um clone da última posição do rabo, como um novo segmento, aos segmentos, Segments:

pub fn growth_system(
    commands: Commands,
    last_tail_position: Res<LastTailPosition>,
    mut segments: ResMut<Segments>,
    mut growth_reader: EventReader<GrowthEvent>,
) {
    if growth_reader.iter().next().is_some() {
        segments.push(spawn_segment_system(commands, last_tail_position.0.clone().unwrap()));
    }
}

Nosso teste está quase passando, precisamos adicionar a informação da última posição de segmentos quando nos movimentamos, senão LastTailPositionserá sempre None e nossa cobra não crescerá. Para isso, adicionamos o recuso mutável, ResMut, ao sistema movement_system e no final dele, buscamos a última posição dos segmentos:

pub fn movement_system(
    segments: ResMut<Segments>,
    mut last_tail_position: ResMut<LastTailPosition>,
    mut heads: Query<(Entity, &Head)>,
    mut positions: Query<(Entity, &Segment, &mut Position)>,
) {
    let positions_clone: HashMap<Entity, Position> = positions
        .iter()
        .map(|(entity, _segment, position)| (entity, position.clone()))
        .collect();
    if let Some((id, head)) = heads.iter_mut().next() {
        (*segments).windows(2).for_each(|entity| {
            if let Ok((_, _segment, mut position)) = positions.get_mut(entity[1]) {
                if let Some(new_position) = positions_clone.get(&entity[0]) {
                    *position = new_position.clone();
                }
            };
        });
        // ...
        *last_tail_position = LastTailPosition(Some(positions_clone.get(segments.last().unwrap()).unwrap().clone())); // <--
    }
}

Aghora sim, nosso teste passa! Sabrmos que podemos utilizar unwrap, pois a cobra inicia seu movimento com 2 segmentos e sabemos que ambos os segmentos possuem Position, assim, positions_clone.get , também, nunca será None. Possivelmente precisaremos adicionar alguns .insert_resource(LastTailPosition::default())no setup dos testes.

No próximo capítulo vamos aprender um pouco mais sobre colisões.