使用键盘控制精灵

这一小节,我们尝试使用键盘控制坦克移动。

具体需求如下:

  1. 按键 w 控制坦克前进
  2. 按键 s 控制坦克后退
  3. 按键 a 控制坦克左转
  4. 按键 d 控制坦克右转

分析以下,其实我们需要处理两个问题,一个是如何让坦克变换,即改变位置,旋转等;另一个是如何接收键盘输入事件。

对坦克进行变换

在上一节中,我们实现了把坦克居中显示,但大家有没有想过为什么默认情况下坦克会居中显示呢,而且为什么又默认是炮筒朝上指的呢?

我们来回忆下生成坦克的代码:


#![allow(unused)]
fn main() {
commands
	.spawn_bundle(SpriteBundle {
		material: materials.add(asset_server.load("images/tank.png").into()),
		sprite: Sprite::new(Vec2::new(50., 50.)),
		..Default::default()
	})
	.insert(Tank);
}

我们曾在前一小节提过,SpriteBundle 是 bevy 内置的一组 Component 集合,其中就包括一个叫做 Transform 的 Component,其定义如下:


#![allow(unused)]
fn main() {
pub struct Transform {
    /// Position of the entity. In 2d, the last value of the `Vec3` is used for z-ordering.
    pub translation: Vec3,
    /// Rotation of the entity.
    pub rotation: Quat,
    /// Scale of the entity.
    pub scale: Vec3,
}
}

可以看到,Transform Component 就包含了精灵的位置(translation),旋转角度(rotation),以及缩放(scale)信息。

当我们不指定 transform 时,bevy 会生成一个默认的 transform:


#![allow(unused)]
fn main() {
Transform {
	translation: Vec3::ZERO,
	rotation: Quat::IDENTITY,
	scale: Vec3::ONE,
}
}

可以看到,默认情况下,位置就是 Vec3::ZERO 也即 (0, 0, 0), 旋转是 Quat::IDENTITY 也即图片默认方向,缩放是 Vec3::ONE 也即默认不做缩放。

而坦克会在屏幕中间显示,是因为 bevy 的坐标系是这样的:

axis

因此如果我们要改变坦克的位置和旋转角度时,只需要修改其对应的 transform 即可:

use bevy::prelude::*;

struct Tank;

fn setup_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
    commands
        .spawn_bundle(SpriteBundle {
            material: materials.add(asset_server.load("images/tank.png").into()),
            sprite: Sprite::new(Vec2::new(50., 50.)),
            ..Default::default()
        })
        .insert(Tank);
}

fn tank_movement_system(mut query: Query<&mut Transform, With<Tank>>) {
    for mut transform in query.iter_mut() {
        transform.translation.y += 0.1;
    }
}

fn main() {
    App::build()
        .insert_resource(WindowDescriptor {
            height: 500.0,
            width: 500.0,
            ..Default::default()
        })
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup_system.system())
        .add_system(tank_movement_system.system())
        .run();
}

该示例演示了如何让坦克朝着y轴方向移动。

以下几点需要注意:

  1. Query<&mut Transform, With<Tank>> 查询同时包含 Transform 和 Tank 两个 Component 的 Entity,这里使用 With 是因为我们不需要读写 Tank 的相关信息
  2. transform.translation.y += 0.1 是让坦克在每帧都向y轴方向移动 0.1 px 的距离

而让坦克旋转的话,稍微有些麻烦,方法如下:


#![allow(unused)]
fn main() {
transform.rotation *= Quat::from_rotation_z(std::f32::consts::PI / 2.);
}

rotation 是一个四元数,上面的效果就是逆时针旋转 90 度。

至此我们已经学习了如何对精灵进行位移和旋转,但其实我们忽略了一个问题那就是如何让坦克一直朝着自己的前方移动呢?我们上面通过修改 transform.translate.y 的值使得坦克朝着 y 轴方向移动,那是因为此时碰巧坦克的朝向就是 y 轴,当坦克向左旋转 90 度后,此时则需要 修改 transform.translate.x 的值才能使得坦克按照其自身方向前进和后退,那如果坦克旋转 30 度呢,99度呢?

此时就需要一点初中数学知识了:

rotation

我们看到旋转 α 角度后,假设我们前进一个单位的长度,其实等同于在 x 轴上位移 -sinα 的长度,在 y 轴上位移 cosα 的长度。

因此我们可以写一个 util 函数:


#![allow(unused)]
fn main() {
pub fn r#move(transform: &mut Transform, px: f32) {
    let (axis, angle) = transform.rotation.to_axis_angle();
    let angle = angle * &axis.z.signum();
    transform.translation.y += angle.cos() * &px;
    transform.translation.x -= angle.sin() * &px;
}
}

接收键盘输入事件

接收键盘处理事件相对要简单一些了,我们可以在 System 函数中通过声明 Res<Input<KeyCode>> 来接收键盘事件:

use bevy::prelude::*;

fn keyboard_input_system(keyboard_input: Res<Input<KeyCode>>) {
    if keyboard_input.pressed(KeyCode::W) {
        println!("w");
    }
    if keyboard_input.pressed(KeyCode::S) {
        println!("s");
    }
    if keyboard_input.pressed(KeyCode::A) {
        println!("a");
    }
    if keyboard_input.pressed(KeyCode::D) {
        println!("d");
    }
}

fn main() {
    App::build()
        .insert_resource(WindowDescriptor {
            height: 500.0,
            width: 500.0,
            ..Default::default()
        })
        .add_plugins(DefaultPlugins)
        .add_system(keyboard_input_system.system())
        .run();
}

最终结果

整合下本节所讲的知识点,如何用键盘控制坦克实现如下:

use bevy::prelude::*;

struct Tank;

fn setup_system(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    commands.spawn_bundle(OrthographicCameraBundle::new_2d());
    commands
        .spawn_bundle(SpriteBundle {
            material: materials.add(asset_server.load("images/tank.png").into()),
            sprite: Sprite::new(Vec2::new(50., 50.)),
            ..Default::default()
        })
        .insert(Tank);
}

fn tank_movement_system(
    mut query: Query<&mut Transform, With<Tank>>,
    keyboard_input: Res<Input<KeyCode>>,
) {
    for mut transform in query.iter_mut() {
        if keyboard_input.pressed(KeyCode::W) {
            r#move(&mut transform, 0.1);
        }
        if keyboard_input.pressed(KeyCode::S) {
            r#move(&mut transform, -0.1);
        }
        if keyboard_input.pressed(KeyCode::A) {
            transform.rotation *= Quat::from_rotation_z(std::f32::consts::PI / 360.);
        }
        if keyboard_input.pressed(KeyCode::D) {
            transform.rotation *= Quat::from_rotation_z(-std::f32::consts::PI / 360.);
        }
    }
}

pub fn r#move(transform: &mut Transform, px: f32) {
    let (axis, angle) = transform.rotation.to_axis_angle();
    let angle = angle * &axis.z.signum();
    transform.translation.y += angle.cos() * &px;
    transform.translation.x -= angle.sin() * &px;
}

fn main() {
    App::build()
        .insert_resource(WindowDescriptor {
            height: 500.0,
            width: 500.0,
            ..Default::default()
        })
        .add_plugins(DefaultPlugins)
        .add_startup_system(setup_system.system())
        .add_system(tank_movement_system.system())
        .run();
}

最终效果如下: