今回は主人公との距離が縮まったら敵キャラが主人公を攻撃してくる機能を作ってみます。
前回までで主人公が敵キャラの検知エリアに入った時に追いかけてくるところまで作りました。
まずは前回まで敵の移動スクリプトをMoveEnemyとしていましたが、攻撃機能も取り付けるのでファイル名とクラス名をEnemyに変更します(面倒な方は特に変えなくてもいいです)。
それに伴いSetPositionスクリプトのMoveEnemyとしていた箇所をEnemyに変更します。
Asset Storeでアニメーション用のデータをインポートしましょう。
カテゴリで3Dキャラクターをチェックし、PricingのFree Assetsにチェックを入れ、Cartoon Zombieをインポートしました。
キャラクターに付いているattackとdamageいうアニメーションを使います。
敵キャラクターが攻撃をしてくる機能の概要
敵キャラクターが主人公に近づいた時に攻撃する為には前回作成した機能のように敵キャラクターが主人公を追いかける必要があります。
主人公を追いかけている状態で、一定の距離内に主人公がきたら敵は攻撃を繰り出します。
一回攻撃をした後も主人公がその場に留まっている場合は攻撃の範囲内にいるのですぐにまた攻撃をしてしまいます。
そこで一回攻撃をしたら次に攻撃するまでのフリーズ状態を作ることにします。
フリーズ状態が終わったら見回りに戻ります。
この時敵のSearchAreaのコライダ内にまだ主人公がいた時は
見回り状態(Walk)→追いかける状態(Chase)
と遷移し、さらに攻撃範囲内にいれば
追いかける状態(Chase)→攻撃状態(Attack)
と遷移させるようにします。
敵キャラが攻撃をする為の処理を作成
敵キャラクターの攻撃処理を作成していきます。
敵キャラの状態を追記
Enemyスクリプトに処理を加えます。
敵キャラクターの状態を増やします。
1 2 3 4 5 6 7 8 9 | public enum EnemyState { Walk, Wait, Chase, Attack, Freeze }; |
EnemyStateに攻撃状態と攻撃後のフリーズ状態を作ります。
Updateメソッドに敵キャラの攻撃遷移処理を追加
次は敵キャラが攻撃に遷移するスクリプト部分をEnemyスクリプトの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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // 攻撃した後のフリーズ時間 [SerializeField] private float freezeTime = 0.5f; void Update () { // 見回りまたはキャラクターを追いかける状態 if (state == EnemyState.Walk || state == EnemyState.Chase) { // キャラクターを追いかける状態であればキャラクターの目的地を再設定 if (state == EnemyState.Chase) { setPosition.SetDestination (playerTransform.position); } if (enemyController.isGrounded) { velocity = Vector3.zero; animator.SetFloat ("Speed", 2.0f); direction = (setPosition.GetDestination () - transform.position).normalized; transform.LookAt (new Vector3 (setPosition.GetDestination ().x, transform.position.y, setPosition.GetDestination ().z)); velocity = direction * walkSpeed; } if (state == EnemyState.Walk) { // 目的地に到着したかどうかの判定 if (Vector3.Distance (transform.position, setPosition.GetDestination ()) < 0.7f) { SetState(EnemyState.Wait); animator.SetFloat ("Speed", 0.0f); } } else if (state == EnemyState.Chase) { // 攻撃する距離だったら攻撃 if (Vector3.Distance (transform.position, setPosition.GetDestination ()) < 1f) { SetState(EnemyState.Attack); } } // 到着していたら一定時間待つ } else if (state == EnemyState.Wait) { elapsedTime += Time.deltaTime; // 待ち時間を越えたら次の目的地を設定 if (elapsedTime > waitTime) { SetState(EnemyState.Walk); } // 攻撃後のフリーズ状態 } else if(state == EnemyState.Freeze) { elapsedTime += Time.deltaTime; if (elapsedTime > freezeTime) { SetState(EnemyState.Walk); } } velocity.y += Physics.gravity.y * Time.deltaTime; enemyController.Move (velocity * Time.deltaTime); } |
攻撃後のフリーズ状態を続ける時間をインスペクタで設定出来るようにします。
状態がEnemyState.Chaseの時に主人公と1mの距離内に入ったらEnemyState.Attack状態にします。
距離の計算はVector3.Distanceで計算出来ます。
SetStateメソッドに攻撃状態と攻撃後状態を追加
EnemyスクリプトのSetStateメソッド内にEnemyState.Attack状態と攻撃後のEnemyState.Freeze状態へ変更する処理を追加します。
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 | // 敵キャラクターの状態変更メソッド public void SetState(EnemyState tempState, Transform targetObj = null) { state = tempState; if (tempState == EnemyState.Walk) { arrived = false; elapsedTime = 0f; setPosition.CreateRandomPosition(); } else if (tempState == EnemyState.Chase) { // 待機状態から追いかける場合もあるのでOff arrived = false; // 追いかける対象をセット playerTransform = targetObj; } else if (tempState == EnemyState.Wait) { elapsedTime = 0f; arrived = true; velocity = Vector3.zero; animator.SetFloat("Speed", 0f); } else if (tempState == EnemyState.Attack) { velocity = Vector3.zero; animator.SetFloat("Speed", 0f); animator.SetBool("Attack", true); } else if (tempState == EnemyState.Freeze) { elapsedTime = 0f; velocity = Vector3.zero; animator.SetFloat("Speed", 0f); animator.SetBool("Attack", false); } } |
アタック状態、フリーズ状態ともにvelocityの値をVector3.zeroで0にして、移動処理を行わないようにします。
またアニメーションパラメータのSpeedも0にしておきます。
SetStateでEnemyState.Freeze状態に変更されるのは攻撃アニメーションが終わった時にイベントとして受け取ったメソッドで行われます。
アニメーターコントローラーで攻撃の状態と遷移を作成
次に敵のAnimatorControllerの設定をしていきます。
まずはアニメーションパラメータAttackをBoolとして作成します。
新しいアニメーションのatack01をアニメーターウインドウ上にドラッグ&ドロップし、Walk、Idleを選択し右クリックしてatack01にMake Transitionします。
ConditionsにAttackを追加し、WalkとIdleからatack01への遷移条件に『Attackがtrue』の時を入れます。
遷移のインスペクタでHas Exit Timeのチェックを外しておきます。
このチェックが入っていると条件がOnになっても元のアニメーションの再生が終わらないと次のアニメーションに行きません。
atack01からIdleへMake Transitionを繋げます。インスペクタ上のHas Exit Timeのチェックを外し、条件にAttackがfalseになった時を入れておきます。
攻撃が終了したら何もしていない状態に遷移させる為です。
敵キャラの攻撃の当たり判定部分(コライダ)を作る
アニメーターコントローラーの設定が終わったので、次は敵キャラの攻撃の当たり判定部分を作成していきます。
プレハブを修正するので、一旦、敵キャラのプレハブをヒエラルキー上にドラッグ&ドロップし、修正します。
攻撃の辺り判定部分にコライダを設定します。
↑のように敵キャラの右手の部分にSphereColliderを追加します。
SphereColliderはスクリプトでOn・Offするので最初はチェックを外しておきます。
AttackZombieという名前で新しいスクリプトを作成し、SphereColliderの設定してある右手の手の部分に設定します。
AttackZombieスクリプトは後で作成します。
SphereColliderを上のような感じで設定し当たり部分を大きめに取ります。
この部分が主人公のCharacterControllerのコライダに当たったら攻撃が当たったという処理をします。
修正が終わったら敵キャラのインスペクタ上にあるPrefabのApplyボタンをクリックしプレハブに反映させておきます。
反映させたらヒエラルキー上の敵キャラはいらないのでチェックをはずすか削除しておきます。
アニメーションクリップの設定をする
Assets→Toon Zombie S→Model→zombie anime.fbxを選択し、インスペクタでRigがHumanoidになるようにし、次にAnimationタブをクリックします。
atack01を選択したら赤く印をつけた場所にチェックを入れます。
アニメーションの動きによってオブジェクトに回転や移動を加えないようにし、スクリプトで制御する為です。
アニメーションイベントを作成する
次はアニメーションの再生位置でイベントを発生させるようにしてみます。
上のようにEventsの所をクリックし、下のマークをクリックします。
新しいウインドウが出てくるのでAttackStartとAttackEnd、StateEndというイベントを作り、赤でマークをした位置に移動させます。
これは攻撃判定開始時と判定終了時、攻撃状態の終了を設定する為にこの位置にしています。(微調整してください)
FloatやIntはそのイベント発生時に渡せる引数の値です。特に渡さない場合は変更しなくて大丈夫です。
これでこのアニメーションが再生され、イベントの個所にくるとAnimatorが設定されているオブジェクトにAttackStart、AttackEnd、StateEndというイベントが送られます。
このイベントを受け取るスクリプトはあとで作成します。
これでアニメーションの途中でイベントを投げる事が出来るようになりました。
アニメーションイベントを受け取り、当たり判定のオン・オフをするスクリプト
敵のゲームオブジェクトにProcessEnemyAnimEventスクリプトを新しく作成し取り付けます。
ProcessEnemyAnimEventスクリプトはアニメーションのイベントを受け取るスクリプトになります。
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 | using UnityEngine; using System.Collections; public class ProcessEnemyAnimEvent : MonoBehaviour { private Enemy enemy; [SerializeField] private SphereCollider sphereCollider; void Start() { enemy = GetComponent<Enemy>(); } void AttackStart() { sphereCollider.enabled = true; } public void AttackEnd() { sphereCollider.enabled = false; } public void StateEnd() { enemy.SetState(Enemy.EnemyState.Freeze); } public void EndDamage() { enemy.SetState(Enemy.EnemyState.Walk); } } |
インスペクタでSphereCollider(手に取り付けた攻撃の当たり判定用コライダ)を設定出来るようにします。
Startメソッド内でEnemyスクリプトを取得しています。
AttackStartとAttackEndはさきほど設定したアニメーションから送られてくるイベントを受け取りますので、AttackStartを受け取ったらコライダをOnにし、当たり判定を有効にします。
AttackEndを受け取ったら、コライダをOffにし、当たり判定を無効にします。
攻撃後(StateEnd)はEnemyState.Freezeに設定します。
アニメーションイベントの問題?
このアニメーションイベントで全てうまくいきそうですが、場合によっては呼ばれない事があります。
それは例えば敵が攻撃開始後すぐに主人公に攻撃を受けて攻撃アニメーションのアニメーションイベント部分までの再生がされず、ダメージアニメーションへ遷移してしまう時です。
AttackEndが呼ばれないと敵の手のコライダがオフにならないままになってしまいます。
そんな時はビヘイビアを使用するとうまくいきます。
もしくは攻撃アニメーションが最後まで再生されてから次のアニメーションを再生するように遷移条件のHas Exit Timeにチェックをして攻撃アニメーションを最後まで再生させます。
攻撃の当たり判定をするスクリプト
次はAttackZombieスクリプトです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | using UnityEngine; using System.Collections; public class AttackZombie : MonoBehaviour { void OnTriggerEnter(Collider col) { if(col.tag == "Player") { Debug.Log("当たり"); col.GetComponent<CharacterScript>().TakeDamage(transform.root); } } } |
Playerタグを持つインスタンスに当たったらキャラクター操作スクリプトCharacterScriptのTakeDamageメソッド(ダメージ処理メソッド)を実行させます。
引数としてこのスクリプトが設定されているオブジェクトのルート(敵キャラ)のTransform情報(位置情報)を渡しています。
つまり主人公キャラのCharacterScriptスクリプトのTakeDamageメソッドに敵キャラのTransform情報を渡すという事になります。
これで敵キャラの攻撃の処理が出来ました。
ただ、攻撃された主人公キャラ側の処理をまだ作っていないので、攻撃されても主人公は棒立ち状態のままです。
主人公を検知するSearchCharacterスクリプトの修正
敵キャラクターの子要素のSearchAreaに取り付けた主人公検知スクリプトのSearchCharacterスクリプト内を修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | void OnTriggerStay(Collider col) { // プレイヤーキャラクターを発見 if (col.tag == "Player") { // 敵キャラクターの状態を取得 Enemy.EnemyState state = enemy.GetState(); // 敵キャラクターが追いかける状態でなければ追いかける設定に変更 if (state == Enemy.EnemyState.Wait || state == Enemy.EnemyState.Walk) { enemy.SetState(Enemy.EnemyState.Chase, col.transform); } } } |
以前は敵キャラクターがEnemyState.Chase状態以外の時のチェイス状態にしましたが、EnemyState.WaitもしくはEnemyState.Walk状態の時だけチェイス状態にするようにします。
主人公側の攻撃を受けた時の処理
主人公側の攻撃を受けた時の処理を作成していきます。
まずは攻撃を受けた時の処理を主人公操作スクリプトに追加します。
さきほど攻撃を受けた時にTakeDamage関数を実行する処理を記述したので、主人公側で攻撃を受けた時の処理を書いていきます。
まずはフィールド部分です。
1 2 3 4 5 6 7 8 | public enum MyState { Normal, Damage }; private MyState state; |
主人公側でも今何をしている状態か?を列挙体と記憶しておくフィールドstateを作ります。
1 2 3 4 5 6 7 8 | public void TakeDamage(Transform enemyTransform) { state = MyState.Damage; velocity = Vector3.zero; animator.SetTrigger ("Damage"); // characterController.Move (enemyTransform.forward * 0.5f); } |
TakeDamageメソッドが呼ばれたら、状態stateをダメージを受けている状態MyState.Damageに変更し、主人公キャラのアニメーションパラメータのDamageトリガーをOnにします。
また攻撃を受けた際に敵の向いている方向に動かしたい時は、コメントアウトしている部分を有効にします(今回は攻撃を受けても動かしません)。
主人公のアニメーターコントローラにダメージ状態と遷移を作成する
主人公キャラクターのAnimatorControllerを修正します。
敵キャラのアニメーターコントローラーとほぼ同じで、アニメーションパラメータにTrigger型のDamageのパラメータを作成し、
Damage状態を作り、DamageトリガーがOnになったらAny StateからDamageに遷移するように作ります。
(今回の記事とは関係ないですが、Walk→IdleのSettingsのInterruption SourceをNext Stateに変更し、何もしていない状態から歩き状態への遷移をスムーズにします)。
Any State→Damageの遷移条件はHas Exit Timeのチェックを外し、Damageがトリガーされた時にすぐに遷移するようにします。
Damage→Idleの遷移の遷移条件はHas Exit Timeにチェックをします。
Damage→Walkの遷移条件はHas Exit Timeにチェックをし、SpeedがGreaterで0.1を設定します。
Damage→他の状態への遷移はHas Exit Timeにチェックをいれているのでダメージアニメーションが終わるまでは次の状態へ遷移しません。
なので主人公のダメージアニメーションに設定したアニメーションイベントのEndDamageが発生することになります。
主人公用のダメージアニメーションにアニメーションイベントを追加
主人公のダメージアニメーションでイベントを発生させておきます。
ダメージアニメーションは最初にインポートしたCartoon Zombieのdamageアニメーションを使用します。
アニメーションが終了した時にEndDamageイベントを発生させています。
主人公キャラクターのアニメーションイベントを受け取るスクリプトの作成
主人公キャラクターのダメージアニメーションにアニメーションイベントを設定したので、このイベントを受け取るスクリプトを作成します。
ProcessCharaAnimEventスクリプトを新しく作り主人公のゲームオブジェクトに取り付けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System.Collections; using System.Collections.Generic; using UnityEngine; public class ProcessCharaAnimEvent : MonoBehaviour { private CharacterScript characterScript; private void Start() { characterScript = GetComponent<CharacterScript>(); } public void EndDamage() { characterScript.SetState(CharacterScript.MyState.Normal); } } |
主人公はダメージを受けた後はMyState.Normalに設定します。
キャラクター操作処理部分を修正する
次に主人公操作スクリプトのUpdateメソッド内を修正します。
ダメージを受けている時に方向キー等を押した場合、ダメージを受けながら移動してしまいますので、そうならない為の条件を加えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | void Update() { if (state == MyState.Normal) { // 地面に接地してる時は速度を初期化 if (characterController.isGrounded) { velocity = Vector3.zero; var input = new Vector3(Input.GetAxis("Horizontal"), 0f, Input.GetAxis("Vertical")); if (input.magnitude > 0f) { animator.SetFloat("Speed", input.magnitude); transform.LookAt(transform.position + input); velocity += input.normalized * walkSpeed; } else { animator.SetFloat("Speed", 0f); } } } velocity.y += Physics.gravity.y * Time.deltaTime; characterController.Move(velocity * Time.deltaTime); } |
キャラクターの状態がMyState.Normalの時だけ移動処理を行います。
終わりに
これで敵の攻撃と主人公が攻撃を受けた時の処理が出来ました。
当たり判定の大きさと主人公キャラとの距離の調整をすればそれなりに見えるんではないかと・・・・・。
今回の作業はいろいろな個所の設定が必要なので、慣れてない人はどれがどれだか解らないという状態になるかもしれません。
説明下手なもので、飛び飛びの解説になってしまいました・・・・。(;_;)
次回は攻撃されてばかりではストレスがたまるので主人公も攻撃を出来るようにしてみます。