Unityに関連する記事です

 ローグライト系のゲーム(Sly The Spire、ファントムローズなど)に見られる、プレイヤーが1マスずつ移動してゴール地点へ向かいながら、
各マスごとに分岐(選択肢)があり、任意のイベントを実行しながら進行するタイプのゲームシステムの実装例です。

 ここでは各マス目に複数の分岐処理がある形で1マスずつイベントを進行させていく方法を実装します。
最初のマスは分岐なし、次のマスは2つの分岐がある、といったようなローグライト系のゲームでよくみられるイベントシステムです。



1.設計

 
 前回につづき、合計4回の手順に分けて実装を行います。

〇1.UI 制作
〇2.イベント用のクラス制作
〇3.データベース制作
◇4.管理クラス制作

 今回は【4.管理クラス制作】を行います。

 管理クラスを作成し、これまでに制作してきたすべての情報をつなぎ合わせてゲームロジックを作ります。


2..サンプルコード  璽押璽爐離灰鵐肇蹇璽蕁璽ラスー

 
 プレイヤーとマス移動、イベント分岐を制御するスクリプトを作成します。


using System.Collections.Generic;
using UnityEngine;
using UniRx;
using UnityEngine.UI;

public class MainGameManager : MonoBehaviour
{
    [SerializeField] private Transform routeBasePrefab;
    [SerializeField] private Transform routeBaseSetTran;

    [SerializeField] private GameObject routeImagePrefab;
    [SerializeField] private Transform playerIcon;

    [SerializeField] private RouteCollection routeCollection;
    [SerializeField] private Transform eventButtonTran;
        
    private List<Transform> routeList = new();
    private List<EventBase> currentEventList = new();
    private int currentRouteIndex = 0; 
    

    void Start() {
        LoadRouteDatas();
        GenerateEventButtons();
    }

    /// <summary>
    /// ルート作成
    /// </summary>
    private void LoadRouteDatas() {
        for (int i = 0; i < routeCollection.routeList.Count; i++) {

            // ルート配置用のベース作成
            Transform ruteBase = Instantiate(routeBasePrefab, routeBaseSetTran, false);
            routeList.Add(ruteBase);

            int index = 0;
            GameObject ruteBrunch = null;
            
            // ルート内のイベント分だけマス目配置
            for (int eventCount = 0; eventCount < routeCollection.routeList[i].eventList.Count; eventCount++) {
                index = eventCount; ;
                ruteBrunch = Instantiate(routeImagePrefab, ruteBase, false);
            }
            
            // イベントが1つしかない場合にはマスの縦方向を広げる
            if (index == 0) {
                ruteBrunch.GetComponent<RectTransform>().sizeDelta = new(100, 240);
            }
        }
    }

    /// <summary>
    /// プレイヤーアイコンの配置位置の更新
    /// </summary>
    /// <param name="nextParentObj"></param>
    private void SetPlayerLocation(Transform nextParentObj) {
        playerIcon.SetParent(nextParentObj);
        playerIcon.localPosition = new(0, 50, 0);

        // TODO マスの色を変える。UI ごと差し替えなどの対応可能
        nextParentObj.GetComponent<Image>().color = Color.red;
    }

    /// <summary>
    /// ゲーム進行用のルート分岐ボタンの作成
    /// </summary>
    private void GenerateEventButtons() {
        
        // 次のイベント用のボタン生成
        for (int i = 0; i < routeCollection.routeList[currentRouteIndex].eventList.Count; i++) {

            int index = i;
            EventBase eventButton = Instantiate(routeCollection.routeList[currentRouteIndex].eventList[i], eventButtonTran, false);
            
            // ボタンのイベントを購読
            eventButton.OnClickEventButtonObserbable
                .ThrottleFirst(System.TimeSpan.FromSeconds(2.0f))
                .Subscribe(async _ =>
                {
                    // プレイヤーのアイコン位置設定(子オブジェクトにする)
                    SetPlayerLocation(routeList[currentRouteIndex].GetChild(index));
                    
                    await eventButton.ExecuteEvent();
                    
                    HandleEventCompletion(index);
                })
                .AddTo(this);

            currentEventList.Add(eventButton);
        }
    }

    /// <summary>
    /// イベント終了後の処理
    /// </summary>
    /// <param name="index"></param>
    private void HandleEventCompletion(int index) {
        DestroyEndEvents();

        currentRouteIndex++;
        
        CheckRoute();
    }

    /// <summary>
    /// ルートが残っているかチェック
    /// 残っていなければルートクリア
    /// 残っていれば次の分岐用のボタンの作成
    /// </summary>
    private void CheckRoute() {
        
        // ルートが残っていなければルートクリア
        if (routeCollection.routeList.Count <= currentRouteIndex) {
            Debug.Log("ルート終了");
            return;
        }
        
        // ルートが残っていれば次の分岐用のボタンの作成
        GenerateEventButtons();
    }
    
    /// <summary>
    /// 分岐用ボタンの削除
    /// </summary>
    private void DestroyEndEvents() {

        // イベント用のボタン削除
        for (int i = 0; i < currentEventList.Count; i++) {
            Destroy(currentEventList[i].gameObject);
        }
        
        // 前のイベントを削除
        currentEventList.Clear();
    }
}


