Unityに関連する記事です

 プログラムの学習において、リファクタリングは大切な学習方法の1つです。

 リファクタリングとは主に、プログラムの動作や振る舞いを変えることなく、内部の設計や構造を見直し、コードを書き換えたり書き直したりすることを指します。
つまり、不具合を直すための修正ではなく、ゲーム画面の表現は変えることなく、ソースコードの見直しを行います。
読みやすい処理に直したり、最適化を図るべく、処理の効率化を検討したり、そういった内部的な処理の再構築の方法です。

 ソースコードを書くことに慣れてきたら、クラス内の処理を見直して、読みやすく、効率のよい処理に置き換えていく方法を学習し、実践しましょう。



<新しい学習内容>
 ・UniRx
 ・UniRx.Triggers
 ・ObsevableTriggers



ケーススタディ


 UniRx には ObsevableTriggers という機能が用意されています。
Unity のイベント関数(Update、OnTrigger〜、など)を Obsevable として扱うことができる機能群です。

 以前に作成した CharaManager クラスの Update メソッドを
こちらの ObsevableTriggers の機能を利用してリファクタリングを行います。

 リファクタリングを通じて、ソースコードを書くスキルのレベルアップを図りましょう。


リファクタリング前のクラス


 今回は CharaManager を例に利用します。


CharaManager.cs

<= クリックしたら開きます。




 プレイヤー役のゲームオブジェクトの構成も変わりありません。

 次の通りです。


構成



親クラスのインスペクター画像(CharaController クラスとコライダー、Rigidbody が一緒にアタッチされています)



子クラスのインスペクター画像(画像と Animator コンポーネントがアタッチされています)




リファクタリングの方向性


 UniRx の機能を活用することで、処理の動きを監視(オブザーバー)し、イベント駆動型の処理に変更できます。

 例えばボタンであれば、「ボタンを押す」という行為を監視し、この状態が発生したら指定した処理が実行されるように紐づけておくような処理が構築できます。

 今回は ReactiveProperty の機能と、ObsevableTriggers の機能を利用して、従来の処理をイベント駆動型の処理にリファクタリングしていきます。


設計 ー役割の考え方ー


 CharaController クラス内の処理を機能別に分けてみましょう。
下記のような仕分けによる分類ができると思います。

・移動させる機能
・アニメーションを移動方向に同期させる機能
・攻撃する機能
・Hp を管理する機能
・レベルと Exp を管理する機能

 このうち、移動させる処理とアニメーションを移動方向に同期させる処理については、キー入力の値を同じ情報として利用しています。
そのため共通する処理をまとめたクラスを1つ作成し、また、このクラスで、それ以外のクラスを管理します。
つまり、管理者(マネージャー)となるクラスを新しく用意し、他の機能をまとめ上げるようにします。


<新しく作る>
・移動させる機能を持つクラス
・アニメーションを移動方向に同期させる機能を持つクラス
・攻撃する機能を持つクラス
・マネージャークラス

 今回は Hp を管理する機能とレベルと Exp を管理する機能についてはクラスは作成しません。
この学習を通じて機能の分割方法を考えて、自作してみてください。


インターフェースを修正する


 IChara インターフェース内の SetUpChara メソッドに引数を追加します。


<= クリックしたら開きます。



インターフェースを実装したクラスを順番に修正する


 各クラスごとに明確な役割を設けるとともに、責務のない処理は書き込まないように配慮します。

 また CharaManager が、分割したすべてのクラスの管理者となります。
そのため、各クラス同士はお互いを知らない状態(メンバ変数で管理していない。疎結合化)を目的にもしています。

 キャラの向いている方向の情報に関しては、複数のクラスで利用するため、そのような情報は CharaManager クラス内に管理します。
各クラス内で別々に方向の情報は取得せず、必要なクラス(CharaAnime と CharaAttack)に、メソッドの引数を通じて送り届ける形で実装しています。


1.CharaMove.cs


 移動に関する機能を役割として持つクラスです。
CharaController クラスにあった、移動に関するメンバ変数とメソッドをこちらに移しています。

 インターフェースに定義されているメソッドである SetUpChara メソッドを実装し、このクラス独自の振る舞いを行っています。

 移動の処理は画面をクリック(タップ)している間、その方向に向かって移動するタイプの移動方法です。
CharaManager の Update メソッド内でクリックの入力を受け付け、Move メソッドの引数に移動方向の情報をもらうことで、移動を実行します。


<= クリックしたら開きます。




構成



管理用フォルダ用のオブジェクト



左下に配置するオブジェクト



