simplestarの技術ブログ

目的を書いて、思想と試行、結果と考察、そして具体的な手段を記録します。

Unity:mecanimのホヘト

mecanim 初めての人はイロハを先に読むとわかりやすいです。
今回はもう一歩先へ進んで、三人称視点で動き回るユニティちゃんを作ります。

simplestar-tech.hatenablog.com

まずは次のリンク先からモデルデータを用意します。
unity-chan.com

Unity でユニティちゃんモデルを読み込みます。
f:id:simplestar_tech:20171118212924j:plain

三人称視点で動き回る Animator を用意したいと思うので、標準アセットをインポートします。
https://www.assetstore.unity3d.com/jp/#!/content/32351

同梱の ThirdPersonAnimatorController をユニティちゃんにセットしますと

f:id:simplestar_tech:20171118225732j:plain

このように、ブレンドツリーのタイプ 2D Freeform Cartesian に補間関係のポイントが示され
それぞれに対応した歩行アニメーションが割り当たった状態となります。

あとは、次のように Animator パラメータとパッドコントローラとの接続処理を書くことで、パッド入力で動き回るユニティちゃんが出来上がります。

        // Fixed update is called in sync with physics
        private void FixedUpdate()
        {
            // read inputs
            float h = Input.GetAxis("Horizontal");
            float v = Input.GetAxis("Vertical");
            bool crouch = Input.GetButton("Fire3");

            // calculate move direction to pass to character
            if (m_Cam != null)
            {
                // calculate camera relative direction to move:
                m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;
                m_Move = v*m_CamForward + h*m_Cam.right;
            }
            else
            {
                // we use world-relative directions in the case of no main camera
                m_Move = v*Vector3.forward + h*Vector3.right;
            }
#if !MOBILE_INPUT
			// walk speed multiplier
	        if (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
#endif

            // pass all parameters to the character control script
            m_Character.Move(m_Move, crouch, m_Jump);
            m_Jump = false;
        }
		public void Move(Vector3 move, bool crouch, bool jump)
		{

			// convert the world relative moveInput vector into a local-relative
			// turn amount and forward amount required to head in the desired
			// direction.
			if (move.magnitude > 1f) move.Normalize();
			move = transform.InverseTransformDirection(move);
			CheckGroundStatus();
			move = Vector3.ProjectOnPlane(move, m_GroundNormal);
			m_TurnAmount = Mathf.Atan2(move.x, move.z);
			m_ForwardAmount = move.z;

			ApplyExtraTurnRotation();

			// control and velocity handling is different when grounded and airborne:
			if (m_IsGrounded)
			{
				HandleGroundedMovement(crouch, jump);
			}
			else
			{
				HandleAirborneMovement();
			}

			ScaleCapsuleForCrouching(crouch);
			PreventStandingInLowHeadroom();

			// send input and other state parameters to the animator
			UpdateAnimator(move);
		}
		void UpdateAnimator(Vector3 move)
		{
			// update the animator parameters
			m_Animator.SetFloat("Forward", m_ForwardAmount, 0.1f, Time.deltaTime);
			m_Animator.SetFloat("Turn", m_TurnAmount, 0.1f, Time.deltaTime);
			m_Animator.SetBool("Crouch", m_Crouching);
			m_Animator.SetBool("OnGround", m_IsGrounded);
			if (!m_IsGrounded)
			{
				m_Animator.SetFloat("Jump", m_Rigidbody.velocity.y);
			}

			// calculate which leg is behind, so as to leave that leg trailing in the jump animation
			// (This code is reliant on the specific run cycle offset in our animations,
			// and assumes one leg passes the other at the normalized clip times of 0.0 and 0.5)
			float runCycle =
				Mathf.Repeat(
					m_Animator.GetCurrentAnimatorStateInfo(0).normalizedTime + m_RunCycleLegOffset, 1);
			float jumpLeg = (runCycle < k_Half ? 1 : -1) * m_ForwardAmount;
			if (m_IsGrounded)
			{
				m_Animator.SetFloat("JumpLeg", jumpLeg);
			}

			// the anim speed multiplier allows the overall speed of walking/running to be tweaked in the inspector,
			// which affects the movement speed because of the root motion.
			if (m_IsGrounded && move.magnitude > 0)
			{
				m_Animator.speed = m_AnimSpeedMultiplier;
			}
			else
			{
				// don't use that while airborne
				m_Animator.speed = 1;
			}
		}

これで、ブレンドツリーのブレンドパラメータがパッド入力によって変化しますので、次のGIF動画のように、入力した通りに動き回ります。
ルートモーションを適用して移動しているので、地面と足はスリップしません。

f:id:simplestar_tech:20171118230823g:plain

もともとの Animator に設定されている、しゃがみ移動、ジャンプ移動も付けてみます。
とりあえずここまではサンプルシーンを確認するだけでチャチャっと導入できます。

f:id:simplestar_tech:20171118233733g:plain

ちょっとややこしいのがカメラ操作を行うスクリプト
こちらも標準アセットを基に作成してみました。

f:id:simplestar_tech:20171119021149g:plain

キャラクターを追いかけるようにカメラが移動してます。

カメラ操作スクリプトはこちら↓

using System;
using UnityEngine;

namespace SimpleStar
{
    public class FreeLookCam : MonoBehaviour
    {
        // This script is designed to be placed on the root object of a camera rig,
        // comprising 3 gameobjects, each parented to the next:

        // 	Camera Rig
        // 		Pivot
        // 			Camera
        [SerializeField] private Transform m_Target;            // The target object to follow
        [SerializeField] private float m_MoveSpeed = 1f;                      // How fast the rig will move to keep up with the target's position.
        [Range(0f, 10f)] [SerializeField] private float m_TurnSpeed = 1.5f;   // How fast the rig will rotate from user input.
        [SerializeField] private float m_TurnSmoothing = 0.0f;                // How much smoothing to apply to the turn input, to reduce mouse-turn jerkiness
        [SerializeField] private float m_TiltMax = 75f;                       // The maximum value of the x axis rotation of the pivot.
        [SerializeField] private float m_TiltMin = 45f;                       // The minimum value of the x axis rotation of the pivot.
        [SerializeField] private bool m_LockCursor = false;                   // Whether the cursor should be hidden and locked.
        [SerializeField] private bool m_VerticalAutoReturn = false;           // set wether or not the vertical axis should auto return
        [SerializeField] private float TiltRate = 0.52f;

        private Transform m_Cam; // the transform of the camera
        private Transform m_Pivot; // the point at which the camera pivots around

        private float m_LookAngle;                    // The rig's y axis rotation.
        private float m_TiltAngle;                    // The pivot's x axis rotation.
		private Vector3 m_PivotEulers;
		private Quaternion m_PivotTargetRot;
		private Quaternion m_TransformTargetRot;
        private Vector3 m_CameraTargetPosition;
        private float m_CameraDefaultDistance;
        

        void Awake()
        {
            m_Cam = GetComponentInChildren<Camera>().transform;
            m_Pivot = m_Cam.parent;
            // Lock or unlock the cursor.
            Cursor.lockState = m_LockCursor ? CursorLockMode.Locked : CursorLockMode.None;
            Cursor.visible = !m_LockCursor;
			m_PivotEulers = m_Pivot.rotation.eulerAngles;

	        m_PivotTargetRot = m_Pivot.transform.localRotation;
			m_TransformTargetRot = transform.localRotation;
            m_CameraDefaultDistance = m_Cam.transform.localPosition.z;
        }


        protected void Update()
        {
            FollowTarget(Time.deltaTime);
            HandleRotationMovement();
            if (m_LockCursor && Input.GetMouseButtonUp(0))
            {
                Cursor.lockState = m_LockCursor ? CursorLockMode.Locked : CursorLockMode.None;
                Cursor.visible = !m_LockCursor;
            }
        }


        private void OnDisable()
        {
            Cursor.lockState = CursorLockMode.None;
            Cursor.visible = true;
        }


        void FollowTarget(float deltaTime)
        {
            if (m_Target == null) return;
            // Move the rig towards target position.
            transform.position = Vector3.Lerp(transform.position, m_Target.position, deltaTime*m_MoveSpeed);
        }


        private void HandleRotationMovement()
        {
			if(Time.timeScale < float.Epsilon)
			return;

            // Read the user input
            var x = Input.GetAxis("HorizontalTurn");
            var y = Input.GetAxis("VerticalTurn");

            // Adjust the look angle by an amount proportional to the turn speed and horizontal input.
            m_LookAngle += x*m_TurnSpeed;

            // Rotate the rig (the root object) around Y axis only:
            m_TransformTargetRot = Quaternion.Euler(0f, m_LookAngle, 0f);

            if (m_VerticalAutoReturn)
            {
                // For tilt input, we need to behave differently depending on whether we're using mouse or touch input:
                // on mobile, vertical input is directly mapped to tilt value, so it springs back automatically when the look input is released
                // we have to test whether above or below zero because we want to auto-return to zero even if min and max are not symmetrical.
                m_TiltAngle = y > 0 ? Mathf.Lerp(0, -m_TiltMin, y) : Mathf.Lerp(0, m_TiltMax, -y);
            }
            else
            {
                // on platforms with a mouse, we adjust the current angle based on Y mouse input and turn speed
                m_TiltAngle -= y*m_TurnSpeed;
                // and make sure the new value is within the tilt range
                m_TiltAngle = Mathf.Clamp(m_TiltAngle, -m_TiltMin, m_TiltMax);
            }

            // Tilt input around X is applied to the pivot (the child of this object)
            m_PivotTargetRot = Quaternion.Euler(m_TiltAngle, m_PivotEulers.y , m_PivotEulers.z);

            float cameraDistanceRate = (1.0f -  (m_TiltAngle > 0 ? (TiltRate * m_TiltAngle / m_TiltMax) : TiltRate * (m_TiltAngle / -m_TiltMin)));
            float cameraTargetDistance = cameraDistanceRate * m_CameraDefaultDistance;

            if (m_TurnSmoothing > 0)
			{
                Vector3 cameraLocalPos = m_Cam.transform.localPosition;
                cameraLocalPos.z = Mathf.Lerp(cameraLocalPos.z, cameraTargetDistance, m_TurnSmoothing);
                m_Cam.transform.localPosition = cameraLocalPos;

                m_Pivot.localRotation = Quaternion.Slerp(m_Pivot.localRotation, m_PivotTargetRot, m_TurnSmoothing * Time.deltaTime);
				transform.localRotation = Quaternion.Slerp(transform.localRotation, m_TransformTargetRot, m_TurnSmoothing * Time.deltaTime);
			}
			else
			{
                Vector3 cameraLocalPos = m_Cam.transform.localPosition;
                cameraLocalPos.z = cameraTargetDistance;
                m_Cam.transform.localPosition = cameraLocalPos;

                m_Pivot.localRotation = m_PivotTargetRot;
				transform.localRotation = m_TransformTargetRot;
			}
        }
    }
}

標準アセットのサンプル-シーンと同じく
Rig オブジェクトにこのスクリプトを割り当てますが、構成として、子供に Pivot、孫に Camera が来る構成にしなくてはなりません。
こちらのスクリプトを使う際はお気を付けください。

Unity の Humanoid のアニメーションは等身が異なるモデルでも問題なく流用できるので、いいですね。
リグを入れるデザイナーの方は、ぜひ Humanoid の適用を意識してボーンを設定していただけると嬉しいです!

次は、マイクロワールドにこのユニティちゃんを登場させてみます。

おまけ:公式アセットの全3キャラについて、同じ処理を行った結果(3人ともまったく同じ動きをしています。)
f:id:simplestar_tech:20171119084748g:plain

ユニティちゃんライセンス

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