Behind Coleus

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

マイキャラ軍団



brogmymxo.hatenablog.comこれ以来の気に入るマイキャラが出来た気がする。

cancam.jp髪型はこの時のベイリスペクト

 

今回もBevy engineのコードレビューやっていきましょうねー

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 = 3;
pub const ENEMY_SPEED: f32 = 200.0;
pub const ENEMY_SIZE: f32 = 130.0;
pub const NUMBER_OF_STARS:usize = 10;
pub const STAR_SIZE: f32 = 30.0;
pub const STAR_SPAWN_TIME: f32 = 1.0;

fn main() {
    App::new()
    .add_plugins(DefaultPlugins)
    .init_resource::<Score>()
    .init_resource::<StarSpawnTimer>()
    .add_systems(Startup, spawn_camera)
    .add_systems(Startup, spawn_player)
    .add_systems(Startup, (spawn_enemies, spawn_stars))
    .add_systems(Update, (player_movement, confine_player_movement,
          enemy_hit_player,))
    .add_systems(Update,  (enemy_movement, update_enemy_direction, confine_enemy_movement).chain())
    .add_systems(Update, (player_hit_star, update_score, tick_star_spawn_timer, spawn_stars_over_time))
    .run();
}
#[derive(Resource)]
pub struct Score{
    pub value: u32,
}

impl Default for Score {
    fn default() -> Self {
        Score {value: 0}
    }
   
}
#[derive(Resource)]
pub struct StarSpawnTimer{
    pub timer: Timer,
}

impl Default for  StarSpawnTimer {
    fn default() -> Self {
        StarSpawnTimer{
            timer: Timer::from_seconds(STAR_SPAWN_TIME, TimerMode::Repeating),
        }
    }    
}
#[derive(Component)]
pub struct Player{}

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

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 spawn_stars(
    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_STARS{
        let random_x = random::<f32>() * (window.width() - STAR_SIZE);
        let random_y = random::<f32>() * (window.height() - STAR_SIZE);

        commands.spawn(
            (
                Sprite{
                image: asset_server.load("sprites/star.png"),
                ..default()
            },
            Transform::from_xyz(random_x, random_y, 0.0),
            Star{},
        ));
    }
}

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();
            }
        }
    }
}

pub fn player_hit_star(
    mut commands: Commands,
    player_query: Query<&Transform, With<Player>>,
    star_query: Query<(Entity, &Transform), With<Star>>,
    asset_server: Res<AssetServer>,
    mut score: ResMut<Score>,
){
    if let Ok(player_transform) = player_query.get_single(){
        for (star_entity, star_transform) in star_query.iter(){
            let distance = player_transform
            .translation
            .distance(star_transform.translation);
 
        if distance < PLAYER_SIZE / 2.0 + STAR_SIZE / 2.0{
            println!("Player hit star!");
            score.value += 1;
            let sound_effect = "audio/laserLarge_000.ogg";
            commands.spawn(AudioPlayer::new(
                asset_server.load(sound_effect),
            ));
            commands.entity(star_entity).despawn();
            }        
        }
    }
}

pub fn update_score(score: Res<Score>){
    if score.is_changed(){
        println!("score: {}", score.value.to_string())
    }
}
pub fn tick_star_spawn_timer(mut star_spawn_timer: ResMut<StarSpawnTimer>, time: Res<Time>) {
    star_spawn_timer.timer.tick(time.delta());
}

pub fn spawn_stars_over_time(
    mut commands: Commands,
    window_query: Query<&Window, With<PrimaryWindow>>,
    asset_server: Res<AssetServer>,
    star_spawn_timer: ResMut<StarSpawnTimer>
){
    if star_spawn_timer.timer.finished(){
        let window = window_query.get_single().unwrap();
        let random_x = random::<f32>() * (window.width() - STAR_SIZE);
        let random_y = random::<f32>() * (window.height() - STAR_SIZE);

        commands.spawn(
            (
                Sprite{
                image: asset_server.load("sprites/star.png"),
                ..default()
            },
            Transform::from_xyz(random_x, random_y, 0.0),
            Star{},
        ));
    }
}
   

今回はBevy_engineとRustを組み合わせた環境で、Visual studio codeを用いて、以上のコードを書き、それをビルドした。

基本的には、前回のコード

