UnityでPlayerがItemに衝突したときにScoreをカウントする方法

unity6が出て、改めて入門しようと思ってRoll a Ballをやってみた
そこでふと、「PlayerがItemと衝突したときに、UIのScoreをカウントアップするのってどう書くのがいいんだっけ?」と疑問に思った
いろいろと試しながら良さげなところを探っていきたい

まずはテンプレートとなるSceneを作成する
使用するunityのバージョンは6000.0.27f1
Hierarchyはこんな感じ
TemplateScene
├── Main Camera
├── Directional Light
├── Global Volume
├── Ground (Cube)
├── Item (Cube)
│ └── Box Collider (Is Trigger: true)
├── Player (Sphere)
│ ├── Sphere Collider
│ └── Rigidbody
├── Canvas
│ └── Score (Text - TextMeshPro)
└── EventSystem
単順にするために、PlayerはRigidbodyで重力に従って落下するだけにしている(Playerの移動操作は実装しない)
Playerが重力で落下してItemと衝突すると、左上のScoreが加算されて「Score: 1」になり、Itemは消失する
PlayerとItemには衝突判定用にそれぞれPlayerタグとItemタグを割り当てている

真っ先に思いついた方法で実装してみる
衝突時の処理をPlayerに書くのか、Itemに書くのか、それとも別のManager的なものに書くのか悩むが、衝突時にItemを消さなくてはいけないので、楽をしてItemに書いてみることにする
ItemにScoreを書き換えるスクリプトをアタッチして、OnTriggerEnter
でスコアの加算と自身を消す処理を記述する
using TMPro;
using UnityEngine;
public class ScoreManager : MonoBehaviour
{
[SerializeField] private GameObject scoreUI;
private int _score;
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
_score += 1;
DisplayScore();
Destroy(gameObject);
}
}
private void DisplayScore()
{
scoreUI.GetComponent<TextMeshProUGUI>().text = "Score: " + _score;
}
}
SelializeField
で更新するスコアのUIをアタッチして、スコアの加算と表示の更新を行う
スコアもこのスクリプトの中に持たせる
これで動かすと、PlayerとItemの衝突でScoreが加算されてItemが消える
が、しかしItemを2つ以上cloneしたときに2つともItemは消えるが、スコアは1しか加算されない

Itemが2つ以上あるときにも対応できるように、スコア情報の管理を別オブジェクトに切り出してみる
新しく空のGameObjectとしてScoreManagerを作成し、ScoreManagerのスクリプトをアタッチする
TemplateScene
├── Main Camera
├── Directional Light
├── Global Volume
├── Ground (Cube)
├── Item1 (Cube)
│ └── Box Collider (Is Trigger: true)
+ ├── Item2 (Cube)
+ │ └── Box Collider (Is Trigger: true)
+ ├── Item3 (Cube)
+ │ └── Box Collider (Is Trigger: true)
├── Player (Sphere)
│ ├── Sphere Collider
│ └── Rigidbody
├── Canvas
│ └── Score (Text - TextMeshPro)
├── EventSystem
+ └── ScoreManager
ScoreManagerにアタッチするスクリプトはこんな感じ
スコア自体と加算用のメソッドと表示用のメソッドを作ってpublicで公開している
using TMPro;
using UnityEngine;
public class ScoreManager : MonoBehaviour
{
[SerializeField] private GameObject scoreUI;
private int _score;
public void AddScore()
{
_score += 1;
}
public void DisplayScore()
{
scoreUI.GetComponent<TextMeshProUGUI>().text = "Score: " + _score;
}
}
各Itemにアタッチされているスクリプトはこんな感じ
先ほどと違い、スコア関連の処理はScoreManager経由で実行している
using UnityEngine;
public class Item : MonoBehaviour
{
[SerializeField] private ScoreManager scoreManager;
private void OnTriggerEnter(Collider other)
{
if (other.CompareTag("Player"))
{
scoreManager.AddScore();
scoreManager.DisplayScore();
Destroy(gameObject);
}
}
}
これで実行すると、複数のItemに対してもスコアが加算されていく

この状態を擬似的に図に起こすとこんな感じになる
Itemが複数あるので、Prefabにしたい
が、Prefabには[SelializeField]
で外出ししたScoreManagerをアタッチできない
PrefabとSceneで存在する場所が違うらしくアタッチがうまくできない
解決策としては、例えば、ScoreManagerをアタッチした状態のItemをテンプレートとしてSceneに1つ配置しておいて、それをcloneで複製するようにすればScoreManagerがアタッチされた状態のItemを複数配置することができる
参考: 【Unity】動的に増やすGameObjectは別にPrefabでなくとも良い
例えばこんな感じでItemManagerを作ってあげるといい
using UnityEngine;
public class ItemManager : MonoBehaviour
{
private const int ItemNumber = 3;
[SerializeField] private GameObject item;
private void Start()
{
for (var i = 0; i < ItemNumber; i++) Instantiate(item);
}
}
このとき、複製元になるオリジナルはSetActiveをfalseにしたGameObjectの子にしておくと非表示になって使い勝手が良い
ItemsはSetActiveがfalse, Item自体はtrue

これでいい気もするが、できればItemはprefabにしたい
ついでにItemはScoreManagerのことを知らなくてもいいようにしたい

Itemが複数あるので、雑に実現しようとするとScoreManagerがUpdate
の中で毎フレームFindObject
を使ってItemの残り個数を数えてScoreに反映するとかを思いつく
ただ、ただでさえ重いFindObject
を毎フレーム走らせるようなことはしたくない