【Unity/3D】舞空術のモーション&コード

舞空術Unityちゃん

3Dシューティング風味なエンドレスランナーの空中移動周りの実装。
地上を捨てる事で、接地判定しなくて良いので楽です。

移動可能範囲の固定は、PlayerのZ座標のみ適用した中心座標から、sqrMagnitudeで判定。
進行方向を、前述の中心座標へのベクトルで上書き、みたいな感じで適当に追加すればOk。

Unityちゃん3Dモデル等: © UTJ/UCL

サンプル動画

*音なし

舞空術モーション

舞空術モーション(Zip)

  • ライセンス: CC0

内包FBXの説明

  • 無印: 足の開きが普通な3Dモデル用。
  • ~FixedLegs: ユニティちゃん等の足の開きが悪いタイプの3Dモデル用。
    (なんかユニティちゃんの3Dモデルでは、足のボーンの影響が弱かった)

各モーション

  • 「metarig|Idle」: 待機時。
  • 「metarig|Stop」: 停止時。
  • 「metarig|Lower One’s Hands」: 両手を下に下げるタイプの飛行モーション。
  • 「metarig|Raise One’s Right Hand」: 右手だけ上に突き上げるタイプの飛行モーション。
  • 「metarig|metarig|Raise One’s Hands」: 両手を上に突き上げるタイプの飛行モーション。

モーション用FBXのインポート&設定

  1. Projectウィンドウに、対象FBXをドラッグ&ドロップしてインポート。
  2. Projectウィンドウ -> 対象FBXを選択、
  3. インスペクター -> Rigタブを選択。
  4. Animation Type: Humanoidに変更。
  5. Apply

Animatorへのモーションの追加と基本モーションの設定

  1. Animatorタブを選択し、Animatorウィンドウを開く。*
    *(開いていない場合は、上部メニュー -> Window -> Animation -> Animatorを選択)
  2. Animatorウィンドウに、モーション用FBXをドラッグ&ドロップ。
  3. Entryステートを右クリック -> Set StateMachine Default State -> 「metarig|Idle」を選択。

モーションの遷移スピードの調整

StateMachineのSpeedを0.25等に調整しておくと、遷移スピードも緩やかになる。

オーラの作り方

  1. 魔法エフェクト系のアセットパックから、オーラっぽい物をピックアップ。
    (サンプルでは、[Archanor VFX] MagicArsenalのLightSprayプレハブを使用)
  2. ParticleSystemの設定を変更。
    • 【ベース部分】Simulation Space: Localに設定し、身体に纏わせる。
    • 【オーラの尾(Trail)部分】ベースのParticleSystemを複製し、Simulation Space: Worldに設定。
    • 【色の濃さ】Color over Lifetime -> Gradient Editor -> 上側のタブをクリックし、Alpha値を変更。

コード

Player

  1. 新規スクリプト「Player」に、以下のコードをコピペ。
  2. 空のオブジェクト「Player」を作成し、Playerスクリプトをアタッチ。
  3. Rigidbodyコンポーネントをアタッチし、UseGravityを無効化しておく。
  4. Player下に、任意の3Dモデルを設置。
  5. Playerを選択し、インスペクターから、各種コンポーネントを紐付けしておく。


using System.Collections;
//使わなかったので、一応コメントアウト。Listとか使う場合は戻してください。
//using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

//インスペクターから各種コンポーネントを紐付けしておく。
	public GameObject go;
	public Transform tf;

	[SerializeField]
	Rigidbody rb;

	[SerializeField]
	Animator anim;



//FixedUpdate相当のコルーチンのウェイトを定数化。
	static readonly WaitForFixedUpdate FixedWait = new WaitForFixedUpdate();
//Time.fixedDeltaの値を定数化。
	static readonly float FixedDelta = 0.02f;

//各モーションのハッシュ(モーション名から数値に変換した奴)。
	static readonly int LevitationTechniqueAHash = Animator.StringToHash("metarig|Lower One's Hands");
	static readonly int StopHash = Animator.StringToHash("metarig|Stop");
	static readonly int IdleHash = Animator.StringToHash("metarig|Idle");

//秒速25メートルを1Fixedフレーム速度に変換している。
	float speed = 25.0f * FixedDelta;

	Vector3 direction;
	Vector3 lookAtVelocity;

//タップ位置に向きを変える(LookAt)する大体の時間。
	static readonly float LookAtSmoothTime = 0.2f;

	Coroutine move;

