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

第24回インスタンスの管理と配列の並べ替え

前回の第23回クラスのデザインとループ処理では、EllipticMotionクラスのデザインを将来の拡張も考えて整理し、タイムラインのフレームアクションでは繰返し処理により複数のインスタンスを配置した図1⁠。

図1 複数のインスタンスが楕円軌道でアニメーションする
図1 複数のインスタンスが楕円軌道でアニメーションする

今回の課題は、まずインスタンスの回転するスピードを、マウスポインタの位置によって変えることにする。つぎに、インスタンスの重ね順を、3次元風の表現に対応するよう修正しよう。

図2 インスタンスの重ね順が3次元風の表現に一部対応していない
図2 インスタンスの重ね順が3次元風の表現に一部対応していない

マウスポインタの位置に応じてインスタンスの回転スビードを変える

まずは、インスタンスが回転するスピードを、マウスポインタの位置によって変えるため、前回のスクリプト2で修正定義したEllipticMotionクラスにもう少し手を加えよう。

今のところ、インスタンスのアニメーションは、コンストラクタメソッドEllipticMotion()の本体で、インスタンスごとのDisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)にリスナー関数rotate()を登録して処理している。

public class EllipticMotion extends MovieClip {
  // ...[中略]...
  public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
  degree = nDegree;
  center = myCenter;
  radius = myRadius;
  addEventListener(Event.ENTER_FRAME, rotate);   // 後に削除
  setRotation();
}

しかし、同じマウスポインタの座標値を、すべてのインスタンスからいちいち確かめるのは効率がよくない。また、のちにインスタンスの重ね順をコントロールする際には、それぞれの仮想の奥行きの値をメインタイムラインから調べる必要がある。

そうだとすれば、タイムラインに配置したインスタンスは、タイムラインのフレームアクションで管理することにしたい。そのうえで、EllipticMotionクラスのrotation()メソッドはタイムラインから呼出し、マウスポインタの座標に応じた回転角の値をその引数として渡そう。

Flashムービー(FLA)ファイルのフレームアクションとして作成した前回のスクリプト3には、タイムラインに配置するEllipticMotionインスタンスが納められるべき配列を新たに変数として宣言し、初期化する。そして、続くforループのコードブロック{}の中で、生成したインスタンスを表示リストに入れるとともに、この配列にも加えることにする。

var instances_array:Array = new Array();   // 追加: 配列を変数として宣言・初期化
for (var i:int = 0; i < nCount; i++) {
  var _mc:EllipticMotion = new EllipticMotion(nDegree * i,myCenter,myRadius);
  addChild(_mc);
  instances_array.push(_mc);   // 追加: 配列にインスタンスを加える
}

これで、タイムラインのフレームアクションで、いつでも配列 (instances_array)からEllipticMotionインスタンスが取出せる。つまり、そのインスタンスを参照して EllipticMotionクラスのrotation()メソッドが呼出せるようになった。そこで、新たな関数rotate()をフレームアクションに定義して、これをタイムラインのDisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)にリスナーとして登録しよう。

var deceleration:Number = 0.1;
// ...[中略]...
addEventListener(Event.ENTER_FRAME, rotate);
function rotate(EventObject:Event):void {   // 追加: リスナー関数
  var nSpeed:Number = (stage.mouseX - myCenter.x)*deceleration;
  var nLength:uint = instances_array.length;
  for (var i:int = 0; i<nLength; i++) {
    var _mc:EllipticMotion = instances_array[i];   // インスタンスの取出し
    _mc.rotate(nSpeed);   // インスタンスのrotate()メソッドを呼出す
  }
}

rotate()関数本体では、マウスポインタのステージ中央からの水平位置に応じた回転角(nSpeed)を求めたうえで、forループの処理によりEllipticMotionインスタンスを配列(instances_array)から順にすべて取出し、その回転角の値を各インスタンスのrotate()メソッドに渡して呼出している[1]⁠。

以上ふたつの修正を加えたフレームアクションが、次のスクリプト1だ。

