レースゲームやかけっこゲームを作っていると、「複数のオブジェクトが同時にゴールへ向かうとき、どの順番で着いたかを判定してテキストで表示したい」という場面にぶつかりますよね。
OnTriggerEnterやListを使えば実現できそうだと分かっていても、実際にどう組み合わせればいいのか迷ってしまう方も多いのではないでしょうか。
この記事では、3つのPrefabをランダムな速度で走らせ、ゴールに到達した順番を判定して「1位」「2位」「3位」とオブジェクト名をリアルタイムでテキスト表示する方法を、順を追って解説していきます。
Unityで複数オブジェクトの到達順を判定する仕組みとは
到達順の判定と聞くと難しそうに感じるかもしれませんが、実はやっていることはとてもシンプルです。大きく分けると「到達を検知する」「到達した順番を記録する」「記録した内容をテキストに反映する」という3つの工程の組み合わせでできています。
まず、動いているオブジェクトがゴールラインに触れたことをOnTriggerEnterというイベントで検知します。次に、触れた順番のままオブジェクトの名前をListというデータの入れ物に追加していきます。ゴールに早く着いたオブジェクトほど、このリストの先頭に近い位置に名前が入る仕組みです。
最後に、ゴールした数をカウントする変数を用意しておき、その数に応じて「1位のテキストには何位のオブジェクト名を表示するか」を切り替えます。たとえば1人目がゴールした瞬間はリストの1番目、2人目がゴールした瞬間はリストの2番目を見に行く、というイメージです。
ワンポイント
「検知」「記録」「表示」という3つの役割を意識しながら読み進めると、この後に出てくるスクリプトがどの役割を担っているのか整理しやすくなりますよ。
この後の見出しでは、この3つの工程をひとつずつ実際のスクリプトと一緒に組み立てていきます。まずは移動用のオブジェクトを準備するところから始めましょう。
到達順判定に必要な移動用Prefabを準備する
まずは、実際にゴールへ向かって走ってくれるオブジェクトの土台を作っていきましょう。ポイントは「Rigidbody」「Box Collider(Is Trigger)」「Tag」の3つを正しく設定することです。
この設定を行うことで、後から追加する移動スクリプトやゴール判定のスクリプトが正しく動くようになります。それでは順番に手を動かしていきましょう。
- ヒエラルキービューで右クリックし、「3D Object」から「Cube」を新規作成する
- 作成した「Cube」を選択し、インスペクタービューから「Add Component」で「Rigidbody」を追加する
- 「Box Collider」コンポーネントにある「Is Trigger」にチェックを入れる
- インスペクター上部の「Tag」から「Add Tag…」を選び、新規タグとして「Target」を作成する
- 作成した「Target」タグを、この「Cube」に設定する
タグとトリガーの設定が終わったら、次はこのCubeを繰り返し使い回せる「Prefab」という素材として保存しておきます。
Rigidbodyの「UseGravity」をオフにする理由
Cubeに追加した「Rigidbody」コンポーネントには、「UseGravity」という項目があります。このチェックは必ず外しておいてください。
注意
UseGravityがオンのままだと、重力の影響でCubeが下に落ち続けてしまい、思い通りに前へ進んでくれません。ゴール判定の前提が崩れてしまうので、忘れずにチェックを外しておきましょう。
Box Colliderを「Is Trigger」にする理由
「Is Trigger」にチェックを入れると、Cube同士がぶつかっても物理的に押し返し合うことがなくなり、すり抜けるような挙動になります。そのかわり、OnTriggerEnterというイベントで「何かが触れた」ことだけを検知できるようになります。
今回作りたいのは物理的な衝突ではなく、あくまで「ゴールラインに触れたかどうか」の判定です。そのため、Is Triggerをオンにしておくことが欠かせない設定になります。
設定がひと通り終わったら、ヒエラルキー上の「Cube」をプロジェクトビューにドラッグ&ドロップしてPrefab化しましょう。Prefab化できたら、ヒエラルキーに残っている元の「Cube」は削除しておいて問題ありません。
ランダムな速度で自動移動させるスクリプトの書き方
Prefabの土台ができたら、次は実際にCubeを動かすスクリプトを作っていきます。ここでのポイントは、Random.Rangeを使って移動速度にばらつきを持たせることです。速度に差をつけることで、複数のCubeがバラバラのタイミングでゴールに到達するようになります。
それでは、移動用のスクリプトを作成していきましょう。
- プロジェクトビューで右クリックし、「Create」から「C# Script」を選択する
- スクリプト名を「CubeMove」に変更する
- 作成した「CubeMove」スクリプトを、Prefab化する前のCube(または保存済みのPrefab)にアタッチする
- スクリプトをダブルクリックして開き、以下のコードを記述する
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeMove : MonoBehaviour
{
private float move;
void Start()
{
// 移動速度を3.0fから5.0fの範囲でランダムに決定し、到着タイミングに差をつける
move = Random.Range(3.0f, 5.0f);
}
void Update()
{
// Z軸方向(前方)へ毎フレーム移動させる
transform.Translate(0f, 0f, move * Time.deltaTime);
}
}
コードの中身を簡単に見ていきましょう。Start()の中にあるRandom.Range(3.0f, 5.0f)は、ゲームが始まった瞬間に一度だけ実行され、3.0から5.0の間でランダムな数値をmoveという変数に入れています。この数値がそのままCubeの移動速度になります。
そしてUpdate()の中にあるtransform.Translateが、毎フレームCubeをZ軸方向(前方)に少しずつ動かしている部分です。Time.deltaTimeをかけることで、パソコンの処理speed(性能)に関係なく、どの環境でも同じ速さで動くようになっています。
ワンポイント
速度の範囲をRandom.Range(3.0f, 5.0f)から、たとえばRandom.Range(1.0f, 10.0f)のように変更すると、到着タイミングの差がさらに大きくなります。動きの雰囲気を見ながら好みの数値に調整してみてください。
スクリプトが書けたら、Cubeが実際に前へ進むかどうか一度再生して確認しておくと安心です。動きが確認できたら、次はこのCubeをゴール地点に向けて複数配置していく準備に進みましょう。
スタート地点に複数のPrefabを自動生成する方法
移動するCubeが準備できたら、次はそのCubeをゲーム開始と同時に3つ自動で並べて配置する仕組みを作っていきます。ここでは、ゴール地点に置くオブジェクトにInstantiateという機能を使ってCubeを生成する処理を書いていきます。
まずは、ゴールラインとなる空のオブジェクトを用意するところから始めましょう。
- ヒエラルキービューで右クリックし、「Create Empty」で空のオブジェクトを作成する
- 作成したオブジェクトに「Box Collider」コンポーネントを追加する
- ゴールラインとして機能するように、Box ColliderのPosition・Sizeを調整して配置する
- プロジェクトビューで新規C#スクリプト「CubeClone」を作成し、このゴールオブジェクトにアタッチする
スクリプトをアタッチできたら、Cubeを自動生成する処理を記述していきます。次の見出しで紹介するゴール判定の処理もあわせてこのスクリプトに書いていくので、まずは生成部分だけ見ていきましょう。
[SerializeField] GameObject prefab; // 生成するCubeのPrefabを指定する
private float xPos;
void Start()
{
xPos = -3.0f; // 初期生成位置のX座標を設定
// 3つのCubeオブジェクトを等間隔(1.5f刻み)に自動生成し、連番の名前を付ける
for(int i = 0; i < 3; i++)
{
xPos += 1.5f;
GameObject cube = Instantiate(prefab, new Vector3(xPos, 0, 0), Quaternion.identity);
cube.name = "Cube" + i.ToString();
}
}
for文を使って3回処理を繰り返すことで、Cubeを3つまとめて生成しています。xPosという変数にループのたびに1.5fずつ足していくことで、CubeどうしがX軸方向に等間隔で並ぶようになっています。
生成したCubeにはcube.name = "Cube" + i.ToString();という処理で、「Cube0」「Cube1」「Cube2」という連番の名前を自動でつけています。この名前が、後ほどゴール判定のリストに表示される際の識別名としてそのまま使われます。
Instantiateの基本的な使い方や、生成位置の指定方法についてさらに詳しく知りたい場合は、こちらの記事もあわせて参考にしてみてください。
インスペクター上の「Prefab」フィールドには、先ほど作成したCubeのPrefabアセットをドラッグ&ドロップして割り当てておきましょう。これで、再生ボタンを押した瞬間に3つのCubeがスタート地点に並んで生成されるようになります。
ゴール到達を検知して順位をリストに記録する仕組み
Cubeが自動で生成できるようになったら、いよいよ今回の記事の核心部分です。ゴールラインに触れたCubeを検知して、触れた順番のままリストに名前を記録していく処理を書いていきます。
先ほど作成した「CubeClone」スクリプトに、続けて処理を追加していきましょう。
OnTriggerEnterで到達を検知する
まずは、Cubeがゴールラインに触れた瞬間を検知する部分です。何かがトリガーに触れたときに呼び出されるOnTriggerEnterというイベントを使って、触れた相手が「Target」タグを持っているかどうかを確認します。
// トリガー(ゴールライン)にオブジェクトが接触した際の処理
void OnTriggerEnter(Collider other)
{
// 接触したオブジェクトのタグが「Target」であるか確認
if (other.gameObject.tag == "Target")
{
// ここに到達順を記録する処理を書いていく
}
}
if文でタグを確認しているのは、ゴールラインに触れる可能性があるオブジェクトが、走らせているCube以外にもあるかもしれないためです。「Target」タグを持つオブジェクトだけを対象にすることで、意図しないオブジェクトまで順位に数えてしまうミスを防げます。
OnTriggerEnterとよく似た機能に、OnCollisionEnterというイベントもあります。名前は似ていますが、Colliderの「Is Trigger」がオンのときはOnTriggerEnterが、オフのときはOnCollisionEnterが呼び出されるという違いがあるので、今回のようにIs Triggerをオンにしている場合はOnTriggerEnterを使う、と覚えておくと迷いにくくなります。
Listに到達順で名前を記録する
タグの確認ができたら、次は実際にゴールした順番を記録していきます。ここで活躍するのがListというデータの入れ物です。
public List<string> myList = new List<string>(); // ゴールした順にオブジェクト名を格納するリスト
private int count; // ゴールしたオブジェクトの数をカウントする変数
void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "Target")
{
// ゴールした順番にオブジェクト名をリストに格納する
myList.Add(other.gameObject.name);
// ゴールしたオブジェクトを非表示(非アクティブ化)にする
other.gameObject.SetActive(false);
// ゴール数を加算
count++;
}
}
myList.Add(other.gameObject.name);という一文で、ゴールに触れたCubeの名前を、触れた順番のままリストの後ろに追加しています。1番早く着いたCubeの名前がmyListの0番目、2番目に着いたCubeの名前が1番目、というように自動的に並んでいく仕組みです。
続くother.gameObject.SetActive(false);では、ゴールしたCubeを非表示にしています。ここで注意したいのが、非表示にしても名前の情報はすでにmyListに記録済みなので消えることはない、という点です。画面から見えなくなるのはあくまで見た目の話で、順位のデータには影響しません。
ワンポイント
Listの使い方や、配列・Dictionaryとの違いについてもっと詳しく知りたい場合は、こちらの記事も参考になります。
また、SetActiveによる非表示化と似たような処理に「Destroy」でオブジェクトそのものを消してしまう方法もありますが、両者は意味が異なります。より詳しい違いを知りたい方は、以下の記事もあわせてチェックしてみてください。
ここまでで、ゴールに触れたCubeの名前を順番通りにリストへ記録する処理が完成しました。次は、このリストの中身を実際の画面上のテキストに反映させていきます。
記録した順位をUIテキストにリアルタイム表示する方法
ゴールした順番がリストに記録できるようになったら、いよいよその情報を画面上のテキストに反映させていきます。ここでは、ゴールした数をカウントするcountという変数を使い、その数に応じて表示するテキストを切り替えていきます。
count変数と到達数に応じたテキスト更新処理
まずは、リストに記録された名前をUI Textへ反映させる処理をUpdate()の中に書いていきましょう。
public Text firstText; // 1位表示用UIテキスト
public Text secondText; // 2位表示用UIテキスト
public Text thirdText; // 3位表示用UIテキスト
void Update()
{
// ゴールした数に応じて、対応する順位のテキストを更新する
if(count == 1)
{
firstText.text = "1位: " + myList[0];
}
else if(count == 2)
{
secondText.text = "2位: " + myList[1];
}
else if(count == 3)
{
thirdText.text = "3位: " + myList[2];
}
}
この処理では、countが1になった瞬間にmyList[0]、つまりリストの一番先頭に入っている名前を1位のテキストに表示しています。同じように、countが2になればmyList[1]を2位のテキストへ、3になればmyList[2]を3位のテキストへ反映する、という流れです。
ここで少し戸惑いやすいのが、リストの番号が0番目から始まっているという点です。myList[0]は「0番目」ではなく「1位」を指しているので、count(何人ゴールしたか)とリストの番号がひとつずれている、と考えると整理しやすくなります。
カウントを使ったオブジェクトの数え方について、もう少し基礎から確認しておきたい場合は、以下の記事も参考にしてみてください。
UI Text(1位・2位・3位)の作成とインスペクターでの紐付け
スクリプト側の準備ができたら、次は実際に画面へ表示するためのUI Textを用意していきます。
- ヒエラルキービューで右クリックし、「UI」から「Text」を選択して作成する(同様の手順であと2つ作成し、合計3つ用意する)
- それぞれの名前を「FirstText」「SecondText」「ThirdText」など、見分けやすいものに変更する
- 各TextのRect Transformにある「PosX」「PosY」を調整し、画面上で横並びになるように配置する
- 「CubeClone」スクリプトをアタッチしたゴールオブジェクトを選択する
- インスペクター上の「First Text」「Second Text」「Third Text」の各フィールドに、それぞれ対応するUI Textオブジェクトをドラッグ&ドロップして割り当てる
3つのフィールドすべてに割り当てが終わったら、再生ボタンを押して動作を確認してみましょう。Cubeがランダムな速度で走り出し、ゴールに触れた順番で「1位: Cube0」のように表示が切り替わっていけば成功です。
順位がずれる・正しく表示されない時によくある原因
ここまでの手順通りに実装しても、実際に動かしてみると「表示がおかしい」「思った通りに順位が出ない」ということがあります。多くの場合、原因は次の3つのうちのどれかに当てはまります。
自分の症状がどれに近いか、照らし合わせながら確認してみてください。
1位のテキストしか表示されず、2位・3位が更新されない場合は、Update()内のifとelse ifの条件分岐に書き間違いがないかを見直してみましょう。count == 1のブロックだけが実行され続けている状態は、countが正しく加算されていないか、条件式の比較演算子に誤りがあるケースがほとんどです。
途中でエラーが出て動作が止まってしまう場合は、myListの中身が空の状態でmyList[0]などを参照してしまっている可能性があります。OnTriggerEnterで名前が正しく追加される前にUpdateが先に呼ばれてしまうと、リストの範囲外を読み込もうとしてエラーになることがあるので、Add処理とcountの加算が同じタイミングで行われているかを確認してみてください。
Cubeが消えたのにゴールの記録も一緒に消えたと勘違いしてしまう場合は、SetActive(false)とDestroyの違いを整理しておくと安心です。SetActive(false)はあくまで見た目を非表示にしているだけで、名前の情報はその前のmyList.Addですでに記録済みのため、消えることはありません。
注意
OnTriggerEnterが呼ばれない場合は、CubeとゴールオブジェクトのどちらかにColliderが設定されていない、またはCube側の「Is Trigger」にチェックが入っていないことが多いです。両方のオブジェクトのCollider設定を見直してみてください。
こうした症状は、どれもスクリプトの一部分だけを見直せば解決できるものがほとんどです。焦らずひとつずつ確認していけば、原因は意外とすぐに見つかりますよ。
よくある質問
- Q4体以上のオブジェクトにも同じ方法で順位判定はできますか?
- A今回紹介した方法は、あらかじめ3位までのテキストを用意し、countの値ごとにif文で分岐させる3体固定の作りになっています。そのため4体以上に増やす場合は、if文を並べる形ではなく、for文でリストの中身を順番にテキストへ反映させるような書き方に変更する必要があります。
- Q複数のオブジェクトが同時にゴールした場合、順位はどうなりますか?
- A見た目上は同時に見えても、OnTriggerEnterが呼び出される順番はUnity内部の処理順に依存しており、完全に同じタイミングで扱われるわけではありません。そのため、ごくわずかな処理順の差によって、どちらかが先にリストへ記録される形になります。
- QUI TextではなくTextMeshProを使いたい場合はどう書き換えればいいですか?
- Aスクリプト冒頭に using TMPro; を追加したうえで、Text型で宣言していた firstText・secondText・thirdText を TMP_Text型に変更するだけで、今回紹介した処理の流れはそのまま利用できます。インスペクター上での割り当て方法も、UI Textと同じ手順で問題ありません。











※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。
※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。