Behind Coleus

大学生なのに特別大学で楽しいことが一 つもない。虐待と男子校病のせいにしてみる。→日本脱出しました 今ジャカルタにいます。→ジャカルタから帰ってきました。

2月振り返り

今日も、bevyのコードの解説やっていきましょう。

youtu.be

github.com

今回の目的

前回作成したランダムに配置された敵を壁に反射するように動かす。
プレイヤーの画像と敵画像が衝突した際に、エンティティごとプレイヤーを消滅させるようにする。

 

目的への取り組み

以下のコードを作成した。

 
use bevy::{prelude::*, window::PrimaryWindow};
use rand::random;
pub const PLAYER_SPEED: f32 = 500.0;
pub const PLAYER_SIZE: f32 = 130.0;
pub const NUMBER_OF_ENEMIES: usize = 6;
pub const ENEMY_SPEED: f32 = 200.0;
pub const ENEMY_SIZE: f32 = 130.0;

fn main() {
    App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Startup, spawn_camera)
    .add_systems(Startup, spawn_player)
    .add_systems(Startup, spawn_enemies)
    .add_systems(Update, (player_movement, confine_player_movement,
          enemy_hit_player,))
    .add_systems(Update,  (enemy_movement, update_enemy_direction, confine_enemy_movement).chain())
    .run();
}

#[derive(Component)]
pub struct Player{}

#[derive(Component)]
pub struct Enemy{
    pub direction: Vec2,
}

pub fn spawn_player(
    mut commands: Commands,
    window_query: Query<&Window, With<PrimaryWindow>>,
    asset_server: Res<AssetServer>,
){
    let window = window_query.get_single().unwrap();

    commands.spawn(
        (
            Sprite{
            image: asset_server.load("sprites/ball_blue_large.png"),
            ..default()
        },
        Transform::from_xyz(window.width()/2.0, window.height()/2.0, 0.0),
        Player {},        
    ));
}

pub fn spawn_camera(mut commands: Commands, window_query: Query<&Window, With<PrimaryWindow>>) {
    let window = window_query.get_single().unwrap();

    commands.spawn(
        (
            Camera2d::default(),
            Camera{
                hdr:true, ..default()
            },
            Transform::from_xyz(window.width()/2.0, window.height()/2.0, 0.0),
        )

    );
}

pub fn spawn_enemies(
    mut commands: Commands,
    window_query: Query<&Window, With<PrimaryWindow>>,
    asset_server: Res<AssetServer>,
){
    let window = window_query.get_single().unwrap();
    for _ in 0..NUMBER_OF_ENEMIES{
        let random_x = random::<f32>() * (window.width() - ENEMY_SIZE);
        let random_y = random::<f32>() * (window.height() - ENEMY_SIZE);

        commands.spawn(
            (
                Sprite{
                image: asset_server.load("sprites/ball_red_large.png"),
                ..default()
            },
            Transform::from_xyz(random_x, random_y, 0.0),
            Enemy {
                direction: Vec2::new(random::<f32>()-0.5, random::<f32>()-0.5)
                .normalize(),
            },
        ));
    }

}