右上に配置するオブジェクト



 他のゲームにも応用できます。


2.CharaAnime.cs


 アニメに関する機能を役割として持つクラスです。
CharaController クラスにあった、アニメに関するメンバ変数とメソッドをこちらに移しています。
また、リテラル表記していた Animator の Parameter 値について、メンバ変数を追加して置き換えてります。

 このクラスでは移動方向に合わせたアニメーションの同期処理を実装しています。
先ほどの CharaMove クラスと同様に、インターフェースを実装し、定義されている SetUpChara メソッド内にて Animator クラスの取得を行っています。

 先ほどの CharaMove クラスの SetUpChara メソッドでは Debug.Log メソッド以外には特に何も処理は書き込んでいませんでした。
このように、同じメソッドであっても実装されている(書かれている)内容が異なっていることが分かります。

 なお、SetUpChara メソッドで実行している Animator コンポーネントを取得する命令は
子オブジェクトに Animator コンポーネントがアタッチされている場合を想定しています。
子オブジェクトがない構成の場合には、SetUpChara メソッドの処理を適宜修正して、コンポーネントの取得先を変更してください。


<= クリックしたら開きます。




 if / else 文による分岐処理のうち、処理内部で同じ変数に対して代入処理を行う場合には、三項演算子を利用して記述することが出来ます。
本来の if /else 文と比較しながら構文を読み解いてみましょう。


参考記事
@crazy_traveler様
参考になる三項演算子


3.CharaAttack.cs


 攻撃に関する機能を役割として持つクラスです。
CharaController クラスにあった、攻撃に関するメンバ変数とメソッドをこちらに移しています。
その際にメソッド名を変更したり、コルーチンメソッドを直接実行しないために仲介役のメソッドを追加しています。

 ポイントは、プレイヤーの最新の方向の情報を CharaManager から参照するためのプロパティを用意している部分です。
C# のプロパティは、値型であっても参照型であっても、外部の変数や値を保持するのではなく、ゲッターやセッターのメソッドを通じて外部との値のやり取りを行います
そのため、プロパティを通じて値を設定すると、内部での値のコピーが行われ、その後も外部変数が更新されるとプロパティのセッターが呼び出されて値が更新されます。
この仕組みにより、最新の情報をプロパティを通じて参照できるようになります。

 今回であれば、CharaManager にある direction 変数は Vector2 型であり、値型です。
その情報を参照するために用意するプロパティの型も Vector3 で値型ですが、プロパティを介して値を設定する事で、最新の参照が可能になっています。

 この機能により、CharaManager の情報を元に自動的に最新のプレイヤーの方向の情報を更新できる仕組みを導入しています。

 また新しい機能として、攻撃を一時停止/再開するための機能を追加します。


<= クリックしたら開きます。




 CharaManager クラスに追加する ReactiveProperty である Direction 変数を購読し、
値が更新されるたびに AttackDirection の値を更新するようにします。

 この処理は CharaManager で用意することもできますが、この CharaAttack クラスは複数の攻撃処理を行う場合には利用しない予定です。
そのため、CharaManager に処理を書いてしまうと、CharaAttack クラスを利用しない場合において不便な点が生じます。
(例えば使っていない変数をコメントアウトなどすると、その変数を使っている部分でエラーが生じます。)


<このような実装もできるが、CharaManager での実装になってしまう>
using UniRx;
using UnityEngine;

public class CharaAttack : MonoBehaviour, IChara
{
    private Vector2 attackDirection;

    // CharaManager からの方向情報を購読するプロパティ
    public Vector2 AttackDirection => attackDirection;
}

public class CharaManager : MonoBehaviour
{
    private CharaAttack charaAttack;
    private ReactiveProperty<Vector2> Direction = new ();

    private void Start()
    {
        if (TryGetComponent(out charaAttack))
        {
            // CharaAttack 側のプロパティを直接更新
            Direction.Subscribe(diruection => charaAttack.AttackDirection = direction);
        }
    }

    // CharaManager の方向情報を更新するメソッド
    public void UpdateDirection(Vector2 newDirection)
    {
        // 方向情報を設定
        Direction.Value = newDirection;
    }
}

 CharaAttack クラス内の処理を CharaManager クラスと切り離して独立性を保つため、 CharaAttack クラス内での購読処理を実装しています。


4.CharaManager.cs


 上記の3つのクラスをまとめて管理するクラスです。
このクラスには RequireComponent 属性を付与し、該当する3つのクラスを指定していますので、
このクラスをアタッチすると、先ほどの3つのクラスも自動的に追加でアタッチされます
アタッチの手間が省けるとともに、アタッチ忘れを防止することも出来ます。

 このクラスのみが3つのクラスをメンバ変数として管理し、それぞれの処理に対して命令を出しています。
