到目前为止,我们还没进行到游戏逻辑上来,那么今天,我们从2D游戏中一个比较重要的概念——精灵入手,看看Bevy是如何使用精灵的。
创建精灵
fn spawn_sprite(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
Sprite {
image: asset_server.load("bevy_bird_dark.png"),
..default()
},
));
}
bevy_bird_dark.png
定义一个System,通过Commands
创建精灵,精灵使用一个Sprite
的结构体,里面定义了精灵使用的图片。如此,我们就创建了一个精灵。当我们把该系统添加到App
中时,我们就能看见这个精灵了:
//other code...
.add_systems(Startup, spawn_sprite)
//other code...
吗?
我们遗漏了一个关键组件:相机Camera
,游戏如果不添加Camera
,我们就看不见任何东西。OK,我们定义一个2D的相机,并添加到App
:
fn spawn_camera(mut commands: Commands) {
commands.spawn(Camera2d::default());
}
//other code...
.add_systems(Startup, (spawn_camera, spawn_sprite))
//other code...
运行
啊哈,我们添加了第一个精灵到我们的游戏中来!非常棒。
尺寸与位置
默认情况下,精灵的大小是按照图片大小来的,所以如果我们想调整精灵的大小,需要定义Sprite
中的custom_size
:
Sprite {
image: asset_server.load("bevy_bird_dark.png"),
custom_size: Some(Vec2::new(100.0, 100.0)), // 设定精灵大小
..default()
}
我们现在更改了精灵的大小为100
。
默认情况下,精灵会显示在坐标系原点(Bevy的坐标系原点位于窗口中心,X
轴右为正,Y
轴上为正)。若需调整位置,需为实体添加Transform
组件:
fn spawn_sprite(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((// 多一个括号,表示元祖,同时也是同一条 Entity
Sprite {
image: asset_server.load("bevy_bird_dark.png"),
custom_size: Some(Vec2::new(100.0, 100.0)),
..default()
},
Transform::from_xyz(-200f32, -200f32, 0f32), // 设定位置
));
}
注意,如果要在同一条Entity中添加组件,需要使用元组的方式,也就是多加一个括号。
看看效果:
正如上面提到的那样,坐标系按照左负右正,上正下负的方式工作,-200f32, -200f32
就是在第三象限,X
和Y
的负方向上均移动了200
。
精灵的重叠
可以通过更改Transform
中,Z
的大小,来定义精灵的重叠关系,Z
的正方向是朝向用户的,也就是朝向屏幕外,所以如果我们想定义一个云朵放置在上面的小鸟下面,那么Z
就需要给一个负数:
fn spawn_sprite(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn((
Sprite {
image: asset_server.load("bevy_bird_dark.png"),
custom_size: Some(Vec2::new(100.0, 100.0)),
..default()
},
Transform::from_xyz(-200f32, -200f32, 0f32),
));
commands.spawn((
Sprite {
image: asset_server.load("cloud.png"),
custom_size: Some(Vec2::new(200.0, 200.0)),
..default()
},
Transform::from_xyz(-200f32, -200f32, -1f32), // 给一个负数
));
}
看看效果:
cloud.png
移动精灵
精灵的移动是通过修改Transform
实现的。
在游戏的大部分场景中,移动都是通过修改Transform
实现的。
现在,我们定义一个系统,该系统有三个参数,time: Res<Time>
用于过去当前时间信息,keyboard_input: Res<ButtonInput<KeyCode>>
用于获取键盘的点击事件(我们总得使用键盘来控制方向吧),然后是mut sprite_query: Query<(&mut Transform, &Sprite), With<Sprite>>
,获取精灵相关的组件:
fn move_sprite(
time: Res<Time>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut sprite_query: Query<(&mut Transform, &Sprite), With<Sprite>>,
) {}
别忘了添加到App
。
.add_systems(Update, move_sprite)
OK,我们的基本思路是,使用经典的WASD
(如果你玩儿过CS)按键来控制精灵的移动:
let mut x = 0f32;
let mut y = 0f32;
if keyboard_input.pressed(KeyCode::KeyW) {
y = 1f32;
} else if keyboard_input.pressed(KeyCode::KeyS) {
y = -1f32;
}
if keyboard_input.pressed(KeyCode::KeyA) {
x = -1f32;
} else if keyboard_input.pressed(KeyCode::KeyD) {
x = 1f32;
}
首先检测按键的输入,然后定义X
和Y
的移动方向。
接下来,修改Transform
来实现移动:
sprite_query.iter_mut().for_each(|(mut t, s)| {
t.translation += Vec3::new(x, y, 0.0);
})
运行我们就可以看到,工作的还不错:
但是,这种方式存在一个关键问题:移动速度与帧率绑定。在真实游戏开发中,我们需要一种更稳定的移动方式。
稳定的移动
上面讲述的系统中,按键的检测没有问题,重点是移动,因为该系统是添加到Update
调度的,这个调度和帧率是强相关的。那么,如果在一个30
FPS的机器上,每秒钟就能移动30
像素,如果在120
FPS的机器上,每秒钟就能移动120
像素,这是不对的(想象一下英雄联盟中,对方的剑魔因为帧率太高导致移动速度比你开大的剑圣还快!)。所以,通常我们做移动的时候,会使用时间和速度,来获得稳定的移动距离:
const SPEED: f32 = 60.0;//定义距离为每秒移动60
fn move_sprite(
time: Res<Time>,
keyboard_input: Res<ButtonInput<KeyCode>>,
mut sprite_query: Query<(&mut Transform, &Sprite), With<Sprite>>,
) {
let delta_time = time.delta_secs();//获取帧之间的时间间隔
let mut x = 0f32;
let mut y = 0f32;
if keyboard_input.pressed(KeyCode::KeyW) {
y = 1f32;
} else if keyboard_input.pressed(KeyCode::KeyS) {
y = -1f32;
}
if keyboard_input.pressed(KeyCode::KeyA) {
x = -1f32;
} else if keyboard_input.pressed(KeyCode::KeyD) {
x = 1f32;
}
sprite_query.iter_mut().for_each(|(mut t, s)| {
t.translation += Vec3::new(x * delta_time * SPEED, y * delta_time * SPEED, 0.0);//采用时间间隔乘以速度来修改移动距离
})
}
OK,time.delta_secs()
会返回一个浮点数,单位是秒,该数值表示上一帧到当前帧的时间间隔。
我们推算一下,看看该结果是否公平:
- 在一台
120
帧率的机器上,delta_time
为1/120
,所以每一帧的移动距离为1/120 * SPEED
。一秒就是120
帧,那么总的移动距离就是SPEED
。 - 以此类推
30
帧率的机器,每秒的移动距离依然是SPEED
。
这样,我们才能获得稳定的移动速度。
方向的陷阱
这里其实还有另外一个陷阱,我们先把速度给的比较高,然后看看效果:
const SPEED: f32 = 800.0;
如果仔细看会发现,斜方向上的移动会比正方向上的移动速度快!
当然了!斜向移动的合成速度是 ,此时实际速度会是 SPEED
的 倍(约为 1.414 倍)。
所以,我们要对移动的矩阵进行单位化,让精灵在任何方向上移动速度都是恒定的。
sprite_query.iter_mut().for_each(|(mut t, s)| {
Vec3::new(x ,y, 0.0)
.try_normalize()
.map(|v| {
t.translation += v * delta_time * SPEED;
});
})
完美!!