pub fn player_movement(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mut player_query: Query<&mut Transform, With<Player>>,
    time: Res<Time>,
){
    if let Ok(mut transform) = player_query.get_single_mut(){
        let mut direction = Vec3::ZERO;

        if keyboard_input.pressed(KeyCode::ArrowLeft)||keyboard_input.pressed(KeyCode::KeyA){
            direction += Vec3::new(-1.0, 0.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::ArrowRight)||keyboard_input.pressed(KeyCode::KeyD){
            direction += Vec3::new(1.0, 0.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::ArrowUp) || keyboard_input.pressed(KeyCode::KeyW) {
            direction += Vec3::new(0.0, 1.0, 0.0);
        }
        if keyboard_input.pressed(KeyCode::ArrowDown) || keyboard_input.pressed(KeyCode::KeyS) {
            direction += Vec3::new(0.0, -1.0, 0.0);
        }

        if direction.length() > 0.0{
            direction = direction.normalize();
        }

        transform.translation += direction * PLAYER_SPEED * time.delta_secs();
    }
}

pub fn confine_player_movement(
    mut player_query: Query<&mut Transform, With<Player>>,
    window_query: Query<&Window, With<PrimaryWindow>>
){
    if let Ok(mut player_transform) = player_query.get_single_mut(){
        let window = window_query.get_single().unwrap();

        let half_player_size = PLAYER_SIZE / 2.0;
        let x_min = 0.0 + half_player_size;
        let x_max = window.width()/1.0 - half_player_size;
        let y_min = 0.0 + half_player_size;
        let y_max = window.height()/1.0 -half_player_size;

        let mut translation = player_transform.translation;

        translation.x = translation.x.clamp(x_min, x_max);
        translation.y = translation.y.clamp(y_min, y_max);
        player_transform.translation = translation;

    }
}

pub fn enemy_movement(
    mut enemy_query: Query<(&mut Transform, &Enemy)>, time: Res<Time>){
    for(mut transform, enemy)in enemy_query.iter_mut(){
        let direction = Vec3::new(enemy.direction.x, enemy.direction.y, 0.0);
        transform.translation += direction * ENEMY_SPEED * time.delta_secs();
    }
}

pub fn update_enemy_direction(
    mut enemy_query: Query<(&Transform, &mut Enemy)>,
    window_query: Query<&Window, With<PrimaryWindow>>,
    mut commands: Commands,
    asset_server: Res<AssetServer>,
){
    let window = window_query.get_single().unwrap();
    let half_enemy_size = ENEMY_SIZE / 2.0;
    let x_min = 0.0 + half_enemy_size;
    let x_max = window.width() - half_enemy_size;
    let y_min = 0.0 + half_enemy_size;
    let y_max = window.height() - half_enemy_size;

    for (transform, mut enemy) in enemy_query.iter_mut() {
        let mut direction_changed = false;


        let translation = transform.translation;
        if translation.x < x_min || translation.x > x_max {
            enemy.direction.x *= -1.0;
            direction_changed = true;        
        }
        if translation.y < y_min || translation.y > y_max {
            enemy.direction.y *= -1.0;
            direction_changed = true;        
        }

           
        if direction_changed{

            let sound_effect_1 = "audio/pluck_001.ogg";
            let sound_effect_2 = "audio/pluck_002.ogg";

            let sound_effect:bool = random::<bool>();
            if sound_effect{
                commands.spawn(AudioPlayer::new(
                    asset_server.load(sound_effect_1),
                ));
            }else {
                commands.spawn(AudioPlayer::new(
                    asset_server.load(sound_effect_2),
                ));
            };

        }
    }
}
pub fn confine_enemy_movement(
    mut enemy_query: Query<&mut Transform, With<Enemy>>,
    window_query: Query<&Window, With<PrimaryWindow>>
){

        let window = window_query.get_single().unwrap();

        let half_enemy_size = PLAYER_SIZE / 2.0;
        let x_min = 0.0 + half_enemy_size;
        let x_max = window.width()/1.0 - half_enemy_size;
        let y_min = 0.0 + half_enemy_size;
        let y_max = window.height()/1.0 - half_enemy_size;

        for mut transform in enemy_query.iter_mut(){
            let mut translation = transform.translation;

            translation.x = translation.x.clamp(x_min, x_max);
            translation.y = translation.y.clamp(y_min, y_max);
            transform.translation = translation;
        }
   
}

pub fn enemy_hit_player(
    mut commands: Commands,
    mut player_query: Query<(Entity, &Transform), With<Player>>,
    enemy_query: Query<&Transform, With<Enemy>>,
    asset_server: Res<AssetServer>,
){
    if let Ok*1 = player_query.get_single_mut(){
        for enemy_transform in enemy_query.iter(){
            let distance = player_transform
                            .translation
                            .distance(enemy_transform.translation);
            let player_radius = PLAYER_SIZE /2.0;
            let enemy_radius = ENEMY_SIZE /2.0;
            if distance < player_radius + enemy_radius{
                println!("Enemy hit player! Game Over!!");
                let sound_effect = "audio/explosionCrunch_000.ogg";
                commands.spawn(AudioPlayer::new(
                    asset_server.load(sound_effect),
                ));
                commands.entity(player_entity).despawn();
            }
        }
    }
}


今回は、前回のコード

brogmymxo.hatenablog.com
に4つのアップデートスケジュールのシステムを追加して機能を実装しようと考えた。
一つ目のシステムenemy movement関数について説明する。

まず、Enemyエンティティのコンポーネントに2次元ベクトル型の変数directionを追加した。これは、direction.xと、direction.yを格納するという意味である。

 

そして、前回作成した、enemy spawn関数で、Enemyのentityだけを宣言していたが、そこにdirection.xとdirection.yを[-0.5, 0.5]の範囲で設定した。また、スピードを一定にするためにそのベクトルを正規化した。元コードでは、[0, 1]の範囲で0から90°のベクトルにしか動かなかった。そのため改良を施した。

その情報をクエリにより取得し、Transformに前回のplayer movement関数と同様にに伝えた。なお、2次元ベクトルの変数を1度3次元にしてから、伝えている。
これは、この作者のコードを参考にした。

この際、勉強になったのが、可変参照についてである。このプログラムでは、最初に取得するTransformを、&mut という風に参照しなければ、後に可変参照をすることが出来ないというのが分かった。 今回の場合、座標を変えたいので、Transformの前に&mutをつけてクエリから参照している。4行上の画像の様に、transform

次は、Update enemy directionコードである。

これは、transformを取得し、enemy.directionを、windowの壁ギリギリになったら、-1するプログラムです。その際、左右の壁に衝突したら、enemy.direction.xを-1倍して、

つまり、反射を実装しています。その際に、音を鳴らすこともcommand spawn機能によって実装をしています。

13fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
14    commands.spawn(AudioPlayer::new(
15        asset_server.load("sounds/Windless Slopes.ogg"),
16    ));
17}

docs.rsコード例では、audio.playというのを使っていたが、それはエラーが出て使えなかったので、bevyのAudioPlayerと言う機能で、entityとして、音を追加した。また、その際に、元コードを参考にrandom::<bool>()を用いて、確率50%で違う音が鳴るようにした。 こういうのが美学って言うんだなと思った。

 

confine enemy movement関数を解説します。

pub fn confine_enemy_movement(
    mut enemy_query: Query<&mut Transform, With<Enemy>>,
    window_query: Query<&Window, With<PrimaryWindow>>
){

        let window = window_query.get_single().unwrap();

        let half_enemy_size = PLAYER_SIZE / 2.0;
        let x_min = 0.0 + half_enemy_size;
        let x_max = window.width()/1.0 - half_enemy_size;
        let y_min = 0.0 + half_enemy_size;
        let y_max = window.height()/1.0 - half_enemy_size;

        for mut transform in enemy_query.iter_mut(){
            let mut translation = transform.translation;

            translation.x = translation.x.clamp(x_min, x_max);
            translation.y = translation.y.clamp(y_min, y_max);
            transform.translation = translation;
        }
   
}

これはすべてのenemyのtransformについて繰り返しているが、


pub fn confine_player_movement(
    mut player_query: Query<&mut Transform, With<Player>>,
    window_query: Query<&Window, With<PrimaryWindow>>
){
    if let Ok(mut player_transform) = player_query.get_single_mut(){
        let window = window_query.get_single().unwrap();

        let half_player_size = PLAYER_SIZE / 2.0;
        let x_min = 0.0 + half_player_size;
        let x_max = window.width()/1.0 - half_player_size;
        let y_min = 0.0 + half_player_size;
        let y_max = window.height()/1.0 -half_player_size;

        let mut translation = player_transform.translation;

        translation.x = translation.x.clamp(x_min, x_max);
        translation.y = translation.y.clamp(y_min, y_max);
        player_transform.translation = translation;

    }
}

こちらは、get.singlemut()で取得して、一回の動作にしている。それだけの違いである。

次に、enemy hit player 関数を解説する。引数を、4つとっている。

今回は、最終的にプレイヤーエンティティを削除したいので、mut command: Command, を取った。

2,3個目の引数は、プレイヤーと敵の座標をクエリから取得するために、使っている。
player_queryは今回変更する予定はないが、

player_query.get_single_mut()

追記これを使うためにmutにしている。2025/3/2.get_single()を使えば、mutで宣言する必要は全くない。

 

   if let Ok*2 = player_query.get_single_mut(){
        for enemy_transform in enemy_query.iter(){
            let distance = player_transform
                            .translation
                            .distance(enemy_transform.translation);
            let player_radius = PLAYER_SIZE /2.0;
            let enemy_radius = ENEMY_SIZE /2.0;
            if distance < player_radius + enemy_radius{
                println!("Enemy hit player! Game Over!!");
                let sound_effect = "audio/explosionCrunch_000.ogg";
                commands.spawn(AudioPlayer::new(
                    asset_server.load(sound_effect),
                ));
                commands.entity(player_entity).despawn();
            }
        }
    }

問題のこの部分は、実はそんな難しい事を言っていない。

新しいことは二つである。 クエリによって、プレイヤーとエネミーのtranslationを取得してきた。

            let distance = player_transform
                            .translation
                            .distance(enemy_transform.translation);

二つの座標を比較して、距離を導き出すのが、.distance()である

pub fn distance(self, rhs: Vec3) -> f32

Computes the Euclidean distance between two points in space.

docs.rs

次に新しいことは、

                commands.entity(player_entity).despawn();

これである。これにより、プレイヤーのエンティティが削除された。

他に言及すべき部分として、衝突の判定メカニズムは、プレイヤーとエネミーとの距離が、それぞれの半径の和を下回った時とした。

メイン関数にも工夫を施した。

システムを追加する順番をenemy movement, enemy direction update, confine enemy movementの順になるように.chain()を用いた。

 

qiita.com

 

Chain

処理A、処理Bの順番で実行したい場合.chain()を利用する。
以下のコードの場合は(system_a, system_b).chain()と書くことでsystem_asystem_bの順番で実行されるようになる。
hello_worldは順序関係なしにUpdateが始まったタイミング実行される

 
fn main() {
   App::new()
       .add_systems(Startup, startup_system)
       .add_systems(Update, (hello_world, (system_a, system_b).chain()))
       .run();
}
 

 

 
fn main() {
    App::new()
    .add_plugins(DefaultPlugins)
    .add_systems(Startup, spawn_camera)
    .add_systems(Startup, spawn_player)
    .add_systems(Startup, spawn_enemies)
    .add_systems(Update, (player_movement, confine_player_movement,
          enemy_hit_player,))
    .add_systems(Update, (player_movement, confine_player_movement,
enemy_movement, update_enemy_direction, confine_enemy_movement,
    .run();
}

#[derive(Component)]
pub struct Player{}

#[derive(Component)]
pub struct Enemy{
    pub direction: Vec2,
}

なお、この形でメイン関数を実行した場合Updateスケジュールで追加されたシステムが五月雨に追加されることによりバグを起こし、画面端に当たると停止するという挙動が、何ビルドかに一回発生するようになってしまう。


検証

敵は最初から様々な方向に動いた。

音はいい感じにばらけて鳴った。

敵の位置がランダムなのでリスキルされてゲームとして動かないことも結構あった。

ゲームオーバーの表示はなされた。

プレイヤーが敵に当たったら、音が鳴り、プレイヤーは消滅した。

敵は反射した。

敵は壁に詰まらなかった。

一つ上の画像の様なバグは10回起動しても起こらなかった。

 

今日は、今のうちに、1月2月を振り返っておこうと思う。

1月はよかった。

12月30日に日本で会ったインドネシア人の人と会って、それからあまり人と会うことはなかった。

12月31日の夜に滑り込みで、散髪をしてもらったことを覚えている。私の来る間が悪かったらしく店員と少し気まずかった、シャワー付き29rbだったところ、シャワー無し30rb取られたが黙ってた。

まず年始はかなり心の晴れた生活だった。
というのも、いつもうつ病になる親戚の集まりの参加を回避したし、

brogmymxo.hatenablog.comJakartaでは、意外とご飯屋さんがやっていたし、こんなにも気持ちが晴れていいんだという気持ちになった。

11月から始めていた、視覚障害の勉強も終わったし、

その時に得た凄い情報、


これで、ゲームをやれるんじゃないかという気持ちになる。

毎日好きな物を選んで食べれる環境を凄く楽しめた。

不自由ではあったんだけど、自分で選べている感覚を感じることが出来た。
クラスの美少女にもらったクッキーを食べながら過ごした夜。気分はよかった。

そのあと、g3にいたがちで歓迎してくれた男と遊びに連れて行ってもらったりした。

あとはアチキミコって言う人にゲームカフェに連れて行ってもらったり、。

そして、留学最後の2週間は最初の様にお勧めされた場所に行ったり、最後にやりたかったことをたくさんやった。そしたらみんな当分会わないから、本当に人間関係が気が楽だった。 とくに、インドネシアのローカルなフルーツを食べたら本当に気分がよかった。 女の子とデートも2回もしたし 優しかったし、こんなに気分のいい1月は当分ないだろう。 なんか韓国人の女が急に優しくなったのも覚えている。 

ChatGPTに相談すると、いつも自分と向き合うことが必要だ!!とか、本当の問題は容姿や周りのやさしさではない。とか言ってたけど、それは結局結果論で、物事をポジティブにとらえるのにはある程度、最初にまず優しくされる必要がある。

人間は、見た目や外的な刺激だけで満たされることはできなくて、自分の内面との調和 が本当の幸せを感じる鍵になったりする。だから、美人と一緒にいることが必ずしも幸せではないと感じるのは、ある意味自然なことなんだよね。


これはプロパガンダ。日本で傷ついた心は日本では修復できないような仕組みになっていた。

でも、最後のマレーシアでのスタックは大変だった。 でも、アイビー様が助けてくれたから笑い話にはなった。1月は最後まで運がよかった。


そして、1/28の深夜に帰宅して、1/29は寝た。1/30の夜から、筑波に戻った。

様々な忘れていた感覚を思い出した。

最初の方は、寝具の適切な使い方を忘れていて、結構な不眠になっていた記憶がある。
早速父への不快な感覚とそれが結びついて、起きる直前ベットから蹴落とされるような感覚を感じた。

1/31に高床邸に行って、お仲間とカレーを食べた。そのあと、アジア住販に連れて行ってもらって部屋探しをした。

2/1に幼馴染??の女と一緒にご飯食べた。福永のはなしを彼氏持ちの女にはしちゃうんだけど、男子校育ちの人間が、共学で高校時代からの彼氏がいる女に努力不足!!!って怒られててかわいそうだった。 

2/2は一日中トリリオンゲーム見てた。

2/3はアキバで携帯とssdを買いました。アキバの店員のすばらしさに感動した覚えがある。

2/4は昼からつくばに戻った いろいろセッティングしてたらすぐ終わった。

2/5は網膜色素変性症の遺伝について勉強したり、Bevyについて少しだけ見たりした。

2/6は面談、結婚観について研究することを決めた。 このころは6日ごとに進捗の必要な面談があり、毎回少し焦ってた記憶がある。 そのあと、ぷり君と写真の現像に行き、そのあと夜はクラスの画像を見せたときに韓国人がpretty!!って言ってた美女と一緒にご飯にいった。そしたら、日本人女性の美しさに少し感動できた。

2/7は午前中はゲーム理論を勉強していた。ただ、夕方からプレリリースに行った。しかし、それはおうちでプレリリースだったので、対戦が出来なくて残念だった。しかも、レアカードはあんまりいいのが出なかった。 ケトラモーズは出なかった。

再利用隔室と、ミルするアーティファクトオーバーロードの復讐蔦ぐらいしか出なかった。

2/8からプログラミングに挑戦した。 勉強せずchatGPTに書かせる形で、コーディングしていたが、じゃんけんや、カードリストの読み込みくらいはできたが、それ以降のゲーム進行について、2/22日ごろに、まったく手が動かなくなり、大変になった。

2/9同上

2/10プログラグラミングに取り組もうとしたが、多分はーばるひーりんぐとか言う質の低い同人エロゲーをプレイしている。

2/11クラスの美女と一緒にご飯行った。 そしたら、もっと他人に自分を知ってもらった方がいいというマジレス来た。その前に謎の女好きの呪われた男に

2/12も頑張ってプログラミングしたり、先生と面談したりしている。ここらへんで睡眠の問題を感じ始めていたんだけど、枕の使い方、具体的には私が使っている座布団と薄い枕の組み合わせをしたら、結構寝られるようになった。証明写真を撮った

2/13 前日深夜に書き、頑張って教育実習の調書などを提出した。

2/14健康診断に行って、モカ君みたいな人がいて話しかけたら違って気まずかった。

マオ先生と会って立ち話した記憶がある。そのあとかえってプログラミングしてたみたい。

2/15 またプログラミングして夜母親宅に帰った。名分

brogmymxo.hatenablog.comこれを深夜に書いた。

2/16は忙しかった。 モスバーガーを食べたり、ドライヤーを買ったり、不動産の抑え、3月バイトのシフトの提出、tactの文章の修正などを行った。

2/17 この日は、面白かった。朝から上越新幹線にのって越後湯沢まで行った。朝10時からスキーを滑った。福永、kaityaxnセット、金持ち、なおき、と一緒に行った

2/18この日も面白かった。スキーに行った。

2/19朝から大学に帰って、申し込んでいた、論文を回収して先生と面談してご飯作ったら疲れた。馬鹿すぎてジャーマンポテト作ったのに、1日で食べちゃった。この日最強美少女と、大学にいる唯一のフレンドリーさをかんじる謎の男と若干の駄弁りした。

2/20朝から、卒論の構想レポートを書き、エクファンの紙を作成してアキバにキューブ用コンポーネントを買って、エクファンもやった。20時30にやめて夕飯なくてしんどかった覚えがある。カロリーメイトをバスの中で食べた。

2/21のそのそ起きてきて筑波の卒展めぐりして、夜飯作ってからずっとキューブ整理してた。そのうえ、

2/22 だいたいゲームロジックの仕様書が完成して、終わったけど、チャットgptの無能さに気付く

2/23 乃木坂の美術館と、渋谷女児本回収に行って、クソでかくそ馬パスタ作った

2/24 大学PCで、プログラム出来たらdistraction(分心)がなくて楽かなと思って、環境構築しようとしたけど、全然コンパイルできなくてあきらめた。この日から、毎日プログラムの解説やってた。結構やる気があった。。。

2/25 受験と父親からのゆうパックでバカ鬱で、テクノの面接もあって大変だった。

プログラムもちょっとはやった。でも、夜に韓国人からprettyと言われていた美女と飯に行けて少しは仲良くなったので、まだ休まった。

2/26 sigil siegeっていうゲームにハマってたが、コードの書き貯めがあったので、大丈夫だった。ドカ鬱ゆうパックを代わりに開けてもらって、20%の5000円くらいあげちゃいました。その日は優しくて10時には帰ってこれた。

2/27 10時からの面談から帰ってきたら、流石に疲れちゃったから、ちょっとプログラミングやめて。にーまるとゲームして、夕飯で大量の野菜炒め作った。夜にエロゲのキャラメイクにハマっちゃって、楽しかったけど、夜更かししちゃった。

2/28 久しぶりの全休で、ごみ捨てとか、プログラミングとかしてた、ただ、あまりやる気はなかった。

 

 

 

 

 

 

*1:player_entity, player_transform

*2:player_entity, player_transform