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

第23回クラスのデザインとループ処理

前回の第22回MovieClipシンボルにクラスを定義するでは、MovieClipシンボルに設定するクラスEllipticMotionを定義した図1⁠。シンボルに設定したクラスは、シンボルのグラフィックやタイムラインなどのアセットに関連づけられる。その結果、インスタンスは、シンボルのアセットをデータとしてもつことになる。

図1 [シンボルプロパティ]のダイアログボックスに[クラス]を設定
図1 [シンボルプロパティ]のダイアログボックスに[クラス]を設定

EllipticMotionインスタンスをタイムラインに配置すると、シンボルのグラフィックが表示され、楕円を描いて3D風にアニメーションした図2⁠。もっとも、前回のEllipticMotionクラスの処理内容は、以前の第17回3D風に回転するアニメーションで書いたフレームアクションに、クラスとして最小限の手を加えたものに過ぎない。

図2 シンボルのグラフィックが3D風に回転のアニメーションをする
図2 シンボルのグラフィックが3D風に回転のアニメーションをする

今回は、まずEllipticMotionクラスの処理について、今後の拡張まで考えてデザインを整理してみたい。その後、タイムラインに複数のインスタンスを配置するといった、繰返し処理のテクニックをご紹介しよう。

クラスのデザインを考える

クラスEllipticMotionのスクリプトには、大きく3つの修正を加える。第1に、インスタンスプロパティの整理だ。第2は、それらのプロパティの修正にメソッドを対応させ、さらにメソッド間の呼出しの構成についても手を入れたい。第3に、get/setアクセサメソッドをひと組追加する。

それでは、第1のプロパティの整理からいこう。まず、前回フレームアクションからは、つぎのようにコンストラクタメソッドの引数として、楕円軌道の中心座標やxy半径をPointインスタンスで渡した。したがって、EllipticMotionクラスでもこれらの値を数値でなく、Pointインスタンスのままプロパティとしてもつようにする。

var myCenter:Point = new Point(stage.stageWidth / 2, stage.stageHeight / 2);
var myRadius:Point = new Point(100, 50);
var _mc:EllipticMotion;
// コンストラクタの呼出し
_mc = new EllipticMotion(0, myCenter, myRadius);   // 中心座標とxy半径はPointインスタンスで渡す

つぎに、EllipticMotionクラスには角度のプロパティとして、度数とラジアンが併存していた。しかし、一方の値がわかれば、他方は変換比率から直ちに導ける。そのため、プロパティは度数のみとし、ラジアンは削除する[1]⁠。最後に、プロパティとして宣言してあった度数からラジアンへの変換比率は、constキーワードを使って「定数」とした。この「定数」の意義については、つぎのスクリプトを見てから説明しよう。

