实现游戏基本功能

现在角色可以推动箱子在区域内移动了.有些(并不是全部)游戏还会设定些目标让玩家去完成.比如有些推箱子类的游戏会让玩家把箱子推到特定的点才算赢.目前我们还没实现类似的功能,还没有检查什么时候玩家赢了并停止游戏,有可能玩家已经把箱子推到目标点了,但我们的游戏并没意识到.接下来就让我们完成这些功能吧!

首先我们需要想一下要检查是否赢了并通知玩家需要添加那些功能. 当玩家闯关时:

  • 需要一个用于保存游戏状态的 resource
    • 游戏是在进行中还是已经完成了?
    • 玩家目前一共走了多少步了?
  • 需要一个用于检查玩家是否完成任务的system
  • 需要一个用于更新移动步数的 system
  • 需要一个用于展示游戏状态的界面(UI )

游戏状态资源

我们之所以选择使用资源(resource)保存游戏状态,是因为游戏状态信息不跟任何一个实体绑定.接下来我们就开始定义一个Gameplay资源.

#![allow(unused)] fn main() { // components.rs #[derive(Default)] pub enum GameplayState { #[default] Playing, Won, } #[derive(Default)] pub struct Gameplay { pub state: GameplayState, pub moves_count: u32, } }

Gameplay 有俩个属性: statemoves_count. 分别用于保存当前游戏状态(当前游戏正在进行还是已经有了赢家)和玩家操作的步数. state枚举(enum)类型, 可以这样定义:

细心的读者会注意到,我们使用了一个宏来为 Gameplay 派生 Default 特性,并为 GameplayState 枚举使用了 #[default] 注解。这个注解的作用是告诉编译器,如果我们调用 GameplayState::default(),我们应该得到 GameplayState::Playing,这是合理的。

现在,当游戏启动时,Gameplay 资源将如下所示:

#![allow(unused)] fn main() { Gameplay { state: GameplayState::Playing, moves_count: 0 } }

计步System

我们可以通过增加Gameplaymoves_count属性值来记录玩家操作的步数.

可以在先前定义的处理用户输入的InputSystem中实现计步的功能.因为我们需要在InputSystem中修改Gameplay的属性值,所以需要在InputSystem中定义SystemData类型时使用Write<'a, Gameplay>.

#![allow(unused)] fn main() { // input_system.rs pub fn run_input(world: &World, context: &mut Context) { let mut to_move: Vec<(Entity, KeyCode)> = Vec::new(); ... }

我们先前已经编写过根据玩家按键移动角色的代码,在此基础上再添加增加操作步骤计数的代码就可以了.

#![allow(unused)] fn main() { // input_system.rs ... // Update gameplay moves if !to_move.is_empty() { let mut query = world.query::<&mut Gameplay>(); let gameplay = query.iter().next().unwrap().1; gameplay.moves_count += 1; } // Now actually move what needs to be moved for (entity, key) in to_move { let mut position = world.get::<&mut Position>(entity).unwrap(); match key { KeyCode::Up => position.y -= 1, KeyCode::Down => position.y += 1, KeyCode::Left => position.x -= 1, KeyCode::Right => position.x += 1, _ => (), } } } }

Gameplay System

接下来是添加一个GamePlayStateSystem用于检查所有的箱子是否已经推到了目标点,如果已经推到了就赢了.除了 Gameplay, 要完成这个功能还需要对Position, Box, 和 BoxSpot进行只读访问.这里使用 Join 结合Box(箱子)Position(位置)创建一个包含每个箱子位置信息的Vector(集合).我们只需要通过遍历这个集合来判断每个箱子是否在目标点上,如果在就胜利了,如果不在,则游戏继续.

#![allow(unused)] fn main() { // systems/gameplay.rs use crate::components::*; use hecs::World; use std::collections::HashMap; pub fn run_gameplay_state(world: &World) { // get all boxes indexed by position let mut query = world.query::<(&Position, &Box)>(); let boxes_by_position: HashMap<(u8, u8), &Box> = query .iter() .map(|(_, t)| ((t.0.x, t.0.y), t.1)) .collect::<HashMap<_, _>>(); // loop through all box spots and check if there is a corresponding // box at that position let boxes_out_of_position: usize = world .query::<(&Position, &BoxSpot)>() .iter() .map(|(_, (position, _))| { if boxes_by_position.contains_key(&(position.x, position.y)) { 0 } else { 1 } }) .collect::<Vec<usize>>() .into_iter() .sum(); // If we made it this far, then all box spots have boxes on them, and the // game has been won if boxes_out_of_position == 0 { let mut query = world.query::<&mut Gameplay>(); let gameplay = query.iter().next().unwrap().1; gameplay.state = GameplayState::Won; } } }

最后还需要在渲染循环中执行我们的代码:

// main.rs /* ANCHOR: all */ // Rust sokoban // main.rs use ggez::{conf, event, Context, GameResult}; use hecs::World; use std::path; mod components; mod constants; mod entities; mod map; mod systems; // 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: handler impl event::EventHandler<ggez::GameError> for Game { fn update(&mut self, context: &mut Context) -> GameResult { // Run input system { systems::input::run_input(&self.world, context); } // Run gameplay state { systems::gameplay::run_gameplay_state(&self.world); } Ok(()) } fn draw(&mut self, context: &mut Context) -> GameResult { // Render game entities { systems::rendering::run_rendering(&self.world, context); } Ok(()) } } // ANCHOR_END: handler // ANCHOR: main pub fn main() -> GameResult { let mut world = World::new(); map::initialize_level(&mut world); entities::create_gameplay(&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) } // ANCHOR_END: main /* ANCHOR_END: all */

游戏信息界面

最后一步是需要提供一个向玩家展示当前游戏状态的界面.我们需要一个用于记录游戏状态的资源和一个更新状态信息的System.可以把这些放到资源GameplayStateRenderingSystem中.

首先需要为GameplayState实现Display特征,这样才能以文本的形式展示游戏状态.这里又用到了模式匹配,根据游戏的状态显示"Playing(进行中)"或"Won(赢了)".

#![allow(unused)] fn main() { // resources.rs impl Display for GameplayState { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { fmt.write_str(match self { GameplayState::Playing => "Playing", GameplayState::Won => "Won", })?; Ok(()) } } }

接下来我们需要在RenderingSystem中添加一个方法draw_text,这样它就可以把游戏状态信息GameplayState显示到屏幕上了.

#![allow(unused)] fn main() { // rendering_systems.rs pub fn draw_text(canvas: &mut Canvas, text_string: &str, x: f32, y: f32) { let text = Text::new(TextFragment { text: text_string.to_string(), color: Some(Color::new(0.0, 0.0, 0.0, 1.0)), scale: Some(PxScale::from(20.0)), ..Default::default() }); canvas.draw(&text, Vec2::new(x, y)); } }

...为了调用draw_text我们还需要把资源 Gameplay 添加 RenderingSystem 中,这样 RenderingSystem 才能获取到资源 Gameplay

#![allow(unused)] fn main() { // rendering.rs // Render any text let mut query = world.query::<&Gameplay>(); let gameplay = query.iter().next().unwrap().1; draw_text(&mut canvas, &gameplay.state.to_string(), 525.0, 80.0); draw_text(&mut canvas, &gameplay.moves_count.to_string(), 525.0, 100.0); }

至此我们编写的推箱子游戏已经可以向玩家展示基本的信息了:

  • 当前的操作步数
  • 当玩家胜利时告诉他们

看起来就像这个样子:

Sokoban play

还有很多可以改进增强的!