Open7

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

usa-chanusa-chan

unity6が出て、改めて入門しようと思ってRoll a Ballをやってみた
https://youtu.be/NgYG1_Si22A?si=CgH7YR06cp0loySI

そこでふと、「PlayerがItemと衝突したときに、UIのScoreをカウントアップするのってどう書くのがいいんだっけ?」と疑問に思った

いろいろと試しながら良さげなところを探っていきたい

usa-chanusa-chan

まずはテンプレートとなる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タグを割り当てている

usa-chanusa-chan

真っ先に思いついた方法で実装してみる
衝突時の処理をPlayerに書くのか、Itemに書くのか、それとも別のManager的なものに書くのか悩むが、衝突時にItemを消さなくてはいけないので、楽をしてItemに書いてみることにする

ItemにScoreを書き換えるスクリプトをアタッチして、OnTriggerEnterでスコアの加算と自身を消す処理を記述する

ScoreManager.cs
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しか加算されない

usa-chanusa-chan

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で公開している

ScoreManager.cs
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経由で実行している

Item.cs
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に対してもスコアが加算されていく

usa-chanusa-chan

この状態を擬似的に図に起こすとこんな感じになる

Itemが複数あるので、Prefabにしたい
が、Prefabには[SelializeField]で外出ししたScoreManagerをアタッチできない

PrefabとSceneで存在する場所が違うらしくアタッチがうまくできない

解決策としては、例えば、ScoreManagerをアタッチした状態のItemをテンプレートとしてSceneに1つ配置しておいて、それをcloneで複製するようにすればScoreManagerがアタッチされた状態のItemを複数配置することができる

参考: 【Unity】動的に増やすGameObjectは別にPrefabでなくとも良い

例えばこんな感じでItemManagerを作ってあげるといい

ItemManager.cs
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

usa-chanusa-chan

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

usa-chanusa-chan

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