【第２回】AR空間上のオブジェクトを削除するときにエフェクトを発生させる【Unity AR 入門編】

# はじめに

普段は企画職を中心に学んでいる私が、株式会社GENEROSITY様での臨地実務実習で行わせていただいた、XR技術を使ったサービスの企画発案から価値検証モデル(Minimum Viable Product, 通称：MVP)の開発内容を全３回に分けて記事にさせていただきます。
第２回では、サービスの主要素でもあったARシーンの開発についてUnity初心者でも分かるように細かく説明していきます。
# 目次

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

# 概要

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

私は「他人に言いにくい本音を抱えているが、誰にも見られたくない」といった課題を解決するために、本音を一人で解消するためのスマホ向けアプリ「本音バルーン」を作成いたしました。
視覚的なカタルシス(爽快感やストレスの解消)を体験した人がより感じるようにするため、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>

# 今回のゴール
配置されているボタンUIを除いた、風船オブジェクトの表示から画面タップでのレイキャストを利用して削除したときにエフェクトを発生させます。

![screen-20241111-120053-ezgif.com-cut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/a68cff1e-85a8-e5a2-463c-b286d3fb8797.gif)

# 環境

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

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

# 実装

## 準備
### プロジェクトの設定
All templates から `AR Mobile` を選び、プロジェクトを作成しましょう。
Unityには便利なテンプレートが多くあるので、作りたい物に合わせてうまく活用しましょう。
写真では`Project name`がデフォルトの状態になっていますが、今回は`sample`と名付けました。

![スクリーンショット 2024-11-11 132100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/dc006cb5-aeab-0200-bf39-e3c3d98699dd.png)

テンプレートから開くと画面が下図のようになっていると思います。特徴としては、`AR Foundation`を使ったAR開発に**必要**な`AR Session`、`Event System`、`XR Origin`オブジェクトがシーンに配置されていることです。また、メインカメラが`Hierarchy`の`XR Origin`オブジェクトの子オブジェクトに置かれています。本来ならば、このオブジェクトを呼ぶためのインポートを行わなければならないのですが、予めテンプレートが全て行ってくれています。

![スクリーンショット 2024-11-11 133306.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/e41c5d4c-bd27-11cb-33b8-fa4520b6f6c7.png)

今回はビルドをAndroidで行うので、Unity内でビルドのための初期設定をする必要があります。
初期設定に関しては以下のブログの2-1の部分をご覧ください。

https://qiita.com/rotoma/items/60e744ffe9258c5d5924#2-1-ar-foundation

今回もプロジェクト内で日本語を使用しているので、必要に応じて以下のブログの準備の部分を参考にしてください。

https://qiita.com/Hikari_Fukazawa/private/9d4606b3115bbde712dc#%E6%BA%96%E5%82%99  //後で変更

