今回はUnityのラグドール機能を使って銃などのダメージを受けてバタッと倒れたキャラクターがアニメーターコントローラーの制御に戻って立ち上がる機能を作成したいと思います。
前の記事でラグドールを使ったヒットリアクション機能を作成しましたが、そちらで作成した敵の移動機能やスクリプトの処理も使っているのでそちらの記事も参照してください。
今回の機能を作成すると以下のようなものが出来上がります。
上の動画ではダメージを受けたら即座にラグドールを使ってダウンしていますが、ヒットリアクションと組み合わせた機能は最後の方で紹介します。
ラグドールでダウン後の起き上り機能の概要
ラグドールを使ったダウンのアニメーションは攻撃を与えた時にその部位のコライダに力を加え、CharacterControllerやAnimatorコンポーネントを無効化し、ラグドールの機能だけで倒れるようにします。
起き上りの機能はラグドールで倒れたキャラクターがどの向きを向いているかを調べ、その状態に近いアニメーションを再生させることで実現させることにします。
ただ、ラグドールでダウンしたキャラクターの位置や角度はダウンする前の位置や角度から動いたり、ダウン後のキャラクターとAnimatorControllerで設定しているアニメーションの位置と角度が合いません。
つまり、単純にCharacterControllerとAnimatorを再度有効にするだけでは実現出来ません。
なのでラグドールでダウンしたキャラクターの両足の位置の間の位置の計算とキャラクターが倒れた方向からキャラクターの角度を計算し、AnimatorControllerの制御に戻す前に位置と角度を変更しておきます。
細かい処理はスクリプトの解説部分を見て頂くとわかると思います。
敵キャラクターにラグドールを取り付ける
敵キャラクターにはCharacterControllerの取り付けとコライダの設定がされており、ラグドールが取り付けられている必要があります。
の記事を参照して設定してください。
ダウン後の立ち上がりアニメーション
ダウン後の立ち上がりアニメーションは各自用意してください。(^_^;)
ヒットリアクション機能で参考にした方のプロジェクトで使用していたアニメーションは著作権がどうなっているのかわからないので、そちらは使わずわたくしは自作しました。
キャラクターの基点は足元にして作成し、
アニメーションはキャラクターのダウン後の状態と立ち上がった状態を回転して作り、その間のフレームを手と足のIKを動かして立ち上がるようなアニメーションに仕上げました。
上はうつ伏せからの立ち上がりアニメーションですが、その他横向きからの立ち上がりアニメーション、仰向けからの立ち上がりアニメーションを作成しておきます。
横向きからの立ち上がりアニメーションは左右の違いはUnityのMirrorで対応するので左向きか右向きかのアニメーションをひとつだけ作ります。
どのアニメーションでも同じ方向を向いて立ち上がるようにしておきます。
Unityでのアニメーションの設定
作成したアニメーションをUnityに取り込み、Animation TypeをHumanoid型にしたらアニメーションの設定を行います。
以下はGetUpFromBackアニメーションの設定です。
Root Transform RotationとRoot Transform Position(Y)はBake Into Poseにチェックを入れ、Based UponはOriginalにし、Root Transform Position(XZ)はBake Into Poseのチェックを外し、Based UponはCenter Of Massにします。
Root Transform Position(XZ)のBake Into Poseのチェックを外しているのは立ち上がりアニメーション後にスーッと元のキャラクターの位置に戻るのを避ける為です。
Based UponをOriginalにして足元を基点にするとスクリプトの記述が楽になるのですが、これだとラグドールからAnimatorControllerを制御を移す時の回転のズレが大きくなる為Center Of Massにしています。
アニメーションの作り方やUnityでのアニメーションの設定によってはスクリプトの処理を変える必要があったり、処理がうまくいかないこともあります。
アニメーションクリップの設定に関しては
や、Unityマニュアルの
も参照してみてください。
AnimatorControllerの状態と遷移の作成
敵のAnimatorに設定するAnimatorControllerの状態と遷移は以下のように作ります。
アニメーションパラメータ―はfloat型のSpeed、Trigger型のGetUpFromBack、GetUpFromProne、GetUpFromLeftSide、GetUpFromRightを作ります。
GetUpFromBackは仰向け立ち上がり、GetUpFromProneはうつ伏せからの立ち上がり、GetUpFromLeftSideは右側で横たわり左側に立ち上がり、GetUpFromRightは左側に横たわり右側に立ち上がりの条件です。
Idle→WalkはSpeedが0.1以上、Walk→IdleはSpeedが0.1以下
IdleやWalkから対応するアニメーションパラメータ―がトリガーされたら遷移し、それぞれの状態からIdleへと遷移します。
Idleへと遷移する時の条件はHas Exit Timeにチェックを入れ、時間が経過したらIdleへと遷移するようにします。
例えばGetUpFromBack→Idleのインスペクタは以下のようになります。
スクリプトの作成
スクリプトを作成していきます。
敵の移動スクリプト
敵の移動スクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour { private CharacterController characterController; private Animator animator; private GetUp getUp; private Vector3 velocity; [SerializeField] private float walkSpeed = 1f; [SerializeField] private float rotationSpeed = 2f; [SerializeField] private Transform movePositions = null; private int destinationValue; public int Hp { get; set; } = 100; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); getUp = GetComponent<GetUp>(); } // Update is called once per frame void Update() { if (!getUp.IsDamage) { if (characterController.isGrounded) { velocity = Vector3.zero; // ダメージを受けている時は回転値と移動値を計算しない if (!getUp.IsDamage) { transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(movePositions.GetChild(destinationValue).position - transform.position), rotationSpeed * Time.deltaTime); animator.SetFloat("Speed", 1f); velocity += transform.forward * walkSpeed; } } velocity.y += Physics.gravity.y * Time.deltaTime; // CharacterControllerが有効な時だけ処理 if (characterController.enabled) { characterController.Move(velocity * Time.deltaTime); } // 目的地に到着したら次の目的地を設定 if (Vector3.Distance(transform.position, movePositions.GetChild(destinationValue).position) < 0.5f) { destinationValue++; if (destinationValue >= movePositions.childCount) { destinationValue = 0; } } } } } |
移動処理に関してはヒットアクション機能の時と変わらないので説明を省きます。
Hpプロパティを用意します。
GetUpスクリプトが立ち上がり機能のスクリプトでIsDamageプロパティでダメージ状態がどうかを判定し、ダメージを受けていなければ移動させています。
立ち上がり機能のスクリプト作成
立ち上がり機能のスクリプトGetUpを作成し、敵に取り付けます。
ラグドール機能でダウンしてから立ち上がる機能のスクリプトを作成していきますが、ヒットリアクション機能のHitReactionスクリプトと同じ部分の説明は排除しています。
フィールド宣言とStartメソッド
まずはフィールド宣言部分とStartメソッドを見ていきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | using UnityEngine; using System.Collections; using System.Collections.Generic; public class GetUp : MonoBehaviour { // キャラクターのラグドールの状態 public enum RagdollState { Animated, SetUpForBlendToAnim, BlendToAnim, Ragdoll, GetUp, } // キャラクターのTransform情報リスト [SerializeField] private List<Transform> myBodyParts = new List<Transform>(); // キャラクターがダメージを受けた後の位置のリスト private List<Vector3> bodyPositions = new List<Vector3>(); // キャラクターがダメージを受けた後の回転のリスト private List<Quaternion> bodyRotations = new List<Quaternion>(); // ラグドールからアニメーションとのブレンドに戻すまでの時間 [SerializeField] private float timeToRecovery = 0.02f; // Rigidbodyに加える力 [SerializeField] private float powerToMove = 1000f; private CharacterController characterController; // キャラクターの状態 [SerializeField] private RagdollState ragdollState = RagdollState.Animated; // ラグドールからAnimatorに遷移する時間 [SerializeField] private float ragdollToAnimatorBlendTime = 0.4f; // ラグドールが終わった時間 private float ragdollingEndTime = 0f; private Animator animator; // ラグドールがダメージを受けている状態かどうかのプロパティ public bool IsDamage { get { return ragdollState != RagdollState.Animated; } set { } } public bool IsRagdoll { get { return (ragdollState == RagdollState.Ragdoll); } set { } } // 倒れた時の状態を確認する為のレイを飛ばす位置 [SerializeField] private Transform rayPosition = null; // 前後に飛ばすレイの長さ [SerializeField] private float lengthOfFrontAndBackRay = 1f; // 左右に飛ばすレイの長さ [SerializeField] private float lengthOfLeftAndRightRay = 0.3f; private Zombie zombie; // ラグドールで倒れている時間 [SerializeField] private float timeToGetUp = 3f; void Start() { animator = GetComponent<Animator>(); characterController = GetComponent<CharacterController>(); zombie = GetComponent<Zombie>(); // キャラクターの子要素からRigidbodyを取得 var transforms = GetComponentsInChildren<Transform>(); // 取得したTransformをリストに追加 foreach (var t in transforms) { myBodyParts.Add(t.transform); } SetKinematic(true); } |
RagdollStateにGetUpとRagdollを追加します。
IsRagdollプロパティは現在ラグドールで倒れている状態かどうかを表します。
ラグドールでダウンする時はダメージを与えないようにする為、プロパティで確認出来るようにしています。
rayPositionは倒れたキャラクターの向いている方向を確認する為のレイを飛ばす位置を設定します。
今回の場合はゾンビのBip01 Pelvisというボーンを設定します。
lengthOfFrontAndBackRayはレイを飛ばす位置から前後に飛ばすレイの長さを設定します。
これはうつ伏せか仰向けに倒れたかどうかを確認する為に腰(Bip01 Pelvis)の位置から前後にレイを飛ばしますが、そのレイの長さです。
lengthOfLeftAndRightRayはレイを飛ばす位置から左右に飛ばすレイの長さを設定します。
これは横向きに倒れたかどうかを確認する為に腰(Bip01 Pelvis)の位置から横にレイを飛ばしますが、そのレイの長さです。
timeToGetUpはラグドール状態になってからAnimatorControllerの制御に戻すまでの時間を設定します。
Updateメソッド
Updateメソッドではキャラクターの状態に応じた処理をしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | void Update() { // ラグドールからAnimatorへの遷移の設定状態 if (ragdollState == RagdollState.SetUpForBlendToAnim) { ragdollingEndTime = Time.time; SetKinematic(true); animator.enabled = true; characterController.enabled = true; // ダメージ後の位置と回転を保持 SaveBodyTransform(); // 設定を繰り返し行わない為に状態を変更する ragdollState = RagdollState.BlendToAnim; } else if (ragdollState == RagdollState.GetUp) { if(animator.GetCurrentAnimatorStateInfo(0).IsName("Idle")) { ragdollState = RagdollState.Animated; } } } |
RagdollState.GetUp状態の時にAnimatorControllerの状態がIdle状態になったらragdollStateをRagdollState.Animatedに変更しAnimatorControllerで制御している状態とします。
ラグドール状態から立ち上がり状態へと変更するメソッドの作成
ラグドール状態から立ち上がり状態へと変更するメソッドを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | // ラグドールから立ち上がり状態へと変更 public void ChangeToGetUp() { SetKinematic(true); animator.enabled = true; characterController.enabled = true; ragdollingEndTime = Time.time; // キャラクターの位置をラグドールで飛んだ後の右足と左足の平均の位置を計算 var averageFootPos = 0.5f * (animator.GetBoneTransform(HumanBodyBones.LeftFoot).position + animator.GetBoneTransform(HumanBodyBones.RightFoot).position); RaycastHit hit; // 地面の位置を取得し、キャラクター位置を変更 if (Physics.Linecast(averageFootPos + Vector3.up, averageFootPos - Vector3.up, out hit, LayerMask.GetMask("Field"))) { // アニメーションのXZ位置の分の補正値を計算 var animationPositionDifference = animator.GetBoneTransform(HumanBodyBones.Spine).position - averageFootPos; // 足の位置+補正値の場所にキャラクターを移動 transform.position = hit.point + new Vector3(animationPositionDifference.x, hit.point.y, animationPositionDifference.z); //transform.position = hit.point; } // キャラクターの向きを確認 Debug.DrawLine(rayPosition.position, new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z), Color.white); // キャラクターの向きで起き上がりアニメーションを変更する // 左側が地面なので右側から起き上る if (Physics.Linecast(rayPosition.position, rayPosition.position + rayPosition.forward * lengthOfLeftAndRightRay, LayerMask.GetMask("Field"))) { transform.rotation = Quaternion.LookRotation(new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z) - rayPosition.position); // アニメーション位置を時間で指定し再生 animator.CrossFadeInFixedTime("GetUpFromRightSide", 0f); // 右側が地面なので左側から起き上る } else if (Physics.Linecast(rayPosition.position, rayPosition.position - rayPosition.forward * lengthOfLeftAndRightRay, LayerMask.GetMask("Field"))) { transform.rotation = Quaternion.LookRotation(new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z) - rayPosition.position); animator.CrossFadeInFixedTime("GetUpFromLeftSide", 0f); } else { // うつ伏せ if (Physics.Linecast(rayPosition.position, rayPosition.position + rayPosition.up * lengthOfFrontAndBackRay, LayerMask.GetMask("Field"))) { // キャラクターの向きを変更 transform.rotation = Quaternion.LookRotation(rayPosition.position - new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z)); animator.CrossFadeInFixedTime("GetUpFromProne", 0f); // 仰向け } else { // キャラクターの向きを変更 transform.rotation = Quaternion.LookRotation(new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z) - rayPosition.position); animator.CrossFadeInFixedTime("GetUpFromBack", 0f); } } ragdollState = RagdollState.GetUp; } |
ChangeToGetUpメソッドはラグドールからAnimatorControllerでの制御に戻す時に呼ぶので、最初にSetKinematicメソッドを呼んでラグドールを無効にし、CharacterController、Animatorを有効にして通常の制御に戻します。
Animatorの右足と左足のボーンを足して0.5を掛けてその間の位置を計算し、averageFootPosに入れます。
足の間の1m上から足の間の1m下までレイを飛ばし、Fieldレイヤーを設定した地面にぶつかった位置の情報をhitに入れます。
ここで、地面にするゲームオブジェクトにはFieldレイヤーが設定されている必要があります。
キャラクターのアニメーションの設定でRoot Transform Position(XZ)をCenter Of Massにして基点をBip01 Pelvisのボーン(中心)にしたのでその為の調整をします。
1 2 3 | var animationPositionDifference = animator.GetBoneTransform(HumanBodyBones.Spine).position - averageFootPos |
Humanoid型のSpineに設定しているBip01 Spineボーン(わたくしの場合はBip01 Pelvisを設定してしまいましたが)の位置から足の間の位置を引いたベクトルを計算します。
キャラクターの位置をhit.point(足の間の地面の位置)にオフセット位置を足した位置にします。
Root Transform Position(XZ)をOriginalにしていた場合はhit.pointのままでいけると思います(アニメーションの作りによりけり)。
ここで位置を変更しているのは、ラグドールで飛ばされたキャラクターと、ラグドールになる前のキャラクターの位置を合わせる為です。
次にキャラクターがラグドールで倒れた後の体の向きに応じてAnimatorControllerで再生するアニメーションを変更する処理です。
まずは横向きに倒れたかどうかを判定します。
1 2 3 4 5 6 7 | if (Physics.Linecast(rayPosition.position, rayPosition.position + rayPosition.forward * lengthOfLeftAndRightRay, LayerMask.GetMask("Field"))) { transform.rotation = Quaternion.LookRotation(new Vector3(averageFootPos.x, rayPosition.position.y, averageFootPos.z) - rayPosition.position); // アニメーション位置を時間で指定し再生 animator.CrossFadeInFixedTime("GetUpFromRightSide", 0f); // 右側が地面なので左側から起き上る |
rayPositionがレイを飛ばす位置でインスペクタで敵(ゾンビ)のBip01 Pelvisのボーンを設定します。
Bip01 Pelvisのボーンのローカルの向きで体の横に向いている矢印の色を確認します。
上のようにゾンビのBip01 Pelvisのボーンの横方向は青色の矢印で、これはローカルのZ軸を表しています。
Z軸はtransform.forwardで表せますのでPhysics.Linecastを使てレイの位置からBip01 PelvisボーンのZ軸(transform.forward)の方向にlengthOfLeftAndRightRayの長さのレイを飛ばしFieldレイヤーのゲームオブジェクト(地面)と接触したかどうかを判定します。
この時はキャラクターの左側が地面の方向です。
キャラクターが横向きの時はキャラクターの足元の向きの方向にキャラクターの向きを合わせる為、キャラクターの足元からレイの位置を引いてキャラクターの足元の方向を求め、その方向にキャラクターを向けています。
足の間の位置のYをrayPositionにしているのは高さをレイの位置と合わせる為です。
キャラクターのアニメーションを再生する時はアニメーションパラメータをトリガーするのではなく
1 2 3 | animator.CrossFadeInFixedTime("GetUpFromRightSide", 0f); |
と直接AnimatorControllerの状態の指定と再生する位置を指定してアニメーションを再生しています。
これはAnimatorControllerのアニメーションパラメータをトリガーするやり方だと元の状態からGetUpFromRightSideの状態へのブレンドが再生されてしまう為です(うまいやり方があるかもしれませんが)。
なのでanimator.CrossFadeInFixedTimeを使って即座に立ち上がりアニメーションを再生させるようにしています。
AnimatorのCrossFadeInFixedTimeを使うとAnimatorに設定したAnimatorControllerの第1引数で指定した状態の第2引数で指定した秒数の位置からアニメーションを再生してくれます。
キャラクターの右側が地面の時も同じやり方です。
横向きと判定されなかった時はうつ伏せから立ち上がりか仰向けから立ち上がりのアニメーションを再生します。
敵のBip01 Pelvisのボーンの前方は緑矢印でtransform.upで表せるので、レイの位置からtransform.upの方向にlengthOfFrontAndBackRayの長さのレイを飛ばし地面とぶつかるかどうかを調べます。
前方に地面がある場合はうつ伏せで倒れたのでキャラクターの向きは足元の位置からレイを飛ばしている位置の方向に合わせます。
仰向け(orどの条件も満たさなかった場合)の場合はキャラクターの向きはレイを飛ばしている位置から足元の方向の向きに合わせます。
どちらも起き上がった時のキャラクターの方向を計算し、その方向をキャラクターが向くようにしています。
今回はキャラクターのボーンから左右と前後にレイを飛ばして地面に対してどちらを向いているかを計算し、うつ伏せ、仰向け、横向きに応じてキャラクターの向くべき方向を計算しています。
たまにラグドール→AnimatorControllerへのアニメーションの繋がりがおかしい時もあるようなのでここら辺の判別方法はもっと良い方法を探す必要があるかもしれません。
ダメージ処理メソッドの作成
ダメージ処理メソッドTakeDamageメソッドを作成していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // ダメージ処理メソッド public void TakeDamage(int damage, RaycastHit hit) { // 連続ダメージに対応する為初期化 InitializeRagdoll(); zombie.Hp--; ragdollState = RagdollState.Ragdoll; //if (zombie.Hp % 10 != 0) { // Invoke("ChangeToBlend", timeToRecovery); //} else { // Invoke("ChangeToGetUp", timeToGetUp); //} Invoke("ChangeToGetUp", timeToGetUp); SetKinematic(false); characterController.enabled = false; animator.enabled = false; // クリックされた位置に力を加える hit.transform.GetComponent<Rigidbody>().AddForceAtPosition((hit.point - Camera.main.transform.position) * powerToMove, hit.point); } |
ダメージを受けたらChangeToGetUpメソッドをtimeToGetUp秒後に実行するようにします。
ヒットリアクション機能と同時に使う場合は通常のダメージはChangeToBlendメソッドを実行し、敵のHPが10で割り切れる場合はChangeToGetUpメソッドを呼び出すようにします。
SaveBodyTransformとSetKinematicとInitializeRagdollメソッド
Transformの保存メソッドSaveBodyTransform、RigidbodyのIsKinematicの切り替えメソッドSetKinematicとラグドールの初期化メソッドInitializeRagdollメソッドはヒットリアクション機能の時と同じなので説明は省きます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | public void SaveBodyTransform() { for (int i = 0; i < myBodyParts.Count; i++) { bodyPositions.Add(myBodyParts[i].position); bodyRotations.Add(myBodyParts[i].rotation); } } // RigidbodyのIsKinematicの切り替え public void SetKinematic(bool flag) { foreach (var t in myBodyParts) { if (t.GetComponent<Rigidbody>()) { t.GetComponent<Rigidbody>().isKinematic = flag; } } } // 連続ダメージを受けた時に一旦リセットする public void InitializeRagdoll() { ragdollState = RagdollState.Animated; characterController.enabled = true; animator.enabled = true; SetKinematic(true); bodyPositions.Clear(); bodyRotations.Clear(); } |
敵攻撃スクリプト
敵を攻撃するスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class MouseAttack : MonoBehaviour { // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { var ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 1000f, LayerMask.GetMask("EnemyRagdoll"))) { var getUp = hit.transform.GetComponentInParent<GetUp>(); if(!getUp.IsRagdoll) hit.transform.GetComponentInParent<GetUp>().TakeDamage(1, hit); } } } } |
マウスの左ボタンを押した位置にカメラからレイを飛ばし、EnemyRagdollレイヤーが設定されたコライダの情報を取得し、GetUpスクリプトのTakeDamageメソッドを呼び出します。
TakeDamageメソッドにはダメージ数とRaycastHit情報を渡します。
これで機能が完成しました。
10ダメージ毎にキャラクターがラグドールの機能でバタッと倒れるようにすると以下のようになります。
終わりに
ラグドールを使ったヒットリアクション機能を作成したのでそれに付随したラグドールで倒れたキャラクターが立ちあがるまでの機能を作成しました。
ラグドール→AnimatorControllerの立ち上がりアニメーションへの遷移はパッと切り替わってしまう感じがしますが、それなりな感じのものは出来たのではないでしょうか。
ラグドールからAnimatorControllerへと切り替える時間timeToGetUpを短くしたり、
ラグドールで倒れる時に加える力を強くして敵が吹っ飛ばされるようにするとうまく動作しない可能性もあります。
そういった不安定な要素を排除したい場合は細かい敵のダメージアニメーションを全部作りAnimatorControllerで全て制御するようにした方がいいかもしれません。(^_^;)