public class EllipticMotion extends MovieClip {
  private var degree:Number = 0;
  // private var radian:Number = 0;   // 削除
  private var speed:Number = 5;
  // private var degreeToRadian:Number = Math.PI / 180;
  private const DEGREE_TO_RADIAN:Number = Math.PI / 180;   // 定数として宣言
  // private var centerX:Number;
  // private var centerY:Number;
  private var center:Point;   // Pointクラスで宣言
  // private var radiusX:Number;
  // private var radiusY:Number;
  private var radius:Point;   // Pointクラスで宣言
  // private var cos:Number = Math.cos(radian);
  private var cos:Number = Math.cos(degree * DEGREE_TO_RADIAN);   // 度数をラジアンに変換
  // private var sin:Number = Math.sin(radian);
  private var sin:Number = Math.sin(degree * DEGREE_TO_RADIAN);   // 度数をラジアンに変換

さて、定数はvarでなくconstキーワードで宣言する。それ以外は、ひとつの点を除いて、プロパティと異なることろはない。その違いというのは、⁠定数」という名前のとおり、設定した値が変更できないことだ。円周率πはギリシャの昔から変わらない。したがって、その値を使った比率は変えないし、変えられない方がよい。そのような場合に、定数を宣言する。ちなみに、プログラミングでは、定数名はすべて大文字にする習慣がある[2]⁠。

const 定数名:型指定   // 定数名はすべて大文字が習慣

EllipticMotionクラスのデザインについて、変更点の第2はメソッドだった。第1のプロパティの整理にともなう修正を、先に済ませておこう。とくに難しいものはない。楕円軌道の中心座標とxy半径をPoint型のプロパティに変えたことと、ラジアン値を定数から計算することに対応させた。

public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
  degree = nDegree;
  // centerX = myCenter.x;
  // centerY = myCenter.y;
  center = myCenter;   // プロパティにPointインスタンスを設定
  // radiusX = myRadius.x;
  // radiusY = myRadius.y;
  radius = myRadius;   // プロパティにPointインスタンスを設定
  addEventListener(Event.ENTER_FRAME, moveX);
  addEventListener(Event.ENTER_FRAME, moveY);
  addEventListener(Event.ENTER_FRAME, scale);
  addEventListener(Event.ENTER_FRAME, blur);
  addEventListener(Event.ENTER_FRAME, update);
}
private function moveX(eventObject:Event):void {
  // x = centerX + cos * radiusX;
  x = center.x + cos * radius.x;   // Pointインスタンスから値を取得
}
private function moveY(eventObject:Event):void {
  // y = centerY + sin * radiusY;
  y = center.y + sin * radius.y;   // Pointインスタンスから値を取得
}
private function update(eventObject:Event):void {
  degree+=speed;
  degree = (degree%360+360)%360;
  // radian=degree*degreeToRadian;
  var radian:Number=degree*DEGREE_TO_RADIAN;   // ラジアン値を定数で計算
  cos=Math.cos(radian);
  sin=Math.sin(radian);
}

それでは、メソッドの構成も変えていこう。修正のポイントは、以下のようにメソッドrotate()の定義を加えたことだ。DisplayObject.enterFrameイベントのリスナーとしてはこのrotate()メソッドのみを登録し、他のアニメーションのためのメソッドはイベントリスナーからは外した。 rotate()メソッド本体からは、これも新たに定義したsetRotation()メソッドを呼出して、そこからmoveX()、moveY()、 scale()、blur()の各メソッドを実行している。

public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
  degree = nDegree;
  center = myCenter;
  radius = myRadius;
  // addEventListener(Event.ENTER_FRAME, moveX);
  // addEventListener(Event.ENTER_FRAME, moveY);
  // addEventListener(Event.ENTER_FRAME, scale);
  // addEventListener(Event.ENTER_FRAME, blur);
  // addEventListener(Event.ENTER_FRAME, update);
  addEventListener(Event.ENTER_FRAME, rotate);   // 追加
}
private function rotate(eventObject:Event):void {   // 追加定義
  degree += speed;
  update();
  setRotation();
}
// private function update(eventObject:Event):void {
private function update():void {   // 引数削除
  // degree += speed;   // rotate()メソッドに処理を移す
  degree = (degree%360+360)%360;
  var radian:Number=degree*DEGREE_TO_RADIAN;
  cos=Math.cos(radian);
  sin=Math.sin(radian);
}
function setRotation():void {   // 追加定義
  moveX();
  moveY();
  scale();
  blur();
}

ただし、メソッドupdate()の呼出しは、setRotation()の中に含めず、rotate()メソッドから行った。また、角度のプロパティ値を加算する処理は、メソッドupdate()からrotate()に取出している。この理由は、クラスEllipticMotionの動作を確かめてから説明することにしよう。なお、イベントリスナーから外したメソッドには引数のイベントオブジェクトが渡されないので、その修正も必要になる。

// private function moveX(eventObject:Event):void {
private function moveX():void {   // 引数削除
  // 処理は省略
}
// private function moveY(eventObject:Event):void {
private function moveY():void {   // 引数削除
  // 処理は省略
}
// private function scale(eventObject:Event):void {
private function scale():void {   // 引数削除
  // 処理は省略
}
// private function blur(eventObject:Event):void {
private function blur():void {   // 引数削除
  // 処理は省略
}