### 外部モデル
表示させたいオブジェクトを事前に用意しておきます。今回はAR空間に3Dオブジェクトを生成します。私は[概要](#概要)の内容に合わせた風船のオブジェクトにしています。下のサイトの無料モデルを使わせていただきました。

https://www.cgtrader.com/free-3d-models/various/various-models/red-blue-yellow-balloons

:::note warn
注意
モデルのダウンロードにはアカウント登録が必要になります！
:::

削除されたときに発生するエフェクトも用意しておきます。Unity公式のAsset Storeにて、とても素晴らしいフリーアセットがあるので活用します。

https://assetstore.unity.com/packages/vfx/particles/cartoon-fx-remaster-free-109565


## スクリプトの作成
今回のゴールは大きく分けて3つのスクリプトで制御されています。どのような役割を持っているのか含めて一つずつ説明していきます。

:::note info
3つ合わせるとエラーの原因究明が難しくなるので、
こまめなデバッグをおすすめします。
:::

### RandomObjectPlacementスクリプト
AR空間にオブジェクトをランダムな配置で生成してくれるスクリプトです。カメラからの距離を測って離れた位置に生成することが可能です。以下の数値は`Inspector`上で自由に調整することが可能なので、仕様に合わせて変更してください。

```php:RandomObjectPlacement
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class RandomObjectPlacement : MonoBehaviour
{

    public List<GameObject> objectPrefabs; // 配置するオブジェクトのプレハブリスト
    public float rangeX = 0.5f; // X軸方向の配置範囲
    public float rangeZ = 0.5f; // Z軸方向の配置範囲
    public float distanceFromCamera = 10.0f; // カメラからオブジェクトを配置する距離
    public float minYPosition = 3.0f; // オブジェクトが配置される最低のY軸の高さ
    public float minScale = 0.1f; // オブジェクトの最小スケール
    public float maxScale = 0.3f; // オブジェクトの最大スケール
    public float minDistanceBetweenObjects = 0.5f; // オブジェクト同士の最小間隔
    public int defaultObjectCount = 10; // 生成されるデフォルトのオブジェクト数

    private List<Vector3> placedObjectPositions = new List<Vector3>(); // 配置済みのオブジェクトの位置を格納するリスト
}
```
以下のスクリプトが一連のオブジェクトの生成メソッドになります。説明すると、シーン開始時にオブジェクトを生成するのですが、その際にオブジェクト同士が衝突していないかの確認の試行を指定回数内で行ってから配置を決定しています。また、オブジェクトがカメラを向けた正面から左右合わせて180度内に限定で生成されるように調整してあります。調整次第でさらに範囲を絞ることもカメラの周囲全体にすることも可能です。
```php:RandomObjectPlacement

private void Start()
    {
        // ゲーム開始時にオブジェクト配置を行う
        PlaceObjects();
    }

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

         // 指定数のオブジェクトをランダムに配置するメソッド
    void PlaceObjectsRandomly(int count)
    {
        for (int i = 0; i < count; i++)
        {
            Vector3 objectPosition; // オブジェクトの配置位置
            int maxAttempts = 200; // 配置位置を探す最大試行回数
            int attempts = 0; // 現在の試行回数
            bool positionFound = false; // 有効な配置位置が見つかったかのフラグ

            do
            {
                // カメラの正面方向を基準にランダムな角度を生成
                Vector3 forward = Camera.main.transform.forward;
                float randomAngle = Random.Range(-90f, 90f); // -90度から90度の範囲で角度を生成
                Quaternion rotation = Quaternion.Euler(0, randomAngle, 0); // 回転を計算
                Vector3 randomDirection = rotation * forward; // 回転を適用した方向を取得

                // カメラ位置から指定の距離分だけ正面方向に離れた位置を計算
                objectPosition = Camera.main.transform.position + randomDirection.normalized * (distanceFromCamera + 2);

                // オブジェクトの高さが最低Y位置より低い場合は最低値に調整
                if (objectPosition.y < minYPosition)
                {
                    objectPosition.y = minYPosition;
                }

                positionFound = true; // 一旦、配置位置が有効と仮定
                // 既存のオブジェクトとの間隔が狭すぎないかチェック
                foreach (Vector3 placedPosition in placedObjectPositions)
                {
                    // 既存の位置と距離が近い場合は無効な位置と判定
                    if (Vector3.Distance(objectPosition, placedPosition) < minDistanceBetweenObjects)
                    {
                        positionFound = false;
                        break;
                    }
                }

                attempts++; // 試行回数をインクリメント
            } while (!positionFound && attempts < maxAttempts); // 有効な位置が見つかるまでまたは最大試行回数まで繰り返す

            // 配置可能な位置が見つかった場合のみオブジェクトを生成
            if (positionFound)
            {
                int randomIndex = Random.Range(0, objectPrefabs.Count); // プレハブリストからランダムに選択
                GameObject newObject = Instantiate(objectPrefabs[randomIndex], objectPosition, Quaternion.identity); // オブジェクトを生成

                // オブジェクトのスケールをランダムに設定
                float randomScale = Random.Range(minScale, maxScale);
                newObject.transform.localScale = new Vector3(randomScale, randomScale, randomScale);

                // 配置したオブジェクトの位置をリストに追加
                placedObjectPositions.Add(objectPosition);
            }
            else
            {
                // 最大試行回数まで位置が見つからなかった場合の警告ログ
                Debug.LogWarning($"Failed to place object after {maxAttempts} attempts.");
            }
        }
    }
```
全体図
```php:RandomObjectPlacement
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class RandomObjectPlacement : MonoBehaviour
{
    public List<GameObject> objectPrefabs; // 配置するオブジェクトのプレハブリスト
    public float rangeX = 0.5f; // X軸方向の配置範囲
    public float rangeZ = 0.5f; // Z軸方向の配置範囲
    public float distanceFromCamera = 10.0f; // カメラからオブジェクトを配置する距離
    public float minYPosition = 3.0f; // オブジェクトが配置される最低のY軸の高さ
    public float minScale = 0.1f; // オブジェクトの最小スケール
    public float maxScale = 0.3f; // オブジェクトの最大スケール
    public float minDistanceBetweenObjects = 0.5f; // オブジェクト同士の最小間隔
    public int defaultObjectCount = 10; // スコアがない場合に生成されるデフォルトのオブジェクト数

    private List<Vector3> placedObjectPositions = new List<Vector3>(); // 配置済みのオブジェクトの位置を格納するリスト

    private void Start()
    {
        // ゲーム開始時にオブジェクト配置を行う
        PlaceObjects();
    }

    // オブジェクトを配置するメソッド
    public void PlaceObjects()
    {
        // ScoreManagerからスコアを取得し、オブジェクト数をスコアに応じて決定
        ScoreManager scoreManager = ScoreManager.instance;

        // ScoreManagerが存在し、スコアが0より大きい場合
        if (scoreManager != null && scoreManager.GetScore() > 0)
        {
            // スコアまたはmaxObjectsの少ない方を配置数とする
            int targetObjectCount = Mathf.Min(scoreManager.GetScore(), maxObjects);
            // 現在配置済みのオブジェクト数との差を追加するオブジェクト数とする
            int additionalObjects = targetObjectCount - placedObjectPositions.Count;

            // 追加するオブジェクトがある場合
            if (additionalObjects > 0)
            {
                PlaceObjectsRandomly(additionalObjects); // 指定数のオブジェクトをランダムに配置
            }
        }
        else
        {
            // スコアが0またはScoreManagerが存在しない場合、デフォルト数のオブジェクトを生成
            PlaceDefaultObjects();
        }
    }

    // デフォルトのオブジェクト数を生成するメソッド
    public void PlaceDefaultObjects()
    {
        // 生成されるべきオブジェクト数を計算
        int additionalObjects = defaultObjectCount - placedObjectPositions.Count;

        // 必要数のオブジェクトを配置
        if (additionalObjects > 0)
        {
            PlaceObjectsRandomly(additionalObjects);
        }
    }

    // 指定数のオブジェクトをランダムに配置するメソッド
    void PlaceObjectsRandomly(int count)
    {
        for (int i = 0; i < count; i++)
        {
            Vector3 objectPosition; // オブジェクトの配置位置
            int maxAttempts = 200; // 配置位置を探す最大試行回数
            int attempts = 0; // 現在の試行回数
            bool positionFound = false; // 有効な配置位置が見つかったかのフラグ

            do
            {
                // カメラの正面方向を基準にランダムな角度を生成
                Vector3 forward = Camera.main.transform.forward;
                float randomAngle = Random.Range(-90f, 90f); // -90度から90度の範囲で角度を生成
                Quaternion rotation = Quaternion.Euler(0, randomAngle, 0); // 回転を計算
                Vector3 randomDirection = rotation * forward; // 回転を適用した方向を取得

                // カメラ位置から指定の距離分だけ正面方向に離れた位置を計算
                objectPosition = Camera.main.transform.position + randomDirection.normalized * (distanceFromCamera + 2);

                // オブジェクトの高さが最低Y位置より低い場合は最低値に調整
                if (objectPosition.y < minYPosition)
                {
                    objectPosition.y = minYPosition;
                }

                positionFound = true; // 一旦、配置位置が有効と仮定
                // 既存のオブジェクトとの間隔が狭すぎないかチェック
                foreach (Vector3 placedPosition in placedObjectPositions)
                {
                    // 既存の位置と距離が近い場合は無効な位置と判定
                    if (Vector3.Distance(objectPosition, placedPosition) < minDistanceBetweenObjects)
                    {
                        positionFound = false;
                        break;
                    }
                }

                attempts++; // 試行回数をインクリメント
            } while (!positionFound && attempts < maxAttempts); // 有効な位置が見つかるまでまたは最大試行回数まで繰り返す

            // 配置可能な位置が見つかった場合のみオブジェクトを生成
            if (positionFound)
            {
                int randomIndex = Random.Range(0, objectPrefabs.Count); // プレハブリストからランダムに選択
                GameObject newObject = Instantiate(objectPrefabs[randomIndex], objectPosition, Quaternion.identity); // オブジェクトを生成

                // オブジェクトのスケールをランダムに設定
                float randomScale = Random.Range(minScale, maxScale);
                newObject.transform.localScale = new Vector3(randomScale, randomScale, randomScale);

                // 配置したオブジェクトの位置をリストに追加
                placedObjectPositions.Add(objectPosition);
            }
            else
            {
                // 最大試行回数まで位置が見つからなかった場合の警告ログ
                Debug.LogWarning($"Failed to place object after {maxAttempts} attempts.");
            }
        }
    }
}

```

### ObjectManagerスクリプト
生成されるオブジェクトの状態を管理します。今回のゴールのための要素に絞って説明します。

UX向上のため、シーン内に生成されたオブジェクトが合計でいくつあるのかを示すテキストを参照しています。(ここで日本語を使用しています)
spawnedObjectsリストは生成されたオブジェクトをタグの付与をしてから格納しています。これにより、手打ちでの抜けミスを防ぎます。
```php:ObjectManager
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine;
using UnityEngine.UI; // UIコンポーネントの使用に必要
using TMPro;

    public List<GameObject> spawnedObjects = new List<GameObject>();

    public class ObjectManager : MonoBehaviour
    {
        [SerializeField] private TextMeshProUGUI remainingBalloonsText; // 残り風船数の表示用テキスト
        public List<GameObject> spawnedObjects = new List<GameObject>();
    }
```
シーン開始時、ランダム生成されたオブジェクトに特定のタグを付与しています。そしてタグのついたオブジェクトをリストに格納しています。このリストに入ったオブジェクトがテキストによってカウントされます。
```php:ObjectManager
private void Start()
{
    // ObjectInteractionなどから生成されたオブジェクトを取得するため、リストを初期化
    UpdateSpawnedObjectsList();
    
    // 残り風船数を表示
    UpdateRemainingBalloonsDisplay();
}
    
        // タグが "Interactable" のオブジェクトをすべて取得してリストに格納
public void UpdateSpawnedObjectsList()
{
    spawnedObjects = new List<GameObject>(GameObject.FindGameObjectsWithTag("Interactable"));
    
    UpdateRemainingBalloonsDisplay(); // 残り風船数を更新
}
```
ここでオブジェクトの状態(表示されているか削除されたか)を確認しています。
```php:ObjectManager
// オブジェクト削除時にリストから削除し、状態を更新
public void RemoveObjectFromList(GameObject obj)
{
    if (spawnedObjects.Contains(obj))
    {
        spawnedObjects.Remove(obj);
        Debug.Log("オブジェクトを削除しました: " + obj.name); // デバッグログを追加

        // 残り風船数を更新
        UpdateRemainingBalloonsDisplay();
    }
}
```
画面に残っているオブジェクトの数を表示します。数値は他のメソッドによって常に更新されます。
```php:ObjectManager
 // 残り風船数を表示するメソッド
 public void UpdateRemainingBalloonsDisplay()
 {
     // 現在残っている風船の数を表示
     int remainingBalloons = spawnedObjects.Count;
     remainingBalloonsText.text = "残りの風船の数: " + remainingBalloons;
 }
```

全体図
```php:ObjectManager
using System.Collections.Generic;
using UnityEngine.EventSystems;
using UnityEngine;
using UnityEngine.UI; // UIコンポーネントの使用に必要
using TMPro;
    
public class ObjectManager : MonoBehaviour
{

    [SerializeField] private TextMeshProUGUI remainingBalloonsText; // 残り風船数の表示用テキスト
    public List<GameObject> spawnedObjects = new List<GameObject>();
    
    private void Start()
    {
        // ObjectInteractionなどから生成されたオブジェクトを取得するため、リストを初期化
        UpdateSpawnedObjectsList();

        // StopBarの表示をScoreManagerのスコアに基づいて更新
        // UpdateStopBarVisibility();

        // 残り風船数を表示
        UpdateRemainingBalloonsDisplay();
    }

    // タグが "Interactable" のオブジェクトをすべて取得してリストに格納
    public void UpdateSpawnedObjectsList()
    {
        spawnedObjects = new List<GameObject>(GameObject.FindGameObjectsWithTag("Interactable"));
        UpdateRemainingBalloonsDisplay(); // 残り風船数を更新
    }

    // オブジェクト削除時にリストから削除し、状態を更新
    public void RemoveObjectFromList(GameObject obj)
    {
        if (spawnedObjects.Contains(obj))
        {
            spawnedObjects.Remove(obj);
            Debug.Log("オブジェクトを削除しました: " + obj.name); // デバッグログを追加


            // 残り風船数を更新
            UpdateRemainingBalloonsDisplay();
        }
    }
}

```
### ObjectInteractionスクリプト
ランダムに生成されたオブジェクトをレイキャストヒットによって削除、同時にエフェクトを発生させます。

レイキャストヒットを行うには、シーン内の**ARRaycastManager**がアタッチされた`XR Origin`を参照しなくてはいけません。必ず参照しましょう。
他には、表示したいエフェクトを格納するリストがあります。
```php:ObjectInteraction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.XR.ARFoundation;  //Must
using UnityEngine.XR.ARSubsystems;  //Must

public class ObjectInteraction : MonoBehaviour
{
    [SerializeField] private Camera arCamera;
    [SerializeField] private ARRaycastManager raycastManager;
    public ObjectManager objectManager;
    public int HitCount; // タッチしたオブジェクトの数

    [SerializeField] private List<GameObject> effectPrefabs;
    [SerializeField] private List<GameObject> effectPrefabs1;
}
```
Updateメソッドでレイキャストヒットによるオブジェクトへの反応をしています。
レイキャストヒットで感知したオブジェクトに特定のタグが付いていた際に削除するメソッドを送っています。
```php:ObjectInteraction
void Update()
{
    if (Input.touchCount > 0 || Input.GetMouseButtonDown(0))
    {
        Vector2 touchPosition = Input.touchCount > 0 ? Input.GetTouch(0).position : (Vector2)Input.mousePosition;
        List<ARRaycastHit> hits = new List<ARRaycastHit>();

        // ARRaycastManagerを使ってタッチ位置からレイキャストを行う
        if (raycastManager.Raycast(touchPosition, hits, TrackableType.PlaneWithinPolygon))
        {
            Pose hitPose = hits[0].pose;
            // ヒットした位置にオブジェクトがあるかPhysics.Raycastで確認
            Ray ray = new Ray(arCamera.transform.position, hitPose.position - arCamera.transform.position);
            if (Physics.Raycast(ray, out RaycastHit hit))
            {
                var target = hit.collider.gameObject;
                if (target.CompareTag("Interactable"))
                {
                    // オブジェクトのインタラクションを処理
                    HandleObjectInteraction(target);
                }
            }
        }
    }
}
```
オブジェクトを削除し、エフェクトを発生させるメソッドです。エフェクトの発生は１つでもいいし、無くてもいい物なのですが、オブジェクトが削除されたと分かりやすくなる視認性と今回のサービスに欠かせなかった爽快感というUXの向上のために必須だと思い、取り入れることにしました。
別々のリストに格納されているエフェクトを同時に呼び出して発生させています。別のリストを使って管理している理由として、爆発のエフェクトと文字が飛び出すエフェクトのどちらも必ず表示されるようにしたかったからです。１つのリストに格納して２つのエフェクトをランダムに抽選して発生させるでも出来ると思うのですが、技術より先に数で押しました。
**ObjectManager**のリストにオブジェクトが削除されたことを伝え、カウントを更新させます。
```php:ObjectInteraction
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);

        // ObjectManagerのリストからオブジェクトを削除
        objectManager?.RemoveObjectFromList(target);

    }

     // オブジェクトの位置を管理リストから削除し、オブジェクトを破棄するメソッド
public void DestroyObject(GameObject obj)
     {
         placedObjectPositions.Remove(obj.transform.position); // 配置リストから削除
         Destroy(obj); // オブジェクトの削除
     }
     
```
全体図
```php:ObjectInteraction
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.XR.ARFoundation;  //Must
using UnityEngine.XR.ARSubsystems;  //Must
public class ObjectInteraction : MonoBehaviour
{
    [SerializeField] private Camera arCamera;
    [SerializeField] private ARRaycastManager raycastManager;
    public ObjectManager objectManager;
    public int HitCount; // タッチしたオブジェクトの数

    [SerializeField] private List<GameObject> effectPrefabs;
    [SerializeField] private List<GameObject> effectPrefabs1;

    void Update()
    {
        if (Input.touchCount > 0 || Input.GetMouseButtonDown(0))
        {
            Vector2 touchPosition = Input.touchCount > 0 ? Input.GetTouch(0).position : (Vector2)Input.mousePosition;
            List<ARRaycastHit> hits = new List<ARRaycastHit>();
    
            // ARRaycastManagerを使ってタッチ位置からレイキャストを行う
            if (raycastManager.Raycast(touchPosition, hits, TrackableType.PlaneWithinPolygon))
            {
                Pose hitPose = hits[0].pose;
                // ヒットした位置にオブジェクトがあるかPhysics.Raycastで確認
                Ray ray = new Ray(arCamera.transform.position, hitPose.position - arCamera.transform.position);
                if (Physics.Raycast(ray, out RaycastHit hit))
                {
                    var target = hit.collider.gameObject;
                    if (target.CompareTag("Interactable"))
                    {
                        // オブジェクトのインタラクションを処理
                        HandleObjectInteraction(target);
                    }
                }
            }
        }
    }

    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);
        
        // ObjectManagerのリストからオブジェクトを削除
        objectManager?.RemoveObjectFromList(target);

    }

    // オブジェクトの位置を管理リストから削除し、オブジェクトを破棄するメソッド
public void DestroyObject(GameObject obj)
     {
         placedObjectPositions.Remove(obj.transform.position); // 配置リストから削除
         Destroy(obj); // オブジェクトの削除
     }
     
}

```

# 実行

作成したスクリプトは全て`XR Origin`にドラッグ＆ドロップでアタッチすることが可能です。
また、`Hierarchy`上に作成した空のオブジェクト内に全てアタッチでも可能になっています。
視認性が上がることで管理しやすくなると思います。
正しく実行されていれば動画のようになるはずです。

![screen-20241111-1200061-ezgif.com-cut.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/ccd7c200-5390-bbf2-97f7-4263e1e55191.gif)

# まとめ

今回はオブジェクトの生成から削除とエフェクトの生成を記しました。実際の制作物では、**ObejectManager**がより多くの配置されているオブジェクトの表示と非表示の状態管理を行っていたり、シーンの開始時とオブジェクトが全て削除された際などのメソッドが組まれています。

![scree.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3929429/672e3c83-e219-09a7-e2c9-b921436cfe71.gif)

長かったと思いますが、お読みいただきありがとうございました。

第3回の記事も合わせてご覧ください

https://qiita.com/Hikari_Fukazawa/private/5390f9d25f486df5909e