brogmymxo.hatenablog.comに、いくつかのリソース、コンポーネント、システムを追加した。

今回の新しい要素は、init_resource::<>(),と、Resourceの宣言、スターのランダムな配置(既出)それに、時間経過による新しいスターの生成関数。リソースの表示システム。スターとプレイヤーとの衝突システム(既出)この4つになります。

 

リソースとは、ほとんどコンポーネントと同じです。しかし、グローバルにアクセス可能なデータで、ゲーム全体に必要な数値を主にリソースにします。特に、設定や、構成データなどで使うそうです。各リソースはユニークである必要があり、同じタイプのインスタンスを複数持つことはできません。そのため、複数のインスタンスが必要な場合はエンティティとコンポーネントを使用するべきです。

リソースをメイン関数で管理するには、3パターンあります。

bevy-cheatbook.github.io


fn my_setup(mut commands: Commands) {
    commands.insert_resource(GoalsReached { main_goal: false, bonus: 100 });
    commands.init_resource::<MyFancyResource>();
    commands.remove_resource::<MyOtherResource>();
}

insert_resource(F{初期値});でもいいんですけど、管理が面倒くさいので、基本的には、

#[derive(Resource)]
pub struct Score{
    pub value: u32,
}

impl Default for Score {
    fn default() -> Self {
        Score {value: 0}
    }

Defalutトレイトというものを、実装しておきます。

すると、    commands.init_resource::<MyFancyResource>();のように、入力すると、なんと初期値をわざわざ打ち込まなくてもリソースを宣言できます。

qiita.comリソースは使う前に初期化が必要です。最も簡単には、リソースにDefaultトレイトを実装しておいて、app.init_resourceを呼びます。ここで書いていて気付いたんですが、まだAppについて説明していませんでした!またいずれ説明します。

最後のパターンは今回使いませんでした。

今回の場合、

#[derive(Resource)]
pub struct Score{
    pub value: u32,
}

impl Default for Score {
    fn default() -> Self {
        Score {value: 0}
    }
   
}
#[derive(Resource)]
pub struct StarSpawnTimer{
    pub timer: Timer,
}

impl Default for  StarSpawnTimer {
    fn default() -> Self {
        StarSpawnTimer{
            timer: Timer::from_seconds(STAR_SPAWN_TIME, TimerMode::Repeating),
        }
    }    
}

これで、リソースの宣言と、appでのリソースの使い方をやりました。

次に、spawn stars関数の解説をします。これは、spawn enemies関数とほとんど同じシステムです。

spawn stars関数の、for ループの回数、STAR_SIZEによる初期値の調整、コンポーネントの宣言、画像のパス、directionを宣言しないこと、だけが違い、あとは一緒です。今回、星は移動しないので、directionは宣言しなかったようです。

次に、時間経過でのスターを追加する関数について書いていきます。

pub fn tick_star_spawn_timer(mut star_spawn_timer: ResMut<StarSpawnTimer>, time: Res<Time>) {
    star_spawn_timer.timer.tick(time.delta());
}

pub fn spawn_stars_over_time(
    mut commands: Commands,
    window_query: Query<&Window, With<PrimaryWindow>>,
    asset_server: Res<AssetServer>,
    star_spawn_timer: ResMut<StarSpawnTimer>
){
    if star_spawn_timer.timer.finished(){
        let window = window_query.get_single().unwrap();
        let random_x = random::<f32>() * (window.width() - STAR_SIZE);
        let random_y = random::<f32>() * (window.height() - STAR_SIZE);

        commands.spawn(
            (
                Sprite{
                image: asset_server.load("sprites/star.png"),
                ..default()
            },
            Transform::from_xyz(random_x, random_y, 0.0),
            Star{},
        ));
    }
}
   

tick_star_spawn_timerシステムでは、引数でtime.delta()という1フレームの微小時間を使うために、Res<Time>を用いています。star_spawn_timer: ResMut<StarSpawnTimer> 可変参照で、StarSpawnTimerというリソースを呼び出しています。
それは今どうなっているかというと、

#[derive(Resource)]
pub struct StarSpawnTimer{
    pub timer: Timer,
}

impl Default for  StarSpawnTimer {
    fn default() -> Self {
        StarSpawnTimer{
            timer: Timer::from_seconds(STAR_SPAWN_TIME, TimerMode::Repeating),
        }
    }    
}

この値がデフォルトで設定されている。これは、STAR SPAWN TIMEになったら、0に戻るタイマーを設定するという仕組みになっている。

その次の、spawn_stars_overtimeシステムは基本的に、spawn_starsシステムと同じなのだが、spawn_starsを繰り返しではなく、毎フレーム呼び出す条件にしている。

tick_star_spawn_timerシステムは、毎フレーム呼び出され、star_spawn_timer.timerを更新します。

timer.tick(delta)メソッドは、タイマーを進めるために必要な処理です。このメソッドは、タイマーの内部状態を更新し、経過時間を加算します。

ChatGPTより引用

これは、タイマーを設置する関数、似ているものをあげるとキッチンタイマーの様な感じです。

