北条ゲームズ Hojo Games

錦の北条の開発ブログ

スポンサーサイト 

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

【初心者向け】Unityで「まともな」プーリング 

5/1 文章ある程度校正

皆さんこんにちは。北条です。

社会人二年目になりました。もう21歳ですし、今年中には22歳です。なんということだ!PIONEERS(最初の作品)に着手した時って16歳くらいだった気がするので月日が流れるのは早いなあ、死がやってくるなあとしみじみ。

いい会社に入ったなと最近よく思ってます。僕はもともと学生の頃など精神的に異常で脆弱な人間だったので(ファ〇マのアレな夜勤で精神を崩壊させ殆ど鬱になる、学校をやめる寸前まで行く、年平均欠課時数200程など)うまいこと成長するように唆してくれた環境には素朴に感謝があり(僕が感謝なんて単語を書くとと胡乱げに見えてしまいそうだが)、色々経験して、ゲーム開発者としての純然なバーバリアンを目指していけてる感じがしますね。まあそれについてはエピソードめいた具象性を帯びた話はここには書けないし書いても他の人に面白いものでもないのでどうでもいいんですが、つまりは結果として、プライベートでも開発は順調にもりもりやっているという事です。(まだ今作は潜伏中ですが、そろそろ表に出していくフェーズは近づいています)


では本題に入りましょう。オブジェクトプーリングですが、Unityユーザーの皆さんご存知でしょうか。
簡単に説明すると
何らかの何度も使う対象は、生成破棄を繰り返して利用するのではなく、場合によっては再利用できるならしよう(なければその時初めてインスタンスを生成する)、みたいなコンセプトですね。パフォーマンス稼ぎの目的です。生成インパクト抑制とGC発生頻度に効果があります。
実装の基本としては、生成されたものをメモリ領域に保持しておいて、もじ利用要求があったら使われてないものを探して利用させてあげる、みたいなものになるでしょう。
オブジェクトプーリングなんてものはUnityだけの話では全くない月並みな何かですが、
UnityのGameObject系は特に生成コスト一定あるのでプールしようという話は一定広まっていてUnity プーリングとかで検索すると日本語の記事が沢山出てきますね。
じゃあ何で今更ここで乗っけるんだって話なんですけど、検索で引っかかって確認した範囲の全ての実装が有名な概念なのに見ていて厳しい表情になるものしかなかったからですね(直球)
タイトルに「まともな」プーリングと記載したのは、そういう訳です。
つまり「ゲーム開発の民主化」の思想にコミュニティの一員として則り、それらをそのままのさばらしておくよりはここらで、素朴ではあるが常識的なものをアップしておこうという魂胆です。社会貢献です。偉い!

ではコードです

GenericObjectPool.cs(プールの仕組み)



	public class GenericObjectPool< T >
		where T : IPoolable
	{
		private Func< T > factoryMethod;

		private List< T > instanceList = new List< T > ();

		public GenericObjectPool (Func< T > factory)
		{
			factoryMethod = factory;
		}

		public T GetInstance (bool markInstanceUsing = true)
		{
			for (int i = 0; i < instanceList.Count; i++) {
				if (!instanceList [i].IsUsing) {
					instanceList [i].IsUsing |= markInstanceUsing;
					return instanceList [i];
				}
			}
			instanceList.Add (factoryMethod.Invoke ());
			var createdInstance = instanceList [instanceList.Count - 1];
			createdInstance.IsUsing |= markInstanceUsing;
			return createdInstance;
		}

		public void Clear ()
		{
			for (int i = 0; i < instanceList.Count; i++) {
				instanceList [i].Destroy ();
			}
			instanceList.Clear ();
		}
	}



IPoolable.cs(プール可能である契約)


	public interface IPoolable
	{
		bool IsUsing {
			get;
			set;
		}

		void Destroy ();
	}


以上です。

一応、検索で引っかかった特定の何かをやり玉に挙げて殴ると悪いインターネットっぽいので抽象的な悪の概念と比較しながら解説しましょう。

これがUnityで利用される時にいくつか特徴を感じられる気がするのは、まず別にプール対象をGameObjectで固定してないところです。(これは、Unity プーリングとかで検索で調べて出てくる概念を知らないと、文脈が謎過ぎる特徴だとは思います……)
生成方法と管理される方法が特定の対象(GameObject等)に規定されず全ての概念に対してプールを適応できます。それで何がいいのかって話は、自分できちんと再利用という概念に対してルールを設けられるところですね。IPoolableを実装して利用されているかどうか(IsUsing)、という契約を履行さえすればそれはプール可能なのです。(状態の詳細が再利用にできることは暗に必要になりますが、まあIsUsingという状態があるならそれは必須であることが分かりましょう)
//逆に、GameObjectに密結合してGameObject.activeSelfとか既存の実装の詳細に直接アクセスし勝手に意図を感じ取って再利用可能かどうかを推測するとかそういうのはちょっとヤバいので良い子は……やめようね!(使用方法がはっきりせず、脆く、限定的で、ゴミである)そういう実装がインターネッツには多分に跋扈してたからマジで真似するなよ!
また、もしこの手法を取らずプール対象を固有にすると、つまりプール自体の仕組みが特定の何かと結びつくと、別の何かに特化したプールを作りたい時、毎回プールの仕組みをベタっと書く訳ですね。プール自体の機構が散乱……うそやん!最悪ですわ、それは。
本質的にプールで利用される概念のルールはシンプルになりますから(IPoolable)、それを満たすものの方をたくさん用意するべきでしょう。


