Unity チュートリアルのタワーディフェンステンプレートを触ってみる(12)

Unity チュートリアルのタワーディフェンステンプレートを触ってみる(11ではエージェント(敵ユニット)の作成を行った。今回はエージェントに使用したスクリプトのうち、AttackingAgent と FlyingAgent の継承元となる Agent 解説を行っていきたいと思う。

1.エージェント編 – Agent.cs

Agent.cs について稚拙ながら解説

「Agent.cs」は「Asset/Scripts/TowerDefense/Agents/Agent.cs」の指しておりスクリプトについては以下の通り。内容としては Agent の挙動制御を行っている。

[cce_csharp]using System;
using ActionGameFramework.Health;
using Core.Utilities;
using TowerDefense.Affectors;
using TowerDefense.Level;
using TowerDefense.Nodes;
using UnityEngine;
using UnityEngine.AI;

namespace TowerDefense.Agents
{
    /// <summary>
    /// An agent will follow a path of nodes
    /// </summary>
    [RequireComponent(typeof(NavMeshAgent)), RequireComponent(typeof(AttackAffector))]
    public abstract class Agent : Targetable
    {   
        /// <summary>
        /// A means of keeping track of the agent along its path
        /// </summary>
        public enum State
        {
            /// <summary>
            /// When the agent is on a path that is not blocked
            /// </summary>
            OnCompletePath,

            /// <summary>
            /// When the agent is on a path is blocked
            /// </summary>
            OnPartialPath,

            /// <summary>
            /// When the agent has reached the end of a blocked path
            /// </summary>
            Attacking,

            /// <summary>
            /// For flying agents, when they move over obstacles
            /// </summary>
            PushingThrough,
            
            /// <summary>
            /// When the agent has completed their path
            /// </summary>
            PathComplete
        }

        /// <summary>
        /// Event fired when agent reached its final node
        /// </summary>
        public event Action<Node> destinationReached;

        /// <summary>
        /// Position offset for an applied affect
        /// </summary>
        public Vector3 appliedEffectOffset = Vector3.zero;
        
        /// <summary>
        /// Scale adjustment for an applied affect
        /// </summary>
        public float appliedEffectScale = 1;

        /// <summary>
        /// The NavMeshAgent component attached to this
        /// </summary>
        protected NavMeshAgent m_NavMeshAgent;

        /// <summary>
        /// The Current node that the agent must navigate to
        /// </summary>
        protected Node m_CurrentNode;

        /// <summary>
        /// Reference to the level manager
        /// </summary>
        protected LevelManager m_LevelManager;

        /// <summary>
        /// Stores the Destination to the next node so we don't need to get new random positions every time
        /// </summary>
        protected Vector3 m_Destination;
        
        /// <summary>
        /// Gets the attached nav mesh agent velocity
        /// </summary>
        public override Vector3 velocity
        {
            get { return m_NavMeshAgent.velocity; }
        }
        
        /// <summary>
        /// The current state of the agent along the path
        /// </summary>
        public State state { get; protected set; }

        /// <summary>
        /// Accessor to <see cref="m_NavMeshAgent"/>
        /// </summary>
        public NavMeshAgent navMeshNavMeshAgent
        {
            get { return m_NavMeshAgent; }
            set { m_NavMeshAgent = value; }
        }

        /// <summary>
        /// The area mask of the attached nav mesh agent
        /// </summary>
        public int navMeshMask
        {
            get { return m_NavMeshAgent.areaMask; }
        }

        /// <summary>
        /// Gets this agent's original movement speed
        /// </summary>
        public float originalMovementSpeed { get; private set; }

        /// <summary>
        /// Checks if the path is blocked
        /// </summary>
        /// <value>
        /// The status of the agent's path
        /// </value>
        protected virtual bool isPathBlocked
        {
            get { return m_NavMeshAgent.pathStatus == NavMeshPathStatus.PathPartial; }
        }

        /// <summary>
        /// Is the Agent close enough to its destination?
        /// </summary>
        protected virtual bool isAtDestination
        {
            get { return navMeshNavMeshAgent.remainingDistance <= navMeshNavMeshAgent.stoppingDistance; }
        }

        /// <summary>
        /// Sets the node to navigate to
        /// </summary>
        /// <param name="node">The node that the agent will navigate to</param>
        public virtual void SetNode(Node node)
        {
            m_CurrentNode = node;
        }

        /// <summary>
        /// Stops the navMeshAgent and attempts to return to pool
        /// </summary>
        public override void Remove()
        {
            base.Remove();
            
            m_LevelManager.DecrementNumberOfEnemies();
            if (m_NavMeshAgent.enabled)
            {
                m_NavMeshAgent.isStopped = true;
            }
            m_NavMeshAgent.enabled = false;

            Poolable.TryPool(gameObject);
        }

        /// <summary> 
        /// Setup all the necessary parameters for this agent from configuration data
        /// </summary>
        public virtual void Initialize()
        {
            ResetPositionData();
            LazyLoad();
            configuration.SetHealth(configuration.maxHealth);
            state = isPathBlocked ? State.OnPartialPath : State.OnCompletePath;

            m_NavMeshAgent.enabled = true;
            m_NavMeshAgent.isStopped = false;
            
            m_LevelManager.IncrementNumberOfEnemies();
        }

        /// <summary>
        /// Finds the next node in the path
        /// </summary>
        public virtual void GetNextNode(Node currentlyEnteredNode)
        {
            // Don't do anything if the calling node is the same as the m_CurrentNode
            if (m_CurrentNode != currentlyEnteredNode)
            {
                return;
            }
            if (m_CurrentNode == null)
            {
                Debug.LogError("Cannot find current node");
                return;
            }

            Node nextNode = m_CurrentNode.GetNextNode();
            if (nextNode == null)
            {
                if (m_NavMeshAgent.enabled)
                {
                    m_NavMeshAgent.isStopped = true;
                }
                HandleDestinationReached();
                return;
            }
            
            Debug.Assert(nextNode != m_CurrentNode);
            SetNode(nextNode);
            MoveToNode();
        }

        /// <summary>
        /// Moves the agent to a position in the <see cref="Agent.m_CurrentNode" />
        /// </summary>
        public virtual void MoveToNode()
        {
            Vector3 nodePosition = m_CurrentNode.GetRandomPointInNodeArea();
            nodePosition.y = m_CurrentNode.transform.position.y;
            m_Destination = nodePosition;
            NavigateTo(m_Destination);
        }

        /// <summary>
        /// The logic for what happens when the destination is reached
        /// </summary>
        public virtual void HandleDestinationReached()
        {
            state = State.PathComplete;
            if (destinationReached != null)
            {
                destinationReached(m_CurrentNode);
            } 
        }
        
        /// <summary>
        /// Lazy Load, if necesaary and ensure the NavMeshAgent is disabled
        /// </summary>
        protected override void Awake()
        {
            base.Awake();
            LazyLoad();
            m_NavMeshAgent.enabled = false;
        }
        
        /// <summary>
        /// Updates the agent in its different states, 
        /// Reset destination when path is stale
        /// </summary>
        protected virtual void Update()
        {
            // Update behaviour for different states
            PathUpdate();
            
            // If the path becomes invalid, repath the agent to the destination
            bool validStalePath = m_NavMeshAgent.isOnNavMesh && m_NavMeshAgent.enabled &&
                                  (!m_NavMeshAgent.hasPath && !m_NavMeshAgent.pathPending);
            if (validStalePath)
            {
                // Compare against squared stopping distance on agent.
                // We intentionally do not pre-square this value so that it can be changed at runtime dynamically
                float squareStoppingDistance = m_NavMeshAgent.stoppingDistance * m_NavMeshAgent.stoppingDistance;
                if (Vector3.SqrMagnitude(m_Destination - transform.position) < squareStoppingDistance &&
                    m_CurrentNode.GetNextNode() != null)
                {
                    // Proceed if we're at our destination
                    GetNextNode(m_CurrentNode);
                }
                else
                {
                    // Otherwise try repath
                    m_NavMeshAgent.SetDestination(m_Destination);
                }
            }
        }

        /// <summary>
        /// Set the NavMeshAgent's destination
        /// </summary>
        /// <param name="nextPoint">The position to navigate to</param>
        protected virtual void NavigateTo(Vector3 nextPoint)
        {
            LazyLoad();
            if (m_NavMeshAgent.isOnNavMesh)
            {
                m_NavMeshAgent.SetDestination(nextPoint);
            }
        }

        /// <summary>
        /// This is a lazy way of caching several components utilised by the Agent
        /// </summary>
        protected virtual void LazyLoad()
        {
            if (m_NavMeshAgent == null)
            {
                m_NavMeshAgent = GetComponent<NavMeshAgent>();
                originalMovementSpeed = m_NavMeshAgent.speed;
            }
            if (m_LevelManager == null)
            {
                m_LevelManager = LevelManager.instance;
            }
        }

        /// <summary>
        /// Move along the path, change to <see cref="Agent.State.OnPartialPath" />
        /// </summary>
        protected virtual void OnCompletePathUpdate()
        {
            if (isPathBlocked)
            {
                state = State.OnPartialPath;
            }
        }

        /// <summary>
        /// Peforms the relevant path update
        /// </summary>
        protected abstract void PathUpdate();

        /// <summary>
        /// The behaviour for when the agent has been blocked
        /// </summary>
        protected abstract void OnPartialPathUpdate();
        
        
#if UNITY_EDITOR
        /// <summary>
        /// Draw the agent's path
        /// </summary>
        protected virtual void OnDrawGizmosSelected()
        {
            if (m_NavMeshAgent != null)
            {
                Vector3[] pathPoints = m_NavMeshAgent.path.corners;
                int count = pathPoints.Length;
                for (int i = 0; i < count - 1; i++)
                {
                    Vector3 from = pathPoints[i];
                    Vector3 to = pathPoints[i + 1];
                    Gizmos.DrawLine(from, to);
                }
                Gizmos.DrawWireSphere(m_NavMeshAgent.destination, 0.2f);
            }
        }
#endif
    }
}[/cce_csharp]

15行目 : 「[RequireComponent(typeof(NavMeshAgent)), RequireComponent(typeof(AttackAffector))]」 は以前にも解説した通り、NavMeshAgent コンポーネントと AttackAffector が必須であることを指している。以前の記事についてはこちらから

16行目 : 「public abstract class Agent : Targetable」 は以前解説を行った Targetable クラスを継承した抽象クラスであることを指している。Targetable クラスについてはこちらを参照してほしい。また、抽象クラスについてはこちらを参照してほしい。

Targetable

 

抽象クラス

21行目 : 「public enum State」 は列挙型のデータを定義している。『C#プログラミング言語の列挙型はCのenumの意味する多くの”小さな整数”を保持する。いくつかの数値演算はenumでは定義されないが、enum値は明示的に整数に変換し元に戻すことができる。またenum変数はenum宣言によって定義されなかった値を保存できる。 』(Wikipedia 列挙型 C# より引用)

とのことだが、最初のうちはイメージとしてリストのイメージを持っていれば良いと思う。果物のリストであったり、今回のように「ブロックされていない経路上にいる状態」「ブロックされている経路上にいる状態」などの状態がのリストと言う認識だ。

150行目 : 「public override void Remove()」 は以前解説を行った Targetable クラスの Remove 関数を上書きしていることを指している。内容としては Targetable クラスの Remove 処理を行った後、「LevelManager」のエレメント要素数を減らし処理を行い、「NavMeshAgent」の無効化を行っている。「LevelManager」と「NavMeshAgent」については改めて解説を行う。

167行目 : 「public virtual void Initialize()」は上書き可能な初期化関数である。位置データの初期化と遅延読み込み、状態の初期化、「NavMeshAgent」の有効化を行っている。

183行目 : 「public virtual void GetNextNode(Node currentlyEnteredNode)」は上書き可能な次のノードを取得する関数である。現在のノードから次のノードを探索し、次のノードへ移動するという内容を行っている模様。ここについては「NavMeshAgent」と「Node」についても確認を行わなければ処理の内容を追うのが難しい。

215行目 : 「public virtual void MoveToNode()」は上書き可能な次のノードへの移動関数である。ここでは、次ノードの座標を取得して実際の処理については「NavigateTo」にて行っている。

226行目 : 「public virtual void HandleDestinationReached()」は上書き可能な到達したノードを記録する関数である。

238行目 : 「protected virtual void Awake()」は Unity に搭載されている起動時処理を行っている。

249行目 : 「protected virtual void Update()」は Unity に搭載されている更新処理を行っている。内容としてはパスが有効であれば、メッシュ上の停止距離を計算してノードの更新次の行き先等の設定を行っている。

280行目 : 「protected virtual void NavigateTo(Vector3 nextPoint)」 は次の地点へのナビゲートを行っている。

292行目 : 「protected virtual void LazyLoad()」は上書き可能な遅延読み込みを行っている。

308行目 : 「protected virtual void OnCompletePathUpdate()」はパスがブロックされているか判定し、ブロックされている際は状態を OnPartialPath にしている

331行目 : 「protected virtual void OnDrawGizmosSelected()」については公式ページに『オブジェクトが選択されている場合は、ギズモを描画するために OnDrawGizmosSelected を実装します。Gizmos はオブジェクトを選択している時のみ描画されます。ギズモは選択可能ではありません。 これはセットアップを簡単に実行するために使用されます。例えば、爆発スクリプトは 爆発半径を表示する球体のギズモを描画することができます。』とある。公式ページについてはこちらを参照していただきたい。

簡単に説明すると「Unity に搭載されている、開発用に表示できる範囲やエリアの表示するための関数(処理については自作)」と思っていただければ良いと思う。

%d人のブロガーが「いいね」をつけました。