今回は銃を撃った時の敵との当たり判定をする機能を作成していきます。
前回
銃それぞれに銃のパラメータを保持しておくスクリプトを設定しました。
今回は銃を撃った時に銃口の先に敵がいたら敵を消す処理を作成していきます。
銃を撃った時に物理的な弾を飛ばして弾が敵に当たったかどうかを判定するのではなく、スクリプトで銃口の先に敵がいるかどうかで当たり判定を行います。
物理的な弾を飛ばしたい方は
を参考に改造してみてください。
またバイオハザード4や5や6等のようなTPS(ThirdPersonShooter)にあるようなレーザーポインタからレーザーを表示し標的に狙いを定める機能も作成しません。
レーザーで狙いを定めたい方は
を参考にして改造してみてください。
銃を撃った時に敵に当たっているか判定する為にどうする?
当たり判定をするにはある位置からある位置まで真っ直ぐにレイを飛ばし、その間に敵がいたら当たったと判定させます。
その為に銃口に『最初の位置』を作成し、そこから『銃口の方向×銃の射程距離』のレイを飛ばします。
まずは銃口の位置を作成します。
↑のように空オブジェクトを作りEquipという名前にし、Equipタグを設定します。
その子要素に武器をドラッグ&ドロップします。
Equipの位置がXYZともに0であれば武器を子要素に移動しても元の位置から変わりません。
銃の子要素に右クリック→3D Object→Sphereを作成し、名前をMuzzleとします。
見た目は必要ないのでMesh Rendererのチェックを外します。
Muzzleの向きを銃口の先にしたいので、↑のようにLocalにして銃口の先に青色の矢印がくるようにします。
↑のようにRadiusの調整と位置、向きを修正してください。
銃口の位置より少し上側にずらしているのは銃に付いている狙いを定めるやつ(名前がわからない)で敵を狙った時にちょうどよく当てる為です。
Muzzleのインスペクタは↑のようになりました。
これで銃口の位置と向きが出来上がりました。
レイを飛ばすにはこの位置と向きを使ってスクリプトからレイを飛ばします。
銃を撃った時の敵との当たり判定を行うスクリプトShotの作成
次に銃に弾を撃った時の処理スクリプトShotを作成しMusasi君に取りつけます。
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 | using UnityEngine; using System.Collections; public class Shot : MonoBehaviour { // 銃のステータススクリプト private FPSWeaponStatus weaponStatus; // 銃口 private Transform muzzle; // 弾の半径 private float muzzleRadius; // 一番近い敵 private GameObject nearEnemy; void Start () { muzzle = GameObject.FindWithTag ("Equip").transform.GetChild (0).Find ("Muzzle").transform; Debug.Log (muzzle); // 主人公のステータスを取得 weaponStatus = muzzle.GetComponentInParent<FPSWeaponStatus>(); // 弾の半径を取得 muzzleRadius = muzzle.GetComponent<SphereCollider>().radius; } void Update() { Debug.DrawLine(muzzle.position, muzzle.position + muzzle.transform.forward * weaponStatus.GetRange ()); } // 銃を撃って敵に当たったか判定するスクリプト public void Judge() { // 飛ばす位置と飛ばす方向を設定 Ray ray = new Ray(muzzle.transform.position, muzzle.transform.forward); // 当たったコライダを入れておく変数 RaycastHit[] hits; // Sphereの形でレイを飛ばしEnemyレイヤーのものをhitsに入れる hits = Physics.SphereCastAll(ray, muzzleRadius, weaponStatus.GetRange (), LayerMask.GetMask("Enemy")); // 射程距離をdistanceに入れる var distance = weaponStatus.GetRange (); // nearEnemyを初期化 nearEnemy = null; // ハンドガンタイプの武器だったら一番近い敵のみ攻撃 if(weaponStatus.GetWeaponType() == FPSWeaponStatus.WeaponType.HandGun) { // レイと接触したコライダで一番近い距離の敵を探す foreach(var hit in hits) { if(Vector3.Distance(transform.position, hit.point) < distance) { distance = Vector3.Distance(transform.position, hit.point); nearEnemy = hit.collider.gameObject; } } // 一番近い敵を消す if(nearEnemy != null) { Destroy(nearEnemy); } // それ以外の武器は当たった敵すべてを攻撃 } else { foreach(var hit in hits) { Destroy(hit.collider.gameObject); } } } } |
少しづつ解説していきます。
Shotスクリプトの解説
Startメソッド内で銃に設定しているWeaponStatusスクリプトを取得します。
またMuzzle(銃口)に設定しているSpehreColliderの半径を取得します。
これはレイを線ではなく丸い空間で飛ばす時に必要となるからです。
Updateメソッド内で行っている事は視覚的に銃の射程距離と方向がどうなっているかを確認する為に処理を書いていますが、特に必要はありません。
Judgeメソッドが実際にレイを飛ばし敵に当たっているかどうかを判定している部分です。
このメソッドを呼び出すのはキャラクター操作スクリプトMyCharaになります。
Rayクラスは
1 2 3 | new Ray(レイを飛ばす位置, レイの方向) |
となるのでレイを飛ばす位置をMuzzleの位置、レイの方向をMuzzleの前方方向を設定しています。
Physics.SphereCastAllはSphere(球)のレイを飛ばし当たったコライダをすべて取得します。
敵にはすべてEnemyレイヤーを設定しておき、当たり判定をするレイヤーはEnemyのみにしておきます。
銃のタイプを取得し、WeaponType.HandGunであれば一番近い敵のみに攻撃を与え、それ以外の武器では貫通した後の敵にも攻撃を与えるようにしています。
すべての敵を距離で判断してダメージを変更するというのもいいかもしれませんね。
とりあえず今回はハンドガンとそれ以外のタイプ、与えるダメージはみな同じとしておきます。
ダメージというより当たったら消していますけどね・・・。
今回はSphereCastでSphereの範囲でレイを飛ばしていますが、単純に線で判定したい場合はRaycastやLinecastを使ってもいいと思います。
ただその場合はSphereCastのように範囲で他のコライダとの判定をしない為に厳密に敵を狙う必要が出てきます。
Linecast等でレイを飛ばして判定する場合はレーザーポインターを表示するとわかりやすいかもしれません。
これでShotスクリプトが完成したので銃に設定しインスペクタでmuzzleにMuzzleゲームオブジェクトを設定してください。
主人公操作スクリプトからShotのJudgeメソッドを呼び出す処理を追加
最後に主人公が銃を撃った時にこのShotスクリプトのJudgeメソッドを呼び出す処理を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 銃を撃った時の処理スクリプト private Shot shot; void Start () { //キャラクターコントローラの取得 cCon = GetComponent<CharacterController>(); animator = GetComponent <Animator> (); myCamera = GetComponentInChildren<Camera>().transform; // キャラクター視点のカメラの取得 initCameraRot = myCamera.localRotation; charaRotate = transform.localRotation; cameraRotate = myCamera.localRotation; shot = GetComponent<Shot>(); } |
上のようにキャラクター自身に取り付けたShotスクリプトを取得します。
1 2 3 4 5 6 7 | // 銃を撃つ if(waitShot && Input.GetButtonDown("Fire1")) { shot.Judge(); animator.SetBool("Shot", true); } |
銃を撃つ処理の所にShotスクリプトのJudgeメソッドを呼び出すようにします。
これで処理が完成しました。
敵に狙いを定めて当たり判定機能を確認してみる
あとはEnemyレイヤーを設定したCubeをいくつか設定して狙って撃ってみましょう。
多少ずれているような気がしますね・・・・。
銃の位置とカメラの向きを合わせる
さきほど銃を撃った時に少しずれている感じがしました。
それはキャラクターの上半身が向いている方向はカメラの方向に合わせていますが、スクリプトでは銃口の方向にレイを飛ばし当たり判定をしています。
なのでゲームプレイヤーがゲーム画面を見て銃を敵に向けた時に、銃の狙いを定める部分がカメラの中心に来るようにしなければ見た目と処理が合わなくなってしまいます。
そこで簡単に銃の狙いを定める部分がカメラの中心にくるようにしましょう。
ヒエラルキー上で右クリック→UI→Imageを作成すると↑のようにCanvasの子要素にImageが作成されます。
Imageのインスペクタを表示し、PosX、PosY、Width、Heightを変更し中央に小さい四角が表示されるようにします。
↑が実際の画面になります。
中央に小さい四角が表示され、ここがカメラの中心になります。
つまり銃の狙いを定める場所をこの小さい四角と合わせればいいわけです。
現時点では↑のように銃の狙いを定める部分と小さい四角がずれている事がわかります。
そこでカメラの位置を調整し銃の狙いと四角が合うようにします。
しかし、Main Cameraの位置を調整してもUnityを実行するとずれてしまいます。
ここで思い出してもらいたいのがMain Cameraの位置はスクリプトでCameraPositionゲームオブジェクトの位置にしていたという事です。
そこでキャラクターのAnimatorのIdleに銃を構えるアニメーションを設定し、Unityを実行します。
実行中にCameraPositionを選択しSceneタブにしてCameraPositionを移動させるかインスペクタの数値を調整して銃の狙いを定める部分と小さい四角の位置を合わせます。
狙いを定める部分をカメラの中心にするか、銃口をカメラの中心にするかはお好みで設定してください。
位置の調整が終わったらUnity実行中のままCameraPositionのインスペクタの歯車を押しCopy Componentを選択します。
Unityの実行を止めてCameraPositionのインスペクタの歯車を押し、Paste Component Valuesを選択します。
これで実行中に調整した位置がCameraPositionに反映されます。
それでは調整が終わったのでUnityの実行をして銃の狙いが合うかどうか確認してみましょう。
↑のように小さい四角と銃の狙いを定める部分が合うようになり、狙ったゲームオブジェクトを消すことが出来るようになりました。
作成したMuzzleのSphereColliderのサイズを少し大きく作ったので当たり判定が広くなっています。
当たり判定を小さくしたい場合はSphereColliderのRadiusを小さくしたり、RaycastやLinecastを使った線での判定がいいかもしれません。
MuzzleのScale自体を変更してしまうとRadiusにも影響が出るみたいなので、MuzzleのScaleはすべて1にしたままRadiusを調整した方がいいと思います。
これで銃を撃った時の当たり判定の機能が完成しました。
銃の狙いを定める部分(フロントサイト?)をカメラの中心に合わせる処理で使った小さい白い四角は作業が終わったら消してください。
わかりやすいように照準(レティクル?)として別の画像に変えて表示したままとするのもいいかもしれません。
ただ照準はずっと真ん中にあるので銃がキャラクターのアニメーションで動いても照準は動かないという風になってしまうので気を付けてください。
敵の前に障害物があった場合の対処をする
さてここまででサンプルのような敵の場合は問題なく処理が出来ていると思います。
しかし実際のゲームでは不具合が発生します。
それは敵の前に障害物があっても敵を攻撃出来てしまうというところです。
敵の前に障害物があるかどうかは銃口からぶつかった相手との距離を計算し、敵が障害物より前にある場合だけ攻撃出来るようにしなければいけません。
そこでShotスクリプトのJudgeメソッドを改良し、間に障害物があった時の対処が出来るようにします。
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 | // 銃を撃って敵に当たったか判定するスクリプト public void Judge() { // 飛ばす位置と飛ばす方向を設定 Ray ray = new Ray(muzzle.transform.position, muzzle.transform.forward); // 当たったコライダを入れておく変数 RaycastHit[] hits; // Sphereの形でレイを飛ばしEnemyレイヤーのものをhitsに入れる hits = Physics.SphereCastAll(ray, muzzleRadius, weaponStatus.GetRange (), LayerMask.GetMask("Enemy", "Block")); // 敵までの距離にshotRangeを入れる var enemyDistance = weaponStatus.GetRange(); // 障害物までの距離にshotRangeを入れる var blockDistance = weaponStatus.GetRange(); // nearEnemyを初期化 nearEnemy = null; // Blockタグが設定されているゲームオブジェクトで一番近い距離を取得 foreach(var hit in hits) { if(hit.collider.gameObject.layer == LayerMask.NameToLayer ("Block")) { if(hit.distance < blockDistance) { blockDistance = hit.distance; } } } // ハンドガンならEnemyタグが設定されていて障害物より手前にある敵で一番近い敵を取得 if(weaponStatus.GetWeaponType() == FPSWeaponStatus.WeaponType.HandGun) { foreach(var hit in hits) { if(hit.collider.gameObject.layer == LayerMask.NameToLayer ("Enemy") && hit.distance < blockDistance && hit.distance < enemyDistance) { enemyDistance = hit.distance; nearEnemy = hit.collider.gameObject; } } if(nearEnemy != null) { Destroy(nearEnemy); } // 他の武器なら障害物手前の敵を全て攻撃 } else { foreach(var hit in hits) { if(hit.collider.gameObject.layer == LayerMask.NameToLayer ("Enemy") && hit.distance < blockDistance) { Destroy(hit.collider.gameObject); } } } } |
あらかじめ敵のゲームオブジェクトにEnemyレイヤー、障害物にはBlockレイヤーを設定しておきます。
↑は障害物ゲームオブジェクト(Sphereという名前はゲームオブジェクトを作成した時のデフォルトの名前なので関係ありません)でBlockレイヤーを作成し、設定しています。
SphereCastAllで接触判定をするレイヤーをBlockとEnemyにして、衝突した相手を取得します。
次にforeach文でループさせて衝突した相手情報から設定されているタグがBlockのものを抽出し銃口から一番近い距離をblockDistanceに入れます。
ハンドガンタイプの場合はEnemyレイヤーが設定されているもので抽出し、障害物の手前でかつ一番手前の敵である時にnearEnemyにそのゲームオブジェクトを入れておきます。
ハンドガンタイプ以外の場合は障害物の前の敵すべてを攻撃するようにしています。
これで対処が出来たので敵の前に障害物を置いて確認してみましょう。
↑の四角形の箱が敵で球を障害物として設定しています。
まずは武器の種類をハンドガンに設定した場合の実行例を見てみましょう。
↑が実行結果です。
障害物前の敵(箱)はひとつづつ消えていますね。
また、障害物の後ろの敵は攻撃を受けません。
次に銃のタイプをハンドガンからショットガンに変更して試してみます。
↑のように銃のタイプをShotGunに変更します。
今回は確認の為ShotGunに変更して試しますが試した後はHandGunに戻してください。
↑が攻撃した時の実行結果です。
障害物前の敵は一気に攻撃され消えています。
しかし障害物の後ろの敵は攻撃されないようになっています。
これで障害物を考慮した判定が出来るようになりました。
FPSを作ってみようカテゴリの最近の記事について
今回はPhysics.RaycastやPhysics.LinecastではなくPhysics.SphereCastAllを使って幅のあるレイを飛ばして当たり判定をしてみましたが、
思ってたより当たり判定が広くなったような気がします。
弾のサイズと同じ球のレイを飛ばしましたが判定の幅が広いような?
今回の場合はCubeを撃って消してるだけで、人型の敵を登場させて動いているところを撃って試してみるまではどうなるかわかりませんね。
--少しズレている感じがした原因(2017年04月19日)--
少しズレていた原因がわかりました。
銃のScaleをヒエラルキーに配置した銃のプレハブのScaleを調整してサイズを合わせていた為に、子要素のMuzzleのSphereのradiusを使った当たり判定がズレてしまう事が原因だったと思われます。
EquipやMuzzleのScaleはX、Y、Zともに1になるようにします。
を参考にヒエラルキーに配置する前の銃のScale Factorを調整しキャラクターのサイズに合わせた銃をヒエラルキーに配置し使用するようにしてください。
--少しズレている感じがした原因終了(2017年04月19日)--
FPSのカテゴリを作った時は初心者向きだとか言ってましたけど、なんだか本格的な感じになってきましたね(^_^;)
ペースもゆっくりといいながら結局は他の記事と同じペースに・・・。
まずいまずい・・・・。
またゆっくりにしていかないと・・・・(;一_一)