Unityに関連する記事です

 リファクタリングを行って、Dictionary や List を活用し、抽象化の状態を保ちつつ、処理の効率化を図ります。



リファクタリングの元になるクラス


 このクラス自体は別の記事でクラスを分割して作成したクラスになります。

   => 肥大化したクラス内の処理を、役割に応じたクラスを複数作成して分割する実装例



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



効率化する処理


 SetUpCharaManager メソッド内の処理と、Update メソッド内の処理について見直します。

    public void SetUpCharaManager() {
        
        // このゲームオブジェクトにアタッチされている IChara インターフェースを実装しているすべてのクラスを取得して配列に代入
        IChara[] charas = GetComponents<IChara>();
        
        // 各クラスの初期設定(インターフェースの情報でクラスを取得してあるので、クラス名がそれぞれ違っていても foreach でまとめて処理できる)
        foreach (var chara in charas) {
            chara.SetUpChara();
        }
        
        // 各クラスの取得
        TryGetComponent(out charaMove);
        TryGetComponent(out charaAnime);
                
        if(TryGetComponent(out charaAttack))
        {
            // 攻撃準備
            charaAttack.PrepareAttackRepeatedly();
        }
    }

 最初に GetComponents メソッドを利用して IChara インターフェースを実装しているクラスを取得していますが、
その後、再度 TryGetComponent メソッドの処理があります。

 そのため、いわば同じクラスに対して2回ずつ取得の命令が実行されている状態になっています。
合わせて、メンバ変数もそれぞれ用意されているため、新しいクラスを追加した際にも追加の変数が必要になります。



 Update メソッド内には適切なタイミングで null チェックが行われていますが、
ネストが浅くなって読みやすくなっている反面、null チェックにより、処理が長くなっています。

    void Update()
    {
        // null チェック
        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);
        }

        // CharaAttack に攻撃用の向きの情報を提供
        if (charaAttack != null) {
            charaAttack.UpdateAttackDirection(direction);
        }

        // 攻撃の一時停止と再開
        if (Input.GetMouseButtonDown(1))
        {
            // 攻撃の一時停止・再開を切り替える
            charaAttack.ToggleAttack();
        }
    }

 これらの部分に注目して、それぞれの機能を活用して効率化を図ります。

 なお、UpdateDirection メソッドは変更ありません。


Dictionary を活用したケース


 クラス用の3つのメンバ変数を削除し、各クラスの取得処理も1回ずつにします。

 クラスの情報は Dictionary 内に格納されていますが、「どれ」が「どの」要素として存在しているかは不明であるため、
実行命令をおこなう際にクラスを指定して処理を行えるように新しく GetChara メソッドを追加しています。


using System.Collections.Generic;  // ← Dictionary を利用するために使います
using UnityEngine;
using System;  // ← Type を利用するために追加します

/// <summary>
/// キャラの管理クラス
/// </summary>
[RequireComponent(typeof(CharaMove))]
[RequireComponent(typeof(CharaAnime))]
[RequireComponent(typeof(CharaAttack))]
public class CharaManager : MonoBehaviour
{
    // private CharaMove charaMove;
    // private CharaAnime charaAnime;
    // private CharaAttack charaAttack;
    
    private Vector2 direction;              //キャラが向いている方向
    public Vector2 Direction => direction;  // get のみのプロパティ

    private Dictionary<Type, IChara> charaDictionary = new();


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

