【第３回】保存したスコアでオブジェクトの生成数を管理する【Unity】

# はじめに
普段は企画職を中心に学んでいる私が、株式会社GENEROSITY様での臨地実務実習で行わせていただいた、XR技術を使ったサービスの企画発案から価値検証モデル(Minimum Viable Product, 通称：MVP)の開発内容を全３回に分けて記事にさせていただきます。
最後の第３回では、スコアを管理するマネージャーの作成と第１回と第２回に作成したものを改良し、シーンごとに連続性を持たせて一つの作品にしたいと思います。
# 目次

[概要](#概要)
[今回のゴール](#今回のゴール)
[環境](#環境)
[実装](#実装) 
[準備](#準備)
[スクリプトの作成](#スクリプトの作成)
[実行](#実行)
[まとめ](#まとめ)

# 概要

https://qiita.com/Hikari_Fukazawa/private/9d4606b3115bbde712dc

https://qiita.com/Hikari_Fukazawa/private/c8595e35a681521b3a4f

私は「他人に言いにくい本音を抱えているが、誰にも見られたくない」といった課題を解決するために、本音を一人で解消するためのスマホ向けアプリ「本音バルーン」を作成いたしました。
視覚的なカタルシス(爽快感やストレスの解消)を体験した人がより感じるようにするため、ARを活用した[ゲーミフィケーション](https://www.youtube.com/watch?v=IXjtRpQKtsI)要素を企画に取り入れました。
私が開発したMVPの全体の流れを動画にしたので、ぜひご覧ください！(0.75倍速推奨)
リリースまでは至っておらず、あくまでもMVPであることをご了承ください。

<iframe width="560" height="315" src="https://www.youtube.com/embed/WVW-WBDzyF0?si=qD9Yadt14TehdPpy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

# 今回のゴール

**ScoreManager**スクリプトを作成し、第１回のシーンと第２回のシーンを繋げます。
文字数をスコアカウントに変換し、スコア量の風船を生成します。

![screen-20241112-135249-ezgif.com-cut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/4626cbce-c853-b318-6cdc-970acf7e696e.gif)

# 環境

#### OS
Windows11
#### Unityバージョン
Unity - 2022.3.49fi
#### テストデバイス
Google Pixel 6(Android 12)

:::note warn
Unity上のSimulatorは実装されていないのでGoogle Pixel 5で行っています
:::

# 実装
## 準備
第１回で作成したシーンと第２回で作成したシーンとスクリプトを全て同じプロジェクト内に保存します。
以前までのスクリプトを残しておきたい人は新しくスクリプトを作成してください。(記事では同名で行います。)

## スクリプトの作成
### ScoreManagerスクリプト
文字数に応じたスコアを加算して保存しておくためのスクリプトです。シングルトン化していることで、シーンを跨いでも削除されることがありません。また、同シーン内で２つ以上存在しないようにするため、既にシーンがスクリプトを保持している場合は削除されるようになっています。

```php:ScoreManager
using TMPro; // TextMeshProを使用するために追加
using UnityEngine;
using UnityEngine.Events;

public class ScoreManager : MonoBehaviour
{
    public static ScoreManager instance;
    private int totalBalloonsCount = 0; // 取得したオブジェクトの総数
    private int score;

    private void Awake()
    {
        if (instance == null)
        {
            instance = this;
            DontDestroyOnLoad(gameObject);  // シーンが切り替わっても破棄しない
            OnScoreReset = new UnityEvent(); // イベントを初期化
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private void Start()
    {
        // 初期表示のための更新を行う
        UpdateScoreDisplay();
    }

    private void Update()
    {
        UpdateScoreDisplay();
    }

    public void AddScore(int amount)
    {
        score += amount;
        totalBalloonsCount += amount;
        PlayerPrefs.SetInt("PlayerScore", score);  // スコアを保存
        UpdateScoreDisplay(); // スコア追加時に表示を更新
    }

    public int GetScore()
    {
        return score;
    }
    
```

### SceneControllerスクリプト
シーンを移動する処理を追加するため、`using UnityEngine.SceneManagement;`を追加しました。

```php:SceneController
using System.Collections;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement; //追加必須
using UnityScreenNavigator.Runtime.Core.Modal;

public class SceneController : MonoBehaviour
{
    [SerializeField]
    private ModalContainer _modalContainer = null;  
    public GameObject inputGameObject;
    public Button button;
    public TMP_InputField inputField;
    
    // テキストの文字数を保存する変数
    private int textLength;

    // シングルトンインスタンスの設定
    public static SceneController Instance { get; private set; }

    private void Awake()
    {
        // インスタンスのセットアップ。既にある場合は削除
        if (Instance == null) Instance = this;
        else Destroy(gameObject);
    }

}
```
文字数を管理するメソッドにオブジェクト数を指定するメソッドを追加します。`InputField`には100字まで入力が可能なので、「～文字ならa～b個」と範囲を区切ることでオブジェクトの生成数をランダムに取得します。
```php:SceneConroller
    public int CalculateObjectCount(int textLength)
    {
        if (textLength < 6) // 6文字未満
            return 0;
        else if (textLength <= 10) // 6～10文字
            return Random.Range(10, 15); // 10〜14の範囲
        else if (textLength >= 11 && textLength <= 15) // 11～15文字
            return Random.Range(15, 20); // 15〜19の範囲
        else if (textLength >= 16 && textLength <= 20) // 16～20文字
            return Random.Range(20, 25); // 20〜24の範囲
        else if (textLength >= 21 && textLength <= 30) // 21～30文字
            return Random.Range(25, 30); // 25〜30の範囲
        else if (textLength >= 31 && textLength <= 75) // 31～75文字
            return Random.Range(30, 35); // 25〜30の範囲
        else // 75文字以上
            return Random.Range(35, 41); // 25〜30の範囲
    }
```
元シーンのボタンUIメソッドに下記のコードを追加します。先ほど取得したオブジェクトの生成数を計算し、その数だけスコアを増加させます。
```php:SceneController
     private IEnumerator NextButton()
     {
         //追加
        // テキスト量に基づいたオブジェクト数を計算
        int objectCount = CalculateObjectCount(textLength);
        
        // ScoreManagerのインスタンスを取得
        ScoreManager scoreManager = ScoreManager.instance;
        
        // ObjectCountの数だけスコアを増加させる
        if (scoreManager != null)
        {
            scoreManager.AddScore(objectCount); // ObjectCountの数だけスコアを追加
        }

     }
```

### OptionSelectorModalスクリプト
スクリプト自体に修正はありませんが、OKボタンにARカメラのシーンを指定します。

ここまでで文字入力のシーンからARカメラのシーンへの移動が行えると思います。
次にARカメラシーンのスクリプトを改良します。

### RandomObejectPlacement
**ScoreManager**から先ほどカウントした生成数を受け取り、最大生成数内の数だけAR空間に生成します。生成数を受け取っていない場合、デフォルト数だけオブジェクトを生成します。
```php:RandomObejectPlacement
public class RandomObjectPlacement : MonoBehaviour
{
        private const int maxObjects = 30; // 最大オブジェクト数
        
    public void PlaceObjects()
    {
        // ScoreManagerからスコアを取得してオブジェクト数を設定
        ScoreManager scoreManager = ScoreManager.instance;
    
        if (scoreManager != null && scoreManager.GetScore() > 0)
        {
            int targetObjectCount = Mathf.Min(scoreManager.GetScore(), maxObjects);
            int additionalObjects = targetObjectCount - placedObjectPositions.Count; // 追加するオブジェクト数
    
            if (additionalObjects > 0)
            {
                PlaceObjectsRandomly(additionalObjects);
            }
        }
        else
        {
            // スコアがない場合はデフォルトで指定された数のオブジェクトを生成
            PlaceDefaultObjects();
        }
    }

    public void PlaceDefaultObjects()
    {
        // デフォルトのオブジェクト数を生成
        int additionalObjects = defaultObjectCount - placedObjectPositions.Count;
    
        if (additionalObjects > 0)
        {
            PlaceObjectsRandomly(additionalObjects);
        }
    }
```

### ObjectManagerスクリプト

変更なし。

### ObjectInteractionスクリプト

オブジェクトが削除されたら、スコアを1減らすコードを追加しました。

```php:ObjectInteraction

public class ObjectInteraction : MonoBehaviour
{
    private void HandleObjectInteraction(GameObject target)
    {
        // effectPrefabsとeffectPrefabs1のそれぞれからランダムにエフェクトを選択し、同時に生成する
        int randomIndex = Random.Range(0, effectPrefabs.Count);
        int randomIndex1 = Random.Range(0, effectPrefabs1.Count);
        GameObject effectPrefab = effectPrefabs[randomIndex];
        GameObject effectPrefab1 = effectPrefabs1[randomIndex1];
    
        // エフェクトを生成し、一定時間後に削除
        GameObject effectInstance = Instantiate(effectPrefab, target.transform.position, Quaternion.identity);
        GameObject effectInstance1 = Instantiate(effectPrefab1, target.transform.position, Quaternion.identity);
        Destroy(effectInstance, 2f);
        Destroy(effectInstance1, 2f);   
        
        // オブジェクトを削除とリスト管理
        DestroyObject(target);
        ScoreManager.instance.SubtractScore(1); //追加
    
        // ObjectManagerのリストからオブジェクトを削除
        objectManager?.RemoveObjectFromList(target);
    
        // ヒットカウントの更新と削除確認
        RemoveObjectCount();
    }
}
```

# 実行

正しく実行されたなら動画のように文字数に応じた数のオブジェクトがARシーン上に生成されると思います。
これにより、今回のゴールであった2つのシーンに連続性を持たせることが出来ました。

![screen-20241112-135249-ezgif.com-cut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/75397f7e-bffd-4d2a-7409-134525bbeabe.gif)

# まとめ

全回に渡って私の開発したMVPの要素を説明させていただきました。実物はこれらの要素に＋α(UXの向上)を加えた、もう少しクオリティが担保された物となっています。([概要](*概要)の動画をご覧ください)
他にも、オブジェクトが無限に生成される中ひたすら風船を割り続けるモードもボーナス要素で付け加えたり、偽のローディング処理を挟んで実際のアプリっぽさを増したり、特定のフラグを踏むことで現れるモーダルなんかも追加しました。(運営の遊び心を表現したかった)

実習自体はこの開発で終わりとなってしまいましたが、私の行ったことがこのブログとして誰かの目に留まって少しでも役に立つものになれば嬉しいです。