i-school - カードゲーム 手順4
 以下の内容で順番に実装を進めていきます。

手順4 ープレイヤーの作成ー
 6.Character クラスを作成する
 7.GameData クラスを作成する



 新しい学習内容は、以下の通りです。

 ・UniRx のインポート
 ・namespace の機能
 ・MonoBehaviour を継承しないクラス
 ・UniRx の活用 ーReactivePropertyー
 ・シングルトンクラス
 ・ラムダ式を利用した、戻り値のあるメソッドの省略記法



6.Character クラスを作成する

1.設計


  プレイヤーとエネミーを制御するためのクラスです。
1つのクラスを両者に利用して管理します。

 このクラスには MonoBehavour クラスは継承しません
そのため、ゲームオブジェクトに紐づかないクラスを利用したアプローチ方法での設計になります。


2.UniRx をインポートする


 Unity のアセットストアからインポートお願いします。

UniRx - Reactive Extensions for Unity
https://assetstore.unity.com/packages/tools/integr...

 ここでは UniRx 自体の解説は行いません。


3.OwnerStatus スクリプトを作成する


 バトル時におけるカードを配置する位置のオーナーや、キャラクターの所有者を設定する際に利用します。
ここでは1つのスクリプトとして作成していますが、次の Character クラス内に記述しても問題ありません。
その場合、入れ子型にはせず、1つの独立した enum として作成しておくとよいでしょう。


OwnerStatus.cs

<= クリックすると開きます。



4.Character クラスを作成する


 System.Serializable 属性をクラスに付与することで、インスペクターにクラス内部が表示されるようになります。


 <= クリックすると開きます



5.<namespace の機能>


 namespaceは、C#でプログラミングする際にコードの整理や構造化を助けるための仕組みです。
Unityプロジェクトでは、複数のスクリプトやクラスが存在する場合があります。これらを適切に整理し、名前の衝突を避けるためにnamespaceを使用します。

 namespaceを使用することで、クラスや関数がどのグループに属しているかを明示的に指定できます。
これにより、異なる場所で同じ名前のクラスや関数を定義しても、それぞれが異なるnamespaceに属している限り、名前の衝突を回避できます。

 例えば、アセットストアからインポートしたアセット内のスクリプトのファイル名に「GameManager」という名前があったとき、
namespace の機能を利用していれば、同名の GameManager を複数作成することが出来ます。

 このように namespaceを使うことで、異なるスクリプトでも同じ名前のクラスや関数を定義でき、それぞれが独立して動作します。

 また大規模なプロジェクトでは、数百ものクラスや関数が存在することがあります。
これらをnamespaceごとにまとめることで、コードの整理がしやすくなります。

 例えば、UIに関するクラスは「UI」namespaceにまとめ、ゲームロジックに関するクラスは「Gameplay」namespaceにまとめるなど、機能ごとに整理することができます。
ピリオドを利用することで、「MyGame.UI」や「MyGame.GamePlay」のように書くことも可能です。

 作成した namespace は using で宣言することが出来ます。
いつもスクリプトの上部に書いているものですね。



 C#でのnamespaceの宣言は非常に簡単です。
通常、ファイルの先頭にnamespaceを宣言します。

 以下は基本的な構文です。

namespace Mynamespace {

    // ここにクラスや関数を定義する

}
 
 例えば、自分のゲームに関連するクラスや関数をまとめるために、次のようにnamespaceを宣言できます。


<使用例>

namespace MyGame {

    public class Player {
        // プレイヤーのプロパティやメソッドを定義
    }

    public class Enemy {
        // 敵のプロパティやメソッドを定義
    }
}

 上記の例では、PlayerクラスとEnemyクラスは共にMyGameというnamespaceに属しています。
これにより、他のnamespace内で同じ名前のクラスが存在しても問題ありません。
 
 作成した namespace は他のクラス内の using で宣言できます。


6.<MonoBehaviour を継承しないクラス>


 MonoBehaviour クラスを継承しないクラスとして作成することで、 ゲームオブジェクトに紐づかない、データ管理のみのクラスとして制作しておきます。

 MonoBehaviourの継承がありませんので、そのクラスはゲームオブジェクトにアタッチすることはできませんし、Instantiate メソッドで生成出来ません。
代わりに、いずれのスクリプトからでもインスタンス(new)して利用することが出来ます。
その場合、コンストラクタメソッドを用意しておくことで初期値の代入処理も可能です。


7.<UniRx の活用 ーReactivePropertyー>


 下記の処理において、UniRx の ReactiveProperty が利用されています。

    public ReactiveProperty<int> Hp = new();
    public ReactiveProperty<int> Shield = new();
    public ReactiveProperty<int> AttackPower = new();


    /// <summary>
    /// Hp 更新
    /// </summary>
    /// <param name="amount"></param>
    public virtual void UpdateHp(int amount) {
        hp = Mathf.Clamp(hp += amount, 0, maxHp);
        Debug.Log($" {status} : {GetHp}");

        Hp.Value = Mathf.Clamp(Hp.Value += amount, 0, maxHp);
        Debug.Log($" {status} : {Hp.Value}");
    }

    /// <summary>
    /// シールド値更新
    /// </summary>
    /// <param name="amount"></param>
    public void UpdateShield(int amount) {
        Shield.Value += amount;
        Debug.Log($" {status} : {Shield.Value}");
    }

 ReactiveProperty はプロパティの扱いとなりますが、シリアライズできますので、インスペクター表示可能です。
