今回はUnityのラグドールを使って敵が倒れる時の制御を失ってバタッと倒れる状態を作成していきたいと思います。
文章だけでは分かり辛いと思いますが、ダークソウルで敵を倒した後に制御を失って吹っ飛び、主人公に引きずられるようになる状態です。
今回の機能を作成すると、
上のような感じで敵のHPが0以下になった時に敵がフニャフニャになって飛ばされます。
倒れるアニメーションを作成しているわけではなくラグドールで設定したRigidbody、Joint、コライダによって動きが作られています。
上のサンプルは遠距離武器ですが、近接武器の実行例も途中で紹介したいと思います。
今回の機能を作る流れ
今回の機能は二つのやり方で作ってみます。
ひとつめの機能作成の流れとしては、
となります。
もうひとつは
という流れになります。
二つ目の方法は一つ目のラグドールを施した敵をインスタンス化するのではなく、通常の敵にラグドールの設定をしておいて、死ぬまでラグドールの動作をさせないというだけです。
なので二つ目の説明はだいぶ省いております。
通常の敵とラグドールを施した敵を分ける方法
まずは通常の敵が倒れた時にラグドールを施した敵をインスタンス化する方法でやってみます。
敵にラグドールを設定しプレハブを作成する
まずは敵にラグドールを設定していきます。
アセットストアで検索窓にZombiと入力し、PricingでFree Assetsにチェックを入れ、PXLTIGERさんが作ったゾンビのモデルをインポートします。
Assets→Zombie→ModelFBX→Zombieを選択し、インスペクタのRigでAnimation TypeをHumanoidにします。
Zombieをヒエラルキー上にドラッグ&ドロップして配置します。
ヒエラルキー上で右クリック→3D Object→Ragdoll…を選択します(敵のモデルは選択しなくて大丈夫です)。
Create Ragdollウインドウが開きます。
Create Ragdollウインドウの設定に、ヒエラルキー上のZombieのボーンをドラッグ&ドロップして設定します。
その他のTotal Mass(全体の質量)、Strength(強さ)、Flip Forward(向きの反転)等は適宜変更してください。
今回は変更しません。
Createボタンを押すと敵の設定したボーンにRigidbodyやJoint、コライダが設定されるのがわかります。
ZombieゲームオブジェクトのインスペクタのAdd ComponentからCharacterControllerを取り付けコライダのサイズを調整します。
Animatorコンポーネントの歯車をクリックしRemove Componentを選択しAnimatorコンポーネントを削除します(Animatorが作用しているとラグドールが作用しない為)。
また名前をZombieからZombiePrefabと変更しておきます。
これでラグドールを設定した敵が出来たのでAssetsフォルダ内にドラッグ&ドロップしてプレハブにし、ヒエラルキー上の敵は削除します。
通常の敵キャラクターの作成
次は通常の歩いたり攻撃をしてきたりする敵キャラクターの作成をします。
今回はただ攻撃を受ける敵キャラクターにし、移動や攻撃の機能は取り付けません。
Assets→Zombie→ModelFBX→Zombieをヒエラルキー上にドラッグ&ドロップをします。
ZombieのインスペクタのLayerにEnemyを設定します(Enemyレイヤーがない場合は作成してください)。
Enemyレイヤーに変更する時にウインドウが出るので、No, this object onlyボタンを押し子要素のレイヤーは変更しないようにします。
インスペクタのAdd ComponentからCharacterControllerを取り付け、コライダのサイズをゾンビのモデルの大きさと合うように調整します。
新しくEnemyスクリプトを作成し取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy : MonoBehaviour { [SerializeField] private int hp = 3; private Animator animator; [SerializeField] private GameObject enemyPrefab; // ダメージを受けた方向 private Vector3 damageDirection; // ダメージを受けた時に飛ばす力 [SerializeField] private float powerToMove = 0.1f; // ラグドールを削除するまでの時間 [SerializeField] private float timeToDeleteEnemy = 5f; // Start is called before the first frame update void Start() { animator = GetComponent<Animator>(); } // ダメージ処理メソッド public void TakeDamage(int damage, Vector3 direction) { hp -= damage; animator.SetTrigger("Damage"); // 敵のHPがなくなったらゲームオブジェクトの削除と飛ばす方向を設定 if (hp <= 0) { damageDirection = direction; // 元の敵のゲームオブジェクトを削除 Destroy(gameObject); // 敵を倒した時にラグドールのプレハブをインスタンス化 var ins = Instantiate<GameObject>(enemyPrefab, transform.position, transform.rotation); // CharacterControllerのMoveメソッドで敵を飛ばす ins.GetComponent<CharacterController>().Move(damageDirection * powerToMove); // 一定時間経過したら削除 Destroy(ins, timeToDeleteEnemy); } public int GetHp() { return hp; } } |
enemyPrefabにはインスペクタで先ほど作ったラグドールを設定した敵のプレハブを設定します。
damageDirectionはダメージを受けた時の方向を入れます。
powerToMoveは敵のラグドールを飛ばす方向に加える力を指定します。
timeToDeleteEnemyはラグドールの敵をインスタンス化してから削除するまでの時間を設定します。
TakeDamageメソッドでは受けたダメージと方向を引数として受け取ります。
自身が保持するhpを減らしてhpが0以下になったらダメージを受けた方向を保持し、自身のゲームオブジェクトを削除します。
インスタンス化したラグドールの敵のCharacterControllerのMoveメソッドを使って攻撃を受けた方向に力を加えて動かします。
その後一定時間が経過したらラグドールの敵も削除します。
TakeDamageメソッドはメインカメラに取り付けるスクリプトから呼び出すことにします。
メインカメラに敵攻撃スクリプトを取り付ける
Main Cameraに新しくAttackスクリプトを取り付けます。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Attack : MonoBehaviour { [SerializeField] private float rayDistance = 1000f; // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, rayDistance, LayerMask.GetMask("Enemy"))) { Enemy enemy = hit.collider.GetComponent<Enemy>(); if (enemy.GetHp() > 0) { enemy.TakeDamage(damage: 1, direction: ray.direction); } } } } } |
rayDistanceはレイの距離を指定します。
Input.GetMouseButtonDown(0)でマウスの左ボタンが押されたかどうかを判定します。
Camera.main.ScreenPointToRay(Input.mousePosition)でメインカメラからマウスの位置にレイを飛ばしその情報をrayに保持します。
Physics.Raycastでrayの情報を元にEnemyレイヤーが設定されたゲームオブジェクトと接触したかどうかを調べ、接触した情報をhitに保持します。
接触した相手のEnemyスクリプトを取得し、GetHpメソッドでhpが0よりあるかどうかを調べて、hpがある場合はEnemyスクリプトのTakeDamageメソッドを呼び出し敵にダメージを与えます。
1 2 3 | enemy.TakeDamage(damage: 1, direction: ray.direction); |
ダメージ量に1を渡し、ダメージ方向にray.directionでカメラから敵に飛ばしたレイの方向の正規化された値を渡しています。
渡す引数の前にdamage:やdirection:という記述がありますが、これはEnemyスクリプトのTakeDamageの引数で割り当てられている名前を指定して、TakeDamageメソッドを呼び出す時にどんな引数を渡しているのかを分かりやすくしているだけです。
行っていることは以下と同じです。
1 2 3 | enemy.TakeDamage(1, ray.direction); |
これで機能が完成しました!
近接武器に対応する
ここまでで機能は出来ましたが、レイを使っての銃器を想定した遠距離武器での対応でした。
しかし近接武器でも同じような感じで対応出来ます。
以前作成した剣を持たせて敵を攻撃する記事で剣に敵との接触判定をするスクリプトを作りました。
そこで剣に取り付けたスクリプトでEnemyレイヤーが設定されている敵と剣が接触した時に敵のEnemyスクリプトのTakeDamageメソッドを呼び出します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using UnityEngine; using System.Collections; public class AttackSword : MonoBehaviour { void OnTriggerEnter(Collider col) { Debug.Log(col.gameObject.layer + " : " + LayerMask.NameToLayer("Enemy")); if (col.gameObject.layer == LayerMask.NameToLayer("Enemy")) { col.GetComponent<Enemy>().TakeDamage(1, transform.root.forward); } } } |
ダメージの方向はtransform.root.forwardで主人公の一番階層が上のZombieのゲームオブジェクトの前方を渡していますが、剣が接触した時の剣先の方向を与えるとそちらの方に敵を飛ばす事も出来ます。
近接攻撃を試すと
上のようになりました。
敵のPowerToMoveの値を0.01に設定して試しました。
敵のラグドール機能をオン・オフする方法
敵が倒れた時にラグドールの機能をオンにするやり方をみていきます。
敵をインスタンス化するかしないかの違いなので、説明はだいぶ省いています。
通常の敵にラグドールを設定する
今回の場合は敵が倒れた時にラグドールを設定した敵をインスタンス化するのではなく、移動や攻撃等をする敵にラグドールを設定しておき、HPが0以下になったらその機能をオンにするというやり方です。
なのでヒエラルキー上に敵を配置したらラグドールの設定をしてください。
ラグドールの設定をしたらボーン全てのLayerをEnemyRagdollに変更します。
今回のゾンビではBip 01の子要素全てのLayerをEnemyRagdollにします。
敵の移動等を行うスクリプトをEnemy3という名前で作成し敵に取り付けます。
スクリプトの名前はわたくしの都合でそうなっているだけなので別の名前にしてください。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Enemy3 : MonoBehaviour { [SerializeField] private int hp = 3; private CharacterController characterController; private Animator animator; // ダメージを受けた時に飛ばす力 [SerializeField] private float powerToMove = 100f; // ラグドールを削除するまでの時間 [SerializeField] private float timeToDeleteEnemy = 5f; // ラグドールに使用する体の部位の配列 private Rigidbody[] ragdollRigidbodyArray; // Start is called before the first frame update void Start() { characterController = GetComponent<CharacterController>(); animator = GetComponent<Animator>(); // ラグドールに使用する体の部位のRigidbody配列の生成 ragdollRigidbodyArray = transform.GetComponentsInChildren<Rigidbody>(); SetKinematic(true); } // キネマティックのオン・オフをするメソッド public void SetKinematic(bool flag) { foreach (var rigid in ragdollRigidbodyArray) { Debug.Log(rigid.name); if (rigid.tag == "EnemyRagdoll") { rigid.isKinematic = flag; } } } // ダメージ処理メソッド public void TakeDamage(int damage, Transform tra, Vector3 direction) { hp -= damage; animator.SetTrigger("Damage"); // 敵のHPがなくなったらゲームオブジェクトの削除と飛ばす方向を設定 if (hp <= 0) { // Rigidbodyのキネマティックをfalseにする SetKinematic(false); // ラグドールの妨げになるコンポーネントのオフ characterController.enabled = false; animator.enabled = false; // ダメージを受けた場所に力を加える tra.GetComponent<Rigidbody>().AddForce(direction * powerToMove, ForceMode.Impulse); // 一定時間経過したら削除 Destroy(gameObject, timeToDeleteEnemy); } } public int GetHp() { return hp; } } |
ragdollRigidbodyArrayはRigidbodyを持つTransformを入れます。
GetComponentsInChildrenを使ってRigidbodyを持つゲームオブジェクトを取得します。
SetKinematicメソッドはRigidbodyのIsKinematicのオン・オフをするメソッドで、ragdollRigidbodyArrayの全てのRigidbodyのIsKinematicを切り替えられるようにしています。
RigidbodyのIsKinematicがfalseの場合は外部から力を受けますが、trueの場合は外部からの力を受けません。
Startメソッドで引数にtrueを渡してSetKinematicメソッドを呼び出し、最初はラグドール機能が働かないようにします。
TakeDamageメソッドは敵がダメージを受けた時に外部から呼び出し、HPが0以下になったらSetKinematicを引数にfalseを渡して呼び出しラグドール機能をオンにします。
またCharacterControllerコンポーネントのコライダがラグドールに使用しているコライダと衝突してしまう為、無効化しています。
Layer Matrixを使ってCharacterControllerを設定しているゲームオブジェクトのレイヤーと、ラグドールに使用しているコライダのゲームオブジェクトのレイヤーの衝突をしないようにしておいても出来ます。
Animatorコンポーネントが有効だとラグドール機能が働かないのでこちらも無効化します。
これで敵の設定は終わりです。
遠距離攻撃の作成
遠距離攻撃を確認する為のスクリプトを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class Attack3 : MonoBehaviour { // Update is called once per frame void Update() { if (Input.GetMouseButtonDown(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit, 1000f, LayerMask.GetMask("EnemyRagdoll"))) { var ragdollEnemy = hit.collider.GetComponentInParent<Enemy3>(); if (ragdollEnemy.GetHp() > 0) { ragdollEnemy.TakeDamage(damage: 1, tra: hit.transform, direction: ray.direction); } } } } } |
マウスの左ボタンを押した時にレイを飛ばし、EnemyRagdollレイヤーのコライダと接触したらそのコライダのゲームオブジェクトのEnemy3スクリプトを取得し、TakeDamageメソッドを呼び出します。
第2引数ではヒットしたコライダのTransformを渡してそのTransformのゲームオブジェクトのRigidbodyに力を加えます。
近距離用の設定
近距離の場合は剣が複数のコライダと接触した時に連続で複数のコライダに力を加えてしまうので、一振りではひとつのコライダにだけ力を加えるようにします。
剣にAttackSword3スクリプトを作成し取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | using UnityEngine; using System.Collections; public class AttackSword3 : MonoBehaviour { private bool doDamage; void OnTriggerEnter(Collider col) { Debug.Log(col.gameObject.layer + " : " + LayerMask.NameToLayer("EnemyRagdoll")); if (col.gameObject.layer == LayerMask.NameToLayer("EnemyRagdoll") && !doDamage) { doDamage = true; Debug.Log(col.name); col.GetComponentInParent<Enemy3>().TakeDamage(1, col.transform, transform.root.forward); } } public void SetDoDamage(bool flag) { this.doDamage = flag; } } |
ダメージを与えた時にdoDamageにtrueを入れます。
一度ダメージを与えたらその攻撃が終わるまで他のコライダと接触してもダメージを与えないようにしています。
本来であればアニメーションイベントで剣を振り終わった時にAttackSword3スクリプトのSetDoDamageメソッドを使ってdoDamageをfalseにしたいところですが、今回はAnimatorControllerのビヘイビアを使ってSetDoDamageメソッドを呼びdoDamageをfalseにします。
ビヘイビアに関しては以下の記事を参照してください。
AnimatorControllerのAttack状態を選択し、新しくFlagTestビヘイビアを作り取り付けます。
名前が変ですが適した名前を付けてください。
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 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class FlagTest : StateMachineBehaviour { // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state //override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // //} // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks //override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // //} // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { animator.GetComponentInChildren<AttackSword3>().SetDoDamage(false); } // OnStateMove is called right after Animator.OnAnimatorMove() //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that processes and affects root motion //} // OnStateIK is called right after Animator.OnAnimatorIK() //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) //{ // // Implement code that sets up animation IK (inverse kinematics) //} } |
OnStateExitはアニメーションの状態が終わった時に呼ばれるので、そこでanimatorからAttackSword3スクリプトを取得しSetDoDamageメソッドを呼び出します。
これで機能が出来ました。
終わりに
ラグドールを使用するとリアルな敵が倒れる様子を作成する事が出来ますね。
手動でRigidbody、Joint、コライダをキャラクターのボーンに取り付ける事でも同じような機能を作成できますが、ラグドールを使用すると該当する場所にボーンを設定するだけで勝手にそれらを作成してくれるので便利ですね。
敵が倒れる時にラグドールの機能を使って倒し、その後再び起き上がってくる機能も移動キャラとラグドールを一緒にしておけば実現が可能かもしれません。
ただやろうと思っても結構難しいです。(^_^;)