スクリプト1 マウスポインタの中央からの水平位置に応じてインスタンスの回転スピードを変える
// タイムライン: メイン
var myCenter:Point = new Point(stage.stageWidth / 2, stage.stageHeight / 2);
var myRadius:Point = new Point(100, 50);
var nCount:uint = 6;
var nDegree:Number = 360 / nCount;
var instances_array:Array = new Array();
var deceleration:Number = 0.1;
for (var i:int = 0; i < nCount; i++) {
  var _mc:EllipticMotion = new EllipticMotion(nDegree * i, myCenter, myRadius);
  addChild(_mc);
  instances_array.push(_mc);
}
addEventListener(Event.ENTER_FRAME, rotate);
function rotate(EventObject:Event):void {
  var nSpeed:Number = (stage.mouseX - myCenter.x) * deceleration;
  var nLength:uint = instances_array.length;
  for (var i:int = 0; i < nLength; i++) {
    var _mc:EllipticMotion = instances_array[i];
    _mc.rotate(nSpeed);
  }
}

この変更にともなって、EllipticMotionクラスにも大きくふたつ修正を加える必要がある。

まずは、すでに触れたとおり、EllipticMotionクラスのコンストラクタメソッドにおけるDisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)へのリスナー登録を削除することだ。

public class EllipticMotion extends MovieClip {
  // ...[中略]...
  public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
    degree = nDegree;
    center = myCenter;
    radius = myRadius;
    // addEventListener(Event.ENTER_FRAME, rotate);   // 削除
    setRotation();
  }

続いて、EllipticMotionクラスのrotate() メソッドが、引数として回転するスピードを受取るようにしなければならない。メソッドはもはやリスナー関数ではなくなるので、引数にイベントオブジェクトは受取らない。替わりに、フレームアクションから渡される回転角の数値を引数とするのである。そして、受取った数値はクラスのインスタンスプロパティ degreeに設定する。さらに、タイムラインから呼出すには、rotate()メソッドのアクセス制御の属性はpublicで指定する必要がある。

// private function rotate(eventObject:Event):void {
  public function rotate(nSpeed:Number):void {
    // degree += speed;   // プロパティspeedは不要に
    degree += nSpeed;
    setRotation();
  }

以上のふたつの修正を加えたのが、次のスクリプト2である。これでメソッドrotate()はリスナー関数ではなくなり、メインタイムラインから呼出せるようになった。なお、インスタンスプロパティとして宣言されていたフレーム当たりの回転角speedは、もはや不要なので削除した。

スクリプト2 メソッドをタイムラインから呼出すように修正
// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
// 回転するMovieClipシンボルの[クラス]に設定
package {
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private var _degree:Number = 0;
    private const DEGREE_TO_RADIAN:Number = Math.PI / 180;
    private var center:Point;
    private var radius:Point;
    private var cos:Number = Math.cos(degree * DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(degree * DEGREE_TO_RADIAN);
    public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
      degree = nDegree;
      center = myCenter;
      radius = myRadius;
      setRotation();
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree % 360 + 360) % 360;
      var radian:Number = _degree * DEGREE_TO_RADIAN;
      cos = Math.cos(radian);
      sin = Math.sin(radian);
    }
    public function rotate(nSpeed:Number):void {
      degree += nSpeed;
      setRotation();
    }
    function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
    }
    private function moveX():void {
      x = center.x + cos * radius.x;
    }
    private function moveY():void {
      y = center.y + sin * radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      var nIndexZ:Number = (nMax - nMin) * (sin + 1) / 2 + nMin;
      return nIndexZ;
    }
  }
}

[ムービープレビュー]を試すと、マウスポインタのステージ中央からの水平位置によって、インスタンスの回転する方向と速さが変わる図3⁠。

図3 マウスポインタのステージ中央からの水平位置により回転の方向とスピードが変わる
図32 マウスポインタのステージ中央からの水平位置により回転の方向とスピードが変わる

インスタンスの重ね順を管理する

インスタンスの重ね順を3次元風の表現に合わせて変えるには、まずインスタンスを正しい重ね順に並べ替える必要がある。メインタイムラインのフレームアクション(スクリプト1)では、EllipticMotionインスタンスを配列に納めた。幸いなことに、配列は並べ替えのメソッドArray.sort()を備えている。このメソッドは、以下のように引数なしで配列に対して呼出せる。trace()関数による[出力]の結果を見ると、エレメントの整数が昇順に並べ替えられている。

var my_array:Array = [3, 0, 1, 4, 2];
my_array.sort();
trace(my_array);   // 出力: 0,1,2,3,4

