3DGameKitの敵キャラクタを分析してみた

#概要

UnityのAseetStoreに無料で公開されている3DGameKitを調べてみると、案外3DGame開発に必要なノウハウが詰まった良質な教科書みたいに情報量が豊富に入っていた。今回は敵キャラクタChomperの仕様についての解説をします。

[3DGameKit](https://assetstore.unity.com/packages/essentials/tutorial-projects/3d-game-kit-beta-115747)
![alt](https://d2ujflorbtfzji.cloudfront.net/key-image/dafb678b-d364-4766-8b74-cce3e0be804c.jpg)


#敵キャラクタChomper
以下が敵キャラクタ"Chomper"になります。こいつの制御方法を紐解いていきます。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">3DGameKit勉強中 <a href="https://t.co/Bsxpuz0GT3">pic.twitter.com/Bsxpuz0GT3</a></p>&mdash; unagi (@UnagiHuman) <a href="https://twitter.com/UnagiHuman/status/992553634071175169?ref_src=twsrc%5Etfw">2018年5月4日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


余談ですがChomperは直訳で「タコ野郎」という意味になります。

#敵キャラクタが動作する最低限の環境を用意する
3DGameKitのchomper(敵キャラクタ)に必要なオブジェクトのみを切り出して検証しやすくします。一番簡単なのは、3DGameKitのLevel01.unityシーン中から必要なものを持ってくることです。
Level01.unityと、新規に作成する検証用のシーン(下記例ではSampleScene)をHierarychy上に置きLevel01.unityから必要なものをSampleSceneにコピーして持っていきます。必要なものは、敵キャラクタ自身と敵キャラクタの攻撃対象であるPlayerになります。

Level01.unityからコピーして持ってくるもの

- 敵キャラクタ
    - Chomper_GamePlay
- Player
    - HealthCanvas
    - CameraRig
    - Ellen

Player関連はEllenがHealthCanvasを参照しているので、参照関係が崩れないように双方同時にコピーして持っていきます。

![hierarchy_chomper.PNG](https://qiita-image-store.s3.amazonaws.com/0/116884/c98476d0-c03a-7fdd-d9dc-9c1c61d21b00.png)

次に、Chomperが歩きまわる為の地面を設置します。
以下の手順を踏むとNavmesh付きの地面が生成されます。

- 適当な大きさのCube作成
- CubeのLayerをEnvironmentに設定
- CubeにNavmeshSurfaceをAdd
- NavmeshSurfaceのAgentType=Chomper, IncludeLayerをEnviroment
- NavmeshSurfaceのBakeを押す

![cubeNavmesh.PNG](https://qiita-image-store.s3.amazonaws.com/0/116884/b9d4eccb-c20a-f8ae-708a-f30686879c71.png)

完成図が以下のようになります。プレイヤーを操作してChomper近くに行くとChomperが襲ってきます。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/unity?src=hash&amp;ref_src=twsrc%5Etfw">#unity</a> <a href="https://twitter.com/hashtag/3DGameKit?src=hash&amp;ref_src=twsrc%5Etfw">#3DGameKit</a><br>敵キャラとプレイヤーを3DGameKitのシーンから分離して、自分で作ったシーンに配置して動かすところまで出来た。 <a href="https://t.co/vJdF5soeZZ">pic.twitter.com/vJdF5soeZZ</a></p>&mdash; unagi (@UnagiHuman) <a href="https://twitter.com/UnagiHuman/status/986450866155634688?ref_src=twsrc%5Etfw">2018年4月18日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


#全体構成
Chomper制御の全体構成は以下のようになる。ChomperBehaviourがChomper制御の大元のクラスになっており、これが状態遷移、移動、攻撃等をコントロールしている。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/116884/dc627120-61e1-c6f4-2792-175290361bc2.png)

#アニメーションとロジック制御

アニメーションの状態遷移は以下のようになる。待機状態を基準として、Playerが索敵範囲内に入ったら"Spotted"->"ChomperRunFoward"状態になり、攻撃範囲内に入ったら"ChomperAttack"になる。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/116884/42b093db-af54-dc1e-eed0-931ce8f0128b.png)
Mecanimは以下のようになっている。各々のStateにはState machine behavioursがAddされている。state mathine behavioursに関しては[【Unity】State Machine Behaviour について](http://techblog.kayac.com/unity_advent_calendar_2016_15)を見ると良く分かる。ChomperはこのState machine behavioursを利用してAnimationのステート遷移をロジックのステート遷移（SMB)として利用している。
つまり、あるステート状態のときに、何の条件を満たしたら何のステートに遷移するかという処理がstate mathine behavioursの中身を追うと分かる。
![chomperState.PNG](https://qiita-image-store.s3.amazonaws.com/0/116884/7faab118-d945-b5e5-ea80-2dbfbeda0d50.png)

例えば、ChomperIdle状態であるときはステート更新処理のたびに下記の処理が走り、chomperがPlayerキャラを発見した時に限って、Playerを追跡する状態に遷移するようになっている。

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;

namespace Gamekit3D
{
    public class ChomperSMBIdle : SceneLinkedSMB<ChomperBehavior>
    {
        public override void OnSLStateNoTransitionUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
        {
            base.OnSLStateNoTransitionUpdate(animator, stateInfo, layerIndex);

            m_MonoBehaviour.FindTarget();
            if (m_MonoBehaviour.target != null)
            {
                m_MonoBehaviour.StartPursuit();
            }
        }
    }
}
```





#移動制御
EnemyControllerで行う。
###役割
* NavimeshによるPlayerの追跡移動
* 攻撃を受けた時の吹っ飛びの物理演算
* 接地判定

移動はNavmeshを利用してターゲットを追跡する動きをし、
プレイヤーからダメージを受けたときにHit状態になってChomperはぶっ飛ぶが、この時はRigidBodyでの制御に切り替えている。
また、AnimationとRigidBodyによる物理演算を連動させるために animator.updateMode = AnimatorUpdateMode.AnimatePhysicsに設定している。Navmeshでの移動、RigidBodyの移動はAnimationと同期させる為にOnAnimatorMove内で計算している。

[Animationと物理演算の連動に関してはこのリンク先をみると良く分かります](http://tsubakit1.hateblo.jp/entry/2017/04/03/224912)


↓Playerに攻撃された時は、物理演算とAnimationの連動を行っている。
![Unity-2018.1.0b13-_64bit_-SampleScene.gif](https://qiita-image-store.s3.amazonaws.com/0/116884/9f5467b8-6f16-5354-281b-1f5a614a75ac.gif)

```csharp:EnemyController.cs
using System.Collections;
using System.Collections.Generic;
using System.Security.Cryptography;
using UnityEngine;
using UnityEngine.AI;

namespace Gamekit3D
{
//this assure it's runned before any behaviour that may use it, as the animator need to be fecthed
    [DefaultExecutionOrder(-1)]
    [RequireComponent(typeof(NavMeshAgent))]
    public class EnemyController : MonoBehaviour
    {
        public bool interpolateTurning = false;
        public bool applyAnimationRotation = false;

        public Animator animator { get { return m_Animator; } }
        public Vector3 externalForce { get { return m_ExternalForce; } }
        public NavMeshAgent navmeshAgent { get { return m_NavMeshAgent; } }
        public bool followNavmeshAgent { get { return m_FollowNavmeshAgent; } }
        public bool grounded { get { return m_Grounded; } }

        protected NavMeshAgent m_NavMeshAgent;
        protected bool m_FollowNavmeshAgent;
        protected Animator m_Animator;
        protected bool m_UnderExternalForce;
        protected bool m_ExternalForceAddGravity = true;
        protected Vector3 m_ExternalForce;
        protected bool m_Grounded;

        protected Rigidbody m_Rigidbody;

        const float k_GroundedRayDistance = .8f;

        void OnEnable()
        {
            m_NavMeshAgent = GetComponent<NavMeshAgent>();
            m_Animator = GetComponent<Animator>();
			//これでAnimationとRigidBodyの物理演算を同期している
            m_Animator.updateMode = AnimatorUpdateMode.AnimatePhysics;

            m_NavMeshAgent.updatePosition = false;

            m_Rigidbody = GetComponentInChildren<Rigidbody>();
            if (m_Rigidbody == null)
                m_Rigidbody = gameObject.AddComponent<Rigidbody>();

            m_Rigidbody.isKinematic = true;
            m_Rigidbody.useGravity = false;
            m_Rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous;
            m_Rigidbody.interpolation = RigidbodyInterpolation.Interpolate;

            m_FollowNavmeshAgent = true;
        }

        private void FixedUpdate()
        {
            CheckGrounded();

            if (m_UnderExternalForce)
                ForceMovement();
        }


		/// <summary>
		/// Raycastで接地判定
		/// </summary>
        void CheckGrounded()
        {
            RaycastHit hit;
            Ray ray = new Ray(transform.position + Vector3.up * k_GroundedRayDistance * 0.5f, -Vector3.up);
            m_Grounded = Physics.Raycast(ray, out hit, k_GroundedRayDistance, Physics.AllLayers,
                QueryTriggerInteraction.Ignore);
        }


		/// <summary>
		/// 物理演算による移動（主にダメージを受けた場合のぶっ飛び）
		/// </summary>
        void ForceMovement()
        {
            if(m_ExternalForceAddGravity)
                m_ExternalForce += Physics.gravity * Time.deltaTime;

            RaycastHit hit;
            Vector3 movement = m_ExternalForce * Time.deltaTime;
            if (!m_Rigidbody.SweepTest(movement.normalized, out hit, movement.sqrMagnitude))
            {
                m_Rigidbody.MovePosition(m_Rigidbody.position + movement);
            }

            m_NavMeshAgent.Warp(m_Rigidbody.position);
        }

        private void OnAnimatorMove()
        {
            if (m_UnderExternalForce)
                return;

            if (m_FollowNavmeshAgent)
            {
                m_NavMeshAgent.speed = (m_Animator.deltaPosition / Time.deltaTime).magnitude;
                transform.position = m_NavMeshAgent.nextPosition;
            }
            else
            {
				///AddForceの物理演算をAnimationと同期させる
                RaycastHit hit;
				///オブジェクトが他のColliderと衝突するかどうか
                if (!m_Rigidbody.SweepTest(m_Animator.deltaPosition.normalized, out hit,
                    m_Animator.deltaPosition.sqrMagnitude))
                {
                    m_Rigidbody.MovePosition(m_Rigidbody.position + m_Animator.deltaPosition);
                }
            }

            if (applyAnimationRotation)
            {
                transform.forward = m_Animator.deltaRotation * transform.forward;
            }
        }

        // used to disable position being set by the navmesh agent, for case where we want the animation to move the enemy instead (e.g. Chomper attack)
        public void SetFollowNavmeshAgent(bool follow)
        {
            if (!follow && m_NavMeshAgent.enabled)
            {
                m_NavMeshAgent.ResetPath();
            }
            else if(follow && !m_NavMeshAgent.enabled)
            {
                m_NavMeshAgent.Warp(transform.position);
            }

            m_FollowNavmeshAgent = follow;
            m_NavMeshAgent.enabled = follow;
        }

		/// <summary>
		/// 物理演算の開始。物理演算自体はOnAnimatorMove内で行う。
		/// </summary>
		/// <param name="force"></param>
		/// <param name="useGravity"></param>
		public void AddForce(Vector3 force, bool useGravity = true)
        {
            if (m_NavMeshAgent.enabled)
                m_NavMeshAgent.ResetPath();

            m_ExternalForce = force;
            m_NavMeshAgent.enabled = false;
            m_UnderExternalForce = true;
            m_ExternalForceAddGravity = useGravity;
        }

		/// <summary>
		/// 物理演算をやめる
		/// </summary>
        public void ClearForce()
        {
            m_UnderExternalForce = false;
            m_NavMeshAgent.enabled = true;
        }


		/// <summary>
		/// 指定した方向に回転する
		/// </summary>
		/// <param name="forward"></param>
        public void SetForward(Vector3 forward)
        {
            Quaternion targetRotation = Quaternion.LookRotation(forward);

            if (interpolateTurning)
            {
                targetRotation = Quaternion.RotateTowards(transform.rotation, targetRotation,
                    m_NavMeshAgent.angularSpeed * Time.deltaTime);
            }

            transform.rotation = targetRotation;
        }

        public void SetTarget(Vector3 position)
        {
            m_NavMeshAgent.destination = position;
        }
    }
}
```

#ダメージ制御
Chomperのダメージは次の2つのケースで発生する。

1. Playerの武器で殴られた時
Playerの武器からRayCastを飛ばして、ChomperのDamageableに接触した時にDamage情報をChomperに渡す
![image.png](https://qiita-image-store.s3.amazonaws.com/0/116884/826eebfb-1666-dca4-1e69-00a6a99bae83.png)

2. ChomperAttack状態以外の時にPlayerに体当たりされた場合
PlayerのコライダーがChomperのコライダーに接触した場合、ContactDamagerから
![image.png](https://qiita-image-store.s3.amazonaws.com/0/116884/9e38d6f6-5f62-8cca-7bab-2db3ea5a39e7.png)


###ReplaceWithRagdoll
敵キャラ死亡時に自らのオブジェクトをsetActive(false)で不可視にし、Ragdollを設定した敵キャラオブジェクトに切り替える。
要するに死亡時のアニメーションはRagDollによる物理演算で行っている。
Ragdollのリグはrigitbody,colliderがjointで接続された構造になっている。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr"><a href="https://twitter.com/hashtag/unity?src=hash&amp;ref_src=twsrc%5Etfw">#unity</a> <a href="https://twitter.com/hashtag/3DGameKit?src=hash&amp;ref_src=twsrc%5Etfw">#3DGameKit</a><br>3DGameKitの敵キャラのRagDoll処理。 <a href="https://t.co/kpyi5Xyn79">pic.twitter.com/kpyi5Xyn79</a></p>&mdash; unagi (@UnagiHuman) <a href="https://twitter.com/UnagiHuman/status/986970537875275776?ref_src=twsrc%5Etfw">2018年4月19日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>