ゲーム実装で身に付くプログラミング 3

背景色に色を混ぜる

背景色に色を混ぜる

ここで、このゲームの肝である背景色を表示し、図4のようにクリックした色ブロックの色を背景色に混ぜてみます。ECSのSystemでゲームステートがMainLoopの時に更新ごと(Update)に「logic_system」関数を実行します。

図4:背景色をセットしたところ

・サンプルコード「main.rs」ファイル

(前略)
// メイン
fn main() {
 App::new()
(中略)
   .add_systems(
     Update,
     (
       movement_system,
       input_system,
       logic_system,
     )
     .run_if(in_state(GameState::MainLoop)),
   )
   .run();
}
// 初期化
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
(中略)
}
// タイトル
fn title_system(
 mouse: Res<ButtonInput<MouseButton>>,
 mut next_state: ResMut<NextState<GameState>>,
) {
(中略)
}
// タイトル画像表示
fn reset_for_title(
 mut title_q: Query<&mut Visibility, With<TitleVisual>>,
 mut item_q: Query<&mut Visibility, (With<ColorItem>, Without<TitleVisual>)>,
) {
(中略)
}
// Stage開始
fn start_stage(
 mut game_data: ResMut<GameData>,
 mut title_q: Query<&mut Visibility, With<TitleVisual>>,
 mut item_q: Query<(&mut Visibility, &mut Transform, &mut ColorItem), Without<TitleVisual>>,
) {
 if let Ok(mut v) = title_q.single_mut() {
   *v = Visibility::Hidden;
 }
 let mut rng = rand::rng();
 let mut r:usize = 0;
 let mut g:usize = 0;
 let mut b:usize = 0;
 while r == g {
   r = rng.random_range(0..game_data.stage);
   g = rng.random_range(0..game_data.stage);
   b = rng.random_range(0..game_data.stage);
 }
 game_data.rgb_counts = Vec3::new(r as f32,g as f32,b as f32);
 let active_count = (game_data.stage * 3).min(50);
 for (i, (mut vis, mut trans, mut item)) in item_q.iter_mut().enumerate() {
   if i < active_count {
     *vis = Visibility::Visible;
     trans.translation = Vec3::new(
       rng.random_range(-450.0..450.0),
       rng.random_range(-280.0..280.0),
       1.0,
     );
     item.velocity = Vec2::new(
       rng.random_range(-0.2..0.2),
       rng.random_range(-0.2..0.2),
     );
   } else {
     *vis = Visibility::Hidden;
   }
 }
}
// 色ブロック移動
fn movement_system(
 mut query: Query<(&mut Transform, &ColorItem), With<Visibility>>,
) {
(中略)
}
// クリック処理
fn input_system(
 mut game_data: ResMut<GameData>,
 mouse: Res<ButtonInput<MouseButton>>,
 windows: Query<&Window, With<PrimaryWindow>>,
 camera_q: Query<(&Camera, &GlobalTransform)>,
 mut item_q: Query<(&mut Visibility, &Transform, &ColorItem)>,
) {
 if !mouse.just_pressed(MouseButton::Left) {
   return;
 }
 let window = windows.single().unwrap();
 let (camera, camera_transform) = camera_q.single().unwrap();
 if let Some(world_pos) = window
   .cursor_position()
   .and_then(|c| camera.viewport_to_world_2d(camera_transform, c).ok())
 {
   for (mut vis, trans, item) in &mut item_q {
     if *vis == Visibility::Visible
       && trans.translation.truncate().distance(world_pos) < SPRITE_SIZE
     {
       *vis = Visibility::Hidden;
       match item.color_type {
         ColorType::Red => game_data.rgb_counts.x += 1.0,
         ColorType::Green => game_data.rgb_counts.y += 1.0,
         ColorType::Blue => game_data.rgb_counts.z += 1.0,
       }
       break;
     }
   }
 }
}
// 背景色セット
fn logic_system(
 game_data: Res<GameData>,
 mut clear_color: ResMut<ClearColor>,
 mut next_state: ResMut<NextState<GameState>>,
 item_q: Query<(&Transform, &Visibility), With<ColorItem>>,
 windows: Query<&Window, With<PrimaryWindow>>,
) {
 let win = windows.single().unwrap();
 let limit = Vec2::new(
   win.width() / 2.0 + BOUNDS_MARGIN,
   win.height() / 2.0 + BOUNDS_MARGIN,
 );
 let rgb = game_data.rgb_counts;
 let i = if rgb.x < rgb.y { if rgb.y < rgb.z { 1.0/rgb.z } else { 1.0/rgb.y } } else { if rgb.x < rgb.z { 1.0/rgb.z } else { 1.0/rgb.x } };
 let r = i * rgb.x;
 let g = i * rgb.y;
 let b = i * rgb.z;
 clear_color.0 = Color::srgb(r,g,b);
 if r == g && g == b { 
   next_state.set(GameState::StageClear);
   return;
 }
 let exists = item_q.iter().any(|(t, v)| {
   *v == Visibility::Visible &&
   t.translation.x.abs() < limit.x &&
   t.translation.y.abs() < limit.y
 });
 if !exists {
   next_state.set(GameState::GameOver);
 }
}

