このたび技術評論社より、拙著『ActionScript 3.0パフォーマンスチューニング 』が発売される。ActionScript 3.0の処理をいかに軽く、あるいは速くするかという技術やコツの解説だ。そこで、本連載の特別編として、著書からいくつかのネタを紹介してみたい。なお、連載の本編も並行して進める。
初めのネタとして採上げるのはVectorクラスだ。第42回「Vector3Dクラスの3次元空間座標とインスタンスへの描画」の「Vectorクラスと3次元空間から2次元平面への変換 」では、「 VectorはいわばArrayを最適化したクラスだ」と紹介した。配列を使った処理がVectorオブジェクトで置換えられるなら、その方がお得だ。ただ、置換えにちょっとした工夫が要る場合もある。その例と、Vectorオブジェクトで最適化するコツをご説明したい。
Vectorクラスのおさらい
まず、Vectorクラスについておさらいしておこう。Vectorクラスを使おうとするとき、配列つまりArrayクラスとは違うふたつの条件があった(前出第42回「Vector3Dクラスの3次元空間座標とインスタンスへの描画」参照) 。この第2の条件については、次項以降で考えたい。
Vectorクラスを使うための条件
エレメントにひとつのデータ型を定める
インデックスが連番になる
もうひとつ触れておきたいのは、エレメントを予め納めたVectorインスタンスのつくり方だ。第44回「ワイヤーフレームの立方体を回す 」の「立方体のワイヤーフレームを描く」で、Flash Professional CS5から使えるつぎのシンタックスをご紹介した。
new <ベース型>[エレメント0, エレメント1, …, エレメントN]
では、Flash CS4 Professionalではどうしたらよいかというと、Vector() コンストラクタメソッド にはエレメントが引数として渡せない。したがって、インスタンスをつくってから、Vector.push() メソッド でエレメントを加えることになる。
Vectorオブジェクト.push(エレメント0, エレメント1, …, エレメントN)
もっとも、1行のステートメントで、エレメントを予め納めたVectorインスタンスがつくれないことはない。Vector() 関数 を使うやり方だ。
Vector.<ベース型>([エレメント0, エレメント1, …, エレメントN])
引数に渡しているのは、必要なエレメントを納めた配列だ。Vector() 関数は、その配列をVectorオブジェクトに変換する。個人的には、この書き方の方が、CS5のnew 演算子を使うシンタックスよりも見た目の違和感が少ない。しかし、配列からVectorにオブジェクトを変換するというのは、負荷がどうしても高くなる。今回のテーマである最適化にはそぐわない。
配列でキーコードとプロパティを扱った例
さて、これから本題だ。配列からVectorクラスを使った処理に書替えるお題として、第15回「配列を使ったキーコードとプロパティの扱い 」でつくったスクリプトを採上げよう。インスタンスを上下左右の矢印キーで動かすムービーだ(図1 ) 。
図1 インスタンスを上下左右の矢印キーで動かす
第15回スクリプト2は、上下左右の矢印キーによる操作を配列で切り分けた。各矢印のキーコードのインデックスに、それぞれ操作するプロパティ(文字列)と移動方向(数値)を入れ子の配列にしてエレメントに納めておく。そして、キーボードのキーが押されたとき、配列からそのキーコードのエレメントを取出し、そのデータにもとづいて操作している。
第15回スクリプト2 操作するプロパティを配列のキーコードのインデックスに定める(再掲)
// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;
var nShiftMove:int = 10;
var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = ["x", -1];
keys_array[Keyboard.RIGHT] = ["x", 1];
keys_array[Keyboard.UP] = ["y", -1];
keys_array[Keyboard.DOWN] = ["y", 1];
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
var nKeyCode:int = eventObject.keyCode;
var bShiftKey:Boolean = eventObject.shiftKey;
xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
var key_array:Array = keys_array[nKeyCode];
if (key_array) {
this[key_array[0]] += key_array[1] * xGetPixels(bShiftKey);
}
}
function xGetPixels(bShiftKey:Boolean):int {
var nPixels:int = bShiftKey ? nShiftMove : nMove;
return nPixels;
}
このスクリプトは、操作するキーのコードとその内容をすべて配列にまとめた。そのため、押したキーが何かを条件判定することなく、キーコードのインデックスに配列エレメントがあれば、そのデータにもとづいて処理している。キー操作についてのデータがわかりやすくまとまっていて、押されたキーの処理(関数xMove())も極めてシンプルだ。
ところが、速さでは条件判定の処理の方が勝る。具体的には、switch ステートメントを用いた第14回「キー操作とif以外の条件判定 」スクリプト2の方が、前掲第15回スクリプト2より速い。そこで、配列をVectorオブジェクトに置換えることによって、処理速度を高めてみよう。
配列をVectorオブジェクトに置換える
第15回スクリプト2の配列をVectorオブジェクトで置換えようとしたとき、すぐにつまずくのがインデックスの数値だ。矢印キーのコードには、第13回表2 (再掲)のとおり、37から40までの整数が割り振られている。ところが、01「Vectorクラスのおさらい」で確かめたとおり、「 インデックスが連番になる」必要があった。
第13回表2 Keyboardクラスの矢印キーの定数(再掲)
定数 キー 値
Keyboard.DOWN 下矢印キー 40
Keyboard.LEFT 左矢印キー 37
Keyboard.RIGHT 右矢印キー 39
Keyboard.UP 上矢印キー 38
この問題を解くのは、さほど難しくない。というのは、キーボードのキーの数はかぎられるからだ。すべてのキーコードのインデックスに、無効であることがわかる整数をエレメントとして入れておけばよい。Vector() コンストラクタの第1引数には、長さ(Vector.length プロパティ)を定める整数が渡せる[1] 。すると、基本的にベース型のデフォルト値が、その数だけVectorオブジェクトのエレメントに加えられる。
前掲第15回スクリプト2の配列は、エレメントに入れ子の配列を加えていた。したがって、置換えるVectorオブジェクトのベース型はArrayにする。また、長さは300もあれば十分だろう[2] 。実は、書替えはたった1行で済む。
// var keys_array:Array = new Array();
var keys_array:Vector .<Array> = new Vector .<Array>(300); // Vectorオブジェクトに置換え
もっとも、変数名に「_array」の文字がつくのは気になる。そこで、変数名も含めて前掲第15回スクリプト2を書替えたのが、つぎのスクリプト1だ。これで、処理の速さは条件判定のswitch ステートメントを使った場合(第14回スクリプト2)にかなり近づく。
スクリプト1 操作するプロパティをVectorオブジェクトのキーコードのインデックスに定める
// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;
var nShiftMove:int = 10;
var keys:Vector .<Array> = new Vector .<Array>(300);
keys[Keyboard.LEFT] = ["x", -1];
keys[Keyboard.RIGHT] = ["x", 1];
keys[Keyboard.UP] = ["y", -1];
keys[Keyboard.DOWN] = ["y", 1];
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
var nKeyCode:int = eventObject.keyCode;
var bShiftKey:Boolean = eventObject.shiftKey;
xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
var key:Array = keys[nKeyCode];
if (key) {
this[key[0]] += key[1] * xGetPixels(bShiftKey);
}
}
function xGetPixels(bShiftKey:Boolean):int {
var nPixels:int = bShiftKey ? nShiftMove : nMove;
return nPixels;
}
[1] さらに、Vector() コンストラクタの第2引数は、長さを固定するかどうかのブール(論理)値を定める。デフォルト値は固定しないfalse だ。
new Vector.<ベース型>(長さ, 長さの固定)
Vectorオブジェクトの長さは予め定めた方が速い
Vectorオブジェクトをつくるとき、必要なエレメント数の上限が予めわかっているときは、コンストラクタの第1引数で長さを定めた方がよい。エレメントを加えるときの扱いが速くなるからだ。英語版ヘルプ([Optimizing Performance for the Flash Platform] > [ActionScript 3.0 performance] > [Vector class versus Array class ])にはつぎのように説明されている(筆者訳※3 ) 。
Vectorの大きさが前もって定められていないと、空きが足りなくなったときに増やされます。Vectorの大きさが増えるたびに、新たなメモリ領域が割当てられます。そのときのVectorの中身は、その新たなメモリ領域にコピーされます。追加の割当てとデータのコピーは、パフォーマンスを損なうことになります。
Vectorオブジェクトをつくると、含まれるエレメントの数ぴったりではなく、予め少し余裕をみてメモリが充てられる。さらにエレメントを加えて、空きが足りなくなると、割当てを増やす。だが、それは追加したエレメント分ではなく、オブジェクト全体(プラス余裕分)の大きさで、その新たな領域にすべてのエレメントがコピーされるというのだ。
マンションの建替えを想像すればよい。戸数が足りなくなったら、その部屋数を既存の建物につけ足すのではない。新たな土地を確保して、より大きなマンションを建て、全員が引越すのだ(図2 ) 。土地の確保と引越には手間ひまがかかる。初めから十分な戸数の大規模マンションを建てておけば、引越などしなくて済む。
図2 新たにメモリを確保してエレメントが全員引越す
上述の理由から、新たなメモリの確保とデータのコピーという負荷を避けるには、Vector() コンストラクタに予め必要なエレメント数を定めた方がよい。
特別編の次回は、大量の処理につきものの繰返し処理を採上げる。とくに、配列をfor ループでいかに速く扱うかが、おもなお題になるだろう。
[3]
日本語のヘルプ([モバイル] > [Flash Platformのパフォーマンスの最適化] > [ActionScript 3.0のパフォーマンス] > [VectorクラスとArrayクラス ])にはつぎのように記述されている。しかし、意味がわかりにくいため、本文では英語版をもとにした。
Vectorのサイズが時間よりも先に指定されない場合、Vectorの容量が不足すると、サイズが増えます。Vectorのサイズが増えるたびに、メモリの新しいブロックが割り当てられます。Vectorの最新の内容がメモリの新しいブロックにコピーされます。データを余分に割り当てて複製することにより、パフォーマンスに影響があります。
今回解説した次のサンプルファイルがダウンロードできます。