で、もう一つの特徴を感じられる気がする箇所は(いやまあ普通レベルの話ですが)、プールするオブジェクトの生成をFunc<T>という形でコンストラクタで受け取ってる点でしょうか。
どんな生成方法をもった概念にも対応できるようにしたいという話ですね。例えばGameObjectもリソースから読み込んでインスタンス化とかやると思いますが、他の概念ではただnewすればいいだけのこともあるわけですが、十把一からげに基盤としては扱えるようにしたいという意図であります。
Func<T>は初心者の方もしかするとなんだか分からないかもしれないですが、Tクラスを戻り値で出すメソッドのオブジェクトです。処理を引数として送り込んでます。
知っていれば簡単な仕組みですが、この辺りも含め上の解説とコードだけだと不親切なので実際にどういう利用をされるか例を示しましょう。
GameObjectに密結合しないからこそできる、loopingじゃないParticleSystemをGameObjectのactiveを変えずに再利用する様子を示します。(これは、パフォーマンス面でSetActiveが走らない分更なる利点がある=再利用の意図を明示的に定義することの利点)
コードは僕の今開発中のゲームで回避アクションのときにシステム的に発生するエフェクトについてプールシステムを利用している概念をちょっとだけいじって適当に持ってきました。


PoolableActionParticleSystem.cs(ParticleSystemをプール可能にするラッパークラス)
//ResourcesRootTypePathは独自のResourcesのパスのクラスです(直接プロジェクトから持ってきているので許してください)

public class PoolableActionParticleSystem:IPoolable
	{
		public static PoolableActionParticleSystem Create (ResourcesRootTypePath resourcesPath, Transform parent = null)
		{
			var gameObjectInstance = GameObject.Instantiate (HojoResources.Load< GameObject > (resourcesPath));

			//GameObjectの本来(それはメタでありシーン)のライフサイクルに依存するのはプールの概念として考えるとよくないので、明示的に破棄されるまでは生きるように
			GameObject.DontDestroyOnLoad (gameObjectInstance);
			if (parent != null) {
				gameObjectInstance.transform.SetParent (parent);
			}
			return new PoolableActionParticleSystem (gameObjectInstance.GetComponent< ParticleSystem > ());
		}

		private ParticleSystem particleSystem;

		public ParticleSystem ParticleSystem {
			get {
				return particleSystem;
			}
		}

		public PoolableActionParticleSystem (ParticleSystem particleSystem)
		{
			if (particleSystem == null) {
				throw new UnityException ("particleSystem is null.");
			}
			if (particleSystem.main.loop) {
				throw new UnityException ("PoolableActionParticleSystem won't accept looping ParticleSystem.");
			}
			this.particleSystem = particleSystem;
		}

		#region IPoolable implementation

		public void Destroy ()
		{
			GameObject.Destroy (particleSystem.gameObject);
		}

		public bool IsUsing {
			get {
				return particleSystem.isPlaying;
			}
			set {
				if (value) {
					particleSystem.Play ();
				} else {
					particleSystem.Stop ();
				}
			}
		}

		#endregion
	}

DodgeEffectFactory.cs(PoolableActionParticleSystemを利用してシステム的回避エフェクトをプールし生成するFactoryクラス)

	public static class DodgeEffectFactory
	{
		private static readonly ResourcesRootTypePath resourcePath = new ResourcesRootTypePath ("Field/CharacterAction/DodgeEffect");

		private static GenericObjectPool< PoolableActionParticleSystem > effectPool;
static DodgeEffectFactory () { effectPool = new GenericObjectPool< PoolableActionParticleSystem > (() => PoolableActionParticleSystem.Create (resourcePath));
} public static void Create (SkinnedMeshRenderer skinnedMeshRenderer, Color dodgeColor) { var instance = effectPool.GetInstance (false); var particleSystem = instance.ParticleSystem; var shape = particleSystem.shape; shape.skinnedMeshRenderer = skinnedMeshRenderer; var main = particleSystem.main; main.startColor = new ParticleSystem.MinMaxGradient (dodgeColor); instance.IsUsing = true; } }

さて、理解できたでしょうか……
正直途中から気付きつつはあったのですが、ここまで書いて、【初心者向け】という記事のラベルは外した方がいいんじゃないかと殆ど認識していますが、まあ貫きます
エンジニア感覚での初心者向けになってますね。
まあ全体としてそこまで複雑ではないのですが、文脈が深いため適切にするならもっとたくさんの日本語を書かないといけない印象があり、それはだるい。
(暇だったら後で解説もっと丁寧にします)

さっき軽く触れたgenericObjectPoolがFunc<T>を受け取ってた所ですが、これがこのケースでは実際にどうなっているかというと
new GenericObjectPool< PoolableActionParticleSystem> (() => PoolableActionParticleSystem.Create (resourcePath));
こんな感じになってます。これはラムダ式という記法で、Func<T>とかメソッドオブジェクトを要求してるところとかで便利に使える簡単な無名メソッドの書き方です。
PoolableActionParticleSystemにCreateというstaticなメソッドが存在し、Tクラス、つまりこの場合PoolableActionParticleSystem自身のことですが、を返すものがあるので、これをそのままFunc<T>の戻り値になるように突っ込んであげてます。
(ラムダ式については検索で調べてください というか、やっぱり初心者でもC#は一通り抑えてくれると色々理解も円滑になると思うので諸々やっておくのはおすすめです)

全体の流れを敷衍すると
・プールできる対象のクラス(PoolableActionParticleSystemのようなIPoolableを実装したもの)
を記述し、
・それを求める特定のものとして作ってくれるもの(DodgeEffectFactoryのようなもの)でGenericObjectPoolに突っこんで利用する
って感じでした。

以上です。GW楽しんでいきましょう!
(GW中に一回虚無と物質の彼女の進捗・情報についての記事アップする予定です……お楽しみに!)


Comment

Add your comment

上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。