サイトトップ

Director Flash 書籍 業務内容 プロフィール
 

Adobe Flash ActionScript講座
ActionScript 3.0による
三次元表現ガイドブック
ActionScript 3.0
プロフェッショナルガイド
■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 Performance Tuning
ActionScript 3.0
パフォーマンス
チューニング

Now on Sale!!
    【お品書き】
  • 01 Math演算を極める
  • 02 イベントを制する
  • 03 オブジェクトを使い回す
  • 04 多態性(ポリモーフィズム)を知る
  • 05 メモリを解き放つ
  • サンプルファイル (Flash CS5形式/約762KB)

01 Math演算を極める

01-01 正数値の小数切捨てはint()関数を使う

    【小数点以下の切捨て】
  1. 正の数にはint()関数を使う
  2. 負の数ではMath.floor()メソッドとint()関数の戻り値が異なる

Math.floor()メソッドは、引数値を超えないもっとも大きい整数を返す。以下のスクリプト001に定義した関数xGetRandomInt()は、最小値と最大値のふたつの整数の引数から、ランダムな整数を返す(ランダムな整数の計算については、少し古いが「Math.random() でランダムな整数を取得する方法」を参照)。たとえば、サイコロの目と同じ1から6までのランダムな整数を得るには、xGetRandomInt(1, 6)のように呼出す。

スクリプト001■Math.floor()メソッドによりランダムな整数を返す関数
  1. function xGetRandomInt(nMin:int, nMax:int):int {
  2.   var nRandom:int = Math.floor(Math.random() * (nMax - nMin + 1)) + nMin;
  3.   return nRandom;
  4. }

int()関数は、引数値の小数点以下を除き、整数部のみを返す。正の数については、Math.floor()メソッドと戻り値は同じになる。そこで、演算の速いint()関数で書替えたのがつぎのスクリプト002だ。なお、ふたつの引数の大小が逆になったとき、変数値の順序を正すよう条件判定の処理も加えた(第2〜6行目)。

スクリプト002■int()関数によりランダムな整数を返す関数
  1. function xGetRandomInt(nMin:int, nMax:int):int {
  2.   if (nMin > nMax) {
  3.     var nTemp:Number = nMin;
  4.     nMin = nMax;
  5.     nMax = nTemp;
  6.   }
  7.   var nRandom:int = int(Math.random() * (nMax - nMin + 1)) + nMin;
  8.   return nRandom;
  9. }

Math.floor()メソッドとint()関数は負の数の扱いが異なる。-2.5から2.5まで1ずつ引数値を増やしとき、ふたつの計算結果は下表001のようになる。

for (var i:Number = -2.5; i < 3; i++) {
  trace(i, Math.floor(i), int(i));
}

表001■正負にまたがる引数に対するMath.floor()メソッドとint()関数の戻り値
引数 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

01-02 Math.max()/Math.min()/Math.abs()メソッドより条件演算子:?で計算した方が速い

ふたつの数値から最大値または最小値を求めたり、絶対値を得る場合には、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は、条件演算子?:の式が、関数として定められていないことだ。式をステートメントとして直接書く(これを「インライン」という)方が、呼出しのない分関数より速くなる(「その他の最適化」参照)。


01-03 距離はPointやVector3DクラスのメソッドよりMath.sqrt()で三平方の定理を使う

    【座標間の距離を求める】
  1. 座標間の距離はMath.sqrt()メソッドを用いて三平方の定理で計算する
  2. 数値の2乗はMath.pow()メソッドより同じ変数を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回乗じている。

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

スクリプト003■2次元平面の2点間の距離を引数のふたつのPointオブジェクトから求めて返す
  1. function distance2D(beginPoint:Point, endPoint:Point):Number {
  2.   var nX:Number = endPoint.x - beginPoint.x;
  3.   var nY:Number = endPoint.y - beginPoint.y;
  4.   var distance:Number = Math.sqrt(nX * nX + nY * nY);
  5.   return distance;
  6. }

3次元空間でも、z座標が加わるだけで、同じように三平方の定理で距離が導ける(図002)。

図002■3次元空間の2点間の距離を三平方の定理で求める

ふたつのVector3Dオブジェクトの座標間の距離を求める関数は、つぎのスクリプト004のように定められる。

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

