实现游戏基本功能

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

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

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

游戏状态资源

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


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

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


#![allow(unused)]
fn main() {
// resources.rs
pub enum GameplayState {
    Playing,
    Won
}
}

眼尖的你应该已经发现,我们使用了宏为Gameplay实现了Default特征,但是枚举GameplayState却没有.如果我们需要把Gameplay用做资源,那它必须具备Default特征.Rust并没有提供为枚举类型实现Default特征的宏,我们只能自己为枚举GameplayState实现Default特征了.


#![allow(unused)]
fn main() {
// resources.rs
impl Default for GameplayState {
    fn default() -> Self {
        Self::Playing
    }
}
}

定义好资源别忘了注册下:


#![allow(unused)]
fn main() {
// resources.rs
pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default());
    world.insert(Gameplay::default());
}
}

当游戏开始时,资源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
use crate::components::*;
use crate::constants::*;
use crate::resources::{InputQueue, Gameplay};
use ggez::event::KeyCode;
use specs::{world::Index, Entities, Join, ReadStorage, System, Write, WriteStorage};

use std::collections::HashMap;

pub struct InputSystem {}

// System implementation
impl<'a> System<'a> for InputSystem {
    // Data
    type SystemData = (
        Write<'a, InputQueue>,
        Write<'a, Gameplay>,
        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, mut gameplay, entities, mut positions, players, movables, immovables) = data;
        ...
}

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


#![allow(unused)]
fn main() {
// input_system.rs
        ...

        // We've just moved, so let's increase the number of moves
        if to_move.len() > 0 {
            gameplay.moves_count += 1;
        }

        // 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,
                    _ => (),
                }
            }
        }
    }
}
}

Gameplay System

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


#![allow(unused)]
fn main() {
// gameplay_state_system.rs
use specs::{Join, ReadStorage, System, Write};
use std::collections::HashMap;

use crate::{
    components::{Box, BoxSpot, Position},
    resources::{Gameplay, GameplayState},
};

pub struct GameplayStateSystem {}

impl<'a> System<'a> for GameplayStateSystem {
    // Data
    type SystemData = (
        Write<'a, Gameplay>,
        ReadStorage<'a, Position>,
        ReadStorage<'a, Box>,
        ReadStorage<'a, BoxSpot>,
    );

    fn run(&mut self, data: Self::SystemData) {
        let (mut gameplay_state, positions, boxes, box_spots) = data;

        // get all boxes indexed by position
        let boxes_by_position: HashMap<(u8, u8), &Box> = (&positions, &boxes)
            .join()
            .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
        for (_box_spot, position) in (&box_spots, &positions).join() {
            if boxes_by_position.contains_key(&(position.x, position.y)) {
                // continue
            } else {
                gameplay_state.state = GameplayState::Playing;
                return;
            }
        }

        // If we made it this far, then all box spots have boxes on them, and the
        // game has been won
        gameplay_state.state = GameplayState::Won;
    }
}
}

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


#![allow(unused)]
fn main() {
// main.rs
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);
        }

        // Run gameplay state system
        {
            let mut gss = GameplayStateSystem {};
            gss.run_now(&self.world);
        }

        Ok(())
    }
    // ...
}
}

游戏信息界面

最后一步是需要提供一个向玩家展示当前游戏状态的界面.我们需要一个用于记录游戏状态的资源和一个更新状态信息的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
impl RenderingSystem<'_> {
    pub fn draw_text(&mut self, text_string: &str, x: f32, y: f32) {
        let text = graphics::Text::new(text_string);
        let destination = Vec2::new(x, y);
        let color = Some(Color::new(0.0, 0.0, 0.0, 1.0));
        let dimensions = Vec2::new(0.0, 20.0);

        graphics::queue_text(self.context, &text, dimensions, color);
        graphics::draw_queued_text(
            self.context,
            graphics::DrawParam::new().dest(destination),
            None,
            graphics::FilterMode::Linear,
        )
        .expect("expected drawing queued text");
    }
}
}

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


#![allow(unused)]
fn main() {
// rendering_system.rs
impl<'a> System<'a> for RenderingSystem<'a> {
    // Data
    type SystemData = (Read<'a, Gameplay>, ReadStorage<'a, Position>, ReadStorage<'a, Renderable>);

    fn run(&mut self, data: Self::SystemData) {
        let (gameplay, 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");
        }

        // Render any text
        self.draw_text(&gameplay.state.to_string(), 525.0, 80.0);
        self.draw_text(&gameplay.moves_count.to_string(), 525.0, 100.0);

        // Finally, present the context, this will actually display everything
        // on the screen.
        graphics::present(self.context).expect("expected to present");
    }
}
}

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

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

看起来就像这个样子:

Sokoban play

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

CODELINK:这里获取目前的完整代码.