在上一篇文章中,我们介绍了2D图像——精灵在Bevy中的应用。今天,我们稍微花点篇幅,介绍一下2D的动画——Sprite Sheet。
什么是Sprite Sheet
Sprite Sheet(精灵图集) 是游戏开发中一种将多个小图(如动画帧、UI元素)合并到一张大图中的技术。它的作用是减少资源加载次数和GPU绘制调用,提升游戏性能。
核心特点:
- 多图合一:所有小图拼成一张大图,减少文件数量;
- 按坐标切分:使用时通过坐标定位提取特定小图;
- 优化性能:减少纹理切换次数,节省内存和渲染开销。
常见用途:角色动画、粒子特效、UI图标等需要频繁切换小图的场景。
所以,我们使用Sprite Sheet来做2D角色的动画。
创建Sprite Sheet
fn spawn_sprite_sheet(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
let texture = asset_server.load("gabe-idle-run.png");
// 创建布局:每个子图尺寸24x24,横向排列7帧,纵向1帧,无边距和偏移
let layout = TextureAtlasLayout::from_grid(UVec2::splat(24), 7, 1, None, None);
let texture_atlas_layout = texture_atlas_layouts.add(layout);
commands.spawn(
Sprite::from_atlas_image(
texture,
TextureAtlas {
layout: texture_atlas_layout,
index: 0,
},
)
);
}
gabe-idle-run.png
首先,使用AssetServer加载该图片。
然后,使用TextureAtlasLayout加载该Sprite Sheet,在生成TextureAtlasLayout的时候,我们需要指定该图集中,每一个小人物的大小,然后是行列,后面两个参数是边距和偏移,我们没有,可以不给。之后将该图集加载到资源中即可
最后,注意在使用Sprite的时候,和之前不同,使用了from_atlas_image接口,创建一个基于Sprite Sheet的精灵。
运行看看效果:
啊哈,人物太小了,我们放大一下:
commands.spawn((
Sprite::from_atlas_image(
texture,
TextureAtlas {
layout: texture_atlas_layout,
index: 0,
},
),
Transform::from_scale(Vec3::splat(6.0)), // 放大6倍
));
我们已经能够看到居中的小人了。接下来,我们让小人动起来!
让精灵动起来
仔细看我们提供的资源,其实只有第二张到第七张图片是动起来的,第一张图片是静止。在Bevy中,使用Sprite Sheet动画,需要我们手动设置Sprite Sheet的下标,然后通过定时器,让精灵能够动起来。
我们稍微让功能复杂一点:当我们点击键盘H(halt)按键的时候,人物静止;当我们点击键盘R(run)的时候,让人物动起来。OK,开始编写相关逻辑:
fn main() {
// 点击R时,更改状态为RUN
let run_sprite = |mut query: Query<&mut SpriteState>| {
query.iter_mut().for_each(|mut s|{
s.0 = State::RUN
})
};
// 点击H时,更改状态为HALT
let halt_sprite = |mut query: Query<&mut SpriteState>| {
query.iter_mut().for_each(|mut s|{
s.0 = State::HALT
})
};
// APP,添加各种系统
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, (spawn_camera, spawn_sprite_sheet))
.add_systems(Update, animate_sprite)
.add_systems(Update, run_sprite.run_if(input_just_pressed(KeyCode::KeyR)))
.add_systems(Update, halt_sprite.run_if(input_just_pressed(KeyCode::KeyH)))
.run();
}
// 定义Sprite Sheet动画下标
#[derive(Component)]
struct SpriteIndex(usize, usize);
// 定义Sprite Sheet动画的计时器
#[derive(Component)]
struct SpriteTimer(Timer);
// 定义当前人物的状态
#[derive(Component)]
struct SpriteState(State);
// 人物状态枚举
enum State {
HALT, // 静止
RUN, // 跑动
}
fn spawn_sprite_sheet(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut texture_atlas_layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
let texture = asset_server.load("gabe-idle-run.png");
let layout = TextureAtlasLayout::from_grid(UVec2::splat(24), 7, 1, None, None);
let texture_atlas_layout = texture_atlas_layouts.add(layout);
commands.spawn((
Sprite::from_atlas_image(
texture,
TextureAtlas {
layout: texture_atlas_layout,
index: 0,// 默认下标
},
),
SpriteState(State::HALT),// 默认静止
Transform::from_scale(Vec3::splat(6.0)),
SpriteIndex(1, 6),// Sprite Sheet动画下标为[1, 6]
SpriteTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),// 定义计时器每0.1秒触发一次
));
}
// 播放人物动画
fn animate_sprite(
time: Res<Time>,
mut query: Query<(
&SpriteIndex,
&mut SpriteTimer,
&mut Sprite,
&mut SpriteState,
)>,
) {
query
.iter_mut()
.for_each(|(index, mut timer, mut sprite, mut state)| match state.0 {
State::HALT => {
if let Some(atlas) = &mut sprite.texture_atlas {
atlas.index = 0 // 静止
}
}
State::RUN => { // 每0.1秒更改一次下标,循环播放
timer.0.tick(time.delta());
if timer.0.just_finished() {
if let Some(atlas) = &mut sprite.texture_atlas {
atlas.index = if atlas.index == index.1 {
index.0
} else {
atlas.index + 1
};
}
}
}
})
}
OK,详细的逻辑我已经写在了代码中,我们看下运行效果:
当我们点击键盘R时,人物会跑动;当我们点击键盘H时,人物会停下来,处于停止状态。