0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rust×Bevyゲーム開発レシピ: スプライトシートを使ってアニメーションさせよう

Posted at

Bevyを使った個人ゲーム開発を行なっている登尾(のぼりお)です。今回はBevy上でスプライトシートを使ったアニメーションをさせてみましょう。

2025-08-2421.30.17-ezgif.com-video-to-gif-converter.gif

アセットを読みこもう

利用したアセットはこちらです。

その中でも以下のスプライトシートを今回動かしてみましょう。

p3_spritesheet.png

(ダウンロードしたZip内のパス: assets/platformer-art-complete-pack/Base pack/Player/p3_spritesheet.png)

ダウンロードしたファイルを解凍後、以下のパスにファイルを置いている前提で話を進めます。

assets/platformer-art-complete-pack

スプライトシートというと複数のスプライトを一緒にまとめている形式で実体はpngファイルです。Bevyからは以下のようにAssetServerを使い画像としてシンプルに読み込めます。

let texture =
        asset_server.load("platformer-art-complete-pack/Base pack/Player/p3_spritesheet.png");

このファイルをただ表示させるのではなくスプライトシートとして扱いアニメーションさせる場合、Bevy上のexampleとしては以下が参考になります。

以下のように分かりやすいサンプルで、

let texture = asset_server.load("textures/rpg/chars/gabe/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);
// Use only the subset of sprites in the sheet that make up the run animation
let animation_indices = AnimationIndices { first: 1, last: 6 };

スプライトシートが横一列で並んだスプライトがありそれを順に表示させようという例なのですが、実は、今回利用しようとしていたPlatformer Art Complete Packに適用させようとするとうまくいきません。

というのも、アニメーションさせたい順番にスプライトシートの画像が並んでいないことが理由で、一緒に添付されてるp3_spritesheet.txtを見てみると分かります。

p3_duck = 365 98 69 71
p3_front = 0 196 66 92
p3_hurt = 438 0 69 92
p3_jump = 438 93 67 94
p3_stand = 67 196 66 92
p3_walk01 = 0 0 72 97
p3_walk02 = 73 0 72 97
p3_walk03 = 146 0 72 97
p3_walk04 = 0 98 72 97
p3_walk05 = 73 98 72 97
p3_walk06 = 146 98 72 97
p3_walk07 = 219 0 72 97
p3_walk08 = 292 0 72 97
p3_walk09 = 219 98 72 97
p3_walk10 = 365 0 72 97
p3_walk11 = 292 98 72 97

アニメーションのIDに対応してx、y、width、heightが並んでいるのですが、よく見るとwalkとしてアニメーションさせたい値に規則性がなく、width、heightは一貫して同じ値ですが、x、yは飛び飛びであることが分かります。

こういったスプライトシートをアニメーションさせる場合の実装について以降解説していきます。
(もしシンプルな順序で並ぶスプライトシートをアニメーションさせる場合は先に挙げましたBevyの例の方が参考になると思います。)

位置に規則性がないスプライトシートのアニメーション

ポイントは最初に空のTextureAtlasLayoutを作りそこに各スプライトごとの描画範囲の枠を指定していくことです。

事前に、以下のようなコンポーネントを用意し、

#[derive(Component)]
struct AnimationIndices {
    order: Vec<usize>,
}

以下のようなsetupシステムを用意しましょう。

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut layouts: ResMut<Assets<TextureAtlasLayout>>,
) {
    let sheet_size = UVec2::new(508, 288);
    let mut layout = TextureAtlasLayout::new_empty(sheet_size);

    let order = vec![
        layout.add_texture(URect::from_corners(
            UVec2::new(0, 0),
            UVec2::new(72 + 0, 97 + 0),
        )),
        layout.add_texture(URect::from_corners(
            UVec2::new(73, 0),
            UVec2::new(72 + 73, 97 + 0),
        )),
        ...
    ];

    let layout_handle = layouts.add(layout);

    let texture = asset_server.load("platformer-art-complete-pack/Base pack/Player/p3_spritesheet.png");

    commands.spawn(Camera2d);

    commands.spawn((
        Sprite::from_atlas_image(
            texture,
            TextureAtlas {
                layout: layout_handle,
                index: order[0],
            },
        ),
        Transform::from_scale(Vec3::splat(1.25)),
        AnimationIndices { order },
        AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
    ));
}

前述しましたp3_spritesheet.txtにはwalkのアニメーションがありましたので、orderの中にadd_textureしてそれぞれのスプライトの範囲を渡しています。

あとはUpdateごとに呼ばれる関数を用意し順次、次のスプライト、スプライトと進めていけばアニメーションされることになります。

fn animate(
    time: Res<Time>,
    mut q: Query<(&mut Sprite, &mut AnimationTimer, &mut AnimationIndices)>,
) {
    for (mut sprite, mut timer, indices) in &mut q {
        timer.tick(time.delta());

        if timer.just_finished() {
            if let Some(atlas) = &mut sprite.texture_atlas {
                atlas.index = if atlas.index == *indices.order.last().unwrap() {
                    *indices.order.first().unwrap()
                } else {
                    atlas.index + 1
                };
            }
        }
    }
}

あとは蛇足になりますが、アニメーションさせたいタイミングをコントロールするためにTimerをうまく使えばいい感じにコントロールできます。

#[derive(Component, Deref, DerefMut)]
struct AnimationTimer(Timer);

fn main() {
    App::new()
        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
        .add_systems(Startup, setup)
        .add_systems(Update, animate)
        .run();
}

2025-08-2421.30.17-ezgif.com-video-to-gif-converter.gif

おしまい

今回のコードは以下の個人リポジトリで公開しています。

cloneした後に、

% cargo run --example spritesheet 

で挙動を起動できます。
スプライトシートを使ったアニメーションを楽しく実装できるBevyですので、ぜひ面白いゲームを作って見てください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?