Bevy Sprite

17 阅读5分钟

Sprites.webp

到目前为止,我们还没进行到游戏逻辑上来,那么今天,我们从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

bevy_bird_dark.png

定义一个System,通过Commands创建精灵,精灵使用一个Sprite的结构体,里面定义了精灵使用的图片。如此,我们就创建了一个精灵。当我们把该系统添加到App中时,我们就能看见这个精灵了:

//other code...
.add_systems(Startup, spawn_sprite)
//other code...

图片.png

吗?

我们遗漏了一个关键组件:相机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...

运行

图片.png

啊哈,我们添加了第一个精灵到我们的游戏中来!非常棒。

尺寸与位置

默认情况下,精灵的大小是按照图片大小来的,所以如果我们想调整精灵的大小,需要定义Sprite中的custom_size

Sprite {
    image: asset_server.load("bevy_bird_dark.png"),
    custom_size: Some(Vec2::new(100.0, 100.0)), // 设定精灵大小
    ..default()
}

我们现在更改了精灵的大小为100

图片.png

默认情况下,精灵会显示在坐标系原点(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中添加组件,需要使用元组的方式,也就是多加一个括号。

看看效果:

图片.png

正如上面提到的那样,坐标系按照左负右正,上正下负的方式工作,-200f32, -200f32就是在第三象限,XY的负方向上均移动了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), // 给一个负数
    ));
}

看看效果:

图片.png

cloud.png

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

首先检测按键的输入,然后定义XY的移动方向。

接下来,修改Transform来实现移动:

sprite_query.iter_mut().for_each(|(mut t, s)| {
    t.translation += Vec3::new(x, y, 0.0);
})

运行我们就可以看到,工作的还不错:

PixPin_2025-03-13_10-47-43.gif

但是,这种方式存在一个关键问题:移动速度与帧率绑定。在真实游戏开发中,我们需要一种更稳定的移动方式。

稳定的移动

上面讲述的系统中,按键的检测没有问题,重点是移动,因为该系统是添加到Update调度的,这个调度和帧率是强相关的。那么,如果在一个30FPS的机器上,每秒钟就能移动30像素,如果在120FPS的机器上,每秒钟就能移动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_time1/120,所以每一帧的移动距离为1/120 * SPEED。一秒就是120帧,那么总的移动距离就是SPEED
  • 以此类推30帧率的机器,每秒的移动距离依然是SPEED

这样,我们才能获得稳定的移动速度。

方向的陷阱

这里其实还有另外一个陷阱,我们先把速度给的比较高,然后看看效果:

const SPEED: f32 = 800.0;

PixPin_2025-03-13_11-01-21.gif

如果仔细看会发现,斜方向上的移动会比正方向上的移动速度快!

当然了!斜向移动的合成速度是 x2+y2\sqrt{x^2 + y^2},此时实际速度会是 SPEED2\sqrt{2} 倍(约为 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;
        });
})

PixPin_2025-03-13_11-11-20.gif

完美!!