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

第54回特別編】配列の処理をVectorオブジェクトで最適化する

このたび技術評論社より、拙著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クラスを使うための条件
  1. エレメントにひとつのデータ型を定める
  2. インデックスが連番になる

もうひとつ触れておきたいのは、エレメントを予め納めた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 インスタンスを上下左右の矢印キーで動かす
図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;
}

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 新たにメモリを確保してエレメントが全員引越す
図2 新たにメモリを確保してエレメントが全員引越す

上述の理由から、新たなメモリの確保とデータのコピーという負荷を避けるには、Vector()コンストラクタに予め必要なエレメント数を定めた方がよい。

特別編の次回は、大量の処理につきものの繰返し処理を採上げる。とくに、配列をforループでいかに速く扱うかが、おもなお題になるだろう。

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

おすすめ記事

記事・ニュース一覧