このArray.sort()メソッドには、ふたつ注意がある。1つは、並べ替えた結果の配列が返されるのではなく、参照した配列そのもののエレメントの順序が変わる。もう1つは、その並べ替えの順序はデフォルトでは、文字列のように頭からひと桁ずつ比較したうえで決められる。たとえば、つぎのスクリプトの結果を見てほしい。

var my_array:Array = [30, 0, 1000, 4, 200];
my_array.sort();
trace(my_array);   // 出力: 0,1000,200,30,4

Array.sort()メソッドには、引数として比較関数が指定できる。比較関数というのは、エレメントの並べ替えの順序を指定する関数だ。つまり、比較関数を定義すれば、配列エレメントの並べ順は自由に決められる。

配列.sort(比較関数);
function 比較関数(引数1:データ型, 引数2:データ型):Number {
  // ふたつの引数の比較評価の結果を数値で返す
}

Array.sort()メソッドに比較関数を指定すると、メソッドが配列エレメントを並べ替えるとき、エレメントをふたつずつ取出してはその関数には引数として渡す。比較関数は、そのふたつの引数のエレメントの大小、つまり並べ順を評価して、その結果を次表1のような数値として返す。

表1 比較関数の引数の大小と戻り値
引数の大小戻り値
引数1 < 引数2-1
引数1 = 引数20
引数1 > 引数21

たとえば、数値のエレメントを数値として比べて並べ替えるには、比較関数(compare)をつぎのように定義したうえで、Array.sort()メソッドの引数に指定すればよい。trace()関数の[出力]結果を見ると、今度は数値の小さい順に並べ替えられたことが確かめられる。

var my_array:Array = [30, 0, 1000, 4, 200];
my_array.sort(compare);
function compare(nA:Number, nB:Number):Number {
  if (nA < nB) {
    return -1;
  } else if (nA > nB) {
    return 1;
  } else {
    return 0;
  }
}
trace(my_array);   // 出力: 0,4,30,200,1000

ところで、Array.sort()メソッドの引数には、次表2のようなArray定数も指定できる。数値の小さい順に並べ替えるのであれば、Array.sort()メソッドの引数に定数Array.NUMERICを指定する方がたやすい[2]⁠。

var my_array:Array = [30, 0, 1000, 4, 200];
my_array.sort(Array.NUMERIC);
trace(my_array); // 出力: 0,4,30,200,1000
表2 Array.sort()メソッドに指定できるArrayクラスの定数
定数説明
CASEINSENSITIVE英字の大文字小文字を区別せずに並べ替える。
DESCENDING並べ替えの順序を、降順(大きい順)に指定する。
NUMERICエレメントが数値の配列を、その値の大きさで順序づける。
RETURNINDEXEDARRAYターゲットの配列は変更せず、並べ替えた結果の整数インデックスをエレメントとした配列が返される。
UNIQUESORTエレメントに重複がない配列のみを並べ替える。重複があれば、0を返す。

本題に戻って、EllipticMotionインスタンスの重ね順の処理を考えよう。インスタンスの仮想3次元の奥行きは、getIndexZ()メソッドで調べられた。したがって、配列 ⁠instances_array)に入ったインスタンスの順序を、インスタンスに対するgetIndexZ()メソッドの戻り値で並べ替える。そのための比較関数compare()の定義は、次のようになる。

function compare(a:EllipticMotion, b:EllipticMotion):Number {
  // 各インスタンスの仮想3次元の奥行きを調べる
  var nA:Number = a.getIndexZ();
  var nB:Number = b.getIndexZ();
  // 各インスタンスの奥行きに応じて並べ順を返す
  if (nA < nB) {
    return -1;
  } else if (nA > nB) {
    return 1;
  } else {
    return 0;
  }
}

仮想3次元の奥行きの順に配列(instances_array)内のEllipticMotionインスタンスを並べ替えても、まだ実際の表示上の重ね順には反映されない。 EllipticMotionインスタンスの表示リスト内の順序を、この配列内の新たな並べ順に合わせる必要がある。

表示リスト内におけるインスタンスのインデックスの位置を変えるのは、DisplayObjectContainer.setChildIndex()メソッドだ。つぎのシンタックスで、(DisplayObjectContainer)インスタンスの表示リストに含まれている子のDisplayObjectインスタンスを、指定したインデックスに移動する。

親インスタンス.setChildIndex(子インスタンス:DisplayObject, インデックス:int):void