//舞空術フラグ。一応パブリックにしている。
	[System.NonSerialized]
	public bool isMoving;

//モーション遷移時間の長さ。
	static readonly float CrossFadeDuration = 0.1f;

//舞空術の解除時に姿勢を戻す時間の長さ。
	static readonly float CorrectRotationDuration = 0.25f;

//舞空術の解除時の擬似慣性の長さ。
	static readonly float InertiaDuration = 0.35f;

	Coroutine inertia;

//擬似慣性にスピードを変換するレート。
	static readonly float InertiaRate = 0.7f;

	Quaternion inertiaPreviousRotation;
	Quaternion inertiaTargetRotation;
	static readonly Quaternion InertiaRelativeRotation = Quaternion.Euler(-90, 0, 0);
	float inertiaElapsedTime;
	Vector3 previousDirection;



	public void StartMove()
	{
		if (move != null) {
			StopCoroutine(move);
		}

		if (inertia != null) {
			StopCoroutine(inertia);
			inertia = null;
		}

		move = StartCoroutine(Move());
	}

	IEnumerator Move()
	{
		isMoving = true;
		anim.CrossFade(LevitationTechniqueAHash, CrossFadeDuration, 0, 0);

/*
//【オーラ管理用サンプル】
		if (releaseAura != null) {
			StopCoroutine(releaseAura);
			releaseAura = null;
		}

		if (chargeAura == null) {
			chargeAura = StartCoroutine(ChargeAura());
		}
*/

		while (true) {
//横向きの姿勢になるので、前ではなく上の方向に進ませる。
			rb.MovePosition(tf.position + tf.up * speed);

			LookAtTarget();

			yield return FixedWait;
		}
	}

	public void EndMove()
	{
		if (move != null) {
			StopCoroutine(move);
			move = null;

			isMoving = false;

			anim.CrossFade(StopHash, CrossFadeDuration, 0, 0);

			inertia = StartCoroutine(Inertia());



/*
//【オーラ管理用サンプル】
			if (chargeAura != null) {
				StopCoroutine(chargeAura);
				chargeAura = null;
			}

			if (releaseAura == null) {
				releaseAura = StartCoroutine(ReleaseAura());
			}
*/
		}
	}



	IEnumerator Inertia()
	{
		previousDirection = tf.up;
		inertiaPreviousRotation = tf.rotation;
		inertiaTargetRotation = inertiaPreviousRotation * InertiaRelativeRotation;

		inertiaElapsedTime = 0;

		while (true) {
			inertiaElapsedTime += FixedDelta;

			if (inertiaElapsedTime < CorrectRotationDuration)
				rb.MoveRotation(Quaternion.Lerp(inertiaPreviousRotation, inertiaTargetRotation, inertiaElapsedTime / CorrectRotationDuration));

			rb.MovePosition(tf.position + previousDirection * speed * InertiaRate);

			if (InertiaDuration < inertiaElapsedTime) {
				anim.CrossFade(IdleHash, CrossFadeDuration, 0, 0);
				inertia = null;
				yield break;
			}

			yield return FixedWait;
		}
	}


	void LookAtTarget()
	{
		GameManager.Instance.CheckTargetPosition();
		direction = Vector3.SmoothDamp(direction, GameManager.Instance.targetPosition - tf.position, ref lookAtVelocity, LookAtSmoothTime, Mathf.Infinity, FixedDelta);
		tf.up = direction;
	}



/*
//【オーラ管理用サンプル】
	static readonly float AuraControlDuration = 0.6f;
	float auraControlElapsedTime;

//インスペクターから、オーラ用のParticleSystemを紐付けしておく。
	[SerializeField]
	ParticleSystem[] pss;

	Coroutine chargeAura;
	Coroutine releaseAura;

	IEnumerator ChargeAura()
	{
		auraControlElapsedTime = 0;
		while (auraControlElapsedTime < AuraControlDuration) {
			auraControlElapsedTime += Time.deltaTime;

			SetRate(Mathf.Lerp(0, 1.0f, auraControlElapsedTime / AuraControlDuration));
			yield return null;
		}
	}

	IEnumerator ReleaseAura()
	{
		auraControlElapsedTime = 0;
		while (auraControlElapsedTime < AuraControlDuration) {
			auraControlElapsedTime += Time.deltaTime;

			SetRate(Mathf.Lerp(1.0f, 0, auraControlElapsedTime / AuraControlDuration));
			yield return null;
		}
	}

	void SetRate(float rate)
	{
		for (int i = 0; i < pss.Length; i++) {
//使用したParticleSystemによって最適な値が変わるので要調整。
			pss[i].startSize = Mathf.Lerp(0, 3.0f, rate);
			pss[i].startSpeed = Mathf.Lerp(0, 0.1f, rate);
		}
	}
*/


/*
//【モーションチェンジ用サンプル】

	static readonly int LevitationTechniqueBHash = Animator.StringToHash("metarig|Raise One's Right Hand");
	static readonly int LevitationTechniqueCHash = Animator.StringToHash("metarig|Raise One's Hands");

	void Update()
	{
		if (!isMoving)
			return;

		if (Input.GetKeyDown("z")) {
			anim.CrossFade(LevitationTechniqueAHash, CrossFadeDuration, 0, 0);
		} else if (Input.GetKeyDown("x")) {
			anim.CrossFade(LevitationTechniqueBHash, CrossFadeDuration, 0, 0);
		} else if (Input.GetKeyDown("c")) {
			anim.CrossFade(LevitationTechniqueCHash, CrossFadeDuration, 0, 0);
		}
	}
*/

}

GameManager

  1. 新規スクリプト「GameManager」に、以下のコードをコピペ。
  2. 空のオブジェクト「GameManager」を作成し、GameManagerスクリプトをアタッチ。
  3. インスペクターから、Playerを紐付けしておく。


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;


public class GameManager : MonoBehaviour
{

	public static GameManager Instance = null;


//プレイヤーを紐付けしておく。
	[SerializeField]
	Player player;


	Camera mainCamera;
	Transform mainCameraTf;

//移動先のターゲット位置。プレイヤー側から読み取る。
	[System.NonSerialized]
	public Vector3 targetPosition;

	Vector3 touchPosition;

//タップ位置をワールド座標へ変換する、ターゲット仮想プレーンまでの距離(Z)。
	static readonly float CameraToTargetDistance = 20.0f;

	PointerEventData pointer = new PointerEventData(EventSystem.current);
	List<RaycastResult> isPointerOverUIResults = new List<RaycastResult>(10);
	EventSystem eventSystem;

//FixedUpdate相当のコルーチンのウェイトを定数化。
	static readonly WaitForFixedUpdate FixedWait = new WaitForFixedUpdate();

//フォローカメラの相対距離。
	static readonly Vector3 FollowCameraRelativePosition = new Vector3(0, 2.0f, -3.5f);

	Coroutine updateFollowCamera;



	void Awake()
	{
		if (Instance == null)
			Instance = this;
		else if (Instance != this)
			Destroy(gameObject);    

		DontDestroyOnLoad(gameObject);



		mainCamera = Camera.main;
		mainCameraTf = mainCamera.transform;

		eventSystem = EventSystem.current;



		StartUpdateFollowCamera();
	}

	void Update()
	{
		if (Input.GetMouseButtonDown(0)) {
			if (IsPointerOverUI())
				return;

			touchPosition = Input.mousePosition;
			touchPosition.z = CameraToTargetDistance;

			player.StartMove();

		} else if (Input.GetMouseButton(0)) {
			if (IsPointerOverUI())
				return;

			touchPosition = Input.mousePosition;
			touchPosition.z = CameraToTargetDistance;

		} else if (Input.GetMouseButtonUp(0)) {
			player.EndMove();
		}
	}


//プレイヤーが移動する前にコレを呼び、現在のタップ位置をワールド座標のターゲット位置に変換しておく。
	public void CheckTargetPosition()
	{
		targetPosition = mainCamera.ScreenToWorldPoint(touchPosition);
	}

//タップした位置にボタン等が被っていないかチェック。
	bool IsPointerOverUI()
	{
		pointer.position = Input.mousePosition;
		eventSystem.RaycastAll(pointer, isPointerOverUIResults);
		return 0 < isPointerOverUIResults.Count;
	}

//カメラをプレイヤーへ追従させている。
	void StartUpdateFollowCamera()
	{
		if (updateFollowCamera != null) {
			StopCoroutine(updateFollowCamera);
		}

		updateFollowCamera = StartCoroutine(UpdateFollowCamera());
	}

	IEnumerator UpdateFollowCamera()
	{
		while (true) {
			mainCameraTf.position = player.tf.position + FollowCameraRelativePosition;

			yield return FixedWait;
		}
	}
}

存分に御活用ください。

タイトルとURLをコピーしました