Value を利用して値にアクセスします。

 値が更新されると、Subscribe されている場合に、値に紐づいて他の処理を連動させることが可能です。
(この時点ではまだ Subscribe はしていません)


7.GameData クラスを作成する

1.設計


 ゲーム内のデータを保持するクラスです。
プレイヤーの情報や敵の情報、ステージの情報などを管理するためのクラスを作成し、一元管理するようにします。
 
 このクラスは多くのクラスから参照されることが多くなる前提ですので、シングルトン・デザインパターンを利用した設計を行います。

 特にシングルトンクラスの場合、シーン遷移時にも破棄されないゲームオブジェクトとすることで
どのクラスからでも変数やメソッドにアクセスでき、シーン遷移後も利用することができるクラスとして成立します。


2.GameData クラスを作成する


 折角ですので、こちらでも namespace も利用してクラスを作成してみましょう。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Phantom
{
    public class GameData : MonoBehaviour
    {
        public static GameData instance;

        private Character player;
        private Character opponent;


        void Awake() {
            if (instance == null) {
                instance = this;
                DontDestroyOnLoad(gameObject);
            }
            else {
                Destroy(gameObject);
            }

      // デバッグ
      InitCharacter(OwnerStatus.Player, 10);
        }

        /// <summary>
        /// キャラクラーの生成
        /// プレイヤーと対戦相手共用
        /// </summary>
        /// <param name="status"></param>
        /// <param name="hp"></param>
        public void InitCharacter(OwnerStatus status, int hp) {
            if (status == OwnerStatus.Player) {
                // プレイヤーのインスタンスを生成し、HPを設定
                player = new (hp, status);
                Debug.Log($"プレイヤー生成 /  Hp : {hp}")
            }else{
                // 対戦相手のインスタンスを生成し、HPを設定
                opponent = new (hp, status);
                Debug.Log($"対戦相手生成 /  Hp : {hp}")
            }
        }

        public Character GetPlayer() => player;
        
        public Character GetOpponent() => opponent;
    }
}

 先ほど作成した Character クラスを利用し、プレイヤーと敵の両方を同名のクラスで管理します。
それぞれが異なるインスタンスを持つため、同じクラスであっても、異なる Hp を管理できます。


3.<シングルトンクラス>


 シングルトンとは、数多くあるデザインパターンの1つです。そのクラスのインスタンスが必ず1つであることを保証するデザインパターンのことを言います。

 GameData クラスでは、このシングルトンを採用しています。つまり、ゲーム中を通じて、このGameData クラスが1つしか存在できないようになります。
実装例は複数ありますが、一番読みやすい方式で記述しています。

 このシングルトンによってインスタンスが1つか生成されないことが保証されますので、このGameData クラスへの参照は、いずれのクラスからであっても変数を介さずに参照を行えるようになります。



 例えば、Enemyというクラスがあり、そのEmenyクラスを持つゲームオブジェクトが5つあった場合、「どの」Enemyクラスであるかを確定できないと、対象となるEnemyクラスへは参照できません。
そのため、Enemy型の変数を用意して、その変数へ参照したいEnemyクラスを代入することによって、はじめてEnemyクラスの情報を扱うことができるようになります。

 ですがシングルトンであるGameData クラスの場合には、このインスタンスは常に1つしかないことが保証されていますので、「どの」という指定の部分が不要になります。
つまり変数への代入が不要になります。

 GameData という指定はすなわち、自動的にただ1つの GameData への参照が行われます。


4.<ラムダ式を利用した、戻り値のあるメソッドの省略記法>


 戻り値のあるメソッド内の処理が1行だけの場合、ラムダ式を利用することで省略記法により記述できます。


<通常の戻り値のあるメソッド>
    /// <summary>
    /// プレイヤーの取得
    /// </summary>
    /// <returns></returns>
    public Character GetPlayer() {
        return player;
    }

    /// <summary>
    /// 対戦相手(エネミー)の取得
    /// </summary>
    /// <returns></returns>
    public Character GetOpponent() {
        return opponent;
    }

   ↓

<ラムダ式を利用した、戻り値のあるメソッドの省略記法>
    public Character GetPlayer() => player;

    public Character GetOpponent() => opponent;

 ラムダ式の書式に則り、引数 => 処理の書式で記述されています。

 戻り値のない void メソッドでは利用できません。


5.GameData ゲームオブジェクトを作成し、GameData クラスをアタッチする


 Create Empty で新しいゲームオブジェクトを作成し、GameData に名前を変えます。

 作成した GameData クラスをアタッチします。


インスペクター画像




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


 どのように処理が動いて、どのようになればいいのか、イメージを作ってから確認していきましょう。

 ゲームを実行し、GameData クラスがヒエラルキー内の DontDestroyOnLoad シーンに移動することを確認してください。



 以上でこの手順は終了です。

 次は 手順5 −−? です。