Цветные коробки
Пришло время немного разнообразить нашу игру! Пока процесс всё ещё простоват: ставь себе коробки на правильные места. Давайте сделаем её чуть более увлекательной, добавив коробкам цвет. Мы начнём с красной и синей — но вы можете их раскрасить так, как вам будет угодно! Теперь, чтобы выиграть, вам нужно будет поставить коробку на место с тем же цветом.
Ресурсы
Для начала давайте добавим новые ресурсы. Сохраните эти картинки с помощью контекстного меню — или создайте свои!
Структура директорий должна выглядеть схожим образом (не забудьте о том, что мы удалили стандартные коробку и место для коробки)
├── resources
│ └── images
│ ├── box_blue.png
│ ├── box_red.png
│ ├── box_spot_blue.png
│ ├── box_spot_red.png
│ ├── floor.png
│ ├── player.png
│ └── wall.png
├── src
│ ├── systems
│ │ ├── gameplay_state_system.rs
│ │ ├── input_system.rs
│ │ ├── mod.rs
│ │ └── rendering_system.rs
│ ├── components.rs
│ ├── constants.rs
│ ├── entities.rs
│ ├── main.rs
│ ├── map.rs
│ └── resources.rs
├── Cargo.lock
├── Cargo.toml
Изменения компонентов
Теперь добавим перечисление для цветов (если двух вам окажется мало, то добавлять новые нужно будет именно здесь):
#![allow(unused)] fn main() { // components.rs pub enum BoxColour { Red, Blue, } }
После чего используем это перечисление и в коробке, и в месте для неё:
#![allow(unused)] fn main() { // components.rs #[derive(Component)] #[storage(VecStorage)] pub struct Box { pub colour: BoxColour, } #[derive(Component)] #[storage(VecStorage)] pub struct BoxSpot { pub colour: BoxColour, } }
Создание сущностей
Давайте добавим цвет как параметр при создании коробок и мест для них и убедимся в том, что мы назначаем правильный ресурс — согласно цвету в перечислении.
Чтобы не ошибиться, прописывая пути к ресурсам, нам нужны понятные имена. Для этого мы будем называть изображения в виде "/images/box_{}.png"
, где {}
— это цвет коробки, которую мы создаём. Трудность в том, что сейчас наши цвета хранятся в перечислении и компилятор не имеет понятия, как преобразовать BoxColour::Red
в строку "red"
. А ведь это было бы очень удобно — просто написать colour.to_string()
и получить нужный цвет. К счастью, у Rust есть отличный способ, которым мы можем это осуществить. Для этого нам будет нужно реализовать типаж Display
для перечисления BoxColour
. Вот как это должно выглядеть — мы просто определяем способ, которым нужно преобразовать элементы перечисления в строки:
#![allow(unused)] fn main() { // components.rs impl Display for BoxColour { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { BoxColour::Red => "red", BoxColour::Blue => "blue", })?; Ok(()) } } }
Теперь добавим перечисление colour
в код создания сущностей и воспользуемся великолепным colour.to_string()
, который мы только что сделали.
#![allow(unused)] fn main() { // entities.rs pub fn create_box(world: &mut World, position: Position, colour: BoxColour) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { path: format!("/images/box_{}.png", colour), }) .with(Box { colour }) .with(Movable) .build(); } pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) { world .create_entity() .with(Position { z: 9, ..position }) .with(Renderable { path: format!("/images/box_spot_{}.png", colour), }) .with(BoxSpot { colour }) .build(); } }
Карта
Теперь немного изменим код генерации карты, чтобы добавить цветные коробки и места для них:
- "BB" — синяя коробка (blue box)
- "RB" — красная коробка (red box)
- "BS" — синее место (blue spot)
- "RS" — красное место (red spot)
#![allow(unused)] fn main() { // map.rs use crate::components::{BoxColour, Position}; use crate::entities::*; use specs::World; pub fn load_map(world: &mut World, map_string: String) { // read all lines let rows: Vec<&str> = map_string.trim().split('\n').map(|x| x.trim()).collect(); for (y, row) in rows.iter().enumerate() { let columns: Vec<&str> = row.split(' ').collect(); for (x, column) in columns.iter().enumerate() { // Create the position at which to create something on the map let position = Position { x: x as u8, y: y as u8, z: 0, // we will get the z from the factory functions }; // Figure out what object we should create match *column { "." => create_floor(world, position), "W" => { create_floor(world, position); create_wall(world, position); } "P" => { create_floor(world, position); create_player(world, position); } "BB" => { create_floor(world, position); create_box(world, position, BoxColour::Blue); } "RB" => { create_floor(world, position); create_box(world, position, BoxColour::Red); } "BS" => { create_floor(world, position); create_box_spot(world, position, BoxColour::Blue); } "RS" => { create_floor(world, position); create_box_spot(world, position, BoxColour::Red); } "N" => (), c => panic!("unrecognized map item {}", c), } } } } }
И обновим её код в main
.
#![allow(unused)] fn main() { // main.rs // Initialize the level pub fn initialize_level(world: &mut World) { const MAP: &str = " N N W W W W W W W W W . . . . W W . . . BB . . W W . . RB . . . W W . P . . . . W W . . . . RS . W W . . BS . . . W W . . . . . . W W W W W W W W W "; load_map(world, MAP.to_string()); } }
Игровой процесс
Теперь, когда самая тяжёлая часть позади, мы можем проверить работу нашего кода. Вы увидите, что почти всё работает — за исключением одного досадного бага. Уровень считается пройденным даже если вы ставите красную коробку на синее место — и наоборот. Давайте это исправим.
Прежде мы выяснили, что данные хранятся в компонентах, а поведение свойственно системам — таковы принципы ECS. Сейчас нам нужно реализовать какое-то поведение, а потому логично предположить, что это будет система. Помните, как мы писали систему для проверки того, выиграл игрок или нет? Так вот — мы снова возвращаемся к ней.
Немного изменим функцию запуска, чтобы проверять совпадение цвета коробки и места для неё:
#![allow(unused)] fn main() { // gameplay_state_system.rs fn run(&mut self, data: Self::SystemData) { let (mut gameplay_state, positions, boxes, box_spots) = data; // get all boxes indexed by position let boxes_by_position: HashMap<(u8, u8), &Box> = (&positions, &boxes) .join() .map(|t| ((t.0.x, t.0.y), t.1)) .collect::<HashMap<_, _>>(); // loop through all box spots and check if there is a corresponding // box at that position. since we now have different types of boxes // we need to make sure the right type of box is on the right // type of spot. for (box_spot, position) in (&box_spots, &positions).join() { if let Some(the_box) = boxes_by_position.get(&(position.x, position.y)) { if the_box.colour == box_spot.colour { // continue } else { // return, haven't won yet return; } } else { gameplay_state.state = GameplayState::Playing; return; } } // If we made it this far, then all box spots have boxes on them, and the // game has been won gameplay_state.state = GameplayState::Won; } } }
Теперь, если вы скомпилируете этот код, он должен ругаться на то, что мы сравниваем два перечисления с помощью оператора ==
. По умолчанию Rust не знает, как это делать, так что мы должны его научить. И лучший способ — реализовать сравнение с помощью типажа PartialEq
.
#![allow(unused)] fn main() { // components.rs #[derive(PartialEq)] pub enum BoxColour { Red, Blue, } }
Самое время обсудить аннотации derive
. Мы уже использовали их ранее, но не погружались в то, для чего они на самом деле нужны. Атрибуты derive
можно применить к структурам или перечислениям — они позволят добавить стандартные реализации типажей к нашим типам. Например, здесь мы сообщаем Rust, что хотим добавить PartialEq
к перечислению BoxColour
.
Так выглядит стандартная реализация сравнения в PartialEq
. Она просто проверяет что-то на равенство самому себе. Если это так, то сравнение успешно, иначе — нет. Не переживайте, если это не особенно внесло ясность.
#![allow(unused)] fn main() { pub trait PartialEq { fn eq(&self, other: &Self) -> bool; fn ne(&self, other: &Self) -> bool { !self.eq(other) }; } }
С помощью строчки #[derive(PartialEq)]
мы поставили Rust в известность, что BoxColour
теперь реализует этот типаж. И это значит, что если мы попытаемся выполнить box_colour_1 == box_colour_2
, Rust будет использовать именно эту реализацию, которая просто проверяет, являются ли colour_1
и colour_2
одним и тем же объектом. Это не самое изящное сравнение на свете, но к нашему случаю оно вполне подойдёт.
ЕЩЁ: Узнать больше о
PartialEq
можно здесь, а о наследуемых типажах — здесь.
Теперь мы можем скомпилировать наш код и вкусить плоды тяжёлых трудов: игра работает и поздравляет с победой только в том случае, если мы её действительно заслужили!
КОД: Увидеть весь код из данной главы можно здесь.