スクリプト004■3次元空間の2点間の距離を引数のふたつのVector3Dオブジェクトから求めて返す
  1. function distance3D(beginVector3D:Vector3D, endVector3D:Vector3D):Number {
  2.   var nX:Number = endVector3D.x - beginVector3D.x;
  3.   var nY:Number = endVector3D.y - beginVector3D.y;
  4.   var nZ:Number = endVector3D.z - beginVector3D.z;
  5.   var distance:Number = Math.sqrt(nX * nX + nY * nY + nZ * nZ);
  6.   return distance;
  7. }

02 イベントを制する

02-01 Event.ENTER_FRAMEイベントのリスナーはひとつにまとめる

DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)は、おもにアニメーションで用いられる。イベントリスナーで扱うインスタンスの数がきわめて多い場合には、インスタンスごとにリスナーを加えるのではなく、ひとつのリスナーでまとめてインスタンスを処理する方が速い。

    // フレームアクション: アニメーションさせるMovieClipシンボル
  1. var nWidth:Number = stage.stageWidth;
  2. var nHeight:Number = stage.stageHeight;
  3. x = nWidth * Math.random();
  4. y = nHeight * Math.random();
  5. addEventListener(Event.ENTER_FRAME, xFall);
  6. function xFall(eventObject:Event):void {
  7.   y += 5;
  8.   if (y > nHeight) {
  9.     y -= nHeight;
  10.   }
  11. }

まず、DisplayObject.enterFrameイベントでまとめて呼出す関数(コールバック)は、Functionベース型のVectorオブジェクト(listeners)に納める。そして、それぞれのインスタンスからVectorオブジェクトにコールバック関数を加える関数が要る(xAddListener())。つぎに、DisplayObject.enterFrameイベントのリスナー関数(xEnterFrame())を定め、Vectorオブジェクトのコールバック関数をforループでまとめて呼出す。

リスナー関数とかたちを揃えるため、コールバック関数にはイベントオブジェクトを引数に渡した(第20行目)。そのため、MovieClipシンボルの上記フレームアクションは、つぎのようにイベントリスナー登録のステートメント1行をメインタイムラインの関数呼出しに書替えれば済む。なお、以下のスクリプト005には、コールバック関数を削除するための関数(xRemoveListener())も定めた。

  1. // addEventListener(Event.ENTER_FRAME, xFall);
    (root as MovieClip).xAddListener(xFall);
スクリプト005■DisplayObject.enterFrameイベントのリスナーからコールバック関数をまとめて呼出す
    // フレームアクション: メインタイムライン
  1. var listeners:Vector.<Function> = new Vector.<Function>();
  2. addEventListener(Event.ENTER_FRAME, xEnterFrame);
  3. function xAddListener(listener:Function):void {
  4.   listeners.push(listener);
  5. }
  6. function xRemoveListener(listener:Function):void {
  7.   var nLength:uint = listeners.length;
  8.   for (var i:uint = 0; i < nLength; i++) {
  9.     var myFunction:Function = listeners[i];
  10.     if (myFunction == listener) {
  11.       listeners.splice(i, 1);
  12.       break;
  13.     }
  14.   }
  15. }
  16. function xEnterFrame(eventObject:Event):void {
  17.   var nLength:uint = listeners.length;
  18.   for (var i:uint = 0; i < nLength; i++) {
  19.     var myFunction:Function = listeners[i];
  20.     myFunction(eventObject);
  21.   }
  22. }

イベントリスナーの仕組みでリスナーにイベントが送られるというのは、内部的にはEventクラスまたはそのサブクラスの(イベント)オブジェクトがEventDispatcher.dispatchEvent()メソッドにより渡されることを意味する。そして、DisplayObject.enterFrameイベントはひとつひとつのインスタンスに直接送られる。そのため、インスタンスごとにEventオブジェクトがつくられるという処理が生じてしまう。コールバック関数を呼出すかたちにすれば、その手間が省ける。


02-02 マウスイベントのバブリング

