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.