    /// <summary>
    /// 各クラスの初期設定。外部クラスから実行する前提
    /// </summary>
    public void SetUpCharaManager() {

        // // 通常の場合。GetComponents と TryGetComponent があるので、同じクラスを実質2回取得している
        // IChara[] charas = GetComponents<IChara>();
        //
        // // 各子クラスの初期設定(インターフェースで取得してあるので、子クラスが違っても foreach でまとめて処理できる)
        // foreach (var chara in charas) {
        //     chara.SetUpChara();
        // }
        //
        // // 各子クラスの取得
        // TryGetComponent(out charaMove);
        //
        // TryGetComponent(out charaAnime);
        //
        //
        // if(TryGetComponent(out charaAttack))
        // {
        //     // 攻撃準備
        //     charaAttack.PrepareAttackRepeatedly();
        // }
        
        // Dictionary の場合、上記の2重取得の問題を解消できる
        IChara[] charas = GetComponents<IChara>();
        
        // 各クラスの初期設定(インターフェースで取得してあるので、クラスが違っても foreach でまとめて処理できる)
        foreach (var chara in charas) {
            charaDictionary[chara.GetType()] = chara;
            chara.SetUpChara();
        }

        GetChara<CharaAttack>()?.PrepareAttackRepeatedly();
  }

    /// <summary>
    /// Dictionary にある指定したクラスを取得
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    private T GetChara<T>() where T : class, IChara   // where に class を指定しないと参照型が保証できないので null が返せない
    {
        if (charaDictionary.TryGetValue(typeof(T), out IChara chara))
        {
            return (T)chara;
        }
        return null;  // 戻り値の型は T? のように null 許容にしなくても問題ない
    }

    void Update()
    {
        // null チェック
        // if (charaMove == null || charaAnime == null) {
        //     return;
        // }

        // タップ入力
        if (Input.GetMouseButton(0)) {
            
            // タップした位置からワールド座標取得
            Vector2 tapPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            
            // 移動とアニメ同期
            //charaMove.Move(tapPos);
            GetChara<CharaMove>()?.Move(tapPos);
            
            // 向きの更新
            UpdateDirection(tapPos);
            
            // 移動向きとアニメの向きの同期
            //charaAnime.UpdateAnimation(direction);
            GetChara<CharaAnime>()?.UpdateAnimation(direction);
        }

    // CharaAttack に攻撃用の向きの情報を提供
        // if (charaAttack != null) {
        //     charaAttack.UpdateAttackDirection(direction);
        // }
        
        GetChara<CharaAttack>()?.UpdateAttackDirection(direction);
                
        // 攻撃の一時停止と再開
        if (Input.GetMouseButtonDown(1))
        {
            // 攻撃の一時停止・再開を切り替える
            //charaAttack.ToggleAttack();
            GetChara<CharaAttack>()?.ToggleAttack();
        }
    }

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



 処理の実行命令が、変数経由ではなく、List 内の所定のクラスを指定して実行する形式に変更になっています。


  GetChara<CharaAttack>()?.PrepareAttackRepeatedly();

 この命令により、下記のメソッドが実行されます。

    private T GetChara<T>() where T : class, IChara
    {
        if (charaDictionary.TryGetValue(typeof(T), out IChara chara))
        {
            return (T)chara;
        }
        return null;
    }

 ジェネリックである T の部分には、実行命令時に GetChara<T> で指定したクラスが入りますので、
今回であれば、T は下記のようにすべて CharaAttack で処理されます。

<ジェネリックである T 部分を置き換える>
    private CharaAttack GetChara<CharaAttack>() where CharaAttack : class, IChara
    {
        if (charaDictionary.TryGetValue(typeof(CharaAttack), out IChara chara))
        {
            return (CharaAttack)chara;
        }
        return null;
    }

 まず T の値である CharaAttack クラスが、where によってクラスか、IChara インターフェースのいずれかであるかチェックされます。
今回はどちらも問題ありませんので、続けてメソッド内の処理が実行されます。

 Dictionary に用意されている TryGetValue メソッドを実行し、Dictionary 内から CharaAttack クラスが取得できるか判定します。
取得できた場合には out キーワードの右辺に用意されている chara 変数に情報が代入されます。
ただしこのとき取得した値は IChara 型であるため、return のタイミングで CharaAttack クラスにキャストして戻します。

 もしも Dictionary 内に CharaAttack クラスが見つからない場合には null を戻します。

  GetChara<CharaAttack>()?.PrepareAttackRepeatedly();

 この処理では、メソッドの最後に ? 演算子があり、戻り値が null ではないか、確認を行っています(null チェック)。
そして、null ではない場合には指定したクラスが取得できた場合のみ、メソッドを実行します。