インスタンスに起こったマウスイベントは表示リストの親インスタンスにも送られまる。送られたイベントは表示リストの階層を順に遡り、頂点の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()メソッドでイベントのバブリングを止める
    // フレームアクション: マウスイベントを捉えるMovieClipインスタンスのシンボル内
  1. addEventListener(MouseEvent.MOUSE_DOWN, xPress);
  2. function xPress(eventObject:MouseEvent):void {
  3.   scaleX = scaleY = 0.8;
  4.   addEventListener(MouseEvent.MOUSE_UP, xRelease);
  5.   stage.addEventListener(MouseEvent.MOUSE_UP, xReleaseOutside);
  6. }
  7. function xRelease(eventObject:MouseEvent):void {
  8.   scaleX = scaleY = 1;
  9.   rotation += 90;
  10.   removeEventListener(MouseEvent.MOUSE_UP, xRelease);
  11.   stage.removeEventListener(MouseEvent.MOUSE_UP, xReleaseOutside);
  12.   eventObject.stopPropagation();
  13. }
  14. function xReleaseOutside(eventObject:MouseEvent):void {
  15.   scaleX = scaleY = 1;
  16.   removeEventListener(MouseEvent.MOUSE_UP, xRelease);
  17.   stage.removeEventListener(MouseEvent.MOUSE_UP, xReleaseOutside);
  18. }

もっとも、Stageオブジェクトのリスナー関数を削除しただけでは、マウスイベントそのものはStageオブジェクトまでバブリングする。そこで、前掲スクリプト006は、Event.stopPropagation()メソッドを呼出すことにより、イベントが表示リストの親にバブリングするのを止めた(第12行目)。


02-03 マウスイベントのキャプチャ

イベントがインスタンスに起こったときを「ターゲット段階」という。そして、マウスイベントのように表示リストの階層を遡るのは「バブリング段階」だ。しかし、それらに先立って、Stageオブジェクトからイベントの生じたインスタンスに向けて表示リストを下る「キャプチャ段階」がある(図004)。

図004■イベントの流れの3つの段階

ラジオボタンは、複数のボタンのうち必ずひとつだけが選ばれる。そこで、ボタンのインスタンスを、親インスタンスの入れ子にする(図005)。そして、マウスイベントをキャプチャ段階で捉えれば、子インスタンスへのターゲット段階を待たずに親のリスナーですべて済ませられる。

図005■ラジオボタンのインスタンスをMovieClipシンボルの入れ子にする

キャプチャ段階のイベントにリスナーを加えるには、EventDispatcher.addEventListener()メソッドの第3引数にtrueを渡す。以下のスクリプト007は、ラジオボタンのインスタンスを入れ子にした親インスタンスのシンボルに書くフレームアクションだ。キャプチャ段階でクリックされたインスタンスを調べて選択状態にするとともに、すでに選択されていたインスタンスをもとに戻す(ボタンのインスタンスには、それぞれmy0_mc〜my3_mcという連番の名前をつけた)。

スクリプト007■キャプチャ段階で子インスタンスのラジオボタンを扱う
    // フレームアクション: ラジオボタンをまとめて納めた親MovieClipインスタンスのシンボル内
  1. var selected_mc:MovieClip = my0_mc;
  2. addEventListener(MouseEvent.CLICK, xChange, true);
  3. xSelectOne(selected_mc);
  4. function xChange(eventObject:MouseEvent):void {
  5.   var _mc:MovieClip = eventObject.target as MovieClip;
  6.   xSelectOne(_mc);
  7.   eventObject.stopPropagation();
  8. }
  9. function xSelectOne(_mc:MovieClip):void {
  10.   selected_mc.alpha = 1;
  11.   _mc.alpha = 0.5;
  12.   selected_mc = _mc;
  13. }

これで、ボタンはつねにひとつだけ選ばれるようになる(図006)。マウスイベントをターゲット段階やバブリング段階に送るのは無駄なので、イベントリスナー(xChange())からEvent.stopPropagation()メソッドを呼出していることに注目してほしい(第7行目)。実際には、このスクリプト007のリスナー関数は、バブリング段階に加えても正しいラジオボタンの動きになる。けれど、キャプチャ段階でいち早くイベントを受取ることにより、そこから先の要らぬ流れを止めているのだ。

図006■ラジオボタンはつねにひとつだけが選ばれる

03 オブジェクトを使い回す

03-01 オブジェクトは設定し直して使い回す

オブジェクトは基本的に新しくつくるより、プロパティなどの設定をし直す方が速く済む。

たとえば、Rectangleオブジェクトは、描画に関わるメソッドの引数にもよく使われる。Rectangleクラスのコンストラクタメソッドは、4つの数値(Number型)の引数で矩形領域の位置と大きさを定める。つぎのスクリプト008は、ループ処理の中でインスタンスを毎回つくり直す。

