ActionScript 3.0で始めるオブジェクト指向スクリプティング

第58回特別編】ビットマップのキャッシュとオブジェクトの使い回し

技術評論社より発売された拙著ActionScript 3.0パフォーマンスチューニングから、ActionScript 3.0の最適化の仕方をご紹介する特別編の3回目は、ふたつのお題を採上げる。第1は、アニメーションを滑らかに再生するためのビットマップイメージの扱いだ。そして第2は、オブジェクトを使い回すこと、いわばエコなスクリプティングの勧めである。

アニメーションの重さを測るふたつの物差し

アニメーションが重いとか軽いとかいわれるとき、実はふたつの物差しがある。ひとつはデータの容量だ。そしてもうひとつは、描画するためのCPUの負荷である。

ベクターグラフィックスが軽いといわれるのは、前者の容量を指す。だが、描画についてはディスプレイがピクセルで表示するため、ベクターはビットマップに変換しなければならない(これをラスタライズという⁠⁠。その処理にCPUが使われる。つまり、ベクターグラフィックスの描画は重くなる図1左図⁠⁠。

他方、ビットマップグラフィックスは、画像のピクセル数が増えるほどデータ容量は大きくなる。けれど、ディスプレイには、ピクセルのまま映せば済む。したがって、CPUの負荷が少ないので、ビットマップグラフィックスの描画は軽い図1右図⁠⁠。

ベクターグラフィックス
  • 数学的なデータ→容量は小さい
  • ビットマップに変換(ラスタライズ)して描画→CPU負荷が高い
ビットマップグラフィックス
  • ピクセルごとのデータ→容量は大きい
  • ピクセルをスクリーンに映す→CPU負荷が低い
図1 ベクターグラフィックスとビットマップグラフィックス
図1 ベクターグラフィックスとビットマップグラフィックス 図1 ベクターグラフィックスとビットマップグラフィックス

とくにPCの環境では、ダウンロードするデータの容量より、画面を描き替えるアニメーションの滑らかさが求められる。そうした場合には、グラフィックスをビットマップにすると、データは膨らんでも描画が速められる。もっとも、これはFlashのアニメーションすべてにいえることで、スクリプトにかぎった話ではない。この知識を踏まえて、次項からActionScriptのお題に移る。

平行移動のアニメーションではDisplayObject.cacheAsBitmapプロパティをtrueにする

まずは、上から下に単純にスクロールするインスタンスを大量につくって、アニメーションがどうなるか見てみよう。⁠ライブラリ]のMovieClipシンボルにクラスを定めて図2⁠、インスタンスは動的につくることにする。

図2 ⁠ライブラリ]のMovieClipシンボルに[クラス]を定める
図2 [ライブラリ]のMovieClipシンボルに[クラス]を定める 図2 [ライブラリ]のMovieClipシンボルに[クラス]を定める

メインタイムラインに書くフレームアクションは、以下のスクリプト1のとおりだ。まず、forループで決められた数(nCount)のMovieClipインスタンス(_mc)をつくってタイムラインのランダムな位置に置き、Vectorオブジェクト(instances)に納める。

つぎに、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)にリスナー関数(xMove())を加えた。そして、リスナー関数は、VectorオブジェクトからforループでMovieClipインスタンスを順に取出し、下にスクロールさせる。なお、ステージ下端(nStageHeight)を超えたインスタンスは、上端に戻している。

スクリプト1 数多くのインスタンスを上から下にスクロールさせる
// フレームアクション: メインタイムライン
var nStageHeight:int = stage.stageHeight;
var nCount:uint = 500;
var instances:Vector.<MovieClip> = new Vector.<MovieClip>(nCount);
for (var j:uint = 0; j < nCount; j++) {
  var _mc:MovieClip = new MyClass();
  instances[j] = _mc;
  addChild(_mc);
  _mc.x = Math.random() * stage.stageWidth;
  _mc.y = Math.random() * stage.stageHeight;
}
addEventListener(Event.ENTER_FRAME, xMove);
function xMove(eventObject:Event):void {
  for (var i:uint = 0; i < nCount; i++) {
    var instance:MovieClip = instances[i];
    instance.y += 5;
    if (instance.y > nStageHeight) {
      instance.y -= nStageHeight;
    }
  }
}

