🟨

【Bevy 0.16.1】初心者がクリックできる画像をつくるのに右往左往した話

に公開

はじめに

今回はRustで構築された驚くほどシンプルなデータ駆動型ゲームエンジン【Bevy】についてです!
最新バージョン(2025/7/27現在) Bevy 0.16.1において、画像をクリックして遊べる超簡単なゲームをつくってみました!

1:インストールとBevyのECSについて

まずはBevyのインストールですが、これは簡単にできます。
初めてBevyに触るならこちらの方が初期設定やBevyの概要を理解する上でとても参考になります
https://www.youtube.com/watch?v=5k66KB6DisI
もうひとつ、バージョンは古い(0.10)ですが、ゲーム制作全体の流れをつかむには最適な動画です
https://www.youtube.com/watch?v=TQt-v_bFdao&list=PLVnntJRoP85JHGX7rGDu6LaF3fmDDbqyd

2:ゲーム

完成したじゃんけんゲーム!
https://github.com/KarterT-1000/Bevy_RSP
ここでやっていることは、

素材画像はこちらのサイトから
https://tsukatte.com/rock-paper-scissors/

3:まずやってみたこと

では画像を画面に追加してみます。
https://docs.rs/bevy/0.15.0/bevy/prelude/struct.SpriteBundle.html

以前のバージョンではSpriteBundleがつかえましたが、現在はPreludeから削除されたようです。
(Bevyのバージョンアップ情報

ですので最初はSpriteで画像を追加してみます。
じゃんけん画像はassetsのrpsフォルダーに入れています。

pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    commands.spawn(Sprite::from_image(asset_server.load("rps/paper.png")));
}


これにより「ぱー」の画像が画面中央に置かれました!

4:画像とカーソルの位置を確かめる

カーソルポジションで座標をとってみます。

fn print_cursor_position(windows: Query<&Window>) {
    let Ok(window) = windows.single() else {
        return;
    };

    if let Some(position) = window.cursor_position() {
        println!("Cursor position: x = {}, y = {}", position.x, position.y);
    }
}


これではビューポート座標とワールド座標が共存し、混乱のもとになります。
vievport_to_world_2dで変換することも可能ですが、もっと簡単にやりましょう!

5:ImageNodeでやってみる

ImageNodeで画像を追加してみます。

pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    commands.spawn((
        ImageNode::new(asset_server.load("rps/paper.png")),
        Node {
            //サイズを調整
            width: Val::Px(500.),
            height: Val::Px(500.),
            ..default()
        },
    ));
}


画像の原点座標が左上になりました!
これにより、画像とカーソルの座標系が一致し、扱いやすくなります。

しかし座標の扱いがややこしいので、よりシンプルな方法をとります

ここでUIのNodeとInteractionを使うことにしました。
https://docs.rs/bevy/latest/bevy/ui/index.html
これでマウスカーソルの座標を気にせずボタンのようにクリックやホバーを検知できるようになります。

commands
        .spawn(Node {
            //画像がきれいに並ぶように設定
            width: Val::Percent(100.),
            height: Val::Percent(100.),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            flex_direction: FlexDirection::Row,
            column_gap: Val::Px(20.),
            ..default()
        })
        //3枚の画像を追加していく
        .with_children(|parent| {
            for (hand, path) in [
                (Hand::Rock, "rsp/rock.png"),
                (Hand::Scissors, "rsp/scissors.png"),
                (Hand::Paper, "rsp/paper.png"),
            ] {
                parent.spawn((
                    ImageNode::from(asset_server.load(path)),
                    Node {
                        width: Val::Px(500.),
                        height: Val::Px(500.),
                        ..default()
                    },
                    //各画像に対しInteractionを指定
                    Interaction::None,
                    //各画像に対しぐーちょきパーを指定
                    RspButton(hand),
                ));
            }
        });

これでクリック可能な画像3枚を横一列に並べられました!
あとはランダムな手をだすCPUを追加し勝敗を判定させれば完成です

use bevy::prelude::*;
use rand::{Rng, thread_rng};

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

//比べることが可能なハンドRSP
#[derive(Component, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Hand {
    Rock,
    Scissors,
    Paper,
}

//クリックする画像はRock,Scissors,Paperのどれかである
#[derive(Component)]
pub struct RspButton(Hand);

pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
    commands.spawn(Camera2d);

    commands
        .spawn(Node {
            width: Val::Percent(100.),
            height: Val::Percent(100.),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            flex_direction: FlexDirection::Row,
            column_gap: Val::Px(20.),
            ..default()
        })
        .with_children(|parent| {
            for (hand, path) in [
                (Hand::Rock, "rsp/rock.png"),
                (Hand::Scissors, "rsp/scissors.png"),
                (Hand::Paper, "rsp/paper.png"),
            ] {
                parent.spawn((
                    ImageNode::from(asset_server.load(path)),
                    Node {
                        width: Val::Px(500.),
                        height: Val::Px(500.),
                        ..default()
                    },
                    //各画像に対しInteractionを指定
                    Interaction::None,
                    //各画像に対しぐーちょきパーを指定
                    RspButton(hand),
                ));
            }
        });
}

//ランダムに[ぐー] [ちょき] [ぱー] を選ぶ機能を追加!!
pub fn random_hand() -> Hand {
    match thread_rng().gen_range(0..=2) {
        0 => Hand::Rock,
        1 => Hand::Paper,
        _ => Hand::Scissors,
    }
}

//クリックした手とランダムな手を比べて勝敗を決定する機能を追加!!
pub fn rsp_system(mut query: Query<(&Interaction, &RspButton), Changed<Interaction>>) {
    // クリックに対して1回ランダムなCPUの手をだす
    let cpu_hand = random_hand();
    for (interaction, rps_button) in &mut query {
        if *interaction == Interaction::Pressed {
            let player_hand = rps_button.0;

            println!("『あなた: {player_hand:?}』 VS 『CPU: {cpu_hand:?}』");

            match (player_hand, cpu_hand) {
                (a, b) if a == b => println!("引き分け!"),
                (Hand::Rock, Hand::Scissors)
                | (Hand::Paper, Hand::Rock)
                | (Hand::Scissors, Hand::Paper) => println!("あなたの勝ち!"),
                _ => println!("あなたの負け!"),
            }
        }
    }
}


やりました!
画像をクリックするとターミナルに勝敗が表示されていますね!

おわりに

今回は飛び込みでBevyに挑戦したため、かなり試行錯誤することになりました。
じっくり勉強すればもっと複雑なゲームをつくれる気がしますね!
初心者だからこそ公式ドキュメントや最新のリリースノートを読みましょう!

今回の記事がBevyを学習し始めた方の参考になれば幸いです!
お疲れさまでした!

参考

Discussion