以上のとおり、EllipticMotionクラスのプロパティを整理し、メソッドのデザイン変更を行ったのが、以下のスクリプト1だ。結果を確かめてみよう。クラスを設定したシンボルが格納されたFlashムービー(FLA)ファイルには、前回と同じフレームアクションを記述すればよい。4つのインスタンスが生成され、90度間隔で楕円軌道を描いてアニメーションする図3⁠。

var myCenter:Point = new Point(stage.stageWidth / 2, stage.stageHeight / 2);
var myRadius:Point = new Point(100, 50);
var _mc:EllipticMotion;
_mc = new EllipticMotion(0, myCenter, myRadius);
addChild(_mc);
_mc = new EllipticMotion(90, myCenter, myRadius);
addChild(_mc);
_mc = new EllipticMotion(180, myCenter, myRadius);
addChild(_mc);
_mc = new EllipticMotion(270, myCenter, myRadius);
addChild(_mc);
スクリプト1 プロパティの整理とメソッドのデザイン変更を行ったEllipticMotionクラスの定義
// 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 var speed:Number = 5;
    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;
      addEventListener(Event.ENTER_FRAME, rotate);
      update();   // 追加
      setRotation();   // 追加
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      update();
      setRotation();
    }
    private function update():void {
      degree = (degree%360+360)%360;
      var radian:Number=degree*DEGREE_TO_RADIAN;
      cos=Math.cos(radian);
      sin=Math.sin(radian);
    }
    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 4つのインスタンスが動的に配置されて回転する
図3 4つのインスタンスが動的に配置されて回転する

上記スクリプト1には、コンストラクタEllipticMotion()のメソッド本体最後に、これまで説明しなかった2行のステートメントが新たに加わっている。この処理の目的を知るには、rotate()メソッドのイベントリスナーへの登録も含めた3行をコメントアウトして[ムービープレビュー]してみるとよい図4⁠。

図4 イベントリスナーの登録と追加された2行とをコメントアウト
図4 イベントリスナーの登録と追加された2行とをコメントアウト

生成されたインスタンスが、タイムラインの基準点の座標(0, 0)つまりメインの左上隅に表示されてしまう。もちろん、イベントリスナーが登録されていればすぐにrotate()メソッドが呼出され、インスタンスは楕円軌道に配置されて回転のアニメーションに入る。しかし、環境によってはイベント発生までの一瞬、左上隅にインスタンスが見えてしまうかもしれない。

それを避けるには、リスナー関数の呼出しを待たず、コンストラクタからインスタンス配置のメソッドsetRotation()を実行しなければならない。そして、インスタンスを初期位置に表示するためには、角度は変えず(つまりプロパティ値は加算せず)にupdate()メソッドで三角関数値を計算する必要があるのだ。これが、角度のプロパティ値加算をrotate()メソッドに移した理由である。

get/setアクセサメソッドで関連する値も更新する

ところで、メソッドupdate()はsetRotation()の中から呼出してもよくはないだろうか。それでも、特に不都合は起こらない。しかし、理屈としてupdate()メソッドの処理は、いつ行うのがよいか考えてみよう。

メソッドが具体的にやっているのは、角度の値にともなう計算だ。そうだとすれば、角度を変えたときに行うのが一番よい。そこで、 EllipticMotionクラスの第3の修正として、角度の取得・設定をget/setアクセサメソッドとし、update()はsetアクセサメソッドに書替えてしまうことにする。