アニメーションの速さを確かめるので、フレームレートは最高の120FPSに設定しておこう図3⁠。

図3 フレームレートを120FPSに設定
図3 フレームレートを120FPSに設定

[ムービープレビュー]でアニメーションを見ると、数多くのインスタンスが下に向かってスクロールする図4⁠。フレームレートに対して明らかに遅いと感じるだろう(処理の速い環境の場合は、インスタンスの数を増やす⁠⁠。

図4 数多くのインスタンスが下に向かってスクロールする
図4 数多くのインスタンスが下に向かってスクロールする

アニメーションするインスタンスは、フレームごとに描き替えられる。インスタンスがベクターグラフィックスであれば、ラスタライズもそのたびに行われる。しかし、インスタンスを単純に移動するだけなら、つぎのフレームでも同じビットマップがつくられるはずだ。DisplayObject.cacheAsBitmapプロパティtrueにすると、そのビットマップをメモリにとっておいて、つぎのフレームでも使えるときは使い回す。

以下のスクリプト2では、⁠ライブラリ]のビットマップ(クラスMyClass)からインスタンスをつくるforループの中で、各インスタンスのDisplayObject.cacheAsBitmapプロパティにtrueが設定してある。⁠ムービープレビュー]を確かめると、アニメーションは明らかに速くなったのがわかるだろう。

スクリプト2 インスタンスDisplayObject.cacheAsBitmapプロパティをtrueに設定する
// フレームアクション: メインタイムライン
var nStageHeight:int = stage.stageHeight;
var nCount:uint = 500;
var instances:Vector.<MovieClip> = new Vector.<MovieClip>(nCount);
for (var j:uint = 0; j < nCount; j++) {
  var _mc:MovieClip = new MyClass();
  instances[j] = _mc;
  addChild(_mc);
  _mc.x = Math.random() * stage.stageWidth;
  _mc.y = Math.random() * stage.stageHeight;
  _mc.cacheAsBitmap = true;
}
addEventListener(Event.ENTER_FRAME, xMove);
function xMove(eventObject:Event):void {
  for (var i:uint = 0; i < nCount; i++) {
    var instance:MovieClip = instances[i];
    instance.y += 5;
    if (instance.y > nStageHeight) {
      instance.y -= nStageHeight;
    }
  }
}

回転のアニメーションをビットマップでキャッシュする

気をつけてほしいのは、DisplayObject.cacheAsBitmapプロパティをtrueにすればつねにムービーが最適化される訳ではないということだ。たとえば、前掲スクリプト2において、DisplayObject.enterFrameイベントに加えたアニメーションのリスナー関数(xMove())で、すべてのインスタンスを回した場合だDisplayObject.rotationプロパティの設定⁠⁠。

function xMove(eventObject:Event):void {
  for (var i:uint = 0; i < nCount; i++) {
    var instance:MovieClip = instances[i];
    instance.rotation += 5;   // インスタンスの回転を追加
    instance.y += 5;
    if (instance.y > nStageHeight) {
      instance.y -= nStageHeight;
    }
  }
}

[ムービープレビュー]を見ると、DisplayObject.cacheAsBitmapプロパティを設定する前に逆戻りした遅いアニメーションになる。インスタンスを回してしまうと、つぎのフレームでは前のビットマップは(角度が違って)使い回せないからだ。それだけではない、使えないビットマップが、毎フレーム無駄にメモリされる。使い回せないときには、プロパティはfalseに戻すべきなのである。

もっとも、単純な回転であれば、ビットマップをキャッシュするという考え方は使える。インスタンスがちょうど1周(360度回転)すると見た目はもとに戻る。つまり、適当な角度の刻みで1回転分のビットマップを用意しておけば、インスタンスを回す代わりにもっとも近い(近似値の)角度のビットマップに差替えればよいのだ。

本稿ではもちろん、これらのビットマップをスクリプトでつくる。MovieClipを始めとするDisplayObjectインスタンスのビットマップイメージをBitmapDataオブジェクトにコピーするには、BitmapData.draw()メソッドを用いる。第1引数には、コピーもとのDisplayObjectインスタンスを渡す。さらに、第2引数にMatrixオブジェクトを定めれば、ビットマップイメージが変形できる[1]⁠。

