Система отрисовки

Пришло время для нашей первой системы — системы отрисовки. Она должна уметь отображать все наши сущности на экране.

Настройка

Первым делом мы определим структуру RenderingSystem, которая будет нужна для доступа к контексту ggez чтобы запускать отрисовку.


#![allow(unused)]
fn main() {
// ANCHOR: init
// Initialize the level
pub fn initialize_level(world: &mut World) {
}

Здесь мы встречаемся с новым синтаксисом. 'a — это аннотация жизненного цикла. Она нужна для того, чтобы компилятор мог знать, как долго ссылка в RenderingSystem будет доступна.

ЕЩЁ: Узнать больше про жизненные циклы вы можете здесь.

Теперь давайте реализуем типаж System для нашей системы отрисовки. Пока ничего нового особенно нет — мы просто ведём подготовительные работы. Определение SystemData автоматически означает, что у нас будет доступ к хранилищу позиций и отрисовываемым компонентам. Это хранилище открыто только для чтения, поэтому у нас будет только неизменяемый доступ. Но это-то нам как раз и нужно!


#![allow(unused)]
fn main() {
        world,
        Position {
            x: 0,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
        // implementation here
    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
}

После чего запустим нашу систему отрисовки в цикле рисования. Это значит, что каждый раз, когда игра будет обновляться, мы будем отрисовывать последнее состояние всех наших сущностей.


#![allow(unused)]
fn main() {
        Position { z: 10, ..position },
        Renderable {
            path: "/images/wall.png".to_string(),
        },
        Wall {},
    ))
}
pub fn create_floor(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 5, ..position },
        Renderable {
            path: "/images/floor.png".to_string(),
        },
    ))
}
}

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

Реализация системы отрисовки

Ниже — реализация системы отрисовки. Она делает следующее:

  • Очищает экран (мы должны быть уверены, что на нем не осталось никаких отработанных состояний с предыдущего кадра).
  • Получает все отрисовываемые сущности и сортирует их по z (мы должны убедиться, что одни объекты рисуются поверх других — например, что пол находится за игроком, иначе мы не сможем его увидеть).
  • Проходит по всем отсортированным сущностям и отрисовывает каждую как изображение.
  • И напоследок отображает всё на экране.

#![allow(unused)]
fn main() {
        },
    );
    create_wall(
        world,
        Position {
            x: 1,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_box(
        world,
        Position {
            x: 2,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
}
// ANCHOR_END: init

// ANCHOR: handler
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
}

Добавление тестовых сущностей

Давайте создадим несколько тестовых сущностей, чтобы убедиться, что всё работает так, как надо.


#![allow(unused)]
fn main() {
    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = context_builder.build()?;

    // 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 */
}

И теперь соберём всё в единое целое и запустим. Вы должны увидеть что-то такое — и это суперздорово! Теперь у нас есть система отрисовки, и мы наконец-то можем видеть что-то на экране. В дальнейшем мы займёмся работой над геймплеем, чтобы сделать из нашей заготовки настоящую игру.

Screenshot

Итоговый код находится ниже.

Обратите внимание: это очень простая реализация отрисовки — она не справится с большим количеством сущностей. Более продвинутая реализация с использованием пакетной отрисовки находится в Главе 3 — Пакетная отрисовка.

/* ANCHOR: all */
// Rust sokoban
// main.rs

use ggez::{
    conf, event,
    graphics::{self, DrawParam, Image},
    Context, GameResult,
};
use glam::Vec2;
use hecs::{Entity, World};

use std::path;

const TILE_WIDTH: f32 = 32.0;

// ANCHOR: components
pub struct Position {
    x: u8,
    y: u8,
    z: u8,
}

pub struct Renderable {
    path: String,
}

pub struct Wall {}

pub struct Player {}

pub struct Box {}

pub struct BoxSpot {}

// ANCHOR_END: components

// ANCHOR: game
// This struct will hold all our game state
// For now there is nothing to be held, but we'll add
// things shortly.
struct Game {
    world: World,
}
// ANCHOR_END: game

// ANCHOR: init
// Initialize the level
pub fn initialize_level(world: &mut World) {
    create_player(
        world,
        Position {
            x: 0,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_wall(
        world,
        Position {
            x: 1,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
    create_box(
        world,
        Position {
            x: 2,
            y: 0,
            z: 0, // we will get the z from the factory functions
        },
    );
}
// ANCHOR_END: init

// ANCHOR: handler
impl event::EventHandler<ggez::GameError> for Game {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        Ok(())
    }

    fn draw(&mut self, context: &mut Context) -> GameResult {
        // Render game entities
        {
            run_rendering(&self.world, context);
        }

        Ok(())
    }
}
// ANCHOR_END: handler

// ANCHOR: entities
pub fn create_wall(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/wall.png".to_string(),
        },
        Wall {},
    ))
}
pub fn create_floor(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 5, ..position },
        Renderable {
            path: "/images/floor.png".to_string(),
        },
    ))
}

pub fn create_box(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/box.png".to_string(),
        },
        Box {},
    ))
}

pub fn create_box_spot(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 9, ..position },
        Renderable {
            path: "/images/box_spot.png".to_string(),
        },
        BoxSpot {},
    ))
}

pub fn create_player(world: &mut World, position: Position) -> Entity {
    world.spawn((
        Position { z: 10, ..position },
        Renderable {
            path: "/images/player.png".to_string(),
        },
        Player {},
    ))
}
// ANCHOR_END: entities

// ANCHOR: rendering_system
fn run_rendering(world: &World, context: &mut Context) {
    // Clearing the screen (this gives us the background colour)
        let mut canvas =
        graphics::Canvas::from_frame(context, graphics::Color::from([0.95, 0.95, 0.95, 1.0]));

    // Get all the renderables with their positions and sort by the position z
    // This will allow us to have entities layered visually.
    let mut query = world.query::<(&Position, &Renderable)>();
    let mut rendering_data: Vec<(Entity, (&Position, &Renderable))> = query.into_iter().collect();
    rendering_data.sort_by_key(|&k| k.1 .0.z);

    // Iterate through all pairs of positions & renderables, load the image
    // and draw it at the specified position.
    for (_, (position, renderable)) in rendering_data.iter() {
        // Load the image
        let image = Image::from_path(context, renderable.path.clone()).unwrap();
        let x = position.x as f32 * TILE_WIDTH;
        let y = position.y as f32 * TILE_WIDTH;

        // draw
        let draw_params = DrawParam::new().dest(Vec2::new(x, y));
        canvas.draw(&image, draw_params);
    }

    // Finally, present the canvas, this will actually display everything
    // on the screen.
    canvas.finish(context).expect("expected to present");
}
// ANCHOR_END: rendering_system

// ANCHOR: main
pub fn main() -> GameResult {
    let world = World::new();

    // Create a game context and event loop
    let context_builder = ggez::ContextBuilder::new("rust_sokoban", "sokoban")
        .window_setup(conf::WindowSetup::default().title("Rust Sokoban!"))
        .window_mode(conf::WindowMode::default().dimensions(800.0, 600.0))
        .add_resource_path(path::PathBuf::from("./resources"));

    let (context, event_loop) = context_builder.build()?;

    // 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 */

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