今回やる事は、Unityのアクションゲーム等で攻撃のアニメーションスピードが速すぎて剣と敵との当たり判定がスルーされてしまう問題の対応です。
この問題は攻撃アニメーションのスピードを落とせば解消されますが、実現したかった速い攻撃を捨てることになります。
そこで、なるべく当たり判定がされるように自身でメッシュを生成し、それをコライダとして使って対応してみたいと思います。
メッシュの生成については
を参照してください。
なにも対応していない場合は
↑のように武器が当たっているように見えて当たり判定がスルーされています。
これを今回作成する機能を適用すれば
↑のように攻撃モーションが速くても当たり判定がされます。
当たり判定がスル―される理由と対応
武器と敵との当たり判定はコライダが他のコライダと接触した時にOnTriggerEnterで判定していますが、フレーム毎の武器の位置で武器のコライダが敵のコライダと接触しているかを判定する為、攻撃のモーションが速いと前フレームと現フレームでの位置が大きく変化する為、当たり判定がされない事になります。
そこで、前フレームでの剣の位置と現フレームでの剣の位置の間のメッシュを生成し、それをMesh Colliderに設定して当たり判定に使用する事にします。
その為、攻撃モーションが速くても前回の武器の位置と現在の武器の位置の間に当たり判定を作る為、当たり判定の範囲が広くなります。
作成するメッシュコライダのイメージとしては↑のような感じで攻撃のモーションの間に作成します。
前フレームでの剣の位置や現フレームでの剣の位置を使ってメッシュを生成する方法は
でやっていますが、今回はそこで作ったメッシュ生成より簡単です。(^^)/
こちらの記事を先に公開するべきだったかもしれませんね・・・・(^_^;)
敵の設定
まずは敵キャラクターをヒエラルキー上に配置し、ボーンの子要素に空のゲームオブジェクトを作成し当たり判定用のコライダを設定します。
↑のように対応するボーンの子要素に空のゲームオブジェクトを作成し、それぞれコライダを設定します。
ここら辺は
を参照してください。
当たり判定を設定した敵は、
↑のようになりました。
敵キャラクター移動用のCharacterControllerも取り付けてますが、今回は使用しません。
またそれぞれの当たり判定用のゲームオブジェクトのTagにEnemeyを新しく作り設定し、コライダを取り付けIs Triggerにチェックを入れて物理的に当たらないようにします。
DamageScriptスクリプトを新しく作成しそれぞれの当たり判定用ゲームオブジェクトに設定しています。
DamageScriptスクリプトの解説
ダメージを受けた時の処理を行うDamageScriptスクリプトの解説をします。
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 | using UnityEngine; using System.Collections; public class DamageScript : MonoBehaviour { // ダメージ表示用 public GameObject damageUI; // ダメージを受けてからの無敵時間 public float invincibleTime; // ダメージを受けたかどうか private bool isDamaged; // ダメージを受けてからの時間 public float elapsedTime; void Start() { Reset (); } void Update() { if (isDamaged) { elapsedTime += Time.deltaTime; if (elapsedTime >= invincibleTime) { Reset (); } } } public void Damage(Collider col) { if (!isDamaged) { GameObject.Instantiate (damageUI, col.bounds.center - Camera.main.transform.forward * 0.5f, col.transform.rotation); Reset (isDamaged : true); Debug.Log ("攻撃を当てたゲームオブジェクト名: " + gameObject.name); } } // ダメージ受付処理 void Reset(bool isDamaged = false) { this.isDamaged = isDamaged; elapsedTime = 0f; } } |
damageUIはダメージを表示するUIプレハブを設定し、攻撃を受けた時にそのUIをインスタンス化し表示します。
ここで使用するダメージUIは
で作成したダメージ用UIプレハブです。
invincibleTimeはダメージを受けた時に次の攻撃を無視する時間を設定します。
isDamagedはダメージを受けた状態かどうか、elapsedTimeはダメージを受けてからの時間経過です。
これらのフィールドは連続でダメージを計算するのを回避する為に作成しました。
前の剣の位置と現在の剣の位置の間に当たり判定を作成するので、剣が移動するたびその当たり判定メッシュを生成します。
その為、その新しく生成したメッシュが敵のコライダと接触するたびにダメージを与えてしまいます。
そこで一旦ダメージを受けたらinvincibleTimeを超えない限りはその場所にダメージを与えないようにします。
Updateメソッドではダメージ状態の時に経過時間を計算し、invincibleTimeを超えたらダメージを受ける状態に変更します。
Damageメソッドではダメージを受けていない状態の時だけダメージ用UIの表示を行っています(実際に敵のHPを減らす処理等は今回の記事とは関係ない為記述していません)。
Resetメソッドはダメージのリセット処理です。
引数でbool値が渡ってこなかった時はfalseに初期化しています。
DamageScriptスクリプトで一旦ダメージを受けてから次に攻撃を受けるまでの無敵時間を作りましたが、主人公が連続攻撃を可能とする場合、次の攻撃モーションの時にも最初のモーション時の無敵時間が残っています。
その為、無敵時間が長いと次のモーションでの攻撃も無視されてしまう事になります。
無敵時間があるのは同じ攻撃時の連続判定を避ける為に取り付けたので、次のモーション時にはリセットするのが自然です。
その対処は主人公キャラクターの処理でやりますので一旦置いておきます。
剣の設定
次に主人公キャラクターが持つ剣の設定をしていきます。
剣にはCapsule Colliderを取り付けIs Triggerにチェックを入れます。
Rigidbodyを取り付けIs Kinematicにチェックを入れます(RigidbodyはOnTriggerEnterを発生させる為設定)。
Attackスクリプトは剣が敵のコライダと接触した時に、敵に設定しているDamageScriptスクリプトのDamageメソッドを呼び出す為のものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 | using UnityEngine; using System.Collections; public class Attack : MonoBehaviour { void OnTriggerEnter(Collider col) { if (col.tag == "Enemy") { col.GetComponent <DamageScript>().Damage (col); } } } |
剣のコライダが敵の当たり判定のゲームオブジェクトのコライダと接触した時にDamageScriptのDamageメソッドを呼び出しています。
前の剣の位置と現在の剣の位置でメッシュを生成しますが、平面のメッシュだとちょっと問題が出てくるので厚みを出す為に剣元に2点、剣先に2点の空のゲームオブジェクトを作成し、前の剣の位置とで厚みのあるメッシュを生成出来るようにします。
↑のように剣の子要素に空のゲームオブジェクトを作成し名前をStartPosition、StartPosition2、EndPosition、EndPosition2とします(名前はわかりやすいように変更してください)。
それぞれの位置は
↑のような位置に移動させます。
とりあえずここまでの機能で剣を振った時に剣と敵との当たり判定をする事が出来ます。
攻撃モーションのビヘイビア
先ほど敵がダメージを受けた時に無敵時間を設けましたが、次のモーションの時には無敵状態を解除したいところです。
そこでAnimator Controllerの攻撃状態にビヘイビアを設定し、そこで無敵状態をリセット出来るようにします。
ビヘイビアに関しては
を参照してください。
今回の主人公キャラクターは連続攻撃が出来るものとし、最初の攻撃中にマウスの左ボタンを押すと次の攻撃モーションへと遷移するようにしています。
↑のような感じの状態と遷移を作成してます。
最初の攻撃モーションの状態を選択し、インスペクタのAdd Behaviourを押して名前をAttackBehaviourとします。
AttackBehaviourでは無敵状態をリセットする為の処理を記述します。
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; public class AttackBehaviour : 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) { ResetInvicibleCondition (); } // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks // 次のモーション再生開始時に全てのダメージを有効にする override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (Input.GetButtonDown ("Fire1")) { animator.SetTrigger ("Attack"); ResetInvicibleCondition (); } } void ResetInvicibleCondition() { DamageScript[] damageScripts = FindObjectsOfType <DamageScript> (); foreach (var damageScript in damageScripts) { damageScript.Reset (isDamaged : false); } } // 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) { // //} // OnStateMove is called right after Animator.OnAnimatorMove(). Code that processes and affects root motion should be implemented here //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} // OnStateIK is called right after Animator.OnAnimatorIK(). Code that sets up animation IK (inverse kinematics) should be implemented here. //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { // //} } |
OnStateEnterはその状態に入った時に実行されるので、このタイミングでFindObjectsTypeでDamageScriptスクリプトを全て取り出して、Resetメソッドを呼び出し初期化します。
OnStateUpdateはその状態でアニメーションが再生中にずっと呼ばれるので、その中でマウスの左ボタンが押されたかどうかを判定し、次の攻撃アニメーションに遷移するかどうかを判定しています。
左ボタンが押されたら次のモーションに移動するので、その時に無敵状態をリセットします。
FindObjectsTypeで全部のDamageScriptを探して処理するので動作が遅くなる可能性もあるかもしれません。
前の剣の位置と現在の剣の位置の間にメッシュを作成する
これでやっとメインの処理である前の剣の位置と現在の剣の位置との間からメッシュを作成する処理に移れます・・・・。
ヒエラルキー上に空のゲームオブジェクトを作成し、名前をTrailColliderとし、Transformの歯車からResetを選択し位置や角度をリセットします。
TrailColliderゲームオブジェクトにはRigidbodyを取り付けIs Kinematicにチェックを入れ、先ほど作成したAttackスクリプトを取り付けます。
さらにTrailColliderスクリプトを新しく作り設定します。
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 | using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.UI; [RequireComponent(typeof(MeshFilter), typeof(MeshCollider))] public class TrailCollider : MonoBehaviour { // 剣元1 public Transform startPosition; // 剣元2 public Transform startPosition2; // 剣先1 public Transform endPosition; // 剣先2 public Transform endPosition2; // メッシュ private Mesh mesh; // 頂点リスト public List<Vector3> verticesLists = new List<Vector3>(); // 三角形のリスト public List<int> tempTriangles = new List<int>(); // 軌跡の表示のオン・オフフラグ private bool isSwordCollider = false; // 前フレームの剣元の位置 private Vector3 oldStartPos; // 前フレームの剣先の位置 private Vector3 oldEndPos; private MeshCollider meshCollider; private MeshFilter meshFilter; // Use this for initialization void Start () { mesh = GetComponent <MeshFilter> ().mesh; meshCollider = GetComponent <MeshCollider> (); // 他の凸コライダと衝突有効にする meshCollider.convex = true; // 物理的に当たらないようにする meshCollider.isTrigger = true; // メッシュコライダに自作メッシュを設定 meshCollider.sharedMesh = mesh; } void LateUpdate () { // 攻撃有効の時だけメッシュ生成 if (isSwordCollider) { CreateMesh (); } } // 剣の軌跡作成メソッド void CreateMesh() { mesh.Clear (); // 頂点以外のリストのクリア verticesLists.Clear (); tempTriangles.Clear (); verticesLists.AddRange (new Vector3[] { oldStartPos, oldEndPos, startPosition.position, endPosition.position, startPosition2.position, endPosition2.position }); // 本当なら全面作らなければいけないがCovexにチェックを入れると勝手に作ってくれる tempTriangles.AddRange (new int[]{ 0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, /* 0, 2, 4, 5, 1, 3 */ }); mesh.vertices = verticesLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); // メッシュコライダの再有効化 meshCollider.enabled = false; meshCollider.enabled = true; oldStartPos = startPosition.position; oldEndPos = endPosition.position; } public void SetSwordCollider(bool swordColliderFlag) { isSwordCollider = swordColliderFlag; mesh.Clear (); // 攻撃有効化する時の剣元、剣先の位置を設定 if (isSwordCollider) { oldStartPos = startPosition.position; oldEndPos = endPosition.position; meshCollider.enabled = false; meshCollider.enabled = true; // 攻撃無効化する時にメッシュコライダを無効化 } else { meshCollider.enabled = false; } } } |
スクリプトが長いので少しづつ見ていきます。
フィールド宣言部分
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 | // 剣元1 public Transform startPosition; // 剣元2 public Transform startPosition2; // 剣先1 public Transform endPosition; // 剣先2 public Transform endPosition2; // メッシュ private Mesh mesh; // 頂点リスト public List<Vector3> verticesLists = new List<Vector3>(); // 三角形のリスト public List<int> tempTriangles = new List<int>(); // 軌跡の表示のオン・オフフラグ private bool isSwordCollider = false; // 前フレームの剣元の位置 private Vector3 oldStartPos; // 前フレームの剣先の位置 private Vector3 oldEndPos; private MeshCollider meshCollider; private MeshFilter meshFilter; |
startPosition、startPosition2、endPosition、endPosition2は剣の子要素に作成したゲームオブジェクトをそれぞれ設定し、位置を取得する為に使用します。
meshは生成したメッシュを入れるフィールド、頂点リストと三角形リストはメッシュを生成する時に使用する情報を入れます。
当たり判定のメッシュのコライダは攻撃モーションの位置によってオン・オフをするので、isSwordColliderフラグを使います。
このフラグはアニメーションイベントから送られてきたイベントでオン・オフをします。
アニメーションイベントに関しては
を参照してください。
meshColliderはコライダコンポーネント、meshFilterはメッシュフィルターコンポーネントを設定します。
Startメソッド
Startメソッドでは初期処理を行っています。
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Use this for initialization void Start () { mesh = GetComponent <MeshFilter> ().mesh; meshCollider = GetComponent <MeshCollider> (); // 他の凸コライダと衝突有効にする meshCollider.convex = true; // 物理的に当たらないようにする meshCollider.isTrigger = true; // メッシュコライダに自作メッシュを設定 meshCollider.sharedMesh = mesh; } |
自身のMeshFilterコンポーネントのMeshクラスをmeshに入れます。
自身のMeshColliderコンポーネントをmeshColliderに入れます。
MeshFilterとMeshColliderコンポーネントはクラス定義の前のアトリビュートで指定している為、TrailColliderスクリプトをゲームオブジェクトに設定すると自動で追加されます。
meshColliderのConvexのチェックをオンにします。
MeshColliderコンポーネントのConvexにチェックを入れると、他のメッシュコライダを持つゲームオブジェクトとの当たり判定をする事が出来るようになります。
このチェックを入れないと他のメッシュコライダを持つゲームオブジェクトだけでなく、Cube等のプリミティブな図形(メッシュコライダと判定される物)との当たり判定が出来ません。
Convexにチェックを入れると凸面を勝手に作成してくれるので、メッシュの三角形を作る手間が省かれます。
詳しい事はメッシュの生成処理で見ていくので置いておきます。
meshColliderのIs Triggerにチェックを入れ物理的に当たらないようにし、sharedMeshにmeshを入れてメッシュコライダのメッシュに自作のメッシュを適用します。
LateUpdateメソッド
LateUpdateメソッドでは当たり判定が有効な時だけCreateMeshメソッドを呼び出しメッシュを生成しています。
1 2 3 4 5 6 7 8 | void LateUpdate () { // 攻撃有効の時だけメッシュ生成 if (isSwordCollider) { CreateMesh (); } } |
CreateMeshメソッド
CreateMeshメソッドで実際に当たり判定のメッシュを生成してます。
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 | // 剣の軌跡作成メソッド void CreateMesh() { mesh.Clear (); // 頂点以外のリストのクリア verticesLists.Clear (); tempTriangles.Clear (); verticesLists.AddRange (new Vector3[] { oldStartPos, oldEndPos, startPosition.position, endPosition.position, startPosition2.position, endPosition2.position }); // 本当なら全面作らなければいけないがCovexにチェックを入れると勝手に作ってくれる tempTriangles.AddRange (new int[]{ 0, 1, 2, 2, 1, 3, 2, 3, 4, 4, 3, 5, /* 0, 2, 4, 5, 1, 3 */ }); mesh.vertices = verticesLists.ToArray (); mesh.triangles = tempTriangles.ToArray (); mesh.RecalculateBounds (); mesh.RecalculateNormals (); // メッシュコライダの再有効化 meshCollider.enabled = false; meshCollider.enabled = true; oldStartPos = startPosition.position; oldEndPos = endPosition.position; } |
CreateMeshメソッドが呼ばれる度にメッシュ、頂点、三角形をクリアにしています。
頂点に使用するのは前の剣元、剣先の位置と、現在の剣元の2点、剣先の2点の6点なのでそれをveritciesListsに登録します。
頂点を登録したらその頂点から三角形を作る必要があります。
MeshColliderのConvexにチェックを入れた場合、厚みのあるコライダを自動で生成する為にメッシュの三角形で厚みのある面を生成しておく必要があります。
と思ってるんですが、厚みを付けて三角形を作成しなくてもエラーがでなくなりました・・・・(謎)。
とりあえず厚みは付けておいた方が問題はでないかもしれません。
以前出ていたエラーの内容としては
ConvexHullBuilder::CreateTrianglesFromPolygons: convex hull has a polygon with less than 3 vertices!
といった内容だったのですが、急に出なくなったのでわかりません。(^_^;)
平面の場合3点より多い頂点を使用しててもエラーになったんですが・・・うーむ。
前の剣元、剣先の位置と現在の剣元の1点、剣先の1点で平面のコライダを作り他のコライダとの判定をしようとしましたが、厚みを付ける為の面も作成しておかないとエラーが発生します。
その為、縦方向と横方向の面を1点ずつ作成します。
本来であれば四角形を2つの三角形で作り、それを4面作って一つの図形を作るところですが、Convexにチェックを入れると縦横の面を作成するだけで勝手に図形のメッシュを作ってくれました。
例えば↑のスクリプトの三角形でConvexにチェックを入れていないと
↑のように縦と横の四角形1面ずつしか作成されませんが、Convexにチェックを入れると
↑のように他の面も作成され面で囲われた図形のメッシュが生成されます。
Convexにチェックを入れない場合でも、四角形の面1つで当たり判定は出来ますがCube等の図形や他のメッシュコライダとの当たり判定が出来なくなります。
こういったことを考慮するとConvexにチェックを入れておくといいかもしれません。
ただ、このConvexにチェックを入れられるのはメッシュの頂点が255以下の場合のみです。
今回のサンプルでは使っている頂点は6つです。
本来であればメッシュコライダは動かないゲームオブジェクトに設定するものなので、頂点が多い動くメッシュと他のメッシュコライダとの当たり判定は処理負荷が大きくなる為、このような制限があるんですかね?他の理由かもしれませんが・・・。
三角形を作る処理でコメント化してあるのが本来作るべき面の設定箇所ですが、作る必要がないのでコメント化しています。
あ・・・・、四角形1面分足りてない気がするが・・・・(-_-)
これでTrailColliderスクリプトが出来たので、TrailColliderゲームオブジェクトのインスペクタの設定を見てみます。
↑のように設定しました。
MeshColliderの設定はスクリプトから設定しているので、特に変更する必要はありません。
アニメーションイベントで当たり判定のオン・オフ
アニメーションイベントは当たり判定開始時にStartSwordTrailイベント、判定終了時にEndSwordTrailイベントが発生するようにしておきます。
名前が剣の軌跡を作った時のイベントと同じなのはタイミングが同じだからで、そのまま使っています。
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 UnityEngine; using System.Collections; public class RecieveSwordColliderEvent : MonoBehaviour { // 剣自体のコライダ public Collider swordCollider; // 前フレームと現フレームの間のコライダ public TrailCollider trailCollider; public void StartSwordTrail() { swordCollider.enabled = true; if (trailCollider != null) { trailCollider.SetSwordCollider (true); } } public void EndSwordTrail() { swordCollider.enabled = false; if (trailCollider != null) { trailCollider.SetSwordCollider (false); } } } |
Animatorコンポーネントを持つキャラクター自身にRecieveSwordColliderEventスクリプトを作成し取り付けます。
swordColliderには剣、trailColliderにはTrailColliderゲームオブジェクトを設定します。
これで全ての機能が出来ました。
当たり判定がされるか確認する
機能が出来たので確認してみましょう。
まずはシーンビューで当たり判定であるメッシュコライダがどのように作成されているかを確認します。
当たり判定は
↑のようになりました。
Cubeのような他のメッシュコライダとの当たり判定も出来ていますね。
動画ではCubeがでかいのでダメージUIがほとんどみえてませんが・・・。
今回の処理で少し気になる点としては、ビヘイビアのRecieveSwordColliderEventスクリプトの処理で全DamageScriptスクリプトを取得している処理ですかね。
処理が遅くなるようならば別のやり方を考えなければいけませんが・・・・良い考えが思い浮かびませぬ(‘_’)
今回の機能を作成した事で当たり判定のスルー問題も解消されそうです。(^^)/