BitmapDataオブジェクト.draw(描画もとオブジェクト, 変換Matrixオブジェクト)

ビットマップのイメージを描くBitmapDataインスタンスは、お約束どおり、BitmapData()コンストラクタメソッドでつくる。第1および第2引数は、インスタンスの幅と高さを定めなければならない。また、背景を透明にするには、オプションの第3引数に透明を示すtrue(デフォルト値)と第4引数に背景色として黒(0x0)の指定が加わる。

new BitmapData(幅, 高さ, 透明設定, 背景色)

BitmapDataインスタンスの幅と高さは、360度回転するイメージを描くために、描画もとMovieClipインスタンスの寸法のでもっとも大きい対角線の長さにしよう図5⁠。

図5 MovieClipインスタンスの寸法でもっとも大きいのは対角線
図5 MovieClipインスタンスの寸法でもっとも大きいのは対角線

BitmapData.draw()メソッドを使うとき気をつけるのは、BitmapDataインスタンスの基準点が左上角にあることだ。MovieClipインスタンスは、回転させるために基準点を中心にとった(前掲図5⁠。すると、メソッドをデフォルトのまま用いれば、BitmapDataオブジェクトにはインスタンスの右下1/4しか描かれない図6⁠。インスタンス全体を描画するには、メソッドの第2引数となるMatrixオブジェクトでイメージを右下にずらさないといけないのだ。

図6 BitmapData.draw()メソッドのデフォルトは左上角を基準点にして描画する
図6 BitmapData.draw()メソッドのデフォルトは左上角を基準点にして描画する

MovieClipインスタンスを0度から1度刻みで359度まで回し、それらを計360個のBitmapDataにコピーしてVectorエレメントとして納めたのが、つぎのスクリプトだ。インスタンスを回転しながら下にスクロールするアニメーションまで含めたフレームアクション全体は、後にスクリプト3として掲げる。

var _mc:MovieClip = new MyClass();
var nWidth:Number = _mc.width;
var nHeight:Number = _mc.height;
var nDiagonal:Number = Math.sqrt(nWidth * nWidth + nHeight * nHeight);
var nMaxDegrees:uint = 360;
var nRotation:uint = 0;
var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>(nMaxDegrees);
for (var i:uint = 0; i < nMaxDegrees; i++) {
  var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  var myMatrix:Matrix = new Matrix();
  myMatrix.rotate(i / 180 * Math.PI);
  myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  myBitmapData.draw(_mc, myMatrix);
  rotationData[i] = myBitmapData;
}

MovieClipインスタンス(_mc)はひとつつくっただけで、新たに加えたforループの中で360度分の回転したBitmapDataインスタンス(myBitmapData)をつくって、Vectorオブジェクト(rotationData)のエレメントに納めた。

Matrixオブジェクトの扱いは前回の第57回インスタンスをクリックした点で回しながらドラッグするで学んだばかりだ。Matrix.rotate()メソッドにより1度ずつ増やした角度で回し、Matrix.translate()メソッドで画像がBitmapDataオブジェクトの中心にくるよう右下にずらしている。

キャッシュしたBitmapDataオブジェクトを使った回転のアニメーション

前ページでVectorオブジェクトに納めたBitmapDataエレメントを用いて、たくさんのインスタンスを回しながら下にスクロールさせるアニメーションに仕上げよう。前掲スクリプト2のフレームアクションに前項のスクリプトをつけ足し、いくつか手を加えたのが以下のスクリプト3だ。おもな修正はコメントアウトして残した。

まず、MovieClipインスタンスは回転のBitmapDataオブジェクトをつくり終えたら使わない。ただし、BitmapDataオブジェクトはそのままではタイムラインに置けないので、ビットマップイメージの容れ物となるBitmapインスタンスをVectorオブジェクト(instances)に必要な数(nCount)だけ納める。

もっとも、Bitmapインスタンスに入れるBitmapDataオブジェクトは回転のアニメーションでつぎつぎに差替えるので、Bitmap()コンストラクタメソッドの引数には渡さないBitmap()コンストラクタの引数については、第34回3次元空間における回転参照⁠⁠。代わりに、Bitmap.bitmapDataプロパティで設定している。

スクリプト3 BitmapDataインスタンスの差替えで回転を表すアニメーション
// フレームアクション: メインタイムライン
var _mc:MovieClip = new MyClass();
var nWidth:Number = _mc.width;
var nHeight:Number = _mc.height;
var nStageHeight:int = stage.stageHeight;
var nDiagonal:Number = Math.sqrt(nWidth * nWidth + nHeight * nHeight);
var nMaxDegrees:uint = 360;
var nRotation:uint = 0;
var nCount:uint = 500;
// var instances:Vector.<MovieClip> = new Vector.<MovieClip>(nCount);
var instances:Vector.<Bitmap> = new Vector.<Bitmap>(nCount);
var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>(nMaxDegrees);
for (var i:uint = 0; i < nMaxDegrees; i++) {
  var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  var myMatrix:Matrix = new Matrix();
  myMatrix.rotate(i / 180 * Math.PI);
  myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  myBitmapData.draw(_mc, myMatrix);
  rotationData[i] = myBitmapData;
}
for (var j:uint = 0; j < nCount; j++) {
  // var _mc:MovieClip = new MyClass();
  var myBitmap:Bitmap = new Bitmap();   // インスタンス名変更
  myBitmap.bitmapData = rotationData[nRotation];
  instances[j] = myBitmap;
  addChild(myBitmap);
  // _mc.x = Math.random() * stage.stageWidth;
  myBitmap.x = Math.random() * stage.stageWidth - nDiagonal / 2;
  // _mc.y = Math.random() * stage.stageHeight;
  myBitmap.y = Math.random() * stage.stageHeight - nDiagonal / 2;
}
addEventListener(Event.ENTER_FRAME, xMove);
function xMove(eventObject:Event):void {
  nRotation += 5;
  nRotation %= nMaxDegrees;
  for (var i:uint = 0; i < nCount; i++) {
    // var instance:MovieClip = instances[i];
    var instance:Bitmap = instances[i];
    instance.bitmapData = rotationData[nRotation];
    instance.y += 5;
    if (instance.y > nStageHeight) {
      instance.y -= nStageHeight;
    }
  }
}

Bitmapインスタンス共通の回転角は変数(nRotation)に入れた。アニメーションのリスナー関数(xMove())は、毎フレームその角度を増やし、その角度に応じたBitmapDataエレメントをVectorオブジェクト(rotationData)から取出して、各インスタンスのBitmap.bitmapDataプロパティに設定することで回転させている。

ただ、BitmapDataエレメントのインデックスは359までなので、360(nMaxDegrees)の剰余を角度(nRotation)に設定し直して、0から359までの整数を繰返すように定めた。これで360個の回転するビットマップがキャッシュされたのと同じ結果が得られる。⁠ムービープレビュー]を確かめれば、回りながらスクロールするアニメーションが、DisplayObject.rotationプロパティで回す場合と比べて目に見えて速くなったはずだ図7⁠。

