【第4回】【Unity】【初心者向け】 AR Foundationを利用してARマーカーを認識して生成したオブジェクトにアニメーションをつける

# はじめに
この度インターンシップにて、株式会社GENEROSITY様でARコンテンツの開発を行いました。そこでの実習内容を第1回から第4回の全4部に渡って記事にさせていただきます。

最終回となる第4回では2週間行ったAR開発の内容のアニメーションに関する部分についてをまとめています。ARの技術として今回は、**ARマーカー**を認識しその上にオブジェクトを出現させるという事を行いました。第3回までのモデル生成を踏まえて内容が進んでいきますので第3回をご覧になっていない方はぜひ以下からご覧になってください。

https://qiita.com/Ryushin_Mano/private/88f042c61f234b32c7e7


# 目次
[対象者](#対象者)
[企画概要](#企画概要)
[使用ツール](#使用ツール)
[開発環境](#開発環境)
　[OS](#os)
　[Unity](#unity)
　[テストデバイス](#テストデバイス)
[制作](#制作)
　[モデルにアニメーションをつける](#モデルにアニメーションをつける)
　 [AnimatorController](#animatorcontroller)
　 [Animatorビュー](#animatorビュー)
　 [Animation](#animation)
　 [Triggerを発動するスクリプトの作成](#triggerを発動するスクリプトの作成)
　 [HydrogenAnimationController.cs](#hydrogen-animation-controllercs)
　 [OxygenAnimationController.cs](#oxygenanimationcontrollercs)
　 [MatchStickFireAnimationController.cs](#matchstickfireanimationcontrollercs)
　 [WaterAnimationController.cs](#wateranimationcontrollercs)
　[フラスコの粉砕](#フラスコの粉砕)
　 [ヒビの入った粉砕させるフラスコの準備](#ヒビの入った粉砕させるフラスコの準備)
　 　[Rigidbodyの設定](#rigidbodyの設定)
　 　[Box Colliderの設定](#box-colliderの設定)
　 [粉砕アニメーションの実行](#粉砕アニメーションの実行)
　 　[FraskCrushing.cs](#fraskcrushingcs)
　 [爆発エフェクトの準備](#爆発エフェクトの準備)
　 　[FraskCrushing.csの追加](#fraskcrushingcsの追加)
[まとめ](#まとめ)





## 対象者
**・プログラミング初心者の方**
**・UnityでのAR開発に興味がある方**




## 企画概要
企画名は**AR授業資料** 。例えば地理の学習でARを用いて仮想の地球儀を出現させ国の位置を目で見て学んだり、化学では物質の分子構造を視覚化した疑似的なモデルをARを用いて出現させることでより理解度が深まる工夫をした、頭だけでなく**視覚的情報**を与えることで子どものさらなる学力を向上を目指したコンテンツです。

今回実習では価値の検証に必要な最低限の機能を備えたプロダクト、**MVP**(Minimum Viable Product)として、**水素と酸素の混合気体の爆発実験のシミュレーション映像**を制作することにしました。

https://monstar-lab.com/dx/about/about-mvp/

## 使用ツール
**Unity**
**Visual Studio 2022**
**blender4.3**
**Adobe Illustrator 2025**
**ChatGPT**



## 開発環境
### OS
**Windows 11 Home**

### Unity
**Unity 2022.3.56f1**
開発開始時にUnity6がリリースされていましたが、Unity6でのAR開発について情報が少なかったため、過去バージョンで制作しました。

**AR Foundation ver. 5.1.5**
AR機能を提供するフレームワークです。

https://docs.unity3d.com/Packages/com.unity.xr.arfoundation@5.1/manual/index.html


### テストデバイス
**Google pixel 6 (Android 15)**
実習にてお借りした端末です。Android端末で**設定**から**開発者向けオプション**を開き、**USBデバッグをオン**にしましょう。




## 制作
第4回はアニメーションについてのまとめとフラスコの粉砕、爆発の実装を行います。

### モデルにアニメーションをつける
ここからは出現させたモデルにUnityの**Animator**を使ってアニメーションを作成し、スクリプトでそのアニメーションを呼びだすという事を行います。

以下のサイトを参考にさせていただきました。アニメーションに関していろいろやらなければいけないことが多く苦戦した箇所なので丁寧に説明したいと思います。

https://note.com/npaka/n/nec1a165866ce


#### AnimatorController
まず最初に、**Animator Controller**というオブジェクトのアニメーションを管理するものがあるのでそれを用意します。

`Project`の`Assets`の上で右クリックをし新しくフォルダを作ってください。名前を`Animaor Controller`とします。ここにコントローラーを入れて整理します。

いま作成した`Animaor Controller`フォルダを選択して右クリック→`Create`→`Animator Controller`を選択します。まずは水素分子のアニメーションから作成しようと思いますので、`Animator Controller`の名前を`HydrogenAnimatorController`とします。

![スクリーンショット 2025-02-05 145459.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/c2c89b45-51a6-526d-8427-7664af3474f2.png)

次に、`Project`の`Assets`から`Prefabs`の中を開き、水素分子のプレハブを選択します。
`Inspector`で下にある`Add Component`から`Animator`を探して追加します。

![スクリーンショット 2025-02-05 145849.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/0153d1a3-bf40-ffa2-ae57-1df52fc5192a.png)

`Animatorの中の`Controller`に、先ほど用意した`HydrogenAnimatorController`をドラッグ&ドロップしてアタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/e1d44b27-8240-64a5-9ab7-2f34b23aaa0e.png)

:::note warn
ここでもう一度、プレハブの`Position`がすべて**0**になっているか確認してください。アニメーションをつける際、ここが0になっていないととても面倒です。
:::

:::note
`Rotation`と`Scale`はアニメーションをつけている時の状況から私はこのような数値になっていますので、この2つに関しては皆様の手元のオブジェクトを見ながら調整してください。
:::



#### Animatorビュー
`HydrogenAnimatorController`をダブルクリックすると以下のようなパネルが開くと思います。ここではアニメーションの遷移を管理するものだと私はとらえています。

![スクリーンショット 2025-02-05 150528.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/f9ecd8d4-48db-f19f-43eb-8b8b4ff6eccd.png)

パネルの中で右クリックをし、`Create`→`Empty`を選択します。

![スクリーンショット 2025-02-05 151332.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/867b61b8-e065-8b93-43e9-980f435dfdfa.png)

すると黄色の`New State`というものが出来ます。

![スクリーンショット 2025-02-05 151535.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/8f13027e-04bd-028a-2d50-7229087b3f7c.png)

これを選択し、`Inspector`で一番上の`New State`と書いてある部分をクリックし名前を変更します。名前は`Idle`とします。

![スクリーンショット 2025-02-05 151602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/bd56dc0f-ecd1-dc2c-8532-93ecdec2b0ba.png)

![スクリーンショット 2025-02-05 151828.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/eb7096c4-1f50-1039-aa13-fa701e99cb2e.png)



#### Animation
**Animation**は実際にオブジェクトにアニメーションをつけるものです。まずは`Project`に新規フォルダを作成してAnimationを入れるためのフォルダを作ります。名前は`Animation`としましょう。

`Animation`フォルダを選択して右クリック→`Create`→`Animation`を選択してください。

![スクリーンショット 2025-02-05 152424.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/953e133f-2caa-ff5a-4a73-b9c274534c6f.png)

水素の動きと大きさをアニメーションさせるので、名前を`HydrogenMoveAndScale`とします。

![スクリーンショット 2025-02-05 152723.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/8a0a9fe6-bd74-6b6a-d00c-714fe7f56d54.png)

続いて、今作ったAnimation `HydrogenMoveAndScale`をAnimatorパネルにドラッグ&ドロップします。

![スクリーンショット 2025-02-05 153016.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/2cde241c-2781-b1b7-165a-673d7c0ef048.png)

グレーの`HydrogenMoveAndScale`が入りました。

どんどんいきます。黄色の`Idle`の上で右クリックをし、`Make Transition`を選択します。

![スクリーンショット 2025-02-05 153310.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/9a059770-2615-ec04-fdea-3cd44935f03d.png)

矢印が引っ張れるので、グレーの`HydrogenMoveAndScale`の上で左クリックをしましょう。すると`Idle`→`HydrogenMoveAndScale`という風につながります。

![スクリーンショット 2025-02-05 153322.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/e443e513-e3e0-f17d-7f0b-11295b9de2fb.png)


**Triggerの設定**
`Animator`パネルの上部の`+`をクリックして`Trigger`を選択します。

![スクリーンショット 2025-02-05 153841.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/4423e324-b5a5-26e0-2cbc-f9fabccce776.png)

名前を`StartAnimation`としました。
※この後酸素やマッチ棒も同じように作るので、名前を`StartHydrogenAnimation`にした方が良かったなと感じました。皆様は区別がつくような名前付けをすることをおすすめします。

**この`Trigger`をスクリプトで呼び出してアニメーションを再生させるという仕組みになっています。**

`Idle`→`HydrogenMoveAndScale`の間の矢印を選択します。`Inspector`が表示され、一番下の`Conditions`の`+`を押し、先ほど名付けた`StartAnimation`を選択します。

![スクリーンショット 2025-02-05 154602.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/a906b687-efe4-49b3-da18-d145c2aaf5d4.png)

このようになっていればOKです。



**Animationの設定**
`Window→`Animation`→`Animation`でAnimationビューが開きます。水素分子モデルのプレハブをダブルクリックをすると下の画像のように`Add Property`がクリックできるようになると思います。

![スクリーンショット 2025-02-05 160546.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/4b7970de-7f21-1aa2-7a6f-1e73c3f286e6.png)

`Add Property`をクリックして`Transform`を開き、`Position`と`Scale'を追加してそれぞれにアニメーションをつけます。


※制作中、UnityのAnimationをうまく使いこなせず、Unity上でのプレビューと、Android端末で実行したときの動きが異なることが何度かあったので頑張って調整してみてください...

これでオブジェクトにアニメーションがつけられました！長かったですね。同じように残りの酸素分子、マッチ棒、水分子にもアニメーションを設定しました。が、何故か水分子だけ時間の都合もあり最後までできませんでした(´；ω；`)



#### Triggerを発動するスクリプトの作成
各モデルのトリガーを発動させるスクリプトを追加します。それらのスクリプトを先ほど書いた`ModelSpawner`スクリプトが参照して、モデルの生成とアニメーションの再生する処理を呼び出します。したがって、`ModelSpawner`も追加でコードを書く必要があります。

まずは各モデルのトリガーを発動させるコードを書きましょう。


#### HydrogenAnimationController.cs
水素分子のアニメーションのトリガーを発動させるスクリプトです。これまでと同じように新規でスクリプトを作成して書き進めます。

```ruby:HydrogenAnimationController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;

public class HydrogenAnimationController : MonoBehaviour
{
    private Animator animator;
    private bool isPlaying = false;

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>(); // Animator を取得
    }

    public void PlayAnimationWithDelay(float delay)
    {
        if (!isPlaying) // アニメーションが再生中でない場合実行
        {
            StartCoroutine(PlayAnimationAfterDelay(delay)); // delayをコルーチンに渡す
        }
    }

    private IEnumerator PlayAnimationAfterDelay(float delay)
    {
        Debug.Log($"Waiting for {delay} seconds before starting animation...");
        yield return new WaitForSeconds(delay); // 指定した秒数待機

        Debug.Log("Triggering StartAnimation!");
        animator.SetTrigger("StartAnimation"); // トリガーを発動
        isPlaying = true; // アニメーションが再生中である事を記録
        Debug.Log("Animation started after delay");
    }
}
```
</details>



#### OxygenAnimationController.cs
酸素分子のアニメーションのトリガーを発動させるスクリプトです。

```ruby:OxygenAnimationController.cs


using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;

public class OxygenAnimationController : MonoBehaviour
{
    private Animator animator;
    private bool isPlaying = false;

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>(); // Animatorを取得
    }

    public void PlayAnimationWithDelay(float delay)
    {
        if (!isPlaying) // アニメーションが再生中でない場合実行
        {
            StartCoroutine(PlayAnimationAfterDelay(delay)); // delayをコルーチンに渡す
        }
    }

    private IEnumerator PlayAnimationAfterDelay(float delay)
    {
        Debug.Log($"Waiting for {delay} seconds before starting animation...");
        yield return new WaitForSeconds(delay); // 指定した秒数待機

        Debug.Log("Triggering StartOxygenAnimation!");
        animator.SetTrigger("StartOxygenAnimation"); // トリガーを発動
        isPlaying = true; // アニメーションが再生中である事を記録
        Debug.Log("OxygenAnimation started after delay");
    }
}

```
</details>


#### MatchStickFireAnimationController.cs
火のついたマッチ棒のアニメーションのトリガーを発動させるスクリプトです。



```ruby:MatchStickFireAnimationController

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARSubsystems;

public class MatchStickFireAnimationController : MonoBehaviour
{
    private Animator animator;
    private bool isPlaying = false;

    // イベント: アニメーション終了後に呼ばれる
    public delegate void AnimationFinished();
    public event AnimationFinished OnAnimationFinished;

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>(); // Animatorを取得
    }

    public void PlayAnimationWithDelay(float delay)
    {
        if (!isPlaying) // アニメーションが再生中でない場合実行
        {
            StartCoroutine(PlayAnimationAfterDelay(delay)); // delayをコルーチンに渡す
        }
    }

    private IEnumerator PlayAnimationAfterDelay(float delay)
    {
        Debug.Log($"Waiting for {delay} seconds before starting animation...");
        yield return new WaitForSeconds(delay); // 指定した秒数待機

        Debug.Log("Triggering StartMatchStickFireAnimation!");
        animator.SetTrigger("StartMatchStickFireAnimation"); // トリガーを発動
        isPlaying = true; // アニメーションが再生中である事を記録
        Debug.Log("MatchStickFire Animation started after delay");

        // アニメーション終了後のイベントを発火
        yield return new WaitForSeconds(3f); // アニメーションが3秒続くと仮定
        OnAnimationFinished?.Invoke(); // イベント発火
    }
}
```
</details>



#### WaterAnimationController.cs
水素分子のアニメーションのトリガーを発動させるスクリプトです。

:::note warn
私の方では水分子のアニメーションが再生されませんでした。スクリプトの問題か、Unityのアニメーションの設定関係の問題か制作中に解明できなかったため、ここではあくまで私が書いたものをそのまま掲載します。
:::


```ruby:WaterAnimationController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

using UnityEngine.XR.ARSubsystems;
using static MatchStickFireAnimationController;

public class WaterAnimationController : MonoBehaviour
{
    private Animator animator;
    private bool isPlaying = false; // アニメーションの再生状態

    // Start is called before the first frame update
    void Start()
    {
        animator = GetComponent<Animator>(); // Animatorを取得
    }

    public void PlayAnimationImmediately()
    {
        Debug.Log("PlayAnimationImmediately called!");

        // アニメーションが再生中でない場合実行
        // isPlayingフラグを使ってアニメーションの重複を防止
        if (animator != null && !isPlaying ) 
        {
            // 適切なトリガーを使ってアニメーションを開始
            animator.SetTrigger("StartWaterAnimation");
        }
        else
        {
            Debug.LogWarning("Animator component is missing on the water molecule.");
        }
    }

    // アニメーションが終了したタイミングでフラグをリセット
    public void OnAnimationEnd()
    {
        isPlaying = false; // 再生中フラグをリセット
        Debug.Log("Animation ended, resetting play flag.");
    }
}
```
</details>

各モデルのトリガーを発動させるスクリプトが書けたら、モデルのそれぞれのプレハブにスクリプトをアタッチしてください。

水素を例にやってみます。水素分子のプレハブを選択した状態で、`HydrogenAnimationController`を`Inspector`へドラッグ&ドロップしてアタッチしましょう。同じ方法で各プレハブにスクリプトをアタッチしましょう。

![スクリーンショット 2025-02-07 151107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/fa7cc761-3ec3-ea38-b3b6-a6831e670fab.png)


#### ModelSpawner.csの追加コード
それぞれのモデルのトリガーを発動させるスクリプトが書けたら、モデルを生成する`ModelSpawner`スクリプトにアニメーションを開始するタイミングでトリガーを呼び出すようにします。

このスクリプトでは**アイコンが押される**→**モデルが生成されると同時に`(各モデルの)Controller`を取得し、指定した時間待ってからアニメーション再生**という流れを行っています。

```ruby:ModelSpawner.cs 追加

using System.Collections.Generic;
using Unity.XR.CoreUtils;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

using System.Collections;

public class ModelSpawner : MonoBehaviour
{
    [SerializeField] private GameObject H2; // 水素分子の3Dモデルのプレハブ
    [SerializeField] private GameObject O2; // 酸素分子の3Dモデルのプレハブ
    [SerializeField] private GameObject MatchStickFire; // マッチ棒の3Dモデルのプレハブ
    [SerializeField] private GameObject H2O; // 水分子の3Dモデルのプレハブ
    [SerializeField] private ARTrackedImageManager trackedImageManager; // ARTrackedImageManagerを設定

    private List<GameObject> hydrogenMolecules = new List<GameObject>(); // 水素分子オブジェクトを格納するリスト
    private List<GameObject> waterMolecules = new List<GameObject>(); // 水分子オブジェクトを格納するリスト

    private ARTrackedImage trackedImage; // トラッキングされたAR画像
    private bool hasHydrogenSpawned = false; // 水素分子が生成されたかどうかのフラグ
    private bool hasOxygenSpawned = false; // 酸素分子が生成されたかどうかのフラグ
    private bool hasMatchStickFire = false; // 火のついたマッチ棒が生成されたかどうかのフラグ
    private bool hasWaterSpawned = false; // 水分子が生成されたかどうかのフラグ

    private void Awake()
    {
        // XR OriginオブジェクトからARTrackedImageManagerを取得
        var xrOrigin = FindObjectOfType<XROrigin>(); // XROriginを探す
        if (xrOrigin != null )
        {
            trackedImageManager = xrOrigin.GetComponent<ARTrackedImageManager>(); // XROrigin内にあるARTrackedImageManagerを取得
        }

        if (trackedImageManager == null)
        {
            Debug.LogError("ARTrackedImageManager not found in XR Origin!");
        }
        else
        {
            Debug.Log("ARTrackedImageManager found and assigned.");
        }
    }

    private void Start()
    {
        //必要に応じてARTrackedImageManagerを設定
        trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
    }

    private void OnEnable()
    {
        // トラッキングされる画像が変わったときのイベントリスナーを設定
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged += OnTrackedImagesChanged;
            Debug.Log("Event listener registered.");
        }
        else
        {
            Debug.LogWarning("ARTrackedImageManager is not assigned.");
        }
    }

    private void OnDisable()
    {
        // トラッキングされる画像が変わったときのイベントリスナーを解除
        if (trackedImageManager != null)
        {
            trackedImageManager.trackedImagesChanged -= OnTrackedImagesChanged;
        }
    }

    private void OnTrackedImagesChanged(ARTrackedImagesChangedEventArgs eventArgs)
    {
        Debug.Log("OnTrackedImagesChanged called!"); // このログが表示されるか確認

        // マーカーの検出時には何も生成しないようにする
        foreach (var trackedImage in eventArgs.updated)
        {
            Debug.Log($"Updated: {trackedImage.referenceImage.name}, Tracking state: {trackedImage.trackingState}");

            // マーカーがトラッキングされている場合
            if (trackedImage.trackingState == UnityEngine.XR.ARSubsystems.TrackingState.Tracking)
            {
                Debug.Log($"marker tracked!");
                this.trackedImage = trackedImage; // トラッキングされた画像を保存
            }
        }
    }

    // Hydrogenアイコンが押されたときに水素分子を生成し、2秒後にアニメーション再生
    public void OnHydrogenIconPressed()
    {
        if (trackedImageManager != null && !hasHydrogenSpawned)
        {
            Debug.Log("Hydrogen icon pressed! Spawning Hydrogen molecules.");
            SpawnHydrogenModels(trackedImage); // 水素分子を生成
            hasHydrogenSpawned = true;

            // 生成してリストに入った水素分子に対してアニメーションを遅延で再生
            foreach (var hydrogen in hydrogenMolecules)
            {
                //hydrogen というゲームオブジェクトから HydrogenAnimationController コンポーネントを取り出し,それを controller という変数に格納している。
                // これによって、後でその controller を使ってアニメーションを操作することができる
                HydrogenAnimationController controller = hydrogen.GetComponent<HydrogenAnimationController>();
                if (controller != null)
                {
                    controller.PlayAnimationWithDelay(2f); // 2秒後にアニメーションを開始
                }
            }
        }
    }

    // Oxygenアイコンが押されたときに酸素分子を1つ生成し、2秒後にアニメーション再生
    public void OnOxygenIconPressed()
    {
        // 酸素分子がすでに出ていない場合のみ生成
        if (trackedImageManager != null && !hasOxygenSpawned)
        {
            Debug.Log("Oxygen icon pressed! Spawning Oxygen molecules.");
            // 酸素分子を生成
            SpawnOxygenModels(trackedImage); 
            hasOxygenSpawned = true; // 酸素分子が生成されたことを記録

            GameObject oxygen = GameObject.FindWithTag("Oxygen");

            if (oxygen != null)
            {
                // アニメーションを2秒後に開始
                OxygenAnimationController controller = oxygen.GetComponent<OxygenAnimationController>();
                if (controller != null)
                {
                    controller.PlayAnimationWithDelay(2f); // 2秒後にアニメーション
                }
            }
        }
    }

    // MatchStickFireアイコンが押されたときに火のついたマッチ棒を1つ生成し、アニメーション再生
    // 8秒後に水分子を生成
    public void OnMatchStickFireIconPressed()
    {
        // 火のついたマッチ棒が出ていない場合のみ生成
        if (trackedImageManager != null && !hasMatchStickFire)
        {
            Debug.Log("MatchStickFire icon pressed! Spawning MatchStickFire.");

            // 火のついたマッチ棒を生成
            SpawnMatchStickFireModels(trackedImage); 
            hasMatchStickFire = true; // 火のついたマッチ棒が生成されたことを記録

            GameObject matchstickfire = GameObject.FindWithTag("MatchStickFire");

            if (matchstickfire != null)
            {
                // アニメーションを2秒後に開始
                MatchStickFireAnimationController controller = matchstickfire.GetComponent<MatchStickFireAnimationController>();
                if (controller != null)
                {
                    controller.PlayAnimationWithDelay(2f); // 2秒後にアニメーション
                }
            }

            // 8秒後に水分子を生成する処理を開始
            StartCoroutine(SpawnWaterAfterDelay(8f));
        }
    }

    // 8秒後に水分子を生成するコルーチン
    private IEnumerator SpawnWaterAfterDelay(float delay)
    {
        yield return new WaitForSeconds(delay); // 指定した秒数待機
        WaterSpawn();
    }

    // 水分子2つを生成し、アニメーションを再生
    public void WaterSpawn()
    {
        // 水分子がすでに出ていない場合のみ生成
        if (trackedImageManager != null && !hasWaterSpawned)
        {
            Debug.Log("Spawning Water molecules.");

            // 水分子を生成
            SpawnWaterModels(trackedImage);
            hasWaterSpawned = true;

            // 生成した水分子にアタッチされているアニメーションを再生
            foreach (var water in waterMolecules)
            {
                //waterというゲームオブジェクトから WaterAnimationController コンポーネントを取り出し,それを controller という変数に格納している。
                // これによって、後でその controller を使ってアニメーションを操作することができる
                WaterAnimationController controller = water.GetComponent<WaterAnimationController>();
                if(controller != null)
                {
                    // ここで即座にアニメーションを開始
                    controller.PlayAnimationImmediately();
                }
            }
        }
    }

    // 水素分子2つを生成するメソッド
    public void SpawnHydrogenModels(ARTrackedImage trackedImage)
    {
        Debug.Log("Spawning 2 Hydrogen molecules...");
        // ARマーカーの位置を取得
        Vector3 markerPosition = trackedImage.transform.position; 
        Debug.Log($"Hydrogen molecule spawned at: {markerPosition}"); // 生成位置確認


        // 水素分子を2つ生成
        GameObject hydrogen1 = Instantiate(H2, markerPosition + new Vector3(0.5f, 2f, 0f), H2.transform.rotation);
        GameObject hydrogen2 = Instantiate(H2, markerPosition + new Vector3(0.5f, 1.8f, 0f), H2.transform.rotation);

        // 生成した水素分子をリストに追加
        hydrogenMolecules.Add(hydrogen1);
        hydrogenMolecules.Add(hydrogen2);

        // HydrogenMoveAndScaleアニメーションを開始する前に位置を調整
        // 位置をマーカー位置に調整
        hydrogen1.transform.position = markerPosition + new Vector3(0.5f, 2f, 0f);
        hydrogen2.transform.position = markerPosition + new Vector3(0.5f, 1.8f, 0f);
    }

    // 酸素分子1つを生成するメソッド
    public void SpawnOxygenModels(ARTrackedImage trackedImage)
    {
        Debug.Log("Spawning 1 Oxygen molecules...");
        // ARマーカーの位置を取得
        Vector3 markerPosition = trackedImage.transform.position;
        Debug.Log($"Oxygen molecule spawned at: {markerPosition}"); // 生成位置確認

        // 酸素分子を1つ生成
        GameObject oxygen = Instantiate(O2, markerPosition + new Vector3(0.5f, 1.75f, 0f), O2.transform.rotation);

        // 位置をマーカーに調整
        oxygen.transform.position = markerPosition + new Vector3(0.5f, 1.75f, 0f);
    }

    

    // 火のついたマッチを1つ生成するメソッド
    public void SpawnMatchStickFireModels(ARTrackedImage trackedImage)
    {
        Debug.Log("Spawning 1 MatchStickFire...");
        // ARマーカーの位置を取得
        Vector3 markerPosition = trackedImage.transform.position;
        Debug.Log($"MatchStickFire spawned at: {markerPosition}"); // 生成位置確認


        // 火のついたマッチ棒を1つ生成
        GameObject matchstickfire = Instantiate(MatchStickFire, markerPosition + new Vector3(0.75f, 1.75f, -0.5f), MatchStickFire.transform.rotation);

        // 位置をマーカーに調整
        matchstickfire.transform.position = markerPosition + new Vector3(0.75f, 1.75f, -0.5f);
    }

    // 水分子2つを生成するメソッド
    public void SpawnWaterModels(ARTrackedImage trackedImage)
    {
        Debug.Log("Spawning 2 Water molecules...");
        // ARマーカーの位置を取得
        Vector3 markerPosition = trackedImage.transform.position;
        Debug.Log($"Water molecule spawned at: {markerPosition}"); // 生成位置確認


        // 水分子を2つ生成
        GameObject water1 = Instantiate(H2O, markerPosition + new Vector3(0.5f, 2f, 0f), H2O.transform.rotation);
        GameObject water2 = Instantiate(H2O, markerPosition + new Vector3(0.5f, 1.8f, 0f), H2O.transform.rotation);

        // 生成した水素分子をリストに追加
        waterMolecules.Add(water1);
        waterMolecules.Add(water2);

        // HydrogenMoveAndScaleアニメーションを開始する前に位置を調整
        // 位置をマーカー位置に調整
        water1.transform.position = markerPosition + new Vector3(0.5f, 2f, 0f);
        water2.transform.position = markerPosition + new Vector3(0.5f, 1.8f, 0f);
    }
}
```
</details>






### フラスコの粉砕
モデルを出現させてアニメーションまで出来ましたら、次はマッチ棒がフラスコに近づいたタイミングでフラスコを粉砕させるようにしたいと思います。

:::note warn
今回の制作で、粉砕させることはできたものの粉砕の仕方や粉砕後の破片の挙動が納得いくものではないため詳しい設定などはご自身で設定、研究してみてください。
:::



#### ヒビの入った粉砕させるフラスコの準備

まずは粉砕を行うフラスコのモデルの準備から行います。最初に生成したフラスコは表面にヒビが入っていないフラスコですが、粉砕を行うには破片がそれぞれ分割したメッシュであるフラスコが必要になります。

ひびの入ったフラスコを`Project`にインポートしましょう。インポートのやり方を忘れてしまった場合は以下をご覧ください。

https://qiita.com/Ryushin_Mano/private/c66ac00adaa55ceebc93#%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE%E3%82%A4%E3%83%B3%E3%83%9D%E3%83%BC%E3%83%88

インポート出来たらプレハブをダブルクリックしてプレハブの中身が見えるようにします。以下の画像のようになれば大丈夫です。

![スクリーンショット 2025-02-07 152418.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/a57c2fcf-8c85-07b6-0802-8beda5f097ca.png)

`Hierarchy`を見ると分割した破片がすべて表示されています。この破片すべてに`Rigidbody`と`Box Collider`を適用させる必要があります。

#### Rigidbodyの設定
破片をすべて選択します。`Hierarchy`で一番上の破片のモデルを選択します。

![スクリーンショット 2025-02-07 153520.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/c9436ba4-193f-22c6-5fa7-1a55c301dc8b.png)

選択した状態で`Hierarchy`の一番下までスクロールします。そして、`Shiftキー`を押しながら一番下の破片をクリックするとすべての破片が選択できます。

![スクリーンショット 2025-02-07 153731.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/52cc06c4-27d2-0fbe-4b4b-8f94ebc172db.png)

その状態で`Inspector`の`Add Component`から`Rigidbody`を選択します。

![スクリーンショット 2025-02-07 154348.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/71f80e59-1b1f-50b5-8bf8-04a0df75fc9c.png)

`Hierarchy`でランダムに破片を選択して下の画像のように`Rigidbody`が入っていれば大丈夫です。

![スクリーンショット 2025-02-07 154550.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/6b34397d-55ff-b008-b6f5-4eae8f17b282.png)

なお、`Rigidbody`に関する詳細は以下からご覧いただけます。

https://docs.unity3d.com/ja/2019.4/Manual/class-Rigidbody.html

`Rigidbody`の設定ができたら次は`Box Collider`を追加します。



#### Box Colliderの設定
`Box Collider`はオブジェクト同士の衝突を発生させるコンポーネントです。これがないと破片はお互いにすり抜けてしまいます。

先ほど破片すべてに`Rigidbody`を適用したときと同様にすべての破片を選択し、`Inspector`の`Add Component`から`Box Collider`を選択します。

![スクリーンショット 2025-02-07 155641.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/1c3491a8-1f0f-1dcc-adc0-282c157b2da3.png)

画像のように`Box Collider`も入っていれば大丈夫です。`Center`と`Size`は破片ごとに異なるのでここは何もいじる必要はありません。

:::note
`Collider`には種類があり、本来はオブジェクトの形そのままが衝突の判定場所になる`Mesh Collider`というものを使う予定でした。ですが`Mesh Collider`で進めていたところ、処理が重いなどの問題があったため、衝突判定が箱型の`Box Collider`を使用しました。

オレンジの枠がメッシュの形、緑の枠が衝突判定(`Box Collider`)の形になっています。

![スクリーンショット 2025-02-07 160105.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/d5ba1567-7b75-a081-fd2a-04f71b46fb5f.png)
:::

ここまで設定出来たらスクリプト内で物理演算を行いフラスコを粉砕をさせます。



#### 粉砕アニメーションの実行
いよいよ粉砕させます。`Scripts`フォルダに新規でスクリプトを用意し、名前を`FraskCrushing`とします。

コードを書く前に一つ準備が必要でした。最初に生成するフラスコのプレハブをダブルクリック→`Hierarchy`で右クリック→`Create Empty`を選択し、名前を`Mouth`とします。

これは爆発の発生源を決めるオブジェクトです。爆発の発生源はお好みで`Position`を調整してください。私は今回フラスコの上部を爆発の発生源にしたかったため空オブジェクトをフラスコの上部に設定しました。

![スクリーンショット 2025-02-07 162835.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/527c9caf-c1d5-c75f-88f0-77f9db4bc419.png)




#### FraskCrushing.cs

```ruby:FraskCrushing.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;


public class FlaskCrushing : MonoBehaviour
{
    public GameObject ShatteredFlaskPrefab; // 破壊されたフラスコのプレハブ
    public float explosionForce = 50f; // 爆発の強さ
    public float explosionRadius = 0.02f;  // 爆発範囲
    public Transform explosionCenter;   // 爆発の中心 (フラスコの口あたり)

    private TrackedImageRotationFixer trackedImageRotationFixer;

    private GameObject currentShatteredFlask; // 亀裂の入った破壊されるフラスコの参照
    private bool hasExploded = false; // 爆発が1回だけ実行されたかを管理


    private void Start()
    {
        // TrackedImageRotationFixerを取得
        trackedImageRotationFixer = FindObjectOfType<TrackedImageRotationFixer>();

        // explosionCenterの位置をフラスコのローカル座標系で設定
        if (trackedImageRotationFixer != null)
        {
            StartCoroutine(WaitForFlaskAndSetExplosionCenter());
        }
        else
        {
            Debug.LogError("TrackedImageRotationFixer not found.");
        }
    }
    
    // フラスコが生成されるのを待ってから explosionCenter を設定するコルーチン
    private IEnumerator WaitForFlaskAndSetExplosionCenter()
    {
        GameObject spawnedFlask = null;

        // フラスコが生成されるまで待つ (最大10秒)
        float timeout = 10f;
        while (timeout > 0f)
        {
            spawnedFlask = trackedImageRotationFixer.GetSpawnedFlask();
            if (spawnedFlask != null)
                break;

            timeout -= Time.deltaTime;
            yield return null; // 1フレーム待つ
        }

        if (spawnedFlask == null)
        {
            Debug.LogError("Spawned Flask not found.");
            yield break;
        }

        // フラスコの "Mouth" という名前の子オブジェクトを探す
        Transform mouthTransform = spawnedFlask.transform.Find("Mouth");

        if (mouthTransform != null)
        {
            explosionCenter = mouthTransform;
            Debug.Log("Explosion center set to Flask's mouth position.");
        }
        else
        {
            Debug.LogWarning("Mouth object not found in the Flask. Using Flask's main position.");
            explosionCenter = spawnedFlask.transform; // フラスコ全体の位置を代替
        }
    }


    public void CrushFlask()
    {
        // 爆発済みなら即リターン (余計な処理を回避)
        if (hasExploded)
        {
            Debug.Log("Explosion already happened, skipping...");
            return; // すでに爆発済みなら何もしない
        }

        // すでに破壊されるフラスコが生成されている場合は処理を終了
        if (currentShatteredFlask != null)
        {
            Debug.Log("Flask already shattered. Skipping...");
            return;
        }

        // CrushFlask()が確実に実行されているか
        Debug.Log("CrushFlask started!");

        // フラスコの参照を取得 (1回だけ)
        GameObject trackedFlask = trackedImageRotationFixer.GetSpawnedFlask();
        if (trackedFlask == null)
        {
            Debug.LogError("Flask object not found");
            return;
        }

        // 破壊されるフラスコを生成
        currentShatteredFlask = Instantiate(ShatteredFlaskPrefab, trackedFlask.transform.position, trackedFlask.transform.rotation);

        // 元のフラスコを非表示にする
        // フラスコを切り替える目的
        trackedFlask.SetActive(false);

        // explosionCenter が null の場合の対策
        if (explosionCenter == null)
        {
            explosionCenter = trackedFlask.transform;
            Debug.LogWarning("Explosion center was null, using flask's main position.");
        }

        // 破片の物理演算処理
        foreach (Transform fragment in currentShatteredFlask.transform)
        {
            Rigidbody rb = fragment.GetComponent<Rigidbody>();
            if (rb != null)
            {
                rb.isKinematic = false; // 固定を解除
                rb.useGravity = true;   // 重力を適用
                rb.AddExplosionForce(explosionForce, explosionCenter.position, explosionRadius);
                Debug.Log($"Explosion applied to {fragment.name} with force {explosionForce}");
            }
            else
            {
                // 破片にRigidbodyがついていない場合の確認
                Debug.LogWarning($"Rigidbody not found on fragment: {fragment.name}");
            }
        }
    }
}
```
</details>

ここまで書けたら、次はそのままエフェクトの設定まで行ってしまいましょう。



#### 爆発エフェクトの準備
そろそろ終盤に差し掛かってきました。もう少し頑張りましょう！水素と酸素が火に反応して爆発が起こるので、その爆発を用意しましょう。エフェクトは中編で紹介したエフェクトを使用しました。

https://assetstore.unity.com/packages/vfx/particles/particle-pack-127325#content

その中の`Big Explosion`プレハブを今回は使用します。エフェクトがピンク色になっている場合はこちらをご覧いただき直してあげてください。

https://zenn.dev/uni2_stu/articles/281b75c4624368

エフェクトのプレハブを選択をし、`Inspector`を見てください。`Looping`のチェックを外しましょう。

![スクリーンショット 2025-02-07 164129.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/5f60dcfb-792c-b50e-bcbf-d885b2b4e1e8.png)

その他の設定に関してはエフェクトによって依存するものだと考えられるため、ここでは詳細は省きます。`Loopng`は共通で外すようにしてください。




#### FraskCrushing.csの追加

エフェクトの発生処理を先ほどの`FraskCrushing`スクリプトに書き加えます。

```ruby: FraskCrushing.cs 追加

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;


public class FlaskCrushing : MonoBehaviour
{
    public GameObject ShatteredFlaskPrefab; // 破壊されたフラスコのプレハブ
    public GameObject BigExplosionPrefab; // 爆発エフェクト
    public float explosionForce = 50f; // 爆発の強さ 元々200
    public float explosionRadius = 0.02f;  // 爆発範囲 元々2
    public Transform explosionCenter;   // 爆発の中心 (フラスコの口あたり)

    private TrackedImageRotationFixer trackedImageRotationFixer;

    private GameObject currentShatteredFlask; // 亀裂の入った破壊されるフラスコの参照
    private bool hasExploded = false; // 爆発が1回だけ実行されたかを管理


    private void Start()
    {
        // TrackedImageRotationFixerを取得
        trackedImageRotationFixer = FindObjectOfType<TrackedImageRotationFixer>();

        // explosionCenterの位置をフラスコのローカル座標系で設定
        if (trackedImageRotationFixer != null)
        {
            StartCoroutine(WaitForFlaskAndSetExplosionCenter());
        }
        else
        {
            Debug.LogError("TrackedImageRotationFixer not found.");
        }
    }
    
    // フラスコが生成されるのを待ってから explosionCenter を設定するコルーチン
    private IEnumerator WaitForFlaskAndSetExplosionCenter()
    {
        GameObject spawnedFlask = null;

        // フラスコが生成されるまで待つ (最大10秒)
        float timeout = 10f;
        while (timeout > 0f)
        {
            spawnedFlask = trackedImageRotationFixer.GetSpawnedFlask();
            if (spawnedFlask != null)
                break;

            timeout -= Time.deltaTime;
            yield return null; // 1フレーム待つ
        }

        if (spawnedFlask == null)
        {
            Debug.LogError("Spawned Flask not found.");
            yield break;
        }

        // フラスコの "Mouth" という名前の子オブジェクトを探す
        Transform mouthTransform = spawnedFlask.transform.Find("Mouth");

        if (mouthTransform != null)
        {
            explosionCenter = mouthTransform;
            Debug.Log("Explosion center set to Flask's mouth position.");
        }
        else
        {
            Debug.LogWarning("Mouth object not found in the Flask. Using Flask's main position.");
            explosionCenter = spawnedFlask.transform; // フラスコ全体の位置を代替
        }
    }


    public void CrushFlask()
    {
        // 爆発済みなら即リターン（余計な処理を回避）
        if (hasExploded)
        {
            Debug.Log("Explosion already happened, skipping...");
            return; // すでに爆発済みなら何もしない
        }

        // すでに破壊されるフラスコが生成されている場合は処理を終了
        if (currentShatteredFlask != null)
        {
            Debug.Log("Flask already shattered. Skipping...");
            return;
        }

        // CrushFlask()が確実に実行されているか
        Debug.Log("CrushFlask started!");

        // フラスコの参照を取得 (1回だけ)
        GameObject trackedFlask = trackedImageRotationFixer.GetSpawnedFlask();
        if (trackedFlask == null)
        {
            Debug.LogError("Flask object not found");
            return;
        }

        // 破壊されるフラスコを生成
        currentShatteredFlask = Instantiate(ShatteredFlaskPrefab, trackedFlask.transform.position, trackedFlask.transform.rotation);

        // 元のフラスコを非表示にする
        // フラスコを切り替える目的。
        trackedFlask.SetActive(false);

        // explosionCenter が null の場合の対策
        if (explosionCenter == null)
        {
            explosionCenter = trackedFlask.transform;
            Debug.LogWarning("Explosion center was null, using flask's main position.");
        }

        // 破片の物理演算処理
        foreach (Transform fragment in currentShatteredFlask.transform)
        {
            Rigidbody rb = fragment.GetComponent<Rigidbody>();
            if (rb != null)
            {
                rb.isKinematic = false; // 固定を解除
                rb.useGravity = true;   // 重力を適用
                rb.AddExplosionForce(explosionForce, explosionCenter.position, explosionRadius);
                Debug.Log($"Explosion applied to {fragment.name} with force {explosionForce}");
            }
            else
            {
                // 破片にRigidbodyがついていない場合の確認
                Debug.LogWarning($"Rigidbody not found on fragment: {fragment.name}");
            }
        }

        // 爆発エフェクトを一回だけ生成
        // BigExplosionPrefabは各自用意したプレハブ名に
        GameObject explosionEffect = Instantiate(BigExplosionPrefab, explosionCenter.position, Quaternion.identity);
        Debug.Log("BigExplosionPrefab spawned at: " + explosionCenter.position);

        // 1秒後に爆発エフェクトを削除
        StartCoroutine(DestroyExplosionEffect(explosionEffect, 1f));

        hasExploded = true; // ★爆発済みに設定
    }

    // 爆発エフェクトを指定した時間後に削除するコルーチン
    private IEnumerator DestroyExplosionEffect(GameObject explosionEffect, float delay)
    {
        yield return new WaitForSeconds(delay); // 指定した時間待機
        Destroy(explosionEffect); // エフェクトを削除
        Debug.Log("Explosion effect destroyed.");
    }
}
```
</details>

ここまで書けたらアタッチの作業です。まずは`FraskCrushing`スクリプトを選択します。`Inspector`の'Shattered Flask Prefab`には粉砕する破片が集まったフラスコのプレハブ`flask_break`を、Big Explosion Prefab`には爆発エフェクトのプレハブをそれぞれ入れます。

![スクリーンショット 2025-02-07 165022.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/9cc95ed5-97d9-ef79-2dba-145f7a413b7e.png)

次はシーンにスクリプトをアタッチする空オブジェクトを用意します。名前を`FlaskCrushingObject`とします。
下の画像のようになっていれば大丈夫です。

![スクリーンショット 2025-02-07 165648.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/ec2670c5-4a3f-e163-197e-7c47417dde8b.png)

最後です！最初に生成するフラスコと、粉砕させるフラスコの2つに`FlaskCrushing`スクリプトをアタッチしてください。
![スクリーンショット 2025-02-07 165928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/733368da-73d2-7c30-92d3-b1dd48b39c1e.png)


![スクリーンショット 2025-02-07 165933.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4001687/c8e2140c-c74d-2374-5a9d-086831a8c3ec.png)

ここまで出来たら実行してみてください！ここまでが今回GENEROSTY様で取り組んだ開発になります。うまく出来ていない箇所、改善が必要な箇所は数多くあると思いますが爆発させるところまでできました。(AR授業資料なのに解説を表示させていないなど...)



### まとめ
　以上が第4回の内容になります！トータルでかなり膨大な量になってしまいましたが私が1か月間取り組んだ開発の内容をお届けすることができたのではないかと思います。もし、ここどうしているの？だったり他にも何か聞きたいことがあったりする方は答えられる範囲でお答えするのでコメント書いてください！感想も待っています。

**1か月間インターン生として私を受け入れていただいた株式会社GENEROSITYの皆様、本当にありがとうございました！**



**おすすめ**

**株式会社GENEROSITY 様**

https://generosity.co.jp/

**第1回はこちら**

https://qiita.com/Ryushin_Mano/items/8e094501f409dc955898


**第2回はこちら**

https://qiita.com/Ryushin_Mano/items/c66ac00adaa55ceebc93


**第3回はこちら**

https://qiita.com/Ryushin_Mano/items/88f042c61f234b32c7e7