スクリプト008■ループ処理内でRectangleインスタンスを毎回つくり直す
    // 抜粋
  1. for (var i:int = 0; i < nCount; i++) {
  2.   var myRectangle:Rectangle = new Rectangle(nX, nY, nWidth, nHeight);
  3. }

それに対してつぎのスクリプト009は、ループに入る前に使い回すためのRectangleインスタンスを予めひとつだけつくっている(第1行目)。そして、forループの中ではオブジェクトの各プロパティ値を設定し直す。ステートメント数は増えるものの、オブジェクトを毎回新たにつくる負荷は減る。

スクリプト009■Rectangleインスタンスのプロパティ値をループ処理内で設定して使い回す
    // 抜粋
  1. var myRectangle:Rectangle = new Rectangle();
  2. for (var i:int = 0; i < nCount; i++) {
  3.   myRectangle.x = nX;
  4.   myRectangle.y = nY;
  5.   myRectangle.width = nWidth;
  6.   myRectangle.height = nHeight;
  7. }

wonderflでRectangleオブジェクトを毎回つくる場合(create)と、使い回す場合(recycle)との処理時間(ミリ秒)を比べてみた。

Recycling vs Creating objects - wonderfl build flash online


03-02 インスタンスを初期化するメソッドが備わったクラスもある

本イベントの基礎編あるいはF-siteセミナー(「ActionScriptでの最速を求める」03-03「パターンのかぎられる変形ならビットマップのキャッシュをつくってしまう」)では、パターンのかぎられるインスタンスの変形アニメーションを、BitmapData.draw()メソッドでつくったビットマップのキャッシュに置換えて最適化した(スクリプト010)。インスタンスのイメージに対する変形は、BitmapData.draw()メソッドに渡す第2引数のMatrixオブジェクトで定める(第13および第16行目)。

スクリプト010■大量のBitmapインスタンスを使った回転と平行移動のアニメーション
    // フレームアクション: メインタイムライン
  1. var _mc:MovieClip = new MyClass();
  2. var nWidth:Number = _mc.width;
  3. var nHeight:Number = _mc.height;
  4. var nStageHeight:int = stage.stageHeight;
  5. var nDiagonal:Number = Math.sqrt(nWidth * nWidth + nHeight * nHeight);
  6. var nMaxDegrees:uint = 360;
  7. var nRotation:uint = 0;
  8. var nCount:uint = 500;
  9. var instances:Vector.<Bitmap> = new Vector.<Bitmap>(nCount);
  10. var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>(nMaxDegrees);
  11. for (var i:uint = 0; i < nMaxDegrees; i++) {
  12.   var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  13.   var myMatrix:Matrix = new Matrix();
  14.   myMatrix.rotate(i / 180 * Math.PI);
  15.   myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  16.   myBitmapData.draw(_mc, myMatrix);
  17.   rotationData[i] = myBitmapData;
  18. }
  19. for (var j:uint = 0; j < nCount; j++) {
  20.   var myBitmap:Bitmap = new Bitmap();
  21.   instances[j] = myBitmap;
  22.   addChild(myBitmap);
  23.   myBitmap.bitmapData = rotationData[nRotation];
  24.   myBitmap.x = Math.random() * stage.stageWidth - nDiagonal / 2;
  25.   myBitmap.y = Math.random() * stage.stageHeight - nDiagonal / 2;
  26. }
  27. addEventListener(Event.ENTER_FRAME, xMove);
  28. function xMove(eventObject:Event):void {
  29.   nRotation += 5;
  30.   nRotation %= nMaxDegrees;
  31.   for (var i:uint = 0; i < nCount; i++) {
  32.     var instance:Bitmap = instances[i];
  33.     instance.bitmapData = rotationData[nRotation];
  34.     instance.y += 5;
  35.     if (instance.y > nStageHeight) {
  36.       instance.y -= nStageHeight;
  37.     }
  38.   }
  39. }

MatrixクラスにはMatrix.identity()という初期化のメソッドがある。引数なしのコンストラクタメソッド(new Matrix())がつくるのと同じ、デフォルト(単位行列)のインスタンスに変わる。

つぎのスクリプト10では、forループの始まる第11行目のうえに、Matrixオブジェクトをつくるステートメントが移動された。そして、繰返す処理の中の第13行目でMatrix.identity()メソッドにより、使い回すオブジェクトを初期化している。なお、度数をラジアンに換算する定数(DEGREES_TO_RADIANS)も設けて、第14行目で用いた。