図7 数多くのインスタンスが回りながら下にスクロールする
図7 数多くのインスタンスが回りながら下にスクロールする

オブジェクトを使い回す

ここまで、ステージに描かれるビットマップの使い回し方について考えてきた。さらに、目に見えないオブジェクトでも、ひとたびつくったものは使い回した方が、メモリにやさしく速さも稼げる。いわばエコなスクリプティングを目指そう。よくあるのが、座標空間に絡むオブジェクトをforループで扱う場合だ。たとえば、2次元平面の座標をPointオブジェクトで定めるとする。

for (var i:int = 0; i 

このとき、forループ内のPointオブジェクトは、繰返すたびに上書きされている。つまり、使い捨てだ。こういう場合、捨てずに使い回す方が、ステートメント数は増えるものの、メモリも速さもお得なのだ。なお、座標値はループごとに変わるものとする。

var myPoint:Point = new Point();
for (var i:int = 0; i 

オブジェクトによっては、初期化つまりコンストラクタメソッドでつくったデフォルト状態に戻せるものもある。たとえば、MatrixクラスならMatrix.identity()メソッドがそれだ。改めて前掲スクリプト3を見ると、回転したBitmapDataオブジェクトを360個つくるforループの中でMatrixオブジェクトを毎回コンストラクタメソッドで新たにつくり直している。

for (var i:uint = 0; i < nMaxDegrees; i++) {
  var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  var myMatrix:Matrix = new Matrix();   // オブジェクトの新規作成
  myMatrix.rotate(i / 180 * Math.PI);
  myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  myBitmapData.draw(_mc, myMatrix);
  rotationData[i] = myBitmapData;
}

最適化を目指すなら、Matrixオブジェクトはforループの前にひとつだけつくり、ループ内ではMatrix.identity()メソッドで初期化した方がよい[2]⁠。その改善を加えたのが、つぎのスクリプト4だ。

スクリプト4 Matrix.identity()メソッドでひとつのMatrixオブジェクトを使い回す
// フレームアクション: メインタイムライン
var _mc:MovieClip = new MyClass();
var nWidth:Number = _mc.width;
var nHeight:Number = _mc.height;
var nStageHeight:int = stage.stageHeight;
var nDiagonal:Number = Math.sqrt(nWidth * nWidth + nHeight * nHeight);
var nMaxDegrees:uint = 360;
var nRotation:uint = 0;
var nCount:uint = 500;
var instances:Vector.<Bitmap> = new Vector.<Bitmap>(nCount);
var rotationData:Vector.<BitmapData> = new Vector.<BitmapData>(nMaxDegrees);
var myMatrix:Matrix = new Matrix();   // ループ外に出す
for (var i:uint = 0; i < nMaxDegrees; i++) {
  var myBitmapData:BitmapData = new BitmapData(nDiagonal, nDiagonal, true, 0x0);
  // var myMatrix:Matrix = new Matrix();
  myMatrix.identity();   // 初期化
  myMatrix.rotate(i / 180 * Math.PI);
  myMatrix.translate(nDiagonal / 2, nDiagonal / 2);
  myBitmapData.draw(_mc, myMatrix);
  rotationData[i] = myBitmapData;
}
for (var j:uint = 0; j < nCount; j++) {
  var myBitmap:Bitmap = new Bitmap();
  instances[j] = myBitmap;
  addChild(myBitmap);
  myBitmap.bitmapData = rotationData[nRotation];
  myBitmap.x = Math.random() * stage.stageWidth - nDiagonal / 2;
  myBitmap.y = Math.random() * stage.stageHeight - nDiagonal / 2;
}
addEventListener(Event.ENTER_FRAME, xMove);
function xMove(eventObject:Event):void {
  nRotation +=  5;
  nRotation %=  nMaxDegrees;
  for (var i:uint = 0; i < nCount; i++) {
    var instance:Bitmap = instances[i];
    instance.bitmapData = rotationData[nRotation];
    instance.y += 5;
    if (instance.y > nStageHeight) {
      instance.y -= nStageHeight;
    }
  }
}

特別編の前回第56回配列エレメントすべてをforループで扱うでは、Array()コンストラクタメソッドより、配列アクセス演算子[]で配列をつくる方が速いと述べた。しかし、使い回すとさらに速い。配列を初期化するにはArray.lengthプロパティを0にすればよい[3]⁠。

my_array.length = 0;   // エレメントがなくなる

初期化の仕方はオブジェクトによって異なる。使い回せそうだと思ったら、ぜひリファレンスを見てほしい。なお、Flash Player 11で、3次元空間座標を扱うクラスにデータのコピー用メソッドがいくつか加えられた。3次元空間では大量の座標を演算する場合が少なくない。データをコピーしてオブジェクトが使い回せれば最適化できる。詳しくはMatrix3Dオブジェクトの行列データをコピーするメソッドVector3Dオブジェクトの座標値をコピーや設定するメソッドをお読みいただきたい。

この特別編の次回(第60回)は、Mathクラスなどの数値演算の最適化について考える。

今回解説した次のサンプルファイルがダウンロードできます。

おすすめ記事

記事・ニュース一覧