ここでメインタイムラインのフレームアクションに、新たな関数 setOrder()を定義する。この関数は、まず前掲の比較関数compare()で配列instances_array内の EllipticMotionインスタンスを、仮想3次元の奥行きの順に並べ替える。つぎに、EllipticMotionインスタンスの表示リスト内の順序を、配列instances_arrayのエレメントの順序に合わせてDisplayObjectContainer.setChildIndex()メソッドにより設定している。

function setOrder():void {
  instances_array.sort(compare);   // 前掲比較関数で並べ替え
  var nLength:uint = instances_array.length;
  for (var i:int = 0; i<nLength; i++) {   // 並べ替えた配列内のインスタンスを順に取出し
    var _mc:EllipticMotion = instances_array[i];
    setChildIndex(_mc, i);   // 表示リスト内のインデックスを設定
  }
}

以上、2つの関数を追加したフレームアクションが、次のスクリプト2だ。なお、関数setOrder()の呼出しは、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)のリスナー関数rotate()の最後に加えている。

スクリプト2 インスタンスを仮想3次元の奥行きの順に並べ替える
// タイムライン: メイン
var myCenter:Point = new Point(stage.stageWidth / 2,stage.stageHeight / 2);
var myRadius:Point = new Point(100,50);
var nCount:uint = 6;
var nDegree:Number = 360 / nCount;
var instances_array:Array = new Array();
var deceleration:Number = 0.1;
for (var i:int = 0; i < nCount; i++) {
  var _mc:EllipticMotion = new EllipticMotion(nDegree * i,myCenter,myRadius);
  addChild(_mc);
  instances_array.push(_mc);
}
addEventListener(Event.ENTER_FRAME, rotate);
function rotate(EventObject:Event):void {
  var nSpeed:Number = (stage.mouseX - myCenter.x) * deceleration;
  var nLength:uint = instances_array.length;
  for (var i:int = 0; i < nLength; i++) {
    var _mc:EllipticMotion = instances_array[i];
    _mc.rotate(nSpeed);
  }
  setOrder();   // 追加: 関数呼出し
}
function setOrder():void {   // 追加定義
  instances_array.sort(compare);
  var nLength:uint = instances_array.length;
  for (var i:int = 0; i<nLength; i++) {
    var _mc:EllipticMotion = instances_array[i];
    setChildIndex(_mc, i);
  }
}
function compare(a:EllipticMotion, b:EllipticMotion):Number {   // 追加定義
  var nA:Number = a.getIndexZ();
  var nB:Number = b.getIndexZ();
  if (nA < nB) {
    return -1;
  } else if (nA < nB) {
    return 1;
  } else {
    return 0;
  }
}

このまま[ムービープレビュー]を確かめると、 EllipticMotionクラスのgetIndexZ()が「アクセスできないメソッド」であるという[コンパイルエラー]が表示される図4⁠。これは、フレームアクションに定義した関数compare()からインスタンスのgetIndexZ()メソッドを呼出している、次の2行のステートメントに対するエラーだ。

function compare(a:EllipticMotion, b:EllipticMotion):Number {
  var nA:Number = a.getIndexZ();
  var nB:Number = b.getIndexZ();
図4 メソッドにアクセスできない旨を伝える[コンパイルエラー]
図4 メソッドにアクセスできない旨を伝える[コンパイルエラー]

これは前掲スクリプト2でEllipticMotionクラスのrotate()メソッドについて、そのアクセス制御の属性を修正したのと同じ理由だ。スクリプト2では、getIndexZ()メソッドにはprivate属性が指定されている。しかし、タイムラインのフレームアクションからメソッドを呼出すには、アクセス制御の属性としてつぎのようにpublicを指定しなければならない。

// private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
  public function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
    var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
    return nIndexZ;
  }

スクリプト2に追加する修正はこの点のみなので、 EllipticMotionクラスの定義全体を再掲することは控えよう。サンプルファイルをダウンロードして確かめてほしい。今度は[ムービープレビュー]がエラーなく行え、インスタンスの重ね順もつねに仮想3次元の奥行きに合わせて表示される図5⁠。

図5 仮想3次元の奥行きに応じてインスタンスの重ね順が設定される
図5 仮想3次元の奥行きに応じてインスタンスの重ね順が設定される

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

おすすめ記事

記事・ニュース一覧