スクリプト011■引数に渡すMatrixオブジェクトをMatrix.identity()メソッドで使い回す
    // 抜粋
  1. var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>(nMaxDegrees);
    const DEGREES_TO_RADIANS:Number = Math.PI / 180;   // 追加
    var myMatrix:Matrix = new Matrix();   // forループ内から移動
  2. for (var i:uint = 0; i < nMaxDegrees; i++) {
  3.   var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  4.   // var myMatrix:Matrix = new Matrix();   // forループの外に移動
      myMatrix.identity();   // 追加
  5.   // myMatrix.rotate(i / 180 * Math.PI);
      myMatrix.rotate(i * DEGREES_TO_RADIANS);   // 修正
  6.   myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  7.   myBitmapData.draw(_mc, myMatrix);
  8.   rotationData[i] = myBitmapData;
  9. }

Matrix3Dオブジェクトの行列データをコピーするメソッド」「Vector3Dオブジェクトの座標値をコピーや設定するメソッド


03-03 配列やVectorオブジェクトは長さを0にして使い回す

新たな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のサンプルが掲げてある。


04 多態性(ポリモーフィズム)を知る

家の中を見渡せば、リモコンがいくつもある。そのほとんどに「電源」と書かれたボタンが見つかるはず(図006)。そのボタンを押せば電源が入る。ただし、どの電化製品が動き出すかは手に取ったリモコン次第だ。

図006■リモコンの電源ボタン

    【ポリモーフィズムの特徴】
  1. 見た目(インターフェイス)は同じ
  2. 中身(処理)は別人

04-01 インスタンスごとに条件分けして異なった動きを与える

タイムラインにMovieClipインスタンスを3つ置いて、それぞれに異なったアニメーションをさせてみる。アニメーションは単純に、ふたつのインスタンスをそれぞれ3次元空間で水平と垂直に回し、残りは2次元平面で伸び縮みさせる(図007)。また、ステージをクリックしたら、インスタンスすべてをもとの状態に戻す。

図007■3つのインスタンスにそれぞれ異なったアニメーションをさせる

タイムラインに置いた3つのMovieClipインスタンスには、それぞれmy0_mc〜my2_mcという連番の名前をつけておく(図008)。

図008■タイムラインに置いた3つのMovieClipにインスタンス名をつける

まずは、インスタンスを条件で振分けて、それぞれに異なるアニメーションを定めた(スクリプト012)。3つのMovieClipインスタンスは、まとめて扱いやすいように配列に入れて変数(mcs_array)に納める(第2行目)。イベントDisplayObject.enterFrameInteractiveObject.clickのリスナー関数(xMove()とxStop())は、ともにforステートメントで配列からインスタンスをすべて取出し(第11行目〜および第32行目〜)、switchステートメントによりインスタンスを仕分けて処理した(第13行目〜および第34行目〜)。

スクリプト012■switchステートメントでインスタンスごとに分けて処理する
    // フレームアクション: メインタイムライン
  1. const DEGREES_TO_RADIANS:Number = Math.PI / 180;
  2. var mcs_array:Array = [my0_mc, my1_mc, my2_mc];
  3. var nAngle:Number = 0;
  4. var nIncrement:Number = 5;
  5. addEventListener(Event.ENTER_FRAME, xMove);
  6. stage.addEventListener(MouseEvent.CLICK, xStop);
  7. function xMove(eventObject:Event):void {
  8.   var nLength:uint = mcs_array.length;
  9.   nAngle += nIncrement;
  10.   nAngle %= 360;
  11.   for (var i:uint = 0; i < nLength; i++) {
  12.     var my_mc:MovieClip = mcs_array[i];
  13.     switch (my_mc) {
  14.       case my0_mc :
  15.         my_mc.rotationY = nAngle;
  16.         break;
  17.       case my1_mc :
  18.         my_mc.rotationX = nAngle;
  19.         break;
  20.       case my2_mc :
  21.         var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
  22.         my_mc.scaleX = nScale;
  23.         my_mc.scaleY = nScale;
  24.         break;
  25.     }
  26.   }
  27. }
  28. function xStop(eventObject:MouseEvent):void {
  29.   var nLength:uint = mcs_array.length;
  30.   removeEventListener(Event.ENTER_FRAME, xMove);
  31.   stage.removeEventListener(MouseEvent.CLICK, xStop);
  32.   for (var i:uint = 0; i < nLength; i++) {
  33.     var my_mc:MovieClip = mcs_array[i];
  34.     switch (my_mc) {
  35.       case my0_mc :
  36.         my_mc.rotationY = 0;
  37.         break;
  38.       case my1_mc :
  39.         my_mc.rotationX = 0;
  40.         break;
  41.       case my2_mc :
  42.         my_mc.scaleX = 1;
  43.         my_mc.scaleY = 1;
  44.         break;
  45.     }
  46.   }
  47. }

