Цветные коробки

Пришло время немного разнообразить нашу игру! Пока процесс всё ещё простоват: ставь себе коробки на правильные места. Давайте сделаем её чуть более увлекательной, добавив коробкам цвет. Мы начнём с красной и синей — но вы можете их раскрасить так, как вам будет угодно! Теперь, чтобы выиграть, вам нужно будет поставить коробку на место с тем же цветом.

Ресурсы

Для начала давайте добавим новые ресурсы. Сохраните эти картинки с помощью контекстного меню — или создайте свои!

Blue box Red box Blue box spot Red box spot

Структура директорий должна выглядеть схожим образом (не забудьте о том, что мы удалили стандартные коробку и место для коробки)

├── 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 можно здесь, а о наследуемых типажах — здесь.

Теперь мы можем скомпилировать наш код и вкусить плоды тяжёлых трудов: игра работает и поздравляет с победой только в том случае, если мы её действительно заслужили!

Sokoban play

КОД: Увидеть весь код из данной главы можно здесь.