【サンプルコードの解説】
「main」関数で、ECSのSystemにlogic_system関数を追加します。
「start_stage」関数でランダムな背景色を「rgb_counts」プロパティに代入します。
「input_system」関数で、色ブロックがクリックされた時にRedならrgb_countsプロパティのxプロパティに、Greenならrgb_countsプロパティのyプロパティに、Blueならrgb_countsプロパティのzプロパティに背景色をインクリメントします。
logic_system関数で、1.0をrgb_countsプロパティの3色で一番大きい値の色の要素で除算した値を「i」変数に代入し、rgb_countsプロパティの3色の要素にi変数を乗算して背景色(clear_color.0)にセットします。赤緑青の3色が全て同じ値になれば真っ白なのでゲームステートを「StageClear」にセットします。全ての色ブロックが画面外に出たらゲームステートを「GameOver」にセットします。

ゲームオーバーとステージクリア

ゲームステートがGameOverならタイマーをリセットし、1.5秒後にタイトル画面に戻ります。ゲームステートがStageClearならタイマーをリセットし、1.5秒後に次のステージに進みます。今回はシンプルに解説するため、ゲームオーバー画像とステージクリア画像は読み込んだり表示したりしません。

・サンプルコード「main.rs」ファイル

(前略)
// メイン
fn main() {
 App::new()
(中略)
   // Result
   .add_systems(OnEnter(GameState::GameOver), setup_result_timer)
   .add_systems(OnEnter(GameState::StageClear), setup_result_timer)
   .add_systems(
     Update,
     result_wait_system.run_if(
       in_state(GameState::GameOver)
         .or(in_state(GameState::StageClear)),
     ),
   )
   .run();
}
// 初期化
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
(中略)
}
// タイトル
fn title_system(
 mouse: Res<ButtonInput<MouseButton>>,
 mut next_state: ResMut<NextState<GameState>>,
) {
(中略)
}
// タイトル画像表示
fn reset_for_title(
 mut title_q: Query<&mut Visibility, With<TitleVisual>>,
 mut item_q: Query<&mut Visibility, (With<ColorItem>, Without<TitleVisual>)>,
) {
(中略)
}
// Stage開始
fn start_stage(
 mut game_data: ResMut<GameData>,
 mut title_q: Query<&mut Visibility, With<TitleVisual>>,
 mut item_q: Query<(&mut Visibility, &mut Transform, &mut ColorItem), Without<TitleVisual>>,
) {
(中略)
}
// 色ブロック移動
fn movement_system(
 mut query: Query<(&mut Transform, &ColorItem), With<Visibility>>,
) {
(中略)
}
// クリック処理
fn input_system(
 mut game_data: ResMut<GameData>,
 mouse: Res<ButtonInput<MouseButton>>,
 windows: Query<&Window, With<PrimaryWindow>>,
 camera_q: Query<(&Camera, &GlobalTransform)>,
 mut item_q: Query<(&mut Visibility, &Transform, &ColorItem)>,
) {
(中略)
}
// 背景色セット
fn logic_system(
 game_data: Res<GameData>,
 mut clear_color: ResMut<ClearColor>,
 mut next_state: ResMut<NextState<GameState>>,
 item_q: Query<(&Transform, &Visibility), With<ColorItem>>,
 windows: Query<&Window, With<PrimaryWindow>>,
) {
(中略)
}
// 結果処理
fn setup_result_timer(mut game_data: ResMut<GameData>) {
 game_data.timer.reset();
}
// ゲームオーバーかステージクリアーを
fn result_wait_system(
 time: Res<Time>,
 mut game_data: ResMut<GameData>,
 current_state: Res<State<GameState>>,
 mut next_state: ResMut<NextState<GameState>>,
) {
 if game_data.timer.tick(time.delta()).just_finished() {
   match current_state.get() {
     GameState::GameOver => {
       game_data.stage = 2;
       next_state.set(GameState::Title);
     }
     GameState::StageClear => {
       game_data.stage = (game_data.stage + 1).min(15);
       next_state.set(GameState::MainLoop);
     }
     _ => {}
   }
 }
}

【サンプルコードの解説】
main関数では、GameOverとStageClearの入り口(OnEnter)で「setup_result_timer」関数を実行します。また、それらの更新(Update)ごとに「result_wait_system」関数を実行します。
setup_result_timer関数でタイマーをリセットします。
result_wait_system関数でゲームオーバーの1.5秒後にゲームステートをTitleにセットします。ステージクリアの1.5秒後にstageをインクリメントしゲームステートをMainLoopにセットします。

【コラム】本の手段

本は、文字、絵、写真がほとんどです。そこでそれ以外を考え出したいです。雑誌なら付録もその1つだし、音が鳴る絵本も、飛び出す絵本も、手触りのある印刷もあります。

おわりに

今回はプログラミング言語「Rust」の「Bevy」クレートを使って「Color」ゲームを作りました。今回は省略しましたが、例えばステージクリア画像やゲームオーバー画像も表示したりクリックで次に進めるようにしたりして改造してみてください。

この記事をシェアしてください

人気記事トップ10

人気記事ランキングをもっと見る

企画広告も役立つ情報バッチリ! Sponsored