アニメーションとか停止といった機能の扱いは、それぞれの関数にまとめられている。けれども、ひとつのインスタンスに対する処理が、それらの関数に分かれてしまっていて、ひと目で捉えることができない。インスタンスや機能を加えたり、インスタンスの動きを修正することが面倒になりそうだ。


04-02 MovieClipシンボルに同じ名前の関数を定義して呼出す

インスタンスの動きは、それぞれのシンボルのフレームアクションとして書くと、処理がまとまって見やすい。すると、メインタイムラインからは、forループで取出したインスタンスに対して、シンボルに定められた関数をそれぞれ呼出さなければならない。そこでポリモーフィズムの考え方を使う。

各シンボルに定める同じ機能の関数には、すべて同じ名前をつけてしまう。アニメーションさせるにはxMove()、その停止はxStop()と、メインタイムラインのリスナー関数と同じ関数名にした(スクリプト014〜016)。こうするとリスナー関数からは、すべてのインスタンスに対して、同じ名前の関数を呼出せば済む(スクリプト013第12および21行目)。

スクリプト013■インスタンスすべてに対して同じ名前の関数を呼出す
    // フレームアクション: メインタイムライン
  1. var mcs_array:Array = [my0_mc, my1_mc, my2_mc];
  2. var nAngle:Number = 0;
  3. var nIncrement:Number = 5;
  4. addEventListener(Event.ENTER_FRAME, xMove);
  5. stage.addEventListener(MouseEvent.CLICK, xStop);
  6. function xMove(eventObject:Event):void {
  7.   var nLength:uint = mcs_array.length;
  8.   nAngle += nIncrement;
  9.   nAngle %= 360;
  10.   for (var i:uint = 0; i < nLength; i++) {
  11.     var my_mc:MovieClip = mcs_array[i];
  12.     my_mc.xMove(nAngle);
  13.   }
  14. }
  15. function xStop(eventObject:MouseEvent):void {
  16.   var nLength:uint = mcs_array.length;
  17.   removeEventListener(Event.ENTER_FRAME, xMove);
  18.   stage.removeEventListener(MouseEvent.CLICK, xStop);
  19.   for (var i:uint = 0; i < nLength; i++) {
  20.     var my_mc:MovieClip = mcs_array[i];
  21.     my_mc.xStop();
  22.   }
  23. }

スクリプト014■インスタンスを3次元空間で水平に回す
    // フレームアクション: インスタンスmy0_mcのシンボル
  1. function xMove(nAngle:Number):void {
  2.   rotationY = nAngle;
  3. }
  4. function xStop():void {
  5.   rotationY = 0;
  6. }

スクリプト015■インスタンスを3次元空間で垂直に回す
    // フレームアクション: インスタンスmy1_mcのシンボル
  1. function xMove(nAngle:Number):void {
  2.   rotationX = nAngle;
  3. }
  4. function xStop():void {
  5.   rotationX = 0;
  6. }

スクリプト016■インスタンスを2次元平面で伸び縮みさせる
    // フレームアクション: インスタンスmy2_mcのシンボル
  1. const DEGREES_TO_RADIANS:Number = Math.PI / 180;
  2. function xMove(nAngle:Number):void {
  3.   var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
  4.   scaleX = nScale;
  5.   scaleY = nScale;
  6. }
  7. function xStop():void {
  8.   scaleX = 1;
  9.   scaleY = 1;
  10. }

スクリプト013のふたつのリスナー関数xMove()とxStop()はともに、配列に納められたインスタンスすべてをforループで取出し、それぞれに対してリスナー関数と同名の関数を呼出している(第10〜13、および第19〜22行目)。前掲スクリプト012とは打って変わり、条件判定がなくなって、すっきりとした。


