Bevy Sprite Sheet

392 阅读3分钟

b242f551a6e70ca619a2059f17cb3236e8569cd1.jpeg

上一篇文章中,我们介绍了2D图像——精灵在Bevy中的应用。今天,我们稍微花点篇幅,介绍一下2D的动画——Sprite Sheet

什么是Sprite Sheet

Sprite Sheet(精灵图集) 是游戏开发中一种将多个小图(如动画帧、UI元素)合并到一张大图中的技术。它的作用是减少资源加载次数和GPU绘制调用,提升游戏性能。

核心特点:

  1. 多图合一:所有小图拼成一张大图,减少文件数量;
  2. 按坐标切分:使用时通过坐标定位提取特定小图;
  3. 优化性能:减少纹理切换次数,节省内存和渲染开销。

常见用途:角色动画、粒子特效、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

gabe-idle-run.png

首先,使用AssetServer加载该图片。

然后,使用TextureAtlasLayout加载该Sprite Sheet,在生成TextureAtlasLayout的时候,我们需要指定该图集中,每一个小人物的大小,然后是行列,后面两个参数是边距和偏移,我们没有,可以不给。之后将该图集加载到资源中即可

最后,注意在使用Sprite的时候,和之前不同,使用了from_atlas_image接口,创建一个基于Sprite Sheet的精灵。

运行看看效果:

图片.png

啊哈,人物太小了,我们放大一下:

commands.spawn((
    Sprite::from_atlas_image(
        texture,
        TextureAtlas {
            layout: texture_atlas_layout,
            index: 0,
        },
    ),
    Transform::from_scale(Vec3::splat(6.0)), // 放大6倍
));

图片.png

我们已经能够看到居中的小人了。接下来,我们让小人动起来!

让精灵动起来

仔细看我们提供的资源,其实只有第二张到第七张图片是动起来的,第一张图片是静止。在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,详细的逻辑我已经写在了代码中,我们看下运行效果:

PixPin_2025-03-14_11-29-54.gif

当我们点击键盘R时,人物会跑动;当我们点击键盘H时,人物会停下来,处于停止状态。