はじめに
この度インターンシップにて、株式会社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というオブジェクトのアニメーションを管理するものがあるのでそれを用意します。
Project
のAssets
の上で右クリックをし新しくフォルダを作ってください。名前をAnimaor Controller
とします。ここにコントローラーを入れて整理します。
いま作成したAnimaor Controller
フォルダを選択して右クリック→Create
→Animator Controller
を選択します。まずは水素分子のアニメーションから作成しようと思いますので、Animator Controller
の名前をHydrogenAnimatorController
とします。
次に、Project
のAssets
からPrefabs
の中を開き、水素分子のプレハブを選択します。
Inspector
で下にあるAdd Component
からAnimator
を探して追加します。
Animatorの中の
Controllerに、先ほど用意した
HydrogenAnimatorController`をドラッグ&ドロップしてアタッチします。
ここでもう一度、プレハブのPosition
がすべて0になっているか確認してください。アニメーションをつける際、ここが0になっていないととても面倒です。
Rotation
とScale
はアニメーションをつけている時の状況から私はこのような数値になっていますので、この2つに関しては皆様の手元のオブジェクトを見ながら調整してください。
Animatorビュー
HydrogenAnimatorController
をダブルクリックすると以下のようなパネルが開くと思います。ここではアニメーションの遷移を管理するものだと私はとらえています。
パネルの中で右クリックをし、Create
→Empty
を選択します。
すると黄色のNew State
というものが出来ます。
これを選択し、Inspector
で一番上のNew State
と書いてある部分をクリックし名前を変更します。名前はIdle
とします。
Animation
Animationは実際にオブジェクトにアニメーションをつけるものです。まずはProject
に新規フォルダを作成してAnimationを入れるためのフォルダを作ります。名前はAnimation
としましょう。
Animation
フォルダを選択して右クリック→Create
→Animation
を選択してください。
水素の動きと大きさをアニメーションさせるので、名前をHydrogenMoveAndScale
とします。
続いて、今作ったAnimation HydrogenMoveAndScale
をAnimatorパネルにドラッグ&ドロップします。
グレーのHydrogenMoveAndScale
が入りました。
どんどんいきます。黄色のIdle
の上で右クリックをし、Make Transition
を選択します。
矢印が引っ張れるので、グレーのHydrogenMoveAndScale
の上で左クリックをしましょう。するとIdle
→HydrogenMoveAndScale
という風につながります。
Triggerの設定
Animator
パネルの上部の+
をクリックしてTrigger
を選択します。
名前をStartAnimation
としました。
※この後酸素やマッチ棒も同じように作るので、名前をStartHydrogenAnimation
にした方が良かったなと感じました。皆様は区別がつくような名前付けをすることをおすすめします。
このTrigger
をスクリプトで呼び出してアニメーションを再生させるという仕組みになっています。
Idle
→HydrogenMoveAndScale
の間の矢印を選択します。Inspector
が表示され、一番下のConditions
の+
を押し、先ほど名付けたStartAnimation
を選択します。
このようになっていればOKです。
Animationの設定
Window→
Animation→
AnimationでAnimationビューが開きます。水素分子モデルのプレハブをダブルクリックをすると下の画像のように
Add Property`がクリックできるようになると思います。
Add Property
をクリックしてTransform
を開き、Position
と`Scale'を追加してそれぞれにアニメーションをつけます。
※制作中、UnityのAnimationをうまく使いこなせず、Unity上でのプレビューと、Android端末で実行したときの動きが異なることが何度かあったので頑張って調整してみてください...
これでオブジェクトにアニメーションがつけられました!長かったですね。同じように残りの酸素分子、マッチ棒、水分子にもアニメーションを設定しました。が、何故か水分子だけ時間の都合もあり最後までできませんでした(´;ω;`)
Triggerを発動するスクリプトの作成
各モデルのトリガーを発動させるスクリプトを追加します。それらのスクリプトを先ほど書いたModelSpawner
スクリプトが参照して、モデルの生成とアニメーションの再生する処理を呼び出します。したがって、ModelSpawner
も追加でコードを書く必要があります。
まずは各モデルのトリガーを発動させるコードを書きましょう。
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
酸素分子のアニメーションのトリガーを発動させるスクリプトです。
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
火のついたマッチ棒のアニメーションのトリガーを発動させるスクリプトです。
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のアニメーションの設定関係の問題か制作中に解明できなかったため、ここではあくまで私が書いたものをそのまま掲載します。
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.");
}
}
各モデルのトリガーを発動させるスクリプトが書けたら、モデルのそれぞれのプレハブにスクリプトをアタッチしてください。
水素を例にやってみます。水素分子のプレハブを選択した状態で、HydrogenAnimationController
をInspector
へドラッグ&ドロップしてアタッチしましょう。同じ方法で各プレハブにスクリプトをアタッチしましょう。
ModelSpawner.csの追加コード
それぞれのモデルのトリガーを発動させるスクリプトが書けたら、モデルを生成するModelSpawner
スクリプトにアニメーションを開始するタイミングでトリガーを呼び出すようにします。
このスクリプトではアイコンが押される→モデルが生成されると同時に(各モデルの)Controller
を取得し、指定した時間待ってからアニメーション再生という流れを行っています。
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
にインポートしましょう。インポートのやり方を忘れてしまった場合は以下をご覧ください。
インポート出来たらプレハブをダブルクリックしてプレハブの中身が見えるようにします。以下の画像のようになれば大丈夫です。
Hierarchy
を見ると分割した破片がすべて表示されています。この破片すべてにRigidbody
とBox Collider
を適用させる必要があります。
Rigidbodyの設定
破片をすべて選択します。Hierarchy
で一番上の破片のモデルを選択します。
選択した状態でHierarchy
の一番下までスクロールします。そして、Shiftキー
を押しながら一番下の破片をクリックするとすべての破片が選択できます。
その状態でInspector
のAdd Component
からRigidbody
を選択します。
Hierarchy
でランダムに破片を選択して下の画像のようにRigidbody
が入っていれば大丈夫です。
なお、Rigidbody
に関する詳細は以下からご覧いただけます。
Rigidbody
の設定ができたら次はBox Collider
を追加します。
Box Colliderの設定
Box Collider
はオブジェクト同士の衝突を発生させるコンポーネントです。これがないと破片はお互いにすり抜けてしまいます。
先ほど破片すべてにRigidbody
を適用したときと同様にすべての破片を選択し、Inspector
のAdd Component
からBox Collider
を選択します。
画像のようにBox Collider
も入っていれば大丈夫です。Center
とSize
は破片ごとに異なるのでここは何もいじる必要はありません。
ここまで設定出来たらスクリプト内で物理演算を行いフラスコを粉砕をさせます。
粉砕アニメーションの実行
いよいよ粉砕させます。Scripts
フォルダに新規でスクリプトを用意し、名前をFraskCrushing
とします。
コードを書く前に一つ準備が必要でした。最初に生成するフラスコのプレハブをダブルクリック→Hierarchy
で右クリック→Create Empty
を選択し、名前をMouth
とします。
これは爆発の発生源を決めるオブジェクトです。爆発の発生源はお好みでPosition
を調整してください。私は今回フラスコの上部を爆発の発生源にしたかったため空オブジェクトをフラスコの上部に設定しました。
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
のチェックを外しましょう。
その他の設定に関してはエフェクトによって依存するものだと考えられるため、ここでは詳細は省きます。Loopng
は共通で外すようにしてください。
FraskCrushing.csの追加
エフェクトの発生処理を先ほどのFraskCrushing
スクリプトに書き加えます。
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
には爆発エフェクトのプレハブをそれぞれ入れます。
次はシーンにスクリプトをアタッチする空オブジェクトを用意します。名前をFlaskCrushingObject
とします。
下の画像のようになっていれば大丈夫です。
最後です!最初に生成するフラスコと、粉砕させるフラスコの2つにFlaskCrushing
スクリプトをアタッチしてください。
ここまで出来たら実行してみてください!ここまでが今回GENEROSTY様で取り組んだ開発になります。うまく出来ていない箇所、改善が必要な箇所は数多くあると思いますが爆発させるところまでできました。(AR授業資料なのに解説を表示させていないなど...)
まとめ
以上が第4回の内容になります!トータルでかなり膨大な量になってしまいましたが私が1か月間取り組んだ開発の内容をお届けすることができたのではないかと思います。もし、ここどうしているの?だったり他にも何か聞きたいことがあったりする方は答えられる範囲でお答えするのでコメント書いてください!感想も待っています。
1か月間インターン生として私を受け入れていただいた株式会社GENEROSITYの皆様、本当にありがとうございました!
おすすめ
株式会社GENEROSITY 様
第1回はこちら
第2回はこちら
第3回はこちら