トップダウン型のような命令系統の仕組みをイメージしてもらえれば分かりやすいでしょう。

 また共通の処理としてキャラの方向の情報を管理し、その情報をアニメーションのクラスと攻撃のクラスに対して、メソッドを通じて渡しています。
こうすることにより、各クラス内で判定をさせる必要なく、共通の値をそれぞれのクラスに利用してもらう設計です。

 ただし、処理のタイミングの問題で、このメソッドの引数で渡す方法では、方向の情報を渡せないクラス(CharaAttack)もあります。
これは攻撃処理のタイミングが CharaManager ではなく、CharaAttack に依存しているため、常に最新の値を渡しておく必要があります。

 最も簡単な方向情報の渡し方は、CharaManager クラスの Update メソッドで CharaAttack 内に用意してある方向情報の値を随時更新するものです。
これであれば、いつ攻撃の処理をおこなっても最新の方向の情報を利用できます。

 今回はそのアプローチではなく、CharaAttack クラスにプロパティを用意し、その参照先を CharaManager クラスの方向情報と紐づけます。
プロパティを通じて値を設定すると、内部での値のコピーが行われ、その後も外部変数が更新されるとプロパティのセッターが呼び出されて値が更新されます。
この仕組みにより、CharaAttack クラス側でも、最新の情報をプロパティを通じて参照できるようになります。

 なお今回は処理の確認のため、Start メソッドを使って処理をスタートさせていますが、
実際には外部クラスから SetUpCharaManager メソッドへ実行命令を受けて動いていく前提で設計しています。


<= クリックしたら開きます。



<リファクタリング ReactiveProperty への置き換え>


 今回、プロパティをリファクタリングし、ReactiveProperty に置き換えています。

<元の処理>
    // direction のプロパティ。内部で CharaAttack の AttackDirection プロパティも同時に更新している
    public Vector2 Direction {
        get => direction;
        set {
            direction = value;

            if (charaAttack) {
                charaAttack.AttackDirection = value;
            }
        }
    }


    /// <summary>
    /// 向きの更新
    /// </summary>
    /// <param name="newPos"></param>
    private void UpdateDirection(Vector2 newPos)
    {
        Direction = (newPos - (Vector2)transform.position).normalized;
    }

    ↓

<リファクタリング後の処理>
    public ReactiveProperty<Vector2> Direction = new();


    /// <summary>
    /// 向きの更新
    /// </summary>
    /// <param name="newPos"></param>
    private void UpdateDirection(Vector2 newPos)
    {
        // ReactiveProperty に置き換え
        //Direction = (newPos - (Vector2)transform.position).normalized;
        Direction.Value = (newPos - (Vector2)transform.position).normalized;
    }
 


<リファクタリング◆ UpdateAsObsevable への置き換えー>

 Update メソッド内の処理のうち、マウスによる移動処理とアニメ同期処理の部分をリファクタリングし、
UniRx の ObsevableTriggers に含まれている UpdateAsObsevable メソッドに置き換えています。

 UpdateAsObsevable メソッドは、MonoBehaviour クラスを継承している場合のみ利用できます。


<リファクタリング後の処理>
    void Update()
    {
    if (charaMove == null || charaAnime == null)
        {
            return;
        }
        
        if (Input.GetMouseButton(0))
        {
            Vector2 tapPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        
            charaMove.Move(tapPos);
        
            //向きの更新
            UpdateDirection(tapPos);
        
            //移動の向きとアニメの向きの同期
            //charaAnime.UpdateAnimation(Direction);
            charaAnime.UpdateAnimation(Direction.Value);
        }

    }
 

    ↓

<リファクタリング後の処理>
using UniRx;
using UniRx.Triggers;

    void Start()
    {
        // デバッグ用
        SetUpCharaManager();

        // 情報が足りているか判定
        if (charaMove == null || charaAnime == null)
        {
            Debug.Log($"処理に必要な情報が足りないので、処理を停止します。 CharaMove : {charaMove} / CharaAnime : {charaAnime}");
            return;
        }
        
        // Update メソッドをリファクタリングし、UpdateAsObservable() に置き換え
        this.UpdateAsObservable()
            .Where(_ => Input.GetMouseButton(0))
            .Select(_ => Camera.main.ScreenToWorldPoint(Input.mousePosition)) // 戻り値はワールド座標
            .Subscribe(tapPos =>  // tapPos は Select の戻り値
            {
                charaMove.Move(tapPos);

                // 向きの更新
                UpdateDirection(tapPos);

                // 移動の向きとアニメの向きの同期
                charaAnime.UpdateAnimation(Direction.Value);
            });
    }



 なお、MonoBehaviour クラスを継承していない場合には、Obsevable.EveryUpdate() メソッドを利用することで置き換え出来ます。
