|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
■Twitter: @FumioNonaka / Mailing List: ActionScript 3.0
JaGra PROFESSIONAL SCHOOL Seminar ActionScript 3.0 パフォーマンスチューニング
|
Date: 2012年2月25日 | Product: Flash Professional | Platform: All | Version: CS5/ActionScript 3.0 |
基礎編については、F-siteセミナー「みんなのFlash効率化大作戦」をご参照いただきたい。
『ActionScript 3.0 パフォーマンス チューニング』 Now on Sale!! |
■サンプルファイル (Flash CS5形式/約762KB) |
Math.floor()メソッドは、引数値を超えないもっとも大きい整数を返す。以下のスクリプト001に定義した関数xGetRandomInt()は、最小値と最大値のふたつの整数の引数から、ランダムな整数を返す(ランダムな整数の計算については、少し古いが「Math.random() でランダムな整数を取得する方法」を参照)。たとえば、サイコロの目と同じ1から6までのランダムな整数を得るには、xGetRandomInt(1, 6)のように呼出す。
スクリプト001■Math.floor()メソッドによりランダムな整数を返す関数
|
int()関数は、引数値の小数点以下を除き、整数部のみを返す。正の数については、Math.floor()メソッドと戻り値は同じになる。そこで、演算の速いint()関数で書替えたのがつぎのスクリプト002だ。なお、ふたつの引数の大小が逆になったとき、変数値の順序を正すよう条件判定の処理も加えた(第2〜6行目)。
スクリプト002■int()関数によりランダムな整数を返す関数
|
Math.floor()メソッドとint()関数は負の数の扱いが異なる。-2.5から2.5まで1ずつ引数値を増やしとき、ふたつの計算結果は下表001のようになる。
表001■正負にまたがる引数に対するMath.floor()メソッドとint()関数の戻り値
for (var i:Number = -2.5; i < 3; i++) {
trace(i, Math.floor(i), int(i));
}
引数 | Math.floor() | int() |
-2.5 | -3 | -2 |
-1.5 | -2 | -1 |
-0.5 | -1 | 0 |
0.5 | 0 | 0 |
1.5 | 1 | 1 |
2.5 | 2 | 2 |
ふたつの数値から最大値または最小値を求めたり、絶対値を得る場合には、Math.max()やMath.min()あるいはMath.abs()メソッドを用いるより、条件演算子:?で処理した方が速い(表002)。
表002■最大値・最小値・絶対値を求めるメソッドの条件演算子:?による書替えMathクラスのメソッド | 条件演算子?:による書替え |
Math.max(a, b) | (a > b) ? a : b |
Math.min(a, b) | (a < b) ? a : b |
Math.abs(n) | (n < 0) ? -n : n |
Mathクラスのメソッドより条件演算子:?の方が速い理由をふたつ補足する。第1に、Math.max()とMath.min()メソッドの引数の数はいくつでもよい。それに対して、条件演算子では、ふたつの数値にかぎって式を立てたので、その分有利になる。
第2は、条件演算子?:の式が、関数として定められていないことだ。式をステートメントとして直接書く(これを「インライン」という)方が、呼出しのない分関数より速くなる(「その他の最適化」参照)。
Point.distance()とVector3D.distance()は、ともに座標と座標の間の距離を求める静的メソッド。引数には、座標が納められたPointまたはVector3Dインスタンスをふたつ渡す。たとえば、2次元平面上のふたつの座標(1, 1)と(2, 1 + √3)との間の距離は、PointオブジェクトとPoint.distance()を使ってつぎのように求められる。
var begin:Point = new Point(1, 1);
var end:Point = new Point(2, 1 + Math.sqrt(3));
var distance:Number = Point.distance(begin, end);
trace(distance); // 出力: 1.9999999999999998
2次元平面上の2点の座標をそれぞれ(x1, y1)および(x2, y2)とし、2点間の距離lは三平方の定理で求められる(図001)。
図001■2次元平面上の2点間の距離を三平方の定理で求める
|
座標間の距離はPoint.distance()やVector3D.distance()メソッドを使うより、三平方の定理で求めた方が速い。ふたつのPointオブジェクトの座標間の距離は、つぎのスクリプト003の関数で得られる。なお、2乗の計算はMath.pow()メソッドは使わずに、変数値を2回乗じている。
スクリプト003■2次元平面の2点間の距離を引数のふたつのPointオブジェクトから求めて返す
var begin:Point = new Point(1, 1);
var end:Point = new Point(2, 1 + Math.sqrt(3));
var distance:Number = distance2D(begin, end);
trace(distance); // 出力: 1.9999999999999998
|
3次元空間でも、z座標が加わるだけで、同じように三平方の定理で距離が導ける(図002)。
図002■3次元空間の2点間の距離を三平方の定理で求めるふたつのVector3Dオブジェクトの座標間の距離を求める関数は、つぎのスクリプト004のように定められる。
スクリプト004■3次元空間の2点間の距離を引数のふたつのVector3Dオブジェクトから求めて返す
var begin:Vector3D = new Vector3D(1, 1, 1);
var end:Vector3D = new Vector3D(2, 1 + Math.sqrt(3), 1);
var distance:Number = distance3D(begin, end);
trace(distance); // 出力: 1.9999999999999998
|
DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)は、おもにアニメーションで用いられる。イベントリスナーで扱うインスタンスの数がきわめて多い場合には、インスタンスごとにリスナーを加えるのではなく、ひとつのリスナーでまとめてインスタンスを処理する方が速い。
// フレームアクション: アニメーションさせるMovieClipシンボル
- var nWidth:Number = stage.stageWidth;
- var nHeight:Number = stage.stageHeight;
- x = nWidth * Math.random();
- y = nHeight * Math.random();
- addEventListener(Event.ENTER_FRAME, xFall);
- function xFall(eventObject:Event):void {
- y += 5;
- if (y > nHeight) {
- y -= nHeight;
- }
- }
まず、DisplayObject.enterFrameイベントでまとめて呼出す関数(コールバック)は、Functionベース型のVectorオブジェクト(listeners)に納める。そして、それぞれのインスタンスからVectorオブジェクトにコールバック関数を加える関数が要る(xAddListener())。つぎに、DisplayObject.enterFrameイベントのリスナー関数(xEnterFrame())を定め、Vectorオブジェクトのコールバック関数をforループでまとめて呼出す。
リスナー関数とかたちを揃えるため、コールバック関数にはイベントオブジェクトを引数に渡した(第20行目)。そのため、MovieClipシンボルの上記フレームアクションは、つぎのようにイベントリスナー登録のステートメント1行をメインタイムラインの関数呼出しに書替えれば済む。なお、以下のスクリプト005には、コールバック関数を削除するための関数(xRemoveListener())も定めた。
スクリプト005■DisplayObject.enterFrameイベントのリスナーからコールバック関数をまとめて呼出す
- // addEventListener(Event.ENTER_FRAME, xFall);
(root as MovieClip).xAddListener(xFall);
|
イベントリスナーの仕組みでリスナーにイベントが送られるというのは、内部的にはEventクラスまたはそのサブクラスの(イベント)オブジェクトがEventDispatcher.dispatchEvent()メソッドにより渡されることを意味する。そして、DisplayObject.enterFrameイベントはひとつひとつのインスタンスに直接送られる。そのため、インスタンスごとにEventオブジェクトがつくられるという処理が生じてしまう。コールバック関数を呼出すかたちにすれば、その手間が省ける。
インスタンスに起こったマウスイベントは表示リストの親インスタンスにも送られまる。送られたイベントは表示リストの階層を順に遡り、頂点のStageオブジェクトまで届く。このようにイベントが上っていくことを「バブリング」という。
マウスイベントのバブリングを活かした例として、インスタンスのクリックつまりインスタンス上でマウスボタンを押してなおかつ放した場合と、インスタンスの外で放した場合とを切り分ける。これは、インスタンスとStageオブジェクトとで、いわばテニスのダブルスのようにInteractiveObject.mouseUpイベントを待受けることにより、条件判定は使わずに実現できる(図003)。
まず、[1]インスタンス上でマウスボタンを押したことは、インスタンスのInteractiveObject.mouseDownイベントにリスナー関数を加えて確かめる。つぎに、[2]インスタンス上でマウスボタンを放すと、インスタンスに登録したInteractiveObject.mouseUpイベントのリスナー関数が呼出される。[3]インスタンスの外でマウスポインタを放した場合は、StageオブジェクトにInteractiveObject.mouseUpイベントのリスナーを登録しておけば、イベントが受取れる。
図003■InteractiveObject.mouseUpイベントをインスタンスとStageオブジェクトで分担処理
ここでの鍵は、[2]のインスタンス上でマウスボタンを放したときだ。マウスイベントはバブリングするので、放っておけば[3]のStageオブジェクトのリスナーにイベントが渡ってしまう。そこで、[2]のリスナー関数から、[3]のStageオブジェクトのイベントリスナーを削除する。これで、マウスボタンを放したのがインスタンス上か外かを切り分けられる。
スクリプト006■Event.stopPropagation()メソッドでイベントのバブリングを止める
|
もっとも、Stageオブジェクトのリスナー関数を削除しただけでは、マウスイベントそのものはStageオブジェクトまでバブリングする。そこで、前掲スクリプト006は、Event.stopPropagation()メソッドを呼出すことにより、イベントが表示リストの親にバブリングするのを止めた(第12行目)。
イベントがインスタンスに起こったときを「ターゲット段階」という。そして、マウスイベントのように表示リストの階層を遡るのは「バブリング段階」だ。しかし、それらに先立って、Stageオブジェクトからイベントの生じたインスタンスに向けて表示リストを下る「キャプチャ段階」がある(図004)。
図004■イベントの流れの3つの段階
ラジオボタンは、複数のボタンのうち必ずひとつだけが選ばれる。そこで、ボタンのインスタンスを、親インスタンスの入れ子にする(図005)。そして、マウスイベントをキャプチャ段階で捉えれば、子インスタンスへのターゲット段階を待たずに親のリスナーですべて済ませられる。
図005■ラジオボタンのインスタンスをMovieClipシンボルの入れ子にする
キャプチャ段階のイベントにリスナーを加えるには、EventDispatcher.addEventListener()メソッドの第3引数にtrueを渡す。以下のスクリプト007は、ラジオボタンのインスタンスを入れ子にした親インスタンスのシンボルに書くフレームアクションだ。キャプチャ段階でクリックされたインスタンスを調べて選択状態にするとともに、すでに選択されていたインスタンスをもとに戻す(ボタンのインスタンスには、それぞれmy0_mc〜my3_mcという連番の名前をつけた)。
スクリプト007■キャプチャ段階で子インスタンスのラジオボタンを扱う
|
これで、ボタンはつねにひとつだけ選ばれるようになる(図006)。マウスイベントをターゲット段階やバブリング段階に送るのは無駄なので、イベントリスナー(xChange())からEvent.stopPropagation()メソッドを呼出していることに注目してほしい(第7行目)。実際には、このスクリプト007のリスナー関数は、バブリング段階に加えても正しいラジオボタンの動きになる。けれど、キャプチャ段階でいち早くイベントを受取ることにより、そこから先の要らぬ流れを止めているのだ。
図006■ラジオボタンはつねにひとつだけが選ばれる
オブジェクトは基本的に新しくつくるより、プロパティなどの設定をし直す方が速く済む。
たとえば、Rectangleオブジェクトは、描画に関わるメソッドの引数にもよく使われる。Rectangleクラスのコンストラクタメソッドは、4つの数値(Number型)の引数で矩形領域の位置と大きさを定める。つぎのスクリプト008は、ループ処理の中でインスタンスを毎回つくり直す。
スクリプト008■ループ処理内でRectangleインスタンスを毎回つくり直す
|
それに対してつぎのスクリプト009は、ループに入る前に使い回すためのRectangleインスタンスを予めひとつだけつくっている(第1行目)。そして、forループの中ではオブジェクトの各プロパティ値を設定し直す。ステートメント数は増えるものの、オブジェクトを毎回新たにつくる負荷は減る。
スクリプト009■Rectangleインスタンスのプロパティ値をループ処理内で設定して使い回す
|
wonderflでRectangleオブジェクトを毎回つくる場合(create)と、使い回す場合(recycle)との処理時間(ミリ秒)を比べてみた。
Recycling vs Creating objects - wonderfl build flash online
本イベントの基礎編あるいはF-siteセミナー(「ActionScriptでの最速を求める」03-03「パターンのかぎられる変形ならビットマップのキャッシュをつくってしまう」)では、パターンのかぎられるインスタンスの変形アニメーションを、BitmapData.draw()メソッドでつくったビットマップのキャッシュに置換えて最適化した(スクリプト010)。インスタンスのイメージに対する変形は、BitmapData.draw()メソッドに渡す第2引数のMatrixオブジェクトで定める(第13および第16行目)。
スクリプト010■大量のBitmapインスタンスを使った回転と平行移動のアニメーション
|
MatrixクラスにはMatrix.identity()という初期化のメソッドがある。引数なしのコンストラクタメソッド(new Matrix())がつくるのと同じ、デフォルト(単位行列)のインスタンスに変わる。
つぎのスクリプト10では、forループの始まる第11行目のうえに、Matrixオブジェクトをつくるステートメントが移動された。そして、繰返す処理の中の第13行目でMatrix.identity()メソッドにより、使い回すオブジェクトを初期化している。なお、度数をラジアンに換算する定数(DEGREES_TO_RADIANS)も設けて、第14行目で用いた。
スクリプト011■引数に渡すMatrixオブジェクトをMatrix.identity()メソッドで使い回す
|
「Matrix3Dオブジェクトの行列データをコピーするメソッド」「Vector3Dオブジェクトの座標値をコピーや設定するメソッド」
新たなArrayインスタンスは、コンストラクタメソッドを呼出す(new Array())より、配列アクセス演算子[]でつくるのがお勧めだ(前出「ActionScriptでの最速を求める」01-04「Flash Playerの仕事を考える」)。けれど、配列も使い回せば、さらに速くなる。その場合、Array.lengthプロパティに0を設定する(表003)。
表003■配列の初期化初期化のし方 | 推奨 | 使い途 |
var my_array:Array = new Array() | △ | 引数で長さを決めて新たな配列をつくるとき |
var my_array:Array = [] | ○ | 新たな空の配列をつくるとき |
my_array.length = 0 | ◎ | 配列を空にして使い回すとき |
F-site「配列を初期化するには」の注[*4]に、配列の3つの初期化を比べるwonderflのサンプルが掲げてある。
家の中を見渡せば、リモコンがいくつもある。そのほとんどに「電源」と書かれたボタンが見つかるはず(図006)。そのボタンを押せば電源が入る。ただし、どの電化製品が動き出すかは手に取ったリモコン次第だ。
図006■リモコンの電源ボタン
タイムラインにMovieClipインスタンスを3つ置いて、それぞれに異なったアニメーションをさせてみる。アニメーションは単純に、ふたつのインスタンスをそれぞれ3次元空間で水平と垂直に回し、残りは2次元平面で伸び縮みさせる(図007)。また、ステージをクリックしたら、インスタンスすべてをもとの状態に戻す。
図007■3つのインスタンスにそれぞれ異なったアニメーションをさせる
タイムラインに置いた3つのMovieClipインスタンスには、それぞれmy0_mc〜my2_mcという連番の名前をつけておく(図008)。
図008■タイムラインに置いた3つのMovieClipにインスタンス名をつける
まずは、インスタンスを条件で振分けて、それぞれに異なるアニメーションを定めた(スクリプト012)。3つのMovieClipインスタンスは、まとめて扱いやすいように配列に入れて変数(mcs_array)に納める(第2行目)。イベントDisplayObject.enterFrameとInteractiveObject.clickのリスナー関数(xMove()とxStop())は、ともにforステートメントで配列からインスタンスをすべて取出し(第11行目〜および第32行目〜)、switchステートメントによりインスタンスを仕分けて処理した(第13行目〜および第34行目〜)。
スクリプト012■switchステートメントでインスタンスごとに分けて処理する
|
アニメーションとか停止といった機能の扱いは、それぞれの関数にまとめられている。けれども、ひとつのインスタンスに対する処理が、それらの関数に分かれてしまっていて、ひと目で捉えることができない。インスタンスや機能を加えたり、インスタンスの動きを修正することが面倒になりそうだ。
インスタンスの動きは、それぞれのシンボルのフレームアクションとして書くと、処理がまとまって見やすい。すると、メインタイムラインからは、forループで取出したインスタンスに対して、シンボルに定められた関数をそれぞれ呼出さなければならない。そこでポリモーフィズムの考え方を使う。
各シンボルに定める同じ機能の関数には、すべて同じ名前をつけてしまう。アニメーションさせるにはxMove()、その停止はxStop()と、メインタイムラインのリスナー関数と同じ関数名にした(スクリプト014〜016)。こうするとリスナー関数からは、すべてのインスタンスに対して、同じ名前の関数を呼出せば済む(スクリプト013第12および21行目)。
スクリプト013■インスタンスすべてに対して同じ名前の関数を呼出す
|
|
|
|
スクリプト013のふたつのリスナー関数xMove()とxStop()はともに、配列に納められたインスタンスすべてをforループで取出し、それぞれに対してリスナー関数と同名の関数を呼出している(第10〜13、および第19〜22行目)。前掲スクリプト012とは打って変わり、条件判定がなくなって、すっきりとした。
ポリモーフィズムは、インターフェイスを用いたクラスで定義するとそのよさがわかる。手始めに、[ライブラリ]に納められているmy0_mc〜my2_mcのシンボル(Clip0〜Clip2)には、クラスとしてClip0〜Clip2を設定しておく(図009)。それぞれのクラスは、スクリプト018〜020として後に定める。なお、MovieClipシンボルに対するクラスの定義については、gihyo.jp連載「ActionScript 3.0で始めるオブジェクト指向スクリプティング」第22回「MovieClipシンボルにクラスを定義する」をお読みいただきたい。
図009■[ライブラリ]のMovieClipシンボルにクラスを設定する
インターフェイスは、実装するクラスが備えなければならないメソッドを宣言する。MovieClipシンボルに設定するクラス(Clip0〜Clip2)に実装させるインターフェイス(IClip)は、つぎのように定義する(スクリプト017)。
スクリプト017■インターフェイスで備えるべきメソッドを宣言する
|
MovieClipシンボルに設定する3つのクラス(Clip0〜Clip2)は、いずれもSpriteクラスを継承し、インターフェイスIClipを実装する(スクリプト018〜020)。すると、3つのクラスにはインターフェイスで宣言されたふたつのメソッド(xMove()とxStop())が必ず備わっていなければならず(実装しないとエラーになる)、それらのメソッドを安心して呼出せる。
スクリプト018■インスタンスを3次元空間で水平に回すクラス
|
|
|
前掲スクリプト013のフレームアクションは、以下のスクリプト021のように書替える。第1に、3つのインスタンスは配列でなく、Vectorオブジェクト(instances)に納めた(第1行目)。第2に、ふたつのリスナー関数(xMove()とxStop())のforループの処理で、Vectorオブジェクトから取出したインスタンス対して、それぞれのクラスに実装されたメソッド(xMove()およびxStop())を呼出している(第11〜12行目ならびに第20〜21行目)。
スクリプト021■インスタンスすべてに対してインターフェイスが実装するメソッドを呼出す
|
このスクリプト021でもうひとつ注目したいのは、3つのインスタンスをインターフェイス(IClip)で型指定していることだ(第1および第20行目)。
もし、MovieClipシンボルに設定した3つのクラスがともに継承するSpriteで型指定すると、それぞれのクラスに実装したふたつのメソッド(xMove()とxStop())が呼出せず、[コンパイルエラー]になる(図010)。Spriteクラスにはそのようなメソッドが(リファレンスに)定義されていないからだ。
図010■インスタンスをSprite型で取出すとクラスのメソッドが呼出せない
かといって、MovieClipシンボルに設定した3つのクラス(Clip0〜Clip2)のいずれか、という型指定はできない。そこで、インターフェイス(IClip)で型指定すれば、実装されたメソッドを呼出すことができ(第20行目)、ベース型をインターフェイスで定めたVectorオブジェクトにも納められる(第1行目)。
メモリの解放は、Flash Playerが後述05-02の「ガベージコレクション」という仕組みで自動的に行う。オブジェクトを消すには、すべての参照をなくさなければならない。変数やプロパティだけでなく、イベントリスナーの登録もリスナー関数やメソッドをもつオブジェクトへの参照になる。それらすべてを破棄する必要がある。
さらに、オブジェクトへの参照をすべてなくしても、ただちにメモリからは消されない。とくにMovieClipインスタンスは、タイムラインが再生されていると、表示リストに入っていなくてもFlash Playerには負荷がかかる。したがって、MovieClip.stop()メソッドで再生ヘッドは止めておくべき。
ガベージコレクションは、どこからも参照されなくなったオブジェクトを自動的にメモリから消し去る技術だ。ただし、ガベージコレクションは重い処理なので、オブジェクトへの参照をすべて消したからといって、直ちにはメモリが解放されない。
たとえば、オブジェクトが繰返し処理を行っていれば、参照をすべて破棄した後もしばらくはメモリに残ったままその動作が続く。ガベージコレクションをスクリプトで明示的に実行する機能は用意されていない。そのため、参照を消すだけでなく、そのオブジェクトが行っている処理は止めておくのがよい。
ガベージコレクションがいつ働くかについては、[ヘルプ]がつぎのように説明する([Mobile]/[Optimizing Performance for the Flash Platform]/[Conserving memory]/[Freeing memory]より筆者訳)。
注意しなければならないのは、オブジェクトがnullに設定されても、必ずしもメモリからは削除されないということです。ガベージコレクタは、使えるメモリがまだ少なくないとみなすと、働かないことがあります。ガベージコレクションの実行は、予め予測できません。オブジェクトが消されたときではなく、メモリが割当てられるときにガベージコレクションは発動します。
ActionScript 3.0には、かぎられた目的でいくつかメモリを解放するメソッドが備わっている。
作成者: 野中文雄
作成日: 2012年2月27日