Система отрисовки
Пришло время для нашей первой системы — системы отрисовки. Она должна уметь отображать все наши сущности на экране.
Настройка
Первым делом мы определим структуру RenderingSystem
, которая будет нужна для доступа к контексту ggez
чтобы запускать отрисовку.
Здесь мы встречаемся с новым синтаксисом. 'a
— это аннотация жизненного цикла. Она нужна для того, чтобы компилятор мог знать, как долго ссылка в RenderingSystem
будет доступна.
ЕЩЁ: Узнать больше про жизненные циклы вы можете здесь.
Теперь давайте реализуем типаж System
для нашей системы отрисовки. Пока ничего нового особенно нет — мы просто ведём подготовительные работы. Определение SystemData
автоматически означает, что у нас будет доступ к хранилищу позиций и отрисовываемым компонентам. Это хранилище открыто только для чтения, поэтому у нас будет только неизменяемый доступ. Но это-то нам как раз и нужно!
После чего запустим нашу систему отрисовки в цикле рисования. Это значит, что каждый раз, когда игра будет обновляться, мы будем отрисовывать последнее состояние всех наших сущностей.
После запуска игры всё должно скомпилироваться успешно, но, скорее всего, ничего пока не произойдёт — потому что у нас нет никакой реализации системы отрисовки и никаких сущностей.
Реализация системы отрисовки
Ниже — реализация системы отрисовки. Она делает следующее:
- Очищает экран (мы должны быть уверены, что на нем не осталось никаких отработанных состояний с предыдущего кадра).
- Получает все отрисовываемые сущности и сортирует их по z (мы должны убедиться, что одни объекты рисуются поверх других — например, что пол находится за игроком, иначе мы не сможем его увидеть).
- Проходит по всем отсортированным сущностям и отрисовывает каждую как изображение.
- И напоследок отображает всё на экране.
Добавление тестовых сущностей
Давайте создадим несколько тестовых сущностей, чтобы убедиться, что всё работает так, как надо.
И теперь соберём всё в единое целое и запустим. Вы должны увидеть что-то такое — и это суперздорово! Теперь у нас есть система отрисовки, и мы наконец-то можем видеть что-то на экране. В дальнейшем мы займёмся работой над геймплеем, чтобы сделать из нашей заготовки настоящую игру.
Итоговый код находится ниже.
Обратите внимание: это очень простая реализация отрисовки — она не справится с большим количеством сущностей. Более продвинутая реализация с использованием пакетной отрисовки находится в Главе 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 */
КОД: Увидеть весь код из данной главы можно здесь.