04-03 インターフェイスとクラスを定義する

ポリモーフィズムは、インターフェイスを用いたクラスで定義するとそのよさがわかる。手始めに、[ライブラリ]に納められているmy0_mc〜my2_mcのシンボル(Clip0〜Clip2)には、クラスとしてClip0〜Clip2を設定しておく(図009)。それぞれのクラスは、スクリプト018〜020として後に定める。なお、MovieClipシンボルに対するクラスの定義については、gihyo.jp連載「ActionScript 3.0で始めるオブジェクト指向スクリプティング」第22回「MovieClipシンボルにクラスを定義する」をお読みいただきたい。

図009■[ライブラリ]のMovieClipシンボルにクラスを設定する

    【インターフェイスの役割】
  1. 決まったメソッドが備わっていることを保証する
  2. 異なるクラスのインスタンスを同じデータ型で扱える

インターフェイスは、実装するクラスが備えなければならないメソッドを宣言する。MovieClipシンボルに設定するクラス(Clip0〜Clip2)に実装させるインターフェイス(IClip)は、つぎのように定義する(スクリプト017)。

スクリプト017■インターフェイスで備えるべきメソッドを宣言する
    // インターフェイス定義ファイル: IClip.as
  1. package {
  2.   public interface IClip {
  3.     function xMove(nAngle:Number):void
  4.     function xStop():void
  5.   }
  6. }

MovieClipシンボルに設定する3つのクラス(Clip0〜Clip2)は、いずれもSpriteクラスを継承し、インターフェイスIClipを実装する(スクリプト018〜020)。すると、3つのクラスにはインターフェイスで宣言されたふたつのメソッド(xMove()とxStop())が必ず備わっていなければならず(実装しないとエラーになる)、それらのメソッドを安心して呼出せる。

スクリプト018■インスタンスを3次元空間で水平に回すクラス
    // クラス定義ファイル: Clip0.as
  1. package {
  2.   import flash.display.Sprite;
  3.   public class Clip0 extends Sprite implements IClip {
  4.     public function Clip0() {}
  5.     public function xMove(nAngle:Number):void {
  6.       rotationY = nAngle;
  7.     }
  8.     public function xStop():void {
  9.       rotationY = 0;
  10.     }
  11.   }
  12. }

スクリプト019■インスタンスを3次元空間で垂直に回すクラス
    // クラス定義ファイル: Clip1.as
  1. package {
  2.   import flash.display.Sprite;
  3.   public class Clip1 extends Sprite implements IClip {
  4.     public function Clip1() {}
  5.     public function xMove(nAngle:Number):void {
  6.       rotationX = nAngle;
  7.     }
  8.     public function xStop():void {
  9.       rotationX = 0;
  10.     }
  11.   }
  12. }

スクリプト020■インスタンスを2次元平面で伸び縮みさせるクラス
    // クラス定義ファイル: Clip2.as
  1. package {
  2.   import flash.display.Sprite;
  3.   public class Clip2 extends Sprite implements IClip {
  4.     private const DEGREES_TO_RADIANS:Number = Math.PI / 180;
  5.     public function Clip2() {}
  6.     public function xMove(nAngle:Number):void {
  7.       var nScale:Number = Math.cos(nAngle * DEGREES_TO_RADIANS);
  8.       scaleX = nScale;
  9.       scaleY = nScale;
  10.     }
  11.     public function xStop():void {
  12.       scaleX = 1;
  13.       scaleY = 1;
  14.     }
  15.   }
  16. }

前掲スクリプト013のフレームアクションは、以下のスクリプト021のように書替える。第1に、3つのインスタンスは配列でなく、Vectorオブジェクト(instances)に納めた(第1行目)。第2に、ふたつのリスナー関数(xMove()とxStop())のforループの処理で、Vectorオブジェクトから取出したインスタンス対して、それぞれのクラスに実装されたメソッド(xMove()およびxStop())を呼出している(第11〜12行目ならびに第20〜21行目)。

