Система отрисовки
Пришло время для нашей первой системы — системы отрисовки. Она должна уметь отображать все наши сущности на экране.
Настройка
Первым делом мы определим структуру RenderingSystem
, которая будет нужна для доступа к контексту ggez
чтобы запускать отрисовку.
#![allow(unused)] fn main() { pub struct RenderingSystem<'a> { context: &'a mut Context, } }
Здесь мы встречаемся с новым синтаксисом. 'a
— это аннотация жизненного цикла. Она нужна для того, чтобы компилятор мог знать, как долго ссылка в RenderingSystem
будет доступна.
ЕЩЁ: Узнать больше про жизненные циклы вы можете здесь.
Теперь давайте реализуем типаж System
для нашей системы отрисовки. Пока ничего нового особенно нет — мы просто ведём подготовительные работы. Определение SystemData
автоматически означает, что у нас будет доступ к хранилищу позиций и отрисовываемым компонентам. Это хранилище открыто только для чтения, поэтому у нас будет только неизменяемый доступ. Но это-то нам как раз и нужно!
#![allow(unused)] fn main() { // System implementation impl<'a> System<'a> for RenderingSystem<'a> { // Data type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>); fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; // implementation here } } }
После чего запустим нашу систему отрисовки в цикле рисования. Это значит, что каждый раз, когда игра будет обновляться, мы будем отрисовывать последнее состояние всех наших сущностей.
#![allow(unused)] fn main() { 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 { let mut rs = RenderingSystem { context }; rs.run_now(&self.world); } Ok(()) } } }
После запуска игры всё должно скомпилироваться успешно, но, скорее всего, ничего пока не произойдёт — потому что у нас нет никакой реализации системы отрисовки и никаких сущностей.
Реализация системы отрисовки
Ниже — реализация системы отрисовки. Она делает следующее:
- Очищает экран (мы должны быть уверены, что на нем не осталось никаких отработанных состояний с предыдущего кадра).
- Получает все отрисовываемые сущности и сортирует их по z (мы должны убедиться, что одни объекты рисуются поверх других — например, что пол находится за игроком, иначе мы не сможем его увидеть).
- Проходит по всем отсортированным сущностям и отрисовывает каждую как изображение.
- И напоследок отображает всё на экране.
#![allow(unused)] fn main() { fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; // Clearing the screen (this gives us the background colour) graphics::clear(self.context, graphics::Color::new(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 rendering_data = (&positions, &renderables).join().collect::<Vec<_>>(); rendering_data.sort_by_key(|&k| k.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::new(self.context, renderable.path.clone()).expect("expected image"); 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)); graphics::draw(self.context, &image, draw_params).expect("expected render"); } // Finally, present the context, this will actually display everything // on the screen. graphics::present(self.context).expect("expected to present"); } }
Добавление тестовых сущностей
Давайте создадим несколько тестовых сущностей, чтобы убедиться, что всё работает так, как надо.
#![allow(unused)] fn main() { 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 }, ); } }
И теперь соберём всё в единое целое и запустим. Вы должны увидеть что-то такое — и это суперздорово! Теперь у нас есть система отрисовки, и мы наконец-то можем видеть что-то на экране. В дальнейшем мы займёмся работой над геймплеем, чтобы сделать из нашей заготовки настоящую игру.
Итоговый код находится ниже.
Обратите внимание: это очень простая реализация отрисовки — она не справится с большим количеством сущностей. Более продвинутая реализация с использованием пакетной отрисовки находится в Главе 3 — Пакетная отрисовка.
// Rust sokoban // main.rs use glam::Vec2; use ggez::{conf, event, Context, GameResult, graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, }; use std::path; const TILE_WIDTH: f32 = 32.0; // Components #[derive(Debug, Component, Clone, Copy)] #[storage(VecStorage)] pub struct Position { x: u8, y: u8, z: u8, } #[derive(Component)] #[storage(VecStorage)] pub struct Renderable { path: String, } #[derive(Component)] #[storage(VecStorage)] pub struct Wall {} #[derive(Component)] #[storage(VecStorage)] pub struct Player {} #[derive(Component)] #[storage(VecStorage)] pub struct Box {} #[derive(Component)] #[storage(VecStorage)] pub struct BoxSpot {} // Systems pub struct RenderingSystem<'a> { context: &'a mut Context, } // System implementation impl<'a> System<'a> for RenderingSystem<'a> { // Data type SystemData = (ReadStorage<'a, Position>, ReadStorage<'a, Renderable>); fn run(&mut self, data: Self::SystemData) { let (positions, renderables) = data; // Clearing the screen (this gives us the background colour) graphics::clear(self.context, graphics::Color::new(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 rendering_data = (&positions, &renderables).join().collect::<Vec<_>>(); rendering_data.sort_by_key(|&k| k.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::new(self.context, renderable.path.clone()).expect("expected image"); 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)); graphics::draw(self.context, &image, draw_params).expect("expected render"); } // Finally, present the context, this will actually display everything // on the screen. graphics::present(self.context).expect("expected to present"); } } // 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, } // This is the main event loop. ggez tells us to implement // two things: // - updating // - rendering 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 { let mut rs = RenderingSystem { context }; rs.run_now(&self.world); } Ok(()) } } // Register components with the world pub fn register_components(world: &mut World) { world.register::<Position>(); world.register::<Renderable>(); world.register::<Player>(); world.register::<Wall>(); world.register::<Box>(); world.register::<BoxSpot>(); } // Create a wall entity pub fn create_wall(world: &mut World, position: Position) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { path: "/images/wall.png".to_string(), }) .with(Wall {}) .build(); } pub fn create_floor(world: &mut World, position: Position) { world .create_entity() .with(Position { z: 5, ..position }) .with(Renderable { path: "/images/floor.png".to_string(), }) .build(); } pub fn create_box(world: &mut World, position: Position) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { path: "/images/box.png".to_string(), }) .with(Box {}) .build(); } pub fn create_box_spot(world: &mut World, position: Position) { world .create_entity() .with(Position { z: 9, ..position }) .with(Renderable { path: "/images/box_spot.png".to_string(), }) .with(BoxSpot {}) .build(); } pub fn create_player(world: &mut World, position: Position) { world .create_entity() .with(Position { z: 10, ..position }) .with(Renderable { path: "/images/player.png".to_string(), }) .with(Player {}) .build(); } // 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 }, ); } pub fn main() -> GameResult { let mut world = World::new(); register_components(&mut world); initialize_level(&mut world); // 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) }
КОД: Увидеть весь код из данной главы можно здесь.