让角色动起来
严格来说当前我们编写的还称不上游戏,因为还不能让玩家操作角色动起来.在这一节我们就开始学习怎么获取用户输入事件从而让角色动起来.
输入事件
要让玩家可以操作角色动起来,首先我们需要监听用户输入事件.怎么监听呢?可以参考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(()) } }
当前的输入处理代码非常简单,就是根据玩家的输入控制角色的位置(虽然我们当前只有一个角色,但是理论上对于多个玩家多个角色的场景也可以这么玩).
酷不? 运行下代码应该就是这样的:
注意到没?现在角色可以穿过墙和盒子.没关系,我们下一节就修复这个问题.
CODELINK: 可以点这里获取当前完整代码.