<処理の説明>

クラスのフィールドとプロパティ

 routeBasePrefab, routeBaseSetTran, routeImagePrefab, playerIcon, routeCollection, eventButtonTran などは、
Unityのシリアライズされたフィールドで、インスペクターから設定できるようにしています。
これにより、ゲームオブジェクトやプレハブ、スクリプタブル・オブジェクトなどを設定できます。

 routeList は、RouteBase を格納するための Transform オブジェクトのリストです。
ルートは複数のイベントから構成されており、それらの管理をします。

 currentEventList は、現在のルート内で表示中のイベントボタンを格納するリストです。
こちらに登録しておくことで、後でイベントボタンを削除するために使用されます。

 currentRouteIndex は、現在のルートのインデックスを示す整数です。
ゲームの進行中に現在のルートを追跡するために使用されます。


Start メソッド
 
 LoadRouteDatas() メソッドを呼び出し、ルートデータの読み込みと RouteBase の作成を行います。
 GenerateEventButtons() メソッドを呼び出し、現在のルートに対応するイベントボタンの生成を行います。


LoadRouteDatas メソッド

 LoadRouteDatas メソッドは、ルートデータの読み込みと RouteBase の作成を行うメソッドです。

 routeCollection 内の各ルートデータに対して、以下の処理を繰り返します。
ルート用のベースオブジェクト RouteBase を Instantiate を使って作成し、routeBaseSetTran の子オブジェクトとして配置します。
これにより、ゲーム画面では水平方向にルートが並ぶようになります。

 次に、その RouteBase 内に各ルートのイベント分岐を表す Route オブジェクトを生成します。
イベントの数が1つの場合、Route オブジェクトの縦方向のサイズを変更し、長細い1マスにします。
イベントの数が2つの場合、垂直方向に並ぶ2マスにします。


SetPlayerLocation メソッド

 SetPlayerLocation メソッドは、プレイヤーアイコンの位置を更新するためのメソッドです。
特定のマス目にプレイヤーアイコンを移動し、適切な位置に設定します。
また、マスの色を変更することで、通過してきた分岐表現も可能としています。


GenerateEventButtons メソッド

 GenerateEventButtons メソッドは、現在のルートに対応するイベントボタンを生成し、ボタンのクリックイベントを購読するためのメソッドです。

 現在のルートに関連付けられた各イベントに対して以下の処理を繰り返します。
イベントボタンを Instantiate を使用して生成し、eventButtonTran の子オブジェクトとして配置します。
これにより、ゲーム画面にはイベント分岐選択用のボタンが水平方向に並びます。

 イベントボタンのクリックイベントを ThrottleFirst で重複防止制御し、ボタンのクリックを実行します。
ボタンのクリック時に、プレイヤーアイコンを適切な位置に設定し、ExecuteEvent メソッドを実行することで、各ボタンに紐づいたイベントを実行します。
生成したイベントボタンを currentEventList に追加します。こうすることにより、イベント終了後、イベントボタンを速やかに削除することが出来ます。


HandleEventCompletion メソッド

 HandleEventCompletion メソッドは、イベントの完了後に実行される処理を管理するメソッドです。

 表示されているイベントボタンを削除するための DestroyEndEvents メソッドを呼び出します。
現在のルートインデックスを更新します。

 次のルートが存在するかどうかを判定するための CheckRoute メソッドを呼び出します。


CheckRoute メソッド

 CheckRoute メソッドは、ゲーム内のルートが残っているかどうかを確認し、処理を分岐するメソッドです。

 ルートがすべて完了した場合、ゲーム内のすべてのルートが完了したかどうかを確認します。
もし完了している場合、デバッグログに "ルート終了" と表示し、ゲームを終了します。実際にはここに、ゲーム終了処理を実装しましょう。

 ルートが残っている場合、次の分岐用のボタンを生成するために GenerateEventButtons メソッドを呼び出します。
これにより、次のルートに進行し、新しいイベントボタンが生成されます。

 ここまでの処理の流れによって、再度、イベントボタンを生成する部分に処理が戻るため、1つのゲームサイクルが完成していることが分かります。


DestroyEndEvents メソッド

 DestroyEndEvents メソッドは、表示されているイベントボタンを削除するためのメソッドです。

 currentEventList 内の各イベントボタンを削除します。これにより、イベントボタンが画面上から削除されます。
前のイベントボタンがすべて削除された後、currentEventList をクリアします。

 これにより、新しいルートに移行すると、古いイベントボタンが削除され、新しいイベントボタンが生成される準備が整います。


<クリックイベントの購読>


 ボタンのクリックイベントは、各イベントボタン(EventBase クラス内)の OnClickEventButtonObserbable を使用して購読されます。
これにより、イベントボタンがクリックされたときに、プレイヤーアイコンの位置の設定、イベントの実行、およびイベント完了後の処理が実行されます。

 クリックイベントは ThrottleFirst を使用して制御され、連続したクリックを防ぎます。

 最終的に、AddTo(this) を使用してイベントの登録を管理し、this オブジェクト(MainGameManager)に対してリンクされます。
