让角色动起来

严格来说当前我们编写的还称不上游戏,因为还不能让玩家操作角色动起来.在这一节我们就开始学习怎么获取用户输入事件从而让角色动起来.

输入事件

要让玩家可以操作角色动起来,首先我们需要监听用户输入事件.怎么监听呢?可以参考ggez提供的例子.其中有监听鼠标和键盘事件的示例代码,现在我们只需要监听键盘按下(key_down_event)事件.比虎画猫让我们开始编写代码吧!

首先引入下需要用到的模块:


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

接下来为Game实现event::EventHandler,这样我们的游戏就可以监听到键盘按键按下的事件了:


#![allow(unused)]
fn main() {
impl event::EventHandler<ggez::GameError> for Game {

    // ...

    fn key_down_event(
        &mut self,
        _context: &mut Context,
        keycode: KeyCode,
        _keymod: KeyMods,
        _repeat: bool,
    ) {
        println!("Key pressed: {:?}", keycode);
    }

    // ...

}
}

你可以运行代码,按下方向键试一下,在控制台中就会输出类似下面的信息:

Key pressed: Left
Key pressed: Left
Key pressed: Right
Key pressed: Up
Key pressed: Down
Key pressed: Left

是不是很神奇?

在使用println输出信息时使用了{:?},这个是Rust提供的方便调试的,比如我们这里输出的keycode其实是一个枚举对象,因为它实现了Debug特征,所以这里可以很方便的把它转换为字符串输出到控制台.如果要对没有实现Debug特征的对象使用{:?},代码就该编译出错了,好在Rust提供了Debug宏可以非常简单方便实现Debug特征.我们在第1章的第3节介绍过宏,如果对宏不是很了解也可以回头再看一下.

资源

资源是用于在系统中共享状态信息的.为什么需要资源呢?因为组件实体模型不适合干这样的事.

接下来我们将添加一个资源,一个用于记录用户按键的队列.


#![allow(unused)]
fn main() {
// Resources
#[derive(Default)]
pub struct InputQueue {
    pub keys_pressed: Vec<KeyCode>,
}
}

当用户按下了按键,Game的方法key_down_event就会执行,这个我们上面已经试过了.现在我们需要在key_down_event方法中把keycode添加到队列里:


#![allow(unused)]
fn main() {
impl event::EventHandler<ggez::GameError> for Game {

    // ...

    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);
    }

    // ...

}
}

最后我们还需要注册下资源,就像注册组件一样.

// Registering resources
pub fn register_resources(world: &mut World) {
    world.insert(InputQueue::default())
}

// Registering resources in main
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)

输入处理

到这里我们已经有了一个持续记录用户按键的队列,接下来就是在系统中处理这个队列了,准确来说是处理队列中记录的按键.


#![allow(unused)]
fn main() {
pub struct InputSystem {}

impl<'a> System<'a> for InputSystem {
    // Data
    type SystemData = (
        Write<'a, InputQueue>,
        WriteStorage<'a, Position>,
        ReadStorage<'a, Player>,
    );

    fn run(&mut self, data: Self::SystemData) {
        let (mut input_queue, mut positions, players) = data;

        for (position, _player) in (&mut positions, &players).join() {
            // Get the first key pressed
            if let Some(key) = input_queue.keys_pressed.pop() {
                // Apply the key to the position
                match key {
                    KeyCode::Up => position.y -= 1,
                    KeyCode::Down => position.y += 1,
                    KeyCode::Left => position.x -= 1,
                    KeyCode::Right => position.x += 1,
                    _ => (),
                }
            }
        }
    }
}
}

最后我们还需要在渲染循环中运行输入处理代码.


#![allow(unused)]
fn main() {
    fn update(&mut self, _context: &mut Context) -> GameResult {
        // Run input system
        {
            let mut is = InputSystem {};
            is.run_now(&self.world);
        }

        Ok(())
    }
}

当前的输入处理代码非常简单,就是根据玩家的输入控制角色的位置(虽然我们当前只有一个角色,但是理论上对于多个玩家多个角色的场景也可以这么玩).

酷不? 运行下代码应该就是这样的:

Moving player

注意到没?现在角色可以穿过墙和盒子.没关系,我们下一节就修复这个问题.

CODELINK: 可以点这里获取当前完整代码.