こちらの場合、OnCompleted メッセージを発行しないため、AddTo メソッドを付与して Dispose を行っています。


<MonoBehaviour クラスを継承していない場合に Update メソッドを Obsevable として扱う方法>
        // MonoBehaviour ない場合、UpdateAsObservable() の代わりに Observable.EveryUpdate() を利用する
        Observable.EveryUpdate()
            .Where(_ => charaMove != null && charaAnime != null)
            .Where(_ => Input.GetMouseButton(0))
            .Select(_ => Camera.main.ScreenToWorldPoint(Input.mousePosition))
            .Subscribe(tapPos =>
            {
                charaMove.Move(tapPos);
        
                // 向きの更新
                UpdateDirection(tapPos);
        
                // 移動の向きとアニメの向きの同期
                charaAnime.UpdateAnimation(Direction.Value);
            })
            .AddTo(this); // サブスクリプションを解除


クラス図


 クラスの関係性に変更はありません。各クラスの関連性を見てみましょう。



<インターフェースとクラスの関係性>



<管理クラスと他のクラスの関係性>



 3つのクラスがインターフェースを実装しているため、すべてのクラスに SetUpChara メソッドが実装されています。
同名のメソッドですが、実際にはそれぞれのクラス内に書いてある処理が実行されますので、振る舞いが変わります。

 また CharaManager クラスは他の Chara 〜 クラスを知っていますが、他のクラス同士にはつながりがありません。
よって、クラス間の関係性が疎結合化でき、クラス内部を修正する際に、他のクラスのことを気にせずに修正することが出来ます。

 このように、インターフェースを利用することで、同名のメソッドを使うが、その内容までは関知しない、という形で処理の抽象化ができ、
加えて、同名のメソッドでありながら、異なる処理を実行していく(振る舞いを変える)という、多態性の概念も一緒に利用しています。

 オブジェクト指向型プログラムは実際に使いながら学習していくと、概念や機能の便利さなどが見えてきますので、
なるべく処理を書いて動かしながら学習をしていくとよいでしょう。


 
 クラス図を書くことにより、自分のイメージを整理する際にも役立ちますし、第三者にクラスの状態を説明する場合にも情報を正確に伝えることが出来ます。


プレイヤー役のゲームオブジェクトに各クラスをアタッチし、設定を行う


 まずプレイヤー役のゲームオブジェクトを複製します。複製後、1つは非表示にします。
カメラなどで追従対象になっている場合には、カメラの設定先も見直してアサインし直してください

 以前の状態のゲームオブジェクトを残しておくことで、インスペクターでの設定値の確認なども出来るためです。



 複製された方のゲームオブジェクトにある CharaController のスクリプトを Remove し、代わりに CharaManager クラスをアタッチしてください。
一緒に3つのクラスも自動的にアタッチされます。コライダーと Rigidbody はそのままで問題ありません。
また、BulletGenerator クラスがある場合には、それもアタッチしてください。

 CharaMove クラスには移動速度と移動範囲の設定、CharaAttack には攻撃の間隔の設定がありますので、それぞれ設定を行ってください。


インスペクター画像



 以上で設定は完了です。



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


 ゲームを実行し、リファクタリングを行う前と同じように動作するかを検証してください。


<確認動画>
動画ファイルへのリンク


設計、およびリファクタリングの重要性


 ソースコードを書く前に、設計を考えることから始めてみてください。
以前よりもたくさんの時間をクラスの設計に費やしてみることをお勧めします。
どのような処理を、どのクラスに書くのかを明確にしておくことで、肥大化しやすいクラスを前もって認知しておくこともできます。

 ソースコードを書いている間も、常に、設計を頭に考えて進めていくようにしてみてください。
設計は書き出しておくことで考えていることを言語化・可視化できるので、よりイメージしやすくなります。

 また書いて終わりにはせず、リファクタリングを行うことも念頭に置いておくと、良質なコーディングが行えるようになっていきます。

コメントをかく


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

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

Menu



技術/知識(実装例)

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

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

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

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

レースゲーム(抜粋)

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

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

3D脱出ゲーム(抜粋)

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

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

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

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

VideoPlayer イベント連動の実装例

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

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

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

private



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

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