Цветные коробки
Пришло время немного разнообразить нашу игру! Пока процесс всё ещё простоват: ставь себе коробки на правильные места. Давайте сделаем её чуть более увлекательной, добавив коробкам цвет. Мы начнём с красной и синей — но вы можете их раскрасить так, как вам будет угодно! Теперь, чтобы выиграть, вам нужно будет поставить коробку на место с тем же цветом.
Ресурсы
Для начала давайте добавим новые ресурсы. Сохраните эти картинки с помощью контекстного меню — или создайте свои!
Структура директорий должна выглядеть схожим образом (не забудьте о том, что мы удалили стандартные коробку и место для коробки)
├── 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 // ANCHOR: box_colour_display impl Display for BoxColour { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { }
После чего используем это перечисление и в коробке, и в месте для неё:
#![allow(unused)] fn main() { // components.rs } pub struct BoxSpot { pub colour: BoxColour, } // ANCHOR_END: box pub struct Movable; pub struct Immovable; }
Создание сущностей
Давайте добавим цвет как параметр при создании коробок и мест для них и убедимся в том, что мы назначаем правильный ресурс — согласно цвету в перечислении.
Чтобы не ошибиться, прописывая пути к ресурсам, нам нужны понятные имена. Для этого мы будем называть изображения в виде "/images/box_{}.png"
, где {}
— это цвет коробки, которую мы создаём. Трудность в том, что сейчас наши цвета хранятся в перечислении и компилятор не имеет понятия, как преобразовать BoxColour::Red
в строку "red"
. А ведь это было бы очень удобно — просто написать colour.to_string()
и получить нужный цвет. К счастью, у Rust есть отличный способ, которым мы можем это осуществить. Для этого нам будет нужно реализовать типаж Display
для перечисления BoxColour
. Вот как это должно выглядеть — мы просто определяем способ, которым нужно преобразовать элементы перечисления в строки:
#![allow(unused)] fn main() { // components.rs BoxColour::Blue => "blue", })?; Ok(()) } } // ANCHOR_END: box_colour_display // ANCHOR: box pub struct Box { pub colour: BoxColour, }
Теперь добавим перечисление colour
в код создания сущностей и воспользуемся великолепным colour.to_string()
, который мы только что сделали.
#![allow(unused)] fn main() { // entities.rs Position { z: 10, ..position }, Renderable { path: format!("/images/box_{}.png", colour), }, Box { colour }, Movable {}, )) } pub fn create_box_spot(world: &mut World, position: Position, colour: BoxColour) -> Entity { world.spawn(( Position { z: 9, ..position }, Renderable { path: format!("/images/box_spot_{}.png", colour), }, BoxSpot { colour }, )) } // ANCHOR_END: create_box pub fn create_player(world: &mut World, position: Position) -> Entity { world.spawn(( }
Карта
Теперь немного изменим код генерации карты, чтобы добавить цветные коробки и места для них:
- "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 hecs::World; // ANCHOR: initialize_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()); } // ANCHOR_END: initialize_level 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 // ANCHOR: map_match 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), } // ANCHOR_END: map_match } } } }
И обновим её код в main
.
#![allow(unused)] fn main() { // main.rs // Create the game state let game = Game { world }; // Run the main event loop event::run(context, event_loop, game) } // ANCHOR_END: main /* ANCHOR_END: all */ }
Игровой процесс
Теперь, когда самая тяжёлая часть позади, мы можем проверить работу нашего кода. Вы увидите, что почти всё работает — за исключением одного досадного бага. Уровень считается пройденным даже если вы ставите красную коробку на синее место — и наоборот. Давайте это исправим.
Прежде мы выяснили, что данные хранятся в компонентах, а поведение свойственно системам — таковы принципы ECS. Сейчас нам нужно реализовать какое-то поведение, а потому логично предположить, что это будет система. Помните, как мы писали систему для проверки того, выиграл игрок или нет? Так вот — мы снова возвращаемся к ней.
Немного изменим функцию запуска, чтобы проверять совпадение цвета коробки и места для неё:
#![allow(unused)] fn main() { // gameplay_state_system.rs {{#include ../../../code/rust-sokoban-c03-01/src/systems/gameplay_state_system.rs:20:52}} }
Теперь, если вы скомпилируете этот код, он должен ругаться на то, что мы сравниваем два перечисления с помощью оператора ==
. По умолчанию Rust не знает, как это делать, так что мы должны его научить. И лучший способ — реализовать сравнение с помощью типажа PartialEq
.
#![allow(unused)] fn main() { // components.rs // ANCHOR: box_colour_display impl Display for BoxColour { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { }
Самое время обсудить аннотации 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
можно здесь, а о наследуемых типажах — здесь.
Теперь мы можем скомпилировать наш код и вкусить плоды тяжёлых трудов: игра работает и поздравляет с победой только в том случае, если мы её действительно заслужили!
КОД: Увидеть весь код из данной главы можно здесь.