4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-02-08

はじめに

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

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

目次

対象者
企画概要
使用ツール
開発環境
 OS
 Unity
 テストデバイス
制作
 モデルにアニメーションをつける
  AnimatorController
  Animatorビュー
  Animation
  Triggerを発動するスクリプトの作成
  HydrogenAnimationController.cs
  OxygenAnimationController.cs
  MatchStickFireAnimationController.cs
  WaterAnimationController.cs
 フラスコの粉砕
  ヒビの入った粉砕させるフラスコの準備
   Rigidbodyの設定
   Box Colliderの設定
  粉砕アニメーションの実行
   FraskCrushing.cs
  爆発エフェクトの準備
   FraskCrushing.csの追加
まとめ

対象者

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

企画概要

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

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

使用ツール

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機能を提供するフレームワークです。

テストデバイス

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

制作

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

モデルにアニメーションをつける

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

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

AnimatorController

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

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

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

スクリーンショット 2025-02-05 145459.png

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

スクリーンショット 2025-02-05 145849.png

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

image.png

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

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

Animatorビュー

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

スクリーンショット 2025-02-05 150528.png

パネルの中で右クリックをし、CreateEmptyを選択します。

スクリーンショット 2025-02-05 151332.png

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

スクリーンショット 2025-02-05 151535.png

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

スクリーンショット 2025-02-05 151602.png

スクリーンショット 2025-02-05 151828.png

Animation

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

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

スクリーンショット 2025-02-05 152424.png

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

スクリーンショット 2025-02-05 152723.png

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

スクリーンショット 2025-02-05 153016.png

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

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

スクリーンショット 2025-02-05 153310.png

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

スクリーンショット 2025-02-05 153322.png

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

スクリーンショット 2025-02-05 153841.png

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

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

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

スクリーンショット 2025-02-05 154602.png

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

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

スクリーンショット 2025-02-05 160546.png

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

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

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

Triggerを発動するスクリプトの作成

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

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

HydrogenAnimationController.cs

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

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");
    }
}

OxygenAnimationController.cs

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

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");
    }
}

MatchStickFireAnimationController.cs

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

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(); // イベント発火
    }
}

WaterAnimationController.cs

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

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

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.");
    }
}

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

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

スクリーンショット 2025-02-07 151107.png

ModelSpawner.csの追加コード

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

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

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);
    }
}

フラスコの粉砕

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

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

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

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

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

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

スクリーンショット 2025-02-07 152418.png

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

Rigidbodyの設定

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

スクリーンショット 2025-02-07 153520.png

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

スクリーンショット 2025-02-07 153731.png

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

スクリーンショット 2025-02-07 154348.png

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

スクリーンショット 2025-02-07 154550.png

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

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

Box Colliderの設定

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

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

スクリーンショット 2025-02-07 155641.png

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

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

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

スクリーンショット 2025-02-07 160105.png

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

粉砕アニメーションの実行

いよいよ粉砕させます。Scriptsフォルダに新規でスクリプトを用意し、名前をFraskCrushingとします。

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

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

スクリーンショット 2025-02-07 162835.png

FraskCrushing.cs

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}");
            }
        }
    }
}

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

爆発エフェクトの準備

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

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

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

スクリーンショット 2025-02-07 164129.png

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

FraskCrushing.csの追加

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

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.");
    }
}

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

スクリーンショット 2025-02-07 165022.png

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

スクリーンショット 2025-02-07 165648.png

最後です!最初に生成するフラスコと、粉砕させるフラスコの2つにFlaskCrushingスクリプトをアタッチしてください。
スクリーンショット 2025-02-07 165928.png

スクリーンショット 2025-02-07 165933.png

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

まとめ

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

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

おすすめ

株式会社GENEROSITY 様

第1回はこちら

第2回はこちら

第3回はこちら

4
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?