 Dictionary によってクラスを管理する場合には、この方法を使うことで、クラスの重複取得やメンバ変数をなくし、
そしてクラスを使うタイミングでチェックして使う方式になっています。

 Update メソッド内での null チェックもすべて上記の ? 演算子による null チェックに置き換えています。
そのため、処理が簡潔に記述されています。


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


 リファクタリングしましたので、いままでと同じように機能するか、確認を行いましょう


List を活用したケース


 削除したメンバ変数や、Update メソッドの内容は Dictionary の場合と同じです。
そのためこちらでは実際にコメントアウト部分を削除し、処理が効率化されていることをわかりやすい形で提示します。

 こちらも List 内には IChara インターフェースを格納しておき、GetChara メソッドを利用してクラスを特定して利用します。
Dictionary の実装時と同名のメソッドですが、こちらの処理には LINQ の OfType<T> メソッドの機能を利用しています。

 複数の箇所に LINQ を使わない場合の処理も一緒に記述してありますので、
どのようにして foreach の処理の部分が LINQ の処理に置き換わっているのかを確認してみましょう。


using System.Collections.Generic;  // ← List を利用するために使います
using UnityEngine;
using System.Linq;  // ← 追加します(System は不要です)

/// <summary>
/// キャラの管理クラス
/// </summary>
[RequireComponent(typeof(CharaMove))]
[RequireComponent(typeof(CharaAnime))]
[RequireComponent(typeof(CharaAttack))]
public class CharaManager : MonoBehaviour
{    
    private Vector2 direction;              //キャラが向いている方向
    public Vector2 Direction => direction;  // get のみのプロパティ

    private List<IChara> charaList = new();


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

    /// <summary>
    /// 各クラスの初期設定。外部クラスから実行する前提
    /// </summary>
    public void SetUpCharaManager() {
        
        // List の場合も、上記の2重取得の問題を解消できる
        charaList.AddRange(GetComponents<IChara>());

        foreach (var chara in charaList)
        {
            chara.SetUpChara();
        }

        ↓
        
    // 上記の foreach 部分は、foreach 内部で実行するメソッドに戻り値がなければ LINQ の ForEach メソッドを利用すると1行で記述できる
    charaList.ForEach(chara => chara.SetUpChara());
        
        GetChara<CharaAttack>()?.PrepareAttackRepeatedly();
  }

    /// <summary>
    /// List 内にある指定したクラスを取得
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    private T GetChara<T>() where T : class, IChara
    {
    // LINQ を使わない一般的な処理
        foreach (var chara in charaList)
        {
            if (chara is T typedChara)
            {
                return typedChara;
            }
        }
        return null;

        ↓
        
        // 上記の foreach 部分は、LINQ の OfType メソッドと FirstOrDefault メソッドを利用すると1行で記述できる
        return charaList.OfType<T>().FirstOrDefault();
    }

    void Update()
    {
        // タップ入力
        if (Input.GetMouseButton(0)) {
            
            // タップした位置からワールド座標取得
            Vector2 tapPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            
            // 移動とアニメ同期
            GetChara<CharaMove>()?.Move(tapPos);
            
            // 向きの更新
            UpdateDirection(tapPos);
            
            // 移動向きとアニメの向きの同期
            GetChara<CharaAnime>()?.UpdateAnimation(direction);
        }
        
        GetChara<CharaAttack>()?.UpdateAttackDirection(direction);
                
        // 攻撃の一時停止と再開
        if (Input.GetMouseButtonDown(1))
        {
            // 攻撃の一時停止・再開を切り替える
            GetChara<CharaAttack>()?.ToggleAttack();
        }
    }

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


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


 リファクタリングしましたので、いままでと同じように機能するか、確認を行いましょう

コメントをかく


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

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

Menu



技術/知識(実装例)

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

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

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

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

レースゲーム(抜粋)

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

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

3D脱出ゲーム(抜粋)

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

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

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

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

VideoPlayer イベント連動の実装例

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

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

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

private



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

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