前回 Unity の ObjectPool を使って、常に 100 枚前後の花びらが舞うシーンを作りました。

再生成により無制限に一定量の花びらが舞い落ちるようになりました。
そこまでは良かったのですが、テストプレイ終了後に SetActive 関数の呼び出し時に以下のエラーが20~40件発生しました。

SetActive関数呼び出し時のエラーメッセージ
「GameObject is already being activated or deactivated.
UnityEngine.StackTraceUtility:ExtractStackTrace ()
Manager:<Start>b__4_1 (SakuraNoHanabira) (at Assets/Manager.cs:56)
UnityEngine.Pool.ObjectPool`1<SakuraNoHanabira>:Get ()
Manager:OnHanabiraInvisible (SakuraNoHanabira) (at Assets/Manager.cs:90)
SakuraNoHanabira:OnBecameInvisible () (at Assets/SakuraNoHanabira.cs:89)」
「GameObject はすでにアクティブまたは非アクティブ状態です」と言っているようです。
この GameObject は SetActive の呼び出し元からみて「桜の花びら」のゲームオブジェクトです。
もしかしたら、同じエラーが発生している方もいるかもしれないので、今回はその対処について紹介します。
参考サイト
エラーメッセージで検索しました。
GameObject is already being activated or deactivated. – Unity Forum
の #5 の回答のソースコードを見て、OnBecameInvisible イベント内でアクティブ→非アクティブ→アクティブと変えたせいなのではないかと思いました。
前回紹介したとおり「桜の花びら」ゲームオブジェクトのスクリプトで画面外に消えるイベント関数で Manager の関数を呼び出しています。
private void OnBecameInvisible()
{
Manager.OnHanabiraInvisible(this);
}
この Manager の関数は以下のように pool の Release, Get を呼び出していますが、その中では SetActive で非アクティブ・アクティブに切り替えています。
Manager の関数で切り替えてはいますが、呼び出し元は、アクティブ切り替え対象の「桜の花びら」ゲームオブジェクトのイベントです。
public void OnHanabiraInvisible(SakuraNoHanabira hanabira)
{
pool.Release(hanabira);
pool.Get();
}
アクティブを何度も切り替える場合、自身のイベント内ではなく、外部から切り替えてもらえば良いと思い、 Manager クラスに新たに作ったリストに追加しておき、Manager クラスの Update イベントでリストの中のゲームオブジェクトを Pool に戻したり、新しく取得するようにしたところ、先ほどのエラーは発生しなくなりました。
後でソースコードを掲載しますので、興味があればご参照ください。
最小モデルでバグの原因を検証はできず
しかし、最小限のコードでバグを再現させようとしたのですが、前述の流れだけでは再現しません。
// 桜の花びらが画面下などに消えた場合に呼び出されるイベント関数。
private void OnBecameInvisible()
{
this.gameObject.SetActive(false);
this.gameObject.SetActive(true);
this.gameObject.SetActive(false);
this.gameObject.SetActive(true);
// エラーは発生しませんでした。
}
これだけではバグが発生しないので、さらに ObjectPool を絡ませる必要があるのかもしれません。
今回はこれ以上の真相の究明はやめましたが、ゲームオブジェクト自身のイベントの処理で何度も SetActive を切り替えると、先ほどの掲示板に書いてあった相談内容同様に SetActive に関するエラーが発生するのかもしれません。
もしも同様のことが再び起きたら、一度外部に預けて置き、その外部のオブジェクトの Update イベント関数などで預かったオブジェクトの SetActive を書き換えると対処できるかもしれないと思いました。
ソースコード
以下に3つのソースコードを掲載します。
Manager.cs
シーンの空のオブジェクト「管理役」に付加したスクリプトです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;
public class Manager : MonoBehaviour
{
/// 作成する花びらの個数です。
public int NumberOfHanabira = 100;
/// 複製元のプレハブです。エディタから指定して下さい。
public GameObject Prefab;
/// 花びらの出現を許可する領域を設定します。
public Rect RespawnArea;
/// 花びらのゲームオブジェクト群の生成と破棄を効率よく管理します。
private ObjectPool pool;
/// 画面下に消えた花びらオブジェクトを一時的に預かるリストです。
private List hanabiraBottomOfScreen = new List();
private void Awake()
{
// DOTween の Tween 数と Sequence 数の許容量を設定します。(既定値で足りなかった場合警告が出ます)
DG.Tweening.DOTween.SetTweensCapacity(tweenersCapacity: 1250, sequencesCapacity: 330);
}
// 有効なゲームオブジェクトの最初の Update イベント関数の前に呼び出されます。
void Start()
{
// スタック形式のオブジェクトプールを作成します。各種イベントハンドラを引数で指定しています。
// プール生成とともに defaultCapacity で指定した個数の「桜の花びら」ゲームオブジェクトが作成されます。
pool = new ObjectPool
(
// オブジェクトを生成するイベントです。
createFunc: () =>
{
Debug.Log("createFunc Called.");
// Prefab のインスタンスを作成します。位置は actionOnGet で取得される直前にランダムに設定します。
GameObject instance = Instantiate(Prefab, Vector3.zero, Quaternion.identity);
return instance.GetComponent();
},
// 使うオブジェクトを取得するイベントです。
actionOnGet : (SakuraNoHanabira hanabira) =>
{
Debug.Log("actionOnGet Called." + hanabira.name);
Vector3 pos = new Vector3();
// 出現場所(固定)を設定します。
pos.x = RespawnArea.x + Random.Range(0f, RespawnArea.width);
pos.y = RespawnArea.y + Random.Range(0f, RespawnArea.height);
pos.z = 0f;
hanabira.transform.position = pos;
// アクティブにします。
if (hanabira.gameObject.activeSelf == false)
{
hanabira.gameObject.SetActive(true);
}
},
// 使い終わったオブジェクトを戻すイベントです。
actionOnRelease : (SakuraNoHanabira hanabira) =>
{
Debug.Log("actionOnRelease Called." + hanabira.name);
// 非アクティブにします。
if (hanabira.gameObject.activeSelf == true)
{
hanabira.gameObject.SetActive(false);
}
},
// オブジェクトを破棄するイベントです。
actionOnDestroy : (SakuraNoHanabira hanabira) =>
{
Debug.Log("actionOnDestroy Called." + hanabira.name);
GameObject.Destroy(hanabira.gameObject);
},
collectionCheck : true,
defaultCapacity : 100,
maxSize : 200
);
// 花びらを指定個数 pool から取得し、表示します。
for (int i = 0; i < NumberOfHanabira; ++i)
{
SakuraNoHanabira hanabira = pool.Get();
hanabira.Manager = this;
}
}
private void Update()
{
// 非アクティブになる Hanabira オブジェクト自身の処理途中ではなく、
// Managerの処理で直前に預かったオブジェクトを ObjectPool に戻し取得します。
// ObjectPool の Release, Get 内部でアクティブを切り替えています。
foreach (SakuraNoHanabira hanabira in hanabiraBottomOfScreen)
{
pool.Release(hanabira);
pool.Get();
}
hanabiraBottomOfScreen.Clear(); // リストを空にします。
}
/// 花びらが画面下に移動した際に外部から呼び出されます。
public void OnHanabiraBottomOfScreen(SakuraNoHanabira hanabira)
{
// Manager の Update で処理するまで一時的に保存しておきます。
hanabiraBottomOfScreen.Add(hanabira);
}
}
SakuraNoHanabira.cs
画像を表示する「桜の花びら」ゲームオブジェクトに付加したスクリプトです。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using DG.Tweening; // DG.Tweening を省略表記できるようにします。
public class SakuraNoHanabira : MonoBehaviour
{
/// 移動速度(秒速)です。
public float Speed = 2.5f;
/// 水平方向の移動幅の範囲です。
[SerializeField]
protected RangeFloat HorizontalDistanceRange;
/// 回転アニメーション 1 のイージングです。
public Ease EaseRotate1 = Ease.OutQuart;
/// 回転アニメーション 1 のイージングです。
public Ease EaseRotate2 = Ease.Linear;
/// 移動アニメーションのイージングです。
public Ease EaseMove = Ease.InQuart;
/// カメラに映らなくなる際の通知先です。
public Manager Manager;
// 有効なオブジェクトの最初の Update イベント関数の呼び出し前に呼ばれます。
void Start()
{
// 水平移動の距離を指定された範囲からランダムで決定します。
float horizontalDistance = Random.Range(HorizontalDistanceRange.Start, HorizontalDistanceRange.End);
// 移動距離に応じてアニメーションの時間を決定します。
float timeAll = 0.15f * horizontalDistance;
float timePart1 = timeAll * 2 / 3;
float timePart2 = timeAll * 1 / 3;
// 移動と並行して行う 2 つの回転アニメーションを seqRotate1 に設定します。
Sequence seqRotate1 = DOTween.Sequence();
seqRotate1.Append(this.transform.DORotate(new Vector3(0f, 0f, -90f), timePart1, RotateMode.Fast).SetEase(Ease.OutQuart));
seqRotate1.Append(this.transform.DORotate(new Vector3(0f, 0f, 0f), timePart2, RotateMode.Fast).SetEase(Ease.Linear));
// 移動と並行して行う 2 つの回転アニメーションを seqRotate2 に設定します。
Sequence seqRotate2 = DOTween.Sequence();
seqRotate2.Append(this.transform.DORotate(new Vector3(0f, 0f, 90f), timePart1, RotateMode.Fast).SetEase(Ease.OutQuart));
seqRotate2.Append(this.transform.DORotate(new Vector3(0f, 0f, 0f), timePart2, RotateMode.Fast).SetEase(Ease.Linear));
// 複数の動きなどを覚えて実行できるオブジェクトを作成します。
Sequence sequence = DOTween.Sequence();
// 一定時間かけて左に移動する動きを覚えさせます。
sequence.Append(this.transform.DOMoveX(-horizontalDistance, timeAll, false).SetEase(Ease.InQuart).SetRelative())
// 移動と合わせて同じ緩急で角度を回転させます。
.Join(seqRotate1);
// 一定時間かけて右に移動する動きを覚えさせます。
sequence.Append(this.transform.DOMoveX( horizontalDistance, timeAll, false).SetEase(Ease.InQuart).SetRelative())
// 移動と合わせて同じ緩急で角度を先ほどとは逆に回転させます。
.Join(seqRotate2);
// 無制限に繰り返すことを指示します。
sequence.SetLoops(-1, LoopType.Restart);
}
// 毎フレーム呼び出されるイベント関数です。
void Update()
{
// 自身の位置情報を tmp にコピーします。
Vector3 tmp = this.transform.position;
// tmp の位置情報を、経過時間と移動速度に応じて下方向に変化させます。
tmp += Vector3.down * Time.deltaTime * this.Speed;
// 自身の位置情報を tmp の位置情報に変更します。
this.transform.position = tmp;
}
private void OnBecameInvisible()
{
Manager.OnHanabiraBottomOfScreen(this);
}
}
RangeFloat.cs
RangeInt の Float 版です。 Float 型の最小値・最大値の範囲を Unity エディタで編集できます。
using UnityEngine;
[System.Serializable]
public struct RangeFloat
{
[SerializeField]
private float start;
[SerializeField]
private float length;
public float Start
{
get { return start; }
set { start = value; }
}
public float Length
{
get { return length; }
set { length = value; }
}
public float End
{
get { return start + length; }
}
}
コメント