    if star_spawn_timer.timer.finished():
        finished()メソッドは、タイマーが設定された時間に達したかどうかをチェックします。
    変数と値:
        finished()はブール値(bool型)を返し、タイマーの状態に応じてtrueまたはfalseを取ります。
        タイマーがまだ経過していない場合、finished()はfalseを返し、経過した場合はtrueを返します。この値によって、星をスポーンするかどうかの条件分岐が行われます。

timer.finished():をifにいれることで、star_spawn_timerが鳴っているかどうかをチェックをします。

つまり、最初のリソースの初期化が、キッチンタイマーを特定の時間にセット、一つ目のシステムが、キッチンタイマーを動かし続ける関数、二つ目のシステムが、タイマーが0になったら何かをする(キッチンタイマーの場合音が鳴るが今回は星が生まれる)関数。この3つの組み合わせで、時間経過で繰り返しなにかを行う関数にしている。

次に、現在のリソースを表示するシステムと、リソースを更新するシステムについて解説する。

star hit playerシステムでは、enemy hit playerと一緒で、まず、プレイヤーとスターの座標を取得し、starとplayer間の空間距離を計算し、それがstarとplayerの大きさの和の1/2未満になったら、starエンティティを消すという処理をしているんだけど、その際に、Score Resourceを1足す処理をしている。

        if distance < PLAYER_SIZE / 2.0 + STAR_SIZE / 2.0{
            println!("Player hit star!");
            score.value += 1;
            let sound_effect = "audio/laserLarge_000.ogg";
            commands.spawn(AudioPlayer::new(
                asset_server.load(sound_effect),
            ));

それにより、スターを取得する度、毎回1ずつscoreリソースのvalueが更新されることになっている。なお、enemy hit playerと同じように、音を鳴らす処理とhitを表示する処理もしている。

ちなみに、前述もしたが、最初にリソースを読み込む際に、scoreリソースのvalueは0に初期化した。

そして、使うのは、.is_changed()である、これは、scoreが変化したフレームでだけ、trueを返すbool型の変数である。

pub fn update_score(score: Res<Score>){
    if score.is_changed(){
        println!("score: {}", score.value.to_string())
    }
}

以上の様にUpdateスケジュールで呼び出すシステムにif文で組み込めば、scoreが変わった時だけ、この関数を呼び出すことが出来る。

ちなみに、.to_string()は使う必要性はまったくない。

大丈夫です。
スターと接触すると、スコアが増えるシステムと、スコアが変化した時に呼び出されるprintln関数で、スコアの増加を表示した。

まとめると、

今回追加した機能は、startupスケジュールでの、スターのランダム配置 、これは、spawn stars関数により実装した。

スターとプレイヤーのヒット機能、それに伴うリソースの変化を記述する機能を追加した。 それは、最初のscore.valueリソース、star hit playerシステム update scoreシステムの組み合わせにより実装した。

タイマーによるスターの追加機能は、最初のstar_spawn_timer.timerのリソース、tick_star_spawn_timerシステム、spawn_star_over_timeシステムの3つの組み合わせにより実装した。

 

 

 

検証

最初から、NUMBERS OF STARS(今回は10)と同じ数スターが表示された

時間経過とともに星が増えた(今回は1秒)

星に触れたら、以下の事象が起きた 

音が鳴った 星が消えた terminalにスコアが記録された。

これにより、今回の目的は達成できたと言える。

 



*1:player_entity, player_transform