課題11の参考資料)非同期処理を導入した GameDirector クラス

2025年3月26日

以下に、非同期処理(async/await) を導入し、SaveLoadManager クラスと連携させた GameDirector クラスの最新バージョンを示します。この実装により、大量のデータを扱う際や保存・読み込みに時間がかかる場合でも、メインスレッドの負荷を軽減し、フレームドロップを防ぐことができます。

更新ポイント

非同期メソッドの呼び出し:

  • ゲーム開始時に非同期でデータを読み込み。
  • ゲーム終了時に非同期でデータを保存。

コルーチンの使用:

  • UnityのMonoBehaviourメソッド(StartUpdate)は直接asyncにできないため、コルーチンを用いて非同期処理を管理。

エラーハンドリング:

  • 非同期処理中に発生する可能性のあるエラーを適切にキャッチし、ログに記録。

更新後の GameDirector クラス

using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using TMPro;
using UnityEngine;

public class GameDirector : MonoBehaviour
{
    public List<GenerationParameters> generationParametersList; // ScriptableObjectのリスト
    private int currentStage = 0;
    private float elapsedTime = 0.0f;

    private GameObject timerText;
    private GameObject pointText;
    private float time = 30.0f;
    public int point = 0; // 現在のスコア
    private int highScore = 0; // ハイスコア
    private GameObject generator;
    private bool isGameOver = false; // ゲーム終了フラグ

    // 例: ゲームオーバー画面のUIを表示のとき必要
    // public GameObject gameOverUI;

    void Start()
    {
        timerText = GameObject.Find("Time");
        pointText = GameObject.Find("Point");
        generator = GameObject.Find("ItemGenerator");

        // 非同期にゲームデータを読み込み
        StartCoroutine(LoadGameDataCoroutine());

        // 初期パラメータセットを適用
        ApplyCurrentGenerationParameters();
    }

    void Update()
    {
        if (isGameOver)
            return; // ゲーム終了後は処理を停止

        time -= Time.deltaTime;
        elapsedTime += Time.deltaTime;

        // パラメータセットの切り替え
        if (currentStage < generationParametersList.Count && elapsedTime > generationParametersList[currentStage].duration)
        {
            elapsedTime = 0.0f;  // elapsedTime をリセット

            ApplyCurrentGenerationParameters(); // currentStage が有効な範囲内でパラメータを適用

            currentStage++; // パラメータ適用後にインクリメント
        }

        // タイマー終了時の処理
        if (time < 0)
        {
            time = 0;
            generator.GetComponent<ItemGenerator>().SetParameter(10000.0f, 0, 0);
            EndGame(); // ゲーム終了処理を呼び出す
        }

        timerText.GetComponent<TextMeshProUGUI>().text = time.ToString("F1");
        UpdateScoreUI();
    }

    // ゲーム終了処理
    private void EndGame()
    {
        isGameOver = true; // フラグを立ててUpdateメソッドを停止
        Debug.Log("Game Over!");

        // ゲームオーバー画面を表示するなどの処理を追加
        // 例: ゲームオーバー画面のUIを表示
        // if(gameOverUI != null)
        // {
        //     gameOverUI.SetActive(true);
        // }

        // 非同期にゲームデータを保存
        StartCoroutine(SaveGameDataCoroutine());
    }

    // パラメータセットを適用するメソッド
    private void ApplyCurrentGenerationParameters()
    {
        if (currentStage >= generationParametersList.Count)
            return;

        GenerationParameters currentParams = generationParametersList[currentStage];
        generator.GetComponent<ItemGenerator>().SetParameter(currentParams.span, currentParams.speed, currentParams.ratio);
    }

    // スコア変更イベントハンドラ
    public void HandleScoreChange(object sender, ScoreEventArgs e)
    {
        point += e.ScoreChange;
        if (point > highScore)
        {
            highScore = point; // ハイスコアを更新
        }
        UpdateScoreUI();
    }

    // スコアUIの更新
    private void UpdateScoreUI()
    {
        pointText.GetComponent<TextMeshProUGUI>().text = $"{point} point (High Score: {highScore})";
    }

    // ゲームデータを非同期に保存するコルーチン
    private IEnumerator SaveGameDataCoroutine()
    {
        Task saveTask = SaveGameDataAsync();
        yield return new WaitUntil(() => saveTask.IsCompleted);

        if (saveTask.IsFaulted)
        {
            Debug.LogError("ゲームデータの保存に失敗しました。");
        }
        else
        {
            Debug.Log("ゲームデータの保存が完了しました。");
        }
    }

    // ゲームデータを非同期に保存するメソッド
    private async Task SaveGameDataAsync()
    {
        GameData data = new GameData
        {
            highScore = this.highScore
        };

        await SaveLoadManager.Instance.SaveGameDataAsync(data);
    }

    // ゲームデータを非同期に読み込むコルーチン
    private IEnumerator LoadGameDataCoroutine()
    {
        Task<GameData> loadTask = LoadGameDataAsync();
        yield return new WaitUntil(() => loadTask.IsCompleted);

        if (loadTask.IsFaulted)
        {
            Debug.LogError("ゲームデータの読み込みに失敗しました。");
        }
        else if (loadTask.Result != null)
        {
            this.highScore = loadTask.Result.highScore;
            // generationParametersList は読み込まない
        }

        UpdateScoreUI();
    }

