kenogadgetsです。
皆さんUnity使ってますか?
Unityでゲームを作ろうとしてほぼ必ず必要となるのが「当たり判定」ですよね。
でも…
「Rigidbody」使いたくなーい!
ってときありません?物理的な処理はゲームを重くしそうだし、思っても見なかった挙動になるときもあります。
だから僕はそれを使わないで当たり判定取る方法を探しました。
以下調査結果です。
1. 2DならColliderだけで出来る
どうやらUnityの2Dなら当たり判定はColliderのみで出来るようです。
方法は以下の通り
https://docs.unity3d.com/6000.1/Documentation/ScriptReference/Collider2D.Overlap.html
このリンクにあるように、Collider2D系のOverlapという関数を使えば今重なってる他のColliderを取得出来るようです。
しかしどうやら3DのColliderにはこの機能はないっぽい…
2. RayCastを使った当たり判定
次に見つかったのはRayCastを使った当たり判定。これってゲームで視線の先にあるものとか銃の撃ったさきを判定するとかそんなので使うと思っていたんですが、光線の他にもBoxCastやSphereCastなど、いくつかの形のCastを飛ばすことができるらしい。飛ばすというか、使い方的には指定した位置に配置するという感じですね。
これを使うことで普通にできるじゃん!と思ったらどうやらオブジェクトの中心に配置することはできない?らしくちょっとずらさなきゃいけないのが難しい。これもちょっと使いづらいなという所感。
結局…
自分で作ることにしました。だって色々使いづらいのだもん。
僕の理想の当たり判定は以下の通り
・Colliderをつけてるもの同士はぶつかるようにする
・ColliderのIsTriggerをOnにするとすり抜けるようになり諸々の判定に使える
・Colliderの範囲をエディターで変えられるようにする
こんな感じ。
この前述の要件を満たした新しいColliderを作ることにしました。
僕が考案したシステムは、まず
1. 当たり判定をとるオブジェクトを全て格納するクラスを作る
2. 新しいColliderのスクリプトを作成し、当たり判定をとりたいオブジェクトに付ける
3. ①に格納されているオブジェクト全てで総当たりで当たり判定をする
という仕組みで作っていきます。
以下作り方になります。気になる方がいたら一緒に作ってみましょう。
なお、今回のバージョンはUnity 2022.3.51f1です。
1. ヒエラルキー上で「空のオブジェクト」作成
これは後で当たり判定のすべてのオブジェクトを格納するデータベースにします。
名前は「AllDataBase」としましょう。
2. ヒエラルキー上で「Cylinder」作成
これが今回動かすプレイヤーになります。名前は「Player」にしときます。また、以下のTransformのように床を作りましょう。
3. スクリプト「AllDataBase.cs」と「NewCollider.cs」を作成
最初にScriptsフォルダを作成しましょう。
この中に2つのスクリプトを作成し仕組みを形作っていきます。
また、この段階でスクリプトをそれぞれのオブジェクトにアタッチしておきましょう。
プレイヤーにはあらかじめCapsuleColliderがついていると思いますが、そちらは右クリックでRemove Componentしちゃってください。
4. AllDataBaseのプログラムを書く
以下の通りにしましょう。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AllDataBase : MonoBehaviour
{
//Actorsに当たり判定をとるオブジェクトを格納する
private List Actors = new List();
private int actorCount = 0; //現在のオブジェクト数
// Start is called before the first frame update
void Start()
{
//オブジェクト数を初期化で取得
actorCount = Actors.Count;
}
//今登録されているオブジェクトのリストを取得する
public List GetAllActors()
{
return Actors;
}
//NewColliderクラスで自身を登録するための関数
//登録時何番目に登録したかの数字を返す
public int AddMySelf(GameObject g)
{
Actors.Add(g);
return actorCount++;
}
//NewColliderクラスで自身を削除するための関数
public void RemoveMySelf(GameObject g)
{
Actors.Remove(g);
}
}
5. NewColliderのプログラムを書く
こちらも同じように書きましょう
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewCollider : MonoBehaviour
{
[SerializeField] bool isTrigger = false; //isTriggerかどうか
[SerializeField] Vector3 boxSize = Vector3.one; //Colliderのサイズを決める
[SerializeField] Vector3 boxPos = Vector3.zero; //Colliderの位置を決める
private Vector3 PrePos = Vector3.zero; //ぶつかり用位置保存変数
private AllDataBase database; //データベース
private List actors = new List();
private int myNumber = 0; //データベースに登録した自身の番号
// Start is called before the first frame update
void Start()
{
PrePos = transform.position; //ぶつかり用変数
// ↓ここの中身はAllDataBaseスクリプトをアタッチしたオブジェクトの名前
database = GameObject.Find("AllDataBase").GetComponent();
myNumber = database.AddMySelf(this.gameObject); //データベースに登録
actors = database.GetAllActors(); //データベースを取得
}
// Update is called once per frame
void Update()
{
GameObject result; //ぶつかった相手の情報を保存する変数
if (OnHit(out result))
{
NewCollider nc = result.GetComponent();
if (!isTrigger && !nc.GetTriggerState())
{
//トリガーじゃない時はぶつかるように
transform.position = PrePos;
}
else
{
//トリガーで取得したいことがあったらここで
}
}
//前のフレームの位置情報を保存しておく
PrePos = transform.position;
}
public bool OnHit(out GameObject result)
{
result = null; //outで取得する当たったオブジェクトの変数
bool isLayered = false; //3軸すべてで重なっていなければそのままfalse
for (int i = 0; i < actors.Count; i++) //LISTに追加したすべてのアクターと計算
{
//自分自身も登録されているため、その場合はスキップ
if (i == myNumber) continue;
Vector3 Pos_me = this.transform.position; //自身の位置
Vector3 Scale_me = boxSize; //自身のサイズ
Vector3 Pos_you = actors[i].transform.position; //対象の位置
Vector3 Scale_you = actors[i].transform.localScale; //対象のサイズ
// ↓それぞれの中心の間の距離 ↓それぞれの半径の和
if (Mathf.Abs(Pos_me.x - Pos_you.x) <= Mathf.Abs(Scale_me.x / 2.0f + Scale_you.x / 2.0f)
&&
Mathf.Abs(Pos_me.y - Pos_you.y) <= Mathf.Abs(Scale_me.y / 2.0f + Scale_you.y / 2.0f)
&&
Mathf.Abs(Pos_me.z - Pos_you.z) <= Mathf.Abs(Scale_me.z / 2.0f + Scale_you.z / 2.0f)
)
{
//X,Y,Zそれぞれの座標で重なる関係にあればisLayeredはtrueになる
isLayered = true;
result = actors[i];
}
}
return isLayered;
}
//この関数でSceneタブのみ当たり判定の範囲が見えるようになる
void OnDrawGizmos()
{
Gizmos.color = Color.green;
Gizmos.DrawWireCube(transform.position + boxPos, boxSize);
}
void OnDestroy()
{
database.RemoveMySelf(this.gameObject);
}
}
基本的にはOnHitという関数でぶつかり計算をしています。このスクリプトの最初にAllDataBaseにあるListを全部コピーしてきて、自身とそれら全部でぶつかり計算をしています。ぶつかるものがあった場合はOnHitの引数に渡されたGameObject型の変数へぶつかっているものの情報を渡して、その後いろいろ使えるようにしています。
6. インスペクターで設定する
先程のスクリプトのGizmosの力でSceneの所のみColliderの範囲が見えていると思うのですがどうでしょうか?
それを見ながらインスペクターでプレイヤー全体がColliderに含まれるよう設定します。僕の場合はPlayerのNewColliderのBoxPos変数を(1,2,1)に設定しました。
さて、これで当たり判定は出来るはずですが、今ここにはプレイヤーしかいないので当たり判定も何もありませんね。敵のような存在を追加してみましょう。
7. ヒエラルキーで「Cube」作成
Cubeの名前はわかりやすく「Enemy」として位置は(-3,0,0)がいいですかね。
ちょっと浮いてるけど気にしない。Playerの時と同様にBoxColliderは外してください。コライダーは2つも要らんのだ。
8. 自分が動くためのスクリプト「Move.cs」作成
自分が動けるようになるために簡単に移動スクリプトを作っておきましょう。
Scriptsフォルダ内で「Move.cs」とスクリプトを作成、以下のコードを書き入れます。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Move : MonoBehaviour
{
[SerializeField] float moveSpeed = 5.0f;
// Update is called once per frame
void Update()
{
Vector3 pos = transform.position;
if (Input.GetKey(KeyCode.A))
{
pos.x -= moveSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.D))
{
pos.x += moveSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.W))
{
pos.z += moveSpeed * Time.deltaTime;
}
if (Input.GetKey(KeyCode.S))
{
pos.z -= moveSpeed * Time.deltaTime;
}
transform.position = pos;
}
}
非常に簡単な移動処理ですが良いでしょう。
コードが出来たらこれもプレイヤーにアタッチしておきましょう。
9. 敵にもColliderをアタッチ
ここ、重要です!
先程NewColliderのスクリプトを作成しましたが、このスクリプトは普通のColliderのように使うことを想定しているので、そのまま敵にもこのColliderをつけるだけで当たり判定が出来ます。
具体的にはNewColliderのStartのところで自動的にデータベースを探し、自分自身を登録しておくようになっているので勝手に当たり判定のリストに入っちゃってるのです!
便利ですよね( ^ v ^ )/
10. いざ、当たり判定
ではようやく、当たり判定ができるかどうか試してみましょう!
プレイボタンをおしてスタート。
WASDでプレイヤーを操作できるはずなのでプレイヤーを敵に向かって動かしてみましょう。
どうですか?当たりますか?
プレイ中にSceneタブを見てみるとわかりやすいかもしれません。どの辺にColliderがあって、どのようにぶつかっているのか見やすいと思います。
ちょっと面倒ですが、この手順を使えば当たり判定を「Rigidbody」なしで取ることができます!
また、この仕組みの良いところは自分で改造出来る所にあります。他にも自分が欲しい機能があればぜひご自分で追加してみてください。
最後に...ひとつ言わなければいけないことが...
じつはこのプログラムには欠陥があります。
それは「ゲーム実行中にAllDataBaseのListを増減しても反映されない」という点です。
プログラムを見てもらえばわかると思うのですが、newColliderの中でListを更新している部分がないんですね。
Updateで毎フレーム更新することも出来るのですが、今後Listのデータ量が大きくなることを考えるとAllDataBaseの中でListの増減が起こったタイミングで、NewColliderで更新をするというのがベストであると思っています。
しかしそのためにはおそらくC#のdelegateやActionなどイベント駆動型のなんちゃらっていうものを使う必要があり、僕はまだ理解していないんですね...
もうちょっと勉強して実装しますのでまた見に来てください。
ここまでありがとうございましたm(_ _)m
kenogadgets.
(補足)後日談なんですが、↑で話していた「ゲーム実行中にAllDataBaseのListを増減しても反映されない」問題について、驚きの事実がわかりました。知らなかったんですが、NewColliderのStartで取得したActorsは参照型なので更新しなくても変更が加わると自動的に変わるんだそうです。変わるというか、その変わっているところを直接参照してるってことですよね。じゃあこれでいいんだ!