推箱子
在前一节中我们可以控制角色移动了,但是角色可以穿越墙和盒子,对环境中的其它东西根本都不care.做人不能这么横,咋能这么目中无箱子呢?这样还咋推箱子呢?接下来我们就在角色移动的时候添加些逻辑判断让它智能起来.
可移动的组件
首先我们需要让代码更通用些.在先前场景我们完成了让玩家控制角色移动的功能,后面我们还需要让箱子也移动起来.说不定以后我们还需要移动其它的什么东西.所以很有必要把组件区分为可移动的和不可移动的.比如:角色箱子是可以移动的;墙是不能移动,角色也不能穿过的;另外还有方框即不像可移动的角色也不像不可移动的墙.
接下来我们增加2个新组件,除了一些小的改变其实也没什么新的:
- 我们使用
NullStorage
而不是VecStorage
,因为这俩个组件没什么属性,只是用做标记,使用NullStorage
更高效. - 为了使用
NullStorage
还需要实现Default
特征. - 把这俩个组件添加到register_components函数中.
#![allow(unused)] fn main() { #[derive(Component, Default)] #[storage(NullStorage)] pub struct Movable; #[derive(Component, Default)] #[storage(NullStorage)] pub struct Immovable; world.register::<Wall>(); world.register::<Box>(); world.register::<BoxSpot>(); world.register::<Movable>(); world.register::<Immovable>(); } pub fn register_resources(world: &mut World) { world.insert(InputQueue::default()) } }
接下来:
- 为角色和箱子实现with(Movable)
- 为墙实现with(Immovable)
- 地板和方框就不做什么处理了(就像我们先前分析的,方框即不能移动也不能像墙一样阻止角色箱子移动)
#![allow(unused)] fn main() { .with(Renderable { path: "/images/wall.png".to_string(), }) .with(Wall {}) .with(Immovable) .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 {}) .with(Movable) .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 {}) .with(Movable) .build(); } // Initialize the level pub fn initialize_level(world: &mut World) { const MAP: &str = " }
移动需求分析
现在我们可以想几种不同的移动场景,这样有助于更好的使用Moveable
和Immovable
.
场景:
(player, floor)
+RIGHT
-> 角色应右移(player, wall)
+RIGHT
-> 角色不能移动(player, box, floor)
+RIGHT
-> 角色和盒子右移(player, box, wall)
+RIGHT
-> 啥都不移动(player, box, box, floor)
+RIGHT
-> 角色, box1 和 box2 都应右移(player, box, box, wall)
+RIGHT
-> 啥都不移动
由此可以得出:
- 碰撞(移动)检测需要所有相关对象一起完成.比如场景6, 如果我们一次检测一个对象,我们就会移动角色,然后移动box1,直到我们检测到box2,才发现box2是不能移动的,因为它前面是个墙,那先前移动的角色和box1还得移动回去.
- 可移动对象碰到空点可以移动的(这里的空点代表即不是可移动对象也不是不可移动对象).
- 可移动对象碰到不可移动对象不可移动.
- 虽然我们只是根据向右移动的场景分析的,但这些结论同样适用于其它方向.
基于此,就可以编写代码了. 以下是一些我们需要实现的逻辑片段:
- 找出所有的可移动实体和不可移动实体 - 这样我们才能区分它们是否对应对有影响.
- 确定基于按键移动实体的方法 - 这个我们先前章节中已经介绍过,可以根据按键在实体位置相应分支上+1或-1;
- 遍历从角色当前位置到地图末尾的所有位置 需要根据移动方向判断地图末尾位置,比如按下的向右方向键,那我们就需要遍历玩家当前位置到
地图宽度
的所有位置信息;如果按下的是向上方向键,那就需要从0遍历到player.y
. - 对于序列中的每个图块 我们需要:
- 如果当前图块是可移动的,就继续执行并记录下当前的图块.
- 如果当前图块是不可移动的,就停止执行,什么也不需要移动.
- 如果当前图块既不是可移动的也不是不可移动的,就移动我们先前记录的所有可移动图块.
下面是新的输入处理的代码,虽然代码有点长但功能实现了!
#![allow(unused)] fn main() { // Data type SystemData = ( Write<'a, InputQueue>, Entities<'a>, WriteStorage<'a, Position>, ReadStorage<'a, Player>, ReadStorage<'a, Movable>, ReadStorage<'a, Immovable>, ); fn run(&mut self, data: Self::SystemData) { let (mut input_queue, entities, mut positions, players, movables, immovables) = data; let mut to_move = Vec::new(); for (position, _player) in (&positions, &players).join() { // Get the first key pressed if let Some(key) = input_queue.keys_pressed.pop() { // get all the movables and immovables let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions) .join() .map(|t| ((t.2.x, t.2.y), t.0.id())) .collect::<HashMap<_, _>>(); let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions) .join() .map(|t| ((t.2.x, t.2.y), t.0.id())) .collect::<HashMap<_, _>>(); // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { KeyCode::Up => (position.y, 0, false), KeyCode::Down => (position.y, MAP_HEIGHT, false), KeyCode::Left => (position.x, 0, true), KeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; let range = if start < end { (start..=end).collect::<Vec<_>>() } else { (end..=start).rev().collect::<Vec<_>>() }; for x_or_y in range { let pos = if is_x { (x_or_y, position.y) } else { (position.x, x_or_y) }; // find a movable // if it exists, we try to move it and continue // if it doesn't exist, we continue and try to find an immovable instead match mov.get(&pos) { Some(id) => to_move.push((key, id.clone())), None => { // find an immovable // if it exists, we need to stop and not move anything // if it doesn't exist, we stop because we found a gap match immov.get(&pos) { Some(_id) => to_move.clear(), None => break, } } } } } } // Now actually move what needs to be moved for (key, id) in to_move { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { KeyCode::Up => position.y -= 1, KeyCode::Down => position.y += 1, KeyCode::Left => position.x -= 1, KeyCode::Right => position.x += 1, _ => (), } } } } } }
现在再运行代码,你会发现角色不再穿越墙了还能推动箱子了.
下面是完整代码:
// Rust sokoban // main.rs use glam::Vec2; use ggez::{ conf, Context, GameResult, event::{self, KeyCode, KeyMods}, graphics::{self, DrawParam, Image}}; use specs::{ join::Join, Builder, Component, ReadStorage, RunNow, System, VecStorage, World, WorldExt, Write, WriteStorage, NullStorage, Entities, world::Index }; use std::collections::HashMap; use std::path; const TILE_WIDTH: f32 = 32.0; const MAP_WIDTH: u8 = 8; const MAP_HEIGHT: u8 = 9; // 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 {} #[derive(Component, Default)] #[storage(NullStorage)] pub struct Movable; #[derive(Component, Default)] #[storage(NullStorage)] pub struct Immovable; // Resources #[derive(Default)] pub struct InputQueue { pub keys_pressed: Vec<KeyCode>, } // 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 backround 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"); } } pub struct InputSystem {} // System implementation impl<'a> System<'a> for InputSystem { // Data type SystemData = ( Write<'a, InputQueue>, Entities<'a>, WriteStorage<'a, Position>, ReadStorage<'a, Player>, ReadStorage<'a, Movable>, ReadStorage<'a, Immovable>, ); fn run(&mut self, data: Self::SystemData) { let (mut input_queue, entities, mut positions, players, movables, immovables) = data; let mut to_move = Vec::new(); for (position, _player) in (&positions, &players).join() { // Get the first key pressed if let Some(key) = input_queue.keys_pressed.pop() { // get all the movables and immovables let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions) .join() .map(|t| ((t.2.x, t.2.y), t.0.id())) .collect::<HashMap<_, _>>(); let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions) .join() .map(|t| ((t.2.x, t.2.y), t.0.id())) .collect::<HashMap<_, _>>(); // Now iterate through current position to the end of the map // on the correct axis and check what needs to move. let (start, end, is_x) = match key { KeyCode::Up => (position.y, 0, false), KeyCode::Down => (position.y, MAP_HEIGHT, false), KeyCode::Left => (position.x, 0, true), KeyCode::Right => (position.x, MAP_WIDTH, true), _ => continue, }; let range = if start < end { (start..=end).collect::<Vec<_>>() } else { (end..=start).rev().collect::<Vec<_>>() }; for x_or_y in range { let pos = if is_x { (x_or_y, position.y) } else { (position.x, x_or_y) }; // find a movable // if it exists, we try to move it and continue // if it doesn't exist, we continue and try to find an immovable instead match mov.get(&pos) { Some(id) => to_move.push((key, id.clone())), None => { // find an immovable // if it exists, we need to stop and not move anything // if it doesn't exist, we stop because we found a gap match immov.get(&pos) { Some(_id) => to_move.clear(), None => break, } } } } } } // Now actually move what needs to be moved for (key, id) in to_move { let position = positions.get_mut(entities.entity(id)); if let Some(position) = position { match key { KeyCode::Up => position.y -= 1, KeyCode::Down => position.y += 1, KeyCode::Left => position.x -= 1, KeyCode::Right => position.x += 1, _ => (), } } } } } // 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 { // Run input system { let mut is = InputSystem {}; is.run_now(&self.world); } Ok(()) } fn draw(&mut self, context: &mut Context) -> GameResult { // Render game entities { let mut rs = RenderingSystem { context }; rs.run_now(&self.world); } Ok(()) } fn key_down_event( &mut self, _context: &mut Context, keycode: KeyCode, _keymod: KeyMods, _repeat: bool, ) { println!("Key pressed: {:?}", keycode); let mut input_queue = self.world.write_resource::<InputQueue>(); input_queue.keys_pressed.push(keycode); } } // 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>(); world.register::<Movable>(); world.register::<Immovable>(); } pub fn register_resources(world: &mut World) { world.insert(InputQueue::default()) } // 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 {}) .with(Immovable) .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 {}) .with(Movable) .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 {}) .with(Movable) .build(); } // Initialize the level pub fn initialize_level(world: &mut World) { const MAP: &str = " N N W W W W W W W W W . . . . W W . . . B . . W W . . . . . . W W . P . . . . W W . . . . . . W W . . S . . . W W . . . . . . W W W W W W W W W "; load_map(world, MAP.to_string()); } 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 match *column { "." => create_floor(world, position), "W" => { create_floor(world, position); create_wall(world, position); } "P" => { create_floor(world, position); create_player(world, position); } "B" => { create_floor(world, position); create_box(world, position); } "S" => { create_floor(world, position); create_box_spot(world, position); } "N" => (), c => panic!("unrecognized map item {}", c), } } } } pub fn main() -> GameResult { let mut world = World::new(); register_components(&mut world); register_resources(&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) }
CODELINK: 你可以点 这里获取目前的完整代码.