はじめまして、昨年11月からGameWithにJoinしている@vividalmaと申します。 既にご存じの方もいるかもしれませんが、GameWithでは現在FPSゲームプレイヤー向けのエイム練習ソフトをUnity(C#)で開発しており、開発を進めるうえで得られた知見などを時々書いていきたいと思います。 今回は初めてUnityについて書く記事ということもあり、ゲームを作る上では避けては通れない当たり判定について書きましたので、初心者向けの内容となっています。 FPSとは、1人称視点のシューティングゲームのことです。 ゲームとしての基本的な動作は、銃を持ったプレイヤーを操作して現れた敵を銃で撃つというものになります。 一連の動き全てを一度に実装すると複雑になるので、一つ一つの細かい動作に分割して考えます。 この中でも今回は、ゲーム開発ではお馴染みの発射した銃弾が当たるようにする制御、通称当たり判定について書いていきます。 当たり判定とは、簡潔に言うと物体同士が接触したことを判定する制御になります。 現実世界では、形あるものに触れようとすれば触れる事が出来ますが、コンピューター上の物体同士では接触したことを判断して、触れた後の動きのプログラムを書かないと物体がすり抜けてしまいます。 当たり判定を行う方法として古典的なのは、物体を構成する線分や頂点の座標情報を使って物体同士の重なりを計算する方法になります。 ややこしい計算を行う面倒な処理になるのですが、Unityではもっと簡単な方法が用意されています。 ※Unityでは、2D用と3D用で別々に当たり判定用のコンポーネントが用意されていますが、ここでは3D用コンポーネントを使用した説明をしています。 オブジェクトの物理的な衝突判定に使う方法です。 この方法を使用すると、衝突した際に跳ね返り等の物理現象をUnityが再現してくれます。 しかし、物理現象の再現を行うため、計算量が多くなり動作が重くなります。 【使用条件】 オブジェクトの侵入判定に使う方法です。 この方法では、侵入を検知するだけで物理現象は再現されません。 物理現象が再現されないため、OnColissionを使用するよりは軽くなりそうに思えますが、オブジェクトにRigidbodyをアタッチするだけでも不必要な計算が発生するため、侵入判定以外でRigidbodyを使用しないのであれば積極的に採用する必要は無いと思われます。 【使用条件】 照射判定に使う方法です。 この方法は設定した距離まで光線を伸ばして光線に当たったオブジェクトを検知する方法になり、物理現象は再現されません。 また、Rigidbodyが不要な分、OnCollisionやOnTriggerよりパフォーマンスが良くなります。 【使用条件】 Colliderとは、Unityに用意されている当たり判定を使用する為のコンポーネントになります。 Colliderにはいくつかの種類があり、その中でも負荷の少ないプリミティブなタイプのColliderとしてBox Collider、Sphere Collider、Capsule Colliderが存在します。 前述した当たり判定を行う場合は、それぞれに示した条件を満たすようにColliderコンポーネントをオブジェクトにアタッチする必要があります。 OnTrigger方式の当たり判定を使用する場合は、以下の画像の右側にある「Is Trigger」のチェックボックスにチェックを入れる必要があります。
Colliderで当たり判定を行っている事が分かる画像を以下に示します。
標的の立方体にアタッチしているCollliderを、銃弾が飛んでくる方向に拡張してOnCollision方式で動作させており、Colliderに銃弾が当たった瞬間に当たり判定が行われている事がわかります。 Rigidbodyとは、オブジェクトに対して物理特性を付与するためのコンポーネントになります。 重さ、重力の影響、空気抵抗などが設定できるため、現実に近い動きを実現したい場合などに利用することになります。 また、OnCollision方式やOnTrigger方式の当たり判定を行う場合にも利用することになります。 前述のOnTrigger方式のデモ画像で銃弾に当たった標的が後ろに移動しているのは、銃弾の方向と力の大きさを元に、このコンポーネントが動きを再現しているためです。 以下に示す画像では、標的の立方体に1kgの重さを設定し、重力の影響を受ける設定を行っています。
これを動作させると以下の画像のようになります。
開始早々に標的が下に落ちているのは、標的の下に標的を支えられる地面的なオブジェクトが作られていないのため、Rigidbodyが重力によってオブジェクトが下に落ちていく動きを再現しているからです。 OnCollisionやOnTriggerを使うための準備は前述した通りとなりますが、オブジェクト同士の接触を検知した場合、そこからダメージ計算などの処理を行う必要があります。 そのためにスクリプト側では、Unityで用意されているMonoBehaviourクラスを継承した派生クラスを用意します。 Unityでコンポーネントを自作する場合はMonoBehaviourの継承がほぼ必須となっており、OnCollisionやOnTriggerイベントをスクリプト側でキャッチする為にも必要になります。 前述のOnCollisionの標的側には以下のようなスクリプトをアタッチしています。 OnTriggerの場合は、オーバーライド元の関数が変わるため以下のようになります。 OnCollisionEnter関数はMonoBehaviourクラスで定義されており、オブジェクト同士の接触を検知するとこの関数が呼ばれる仕組みとなっているため、これを派生クラスTargetBehaviourでオーバーライドし、呼ばれた際の処理を追加しています。 この例では、Unityエディタのコンソール上にログを出力して接触してきたオブジェクトを破棄しています。 またUnityでは、オブジェクトにタグやレイヤーを設定することが出来るため、その機能を使うことで特定のオブジェクトを判定することも可能になります。 Raycastを使用する場合は、光線を飛ばす処理を実行させて飛ばした先で何かに当たれば、当たったオブジェクトの情報が取得できます。 以下に例示した画像を作成する際に使用したスクリプトを示します。 標的のレイヤーに"Target"を設定し、Rayを飛ばす際に指定しているためTargetレイヤーに属しているオブジェクトに光線が当たるとオブジェクト情報が返ってきます。 標的に当たった際のオブジェクト情報は、光線を飛ばす際に指定した第3引数のhitに格納されているため、実際にはこれを使用してオブジェクトの破棄などを行います。 光線の当たったオブジェクトを破棄するコードを追加した、以下のスクリプトを実行した際の動きを示します。
※光線を可視化するためには、Debug.DrawRayを使用する必要があります。
また、シーンビューやゲームビューで可視化するためには合わせてGismosを有効にする必要があります。 エイム練習で求められることは、標的に対して”正確に速く照準をあわせる”練習が出来ることになりますので、撃った後の弾が飛んでいく様や、撃たれた標的が倒れる動作は不要だと考えられます。 また多くの標的を出現させたり、標的の動きなどを複雑にすることも考えられますので、出来るだけリソースはそちらに回したいと考えると動作が軽い方が良いとなりますので、Raycast方式を採用することにしました。 ここまでの流れだと、あたかも最初からRaycast方式を採用して開発を進めているように見えますが実のところ最初はOnCollision方式で開発していました。 というのも開発の初期では銃弾を飛ばす事を考えていたためです。 このソフトは、FPSゲームのプレイ技術の上達を目的としており、遊ぶであろうゲームの仕様を考えると銃弾が飛んで行って標的に当たることでダメージを与える仕組みのため、ゲームの体験に近い練習ができる方が良いだろうと考えておりました。 しかし、狙ったところに速く正確に照準を合わせる技能を鍛えることが最も重要な要素となると、前述した通り着弾結果は不要という話になったことと、ゲームの合間に気軽にプレイできるように処理が軽ければ軽いほど良いということを考えてRaycast方式を採用することになったというオチになります。 最後まで読んでいただきありがとうございます。 現在GameWithでは、エイム練習ソフトの開発を行っていただけるUnityエンジニアを募集しています。 GameWithの理念に共感出来る方、ゲームが好きな方、興味を持っていただけた方などはお問合せをお願いします!はじめに
FPSゲームっぽくするにはどうする?
当たり判定とは?
Unityで当たり判定を実現する方法
OnCollision
OnTrigger
Raycast
Colliderの準備方法
Rigidbodyの準備方法
スクリプトの用意
OnCollision用スクリプト
using UnityEngine;
public class TargetBehaviour : MonoBehaviour
{
private void OnCollisionEnter(Collision collision)
{
Debug.Log("当たったよ");
Destroy(collision.gameObject);
}
}
OnTrigger用スクリプト
using UnityEngine;
public class TargetBehaviour : MonoBehaviour
{
private void OnTriggerEnter(Collider collider)
{
Debug.Log("当たったよ");
Destroy(collider.gameObject);
}
}
Raycast用スクリプト
using UnityEngine;
public class RaycastScript : MonoBehaviour
{
private void Update()
{
var ray = new Ray(new Vector3(this.transform.position.x + 0.1f, this.transform.position.y, this.transform.position.z), new Vector3(1, 0, 0));
if (Physics.SphereCast(ray, 0.01f, out RaycastHit hit, Mathf.Infinity, LayerMask.GetMask("Target")))
{
var dir = ray.direction * hit.distance;
Debug.DrawRay(new Vector3(this.transform.position.x + 0.5f, this.transform.position.y, this.transform.position.z), dir, Color.red, 10, false);
Debug.Log("当たったよ");
}
}
}
using UnityEngine;
public class RaycastScript : MonoBehaviour
{
private void Update()
{
var ray = new Ray(new Vector3(this.transform.position.x + 0.1f, this.transform.position.y, this.transform.position.z), new Vector3(1, 0, 0));
if (Physics.SphereCast(ray, 0.01f, out RaycastHit hit, Mathf.Infinity, LayerMask.GetMask("Target")))
{
var dir = ray.direction * hit.distance;
Debug.DrawRay(new Vector3(this.transform.position.x + 0.5f, this.transform.position.y, this.transform.position.z), dir, Color.red, 10, false);
Debug.Log("当たったよ");
// 照射先のオブジェクトを破棄する
Destroy(hit.collider.gameObject);
}
}
}
どの当たり判定を採用したか
試行錯誤はつきもの
おわりに