public class EllipticMotion extends MovieClip {
  // private var degree:Number = 0;
  private var _degree:Number = 0;   // プロパティ名変更
  // ...[中略]...
  private function get degree():Number {   // 追加定義
    return _degree;
  }
  // private function update():void {   // setアクセサメソッドに変更
  private function set degree(nDegree:Number):void {
    // degree = (degree%360+360)%360;
    _degree = (nDegree%360+360)%360;
    // var radian:Number=degree*DEGREE_TO_RADIAN;
    var radian:Number = _degree * DEGREE_TO_RADIAN;
    cos = Math.cos(radian);
    sin = Math.sin(radian);
  }

get/setアクセサメソッドを定義したので、メソッドupdate()の呼出しはコンストラクタおよびrotate()メソッドから削除する。

public function EllipticMotion(nDegree:Number, myCenter:Point, myRadius:Point) {
  degree = nDegree;
  center = myCenter;
  radius = myRadius;
  addEventListener(Event.ENTER_FRAME, rotate);
  // update();
  setRotation();
}
private function rotate(eventObject:Event):void {
  degree += speed;
  // update();
  setRotation();
}

これで、角度が変更されると同時に、角度に関わる演算が必ず処理されるようになった。今回のEllipticMotionクラスの修正はここまでだスクリプト2⁠。インスタンスのアニメーションについては、とくに変わりはない。インスタンスの重ね順が気になるのは、次回に対処する図5⁠。

スクリプト2 角度をget/setアクセサメソッドで定義
// 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 var speed:Number = 5;
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;
  addEventListener(Event.ENTER_FRAME, rotate);
  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);
}
private function rotate(eventObject:Event):void {
  degree += speed;
  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;
}
  }
}
図5 インスタンスの重ね順が管理されていない
図5 インスタンスの重ね順が管理されていない

ループ処理で複数のインスタンスを生成する

今回の残る課題は、フレームアクションで複数のインスタンスを生成する処理だ。同じようなパターンのステートメントを必要な分だけ繰返したいとき、ループ処理という構文を用いる。具体的には、forステートメントを使ってみることにしよう。シンタックスは、つぎのとおりである。

for (初期設定; 継続条件; 更新処理) {
   // 繰返し処理
}

第1の初期設定は、繰返し処理を始める前に行っておく設定だ。多くの場合、処理回数を数えるカウンタ変数の値が設定される。第2は、繰返し処理を継続する条件である。終了条件ではないことに注意が必要だ。また、誤ってfalseにならない条件を指定すると、無限ループに陥ってしまう。第3は、ひとつのループごとに行われる更新処理を記述する。カウンタ変数を使ったときは、その増減の処理(カウントアップ/ダウン)が定番だ。

それでは、Flashムービー(FLA)ファイルのフレームアクションを、forループによる処理に書直してみよう。作成するEllipticMotionインスタンスの数も、6つに増やすことにするスクリプト3⁠。

スクリプト3 forループでEllipticMotionインスタンス6つを等間隔に配置する
// タイムライン: メイン
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;
for (var i:int = 0; i < nCount; i++) {
  var _mc:EllipticMotion = new EllipticMotion(nDegree * i, myCenter, myRadius);
  addChild(_mc);
}

forステートメントでは、第1に初期設定として、カウンタ変数iを整数(int型)で宣言した。第2の継続条件は、カウンタ変数値が変数nCountの値6未満の間だ。第3に更新処理で、カウンタ変数を1ずつカウントアップする。++はインクリメントの演算子で、⁠+= 1」と同じ処理になる[3]⁠。カウンタ変数iの初期値は0なので、ループ処理は都合6回行われる。

forステートメントのコードブロック{}内では、インスタンスを作成して、それを表示リストに加えている。コンストラクタEllipticMotion()の第1引数に渡す角度は、カウンタ変数iを掛け算して、均等間隔に配置した。なお、コードブロックの中で変数_mcをvar宣言しても、初期化は1度行われるだけなので、エラーになることはない。[ムービープレビュー]を確かめると、6つのインスタンスが楕円軌道に配置されて回転する図6⁠。

図6 6つのインスタンスが楕円軌道を描いてアニメーションする
図6 6つのインスタンスが楕円軌道を描いてアニメーションする

次回は、インスタンスの重ね順を正しく設定するとともに、アニメーションの回転する速さをマウスポインタの位置に応じて変えてみたい。

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

おすすめ記事

記事・ニュース一覧