スクリプト021■インスタンスすべてに対してインターフェイスが実装するメソッドを呼出す
    // フレームアクション: メインタイムライン
  1. var instances:Vector.<IClip> = new <IClip>[my0_mc, my1_mc, my2_mc];
  2. var nAngle:Number = 0;
  3. var nIncrement:Number = 5;
  4. addEventListener(Event.ENTER_FRAME, xMove);
  5. stage.addEventListener(MouseEvent.CLICK, xStop);
  6. function xMove(eventObject:Event):void {
  7.   var nLength:uint = instances.length;
  8.   nAngle += nIncrement;
  9.   nAngle %= 360;
  10.   for (var i:uint = 0; i < nLength; i++) {
  11.     var my_mc:IClip = instances[i];
  12.     my_mc.xMove(nAngle);
  13.   }
  14. }
  15. function xStop(eventObject:MouseEvent):void {
  16.   var nLength:uint = instances.length;
  17.   removeEventListener(Event.ENTER_FRAME, xMove);
  18.   stage.removeEventListener(MouseEvent.CLICK, xStop);
  19.   for (var i:uint = 0; i < nLength; i++) {
  20.     var my_mc:IClip = instances[i];
  21.     my_mc.xStop();
  22.   }
  23. }

このスクリプト021でもうひとつ注目したいのは、3つのインスタンスをインターフェイス(IClip)で型指定していることだ(第1および第20行目)。

もし、MovieClipシンボルに設定した3つのクラスがともに継承するSpriteで型指定すると、それぞれのクラスに実装したふたつのメソッド(xMove()とxStop())が呼出せず、[コンパイルエラー]になる(図010)。Spriteクラスにはそのようなメソッドが(リファレンスに)定義されていないからだ。

図010■インスタンスをSprite型で取出すとクラスのメソッドが呼出せない
図004上

図004下

かといって、MovieClipシンボルに設定した3つのクラス(Clip0〜Clip2)のいずれか、という型指定はできない。そこで、インターフェイス(IClip)で型指定すれば、実装されたメソッドを呼出すことができ(第20行目)、ベース型をインターフェイスで定めたVectorオブジェクトにも納められる(第1行目)。


05 メモリを解き放つ

05-01 インスタンスをメモリから消し去るには

    【インスタンスをメモリから消し去るには】
  1. DisplayObjectContainer.removeChild()メソッドで表示リストから除く
  2. DisplayObject.removeEventListener()メソッドでイベントリスナーを削除する
  3. MovieClip.stop()メソッドで再生ヘッドを止める
  4. インスタンスへの参照をすべて破棄する

メモリの解放は、Flash Playerが後述05-02の「ガベージコレクション」という仕組みで自動的に行う。オブジェクトを消すには、すべての参照をなくさなければならない。変数やプロパティだけでなく、イベントリスナーの登録もリスナー関数やメソッドをもつオブジェクトへの参照になる。それらすべてを破棄する必要がある。

さらに、オブジェクトへの参照をすべてなくしても、ただちにメモリからは消されない。とくにMovieClipインスタンスは、タイムラインが再生されていると、表示リストに入っていなくてもFlash Playerには負荷がかかる。したがって、MovieClip.stop()メソッドで再生ヘッドは止めておくべき。


05-02 ガベージコレクションの仕組みと働き

ガベージコレクションは、どこからも参照されなくなったオブジェクトを自動的にメモリから消し去る技術だ。ただし、ガベージコレクションは重い処理なので、オブジェクトへの参照をすべて消したからといって、直ちにはメモリが解放されない。

たとえば、オブジェクトが繰返し処理を行っていれば、参照をすべて破棄した後もしばらくはメモリに残ったままその動作が続く。ガベージコレクションをスクリプトで明示的に実行する機能は用意されていない。そのため、参照を消すだけでなく、そのオブジェクトが行っている処理は止めておくのがよい。

ガベージコレクションがいつ働くかについては、[ヘルプ]がつぎのように説明する([Mobile]/[Optimizing Performance for the Flash Platform]/[Conserving memory]/[Freeing memory]より筆者訳)。

注意しなければならないのは、オブジェクトがnullに設定されても、必ずしもメモリからは削除されないということです。ガベージコレクタは、使えるメモリがまだ少なくないとみなすと、働かないことがあります。ガベージコレクションの実行は、予め予測できません。オブジェクトが消されたときではなく、メモリが割当てられるときにガベージコレクションは発動します。

05-03 メモリを解放するメソッド

ActionScript 3.0には、かぎられた目的でいくつかメモリを解放するメソッドが備わっている。


作成者: 野中文雄
作成日: 2012年2月27日


Copyright © 2001-2012 Fumio Nonaka.  All rights reserved.