技術評論社より発売された拙著『ActionScript 3.0パフォーマンスチューニング』から、ActionScript 3.0の最適化の仕方をご紹介する特別編の3回目は、ふたつのお題を採上げる。第1は、アニメーションを滑らかに再生するためのビットマップイメージの扱いだ。そして第2は、オブジェクトを使い回すこと、いわばエコなスクリプティングの勧めである。
アニメーションの重さを測るふたつの物差し
アニメーションが重いとか軽いとかいわれるとき、実はふたつの物差しがある。ひとつはデータの容量だ。そしてもうひとつは、描画するためのCPUの負荷である。
ベクターグラフィックスが軽いといわれるのは、前者の容量を指す。だが、描画についてはディスプレイがピクセルで表示するため、ベクターはビットマップに変換しなければならない(これを「ラスタライズ」という)。その処理にCPUが使われる。つまり、ベクターグラフィックスの描画は重くなる(図1左図)。
他方、ビットマップグラフィックスは、画像のピクセル数が増えるほどデータ容量は大きくなる。けれど、ディスプレイには、ピクセルのまま映せば済む。したがって、CPUの負荷が少ないので、ビットマップグラフィックスの描画は軽い(図1右図)。
- ベクターグラフィックス
- 数学的なデータ→容量は小さい
- ビットマップに変換(ラスタライズ)して描画→CPU負荷が高い
- ビットマップグラフィックス
- ピクセルごとのデータ→容量は大きい
- ピクセルをスクリーンに映す→CPU負荷が低い
とくにPCの環境では、ダウンロードするデータの容量より、画面を描き替えるアニメーションの滑らかさが求められる。そうした場合には、グラフィックスをビットマップにすると、データは膨らんでも描画が速められる。もっとも、これはFlashのアニメーションすべてにいえることで、スクリプトにかぎった話ではない。この知識を踏まえて、次項からActionScriptのお題に移る。
平行移動のアニメーションではDisplayObject.cacheAsBitmapプロパティをtrueにする
まずは、上から下に単純にスクロールするインスタンスを大量につくって、アニメーションがどうなるか見てみよう。[ライブラリ]のMovieClipシンボルにクラスを定めて(図2)、インスタンスは動的につくることにする。
メインタイムラインに書くフレームアクションは、以下のスクリプト1のとおりだ。まず、forループで決められた数(nCount)のMovieClipインスタンス(_mc)をつくってタイムラインのランダムな位置に置き、Vectorオブジェクト(instances)に納める。
つぎに、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)にリスナー関数(xMove())を加えた。そして、リスナー関数は、VectorオブジェクトからforループでMovieClipインスタンスを順に取出し、下にスクロールさせる。なお、ステージ下端(nStageHeight)を超えたインスタンスは、上端に戻している。
アニメーションの速さを確かめるので、フレームレートは最高の120FPSに設定しておこう(図3)。
[ムービープレビュー]でアニメーションを見ると、数多くのインスタンスが下に向かってスクロールする(図4)。フレームレートに対して明らかに遅いと感じるだろう(処理の速い環境の場合は、インスタンスの数を増やす)。
アニメーションするインスタンスは、フレームごとに描き替えられる。インスタンスがベクターグラフィックスであれば、ラスタライズもそのたびに行われる。しかし、インスタンスを単純に移動するだけなら、つぎのフレームでも同じビットマップがつくられるはずだ。DisplayObject.cacheAsBitmapプロパティをtrueにすると、そのビットマップをメモリにとっておいて、つぎのフレームでも使えるときは使い回す。
以下のスクリプト2では、[ライブラリ]のビットマップ(クラスMyClass)からインスタンスをつくるforループの中で、各インスタンスのDisplayObject.cacheAsBitmapプロパティにtrueが設定してある。[ムービープレビュー]を確かめると、アニメーションは明らかに速くなったのがわかるだろう。
回転のアニメーションをビットマップでキャッシュする
気をつけてほしいのは、DisplayObject.cacheAsBitmapプロパティをtrueにすればつねにムービーが最適化される訳ではないということだ。たとえば、前掲スクリプト2において、DisplayObject.enterFrameイベントに加えたアニメーションのリスナー関数(xMove())で、すべてのインスタンスを回した場合だ(DisplayObject.rotationプロパティの設定)。
[ムービープレビュー]を見ると、DisplayObject.cacheAsBitmapプロパティを設定する前に逆戻りした遅いアニメーションになる。インスタンスを回してしまうと、つぎのフレームでは前のビットマップは(角度が違って)使い回せないからだ。それだけではない、使えないビットマップが、毎フレーム無駄にメモリされる。使い回せないときには、プロパティはfalseに戻すべきなのである。
もっとも、単純な回転であれば、ビットマップをキャッシュするという考え方は使える。インスタンスがちょうど1周(360度回転)すると見た目はもとに戻る。つまり、適当な角度の刻みで1回転分のビットマップを用意しておけば、インスタンスを回す代わりにもっとも近い(近似値の)角度のビットマップに差替えればよいのだ。
本稿ではもちろん、これらのビットマップをスクリプトでつくる。MovieClipを始めとするDisplayObjectインスタンスのビットマップイメージをBitmapDataオブジェクトにコピーするには、BitmapData.draw()メソッドを用いる。第1引数には、コピーもとのDisplayObjectインスタンスを渡す。さらに、第2引数にMatrixオブジェクトを定めれば、ビットマップイメージが変形できる[1]。
ビットマップのイメージを描くBitmapDataインスタンスは、お約束どおり、BitmapData()コンストラクタメソッドでつくる。第1および第2引数は、インスタンスの幅と高さを定めなければならない。また、背景を透明にするには、オプションの第3引数に透明を示すtrue(デフォルト値)と第4引数に背景色として黒(0x0)の指定が加わる。
BitmapDataインスタンスの幅と高さは、360度回転するイメージを描くために、描画もとMovieClipインスタンスの寸法のでもっとも大きい対角線の長さにしよう(図5)。
BitmapData.draw()メソッドを使うとき気をつけるのは、BitmapDataインスタンスの基準点が左上角にあることだ。MovieClipインスタンスは、回転させるために基準点を中心にとった(前掲図5)。すると、メソッドをデフォルトのまま用いれば、BitmapDataオブジェクトにはインスタンスの右下1/4しか描かれない(図6)。インスタンス全体を描画するには、メソッドの第2引数となるMatrixオブジェクトでイメージを右下にずらさないといけないのだ。
MovieClipインスタンスを0度から1度刻みで359度まで回し、それらを計360個のBitmapDataにコピーしてVectorエレメントとして納めたのが、つぎのスクリプトだ。インスタンスを回転しながら下にスクロールするアニメーションまで含めたフレームアクション全体は、後にスクリプト3として掲げる。
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プロパティで設定している。
Bitmapインスタンス共通の回転角は変数(nRotation)に入れた。アニメーションのリスナー関数(xMove())は、毎フレームその角度を増やし、その角度に応じたBitmapDataエレメントをVectorオブジェクト(rotationData)から取出して、各インスタンスのBitmap.bitmapDataプロパティに設定することで回転させている。
ただ、BitmapDataエレメントのインデックスは359までなので、360(nMaxDegrees)の剰余を角度(nRotation)に設定し直して、0から359までの整数を繰返すように定めた。これで360個の回転するビットマップがキャッシュされたのと同じ結果が得られる。[ムービープレビュー]を確かめれば、回りながらスクロールするアニメーションが、DisplayObject.rotationプロパティで回す場合と比べて目に見えて速くなったはずだ(図7)。
オブジェクトを使い回す
ここまで、ステージに描かれるビットマップの使い回し方について考えてきた。さらに、目に見えないオブジェクトでも、ひとたびつくったものは使い回した方が、メモリにやさしく速さも稼げる。いわばエコなスクリプティングを目指そう。よくあるのが、座標空間に絡むオブジェクトをforループで扱う場合だ。たとえば、2次元平面の座標をPointオブジェクトで定めるとする。
このとき、forループ内のPointオブジェクトは、繰返すたびに上書きされている。つまり、使い捨てだ。こういう場合、捨てずに使い回す方が、ステートメント数は増えるものの、メモリも速さもお得なのだ。なお、座標値はループごとに変わるものとする。
オブジェクトによっては、初期化つまりコンストラクタメソッドでつくったデフォルト状態に戻せるものもある。たとえば、MatrixクラスならMatrix.identity()メソッドがそれだ。改めて前掲スクリプト3を見ると、回転したBitmapDataオブジェクトを360個つくるforループの中でMatrixオブジェクトを毎回コンストラクタメソッドで新たにつくり直している。
最適化を目指すなら、Matrixオブジェクトはforループの前にひとつだけつくり、ループ内ではMatrix.identity()メソッドで初期化した方がよい[2]。その改善を加えたのが、つぎのスクリプト4だ。
特別編の前回第56回「配列エレメントすべてをforループで扱う」では、Array()コンストラクタメソッドより、配列アクセス演算子[]で配列をつくる方が速いと述べた。しかし、使い回すとさらに速い。配列を初期化するにはArray.lengthプロパティを0にすればよい[3]。
初期化の仕方はオブジェクトによって異なる。使い回せそうだと思ったら、ぜひリファレンスを見てほしい。なお、Flash Player 11で、3次元空間座標を扱うクラスにデータのコピー用メソッドがいくつか加えられた。3次元空間では大量の座標を演算する場合が少なくない。データをコピーしてオブジェクトが使い回せれば最適化できる。詳しくは「Matrix3Dオブジェクトの行列データをコピーするメソッド」と「Vector3Dオブジェクトの座標値をコピーや設定するメソッド」をお読みいただきたい。
この特別編の次回(第60回)は、Mathクラスなどの数値演算の最適化について考える。
今回解説した次のサンプルファイルがダウンロードできます。