今回はUnityのラグドールを使ったヒットリアクションの作成をしていきたいと思います。
FPSのようなゲームで銃を撃って敵に当たった場合に当たった場所に力を加えて敵がダメージを受けるアニメーションをする機能です。
ラグドールを使わずにAnimatorControllerを使ってダメージを受けた場所によって細かくアニメーション遷移をさせるという方法もありますが、こちらの場合はその分だけアニメーションを作成する必要があります。
そこで、ダメージを受けた時にAnimatorコンポーネントを無効化し、ダメージアニメーションをラグドールを使って表現する方法にし、通常はAnimatorコンポーネントを使って制御をすると個々のダメージアニメーションを作らなくて済むので今回はこちらの機能を作っていきます。
ただこの方法を実現しようと思うと結構難しいです・・・・(^_^;)
今回作成した機能は以下のようなものになります。
敵の体の部位をマウスクリックしてダメージを与えた時にカメラからその部位の方向に力を加えています。
ダメージを与えた時は敵の動きを止めるようにしています。
そのまま敵が歩き続けるも可能ではありますが、ダメージを負ったまま動いて変な感じになるのでそこら辺は別途処理を細かく作る必要はありそうですね。
ラグドールを使ったヒットリアクションの概要
ダメージを受けた時にAnimatorコンポーネントを無効化してラグドールを機能させて、ダメージを受けた時のキャラクターの子要素のTransform全ての位置と角度を保持します。
数秒後にまたAnimatorコンポーネントを有効化しAnimatorControllerでの制御に戻し、Rigidbodyに力を加えて移動させたキャラクターの子要素のTransformの位置や角度を現在のTransformの位置や角度にしていきます。
ただここで問題になってくるのが、キャラクターの個々のボーンの位置と角度を各々で戻してしまうと、見た目的にメッシュが伸びたり変に曲がってしまうことです。
これが全然解消できません。(´Д`)
ネットで調べてみると、大学のコンピュータゲーム科の助教授の方が同じような機能を作っていたのでそちらを参考にさせて頂きました。
こちらではラグドールを使ってバタッと倒れたキャラクターの向いている方向で立ち上がりアニメーションを再生させ立ち上がるということをしています。
そこではラグドールからAnimatorへと遷移させる時に、キャラクターのHipsのボーンの時だけ位置を変更するようにして、回転の場合は全てのTransformを変更するようにしています。
こうすることで変なメッシュの伸びが大体なくなります。
キャラクターのボーンは階層になっているので個々のボーンで位置を変化させると破綻が出るので重心をHipsにして後のボーンは回転だけを変化させているのかもしれません。
ラグドールを使ったヒットリアクションの作成
それでは機能を作成していきましょう。
ゾンビキャラクターにラグドールを設定
まずは敵であるゾンビキャラクターを作り、ラグドールを設定します。
ゾンビキャラクターは
を使用させて頂きます。
また、この後取り付けるラグドールのボーンの設定の仕方によってもうまく動かない可能性も出てきます。
ゾンビキャラクターをヒエラルキー上に配置し、CharacterControllerを取り付け、AnimatorControllerを作成してAnimatorに設定します。
ここら辺は
を参考にして作成してみてください。
AnimatorControllerはZombieという名前にし、以下のようにIdle状態とWalk状態を作成し、アニメーションパラメータのSpeedの値で遷移するようにします。
IdleからWalkはSpeedが0.1以上、Walk→IdleはSpeedが0.1以下の時にします。
敵のAnimatorコンポーネントにZombieアニメーターコントローラー設定し、CharacterControllerコンポーネントを取り付けコライダを調整します。
次にラグドールを設定します。
ラグドールの設定方法は
を参照してラグドールを取り付けてください。
ラグドールを設定すると、敵キャラクターには以下のようにキャラクターの部位事にコライダが設定されます。
これで敵にAnimatorコンポーネントの設定、CharacterControllerコンポーネントの取り付けと設定、ラグドールの取り付けが出来ました。
ラグドールが設定されたボーンのレイヤーを変更する
ラグドールが取り付けられたので、次はキャラクターのボーンに新しくEnemyRagdollレイヤーを設定します。
何らかのゲームオブジェクトを選択し、インスペクタのLayerの部分を押し、Add Layerを選択し開いている部分に新しくEnemyRagdollと入力するとレイヤーが作成されます。
レイヤーを作成したらキャラクターのボーンの一番親を選択し、インスペクタのLayerをEnemyRagdollを選択し、新しく出てきたダイアログでYes, change childrenを選択し子要素のボーンも全てEnemyRagdollレイヤーにします。
最初のゾンビをキャラクターにした場合はボーンの親であるBip01を選択し、インスペクタのLayerをEnemyRagdollにし、出てきたダイアログでYes, change childrenを押します。
これで敵キャラクターの設定が出来ました。
ヒットリアクションをさせるスクリプトの作成
それではメイン機能であるヒットリアクションスクリプトを作成していきましょう。
新しくHitReactionスクリプトを作成し、敵キャラクターに取り付けます。
スクリプトが長いので分割して解説します。
クラス定義とフィールド部分
まずはクラス定義とフィールド部分を作成します。
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 | using UnityEngine; using System.Collections; using System.Collections.Generic; public class HitReaction : MonoBehaviour { // キャラクターのラグドールの状態 public enum RagdollState { Animated, SetUpForBlendToAnim, BlendToAnim } // キャラクターの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 { } } |
RagdollStateはキャラクターがAnimatorで制御しているのか、Animatorとラグドールのブレンドの設定なのか、ブレンド中なのかを表しています。
myBodyPartsリストはキャラクターの子要素のTransformを入れます。
bodyPositionsとbodyRotationsはラグドール状態になって力を受けた後の位置と回転をmyBodyPartsリストの順番に入れます。
timeToRecoveryはラグドール状態にしてからAnimatorとラグドールのブレンド設定を行うまでの時間を設定します。
powerToMoveは敵のダメージを受けた部位に加える力を設定します。
ragdollToAnimatorBlendTimeはラグドール状態からAnimatorで制御するまでの遷移時間を設定します。
ragdollingEndTimeはラグドール状態が終わった時間を入れます。
IsDamageプロパティは現在ダメージを受けた状態かどうかのプロパティを返します。
今回はRagdollState.Animated状態以外であればダメージを受けている状態として判断します。
StartメソッドとUpdateメソッド
次にStartメソッドとUpdateメソッドを作成します。
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 | void Start() { animator = GetComponent<Animator>(); characterController = GetComponent<CharacterController>(); // キャラクターの子要素からRigidbodyを取得 Rigidbody[] rigidbodys = GetComponentsInChildren<Rigidbody>(); // 取得したTransformをリストに追加 foreach (var rigid in rigidbodys) { myBodyParts.Add(rigid.transform); } SetKinematic(true); } void Update() { // ラグドールからAnimatorへの遷移の設定状態 if (ragdollState == RagdollState.SetUpForBlendToAnim) { ragdollingEndTime = Time.time; SetKinematic(true); animator.enabled = true; characterController.enabled = true; // ダメージ後の位置と回転を保持 SaveBodyTransform(); // 設定を繰り返し行わない為に状態を変更する ragdollState = RagdollState.BlendToAnim; } } |
Startメソッドでは最初にGetComponentsInChildrenを使ってキャラクター子要素のRigidbodyを全て取得し、そのRigidbodyのTransformをmyBodyPartsリストに追加していきます。
SetKinematicメソッドにtrueを引数として渡し、myBodyPartsに保持しているTransformを持つゲームオブジェクトのRigidbodyのIsKinematicを全てtrueにします。
これは初期状態ではRigidbodyのIsKinematicをtrueにしてラグドールが作用しないようにする為です。
SetKinematicメソッドは後で作成します。
UpdateメソッドではRagdollState.SetUpForBlendToAnim状態の時にragdollingEndTimeにUnity実行開始からの時間を入れ、SetKinematicメソッドにtrueを引数として与えて呼び出し、ラグドールを無効化します。
animatorを有効にし、AnimatorControllerで制御出来るようにします。
characterControllerを有効にし、敵のキャラクター移動用のコライダを有効にします。
SaveBodyTransformメソッドでmyBodyPartsリストに保持しているTransformのダメージ後の位置と回転をbodyPositionsとbodyRotationsリストにそれぞれ保持します。
RagdollState.BlendToAnim状態へと変更します。
ブレンド状態への変更メソッドChangeToBlendと位置と回転をAnimatorの制御に戻すLateUpdateメソッド内の処理
ラグドールとAnimatorのブレンド状態への変更メソッドとキャラクターの各部位の位置と回転をダメージ後からAnimator制御の位置と回転に変更する処理を行っているLateUpdateメソッドを作成します。
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 | // ラグドールからブレンド状態へと変更 public void ChangeToBlend() { ragdollState = RagdollState.SetUpForBlendToAnim; } void LateUpdate() { // ラグドールからAnimatorへの遷移状態 if (ragdollState == RagdollState.BlendToAnim) { // (現在の時間 - ラグドール開始時間)/ ラグドール遷移時間を計算し、現在の割合を計算する float ragdollBlendAmount = (Time.time - ragdollingEndTime) / ragdollToAnimatorBlendTime; // 0~1の間に補正する ragdollBlendAmount = Mathf.Clamp01(ragdollBlendAmount); // キャラクターのTransformリスト分繰り返し、Animator制御の状態に戻していく for (int i = 0; i < myBodyParts.Count; i++) { // TransformがキャラクターのボーンのHipsにあたる部分の時だけ位置を変更 if (myBodyParts[i] == animator.GetBoneTransform(HumanBodyBones.Hips)) { myBodyParts[i].position = Vector3.Lerp(bodyPositions[i], myBodyParts[i].position, ragdollBlendAmount); } // 全てのTransformの角度は変更する myBodyParts[i].rotation = Quaternion.Slerp(bodyRotations[i], myBodyParts[i].rotation, ragdollBlendAmount); } // ブレンドの割合が1になったらAnimator制御の状態 if (ragdollBlendAmount == 1) { InitializeRagdoll(); } } } |
ChangeToBlendメソッドは状態をブレンド状態に変更しているだけです。
LateUpdateメソッドはUpdateメソッドの後に実行されるメソッドです。
その中で状態がRagdollState.BlendToAnim状態であればラグドールで移動したキャラクターのボーンの位置と回転をAnimator制御の状態に戻していきます。
最初にTime.time – ragdollingEndTimeで現在の経過時間からラグドール状態が終わった時の時間を引いてラグドール終了からの時間を求めます。
それをragdollToAnimatorBlendTimeで割ることで、ラグドールからAnimatorへと遷移する時間の現在の割合を計算出来ます。
0~1の間の値が得られます(Time.time – ragdollingEndTimeがragdollToAnimatorBlendTimeを越えない限りは)。
Time.time – ragdollingEndTimeがragdollToAnimatorBlendTimeを越えるとブレンドしている時間を越えてしまうので、Mathf.Clamp01メソッドを使ってで0~1の値に制限を加えます。
for文を使ってmyBodyPartsリストの数分の繰り返しを行います。
現在見ている部位がHipsの時は現在のその部位の位置をVector3.Lerpを使ってダメージ後の状態からAnimatorの位置に徐々に変更します。
Vector3.Lerpは第1引数の値と第2引数の値の間を第3引数の割合で取得します。
第3引数の割合は0~1の間の値を設定します。
第1引数はダメージを受けた後の位置、第2引数がAnimatorで制御している部位の位置、第3引数が先ほど求めた0~1の値になります。
つまり最初は0に近い値がragdollBlendAmountに入っているので、第1引数のダメージを受けた後の位置に近く、時間経過とともにragdollBlendAmountは1に近くなり第2引数のAnimatorの位置に近い状態になります。
回転も同様にQuaternion.Slerpを使って行います。
ragdollBlendAmountが1になったらAnimatorに制御を移す為、InitializeRagdollメソッドを呼んで元のAnimatorの制御に戻します。
ダメージ処理メソッドTakeDamage
敵のダメージ処理であるTakeDamageメソッドを作成していきます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // ダメージ処理メソッド public void TakeDamage(int damage, RaycastHit hit) { // 連続ダメージに対応する為初期化 InitializeRagdoll(); // Invoke("ChangeToBlend", timeToRecovery); SetKinematic(false); characterController.enabled = false; animator.enabled = false; // クリックされた位置に力を加える hit.transform.GetComponent<Rigidbody>().AddForceAtPosition((hit.point - Camera.main.transform.position) * powerToMove, hit.point); } |
TakeDamageメソッドは他のスクリプトから呼び出し、引数でダメージ数とRaycastHit型の引数を受け取ります。
InitializeRagdollメソッドを呼んでダメージを受けた時は初期化処理を実行します。
Invokeを使ってtimeToRecovery秒後にChangeToBlendメソッドを実行するようにします。
これはtimeToRecovery秒後にラグドールからAnimatorへのブレンドの状態に遷移させたい為に遅延して実行させています。
ダメージを受けた時はラグドールを有効にしたいので、SetKinematicメソッドにfalseを引数として渡してRigidbody群のIsKinematicをfalseにし、ラグドールの制御に邪魔になるcharacterControllerとanimatorを無効化しています。
その後、引数で受け取ったhitから攻撃を受けた場所のRigidbodyを取得し、AddForceAtPositionを使ってカメラから見たダメージを受けた位置の方向からダメージを受けた場所に力を加えます。
引数でダメージ数を受け取っていますが、今回は使っていません。
Transform保存メソッド、IsKinematic変更メソッド、ヒットリアクション初期化メソッド
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 | // 現在のTransformの位置と回転をリストに追加 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) { t.GetComponent<Rigidbody>().isKinematic = flag; } } // 連続ダメージを受けた時に一旦リセットする public void InitializeRagdoll() { ragdollState = RagdollState.Animated; characterController.enabled = true; animator.enabled = true; SetKinematic(true); bodyPositions.Clear(); bodyRotations.Clear(); } |
SaveBodyTransformメソッドではmyBodyParts分の繰り返しを行い、部位ごとに位置と回転をリストに保持します。
SetKinematicメソッドはmyBodyPartsのRigidbodyのIsKinematicを部位ごとに引数で受け取ったflagに変更します。
InitializeRagdollメソッドは今回のヒットリアクションの初期化処理をしているので、最初にAnimatorで制御している状態に戻しています。
bodyPositionsとbodyRotationsはリストとして作成しているので、ダメージを受けた時に一旦クリアにしないとデータが蓄積されてしまう為Clearを使ってリストを空にしています。
これでHitReactionスクリプトが出来ました。
敵を攻撃するスクリプト
次に敵を攻撃するスクリプトMouseAttackスクリプトを作成し、MainCameraに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | 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"))) { hit.transform.GetComponentInParent<HitReaction>().TakeDamage(1, hit); } } } } |
マウスの左ボタンを押したらメインカメラからマウスカーソルの位置にレイを飛ばしRay型のrayに入れます。
Physics.Raycastを使ってそのレイ情報を使って1000m先まで線を引きレイヤーにEnemyRagdollが設定されたコライダ(ゲームオブジェクト)があったらその情報をhitに入れます。
今回の場合はラグドールに使用するコライダが設定されているゲームオブジェクトにEnemyRagdollレイヤーを設定したので、マウスの左ボタンを押したキャラクターの部位の情報がhitに入ります。
hitの親からHitReactionスクリプト(キャラクターのルートに取り付けているはず)を取得しTakeDamageメソッドにダメージ1とhitを渡して呼び出します。
これで敵にダメージを与える事が出来るようになりました。
敵キャラクターを動かしてヒットリアクションを確認
既に止まっている敵を攻撃してヒットリアクションを確認することが出来ますが、動いている敵を攻撃した時も確認したいので、敵を巡回させてみます。
敵が移動する地面はPlane等を使って作っておきます。
巡回ポイントの作成
まずは敵が移動する巡回ポイントを作成します。
ヒエラルキー上で右クリックからCreate Emptyを選択し空のゲームオブジェクトを作成してからF2キーを押して名前をMovePositionsとします。
MovePositionsのインスペクタのTransformのPositionのXYZを0、RotationのXYZを0、ScaleのXYZが1になっていることを確認します。
なっていない場合はTranformコンポーネント右上の3つの点の部分を押しResetを選択します。
MovePositionsを選択した状態で右クリックからCreate Emptyを選択し名前をPos1とします。
空のゲームオブジェクトだとシーンビューで位置を確認し辛いのでアイコンを設定します。
Pos1のインスペクタでアイコンを押し、黄色を選択します。
同様にPos2、Pos3、Pos4と巡回地点を作成します。
シーンビューでPos1~Pos4を巡回させたい位置に移動させます(PositionのY軸は0のまま)。
今回は以下のような位置に移動させました。
これで敵の巡回ポイントが出来ました。
敵キャラクター移動スクリプト
敵キャラクター移動スクリプトZombieスクリプトを作成し敵に取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Zombie : MonoBehaviour { private CharacterController characterController; private Animator animator; private HitReaction hitReaction; private Vector3 velocity; [SerializeField] private float walkSpeed = 1f; [SerializeField] private float rotationSpeed = 2f; [SerializeField] private Transform movePositions = null; private int destinationValue; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); hitReaction = GetComponent<HitReaction>(); } // Update is called once per frame void Update() { if(characterController.isGrounded) { velocity = Vector3.zero; // ダメージを受けている時は回転値と移動値を計算しない if (!hitReaction.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; } } } } |
movePositionsには先ほど作ったMovePositionsゲームオブジェクトをインスペクタで設定します。
destinationValueは現在の目的地の値です。
Startメソッドではコンポーネントの取得をしています。
UpdateメソッドではHitReactionスクリプトのIsDamageプロパティを参照し、ダメージを受けていない場合は
キャラクターの向きを目的地の方向へと徐々に回転させる。
キャラクターが向いている方向の速度を計算する。
ということをしています。
敵は常に歩いているアニメーションにする為、アニメーションパラメータのSpeedに1を設定します。
ヒットリアクション機能でCharacterControllerコンポーネントの無効と有効を切り替えている為、CharacterControllerコンポーネントが無効な時はCharacterControllerのMoveメソッドの処理は行わないようにします。
現在の目的地に着いたら次の目的地に変更しています。
これで全ての機能が出来ました。
終わりに
今回はマウスの左ボタンを押した時にその押したキャラクターの部位に力を加えるようにしていますが、TPSやFPS等でダメージを与える時も同じように作成出来ると思います(銃で撃ったコライダのRaycastHitを取得に変えるだけ)。
HitReactionスクリプトのインスペクタで設定する値によっては動きが不自然になることがあります。
例えばpowerToMoveの値を大きくするとそれだけ加える力が強くなるのでラグドール時に敵の体が遠くに飛びます。
そこから元に戻しますが、位置はHipsだけをうごかしているので、吸い寄せられるような動きになってしまいます。
なので力を大きくした場合はtimeToRecoveryの数値を小さくしたり、ragdollToAnimatorBlendTimeの値を小さくする必要があります。
もっと自然な感じにするにはラグドールを使わず細かいアニメーションクリップを作成し、AnimatorControllerで細かいアニメーション制御をするという方法の方がいいのかもしれません。
powerToMoveの値が小さい場合はその部位に与える力は弱くなり、どこの部位を攻撃しても同じようなヒットリアクションになってしまいます。
これはその部位にほとんど力を与えてないが、ラグドールのRigidbodyの重力の影響でバタッと倒れるようになるからです。