    // ゲームデータを非同期に読み込むメソッド
    private async Task<GameData> LoadGameDataAsync()
    {
        return await SaveLoadManager.Instance.LoadGameDataAsync();
    }
}

コードの詳細説明

非同期メソッドの呼び出し:

  • Start() メソッド:
    • StartCoroutine(LoadGameDataCoroutine()); を呼び出し、ゲーム開始時に非同期でゲームデータを読み込みます。
  • EndGame() メソッド:
    • ゲーム終了時に StartCoroutine(SaveGameDataCoroutine()); を呼び出し、非同期でゲームデータを保存します。

コルーチンの実装:

  • LoadGameDataCoroutine():
    • LoadGameDataAsync() メソッドを非同期に実行し、その完了を待ちます。
    • 読み込みに失敗した場合や成功した場合にログを出力します。
    • 読み込まれたデータが存在する場合、highScore を更新します。
  • SaveGameDataCoroutine():
    • SaveGameDataAsync() メソッドを非同期に実行し、その完了を待ちます。
    • 保存に失敗した場合や成功した場合にログを出力します。

非同期メソッドの実装:

  • SaveGameDataAsync():
    • GameData オブジェクトを作成し、SaveLoadManager.Instance.SaveGameDataAsync(data) を呼び出して非同期に保存します。
  • LoadGameDataAsync():
    • SaveLoadManager.Instance.LoadGameDataAsync() を呼び出して非同期にデータを読み込み、結果を返します。

GameDirectorが非同期メソッド(SaveGameDataAsync、LoadGameDataAsync)を呼び出しているため、SaveLoadManager側にも非同期対応のラッパーメソッドを実装する必要があります。具体的な更新内容は以下の通りです。


更新内容

  1. 非同期メソッドの追加
    既存の同期メソッド(SaveGameData、LoadGameData)をラップする形で、非同期メソッドを追加します。たとえば、以下のように実装できます。
   using System.Threading.Tasks; // この名前空間を追加

   public async Task SaveGameDataAsync(GameData data)
   {
       SaveGameData(data);  // 同期メソッドを呼び出す
       await Task.CompletedTask; // 非同期メソッドとしてラップ
   }

   public async Task<GameData> LoadGameDataAsync()
   {
       GameData data = LoadGameData(); // 同期メソッドを呼び出す
       return await Task.FromResult(data); // 非同期メソッドとしてラップ
   }
  1. 名前空間のインポート
    非同期メソッドを利用するために、System.Threading.Tasksをインポートする必要があります。これにより、Taskを利用できるようになります。
  2. Unityのメインスレッド上での処理
    UnityのPlayerPrefsはメインスレッドでの実行が前提のため、上記のようなラッパーメソッドで非同期処理に対応する形が適切です。
    ※もし重い処理が必要な場合は別スレッドで処理する必要がありますが、PlayerPrefsの操作は軽量なためこの方法で問題ありません。

まとめ

  • GameDirector側は非同期でセーブ/ロードを行うため、SaveLoadManagerに非同期ラッパーメソッド(SaveGameDataAsyncとLoadGameDataAsync)が必要です。
  • SaveLoadManager側に上記の非同期メソッドを追加することで、GameDirectorの非同期呼び出しに対応できます。

この更新により、GameDirectorの非同期処理が正しく動作し、ゲームのセーブ/ロードがスムーズに実行されるようになります。

エラーハンドリング:

  • 各コルーチン内でタスクの完了を待機した後、タスクが失敗しているかどうかをチェックし、適切なログを出力します。
  1. UIの更新:
    • スコアの更新時やデータの読み込み後に UpdateScoreUI() を呼び出して、UIを最新の状態に保ちます。

注意点

  • Unity APIの制約:
    • Unityの多くのAPI(例:GameObject.FindGetComponentTextMeshProUGUI など)はメインスレッドでのみ安全に呼び出せます。そのため、非同期メソッド内で直接これらのAPIを呼び出さないようにし、必要な場合はコルーチンを通じてメインスレッドで実行します。
  • シングルトン SaveLoadManager の存在:
    • SaveLoadManager クラスがシングルトンとして正しく実装され、シーン内に存在することを確認してください。シーンに存在しない場合、SaveLoadManager.Instancenull になる可能性があります。
  • データの整合性:
    • 非同期処理中にデータの整合性が損なわれないよう、適切なロックや同期機構を導入することを検討してください。今回の実装では、highScore の更新はメインスレッド内で行われるため、基本的な整合性は保たれています。

実装の流れ

ゲーム開始時:

  • Start() メソッドで LoadGameDataCoroutine() を開始し、非同期でゲームデータを読み込みます。
  • 読み込みが完了すると、highScore が更新され、スコアUIが最新の状態に更新されます。

ゲーム終了時:

  • タイマーがゼロになった時点で EndGame() メソッドが呼び出され、SaveGameDataCoroutine() を開始します。
  • 非同期でゲームデータが保存され、保存が完了するとログが出力されます。

スコアの更新:

  • HandleScoreChange() メソッドがスコアの変更をハンドルし、必要に応じて highScore を更新します。
  • UpdateScoreUI() により、スコアがUIに反映されます。

参考資料


Unity

Posted by hidepon