おことわり
この記事には、新しい版 (GitHub)が存在します。
まえがき
環境条件
この記事は、「Unity 2017~」および「.NET 4.x (C#6)」を前提としています。
プレハブを使ってオブジェクトを動的に生成したい
同じオブジェクト(GameObject)を複数作って使う場合、プレハブを使いますよね。
あらかじめシーンに配置して使うこともありますが、私はどちらかというと動的に生成したいことが多いです。
例えば、以下のような使い方で、uGUIでフライテキスト(ダメージの数値表示とかテロップとかの類)を表示するプレハブを作るものとします。
FlyText.Create ("Start!");
使う側は、単にクラスの静的メソッドを呼ぶだけです。
このFlyTextクラスは、例えば以下のようになります。
using UnityEngine;
using UnityEngine.UI;
/// <summary>フライテキスト</summary>
public class FlyText : MonoBehaviour {
#region Static
public static GameObject Prefab {
get {
if (!prefab) {
prefab = Resources.Load (typeof (FlyText).Name) as GameObject;
}
return prefab;
}
}
private GameObject prefab;
public static FlyText Create (GameObject parent, string message, float end = 2.5f) {
var instance = Instantiate (Prefab, parent.transform);
instance.init (message, end);
return instance;
}
#endregion
private void init (string message, float end) {
this.transform.SetAsLastSibling ();
this.gameObject.SetActive (true);
var text = GetComponentInChildren<Text> ();
if (text != null) { text.text = message; }
var panelRect = GetComponent<RectTransform> ();
panelRect.sizeDelta = new Vector2 (text.preferredWidth + 128, panelRect.sizeDelta.y); // 文字量に応じたサイズ
Destroy (this.gameObject, end);
}
}
このクラスを使う場合は、クラス名と同じ名前のプレハブをResourceフォルダに用意することになります。
(ここでは、簡素化するためにエラーは無視しています。)
しかし、このままでは、タイミング次第で複数が重なって表示されることになります。
インスタンスの数を管理したい
上記のフライテキストの場合なら、「既に表示中なら、表示中のものを消してから、新たに表示する」ようにしたいです。
そして、それを組み込むだけなら話は簡単です。
しかし、使い道によっては、「既に表示中なら、表示中のものを残して、新たに表示しようとしたものを破棄する」ような場合もありそうです。
他にも、モーダルダイアログのように複数重ねる可能性があって、最前だけを特別扱いしたいような場合もあるでしょう。
uGUIの例ばかりですが、私の場合は、プレハブから動的に生成して数や挙動を管理したいものが多くあります。
それぞれに同じようなことを書くのが面倒なので、共通化できないか考えた末に、以下のようなアプローチになりました。
using System;
using System.Collections.Generic;
using UnityEngine;
public class ManagedInstance<T> : IDisposable where T : MonoBehaviour {
public int MaxInstances { get; protected set; } // 最大インスタンス数 (0で無制限)
public bool AutoDelete { get; protected set; } // 上限数を超えたら最古を破棄する
public List<T> Instances { get; protected set; } // インスタンス一覧
public T LastInstance { get { return (Instances == null || Instances.Count <= 0) ? null : Instances [Instances.Count - 1]; } } // 最新のインスタンス
public bool OnMode { // ひとつ以上生成されている
get { return (Instances != null && Instances.Count > 0); }
set {
if (!value) { // 全インスタンス破棄
foreach (var instance in Instances) {
if (instance != null) { GameObject.Destroy (instance.gameObject); }
}
}
}
}
public GameObject Prefab { // デフォルトプレハブ
get {
if (!prefab) {
prefab = Resources.Load (typeof (T).Name) as GameObject;
if (!prefab) {
throw new System.ArgumentException ($"Resources.Load (\"{typeof (T).Name}\") failed");
} else {
Debug.Log ($"Resources.Load (\"{typeof (T).Name}\") success");
}
}
return prefab;
}
}
private GameObject prefab;
public ManagedInstance (int max = 0, bool autoDelete = false) {
if (Instances == null) {
MaxInstances = max;
AutoDelete = autoDelete;
Instances = new List<T> { };
}
}
/// <summary>uGUIオブジェクト生成</summary>
public T Create (GameObject parent, GameObject prefab = null, bool control = true) {
if (!preCreate ()) { return null; }
try {
return postCreate (GameObject.Instantiate (prefab ?? Prefab, parent.transform), control);
}
catch {
return null;
}
}
/// <summary>3Dオブジェクト生成</summary>
public T Create (Vector3 position, Quaternion rotation, GameObject prefab = null, bool control = true) {
if (!preCreate ()) { return null; }
try {
return postCreate (GameObject.Instantiate (prefab ?? Prefab, position, rotation), control);
}
catch {
return null;
}
}
/// <summary>生成前処理</summary>
private bool preCreate () {
if (MaxInstances > 0 && Instances.Count >= MaxInstances) { // 制限数オーバー
if (AutoDelete) { // 最古を破棄して成り代わり
GameObject.Destroy (Instances [0].gameObject);
} else {
return false; // 生成忌避
}
}
return true;
}
/// <summary>生成後処理</summary>
private T postCreate (GameObject obj, bool control) {
var instance = obj.GetComponent<T> () ?? obj.AddComponent<T> ();
if (instance != null) {
if (control) {
Instances.Add (instance);
obj.AddOnDestroyCallback (() => Instances.Remove (instance));
}
} else {
GameObject.Destroy (obj);
}
return instance;
}
/// <summary>全インスタンス破棄</summary>
public void Destroy () {
foreach (var instance in Instances) {
if (instance != null) { GameObject.Destroy (instance.gameObject); }
}
}
/// <summary>後始末</summary>
public void Dispose () {
OnMode = false;
if (prefab) {
Resources.UnloadAsset (prefab);
prefab = null;
}
}
}
このクラスを使うフライテキストは、以下のようになります。
using UnityEngine;
using UnityEngine.UI;
/// <summary>フライテキスト</summary>
public class FlyText : MonoBehaviour {
#region Static
public static ManagedInstance<FlyText> managedInstance { get; protected set; }
static FlyText () {
if (managedInstance == null) { managedInstance = new ManagedInstance<FlyText> (1, true); } // 1個だけ、既にあれば消して成り代わる
}
public static FlyText Create (GameObject parent, string message, float end = 2.5f) {
var instance = managedInstance.Create (parent);
if (instance != null) {
instance.init (message, end);
}
return instance;
}
#endregion
private void init (string message, float end) {
this.transform.SetAsLastSibling ();
this.gameObject.SetActive (true);
var text = GetComponentInChildren<Text> ();
if (text != null) { text.text = message; }
var panelRect = GetComponent<RectTransform> ();
panelRect.sizeDelta = new Vector2 (text.preferredWidth + 128, panelRect.sizeDelta.y); // 文字量に応じたサイズ
Destroy (this.gameObject, end);
}
}
フライテキストの基本的な使い方は変わっていません。
FlyText.Create ("Start!");
以下のような使い方が可能になります。
if (FlyText.managedInstance.OnMode) {
// フライテキスト表示中
}
FlyText.managedInstance.OnMode = false; // フライテキスト全削除
モーダルダイアログのように必要なだけ重ねて使いたい場合は、コンストラクタへ渡す引数が省略されて、以下のようになります。
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.UI;
public class ModalDialog : MonoBehaviour {
#region Static
public static ManagedInstance<ModalDialog> managedInstance { get; protected set; }
static ModalDialog () {
if (managedInstance == null) { managedInstance = new ManagedInstance<ModalDialog> (); } // 数を制限しない
}
//~
そして、複数のダイアログが重なり合っていても、以下のようにして応答すべき最も手前のダイアログを判別できます。
if (managedInstance.LastInstance == this) {
// 一番手前のダイアログなら
}
あとがき
最後までお読みいただき、どうもありがとうございます。
初めてQiitaで記事を書きました。
ご意見、ご感想、ご提案など、何でもいただければうれしいです。
なお、ManagedInstance.cs以外のスクリプトは未検証です。
また、マジックナンバー使ってて汚いですが、その辺りはご容赦ください。
※「ManagedInstance.cs」で使用している「OnDestroyCallback」は、この記事のスクリプトには含まれていませんので、コガネブログ様の記事をご覧ください。
いつも役立つ記事をありがとうございます。この場を借りてお礼申し上げます。