2018年6月27日水曜日

[Unity C#] 汎用的かつシンプルなSingletonの実装方法(サンプルあり)

Unityで実装する際、結構な頻度でManagerクラスが必要だったりするので簡単なSingletonクラスを作成しました。
一言にSingletonクラスといっても、Attributeの設定やら初期設定やら色々有るとは思いますが、今回自分が作成したのは以下の3パターンのみです。

今回実装したSingletonクラス一覧

  1. 全てのクラスで継承可能なSingletonクラス。
    • インスタンス生成時は必ず引数なしのコンストラクタが呼び出されます。
    • 引数ありのコンストラクタが使いたい場合は別途Initialize関数でも作ってください。
  2. 全てのMonoBehaviourを継承したいクラスで継承可能なMonoSingletonクラス。
    • Scene上で同じクラスを保持しているオブジェクトがあるかを確認し、ない場合はインスタンス生成します。
    • インスタンス生成が必要な際は、まず空のGameObjectを作成し、AddComponentで自身を追加します。
    • つまりこちらも引数なしのコンストラクタが呼び出されます。
    • 引数ありのコンスタクタを使いたい場合は(ry
  3. Scene遷移時に削除されないようにしたMonoSingletonを継承したクラス、PersistentMonoSingleton.
    • MonoSingletonを継承し、Scene遷移時に削除されないようにしただけ。
    • Sceneに依存するのか、生存し続けるのかわからなくなりそうだったのでクラス分けしただけです。

サンプルコード

今回のサンプルはかなりシンプル。
ただUpdate関数内でひたすらInstance取得を行うだけ。
また、各Singletonクラスでは生成時にログ吐く用に設定してあります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// <summary>
/// Singleton manager sample.
/// </summary>
public class SingletonManager : MonoBehaviour
{
    /// <summary>
    /// Update this instance.
    /// </summary>
    private void Update()
    {
        // Get instance of singleton every frame to check.
        SampleSingleton singleton = SampleSingleton.Instance;
        SampleMonoSingleton monoSingleton = SampleMonoSingleton.Instance;
        SamplePersistentMonoSingleton persistentMonoSingleton = SamplePersistentMonoSingleton.Instance;
 
        if(singleton == null)
        {
            Debug.LogError("SampleSingleton is null.");
        }
        if(monoSingleton == null)
        {
            Debug.LogError("SampleMonoSingleton is null.");
        }
        if(persistentMonoSingleton == null)
        {
            Debug.LogError("SamplePersistentMonoSingleton is null.");
        }
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TC;
 
public class SampleSingleton : Singleton<SampleSingleton>
{
    public SampleSingleton()
    {
        Debug.Log("Constructor called for " + GetType().FullName);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TC;
 
public class SampleMonoSingleton : MonoSingleton<SampleMonoSingleton>
{
    public SampleMonoSingleton()
    {
        Debug.Log("Constructor called for " + GetType().FullName);
    }
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TC;
 
public class SamplePersistentMonoSingleton : PersistentMonoSingleton<SamplePersistentMonoSingleton>
{
    public SamplePersistentMonoSingleton()
    {
        Debug.Log("Constructor called for " + GetType().FullName);
    }
}

実装サンプル






実際のソースコード

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
namespace TC
{
    /// <summary>
    /// Singleton class for all non MonoBehaviour class.
    /// </summary>
    public abstract class Singleton<T> where T : new()
    {
        private static T instance;
        public static T Instance
        {
            get
            {
                if(instance == null)
                {
                    instance = new T();
                }
                return instance;
            }
        }
 
        /// <summary>
        /// Creates new instance if need.
        /// </summary>
        /// <returns><c>true</c>, if instance is successfully loaded or created, <c>false</c> otherwise.</returns>
        public static bool CreateIfNeed()
        {
            if(instance != null)
            {
                return false;
            }
            // Create instance by calling Getter.
            return Instance != null;
        }
    }
 
    /// <summary>
    /// Singleton class for all Monobehaviour class.
    /// </summary>
    public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoBehaviour
    {
        // Callback on when Instance is requested.
        protected static System.Action onRequestInstance = null;
        // Callback on when Instance is newly created.
        protected static System.Action onCreateInstance = null;
        // Callback on when Instance is found from scene and set to Instance.
        protected static System.Action onUpdateInstance = null;
 
        private static T instance;
        public static T Instance
        {
            get
            {
                if(instance == null)
                {
                    T[] validInstances = GameObject.FindObjectsOfType<T>();
                    if(validInstances == null || validInstances.Length <= 0)
                    {
                        // Create new instance.
                        GameObject gameObject = new GameObject();
                        // At this moment, instance settings will not be applied.
                        // Each settings such as name will be updated on Awake.
                        instance = gameObject.AddComponent<T>();
 
                        if(onCreateInstance != null)
                        {
                            onCreateInstance();
                        }
                    }
                    else
                    {
                        // Load instance from scene.
                        instance = validInstances[0];
                        if(validInstances.Length > 1)
                        {
                            Debug.Log("More than 1 instance is created. Destroying duplicate instances.");
                            for(int i = 1; i < validInstances.Length; i++)
                            {
                                Destroy(validInstances[i].gameObject);
                            }
                        }
 
                        if(onUpdateInstance != null)
                        {
                            onUpdateInstance();
                        }
                    }
                }
 
                if(onRequestInstance != null)
                {
                    onRequestInstance();
                }
                return instance;
            }
        }
 
        /// <summary>
        /// Creates new instance if need.
        /// </summary>
        /// <returns><c>true</c>, if instance is successfully loaded or created, <c>false</c> otherwise.</returns>
        public static bool CreateIfNeed()
        {
            if(instance != null)
            {
                return false;
            }
            // Create instance by calling Getter.
            return Instance != null;
        }
 
 
        /// <summary>
        /// Awake this instance.
        /// </summary>
        protected virtual void Awake()
        {
            InitializeInstance();
        }
 
        /// <summary>
        /// Initializes the instance.
        /// </summary>
        protected virtual void InitializeInstance()
        {
            gameObject.name = this.GetType().FullName;
        }
    }
 
    /// <summary>
    /// Persistent singleton.
    /// This class will not be destroyed on load.
    /// </summary>
    public abstract class PersistentMonoSingleton<T> : MonoSingleton<T> where T : MonoBehaviour
    {
        /// <summary>
        /// Awake this instance.
        /// </summary>
        protected override void Awake()
        {
            base.Awake();
            GameObject.DontDestroyOnLoad(Instance);
        }
    }
}

実際のソースコードはこちらにUPしてあります。
https://github.com/table-cloth/tc-common/blob/develop/Assets/Scripts/Singleton.cs

2018年6月26日火曜日

[Unity C#] オブジェクトのプールの実装方法(サンプルあり)

Unityで開発する際、よくオブジェクトをプールする必要があるのですが、毎回調べなおしたり作り直したりするのが億劫になったので、使い回す用のUtil作成してみました。

とりあえず、ソースはこちらにUPしてあります。

[Pool.cs]
https://github.com/table-cloth/tc-common/blob/develop/Assets/Scripts/Pool.cs
[PoolManager.cs]
https://github.com/table-cloth/tc-common/blob/develop/Assets/Scripts/PoolableObject.cs

オブジェクトのプールって何?

そもそもプールって何よ?という方向けの説明です。
知っている方は読み飛ばしてください。

プールとは何か。

プールとはオブジェクトのストックを保持する場所のことで、オブジェクトの生成、取得処理を効率化されるためによく使われている手法です。
プールを使用する場合、基本的にプールからオブジェクトを取得します。
そしてプールの中に使用可能なオブジェクトが存在する場合はそれを渡し、存在しない場合のみオブジェクトを新規生成しそれを渡します。
また、使用終了したオブジェクトをプールに返すことによって、可能な限りオブジェクトのインスタンス化する処理を省くというものです。

例えば、マシンガンで1万発弾を打つ場合、本来なら1万個のオブジェクト生成が必要ですが、0.01秒に弾を一発発射し、0.1秒で着弾(使用終了)する場合、オブジェクトの生成回数は10回のみで済みます。
そうすることで処理は高速化されますし、シーン上に無駄にオブジェクトを増やさなくて済むことになります。

オブジェクトをプールする方法について。

今回実装した内容は、以下のような内容になっております。

  1. PoolableObjectを設定したPrefabを作成する。
  2. Poolインスタンスを保持したクラスを用意する。
    • 今回はManagerクラスという名前ということにしておく。
  3. ManagerクラスとPoolableObjectを設定したPrefabを紐付ける。
    • SerializeFieldPrefabを設定するか、Resources.LoadPrefabを取得する想定です。
  4. Pool生成時にPoolableObjectを設定。
  5. Pool.GetOrCreate()という関数を呼び出せば自動的に設定したPoolableObjectが取得、または生成される。

ちょっと3番のPoolPrefabの紐づけが手動で煩わしいけれど、プールしたい物体なら毎回Prefabを設定するだろうということで今回の実装となっています。

サンプルコード

0.1秒ごとにオブジェクトをプールから取得し、1.0秒毎にオブジェクトをプールに返すサンプル。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// <summary>
/// Sample pool manager.
/// </summary>
public class SamplePoolManager : MonoBehaviour
{
    private const float SpawnDelay = 0.1f;
    private const float DestroyDelay = 1.0f;
 
    [SerializeField]
    private PoolableObject poolableObject;
    private Pool pool;
 
    public void Start()
    {
        pool = new Pool(poolableObject);
        StartCoroutine(RepeatSpawnPoolableObject(SpawnDelay));
    }
 
    /// <summary>
    /// Repeats the spawn poolable object.
    /// </summary>
    /// <returns>The spawn poolable object.</returns>
    /// <param name="_delay">Delay.</param>
    private IEnumerator RepeatSpawnPoolableObject(float _delay)
    {
        yield return new WaitForSeconds(_delay);
 
        PoolableObject obj = pool.GetOrCreate(this.transform);
 
        float randX = Random.Range(0.0f, 1.0f);
        float randY = Random.Range(0.0f, 1.0f);
        float randZ = Random.Range(0.0f, 1.0f);
        obj.GetComponent<Rigidbody>().velocity = new Vector3(randX, randY, randZ);
        obj.transform.localPosition = Vector3.zero;
 
        obj.Return2Pool(DestroyDelay);
 
        StartCoroutine(RepeatSpawnPoolableObject(_delay));
    }
}

実行サンプル


実際のコード

プールクラス。

オブジェクトのプールを行う場合、こちらのインスタンスが1つ必要。
インスタンスが複数存在してしまうと、プールの中身が分散されてしまうので要注意。

基本的にGetOrCreateReturn2Poolだけで使えます。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// <summary>
/// Class for basic pooling.
/// Will need to be used with PoolableObject.
/// </summary>
public class Pool
{
 
    private const int PoolSizeNoLimit = -1;
 
    // Object to be pooled.
    private readonly PoolableObject poolableObject;
    private readonly Queue<PoolableObject> pool;
 
    /// <summary>
    /// Initializes a new instance of the <see cref="Pool"/> class.
    /// </summary>
    /// <param name="_poolableObject">Poolable object.</param>
    /// <param name="_maxPoolSize">Max pool size.</param>
    public Pool(PoolableObject _poolableObject, int _maxPoolSize = PoolSizeNoLimit)
    {
        poolableObject = _poolableObject;
        pool = _maxPoolSize == PoolSizeNoLimit
            ? new Queue<PoolableObject>()
            : new Queue<PoolableObject>(_maxPoolSize);
    }
 
    /// <summary>
    /// Gets the pool instance from queue or creates new pool instance.
    /// </summary>
    /// <returns>The or create.</returns>
    /// <param name="_parent">Parent.</param>
    /// <param name="_localPos">Local position.</param>
    public PoolableObject GetOrCreate(Transform _parent, Vector3 _localPos = default(Vector3))
    {
        PoolableObject poolObj = GetPoolableObject(_parent);
        if (poolObj == null) poolObj = CreatePoolableObject(_parent);
 
        // Initialize poolable object, no matter get or create.
        poolObj.gameObject.SetActive(true);
        poolObj.transform.localPosition = _localPos;
 
        return poolObj;
    }
 
    public List<PoolableObject> GetAllObjectsInPool()
    {
        return new List<PoolableObject>(pool);
    }
 
    /// <summary>
    /// Returns pool instance to the pool.
    /// </summary>
    /// <param name="_poolableObject">Poolable object.</param>
    public void Return2Pool(PoolableObject _poolableObject)
    {
        pool.Enqueue(_poolableObject);
        _poolableObject.gameObject.SetActive(false);
    }
 
    /// <summary>
    /// Gets the poolable object from queue.
    /// </summary>
    /// <returns>The poolable object.</returns>
    /// <param name="_parent">Parent.</param>
    private PoolableObject GetPoolableObject(Transform _parent)
    {
        if (!IsPoolObjectAvailable()) return null;
 
        PoolableObject poolObj = pool.Dequeue();
        poolObj.transform.SetParent(_parent);
        return poolObj;
    }
 
    /// <summary>
    /// Creates new poolable object.
    /// </summary>
    /// <returns>The poolable object.</returns>
    /// <param name="_parent">Parent.</param>
    private PoolableObject CreatePoolableObject(Transform _parent)
    {
        PoolableObject poolObj = PoolableObject.Instantiate(this, poolableObject);
        poolObj.transform.SetParent(_parent);
        return poolObj;
    }
 
    /// <summary>
    /// Determines whether reusable pool object is available in pool.
    /// </summary>
    /// <returns><c>true</c> if this instance is pool object available; otherwise, <c>false</c>.</returns>
    private bool IsPoolObjectAvailable()
    {
        return pool.Count > 0;
    }
}

プールしたいオブジェクト用クラス。

基本的にこちらの処理を呼び出す必要はなし。
必要に応じてReturn2Poolを呼び出してあげればOK。
Return2PoolPool、または PoolableObjectのどちらかで1回呼ぶだけでOK。)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// <summary>
/// Class for pooling objects.
/// </summary>
public class PoolableObject : MonoBehaviour
{
 
    // Pool where this object is spawned from.
    private Pool pool;
 
    /// <summary>
    /// Instantiate the specified _poolableObject in _pool.
    /// </summary>
    /// <param name="_pool">Pool.</param>
    /// <param name="_poolableObject">Poolable object.</param>
    public static PoolableObject Instantiate(Pool _pool, PoolableObject _poolableObject)
    {
        PoolableObject poolableObject = Instantiate(_poolableObject);
        poolableObject.pool = _pool;
        return poolableObject;
    }
 
    /// <summary>
    /// Returns this poolable object to pool.
    /// </summary>
    public void Return2Pool() {
        pool.Return2Pool(this);
    }
 
    /// <summary>
    /// Returns this poolable object to pool after delay sec.
    /// </summary>
    public void Return2Pool(float _delay)
    {
        StartCoroutine(CoReturn2Pool(_delay));
    }
 
    /// <summary>
    /// Returns this poolable object to pool after delay sec.
    /// </summary>
    private IEnumerator CoReturn2Pool(float _delay)
    {
        yield return new WaitForSeconds(_delay);
        Return2Pool();
    }
 
}
sa

2018年6月25日月曜日

【ブログ初心者用】htmlでの記事投稿お助けツールAtomについて


実は私、今Bloggerで記事を書いていますが、つい最近ブログを始めてみようと思った結果、はじめに選んだものはLivedoorBlogでした。

そしてブログを作って「さぁ書くぞー!」ってなってふと気がついたことが…。


「あれ、ブログってマークダウンじゃなくてHTMLで書くの…?」

冷静に考えればまぁそりゃそうだ、な感じなんですが、なぜか当然のようにマークダウンでブログをかける感覚でいました…。
(調べた所、別にマークダウン対応しているブログもいくつかあるようでしたが、作って早々に書きづらいから、でブログを変えるのもなー、と思い対策を調べてみました。)

で、そこでまさかのマークダウンで書いた文章をHTMLに変換できるエディターを発見したので早速インストール!

[Atom]
https://atom.io/

試してみた所、マークダウンを書きながら、リアルタイムでHTMLプレビュー変換できたり、簡単にHTMLとしてファイル保存/出力できたりとかなり便利な感じでした。
ので、とりあえず暫くの間このエディタを使って記事を書かせていただこうと思います。



ただ実はもう一点問題があり、
流石にマークダウンだけではブログの見た目の改修はできないので、流石にCSSは覚えないとなーという感じです。
そのうちちょこちょこ調整するとは思いますが、CSS勉強したら実際のCSSに関する記事とか上げるかもしれません。

実はまだBloggerでCSS適用できんの?とか
もっと楽な方法あるんじゃないの?とかはまだ調べていないのでわかりませんが…。
何か良い方法とか勉強進んだら記事にするかもしれません。

Atomの便利なショートカット(随時追加予定)

  1. Ctrl+Shift+M : マークダウン -> HTML変換


参考にさせていただいたサイト様

https://webtatan.com/blog/wordpress/wordpress-markdown

2018年6月23日土曜日

GitHubで新規プロジェクトを作成した際に毎回行う必須手順。

せっかくBloggerに移ったので、以前のブログを消すのに合わせて以前書いた記事をちょっと更新しつつ書き直しておきます。


2018年6月22日金曜日

はじめてみました。

Bloggerでブログをはじめて見ました。
元々LivedoorBlogを登録してみて、運用してみようかなと試していたのですが、なにやらアドセンスが登録できなかったりしたもので、色々調べた結果Bloggerを試してみることにしました。

よろしくおねがいします。

技術ブログを始める際に困ったこと。コードの見やすさについて。

以前の記事 で Atom使ってマークダウンで記事書けばすっごい楽! ということを書きましたが、色々試しているうちにどうやらマークダウンだけでは少々問題がありそうだったので、主な問題とその解決方法について書いていきます。 まぁタイ...