全8回の短期連載「Starlingフレームワークを用いたStage3Dによる2Dアニメーション」は今回が最後になります。そこで、少し視点を移して、Starlingフレームワークの使い方でなく、Starlingのクラスがどのようにつくられているかを見てみます。具体的には、Stage3Dの速さを活かすため、どう最適化がはかられているかを確かめます。中でもとくに、オブジェクトの使い回しに注目して、Starlingフレームワークにかぎらず、ActionScript 3.0で使える技を拾っていきましょう。
今年1月7日、Adobe Developer Connection (ADC)にKeith Gladstien氏による「Flashのパフォーマンスの最適化」という記事が掲載されました[*1]。CPU/GPUの負荷とメモリの消費をいかに適切な範囲に抑えるかについて解説しています。そして、最適化にもっとも効果のある技術のひとつが、「Stage3Dを使用する」だとしています。ただし難点として、「Stage3D APIを使用するのは難しい」ことを挙げます。これは、Starlingなどのフレームワークを用いることで、克服できます。
また、イベントリスナーの「dispatchEventをコールバック関数に置き換える」(「メモリの管理」)ことも有効とされます。「イベントをコールバック関数で扱う」ことの意義については、第4回「StarlingフレームワークのTweenクラスにおける最適化とJugglerクラスの実装」ですでに解説しました。
本稿のお題は、オブジェクトのリサイクルです。ADCの記事も、「オブジェクトの再利用」が、パフォーマンスを向上させるのに役立つと述べます(「メモリの管理」)。さらに「オブジェクトのプール」というのは、オブジェクトが使い回されるように定めた仕組みです。パフォーマンスもさることながら、費やすメモリが抑えられます(同前)。
では、Starlingフレームワークの中に、オブジェクトのリサイクルがどのように採入れられているのかを見ていきましょう。
[*1] もとの英文記事「Optimizing Flash performance」は、2012年10月22日に書かれました。日本語記事に「原文 作成日: 2011/10/22」とあるのは誤りでしょう。 |
第4回「StarlingフレームワークのTweenクラスにおける最適化とJugglerクラスの実装」02「Tweenオブジェクトを使い回す」でご説明したとおり、「オブジェクトは新たにつくるより設定し直す方が速い」です。
たとえば、矩形領域を表すRectangleオブジェクトは、コンストラクタに4つの引数を渡してつくります。
new Rectangle(x座標, y座標, 幅, 高さ)
それに対して、すでにRectangleオブジェクトがあれば、4つの引数の値はプロパティで定めることもできます。
Rectangleオブジェクト.x = x座標;
Rectangleオブジェクト.y = y座標;
Rectangleオブジェクト.width = 幅;
Rectangleオブジェクト.height = 高さ;
すでにあるオブジェクトはもう使わないのであれば、そのプロパティを書替える方が、新たにオブジェクトをつくり直すよりエコでお得なのです[*2]。もっとも、プロパティをひとつひとつ定めるのは、入力の手間がかかります。コンストラクタと同じ手軽さで、初期化と設定のできるメソッドがあると便利です。
そこで、Tweenクラスに目を向けると、Tween.reset()
というまさに初期化のためのメソッドがあります(前出「Tweenオブジェクトを使い回す」参照)。引数はつぎのように、Tween()
コンストラクタと同じです。つまり、もう使わないTweenオブジェクトがあれば、コンストラクタの替わりにTween.reset()
メソッドを呼出せばよいのです。
Tweenオブジェクト.reset(インスタンス, 時間, イージング)
new Tween(インスタンス, 時間, イージング)
Tweenクラスを見ると、Tween()
コンストラクタの実装はつぎのように、同じ引数で直ちにTween.reset()
メソッドを呼出しています。
public function Tween(target:Object, time:Number, transition:String = "linear") { reset(target, time, transition); }
Tween.reset()
メソッドの実装は、引数に受取った値をプロパティに定めるとともに、他に必要なプロパティを初期化しています。使い回しが求められるオブジェクトのクラスを定めるときには、このようにコンストラクタから初期化のメソッドを呼出し、そのメソッドも公開するという手法は参考になります。
public function reset(target:Object, time:Number, transition:Object="linear"):Tween { mTarget = target; mTotalTime = Math.max(0.0001, time); // プロパティの初期化 if (transition is String) this.transition = transition as String; else if (transition is Function) this.transitionFunc = transition as Function; // ...[中略]... return this; }
[*2]「オブジェクトを使い回す」はRectangleオブジェクトについて、コンストラクタでつくるときと、プロパティを設定し直す場合とでどれだけの差があるか、wonderflのサンプルで比べてみました。 |
オブジェクトをリサイクルすると、使うメモリも抑えられます。もちろん、オブジェクトをたくさんつくっても、要らなくなればかたづけられます。けれど、すぐにメモリからは消されないのです。
メモリに残るオブジェクトが要るか要らないかは、「ガベージコレクション」という仕組みが調べます。そのオブジェクトが参照されていれば使われているとみなし、参照がなくなると要らないオブジェクトとしてメモリから消し去ります。
いわば食堂や居酒屋で、お客さんが席を立って帰ったら、店員が空いたテーブルを片づけるのと同じです(図001)。もっとも、料理をつくって席に運ぶ本来の仕事もあります。あまりに大勢の客が出入りすると、合間のかたづけが間に合わなくなってきます。
図001■席が空いたらテーブルをかたづける
同じように、オブジェクトがたくさん使い捨てされると、ガページコレクションが大忙しです。参照のあるなしを確かめるのに手間取り、かたづけきれないオブジェクトはメモリに居座ります。
後の客が前の客の席を自分でよしなに使ってくれれば、店員はかたづけをせずに済みます。無駄な空席もなくなるでしょう。つまり、オブジェクトが使い回されれば、ガベージコレクションの手間は減り、メモリも抑えられるということです。ちなみに、第1回でご紹介した「ヤサイマシマシ」のお店は、前の客が食器をカウンターにかたづけてテーブルを拭くそうなので、これもガページコレクションを減らす工夫といえるでしょう。
Starlingフレームワークは、使い捨てるとわかっているオブジェクトは、予めクラスの静的プロパティにもっています。Touch.getLocation()
メソッドの実装を見ましょう(「starling.events.Touchクラスと座標の変換」参照)。Touchクラスにはリサイクル用のMatrixオブジェクトが、内部的に静的プロパティ(sHelperMatrix)として備えられています。
Touch.getLocation()
メソッドは、DisplayObject.base
プロパティが参照する表示リスト最上位のインスタンスに対して、DisplayObject.getTransformationMatrix()
メソッドを呼出します。このとき、メソッドの第2引数に静的プロパティ(sHelperMatrix)のMatrixオブジェクトを渡しています。そして、変換が加えられたMatrixオブジェクトはMatrixUtil.transformCoords()
メソッドに渡し、戻り値として得たPointオブジェクトを返します。
public class Touch {
private static var sHelperMatrix:Matrix = new Matrix();
public function getLocation(space:DisplayObject, resultPoint:Point = null):Point {
// ...[中略]...
space.base.getTransformationMatrix(space, sHelperMatrix);
return MatrixUtil.transformCoords(sHelperMatrix, mGlobalX, mGlobalY, resultPoint);
}
}
DisplayObject.getTransformationMatrix()
メソッドの実装は、第2引数にMatrixオブジェクトを受取ればMatrix.identity()
メソッドで初期化し、引数がなければ新たなMatrixオブジェクトをつくります。したがって、第2引数を渡さなくても処理は進められます。けれども、リサイクル用オブジェクト(sHelperMatrix)を使うことで、ガベージコレクションの手間やメモリの無駄が省けます[*3]。
public class DisplayObject extends EventDispatcher {
public function getTransformationMatrix(targetSpace:DisplayObject, resultMatrix:Matrix = null):Matrix {
// ...[中略]...
if (resultMatrix) resultMatrix.identity();
else resultMatrix = new Matrix();
// ...[中略]...
return resultMatrix;
}
}
MatrixUtil.transformCoords()
メソッドの実装も確かめておきましょう。第1引数のMatrixオブジェクトにもとづき、第2および第3引数のxy座標を変換してPointオブジェクトで返します。
public class MatrixUtil {
public static function transformCoords(matrix:Matrix, x:Number, y:Number, resultPoint:Point = null):Point {
if (resultPoint == null) resultPoint = new Point();
resultPoint.x = matrix.a * x + matrix.c * y + matrix.tx;
resultPoint.y = matrix.d * y + matrix.b * x + matrix.ty;
return resultPoint;
}
}
以上のとおり、静的プロパティ(sHelperMatrix)のオブジェクトは使い捨てで、Touch.getLocation()
メソッドを呼出すたびに使い回されることが確かめられました。
[*3]「オブジェクトを使い回す」はMatrixオブジェクトについても、 Recycling vs Creating objects 2 - wonderfl build flash online なお、 |
オブジェクトをプールに溜めて使い回す仕組みが「オブジェクトプーリング」です。オブジェクトが使いたくなったら、すぐにつくるのではなく、まずプールを探します。すでにオブジェクトがあればそれを用い、なければ新たなオブジェクトをつくります。そして、使い終えたオブジェクトはプールに戻します。
第5回「Starlingフレームワークでイベントリスナーを扱う」04「イベントオブジェクトを使い回して配信するEventDispatcher.dispatchEventWith()メソッド」でご説明した、EventDispatcher.dispatchEventWith()
メソッドがイベントオブジェクトを使い回す仕組みはこのオブジェクトプーリングです。EventDispatcher.dispatchEventWith()
メソッドの実装から見ましょう。
新たなEventオブジェクトは、Event()
コンストラクタではなく、静的メソッドEvent.fromPool()で取出します。そして、EventDispatcher.dispatchEvent()
メソッドでイベントを配信したら、使い終えたEventオブジェクトをEvent.toPool()メソッドで戻します。
public class EventDispatcher {
public function dispatchEventWith(type:String, bubbles:Boolean=false, data:Object=null):void {
// ...[中略]...
var event:Event = Event.fromPool(type, bubbles, data);
dispatchEvent(event);
Event.toPool(event);
// ...[中略]...
}
}
Eventクラスの実装は、静的メソッドfromPool()とtoPool()を内部的につぎのように定めています[*4]。
fromPool()メソッドはEventオブジェクトを溜めるVectorオブジェクト(sEventPool)を確かめて、オブジェクトがあれば初期化し、なければコンストラクタでつくって返します。初期化のメソッドreset()は、Eventクラスでは内部的に定められています。また、toPool()メソッドは、Eventオブジェクトのプロパティを初期化して、Vectorオブジェクトに戻します。
public class Event {
private static var sEventPool:Vector. = new [];
starling_internal static function fromPool(type:String, bubbles:Boolean=false, data:Object=null):Event {
if (sEventPool.length) return sEventPool.pop().reset(type, bubbles, data);
else return new Event(type, bubbles, data);
}
starling_internal static function toPool(event:Event):void {
event.mData = event.mTarget = event.mCurrentTarget = null;
sEventPool.push(event);
}
starling_internal function reset(type:String, bubbles:Boolean=false, data:Object=null):Event {
mType = type;
mBubbles = bubbles;
mData = data;
mTarget = mCurrentTarget = null;
mStopsPropagation = mStopsImmediatePropagation = false;
return this;
}
}
第6回「Starlingフレームワーク1.3の新たなフィルタのクラスとトゥイーンのメソッド」04「Jugglerクラスの新たなメソッドでトゥイーンを加える」でご紹介したJuggler.tween()
メソッドも、同じようにオブジェクトプーリングの仕組みを採入れています。
[*4] starling_internalは、starling.coreパッケージに定められた名前空間です。決まった他のクラスからのみ参照できるように設けられた非ドキュメントの名前空間です。将来のバージョンのStarlingでは除かれる可能性があります。 |
定義済みのActionScript 3.0とは違って、StarlingフレームワークはActionScriptの実装を見て確かめることができます。それは、Starlingフレームワークへの理解を深めるとともに、ActionScriptのクラスを設計するうえで役立つことも多いでしょう。Stage3Dを使い、最適化されたスクリプティングを考えるときに、本連載が何らかのお役に立てば幸いです。