これにより、クラスの破棄(ゲームオブジェクトの破壊)と同時にクリックイベントの購読も終了します。



 ボタンの購読処理を EventBase クラス内ではなく、MainGameManager クラス内で行うアプローチは、
ボタンのクリックイベントが EventBase クラスに依存せず、ボタンのクリックに応じて異なる処理を実行するために柔軟性を提供する設計です。

 主要な利点として、次の点が挙げられます。


1.分離性

 ボタンのクリックに関連する処理を MainGameManager に集納することで、イベントベースのクラスはその役割に専念できます。
これにより、クラスの役割が明確に分かれ、コードの理解が容易になります。


2.柔軟性

 新しいイベントを追加する際、ボタンのクリック処理を変更する必要がある場合、これを MainGameManager 内で行えるため、
イベントに関連するすべての処理を中央で管理できます。これは、将来的にコードを拡張しやすくする重要な要素です。


3.テスト可能性

 ボタンクリック処理が MainGameManager 内に集納されているため、ユニットテストを実行しやすくなります。
異なるイベントのハンドリング・シナリオをテストするのが容易です。


<イベント処理の抽象化>


 イベントの抽象化は、このゲームの柔軟性と拡張性を向上させる重要な要素です。
これにより、異なる種類のイベントを簡単に追加し、それぞれのイベントが異なる振る舞いを持つことができます。


EventBase クラス

 EventBase クラスは抽象クラスで、実際のイベントに共通のプロパティとメソッドを提供します。
これには ExecuteEvent メソッドが含まれています。このメソッドは各サブクラスで実装されます。

 また、OnClickEventButtonObserbable という IObservable<Unit> プロパティが提供され、ボタンのクリックイベントが発生したときに通知を受けるために使用されます。
このプロパティにより、ボタンのクリックイベントを非常に効果的に購読できます。


各具体的なイベントクラス (例: SearchEvent, BattleEvent):

 各具体的なイベントクラスは EventBase クラスを継承し、共通のプロパティとメソッドを受け継ぎます。
しかし、各イベントクラスは異なる振る舞いを持つため、ExecuteEvent メソッドをオーバーライドして独自の実装を提供します。

 これにより、例えば「探索イベント」と「戦闘イベント」など、異なるタイプのイベントを簡単に追加できます。
各イベントは共通のクリックイベントを持ち、クリックが発生すると対応する ExecuteEvent メソッドが呼び出されます。

 このアプローチにより、新しいイベントを追加する際には、新しい具体的なイベントクラスを作成し、必要な振る舞いを実装するだけで済みます。
既存のコードを変更する必要はありません。これにより、ゲームの要素を効率的に拡張できます。

 また、await eventButton.ExecuteEvent(); の部分では、現在表示されているイベントボタンの種類に応じて、
対応する具体的なイベントクラスの ExecuteEvent メソッドが呼び出されます。従って、異なるイベントが異なる方法で処理されるため、柔軟性が向上します。

 このアーキテクチャにより、新しいイベントを追加し、既存のイベントを変更するために、コード全体を大幅に変更する必要がありません。
そのため、ゲームの開発と保守が簡単になり、異なる種類のイベントを効率的にサポートできます。



 以上の内容により、Unityでルート選択型ゲームを制御するためのメインのロジックが提供されます。
 

MainGameManager ゲームオブジェクトを作成し、MainGameManager スクリプトをアタッチして設定を行う


 ヒエラルキーの空いている場所で右クリックをしてメニューを開き、Create Empty を行って新しいゲームオブジェクトを作成します。
名前を MainGameManager に変更し、先ほど作成した MainGameManager スクリプトをアタッチします。

 インスペクターを確認し、各変数に必要な情報をアサインしてください。


インスペクター画像



 以上で完了です。



ゲームを実行して動作を確認する


 RouteCollection に登録したイベントの内容でルート情報が生成されるか確認してください。





 また最初に生成されるイベントのボタンも、RouteCollection に登録したイベントの内容になっているかを確認してください。





 スクリプタブル・オブジェクトに登録した情報通り正しく処理が動いている場合には、次の確認を行います。



 イベントのボタンを押し、Console ビューにログが表示されるか確認してください。
その後、次のイベントの分岐ボタンが作成されるか、それらも押すことで分岐しながら処理が進んでいくことを確認してください。

 一緒にプレイヤーのアイコンが移動し、分岐で選択したマスの色も変わります。


<実装動画>
動画ファイルへのリンク



 最後のイベントが終了したら、イベント終了のログが表示されます。
こちらも合わせて確認してください。






 すべて問題なく動作したら、スクリプタブル・オブジェクトに登録するイベントの情報を変更したり、ルート数を増やして
同じように動作するかを確認してみましょう。










 この処理により、ゲームの基本的なサイクルが完成し、その後は、MainGameManager のソースコードの修正は不要で、ルートの変更・追加が行えます。

 処理を抽象化し、スクリプタブル・オブジェクトなどを活用することで、ゲームロジックを作り出すことが出来ます。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます