3Dシューティング風味なエンドレスランナーの空中移動周りの実装。
地上を捨てる事で、接地判定しなくて良いので楽です。
移動可能範囲の固定は、PlayerのZ座標のみ適用した中心座標から、sqrMagnitudeで判定。
進行方向を、前述の中心座標へのベクトルで上書き、みたいな感じで適当に追加すればOk。
Unityちゃん3Dモデル等: © UTJ/UCL
サンプル動画
*音なし
舞空術モーション
- ライセンス: 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のインポート&設定
- Projectウィンドウに、対象FBXをドラッグ&ドロップしてインポート。
- Projectウィンドウ -> 対象FBXを選択、
- インスペクター -> Rigタブを選択。
- Animation Type: Humanoidに変更。
- Apply
Animatorへのモーションの追加と基本モーションの設定
- Animatorタブを選択し、Animatorウィンドウを開く。*
*(開いていない場合は、上部メニュー -> Window -> Animation -> Animatorを選択) - Animatorウィンドウに、モーション用FBXをドラッグ&ドロップ。
- Entryステートを右クリック -> Set StateMachine Default State -> 「metarig|Idle」を選択。
モーションの遷移スピードの調整
StateMachineのSpeedを0.25等に調整しておくと、遷移スピードも緩やかになる。
オーラの作り方
- 魔法エフェクト系のアセットパックから、オーラっぽい物をピックアップ。
(サンプルでは、[Archanor VFX] MagicArsenalのLightSprayプレハブを使用) - ParticleSystemの設定を変更。
- 【ベース部分】Simulation Space: Localに設定し、身体に纏わせる。
- 【オーラの尾(Trail)部分】ベースのParticleSystemを複製し、Simulation Space: Worldに設定。
- 【色の濃さ】Color over Lifetime -> Gradient Editor -> 上側のタブをクリックし、Alpha値を変更。
コード
Player
- 新規スクリプト「Player」に、以下のコードをコピペ。
- 空のオブジェクト「Player」を作成し、Playerスクリプトをアタッチ。
- Rigidbodyコンポーネントをアタッチし、UseGravityを無効化しておく。
- Player下に、任意の3Dモデルを設置。
- 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
- 新規スクリプト「GameManager」に、以下のコードをコピペ。
- 空のオブジェクト「GameManager」を作成し、GameManagerスクリプトをアタッチ。
- インスペクターから、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;
}
}
}
存分に御活用ください。