サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash CS3 Professional ActionScript 3.0

□10 イベントの配信とオーバーライド/インターフェイス

前章09「MovieClipシンボルにクラスを定義する」で作成したクラスEllipticMotionを、さらに発展させていきます。クラスをさらにいくつか加えて、インスタンスをマウスに対してインタラクティブにアニメーションさせましょう。そして、数多くのクラスを管理するための仕組みや、異なるクラスのインスタンス間で行うコミュニケーションの処理、あるいはクラスの備えるべき機能を指定する方法などについて学びます。

10-01 パッケージを使う
ActionScript 3.0の多くのクラスは、パッケージに属していました。カスタムクラスも、パッケージを使うことができます。そこで、前章で作成したクラスEllipticMotionを、新たにパッケージに加えてみます。

●クラスをパッケージに加える
前述(Tips 08-002)のとおり、「パッケージはクラスをグループ分けする仕組み」でした。大規模な開発では、スクリプトに携わる人数も、作成されるクラスの数も多くなります。プロジェクトやその中の機能分担などによってパッケージを分ければ、開発の管理がしやすいでしょう。また、他のプロジェクトにも使える汎用的なクラスをライブラリ化しようという場合も、機能によって分類管理できると便利です。

パッケージを使うもうひとつの理由は、クラスの名前が重複するのを避けられるということです。完全修飾名が同じクラスを、同時に使うことはできません。パッケージを利用すれば、仮に同じ名前のクラスが定義されたとしても、パッケージまで含めた完全修飾クラス名は重複しません。したがって、各パッケージの中さえクラス名がかぶらないようにしておけば、それらのクラスは同じプロジェクト内で問題なく用いることができるのです。

【パッケージを使う目的】
  1. クラスを分類して管理する。
  2. クラス名がぶつからないようにする。


パッケージで分類管理し、同姓同名をつくらない。

パッケージをつくるには、(1)クラス定義にパッケージを指定し、(2)ActionScript(AS)ファイルの保存場所をバッケージに合わせます。前章09「MovieClipシンボルにクラスを定義する」で作成したクラスEllipticMotionを、バッケージrotationに組込んでみましょう。なお、パッケージ名は、もちろん識別子で設定します(小文字が通例)。

第1のクラス定義に対するパッケージの指定は、package定義キーワードの後にパッケージ名を記述して行います。したがって、前掲クラスEllipticMotion(スクリプト09-010)のpackageキーワードの後に、つぎのようにパッケージ名rotationを加えます(図10-001)。

// package {
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
図10-001■クラス定義のpackageの後にパッケージ名を記述する
パッケージ名は、小文字の識別子で指定する。

第2に、パッケージを指定したActionScript(AS)ファイルは、パッケージと同名のフォルダに納める必要があります。そして、そのフォルダの保存場所は、「クラスパス」です。「クラスパス」は、FlashがSWFを書出すとき、クラスの定義されたActionScript(AS)ファイルを探す、ディスク内の定められた場所になります。もっとも、そのクラスパスのひとつは、もうすでに使っています。

第08章「カスタムクラスを定義する」から、ActionScript 3.0クラス定義ファイルは、そのクラスを使うFlashムービー(FLA)ファイルと同じ場所に保存しました。それでクラスが問題なく働いたのは、Flashムービー(FLA)ファイルの格納場所がデフォルトではクラスパスとして登録されており、同じ場所のActionScript(AS)ファイルに定義されたクラスがSWFに正しく書出されたからです。

つまり、パッケージのフォルダも、FLAファイルと同じ場所に保存すればよいのです。そこで、パッケージ名をつけたフォルダroationは、クラスを使うFlashムービー(FLA)ファイルと同じ場所につくります。そして、パッケージに含めるクラスEllipticMotionが定義されたActionScript(AS)ファイルは、このrotationフォルダ内に移します(図10-002)。

図10-002■クラスパスのパッケージと同名のフォルダにパッケージに含めるクラス定義ファイルを保存
パッケージの名前をつけたフォルダはクラスパスに配置し、パッケージに含めるクラス定義ファイルを納める。
【パッケージの作成】
  1. クラス定義のpackageにパッケージ名を指定する。
  2. クラスパスに配置したパッケージと同名のフォルダにクラス定義ファイルを格納する。

●パッケージに加えたカスタムクラスを使う
EllipticMotionクラスをパッケージrotationに加えましたので、そのクラスを使うFlashムービーの側にも修正が必要になります。まず、前章で作成したフレームアクション(前掲スクリプト09-008)です。以下のように、EllipticMotionクラスの完全修飾名を、import宣言する必要があります。そうすれば、後はクラス名のみでEllipticMotionクラスが参照できますので、以降のステートメントには変更はありません(図10-003)。

import rotation.EllipticMotion;

図10-003■パッケージのクラスを使うフレームアクションはimport宣言する必要がある
冒頭でimport宣言さえすれば、以降のクラスへのアクセスは変更の必要はない。

Tips 10-001■import宣言はフルパス
import宣言は完全修飾クラス名、つまりフルパスで行います(Maniac! 09-001「ディレクティブおよび完全修飾名という用語つにいて」参照)。相対パスの指定ではありませんから、パッケージのクラスを使うファイル(Flashムービー(FLA)ファイルや別のクラスのActionScript(AS)ファイル)がどのパッケージに属しても、importステートメントの記述は変わりません。

ただし、使うクラスが同じパッケージ内にあるときは、import宣言する必要はありません。


AS1&2 Note 10-001■パッケージのクラスにimport宣言は必須
ActionScript 2.0では、パッケージのクラスはimport宣言をしなくても、完全修飾クラス名で記述すれば、そのクラスにアクセスすることができました。たとえば、ActionScript 2.0でつぎのステートメントは、import宣言がなくても、エラーなくBlurFilterインスタンスを生成します。

var myFilter:flash.filters.BlurFilter = new flash.filters.BlurFilter();

しかし、ActionScript 3.0では、flash.filtersパッケージかBlurFilterクラスを完全修飾名でimportしなければ、コンパイルエラーになります。ただし、フレームアクションでは、flash.filtersパッケージはFlashが自動的にimportするので正しく動作します(Tips 09-002「タイムラインで自動的にimport宣言されるクラス」参照)。詳しくは、[ヘルプ]の[ActionScript 3.0 のプログラミング] > [ActionScript 言語とシンタックス] > [パッケージと名前空間] > [パッケージ]をお読みください。

もうひとつ忘れていけないのは、EllipticMotionクラスは[ライブラリ]のMovieClipシンボルに[クラス]として設定していたことです。このクラスの指定も、変更しなければなりません。[ライブラリ]のMovieClipシンボルを選んで[リンケージプロパティ]ダイアログボックスを開いたら、[クラス]のフィールドには完全修飾クラス名のrotation.EllipticMotionを入力します(図10-004)。

図10-004■[リンケージプロパティ]ダイアログボックスで[クラス]に完全修飾クラス名を入力
完全修飾クラス名rotation.EllipticMotionを、[クラス]フィールドに入力し直す。

これで、パッケージrotationに定義し直したクラスEllipticMotionが、MovieClipシンボルに対して正しく設定されました。[ムービープレビュー]を確かめると、前章と同じようにMovieClipインスタンスが、楕円軌道を描いて3D風にアニメーョンします(図10-005)。

図10-005■パッケージに加えたクラスをMovieClipシンボルの[クラス]に設定

[*編集者用注釈] ActionScript30_09_029.pngを流用。
パッケージrotationに定義し直したクラスEllipticMotionにより、前章と同じようにインスタンスが楕円軌道を描いて3D風にアニメーョンする。

●クラスパスとパッケージ
Flashムービーに利用するクラスやパッケージをFLAファイルと一緒に保存しておくことは、小さなプロジェクト単位でまとめるには便利です。しかし、規模の大きなプロジェクトで管理したり、他のプロジェクトにも使い回せる汎用的なクラスをライブラリとして開発する場合には、保存場所は別にしておきたいことが多いでしょう。そのようなときには、クラスパスを追加することができます。

[環境設定]ダイアログボックス(Windowsは[編集]、Machintoshは[Flash]メニュー)を開き、[カテゴリ]リストから[ActionScript]を選んで、[ActionScript 3.0設定]のボタンをクリックします。すると、[ActionScript 3.0設定]ダイアログボックスが開いて、[クラスパス]のリストが表示されます(図10-006)。

図10-006■[環境設定]ダイアログボックスの[ActionScript]から[ActionScript 3.0設定]ダイアログボックスを開く
[ActionScript 3.0設定]ダイアログボックスには、[クラスパス]のリストが表示される。

クラスパスを追加するには、[ActionScript 3.0設定]ダイアログボックスの[パスの参照]ボタンをクリックします(図10-006)。フォルダを選択するためのダイアログボックスが開きますので、クラスパスに設定したいフォルダを指定します。[ActionScript 3.0設定]ダイアログボックスの[+](新規パスの追加)ボタンをクリックすると、パスを直接手入力することも可能です。パスの指定を間違えたときや、過去に加えたクラスパスが不要になったときは、[-](選択したパスの削除)ボタンで削除できます。

Tips 10-002■デフォルトのクラスパスは削除しない
[ActionScript 3.0設定]ダイアログボックスの[クラスパス]リストには、デフォルトでは「.」(ドット)と「$(AppConfig)/ActionScript 3.0/Classes」のふたつが設定されています。これらのパスは、削除しないでください。ActionScriptの定義済みクラスやカスタムクラスに、Flashがアクセスできなくなります。


Tips 10-003■グローバルクラスパスとドキュメントレベルクラスパス
本文で説明した[環境設定]で指定するクラスパスは、「グローバルクラスパス」と呼ばれます。Flash CS3アプリケーションに対して、クラスパスが設定されます。

もうひとつ、Flashムービー(FLA)ファイルごとにクラスパスを追加できる「ドキュメントレベルクラスパス」があります。ドキュメントレベルクラスパスを指定するには、[パブリッシュ設定]ダイアログボックスの[Flash]タブで、[ActionScriptのバージョン]ポップアップメニューの隣にある[設定]ボタンををクリックして、[ActionScript 3.0設定]ダイアログボックスを開きます。[クラスパス]の追加の仕方は、グローバルクラスパスの場合と基本的に同じです。

もっとも、Flashムービー(FLA)ファイルと一緒に管理したいクラスやパッケージは、グローバルクラスパスであるFLAファイルと同じ場所に保存すれば通常は足りるでしょう。


Maniac! 10-001■デフォルトのふたつのクラスパス
[ActionScript 3.0設定]ダイアログボックスの[クラスパス]リストにデフォルトで最初に設定されている「.」(ドット)は、Flashムービー(FLA)ファイルと同じ場所を意味します。デフォルトのふたつ目のクラスパス「$(AppConfig)/ActionScript 3.0/Classes」先頭の「$(AppConfig)」は、Flash CS3アプリケーションのConfigurationフォルダを示します。Configurationフォルダのパスは、通常下表10-001ののとおりです。

表10-001■Flash CS3アプリケーションのConfigurationフォルダ
オペレーティングシステム Configurationフォルダのパス
Windows(Vista/XP/2000) C:\Program Files\Adobe\Adobe Flash CS3\ja\Configuration\
Mac OS X Macintosh HD:アプリケーション:Adobe Flash CS3:Configuration:

パッケージは、階層化することができます。たとえば、MovieClipクラスの完全修飾名は、flash.display.MovieClipでした。パッケージflashの中にはdisplay以外にも、eventsやfilters geomといったパッケージが含まれます。MovieClipクラスはその中のdisplayに属しているということになります。

階層化したパッケージに加えたクラスのActionScript(AS)ファイルは、パッケージ名のついたフォルダを入れ子にして、その中に保存します。たとえば、デフォルトのクラスパスのひとつ($(AppConfig)/ActionScript 3.0/Classes)には、flというフォルダがあり、その中のサブフォルダmotionには、さらにサブフォルダeasingが含まれ、いくつかのASファイルが納められています(図10-007左図)。

他方、[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド]を見ると、fl.motion.easingパッケージにクラスが定義されています(図10-007右図)。それらのクラス名は、上述のフォルダeasing内にあるASファイルの名前と一致しています。パッケージは、ActionScriptの中でクラスを分類・管理するだけでなく、ディスク内のActionScript(AS)ファイルを整理することにもなるのです。

図10-007■フォルダ分けされたASファイルと階層化されたパッケージ
デフォルトのクラスパス($(AppConfig)/ActionScript 3.0/Classes)にあるflフォルダ内の階層構造(左図)と、[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド]におけるfl.motion.easingパッケージの項(右図)。

Tips 10-004■世界にひとつだけのパッケージ
汎用的なクラスやライブラリは、広く一般に公開されることもあります。自作のクラスを公開したり、あるいは公開されているライブラリを利用しようとするとき、名前がぶつからないように気をつけなければなりません。

ネットは世界に開かれています。ですから、世界中の誰ともかぶらない、世界にひとつだけのパッケージ名がほしいです。私たちになじみの深い「世界にひとつだけの名前」には、ドメイン名があります。そこで、Javaではドメイン名を逆順にしてパッケージ名とすることが習いとなっています。

たとえば、筆者のドメインはfumiononaka.comです。したがって、逆順のcom.fumiononakaをパッケージとし、その中に自作のパッケージやクラスを構築していくということです。そうすれば、完全修飾クラス名は、世界中の誰とも重複することはありません。なお、ドメイン名を逆順にするのは、パッケージの階層は一般的・抽象的なものから個別的・具体的なものへという順序になるべきだからです。

ただし、本文はあくまでクラス作成の練習ですので、簡略にするため、あえてドメイン名のパッケージは使わないことにします。

[*筆者用参考] WEBワークショップ: 初心者のためのJava講座 「パッケージについて理解する」、Javaプログラミング・ワンポイントレクチャー「パッケージを理解し利用する」、@IT:Eclipseではじめるプログラミング「Javaのパッケージを理解する」、基本情報技術者Web学習室Java「パッケージとは」。

10-02 回転するインスタンスをコントロールするクラス
rotationパッケージに、クラスをもうひとつ新たに加えます。回転するEllipticMotionインスタンスをコントロールするクラスです。これまで楕円軌道の中心座標やx軸・y軸方向の半径はEllipticMotionインスタンスがそれぞれ保持し、DisplayObject.enterFrameイベントも個別に受取って、自らの位置や重ね順などを制御してきました。けれど、それらの値や処理の中には、まとめて管理した方がよいものもあります。そのためのクラスRotationControllerを定義します。

クラスの備えるべき仕様
新たなクラスRotationControllerを、まずは必要な最小限の機能のみで定義します。インスタンスメソッドとしては、コンストラクタのほかにふたつ用意します。

第1は、楕円軌道でアニメーションする複数のEllipticMotionインスタンスを、RotationControllerインスタンスに保持させるためのaddListener()メソッドです。メソッドの引数には、EllipticMotionインスタンスを渡します。その結果、RotationControllerインスタンスは、複数のEllipticMotionインスタンスをまとめて管理できるようになります。

第2は、保持しているEllipticMotionインスタンスを均等の角度に配置して、アニメーションを開始するためのメソッドstartRotation()メソッドです。このメソッドには、引数はありません。

最後に、コンストラクタメソッドには、3つの引数を渡すことにします。第1引数は、EllipticMotionインスタンスを配置して、ステージ上に表示するためのタイムラインです。RotationControllerクラスはフレームアクションと異なり、Stageを頂点とする表示リストには含まれませんので、アニメーションを表示するにはタイムライン(DisplayObjectContainerインスタンス)の参照が必要となります。第2引数と第3引数は、いずれもPointインスタンスで、それぞれ楕円軌道の中心座標とx軸・y軸方向の半径を指定することにします。

以上からクラスRotationControllerの備えるべきプロパティやメソッドは、つぎのような構成になるでしょう。import宣言やプロパティの初期値、メソッドの処理内容などは、あとから詰めていきます。

package rotation{
  public class RotationController {
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array;
    public function RotationController(my_mc:MovieClip, myCenter:Point, myRadius:Point) {}
    public function addListener(my_mc:EllipticMotion):uint {}
    public function startRotation():void {}
  }
}

private属性を指定した4つのインスタンスプロパティのうち、timeline_mcとcenter、radiusの3つは、コンストラクタが受取る3つの引数、タイムラインと楕円軌道の中心座標、およびx軸・y軸半径を設定します。4つ目のinstances_arrayは、addListener()メソッドに引数として渡されるEllipticMotionインスタンスを納める配列です。

メソッドは、Flashムービーのタイムラインから呼出しますので、アクセス制御の属性はpublicにします。インスタンスメソッドaddListener()は、戻り値として正の整数uintが指定されています。このメソッドは、プロパティとして宣言した配列instances_arrayにEllipticMotionインスタンスを加えるので、Array.push()メソッドと同じく、追加後のインスタンスの数を返すことにしたのです。もうひとつのインスタンスメソッドstartRotation()は、戻り値がvoidで、値を返しません。

つぎに、これらのメソッドをタイムラインからどう呼出すか考えましょう。関数やメソッドを定義するには、どう使いたいかを考え、「夢をかたちに」していくと作業が進めやすいからです(03-06「数値を指定桁数の文字列で返す関数」参照)。Flashムービーファイルのフレームアクションからは、たとえばつぎのようにRotationControllerインスタンスを生成し、そのメソッドを呼出すことになります(スクリプト10-001)。

スクリプト10-001■RotationControllerクラスを使うフレームアクション

// フレームアクション
import flash.geom.Point;
import flash.display.MovieClip;
import rotation.EllipticMotion;
import rotation.RotationController;
var nCount:int = 6;
var center:Point = new Point(stage.stageWidth/2, stage.stageHeight/2);
var radius:Point = new Point(120, 50);
var myRotation:RotationController = new RotationController(this, center, radius);
for (var i:int = 0; i<nCount; i++) {
  myRotation.addListener(new EllipticMotion());
}
myRotation.startRotation();

まず、RotationControllerインスタンスは、コンストラクタにタイムラインおよび楕円軌道の中心座標とx軸・y軸半径を渡して生成します。つぎに、forループによる繰返し処理で、EllipticMotionインスタンスを必要な個数だけRotationControllerインスタンスにaddListener()メソッドで追加します。そして最後に、startRotation()メソッドにより、EllipticMotionインスタンスを等間隔(角度)に配置して、楕円軌道のアニメーションを開始します。

なお、クラスEllipticMotionとRotationControllerはパッケージrotationに定義しましたので、これらのクラスは完全修飾名をimport宣言してあります。そこで、自動的にimportが行われるActionScript定義済みのクラス(PointとMovieClip)も、明示的にimport宣言を加えることにしました(Tips 09-002「タイムラインで自動的にimport宣言されるクラス」参照)。こうしておくと、このフレームアクションをさらにクラスとして定義し直そうといった場合にあわてずに済みます。

RotationControllerクラスの定義
前述の仕様に沿って、RotationControllerクラスを定義してみることにしましょう。基本的には、これまでの知識で作成できる内容です。スクリプトは、つぎのようになります(スクリプト10-002)。

スクリプト10-002■RotationControllerクラス

// ActionScript 3.0クラス定義ファイル: RotationController.as
package rotation{
  import flash.display.MovieClip;
  import flash.geom.Point;
  import flash.events.Event;
  public class RotationController {
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array = new Array();
    public function RotationController(my_mc:MovieClip, myCenter:Point=null, myRadius:Point=null) {
      timeline_mc = my_mc;
      if (myCenter is Point) {
        center = myCenter;
      } else {
        center = new Point(my_mc.stage.stageWidth/2, my_mc.stage.stageHeight/2);
      }
      if (myRadius is Point) {
        radius = myRadius;
      } else {
        radius = center.clone();
        radius.normalize(radius.length*0.7);
      }
    }
    public function addListener(my_mc:EllipticMotion):uint {
      var nInstances:uint = instances_array.push(my_mc);
      return nInstances;
    }
    public function startRotation():void {
      var nInstances:uint = instances_array.length;
      var nDegree:Number = 360/nInstances;
      var my_mc:EllipticMotion;
      for (var i:int = 0; i<nInstances; i++) {
        my_mc = instances_array[i];
        timeline_mc.addChild(my_mc);
        my_mc.setPosition(i*nDegree, center, radius);
      }
    }
  }
}

第1に、addListener()メソッドは、引数にEllipticMotionインスタンスを受取り、それをArray型のプロパティinstances_arrayにArray.push()メソッドでエレメントとして加えます。そのために、instances_arrayプロパティには、var宣言のときに初期値として、空のArrayインスタンスが設定されています。Array.push()メソッドから返されたインスタンスの総数(配列の長さ)は、addListener()メソッドの戻り値になります。

第2に、startRotation()メソッドは、前章09-04「複数のインスタンスを配置する ー forステートメント」のフレームアクション(スクリプト09-008)における後半のループ処理とほぼ同じ内容です。細かい点として、スクリプト09-008とは異なり、インスタンス間の間隔(角度)を、forループの外でローカル変数(nDegree)に格納しています。これは、ループ処理の中で何度も同じ計算をすると、無駄が生じるからです。なお、DisplayObjectContainer.addChild()メソッドのターゲットとされるプロパティtimeline_mcの値は、前述のとおりコンストラクタを呼出すときに引数として渡されるタイムラインです。

第3に、コンストラクタメソッドは、本来以下のような処理が骨格です。上記スクリプト10-002ではこれにまず、第2および第3引数をオプションとするため、前に説明したスクリプト09-007と同じように、デフォルト値nullを与えました。そしてさらに、nullの場合にif条件に対するelseステートメントで、各プロパティに対して独自の初期値を設定しています。

public function RotationController(my_mc:MovieClip, myCenter:Point, myRadius:Point) {
  timeline_mc = my_mc;
  center = myCenter;
  radius = myRadius;
}

まず、centerプロパティに設定する第2引数の中心座標(myCenter)が指定されておらず、nullの場合には、ステージ中央の座標をPointインスタンスとして代入していてます。つぎに、radiusプロパティに設定する第3引数のx軸・y軸方向の半径(myRadius)もnullのときは、楕円軌道の幅と高さがともにステージの0.7(70%)の割合になるように、Pointインスタンスの操作を行います。その際に、Pointクラスのプロパティやメソッドを用いていますので、その説明をしましょう。

Pointクラスは、単にxy座標を保持するだけでなく、インスタンスを原点(0, 0)からその座標(x, y)までのベクトルとして扱うことができます(図10-008)。今回利用したのは、下表10-002に掲げたPointクラスのプロパティひとつと、メソッドがふたつです。

図10-008■Pointインスタンスは原点からxy座標までのベクトル
メインタイムラインでは、ステージ左上隅(0, 0)からPointインスタンスの座標(x, y)までのベクトルとして扱われる。

表10-002■Pointクラスのプロパティとメソッド(今回使ったもの)
プロパティ:データ型
length:Number [読取り専用]Pointインスタンスのxy座標値を、原点(0, 0)からのベクトルとみなして、その長さを数値で返します。

メソッド 説明
clone():Point 参照したPointインスタンスと同じxy座標値をもつ、新たなPointインスタンスを複製して返します。
normalize(長さ:Number):void Pointインスタンスのxy座標値を、原点(0, 0)からのベクトルとみなして、その長さを引数値の値に変更(伸縮)します。

前掲スクリプト10-002では、インスタンスプロパティcenterに設定されたPointインスタンスをもとに、同じくインスタンスプロパティradiusに与えるPointインスタンスを求めています。

しかし、Pointインスタンスは「参照渡し」(Column 06「値と参照」を参照)ですので、centerプロパティのPointインスタンスを直接参照して操作すれば、centerの値そのものが書き替わってしまいます。そこで、Point.clone()メソッドを用いると、参照したPointインスタンスと同じxy座標値をもつ新たなPointインスタンスが複製して返されます。

複製されたPontインスタンスをプロパティradiusに納めたうえで、そのベクトルの長さが現在の0.7(70%)の割合になるようにxy座標値を変更します。そのときに使うのが、Point.normalize()メソッドです。引数には、変更後のベクトルの長さを渡します。ベクトルの現在の長さはPoint.lengthプロパティで得られますので、スクリプト10-002ではその値に0.7を乗じて引数に指定しています。

Tips 10-005■Point.normalize()メソッドを使う理由
Pointインスタンスのベクトルとしての長さ(Point.lengthプロパティの値)を0.7(70%)の割合にするというのは、インスタンスのxy各座標値に0.7を掛合わせるのと同じ結果です。したがって、プロパティradiusの値の設定は、つぎのように記述することもできます。

if (myRadius is Point) {
  radius = myRadius;
} else {
  radius = new Point(center.x*0.7, center.y*0.7);
}

今回は、Pointインスタンスのベクトルとしての扱いをご紹介するために、Point.normalize()メソッドを始めとする前掲表10-002のプロパティとメソッドを使いました。上記のようなif/elseステートメントの処理にしても、とくに問題はありません。


Maniac! 10-002■radiusプロパティのデフォルト値
RotationControllerのコンストラクタに第3引数が与えられなかったとき、radiusプロパティをcenterプロパティの0.7(70%)の割合に設定するというのは、第2引数も指定されずにcenterのxy座標がステージ中央であることを想定しています。しかし、第2引数が与えられ、しかもその座標値がステージ中央より大きな値であった場合には、0.7を乗じたPointインスタンスの値が必ずしも適切とはいえないでしょう。ただ、練習用のサンプルとしては、そこまで例外処理に深くは踏込みません。

クラスEllipticMotionについては、前節10-01「パッケージを使う」でパッケージをrotationにしたほかは、前章スクリプト09-010から変更はありません。ただ、参照の便のため、packageをrotationに指定したEllipticMotionクラスの定義をつぎに掲げておきます(スクリプト10-003)。

スクリプト10-003■パッケージをrotationに変更したEllipticMotionクラス

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    private var speed:Number = 5;
    private var center:Point = new Point(0, 0);
    private var radius:Point = new Point(100, 50);
    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    private var front:Boolean = false;
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
      degree = nDegree;
      if (myCenter is Point) {
        center = myCenter;
      }
      if (myRadius is Point) {
        radius = myRadius;
      }
      addEventListener(Event.ENTER_FRAME, rotate);
      setRotation();
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
      setOrder();
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x*currentScale;
    }
    private function moveY():void {
      y = center.y+sin*radius.y*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function setOrder():void {
      if (parent) {
        if ((getIndexZ()>0) != front) {
          var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;
          parent.setChildIndex(this, nIndex);
        }
      }
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}


10-03 EventDispatcherクラスを継承して利用する
RotationControllerクラスに、さらに機能を加えていきます。これまでEllipticMotionインスタンスは、それぞれがDisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)にイベントリスナーを登録して、そのrotate()メソッドを実行してきました。このEllipticMotion.rotate()メソッドを、RotationControllerクラスから呼出すように変更してみましょう。

EventDispatcherクラスを継承する
RotationControllerインスタンスは、Array型のプロパティinstances_arrayにEllipticMotionインスタンスを格納しています。ですから、forループでそれらのインスタンスをすべて取出して、rotate()メソッドを呼出す方法も考えられます。しかし、RotationControllerクラスにEventDispatcherクラスを継承させれば、イベントリスナーの仕組みが使えるようになり、rotate()メソッドをリスナーとして呼出すことができます。

そこで、まずはEventDispatcherクラスを継承した簡単なサンプルのクラスを定義して、イベントリスナーの扱い方について学んでおきます。サンプルのクラスは、EventDispatcherとします。クラスの中身が空でも、EventDispatcherクラスを継承しさえすれば、EventDispatcher.addEventListener()などのメソッドは使えるようになります。

package {
  import flash.events.EventDispatcher;
  public class EventDispatcherTest extends EventDispatcher {
    public function EventDispatcherTest() {
    }
  }
}

RotationControllerクラスを定義したActionScript(AS)ファイルと同階層に置いたFlashムービー(FLA)ファイルで、フレームアクションを記述してイベントリスナーの登録を試してみましょう。EventDispatcherTestインスタンスに対して、実際には存在しない"myEvent"を第1引数のイベントに指定して、つぎのようにEventDispatcher.addEventListener()メソッドを呼出します。

var test:EventDispatcherTest = new EventDispatcherTest();
test.addEventListener("myEvent", testHandler);
trace(test.hasEventListener("myEvent"));
function testHandler(eventObject:Event):void {
  trace(eventObject);
}

そして、EventDispatcher.hasEventListener()メソッド(Word09-006)でイベント"myEvent"にリスナーが登録されているかどうかを調べて、その結果をtrace()関数で[出力]するとtrueが表示されます(図10-009)。つまり、確かにEventDispatcherクラスのメソッドが使えて、イベントリスナーは登録されていることがわかります。

図10-009■EventDispatcherクラスを継承したクラスのインスタンスにはイベントリスナーが登録できる
EventDispatcher.addEventListener()メソッドで架空のイベントにリスナーを登録して、EventDispatcher.hasEventListener()メソッドでリスナーの存在を調べるとtrueが返される。

ただし、いつまで待っても、イベント"myEvent"は発生しません。Flash Playerには、このイベントが何なのか知る由もないからです。イベントを発生させ、リスナーを呼出すには、その処理をEventDispatcherTestクラスに書き加えなければならないのです。

EventDispatcher.dispatchEvent()メソッドを呼出す
イベントを発生させるには、EventDispatcher.dispatchEvent()メソッドを用います(Word 10-001)。引数はひとつで、Eventクラスまたはそのサブクラスのインスタンスを渡します。それでは、発生させるイベントは、どこに指定するのでしょう。今回のEventDispatcherTestクラスのサンプルでは、"myEvent"というイベントを配信するつもりです。

イベントの指定は、引数に渡すEventオブジェクトのEvent.typeプロパティとして設定するのです。そして、Eventクラスのコンストラクタメソッドは、第1引数がEvent.typeプロパティの指定になります(第2および第3引数もありますが、省略可能なオプションです)。したがって、第1引数にイベント名の文字列"myEvent"を指定してEventクラスのコンストラクタを呼出せば、イベント"myEvent"を発生させるためのEventインスタンスが生成できます。

var myEventObject:Event = new Event(イベント名の文字列)

Word 10-001■EventDispatcher.dispatchEvent()メソッド
つぎのシンタックスで、インスタンスにイベントを発生させます。

インスタンス.dispatchEvent(イベントオブジェクト:Event):Boolean

EventDispatcherクラスあるいはそのサブクラスのインスタンスに対してイベントを発生させ、インスタンスの属するイベントフロー内にイベントを配信します。イベントを受取ったインスタンスに登録されたリスナー関数が呼出されます。

イベントが正しく配信されれば、メソッドはtrueを返します。イベントが配信できなかった場合や、EventDispatcher.preventDefault()メソッドが呼出されたときは、falseが返されます。

イベントオブジェクト ー Eventクラスまたはそのサブクラスのインスタンスで、Event.typeプロパティに指定されたイベントが配信されます。


Tips 10-006■イベントフロー
イベントの発生したEventDispatcher(あるいはそのサブクラスの)インスタンスが表示リストに属する場合、イベントによっては表示リスト内の親インスタンスにも配信されます。このような表示リスト内のイベントの流れを、「イベントフロー」と呼びます。詳しくは[ヘルプ]の[ActionScript 3.0のプログラミング] > [イベントの処理] > [イベントフロー]をご参照ください。

EventDispatcherTestクラスから、イベント"myEvent"を配信してみましょう。もちろん、イベントをいつ発生させるかは、自由に決めることができます。テスト用のサンプルですのでごく簡単に、Timerクラス(03-05「Stringクラスで文字列を操作する」の「日付の更新処理を加える」を参照)を使って、EventDispatcherTestインスタンスを生成した1秒後に1度だけイベントを配信することにします。その処理を加えたクラスEventDispatcherTestの定義は、つぎのようになります。

package {
  import flash.events.EventDispatcher;
  import flash.events.Event;
  import flash.events.TimerEvent;
  import flash.utils.Timer;
  public class EventDispatcherTest extends EventDispatcher {
    var testTimer:Timer;
    public function EventDispatcherTest() {
      testTimer = new Timer(1000, 1);
      testTimer.addEventListener(TimerEvent.TIMER, onTimer);
      testTimer.start();
    }
    private function onTimer(eventObject:TimerEvent):void {
      var myEventObject:Event = new Event("myEvent");
      dispatchEvent(myEventObject);
    }
  }
}

第1に、EventDispatcherTestクラスのコンストラクタにおけるTimerクラスを使った処理です。まず、1000ミリ秒後に1回イベントを発生させる指定で、Timerインスタンスを生成します。そして、Timer.timerイベント(定数TimerEvent.TIMER)に、リスナーとしてメソッドonTimerを指定しています。そのうえで、Timer.start()メソッドにより、Timerインスタンスの処理を開始します。これで、インスタンス生成から1秒後に、onTimer()メソッドが呼出されるようになります。

そこで第2に、onTimer()メソッドにより、リスナーに対するイベントの配信を行います。Eventインスタンスは、前述のとおり、コンストラクタの第1引数にイベント名の文字列"myEvent"を渡して生成します。そして、EventDispatcher.dispatchEvent()メソッドの引数にこのEventオフジェクトを渡して呼出せば、Event.typeプロパティに設定されたイベントがリスナーに対して発生します。

前掲のフレームアクションでは、リスナー関数testHandler()の中で引数として受取ったEventインスタンスをtrace()関数で[出力]しています。したがって、[出力]パネルには、以下のようにEventインスタンスの情報が表示されます(図10-010)。Event.typeプロパティには、イベントとして"myEvent"が設定されています。

[Event type="myEvent" bubbles=false cancelable=false eventPhase=2]
図10-010■フレームアクションでEventDispatcherTestインスタンスにイベントリスナーを登録する
リスナー関数により[出力]されたEventインスタンスの情報から、Event.typeプロパティにイベントとして"myEvent"が設定されていることを確認できる。

RotationControllerクラスからイベントを配信する
それでは、RotationControllerクラスにEventDispatcherクラスを継承させ、EllipticMotionインスタンスのrotate()メソッドをリスナーとして登録できるように修正します。イベント名は"rotate"として、クラス(static)定数ROTATEに設定します。さらに、イベント配信の処理まで加えたクラスRotationControllerの定義は、つぎのスクリプト10-004のとおりです。

スクリプト10-004■RotationControllerクラスから"rotate"イベントを配信する

// ActionScript 3.0クラス定義ファイル: RotationController.as
package rotation{
  import flash.display.MovieClip;
  import flash.geom.Point;
  import flash.events.Event;
  import flash.events.EventDispatcher;   // 追加
  // public class RotationController {
  public class RotationController extends EventDispatcher {   // 継承を追加
    private static const ROTATE:String = "rotate";   // 追加
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array = new Array();
    public function RotationController(my_mc:MovieClip, myCenter:Point=null, myRadius:Point=null) {
      timeline_mc = my_mc;
      if (myCenter is Point) {
        center = myCenter;
      } else {
        center = new Point(my_mc.stage.stageWidth/2, my_mc.stage.stageHeight/2);
      }
      if (myRadius is Point) {
        radius = myRadius;
      } else {
        radius = center.clone();
        radius.normalize(radius.length*0.7);
      }
    }
    public function addListener(my_mc:EllipticMotion):uint {
      var nInstances:uint = instances_array.push(my_mc);
      return nInstances;
    }
    public function startRotation():void {
      var nInstances:uint = instances_array.length;
      var nDegree:Number = 360/nInstances;
      var my_mc:EllipticMotion;
      for (var i:int = 0; i<nInstances; i++) {
        my_mc = instances_array[i];
        timeline_mc.addChild(my_mc);
        my_mc.setPosition(i*nDegree, center, radius);
        addEventListener(ROTATE, my_mc.rotate);   // 追加
      }
      timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);   // 追加
    }
    private function rotate(eventObject:Event):void {   // メソッドを追加
      var myEventObject:Event = new Event(ROTATE);
      dispatchEvent(myEventObject);
    }
  }
}

イベントの配信に関わる変更としては、startRotation()メソッドの修正と、rotate()メソッドを追加して定義したことの2点です。

まず、startRotation()メソッドについては、第1にforループの処理の中でRotationControllerインスタンスに対してEventDispatcher.addEventListener()メソッドにより、定数ROTATEのイベントにEllipticMotionインスタンスのrotate()メソッドをリスナーとして登録しています。そして第2に、RotationControllerインスタンスがイベントを配信するため、タイムラインtimeline_mcに対するEventDispatcher.addEventListener()メソッドで、DisplayObject.enterFrameイベント(定数Event.ENTER_FRAME)に、新しく加えたrotate()メソッドをリスナーとして設定しました。

つぎに、イベントを配信するためのメソッドrotate()の追加定義です。Eventインスタンスは、コンストラクタに定数ROTATEを渡して生成します。そのうえで、このEventオフジェクトをEventDispatcher.dispatchEvent()メソッドに渡して呼出すことにより、定数ROTATEのイベントをリスナーに配信しています。

以上により、RotationControllerインスタンスにはリスナーにイベントを配信する機能が備わり、EllipticMotionインスタンスのrotate()メソッドがROTATEイベントのリスナーとして登録されます。他方で、タイムラインのDisplayObject.enterFrameイベントにRotationControllerクラスのrotate()メソッドを登録しているので、毎フレーム呼出されるこのメソッドから、RotationControllerインスタンスのリスナーにROTATEイベントが配信されます。よって、EllipticMotionインスタンスのrotate()メソッドが呼出されることになるのです。


タイムラインは、毎フレームRotationControllerのrotate()メソッドを呼んで、リスナーにROTATEイベントを配信。

RotationControllerインスタンスからイベントを受取るEllipticMotionクラスにも、2点修正が必要になります(スクリプト10-005)。

第1は、DisplayObject.enterFrameイベントには、もうリスナーは登録しないということです。イベントはRotationController.ROTATEとして、RotationControllerインスタンスから配信されるからです。したがって、setPosition()メソッド内に記述されていたEventDispatcher.addEventListener()の呼出しは削除(またはコメントアウト)します。

第2に、rotate()メソッドに指定したアクセス制御の属性です。private属性の指定では、別のクラスであるRotationControllerのインスタンスからは、リスナー関数として登録しても呼出せません(図10-011)。そこで、同じパッケージ内からのアクセスが可能なinternal属性の指定に変えました(表10-003)。なお、internalは、クラスやプロパティ・メソッドのデフォルト属性です。

図10-011■private属性で指定されたメソッドは他のクラスからリスナーとして呼出せない
EllipticMotionクラスのrotate()メソッドはprivate属性なので、RotationControllerインスタンスからはリスナーに登録しても呼出せない。

表10-003■アクセス制御の属性キーワード
アクセス制御の属性 説明
internal(デフォルト) 同じパッケージ内のクラスから、アクセスすることができます。クラスやプロパティ・メソッドにとくにアクセス制御の属性を指定しなければ、internalとして扱われます(ただし、コンストラクタを除きます)。
private 定義されたクラス内からのみ、アクセスすることができます。
protected 定義されたクラスとそのサブクラスからのみ、アクセスすることができます。
public 任意のクラスやタイムラインから、アクセスすることができます。コンストラクタメソッドは、このpublic属性しか指定できません。

スクリプト10-005■EllipticMotionクラスはRotationControllerからイベントを受取る

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    private var speed:Number = 5;
    private var center:Point = new Point(0, 0);
    private var radius:Point = new Point(100, 50);
    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    private var front:Boolean = false;
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
      degree = nDegree;
      if (myCenter is Point) {
        center = myCenter;
      }
      if (myRadius is Point) {
        radius = myRadius;
      }
      // addEventListener(Event.ENTER_FRAME, rotate);
      setRotation();
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
      setOrder();
    }
    // private function rotate(eventObject:Event):void {
    internal function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x*currentScale;
    }
    private function moveY():void {
      y = center.y+sin*radius.y*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function setOrder():void {
      if (parent) {
        if ((getIndexZ()>0) != front) {
          var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;
          parent.setChildIndex(this, nIndex);
        }
      }
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

これで、EllipticMotionクラスからは、イベントリスナーへの登録がされなくなりました。その代わりに、RotationControllerインスタンスにaddListener()メソッドでインスタンスを加えておくと、RotationController.startRotation()メソッドの呼出しにより、EllipticMotionクラスに定義されたrotate()メソッドが毎フレーム実行されるようになります。

AS1&2 Note 10-002■addListener()メソッド
addListener()メソッドでイベントの指定することなしにインスタンスを登録すると、そのインスタンスに設定された特定の名前のメソッドがイベント発生時に呼出されるというのは、ActionScript 2.0/1.0のKeyやMouseクラスのaddListener()メソッドと同じ仕様です。そのため、RotationControllerクラスのメソッドも、addListener()という名前で定義しました。

RotationControllerクラスを使うFlashムービー(FLA)ファイルのフレームアクション(スクリプト10-001)には、とくに変更はありません。[ムービープレビュー]すると、インスタンスが楕円軌道を描いてアニメーションします。ただし、EllipticMotion.rotate()メソッドがRotationControllerインスタンスからのイベント配信により呼出されるという処理の流れに変わっています。


10-04 配列を並べ替える
EllipticMotionインスタンスの重ね順は、前章09-05「重ね順と遠近法」で、楕円軌道の上部と下部の境を基準に変更しました。そのため、楕円軌道の両端が最前面と最背面になっており、仮想z軸で1番手前になるべき楕円軌道の中央最下部が最前面にはなりません(図10-012)。

図10-012■楕円軌道の中央最下部が最前面にならない
楕円軌道の上部と下部の境で重ね順を変えているので、時計回りなら右端が最前面で左端が最背面になってしまう。

これは個々のEllipticMotionインスタンスからは、自分が全体の重ね順の何番目にいるかを知るのが難しかったため、いわば簡易な手法を採った結果です。けれど、RotationControllerインスタンスなら、EllipticMotionインスタンスをまとめて管理しています。そこで、インスタンスの重ね順を、仮想z軸上の位置に合わせて決めることにしましょう。

Array.sort()メソッド
RotationControllerクラスで、EllipticMotionインスタンスは、プロパティinstances_arrayに設定した配列に納められています。Array.sort()メソッドを使うと、配列内のエレメントをさまざまな基準で並べ替えることができます。そこでまずは、このArray.sort()メソッドについて、簡単に学習しておきましょう。

Array.sort()メソッドは、配列エレメントをデフォルトでは、Unicode値にもとづいて昇順(値の小さい順)に並べ替えます。よって、数値は数字の小さい順になります。なお、Array.sort()メソッドは、エレメントの順序を並べ替えた新しい配列を返すのではなく、ターゲットの配列エレメントそのものを変更することにご注意ください。

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

また、テキストのエレメントは、ABC順に並び替わります。

var my_array:Array = ["Flash", "Dreamweaver", "Fireworks", "Illustrator", "Photoshop"];
my_array.sort();
trace(my_array);   // 出力: Dreamweaver,Fireworks,Flash,Illustrator,Photoshop

注意すべきことのひとつは、Unicode値というのは文字列扱いを意味します。つまり、エレメントの文字を頭からひとつひとつ比べて、順序が決まります。したがって、数値はその大きさの順にはなりません。たとえば、つぎの例では、1000が11よりも前に置かれています。

var my_array:Array = [29, 11, 300, 1000, 200];
my_array.sort();
trace(my_array);   // 出力: 1000,11,200,29,300

またふたつ目の注意として、英字の大文字と小文字は区別され、大文字の方が小文字より先になります。そのため、つぎの配列の並べ替えでは、頭を小文字にした"flash"が最後になっています。なお、エレメントに数字と文字がある場合は、数字が先に並びます。

var my_array:Array = ["flash", "dreamweaver", "Fireworks", "Illustrator", "Photoshop", 2008];
my_array.sort();
trace(my_array);   // 出力: 2008,Fireworks,Illustrator,Photoshop,dreamweaver,flash

Array.sort()メソッドの引数にArrayクラスの定数を指定すると、デフォルトの並べ方の条件を変えることができます。Array.NUMERIC定数は、エレメントの数値をその値の大きさによって順序づけます。

var my_array:Array = [29, 11, 300, 1000, 200];
my_array.sort(Array.NUMERIC);
trace(my_array);   // 出力: 11,29,200,300,1000

Array.CASEINSENSITIVE定数を指定すれば、エレメントの英字の大文字小文字(case)を区別せず(insensitive)に並べ替えます。

var my_array:Array = ["flash", "dreamweaver", "Fireworks", "Illustrator", "Photoshop"];
my_array.sort(Array.CASEINSENSITIVE);
trace(my_array);   // 出力: dreamweaver,Fireworks,flash,Illustrator,Photoshop

Array.sort()メソッドの引数に渡して並べ替えの条件を指定できるArrayクラスの定数には、次表10-003のようなものがあります。定数は正の整数(uint型)で、2の累乗の値が設定されています。

表10-004■Array.sort()メソッドに指定できるArrayクラスの定数
プロパティ:データ型 説明
CASEINSENSITIVE:uint 1 = 20 英字の大文字小文字を区別せずに並べ替えます。
DESCENDING:uint 2 = 21 並べ替えの順序を、降順(大きい順)に指定します。
NUMERIC:uint 16 = 24 エレメントが数値の配列を、その値の大きさで順序づけます。
RETURNINDEXEDARRAY:uint 8 = 23 ターゲットの配列は変更せず、並べ替えた結果の整数インデックスをエレメントとした配列が返されます。
UNIQUESORT:uint 4 = 22 エレメントに重複がない配列のみを並べ替えます。重複があれば、0を返します。

Array.sort()メソッドの引数には、ふたつ以上のArray定数を同時に指定することもできます。その場合は、ビット単位の論理和演算子|を用いて、指定したい定数の論理和をとって、Array.sort()の引数として渡します(ビット単位の論理和演算子の処理については、後述Column 10「2進数・16進数とビット演算」を参照)。たとえば、つぎのように引数を指定すると、エレメントの大文字と小文字を区別せず、降順に並べ替えが行われます。

var my_array:Array = ["flash", "dreamweaver", "Fireworks", "Illustrator", "Photoshop"];
my_array.sort(Array.CASEINSENSITIVE | Array.DESCENDING);
trace(my_array);   // 出力: Photoshop,Illustrator,flash,Fireworks,dreamweaver

比較関数とArray.sortOn()メソッド
配列エレメントの値を比較したのでは、意図した並べ替えはできないことがあります。順序づけに特殊な要素を加えたいときや、エレメントがクラスのインスタンスで値としての比較に適さないといった場合です。Array.sort()メソッドには引数として、エレメントを比較して順序づけるための関数である「比較関数」が指定できます。

Array.sort()メソッドの引数に比較関数が渡されると、配列エレメントの並べ替えをするため内部的にエレメントの比較をするたびに、その比較関数を呼出します。比較関数は、ふたつの配列エレメントを引数として受取ります。そして、そのふたつのエレメントを比べたうえで、結果を3つの場合に分けてそれぞれ異なる整数を返します。比較関数の満たすべき仕様は、つぎのとおりです。

【比較関数の仕様】
引数: 比べるふたつの配列エレメント。aとbとします。

戻り値:
a > bと評価すべき場合: 1を返します。
a < bと評価すべき場合: -1を返します。
a = b(等号は数学的な等価)と評価すべき場合: 0を返します。

そうすると、比較関数の典型的な定義は、つぎのような記述になります。

function compare(a, b):int {
  if (a>bと評価すべき条件) {
    return 1;
  } else if (a<bと評価すべき条件) {
    return -1;
  } else {   // a == bと評価される場合
    return 0;
  }
}

[*筆者用参考] 15.4.4.11「Array.prototype.sort (comparefn)

それでは、サンプルとして、MovieClipインスタンスの配列を比較関数で、ステージ上の配置における左から順、つまりDisplayObject.xプロパティの小さい順に並べ替えてみましょう。MovieClipインスタンスはpen0からpen4までの5個が、配列mcs_arrayに納められているものとします(図10-013)。

図10-013■タイムラインに配置されたMovieClipインスタンス
MovieClipインスタンスpen0からpen4を、配列mcs_arrayに納めておく。

配列エレメントとしてMovieClipインスタンスを受取り、そのDisplayObject.xプロパティを比較して、上記仕様にしたがった値を返す比較関数compare()は、つぎのフレームアクションのように定義されます。

var mcs_array:Array = [pen0, pen1, pen2, pen3, pen4];
mcs_array.sort(compare);
function compare(a:MovieClip, b:MovieClip):int {
  var nA:Number = a.x;
  var nB:Number = b.x;
  if (nA>nB) {
    return 1;
  } else if (nA<nB) {
    return -1;
  } else {
    return 0;
  }
}

Array.sort()メソッドは、各配列エレメントの順序を比較関数(compare())から返される整数値によって決め、並べ替えを行います。前記フレームアクションによる並べ替えの結果は、処理後の配列エレメントのDisplayObject.nameプロパティを、つぎのようにforループで[出力]して確かめることができます(図10-014)。

for (var i:int=0; i<mcs_array.length; i++) {
  trace(mcs_array[i].name);
}
図10-014■比較関数でエレメントのプロパティを比較して並べ替える
並べ替えた結果は、forループでインスタンスのDisplayObject.nameプロパティを[出力]して確かめる。

エレメントがインスタンスで、そのプロパティの値により順序づけしたい場合には、Array.sortOn()メソッドが使えます。Array.sortOn()メソッドには、第1引数としてプロパティ名を文字列で渡します([ヘルプ]では「フィールド」と呼んでいます)。また、オプションの第2引数に、Arrayクラスの定数を指定することができます。

配列インスタンス.sortOn(プロパティ名[, Array定数])

前述の比較関数を使ってMovieClipインスタンスのDisplayObject.xプロパティの値で並べ替えた例を、Array.sorOn()メソッドによる処理に書替えてみましょう。Array.sortOn()メソッドの第1引数には並べ替えるプロパティ名の"x", そして数値として順序づけますので第2引数には定数Array.NUMERICを指定します。

var mcs_array:Array = [pen0, pen1, pen2, pen3, pen4];
mcs_array.sortOn("x", Array.NUMERIC);

Tips 10-007■Array.sortOn()メソッドに複数のフィールドを指定する
Array.sortOn()メソッドには、配列を使って第1引数に複数のフィールド(プロパティ名)を指定することができます。その場合、エレメントはまず最初のフィールドで順位づけされ、その順位が同じであれば2番目のフィールドで並べ替えられ、3番目以降のフィールドも同じように上位のフィールドの順位づけが同じ場合に適用されます。

また、第2引数のArray定数も、配列により複数の指定が可能です。その場合、配列の長さ(エレメント数)は、第1引数と同じでなければなりません。

配列インスタンス.sortOn([プロパティ名, プロパティ名, ...], [Array定数, Array定数, ...])

RotationControllerクラスでインスタンスの重ね順を変える
配列エレメントの並べ替えについて予備知識が得られましたので、RotationControllerクラスにEllipticMotionインスタンスの重ね順を管理させるよう修正していきます。まず、EllipticMotionクラスから、自らのインスタンスの重ね順を操作する処理は除きます。以下のようにsetOrder()メソッドは削除(コメントアウト)し、setRotation()メソッドからのsetOrder()メソッドの呼出しを外します。setOrder()メソッドで使われていたインスタンスプロパティfrontも不要になります。

public class EllipticMotion extends MovieClip {
  // ...[中略]...
  // private var front:Boolean = false;
  // ...[中略]...

  private function setRotation():void {
    moveX();
    moveY();
    scale();
    blur();
    // setOrder();
  }
  // ...[中略]...
  /*
  private function setOrder():void {
    if (parent) {
      if ((getIndexZ()>0) != front) {
        var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;
        parent.setChildIndex(this, nIndex);
      }
    }
  }
  */

これで一旦、EllipticMotionインスタンスの重ね順は、コントロールされなくなりました。[ムービープレビュー]を確かめると、インスタンスの重ね順が変わりませんので、仮想z軸の奥にあるべきインスタンスが手前に表示されたりします(図10-015)。なお、フレームアクションは前掲スクリプト10-001のまま、とくに変更の必要はありません。

図10-015■EllipticMotionクラスからsetOrder()メソッドとその呼出しを除く
重ね順のコントロールがされなくなったので、仮想z軸で奥にあるべきインスタンスが手前に表示される。

改めてRotationControllerクラスに、EllipticMotionインスタンスの重ね順を管理するメソッドトして以下のようにsetOrder()を定義します。このメソッドは、EllipticMotionインスタンスが納められたArray型のプロパティinstances_arrayをターゲットに、仮想z軸の順序で並べ替えを行います。そして、表示リスト内の各インスタンスの位置を、その並びに合わせて変更するのです。

Array.sort()メソッドでEllipticMotionインスタンスを並べ替えるために、引数として比較関数compareメソッドを指定しています。compare()メソッドは、EllipticMotionクラスのメソッドgetIndexZ()により仮想z軸における位置を調べ、EllipticMotionインスタンスの順序づけをします。setOrder()メソッドの実行は、DisplayObject.enterFrameイベントのリスナーとして呼出されるrotate()メソッドの最後に加えました。

public class RotationController extends EventDispatcher {
  private function rotate(eventObject:Event):void {
    var myEventObject:Event = new Event(ROTATE);
    dispatchEvent(myEventObject);
    setOrder();   // 追加
  }
  private function setOrder():void {   // 追加定義
    var nInstances:uint = instances_array.length;
    instances_array.sort(compare);
    for (var i:int=0; i<nInstances; i++) {
      timeline_mc.setChildIndex(instances_array[i], i);
    }
  }
  private 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;
    }
  }
}

setOrder()メソッドは、instances_arrayプロパティに格納されたEllipticMotionインスタンスを比較関数compareで仮想z軸の順に並べ替えたうえで、forループの処理により表示リスト内のインスタンスの位置をDisplayObjectContainer.setChildIndex()メソッドで同じ順序に設定しています。

compare()メソッドの処理は、前述の解説で紹介したサンプルと基本的に同じ、スタンダードな構成です。順序づける基準は、EllipticMotionインスタンスのgetIndexZ()メソッドから返される値です。メソッドを引数なしに呼出していますので、楕円軌道最上部が-1、最下部で1が返されます。配列内のインデックスは値が小さいほど前になり、表示リストでは値の小さいインスタンスほど背面に配置されます。

以上の修正だけでは、実は[コンパイルエラー]が生じます(図10-016)。これはRotationControllerクラスのcompare()メソッドで、private属性で指定されたEllipticMotionクラスのgetIndexZ()メソッドを呼出しているためです。ふたつのクラスは同じパッケージrotationに属しますので、EllipticMotion.getIndexZ()メソッドのアクセス制御の属性は少なくともinternalで指定する必要があります(もちろん、publicで指定しても、エラーは解消します)。

// private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
internal function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
図10-016■private属性のメソッドは他のクラスから呼出せない
EllipticMotion.getIndexZ()メソッドはprivate属性で指定されているので、RotationControllerクラスから呼出すと[コンパイルエラー]が発生する。

これで必要な修正はできました。[ムービープレビュー]で確かめると、インスタンスは楕円軌道の最下部で重ね順が最前面になり、最上部は最背面に表示されるようになります(図10-017)。

図10-017■RotationControllerクラスのsetOrder()メソッドで重ね順を制御
楕円軌道の最下部が最前面、最上部が最背面として表示。

RotationControllerクラスのsetOrder()メソッド内におけるEllipticMotionインスタンスの並べ替えで、Array.sort()メソッドと比較関数でなく、Array.sortOn()メソッドを使うことはできないでしょうか。Array.sortOn()メソッドで並べ替えるには、その基準となる値が配列エレメントであるEllipticMotionインスタンスのプロパティでなければなりません。でしたら、順序づけるためのgetIndexZ()メソッドの戻り値をEllipticMotionクラスのプロパティとして定義してしまえばよいでしょう。

この場合、getアクセサメソッド(08-04「get/setアクセサメソッド」参照)を定義して、以下のようにgetIndexZ()メソッドの値を返せば簡単です。メソッド名はindexZとして、public属性で指定します。EllipticMotionクラスのsetOrder()メソッドを削除し、setRotation()メソッドからその呼出しを外すのは、前述のArray.sort()メソッドと比較関数を使ったときと同じです。

public class EllipticMotion extends MovieClip {
  // ...[中略]...
  public function get indexZ():Number {   // 追加定義
    return getIndexZ();
  }
  // ...[中略]...
  private function setRotation():void {
    moveX();
    moveY();
    scale();
    blur();
    // setOrder();
  }
  // ...[中略]...
  /*
  private function setOrder():void {
    if (parent) {
      if ((getIndexZ()>0) != front) {
        var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;
        parent.setChildIndex(this, nIndex);
      }
    }
  }
  */

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

getアクセサメソッドを使う利点として、getIndexZ()メソッドのアクセス制御をprivate属性のままにしておけます。代わりに、getアクセサメソッドindexZがpublic属性になるものの、setアクセサメソッドを定義していないので読取り専用プロパティとして限定することができます。

Maniac! 10-003■Array.sortOnメソッドに指定するプロパティはpublic属性
Array.sortOn()メソッドの第1引数として指定するプロパティは、アクセス制御の属性がpublicで指定されていないと、プロパティが見つからないというランタイムエラー(コード1069)になります。

RotationControllerクラスにおける並べ替えの処理は、比較関数が要らない分簡潔になります。Array.sortOn()メソッドを使ってEllipticMotionインスタンスの並べ替えを行うメソッドsetOrder()の定義は、つぎのとおりです。

public class RotationController extends EventDispatcher {
  // ...[中略]...
  private function rotate(eventObject:Event):void {
    var myEventObject:Event = new Event(ROTATE);
    dispatchEvent(myEventObject);
    setOrder();   // 追加
  }
  private function setOrder():void {   // 追加定義
    var nInstances:uint = instances_array.length;
    instances_array.sortOn("indexZ", Array.NUMERIC);
    for (var i:int=0; i<nInstances; i++) {
      timeline_mc.setChildIndex(instances_array[i], i);
    }
  }

Array.sortOn()メソッドもデフォルトでは、Array.sort()メソッドと同じく、エレメントを文字列として比較して並べ替えます。したがって、数値として順序づけるには、メソッドの第2引数に定数Array.NUMERICを指定する必要があります。

以下に修正後のクラスEllipticMotion(スクリプト10-006)とRotationController(スクリプト10-007)の全体を掲げます。Flashムービー(FLA)ファイルのフレームアクションは、前掲スクリプト10-001のまま変わりません。

スクリプト10-006■EllipticMotionクラスからsetOrder()メソッドを外してgetアクセサメソッドindexZを追加

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    private var speed:Number = 5;
    private var center:Point = new Point(0, 0);
    private var radius:Point = new Point(100, 50);
    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    // private var front:Boolean = false;
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function get indexZ():Number { // 追加定義 // Array.sortOn()を使う場合
      return getIndexZ();
    }
    public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
      degree = nDegree;
      if (myCenter is Point) {
        center = myCenter;
      }
      if (myRadius is Point) {
        radius = myRadius;
      }
      setRotation();
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
      // setOrder();
    }
    internal function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x*currentScale;
    }
    private function moveY():void {
      y = center.y+sin*radius.y*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    /*
    private function setOrder():void {
      if (parent) {
        if ((getIndexZ()>0) != front) {
          var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;
          parent.setChildIndex(this, nIndex);
        }
      }
    }
    */
    // internal function getIndexZ(nMin:Number=-1, nMax:Number=1):Number { // Array.sort()を使う場合

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


スクリプト10-007■RotationControllerクラスにメソッドsetOrder()を加えてインスタンスの重ね順制御

// ActionScript 3.0クラス定義ファイル: RotationController.as
package rotation{
  import flash.display.MovieClip;
  import flash.geom.Point;
  import flash.events.Event;
  import flash.events.EventDispatcher;
  public class RotationController extends EventDispatcher {
    private static const ROTATE:String = "rotate";
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array = new Array();
    public function RotationController(my_mc:MovieClip, myCenter:Point=null, myRadius:Point=null) {
      timeline_mc = my_mc;
      if (myCenter is Point) {
        center = myCenter;
      } else {
        center = new Point(my_mc.stage.stageWidth/2, my_mc.stage.stageHeight/2);
      }
      if (myRadius is Point) {
        radius = myRadius;
      } else {
        radius = center.clone();
        radius.normalize(radius.length*0.7);
      }
    }
    public function addListener(my_mc:EllipticMotion):uint {
      var nInstances:uint = instances_array.push(my_mc);
      return nInstances;
    }
    public function startRotation():void {
      var nInstances:uint = instances_array.length;
      var nDegree:Number = 360/nInstances;
      var my_mc:EllipticMotion;
      for (var i:int = 0; i<nInstances; i++) {
        my_mc = instances_array[i];
        timeline_mc.addChild(my_mc);
        my_mc.setPosition(i*nDegree, center, radius);
        addEventListener(ROTATE, my_mc.rotate);
      }
      timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);
    }
    private function rotate(eventObject:Event):void {
      var myEventObject:Event = new Event(ROTATE);
      dispatchEvent(myEventObject);
      setOrder();   // 追加
    }
    private function setOrder():void {   // 追加定義
      var nInstances:uint = instances_array.length;
      // instances_array.sort(compare);   // Array.sort()を使う場合
      instances_array.sortOn("indexZ", Array.NUMERIC);   // Array.sortOn()を使う
      for (var i:int=0; i<nInstances; i++) {
        timeline_mc.setChildIndex(instances_array[i], i);
      }
    }
    /* Array.sort()を使う場合
    private function compare(a:EllipticMotion, b:EllipticMotion):Number {   // 追加定義
      var nA:Number = a.indexZ;
      var nB:Number = b.indexZ;
      if (nA>nB) {
        return 1;
      } else if (nA<nB) {
        return -1;
      } else {
        return 0;
      }
    }
    */

  }
}


10-05 イベントオブジェクトで情報を送る
楕円軌道のアニメーションを、マウスポインタの位置に応じてインタラクティブに変化させてみましょう。具体的には、第05章の「マウスでスクロールさせるMovieClip」で作成したムービーと同じく、楕円軌道の中心からマウスポインタの水平距離が大きくなるほど回転スピードを上げるとともに、垂直方向の位置に合わせて楕円軌道のy軸方向の半径を変えることにします(図10-018)。

図10-018■マウスポインタの位置に応じて回転スピードと楕円軌道を変化させる
楕円軌道の中心とマウスポインタの水平座標の差に応じて回転スピードを上げ、垂直座標に合わせて楕円軌道のy軸方向の半径を変える。

Eventクラスのサブクラスを定義する
それでは、楕円軌道の中心座標やマウスポインタの座標をもとに、回転スピードや楕円軌道のy軸方向の半径を計算する処理は、どのクラスで行いましょう。これらの値は、回転するEllipticMotionインスタンスすべてに共通となります。また、RotationControllerクラスは、せっかくEventDispatcherクラスを継承して、リスナーにイベントを配信しています。そうであれば、イベントと一緒に、つまりイベントオブジェクトとして送ることができるとよさそうです。

しかし、Eventクラスやそのサブクラスのインスタンスに、定義済みのプロパティ以外の値は設定できません。そこで、Eventクラスを継承したカスタムクラスを定義して、そのプロパティとして必要な値を加えることにしましょう。クラス名はRotationEventとします。コンストラクタ以外にsetRotationInfo()というメソッドをひとつだけ定義し、RotationControllerクラスから呼出して各プロパティ値を設定することにします。


リスナーへの情報は、イベントオブジェクトに入れて送る。

RotationEventオブジェクトは、イベントに関わる情報を送る、いわば容れ物です。よって、リスナーに送るべき情報を中心としたpublicプロパティと、それらの値を設定するpublicメソッドsetRotationInfo()で構成されるシンプルなクラスになります(スクリプト10-008)。インスタンスプロパティは、すべてpublic属性のNumber型で宣言しています。speedは毎フレーム回転する度数、altitudeが楕円軌道のy軸方向の半径で3D表現上は最前面のEllipticMotionインスタンスの高さを変化させます。楕円軌道の中心のxy座標はcenterXとcenterY、楕円軌道のx軸・y軸方向の半径の初期値がradiusXとradiusYになります。あと3点だけ、補足しておきます。

第1に確認しておきたいのは、RotationEventクラスがEventクラスを継承していることです。これは、あとで第3の点として、コンストラクタの処理を説明する際に関わってきます。第2に、static属性のイベント定数としてROTATEを宣言しました。この定数は、これまでRotationControllerクラスに宣言されていたものです(前掲スクリプト10-007)。しかし、新たにRotationEventクラスを定義しましたので、イベント定数はこちらに加えた方がよいでしょう。RotationControllerクラスの方の定数は、のちの修正でつぎのように外すことにします。

public class RotationController extends EventDispatcher {
  // private static const ROTATE:String = "rotate";
スクリプト10-008■Eventクラスを継承するRotationEventクラスの定義

// ActionScript 3.0クラス定義ファイル: RotationEvent.as
package rotation{
  import flash.events.Event;
  import flash.geom.Point;
  public class RotationEvent extends Event {
    public static const ROTATE:String = "rotate";
    public var speed:Number;
    public var altitude:Number;
    public var centerX:Number;
    public var centerY:Number;
    public var radiusX:Number;
    public var radiusY:Number;
    public function RotationEvent() {
      super(ROTATE);
    }
    public function setRotationInfo(mySpeed:Number, myAltitude:Number, center:Point, radius:Point):void {
      speed = mySpeed;
      altitude = myAltitude;
      centerX = center.x;
      centerY = center.y;
      radiusX = radius.x;
      radiusY = radius.y;
    }
  }
}

第3として、RotationEventクラスのコンストラクタメソッドの本体内で、super()ステートメントが呼出されています。super()ステートメントは、コンストラクタ内に記述して、スーパークラスのコンストラクタを呼出します(Word 10-002)。

前述(10-03「EventDispatcherクラスを継承して利用する」)のとおり、EventDispatcher.dispatchEvent()メソッドで発生させるイベントは、メソッドの引数として渡すイベントオブジェクトのEvent.typeプロパティに設定します。しかし、この値はEventクラスのコンストラクタに第1引数として渡さなければならず、あとから値は変えられません(読取り専用プロパティ)。そこで、super()ステートメントでスーパークラスEventのコンストラクタを呼出し、引数としてEvent.typeプロパティに設定すべき文字列である静的(static)定数ROTATEの値を渡しているのです。

Word 10-002■super()ステートメント
つぎのシンタックスで、スーパークラスのコンストラクタを呼出します。

super([引数0, 引数1, ..., 引数N])

サブクラスのコンストラクタメソッドの本体内に記述して、スーパークラスのコンストラクタを呼出します。渡すべき引数は、スーパークラスのコンストラクタのシンタックスにしたがいます。

サブクラスのコンストラクタが呼出されてインスタンスを生成する際には、必ずスーパークラスのコンストラクタも呼出さなければなりません。サブクラスのコンストラクタにsuper()ステートメントの記述がない場合には、コンパイル時(SWFを書出すとき)にsuper()ステートメントが引数なしに、コンストラクタの最初のステートメントとして自動的に挿入されます。

しかし、スーパークラスのコンストラクタに引数を渡さなければならないときには、その引数を指定してsuper()ステートメントをサブクラスのコンストラクタ内に記述する必要があります。

RotationControllerクラスを修正する
RotationEventオブジェクトの各インスタンスプロパティ値は、RotationControllerクラスが算出し、setRotationInfo()メソッドで設定したうえで、EventDispatcher.dispatchEvent()メソッドによりイベントリスナーに送ります。RotationControllerクラスで修正すべきプロパティやメソッドは以下のとおりです。

第1に、プロパティおよび定数です。前述のとおり、RotationEvent.ROTATE定数を定義しましたので、RotationControllerクラスの定数ROTATEは除きます。private属性で指定したふたつのNumber型のインスタンスプロパティdecelerationとmaxSpeedは、rotate()メソッドで用いるために加えました。ですから、rotation()メソッドの箇所でご説明します。

第2に、修正したふたつのメソッドのうちのstartRotation()は、ステートメントを1行書直しただけです。RotationControllerインスタンスに対してEventDispatcher.addEventListener()メソッドを呼出す際の第1引数を、新たに定義した定数RotationEvent.ROTATEに変えています。


public class RotationController extends EventDispatcher {
  // private static const ROTATE:String = "rotate";
  // ...[中略]...

  private var sensitivity:Number = 0.1;   // 追加
  private var maxSpeed:Number = 15;   // 追加
  // ...[中略]...
  public function startRotation():void {
    var nInstances:uint = instances_array.length;
    var nDegree:Number = 360/nInstances;
    var my_mc:EllipticMotion;
    for (var i:int = 0; i<nInstances; i++) {
      my_mc = instances_array[i];
      timeline_mc.addChild(my_mc);
      my_mc.setPosition(i*nDegree, center, radius);
      // addEventListener(ROTATE, my_mc.rotate);
      addEventListener(RotationEvent.ROTATE, my_mc.rotate);
    }
    timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);
  }
  private function rotate(eventObject:Event):void {
    // var myEventObject:Event = new Event(ROTATE);
    var myEventObject:RotationEvent = new RotationEvent();
    var speed:Number = (-center.x+timeline_mc.stage.mouseX)*sensitivity;
    speed = Math.min(Math.max(speed, -maxSpeed), maxSpeed);
    var altitude:Number = -center.y+timeline_mc.stage.mouseY;
    altitude = Math.min(Math.max(-radius.y, altitude), radius.y);
    myEventObject.setRotationInfo(speed, altitude, center, new Point(radius.x, radius.y));
    dispatchEvent(myEventObject);
    setOrder();
  }

rotate()メソッドは本体の最後の2行のステートメント以外は、すべて書替えました。まず、イベントオブジェクトは、クラスRotationEventのコンストラクタを呼出して生成します。

つぎに、楕円軌道の中心座標とマウスポインタの座標から、水平方向の回転角(speed)と楕円軌道のy軸方向の半径(altitude)を計算しています。これらの値の決め方は、第05章の05-04 「マウスポインタの位置に応じてスクロールスピードを変える」でインスタンスのスクロールスピードを定めたときの考え方と基本的に同じです。水平方向の回転角(speed)を求める際に、インスタンスプロパティmaxSpeedは制限速度、sensitivityが値の変化を調整する比例係数の役割を果たします。楕円軌道のy軸方向の半径(altitude)は、RotationControllerインスタンス作成時の初期値(radius.y)を最大値としています。

なお、ローカル変数speedおよびaltitudeの値を一定の範囲に収めるため、Math.max()およびMath.min()メソッドを使っています。このふたつのメソッドは、引数に指定した複数の数値のうち、前者が最大値、後者が最小値を返します(表10-005)。予め最大値および最小値を定め、値が最大値を超えたら最大値、最小値を下回ったら最小値、その範囲内であればもとの値を変数に設定するには、つぎのような式を書きます。

変数 = Math.min(Math.max(値, 最小値), 最大値)
表10-005■Mathクラスの最大値・最小値を返すクラス(静的)メソッド
メソッド 説明
max(数値0:Number, 数値1:Number, ..., 数値N:Number):Number 引数に指定された複数の数値の中で最大値を返します。
min(数値0:Number, 数値1:Number, ..., 数値N:Number):Number 引数に指定された複数の数値の中で最小値を返します。

Tips 10-008■Math.max()およびMath.min()メソッドとifステートメントとの比較
予め定めた最大値と最小値の範囲内に値を収めるには、if/else if/elseステートメントを使った条件判定により処理することもできます。処理のスピードでいえば、条件判定の方が高速でしょう。まず、初めのif条件がtrueと評価されればelse if/elseの処理は行われないのに対して、Mathクラスのメソッドを使うと値が最大値と最小値の両方と必ず比較されます。加えて、Mathクラスのメソッドの処理は、一般にあまり速くありません。

しかし、ことさら処理速度が求められない場合には、本文の式は値を最小値から最大値の範囲に収めるいわば公式として覚えてしまえば、記述も短く見やすいといえます。

以上に説明したとおり、リスナーに対してRotationEventオブジェクトを配信するように修正したRotationControllerクラスの定義が、つぎのスクリプト10-009です。なお、RotationEventクラスは、同じrotationパッケージに属しますので、import宣言は必要ありません。

スクリプト10-009■RotationEventオブジェクトを配信するように修正したRotationControllerクラス

// ActionScript 3.0クラス定義ファイル: RotationController.as
package rotation{
  import flash.display.MovieClip;
  import flash.geom.Point;
  import flash.events.Event;
  import flash.events.EventDispatcher;
  public class RotationController extends EventDispatcher {
    // private static const ROTATE:String = "rotate";
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array = new Array();
    private var sensitivity:Number = 0.1;   // 追加
    private var maxSpeed:Number = 15;   // 追加
    public function RotationController(my_mc:MovieClip, myCenter:Point=null, myRadius:Point=null) {
      timeline_mc = my_mc;
      if (myCenter is Point) {
        center = myCenter;
      } else {
        center = new Point(my_mc.stage.stageWidth/2, my_mc.stage.stageHeight/2);
      }
      if (myRadius is Point) {
        radius = myRadius;
      } else {
        radius = center.clone();
        radius.normalize(radius.length*0.7);
      }
    }
    public function addListener(my_mc:EllipticMotion):uint {
      var nInstances:uint = instances_array.push(my_mc);
      return nInstances;
    }
    public function startRotation():void {
      var nInstances:uint = instances_array.length;
      var nDegree:Number = 360/nInstances;
      var my_mc:EllipticMotion;
      for (var i:int = 0; i<nInstances; i++) {
        my_mc = instances_array[i];
        timeline_mc.addChild(my_mc);
        my_mc.setPosition(i*nDegree, center, radius);
        // addEventListener(ROTATE, my_mc.rotate);
        addEventListener(RotationEvent.ROTATE, my_mc.rotate);
      }
      timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);
    }
    private function rotate(eventObject:Event):void {
      // var myEventObject:Event = new Event(ROTATE);
      var myEventObject:RotationEvent = new RotationEvent();
      var speed:Number = (-center.x+timeline_mc.stage.mouseX)*sensitivity;
      speed = Math.min(Math.max(speed, -maxSpeed), maxSpeed);
      var altitude:Number = -center.y+timeline_mc.stage.mouseY;
      altitude = Math.min(Math.max(-radius.y, altitude), radius.y);
      myEventObject.setRotationInfo(speed, altitude, center, new Point(radius.x, radius.y));
      dispatchEvent(myEventObject);
      setOrder();
    }
    private function setOrder():void {
      var nInstances:uint = instances_array.length;
      instances_array.sortOn("indexZ", Array.NUMERIC);
      for (var i:int=0; i<nInstances; i++) {
        timeline_mc.setChildIndex(instances_array[i], i);
      }
    }
  }
}

EllipticMotionクラスを修正する
EllipticMotionクラスは、RotationControllerインスタンスから配信されるRotationEventオブジェクトを、リスナーに登録したrotate()メソッドで受取って処理します。フレーム毎の回転スピードや楕円軌道の中心座標、x軸・y軸方向の半径などの値は、すべてRotationEventオブジェクトから受取ります。そのため、EllipticMotionクラスは、全体にわたって細かな修正が必要になります。その内容を、順に説明していきましょう。

まず、プロパティについては、RotationEventオブジェクトからリスナーメソッドが得られる値は、EllipticMotionクラスのインスタンスプロパティとして保持しておく必要はありません。したがって、フレーム毎の回転スピードであるspeedや楕円軌道の中心座標center、x軸・y軸方向の半径radiusは、つぎのようにプロパティ宣言から外します。

public class EllipticMotion extends MovieClip {
  // ...[中略]...
  // private var speed:Number = 5;
  // private var center:Point = new Point(0, 0);
  // private var radius:Point = new Point(100, 50);

つぎに、メソッドは、RotationControllerインスタンスのリスナーに登録されて、RotationEvent.ROTATEイベントでRotationEventオブジェクトを受取るrotate()から修正しましょう。変更後のメソッド部分の抜粋は、以下に示すとおりです。第1に、メソッドの引数(eventObject)のデータ型は、もちろんRotationEventで指定します。

第2に、引数で受取ったRotationEventオブジェクト(eventObject)から、アニメーションの処理に必要なフレーム毎の回転角(speed)や楕円軌道の中心座標(centerXとcenterY)、x軸・y軸方向の半径(radiusXとaltitude)のプロパティ値を取出します。そのうえで、回転角の値は角度のプロパティdegreeに足し込みます。楕円軌道の中心座標やx軸・y軸方向の半径はRotationEventオブジェクトに数値のプロパティとして定義されていますので、EllipticMotionクラスで扱うためにそれらの値からふたつのPointインスタンス(centerとradius)を生成します。

そして第3に、楕円軌道の中心座標とx軸・y軸方向の半径がそれぞれ設定されたPointインスタンス(centerとradius)をふたつの引数にして、setRotation()メソッドを呼出します。EllipticMotionクラスのインスタンスプロパティとしてPoint型で宣言されていた楕円軌道の中心座標centerとx軸・y軸方向の半径radiusは上述の修正で除きましたので、これらの必要な値は引数にしてメソッドに渡さなければなりません。したがって、メソッドsetRotation()はこれらの引数を受取るように修正する必要があります。

// internal function rotate(eventObject:Event):void {
internal function rotate(eventObject:RotationEvent):void {
  // degree += speed;
  degree += eventObject.speed;
  var center:Point = new Point(eventObject.centerX, eventObject.centerY);   // 追加
  var radius:Point = new Point(eventObject.radiusX, eventObject.altitude);   // 追加
  // setRotation();
  setRotation(center, radius);
}

Maniac! 10-004■値をプロパティに設定するかメソッドの引数で渡すか
メソッド本体の処理で必要な値をプロパティに設定すれば、他のメソッドを呼出すときにいちいち引数で渡さずに済みます。値をプロパティに設定するのがよいか、メソッドの引数として渡す方がよいかは、クラスのデザインによります。

プロパティに値を設定した場合、メソッドを引数なしに呼べるものの、プロパティがつねに最新の値をもつ仕組みにするか、まず値を更新してからメソッドを呼出す必要が生じます。

引数を指定する場合は、渡す値を用意しなければならない反面、値さえ適切ならいつでもどこからでもメソッドを呼出せる利点があります。

2番目に修正するメソッドとして、setRotation()の前に、setPosition()を見ておきます。以下にメソッドを抜粋したとおり、本体の記述は2行のステートメントになります。第1に、引数のデフォルト値の設定を削除します。インスタンスプロパティcenterとradiusの宣言がなくなりましたので、第2および第3引数が渡されない場合に用いるべきプロパティ値がなく、引数の指定は必須になるからです(第1引数のみにデフォルト値を設定しても、意味がありません)。

よって第2に、あとのふたつの引数が指定されない、つまりデフォルト値nullの場合に備えた条件判定の処理も、必要がなくなります。そして第3として、setRotation()メソッドは、楕円軌道の中心座標とx軸・y軸方向の半径がそれぞれ指定されたふたつのPointインスタンスを引数に渡して呼出すことになります。

// public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
public function setPosition(nDegree:Number, center:Point, radius:Point):void {
  degree = nDegree;
  /*
  if (myCenter is Point) {
    center = myCenter;
  }
  if (myRadius is Point) {
    radius = myRadius;
  }
  */
  // setRotation();

  setRotation(center, radius);
}

Tips 10-009■デフォルト値をもたせるクラス
RotationControllerクラスにもコンストラクタメソッドの本体内に、デフォルト値として楕円軌道の中心座標とx軸・y軸方向の半径をそれぞれPointインスタンスで自らのプロパティに設定する処理があります(10-02「回転するインスタンスをコントロールするクラス」参照)。そのため、EllipticMotionクラスの側でも、別途同じ役割のデフォルト値を各インスタンスのプロパティとしてもつ意味は少なくなっていました。

それでは3番目に、setRotation()メソッドの修正に入ります。まず第1は、楕円軌道の中心座標とx軸・y軸方向の半径をふたつのPoint型の引数として受取ります。第2に、引数から値を取出して、メソッド本体から呼出す4つのメソッドに必要に応じて渡します。具体的には、メソッドscale()とblur()は、getIndexZ()メソッドの戻り値とプロパティ値を使って処理しますので、引数は不要です。メソッドmoveX()とmoveY()は、引数から取出したxとyそれぞれの座標値と半径の値を渡して呼出す必要があります。

// private function setRotation():void {
private function setRotation(center:Point, radius:Point):void {
  // moveX();
  moveX(center.x, radius.x);
  // moveY();
  moveY(center.y, radius.y);
  scale();
  blur();
}

最後に、メソッドmoveX()とmoveY()に加える修正は、基本的に同じ考え方ですので、まとめて行います。第1に、楕円軌道の中心座標と半径を、ふたつのNumber型の引数として受取ります。第2に、引数の数値を使ってインスタンスの位置座標を計算するように、プロパティへの代入ステートメントの右辺の式を修正しました。

// private function moveX():void {
private function moveX(nCenterX:Number, nRadiusX:Number):void {
  // x = center.x+cos*radius.x*currentScale;
  x = nCenterX+cos*nRadiusX*currentScale;
}
// private function moveY():void {
private function moveY(nCenterY:Number, nRadiusY:Number):void {
  // y = center.y+sin*radius.y*currentScale;
  y = nCenterY+sin*nRadiusY*currentScale;
}

以上の修正を加えたEllipticMotionクラスの定義は、つぎのスクリプト10-010のとおりです。

スクリプト10-010■RotationEventオブジェクトを受取るように修正したEllipticMotionクラス

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    // private var speed:Number = 5;
    // private var center:Point = new Point(0, 0);
    // private var radius:Point = new Point(100, 50);

    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function get indexZ():Number {
      return getIndexZ();
    }
    // public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
    public function setPosition(nDegree:Number, center:Point, radius:Point):void {
      degree = nDegree;
      /*
      if (myCenter is Point) {
        center = myCenter;
      }
      if (myRadius is Point) {
        radius = myRadius;
      }
      */
      // setRotation();

      setRotation(center, radius);
    }
    // private function setRotation():void {
    private function setRotation(center:Point, radius:Point):void {
      // moveX();
      moveX(center.x, radius.x);
      // moveY();
      moveY(center.y, radius.y);
      scale();
      blur();
    }
    // internal function rotate(eventObject:Event):void {
    internal function rotate(eventObject:RotationEvent):void {
      // degree += speed;
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);   // 追加
      var radius:Point = new Point(eventObject.radiusX, eventObject.altitude);   // 追加
      // setRotation();
      setRotation(center, radius);
    }
    // private function moveX():void {
    private function moveX(nCenterX:Number, nRadiusX:Number):void {
      // x = center.x+cos*radius.x*currentScale;
      x = nCenterX+cos*nRadiusX*currentScale;
    }
    // private function moveY():void {
    private function moveY(nCenterY:Number, nRadiusY:Number):void {
      // y = center.y+sin*radius.y*currentScale;
      y = nCenterY+sin*nRadiusY*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      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 {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

Flashムービー(FLA)ファイルのフレームアクションはスクリプト10-001を使って[ムービープレビュー]で確かめると、楕円軌道を描くEllipticMotionインスタンスのアニメーションがマウスポインタの位置に応じて変化します。マウスポインタを水平方向にステージ中央から外側に動かすと回転のスピードが増し、垂直方向に上下させると最前面のインスタンスが追随するように軌道を変えます(図10-019)。

図10-019■回転角や軌道の高さなどの情報がRotationEventオブジェクトによってリスナーに配信される
マウスポインタの位置に応じて、EllipticMotionインスタンスの回転スピードや楕円軌道の高さが変化する。

10-06 スーパークラスのメソッドを再定義する ー オーバーライド
複数のクラスを使ってプロジェクトやライブラリを構築していくと、処理のバリエーションがほしくなることもあります。たとえば、本章で作成してきたEllipticMotionインスタンスのアニメーションに変化をつけてみたいとします。ここでは簡単な例として、マウスポインタを上に動かしたとき、今とは逆に、楕円軌道が垂直方向に下がって回転するインスタンスを加えてみます(図10-020)。

図10-020■マウスポインタと楕円軌道の上下の動きが逆になるアニメーション
マウスポインタを上に動かすと、アニメーションの楕円軌道が垂直方向に下がるインスタンスを追加。

各EllipticMotionインスタンスの楕円軌道の上下、つまりy軸方向の半径を扱っているのは、rotate()メソッドです(前掲スクリプト10-010)。この程度の変更であれば、フラグとなるプロパティでもひとつ定めて、その値によって上下の動きを逆にすれば簡単です。しかし、本節ではもっと大幅な処理の書替えが求められる場合でも対応できる方法をご紹介します。

メソッドのオーバーライド
新たなクラスEllipticMotion2を定義して、rotate()メソッドを書替えてしまいましょう。そして、MovieClipシンボルも別に用意して([複製]で構いません)、[リンケージプロパティ]ダイアログボックスで[クラス]にEllipticMotion2を設定します(図10-021)。

図10-021■別のMovieClipシンボルに[リンケージプロパティ]ダイアログボックスで新たな[クラス]を設定
シンボルは複製して、[リンケージプロパティ]ダイアログボックスで[クラス]に新たなEllipticMotion2を設定。

問題は、このEllipticMotion2をどうやって定義するかです。EllipticMotionクラスをコピーして名前を変え、rotate()メソッドを書替えれば、作業としては簡単そうです。しかし、クラス名とrotate()メソッド以外、EllipticMotionとまったく同じクラスをつくるというのも無駄な気がします。

これが新しいメソッドの追加だったら、何も迷わないでしょう。EllipticMotionクラスのサブクラスを定義し、新しいメソッドだけを書き加えればよいことです。これまで何度も練習してきました。しかし実は、継承するスーパークラスのメソッドは、サブクラスで再定義つまり書き直しができるのです。これをメソッドの「オーバーライド」(override)といいます。

もっとも、スーパークラスと同じメソッドをサブクラスにただ定義し直しただけでは[コンパイルエラー]になります(図10-022)。エラーコードの1024を[ActionScript 3.0コンポーネントリファレンスガイド]の[コンパイルエラー]のページで調べると、スーパークラスのメソッドをオーバーライドするには、「override属性を使用して明示的に宣言する必要があります」と説明されています。

図10-022■継承するスーパークラスのメソッドを単純に再定義するとコンパイルエラー
エラーコードの1024は、メソッドの定義にoverride属性が必要だとする。

override属性は、メソッドの定義において、functionキーワードの前に指定します。そして、override属性の指定に続くメソッド定義の冒頭のステートメントは、基本的に再定義しようとするスーパークラスのメソッドから、コピー&ペーストします。つまり、アクセス制御の属性からメソッド名、引数の数とデータ型、メソッドの戻り値のデータ型まで、サブクラスでもまったく同じように定義する必要があります(Word 10-003)。

Word 10-003■override属性キーワード
つぎのシンタックスで、スーパークラスのメソッドを再定義(オーバーライド)します。

override [アクセス制御属性] function メソッド名([引数:データ型]):戻り値のデータ型 {}

スーパークラスから継承されるメソッドを、サブクラスで再定義して書替える際に、その属性として指定します。サブクラスに再定義するメソッドは、継承されるメソッドと、その名前、および引数の数とそれぞれのデータ型、戻り値のデータ型、ならびにアクセス制御の属性がすべて同じでなければなりません。

プロパティや定数は、メソッドではないので、オーバーライドすることはできません。けれど、get/setアクセサメソッド(08-04「get/setアクセサメソッド」参照)でしたら、実質はメソッドですから再定義は可能です。

静的なメソッドは継承されないため、オーバーライドできません。静的なメソッドが継承されないことについては、[ヘルプ]の[ActionScript 3.0 のプログラミング] > [ActionScriptのオブジェクト指向プログラミング] > [クラス] > [メソッド]または筆者サイトFumioNonaka.com「静的プロパティ・メソッドの継承」<http://www.fumiononaka.com/TechNotes/Flash/FN0701001.html>をお読みください。


Maniac! 10-005■[ヘルプ]のoverride属性キーワードの説明
[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド]で「override属性キーワード」の項を調べると、継承したメソッドをオーバーライドするには「名前、数値、パラメータの型、および戻り値の型は完全に一致する必要があります」と述べています。

この文には、2点問題があります。第1に、「数値」が何を指すのかわかりません。原文を見ると、"number, type of parameters"とあり、必ずしも明確ではありませんが、文脈として「パラメータの数と型」を意味すると解されます。第2に、アクセス制御の属性も同じにすべきとの記載が抜けています。

したがって、EllipticMotionクラスを継承する新たなクラスEllipticMotion2において、継承するメソッドrotate()の再定義(オーバーライド)は、つぎのような記述で始めるべきことになります。

override internal function rotate(eventObject:RotationEvent):void {

今回、rotate()メソッドの処理の中身としては、楕円軌道のy軸方向の半径を、EllipticMotionクラスの値とは正負逆にして、つぎのように指定することにします。値をマイナスにしましたので、マウスポインタの上下とインスタンスは逆方向に動きます。また、係数0.2を掛合わせたため、動きの度合いは小さくなります。

var radius:Point = new Point(eventObject.radiusX, -eventObject.altitude*0.2);

スーパークラスEllipticMotionのメソッドをオーバーライドするrotate()が定義された新たなクラスEllipticMotion2は、つぎのスクリプト10-011のとおりです。

スクリプト10-011■スーパークラスのメソッドをオーバーライドしたEllipticMotion2クラス

// ActionScript 3.0クラス定義ファイル: EllipticMotion2.as
package rotation{
  import flash.geom.Point;
  public class EllipticMotion2 extends EllipticMotion {
    private var sensitivity:Number = 0.2;
    public function EllipticMotion2() {
    }
    override internal function rotate(eventObject:RotationEvent):void {
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);
      var radius:Point = new Point(eventObject.radiusX, -eventObject.altitude*sensitivity);
      setRotation(center, radius);
    }
  }
}


Tips 10-010■オーバーライドされたスーパークラスのメソッドを呼出す
オーバーライドされたスーパークラスのメソッドは、サブクラスの再定義により上書きされてなくなってしまう訳ではありません。

呼出しをサブクラスのメソッドが先に受取ってしまうため、スーパークラスのメソッドにアクセスできないというだけのことです。ちょうど変数について、タイムライン変数と同名のローカル変数が宣言されたとき、ローカル変数の方が先にアクセスされて値を返すのと同じです(02-06「タイムライン変数とローカル変数」参照)。

superステートメントを、コンストラクタからの呼出しとは異なるつぎのシンタックスで用いると、オーバーライドされたスーパークラスのメソッドを呼出すことができます。

super.メソッド名([引数])

このシンタックスは、サブクラスのコンストラクタ内にかぎらず、インスタンスメソッドの本体内からも使うことができます。渡す引数の数やデータ型は、呼出すメソッドの定義にしたがいます。

サブクラスからのアクセスを許す
EllipticMotion2クラスの作成にともない、スーパークラスのEllipticMotionにも、一部修正が必要とされます。EllipticMotionクラスのメソッドは、とくに必要がある場合を除いて、アクセス制御の属性はprivateで指定されています。しかし、サブクラスのEllipticMotion2から呼出しているメソッドがあります。get/setアクセサメソッドのdegreeとsetRotation()メソッドです。これらのアクセス制御の属性は、サブクラスからのアクセスを許すprotectedに指定を変更します(スクリプト10-012)。

スクリプト10-012■一部のメソッドをprotected属性に変更したEllipticMotionクラス

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    // private function get degree():Number {
    protected function get degree():Number {
      return _degree;
    }
    // private function set degree(nDegree:Number):void {
    protected function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function get indexZ():Number {
      return getIndexZ();
    }
    public function setPosition(nDegree:Number, center:Point, radius:Point):void {
      degree = nDegree;
      setRotation(center, radius);
    }
    // private function setRotation(center:Point, radius:Point):void {
    protected function setRotation(center:Point, radius:Point):void {
      moveX(center.x, radius.x);
      moveY(center.y, radius.y);
      scale();
      blur();
    }
    internal function rotate(eventObject:RotationEvent):void {
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);
      var radius:Point = new Point(eventObject.radiusX, eventObject.altitude);
      setRotation(center, radius);
    }
    private function moveX(nCenterX:Number, nRadiusX:Number):void {
      x = nCenterX+cos*nRadiusX*currentScale;
    }
    private function moveY(nCenterY:Number, nRadiusY:Number):void {
      y = nCenterY+sin*nRadiusY*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      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 {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

あとのクラスRotationController(前掲スクリプト10-009)およびRotationEvent(前掲スクリプト10-008)は、これまでのままとくに修正すべき点はありません。Flashムービー(FLA)ファイルのフレームアクションにはEllipticMotion2クラスをimport宣言し、そのインスタンスをEllipticMotionインスタンスと交互に同数RotationControllerインスタンスに加えてみることにしましょう(スクリプト10-013)。

スクリプト10-013■EllipticMotionとEllipticMotion2インスタンスを生成するフレームアクション

// フレームアクション
import flash.geom.Point;
import flash.display.MovieClip;
import rotation.EllipticMotion;
import rotation.EllipticMotion2;
import rotation.RotationController;
var nCount:int = 4;
var center:Point = new Point(stage.stageWidth/2, stage.stageHeight/2);
var radius:Point = new Point(120, 50);
var myRotation:RotationController = new RotationController(this, center, radius);
for (var i:int = 0; i<nCount; i++) {
  myRotation.addListener(new EllipticMotion());
  myRotation.addListener(new EllipticMotion2());
}
myRotation.startRotation();

[ムービープレビュー]を確かめると、マウスポインタの上下の動きに対して、楕円軌道を回転するEllipticMotionとEllipticMotion2の各インスタンスが交互に上がったり下がったりします(図10-023)。

図10-023■EllipticMotionとEllipticMotion2のインスタンスを交互に同数RotationControllerインスタンスに加える
マウスポインタを上下すると、楕円軌道を回転するEllipticMotionとEllipticMotion2の各インスタンスが交互に上がり下がりする。

10-07 クラスに資格を与える ー インターフェイス
クラスはデータ型として、プロパティ(変数)やメソッド(関数)の引数に指定できます。すると、そのプロパティやメソッドの引数には、そのクラスまたはそれを継承するサブクラスのインスタンスしか値として受入れられなくなります。

前節(10-06「スーパークラスのメソッドを再定義する ー オーバーライド」)ではEllipticMotionクラスを継承するサブクラスEllipticMotion2で、スーパークラスのメソッドをオーバーライドしました。すると、EllipticMotion2は、EllipticMotionとは異なったクラスでかつメソッドのふるまいも変えながら、そのインスタンスがEllipticMotionのデータ型として扱えました。つまり、EllipticMotion2インスタンスが、RotationControllerクラスのaddListener()メソッドに引数として渡せたのです。

インターフェイス(interface)の役割と定義の仕方
では、メソッドの引数には、データ型に指定されたクラスとそのサブクラスしか渡せないのでしょうか。実は、まったく別のクラスであっても、特定のメソッドで扱えるいわばお墨付きを与えて、引数に渡せる方法があります。それは「インターフェイス」(interface)という仕組みで、クラスに一定のメソッドが予め決められた名前や、引数とデータ型、戻り値で定義されていることを保障するものです。

インターフェイスのポイントはふたつあります。ひとつは、インターフェイスが継承とは別に指定できることです。したがって、継承関係にないクラスであっても、同じインターフェイスを指定することができます。なお、指定されたクラスの側からは、インターフェイスを「実装」(implement)するといいます。もうひとつのポイントは、インターフェイスがデータ型として指定できることです。すると、まったく異なるクラスのインスタンスであっても、そのインターフェイスを実装してさえいれば引数に渡せるのです。

いってみれば、継承が血筋でその正当性を示すのに対し、インターフェイスは血縁を排してデータ型に適する資格を与えようというのです。


継承は血筋、インターフェイスは資格を示す。


Maniac! 10-006■interfaceのカタカナ表記
コンピュータの一般的な用語としては、"interface"は「インターフェース」と表記されることが多いようです(広辞苑第6版、IT用語辞典e-Words<http://e-words.jp/>など)。しかし、ここで学習するプログラミングの機能としては、Javaなどで「インタフェース」という語が頻繁に見受けられます。本書では[ヘルプ]の記載にしたがって、「インターフェイス」と呼ぶことにます。なお、Javaのテキストにも「インターフェイス」と表記しているものはあります。

インターフェイスは以下のように、実装すべきメソッドを定義します。定義の仕方は、クラスと似ています。しかし、大きく3点つ異なる点があります。

package [パッケージ名]{
  [アクセス制御の属性] interface インターフェイス名 {
    function メソッド名([引数:データ型]):戻り値のデータ型;
  }
}

第1に、インターフェイスは、interfaceキーワードで定義します。クラスと同じようにその名前を指定し、アクセス制御の属性も指定できます。第2に、定義できるのはメソッドのみで、プロパティや定数は宣言できません。また、インターフェイスからインスタンスはつくれませんので、コンストラクタメソッドは定義しません。第3に、メソッド本体の処理(「実装」といいます)は書かず、その名前および引数とデータ型、ならびに戻り値のデータ型のみを定義します。なお、メソッドにアクセス制御の属性は指定できません。

インターフェイスの定義と実装
それでは、実際にインターフェイスを定義して、それを実装してみます。お題としては前節と同じく、EllipticMotionクラスに定義されているメソッドrotate()を、ふたとおりの動きに書き分けます。

ただし、今回はオーバーライドするのでなく、メソッドはEllipticMotionクラスから切離し、クラスRotatorとRotator2を定義して、その中にそれぞれのrotate()メソッドを記述します。そして、インターフェイスIRotatorを定義したうえで、これらふたつのクラスに実装するという構成です。そして、RotationControllerクラスのaddListener()メソッドは、引数をインターフェイスIRotatorで型指定します。すると、クラスRotatorとRotator2のインスタンスがともに引数として渡せるようになります(図10-024)。

図10-024■インターフェイスを実装してそのデータ型で指定した引数として渡す
クラスRotatorとRotator2にインターフェイスIRotatorを実装すると、引数をそのデータ型で指定したメソッドRotationController.addListener()に渡せる。

ところで、この構成ではクラスRotatorとRotator2は、ともにEllipticMotionクラスを継承しています。しかし、前述のとおり、インターフェイスの仕組みでは、ふたつのクラスが同じスーパークラスをもつ必要はありません。今回は新たに別のクラスを作成する手間は省き、インターフェイスの定義とその実装の仕方を端的にご説明するために、EllipticMotionクラスを使い回すことにしました。

それでは、まずインターフェイスIRotatorから定義します(スクリプト10-014)。インターフェイスの名前は、頭文字をIとすることが多いです。パッケージ(package)の試用やimport宣言は、クラスと変わりません。インターフェイスは、前述のとおり、interfaceキーワードで定義します。インターフェイスに対するアクセス制御の属性は、publicinternalのみ使えます。なお、この点はクラスでも同じです。

スクリプト10-014■インターフェイスIRotatorの定義

// ActionScript 3.0インターフェイス定義ファイル: IRotator.as
package rotation{
  import rotation.RotationEvent;
  public interface IRotator {
    function rotate(eventObject:RotationEvent):void;
  }
}

インターフェイスIRotatorは、RotationControllerクラスのaddListener()メソッドの引数にデータ型として指定します。つまり、addListener()メソッドで扱うことのできる資格を定めることになるのです。addListener()メソッドに引数として渡されたインスタンスに対しては、rotateイベントが発生します。したがって、rotate()メソッドが備わっていなければなりません。そこで、インターフェイスIRotatorには、メソッドrotate()を定義することになります。インターフェイスを実装すべきクラスがメソッドを再定義していないと、[コンパイルエラー]が起こります(図10-025)。

図10-025■インターフェイスを実装したクラスはメソッドを再定義しなければならない
インターフェイスに定義されたメソッドを再定義していないと[コンパイルエラー]が生じる。

インターフェイスを実装したクラスは、インターフェイスのメソッドを必ず再定義つまりオーバーライドしなければならない決まりです。したがって、インターフェイスのメソッド定義には処理内容を書く必要はなく、また書くことはできません。インターフェイスのメソッドには、その名前および引数とそのデータ型、ならびに戻り値のデータ型のみを定義します。メソッドにアクセス制御の属性は、指定できません。ActionScript 3.0では、インターフェイスのメソッドは、それを実装するクラスがpublic属性を指定すべきことに決まっているからです。

ではつぎに、インターフェイスIRotatorを実装するふたつのクラスRotator(スクリプト10-015)とRotator2(スクリプト10-016)の定義です。インターフェイスは、クラスにimplements定義キーワードで実装します。継承のextends定義キーワードとは別途の指定であることがポイントです。つまり、血筋である継承とは別に、資格としてのインターフェイスを指定することができるのです。

インターフェイスを実装するメソッドは、必ずpublic属性で指定しなければなりません。メソッドの名前、引数とそのデータ型、および戻り値のデータ型は、インターフェイスに定義されていたとおりに記述します。この点は、スーパークラスのメソッドをオーバーライドするときと同じです。ただし、override属性キーワードは使いません。

スクリプト10-015■インターフェイスIRotatorを実装するクラスRotatorの定義

// ActionScript 3.0クラス定義ファイル: Rotator.as
package rotation{
  import flash.geom.Point;
  public class Rotator extends EllipticMotion implements IRotator {
    public function Rotator() {
    }
    public function rotate(eventObject:RotationEvent):void {
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);
      var radius:Point = new Point(eventObject.radiusX, eventObject.altitude);
      setRotation(center, radius);
    }
  }
}


スクリプト10-016■インターフェイスIRotatorを実装するクラスRotator2の定義

// ActionScript 3.0クラス定義ファイル: Rotator2.as
package rotation{
  import flash.geom.Point;
  public class Rotator2 extends EllipticMotion implements IRotator {
    private var sensitivity:Number = 0.2;
    public function Rotator2() {
    }
    public function rotate(eventObject:RotationEvent):void {
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);
      var radius:Point = new Point(eventObject.radiusX, -eventObject.altitude*sensitivity);
      setRotation(center, radius);
    }
  }
}


Tips 10-011■複数のインターフェイスを実装する
インターフェイスの実装(implements定義キーワード)は、スーパークラスの継承(extends定義キーワード)とは異なり、複数のインターフェイスをカンマ(,)区切りで指定することができます。

class クラス implements インターフェイス0, インターフェイス1, ...

ふたつのクラスRotatorとRotator2にメソッドrotate()を定義する替わりに、あとでスーパークラスのEllipticMotionからrotate()メソッドを削除します。処理の内容そのものは、前節でつくったEllipticMotion2と基本的に同じですから、細かい説明は要らないでしょう。rotate()メソッドにおけるPoint型のローカル変数radiusのyプロパティの計算値が、RotatorはEllipticMotionと、Rotator2はEllipticMotion2と同じ式になっています。

それでは、RotationControllerクラスの修正に入ります。メソッドaddListener()とstartRotation()を、以下のように書替えます。当初の予定どおり、addListener()メソッドの引数はインターフェイスIRotatorで型指定します。さらに、startRotation()メソッドでインスタンスを納める配列instances_arrayから取出すエレメント(ローカル変数my_mc)も、データ型をIRotatorとしています。

// public function addListener(my_mc:EllipticMotion):uint {
public function addListener(my_mc:IRotator):uint {
  var nInstances:uint = instances_array.push(my_mc);
  return nInstances;
}
public function startRotation():void {
  var nInstances:uint = instances_array.length;
  var nDegree:Number = 360/nInstances;
  // var my_mc:EllipticMotion;
  var my_mc:IRotator;
  var _mc:EllipticMotion;
  for (var i:int = 0; i<nInstances; i++) {
    my_mc = instances_array[i];
    if (my_mc is EllipticMotion) {
      _mc = my_mc as EllipticMotion;
      // timeline_mc.addChild(my_mc);
      timeline_mc.addChild(_mc);
      // my_mc.setPosition(i*nDegree, center, radius);
      _mc.setPosition(i*nDegree, center, radius);
    }
    addEventListener(RotationEvent.ROTATE, my_mc.rotate);
  }
  timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);
}

startRotation()メソッドには、配列instances_arrayから取出したエレメントがEllipticMotionインスタンスであることを前提とした処理があります。DisplayObjectContainer.addChild()メソッドでインスタンスを表示リストに加えたり、EllipticMotionクラスのメソッドsetPosition()を呼出すステートメントです。

もちろん、クラスRotatorとRotator2は、ともにEllipticMotionクラスを継承しています。しかし、インターフェイスというのは、継承とは切離して、クラスのデータ型を指定できる仕組みです。EllipticMotionクラスは継承せずにインターフェイスIRotatorを実装するクラスが、将来に定義されるかもしれません。

そのため、上記スクリプトでは条件判定の処理を加えて、EllipticMotionインスタンスかどうかをis演算子(Word 09-007「is演算子」)で調べています。そして、EllipticMotionインスタンスであることが確かめられたら、データ型をas演算子(Word 10-004)でEllipticMotionに変換したうえで、上述のステートメントを実行しています。

Word 10-004■as演算子
つぎのシンタックスで、式の値を指定のデータ型として評価し直します。

式 as データ型

式の値が指定のデータ型で扱えるのは、値がそのデータ型のインスタンスであるか、そのデータ型のサブクラスのインスタンスである場合、あるいはそのインスタンスの属するクラスがインターフェイスを実装する場合です。

式の値が指定のデータ型で扱えるときは、値をそのデータ型に変換します。扱えない場合には、nullを返します。


Tips 10-012■キャスト
あるデータ型を明示的に他のデータ型に変換することは「キャスト」と呼ばれます([ヘルプ]の[ActionScript 3.0のプログラミング] > [ActionScript言語とシンタックス] > [データ型] > [型変換]参照)。キャストについては、as演算子を使う以外に、つぎのようにして式を指定のクラスのデータ型に変換することもできます。

クラス(式)

たとえば、前記のIRotatorでデータ型を指定した変数(my_mc)の値は、つぎのステートメントでEllipticMotionにキャストすることが可能です。

_mc = EllipticMotion(my_mc);

Maniac! 10-007■ふたつのキャストの違い
キャストしようとする値が指定のデータ型で評価できるなら、大抵はas演算子を使っても、「クラス()」の明示的な型変換によっても、結果は異なりません。ふたつの手法で違ってくる場合を、ふたつだけ紹介します。

ひとつは、文字列を数値に変換することは、as演算子ではできません。

var number_str:String = "1";
trace(number_str as Number);   // 出力: null
trace(Number(number_str));   // 出力: 1

もうひとつ、配列へのキャストには、as演算子を用いる必要があります。Array()は配列を作成する関数になってしまうので、引数をエレメントとする入れ子の配列が作成されてしまうからです(以下の例では、変数_arrayには、[[0, 1]]という配列が代入されます)。

var oArray:Object = [0, 1];
trace(oArray as Array);   // 出力: 0,1
var _array:Array = Array(oArray);
trace(_array[0]);   // 出力: 0,1
trace(_array[0] is Array, _array.length);   // 出力: true 1

もっとも、Array()でキャストしようとすれば、警告(Warning)が一応出されます。「一応」と述べた理由は2点です。第1に、[コンパイルエラー]パネルに表示されるWarningのコードが1112なのに対して、[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド] > [コンパイラ警告]には1113として掲載されています。しかも、[ヘルプ]には警告メッセージ以上の説明はありません。

図10-026■Array()でキャストしようとした場合の警告(Warning)
[出力]パネルとドッキングされていると、[コンパイルエラー]パネルは背後に隠れてしまうので注意。

そして第2に、以下のように警告するメッセージの後段の意味がわかりません。

Warning: 1112: Array(x)はnew Array(x)と同じように動作します。Array型に値をキャストして、Array(x)の代わりに式xをArrayとして使用します。

後段の文は、英語版の警告メッセージを調べると、"To cast a value to type Array use the expression x as Array instead of Array(x)."となっています。つまり、「値をArray型にキャストするには、Array(x)でなくx as Arrayの式を使います」ということです。as演算子を日本語にしてしまった誤訳でしょう。

ふたつのメソッドaddListener()とstartRotation()に修正を加えたクラスRotationControllerの定義は、つぎのスクリプト10-017のとおりです。

スクリプト10-017■addListener()とstartRotation()に修正を加えたクラスRotationControllerの定義

// ActionScript 3.0クラス定義ファイル: RotationController.as
package rotation{
  import flash.display.MovieClip;
  import flash.geom.Point;
  import flash.events.Event;
  import flash.events.EventDispatcher;
  public class RotationController extends EventDispatcher {
    private var timeline_mc:MovieClip;
    private var center:Point;
    private var radius:Point;
    private var instances_array:Array = new Array();
    private var sensitivity:Number = 0.1;
    private var maxSpeed:Number = 15;
    public function RotationController(my_mc:MovieClip, myCenter:Point=null, myRadius:Point=null) {
      timeline_mc = my_mc;
      if (myCenter is Point) {
        center = myCenter;
      } else {
        center = new Point(my_mc.stage.stageWidth/2, my_mc.stage.stageHeight/2);
      }
      if (myRadius is Point) {
        radius = myRadius;
      } else {
        radius = center.clone();
        radius.normalize(radius.length*0.7);
      }
    }
    // public function addListener(my_mc:EllipticMotion):uint {
    public function addListener(my_mc:IRotator):uint {
      var nInstances:uint = instances_array.push(my_mc);
      return nInstances;
    }
    public function startRotation():void {
      var nInstances:uint = instances_array.length;
      var nDegree:Number = 360/nInstances;
      // var my_mc:EllipticMotion;
      var my_mc:IRotator;
      var _mc:EllipticMotion;
      for (var i:int = 0; i<nInstances; i++) {
        my_mc = instances_array[i];
        if (my_mc is EllipticMotion) {
          _mc = my_mc as EllipticMotion;   // 追加
          // timeline_mc.addChild(my_mc);
          timeline_mc.addChild(_mc);
          // my_mc.setPosition(i*nDegree, center, radius);
          _mc.setPosition(i*nDegree, center, radius);
        }
        addEventListener(RotationEvent.ROTATE, my_mc.rotate);
      }
      timeline_mc.addEventListener(Event.ENTER_FRAME, rotate);
    }
    private function rotate(eventObject:Event):void {
      var myEventObject:RotationEvent = new RotationEvent();
      var speed:Number = (-center.x+timeline_mc.stage.mouseX)*sensitivity;
      speed = Math.min(Math.max(speed, -maxSpeed), maxSpeed);
      var altitude:Number = -center.y+timeline_mc.stage.mouseY;
      altitude = Math.min(Math.max(-radius.y, altitude), radius.y);
      myEventObject.setRotationInfo(speed, altitude, center, new Point(radius.x, radius.y));
      dispatchEvent(myEventObject);
      setOrder();
    }
    private function setOrder():void {
      var nInstances:uint = instances_array.length;
      instances_array.sortOn("indexZ", Array.NUMERIC);
      for (var i:int=0; i<nInstances; i++) {
        timeline_mc.setChildIndex(instances_array[i], i);
      }
    }
  }
}


Maniac! 10-008■EllipticMotionクラスは継承せずにインターフェイスIRotatorを実装するクラス
EllipticMotionクラスは継承せずにインターフェイスIRotatorを実装するクラスが定義される場合を考えて、前掲RotationControllerクラス(スクリプト10-017)のメソッドstartRotation()では、EllipticMotionインスタンスであることを確かめたうえで、そのsetPosition()メソッドを呼出しました。

しかし、メソッドの第1引数に配置する角度を指定するとき、ローカル変数iにはEllipticMotionクラスを継承しないインスタンスも数にカウントされています。それ以前に、等間隔の角度nDegreeを計算するときに使ったプロパティinstances_arrayのArray.lengthプロパティ値には、EllipticMotion以外のインスタンスもエレメントとして含まれてしまいます。これらの対応はしなくてよいでしょうか。

現段階では、EllipticMotionを継承しないIRotatorを実装するクラスがどのようなものになるのかデザインはできていません。DisplayObjectクラスは継承し、表示リストに加わるのかどうかも定かでない状況です。インスタンスが表示リストに入らないのなら、RotationControllerクラスのsetOrder()メソッドも修正が必要でしょう。表示リストに加わらないインスタンスは、プロパティinstances_arrayに納めず、別に管理する必要があるかもしれません。

したがって、新クラスの設計が具体的にできるまでは、インターフェイスIRotatorでデータ型を指定した値に対して、最低限の処理を加えるだけでよいでしょう。

残る修正は、EllipticMotionクラスです。これは、サブクラスRotatorとRotator2がそれぞれ独自に定義することになったメソッドrotate()を削除するだけで足ります(スクリプト10-018)。

スクリプト10-018■rotate()を削除したクラスEllipticMotionの定義

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package rotation{
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  import flash.geom.Point;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var _degree:Number = 0;
    private var cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
    private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
    private var minScale:Number = 0.8;
    private var focalLength:Number = minScale/(1-minScale);
    private var currentScale:Number;
    public function EllipticMotion() {
    }
    protected function get degree():Number {
      return _degree;
    }
    protected function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
      currentScale = focalLength/(focalLength+getIndexZ(1, 0));
    }
    public function get indexZ():Number {
      return getIndexZ();
    }
    public function setPosition(nDegree:Number, center:Point, radius:Point):void {
      degree = nDegree;
      setRotation(center, radius);
    }
    protected function setRotation(center:Point, radius:Point):void {
      moveX(center.x, radius.x);
      moveY(center.y, radius.y);
      scale();
      blur();
    }
    /*
    internal function rotate(eventObject:RotationEvent):void {
      degree += eventObject.speed;
      var center:Point = new Point(eventObject.centerX, eventObject.centerY);
      var radius:Point = new Point(eventObject.radiusX, eventObject.altitude);
      setRotation(center, radius);
    }
    */

    private function moveX(nCenterX:Number, nRadiusX:Number):void {
      x = nCenterX+cos*nRadiusX*currentScale;
    }
    private function moveY(nCenterY:Number, nRadiusY:Number):void {
      y = nCenterY+sin*nRadiusY*currentScale;
    }
    private function scale():void {
      scaleX = scaleY = currentScale;
      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 {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

テスト用のFlashムービー(FLA)ファイルのフレームアクションは、前掲スクリプト10-013のクラスEllipticMotionとEllipticMotion2を、RotatorとRotator2に置換えればよいだけです(スクリプト10-019)。[ムービープレビュー]の結果は、EllipticMotionとEllipticMotion2インスタンスを使った場合と同じです(図10-027)。

スクリプト10-019■RotatorとRotator2インスタンスを生成するフレームアクション

// フレームアクション
import flash.geom.Point;
import flash.display.MovieClip;
import rotation.Rotator;
import rotation.Rotator2;
import rotation.RotationController;
var nCount:int = 4;
var center:Point = new Point(stage.stageWidth/2, stage.stageHeight/2);
var radius:Point = new Point(120, 50);
var myRotation:RotationController = new RotationController(this, center, radius);
for (var i:int = 0; i<nCount; i++) {
  myRotation.addListener(new Rotator());
  myRotation.addListener(new Rotator2());
}
myRotation.startRotation();


図10-027■RotatorとRotator2インスタンスがそれぞれのrotate()メソッドでアニメーションする
アニメーションは、EllipticMotionとEllipticMotion2インスタンスの場合と同じ。


Column 10 2進数・16進数とビット演算
2進数や16進数の表し方と、2進数を基礎としたビット演算について簡単にご紹介します。

Word 10-005■ビット
ビット(bit)は、コンピュータが扱う最小の単位で、2値つまりふたつの状態のうちのひとつを示します。2値を0か1とすれば、2進数の1桁が1ビットになります。

ビット演算は、2進数をビットつまり1桁ごとに処理します。

[*筆者用参考] Wikipedia「ビット

2進数
2進数は0と1のみで数を表します。10進数が9に1を加えた10で繰り上がるのに対して、2進数は1に1を足した2で桁が上ります。したがって、2進数の10(= 1+1)は、10進数の2になる訳です。同じように、2進数の100と1000は、それぞれ10進数の4と8になります。ですからたとえば、2進数の1010は、つぎのように10進数の10と計算できます。

1×8+0×4+1×2+0×1 = 10

8は23、4は22、2は21、そして1は20ですから、上の式はつぎのように書直すこともできます。

1×23+0×22+1×21+0×20 = 10

一般に、2進数で表した数の各桁の小さい方から順に、n1、n2、...、nkとすると、これを10進数にするのはつぎの式になります。

nk×2k-1+...+n2×21+n1×20

また、10進数についても、各桁の数を小さい方から順に、n1、n2、...、nkとすれば、べき乗(累乗)される数を10に替えて、つぎのように示すことができます。

nk×10k-1+...+n2×101+n1×100

この考え方はさらに一般化できますので、i進数(iは正の整数)はつぎの式で表されます。

nk×ik-1+...+n2×i1+n1×i0

複数のフラグをひとつの整数で表す
2進数を使うと、複数の2値のフラグをひとつの数値で効率的に表すことができます。

たとえば、2値のフラグとなるBoolean型の変数が4つあったとします。仮に、変数をa、b、c、dとし、それぞれの値がtruefalsetruefalseだったとしましょう。このとき、4桁の2進数を考え、頭から変数a、b、c、dを当てはめて、trueを1、falseを0とすれば、4つのフラグの値は以下のように2進数1010で表されます。したがって、10進数の10でフラグaとcがオン(true)であることを示せます。

フラグの変数 a b c d
変数の値 true false true false
2進数の桁番号 4 3 2 1
桁に乗じる値 23 22 21 20
2進数の桁の値 1 0 1 0

このような2進数を使った値の決め方は、ActionScript 3.0でも利用されています。たとえば、10-04「配列を並べ替える」でご紹介したArrayクラスの定数は、各値が2の累乗で定められていました(前掲表10-004)。つまり、各値を加算して、複数の定数の組合わせが示せます。

Arrayクラスの定数 NUMERIC RETURNINDEXEDARRAY UNIQUESORT DESCENDING CASEINSENSITIVE
定数の値 24 = 16 23 = 8 22 = 4 21 = 2 20 = 1

たとえば、大文字小文字を区別せずにアルファベットの逆順というのは、Array定数のCASEINSENSITIVE(1 = 20)とDESCENDING(2 = 21)を足合わせて、2進数で11、10進数では3になります。よって、Array.sort()メソッドの引数に3を渡せば、配列エレメントをその指定の順序で並べ替えることができます。

var my_array:Array = ["flash", "dreamweaver", "Fireworks", "Illustrator", "Photoshop"];
my_array.sort(3);
trace(my_array); // 出力: Photoshop,Illustrator,flash,Fireworks,dreamweaver

ビット単位の論理演算子
[ヘルプ]でArray.sort()Array.sortOn()メソッドの項を見ると、Array定数を複数指定する場合に、加算演算子+ではなく、ビット単位の論理和演算子|を用いるよう解説されています。結論として、Array定数を指定するに当たっては、どちらの演算子を使っても同じ結果になります。ただ、2進数をビット単位で扱う演算子は、コンピュータのデジタル(1/0)のロジックにより近く、処理効率で有利な面があります。

[*筆者用参考] ConquestArrow.addEventListener();「ActionScript 3最適化・高速化Tips簡易まとめ」、polygonal labs「Bitwise gems - fast integer math

ビット単位の論理演算子は、2進数を各桁ごとに演算し、他の桁には影響を与えません。つまり、繰り上がりや繰り下がりがなく、結果は0か1のいずれかになります。オペランド(被演算子)も0か1ですので、4とおりの組合わせが考えられ、ビット単位の論理和演算子|の演算結果は下表10-006のとおりです。

表10-006■ビット単位の論理和演算子|の演算結果
オペランド 0 1
0 0 1
1 1 1

「論理和」という名称は、条件を指定するときの論理和演算子||にも使われていました。この演算子は、オペランドがブール(論理)値の場合、いずれか一方がtrueであればtrueを返します。ビット単位の論理和演算子|も、同じように、オペランドのいずれか一方が1であれば1を返します。したがって、2進数で表記した1010 | 1100の演算結果は、2進数の1110となります。

Tips 10-013■2進数と10進数の変換
本文の「1010 | 1100」という演算は、もちろんそのままスクリプトでは試せません。数値が10進数として扱われてしまうからです。したがって、2進数を一旦10進数に直して、論理和演算子|を用いなければなりません。

2進数を10進数に数値として変換するには、本文に述べた2の累乗を各桁に掛合わせる式を使う必要があります。しかし、文字列で表した2進数は、perseInt()関数で簡単に10進数に変えられます。

perseInt()関数は、つぎのシンタックスで用います。「底」というのは、何進数として解析・変換するのかという正の整数です。

perseInt(文字列で示した整数, 数値に変換する底)

したがって、文字列で表した2進数の"1010"と"1100"を10進数に変換するには、perseInt()関数を以下のように使います。それぞれの値は、10進数の10と12になります。

var a_str:String = "1010";
var b_str:String = "1100";
var nA:int = parseInt(a_str, 2);
var nB:int = parseInt(b_str, 2);
trace(nA, nB);   // 出力: 10 12

論理和演算子|はオペランド(被演算子)に10進数を指定しても、2進数に直して演算し、結果をまた10進数で返します。10進数の数値を、perseInt()関数とは逆に、2進数の文字列に変換したいときは、Number.toString()メソッドに引数として底を渡します(デフォルトは10)。

var nC:int = nA | nB;
trace(nC);   // 出力: 14
var c_str:String = nC.toString(2);
trace(c_str);   // 出力: 1110

加算演算子+は1+1が2進数の10になることに、ビット単位の論理和演算子|との違いがあります。しかし、Array定数は複数のフラグを合成してひとつの整数にするため、それぞれが2進数の異なった桁に値1をもっていました。したがって、桁ごとのビット単位の演算で、ふたつのオペランドがともに1になる、つまり1 | 1を計算することはあり得ません。そのため、加算演算子+を使った場合と、実際上結果は変わらないのです。

Array定数 2進数で表した値
CASEINSENSITIVE
0
0
0
0
1
DESCENDING
0
0
0
1
0
UNIQUESORT
0
0
1
0
0
RETURNINDEXEDARRAY
0
1
0
0
0
NUMERIC
1
0
0
0
0
*各定数で、1をもつ桁が異なる。

ビット単位の論理演算子には、ほかにビット単位の論理積演算子&とビット単位の排他的論理和演算子^があります。ビット単位の論理積演算子&は、条件の論理積演算子&&と同じ考え方で、オペランドがともに1のときのみ1を返します(他の場合は0)。ビット単位の排他的論理和演算子^は、オペランドの一方のみが1のとき1を返し、それ以外は0を返します。下表10-007と10-008に、その演算結果を示します。

表10-007■ビット単位の論理積演算子&の演算結果
オペランド 0 1
0 0 0
1 0 1

表10-008■ビット単位の排他的論理和演算子^の演算結果
オペランド 0 1
0 0 1
1 1 0

●16進法とRGBカラー値
16進数もコンピュータではよく使われます。もちろん、16進数も前掲の式にしたがって、つぎのように表されます。

nk×16k-1+...+n2×161+n1×160

しかし、私たちが使う算用数字には、0から9までしかありません。そこで、それ以降はAからFまでのアルファベットで示されます。もっとも、16進数表記は、Flashを始めWebではよく見かけます。なじみが深いのはRGBカラー値でしょう。このRGBカラーも、赤(R)緑(G)青(B)それぞれに別の桁を割振ることにより、ひとつの値で3つのカラー成分値を表します(図10-028)。

図10-028■RGBカラー値
6桁の16進数で、RGBそれぞれの成分値をひとつの数値で表す。

ActionScriptでは、16進数表記で数値を示すことができます。その場合、数値の前に0xを記述します。たとえば、ブルーの#0000FFは、つぎのように数値として表します。

var nColor:int = 0x0000FF;
trace(nColor);   // 出力: 255

0xFFが255なのは、カラー成分値が0から255までの256階調であることにもとづきます。つまり、16進数で表しているとはいえ、各カラーは2桁を使っていますので、実質256進数といえます。そこで、たとえば各カラー成分値が0から255までの整数で3つの変数に代入されていた場合、3つの成分を合わせたカラー値はつぎの式で求められます。

カラー値 = R成分値×2562+G成分値×2561+B成分値×2560

スクリプトでは、たとえば以下のように試すことができます。

var nR:int = 0xFF;
var nG:int = 0x33;
var nB:int = 0x99;
var nColor:int = nR*256*256+nG*256+nB;
trace(nColor, nColor.toString(16));   // 出力: 16724889 ff3399

AS1&2 Note 10-003■8進数のサポート
ActionScript 2.0/1.0では、0で始まる数値は8進数として扱われました。しかし、ActionScript 3.0では、8進数がサポートされなくなりました。

trace(010);

// ActionScript 2.0/1.0の出力: 8

// ActionScript 3.0の出力: 10

[*筆者用参考] Alternative 笑門来福「知ってた?8進数のナゾ

●カラー値とビット演算
16進数がコンピュータでよく使われるのは、値を2進数4桁で扱うことができるからです。つまり、16進数の0からFは、2進数の0から1111までの値になります。16進数で1桁繰り上がるつまり16を掛合わせるという処理は、2進数を左に4桁ずらすのと同じです。また、16進数で2桁繰り上げるのは256を掛ける演算で、2進数では8桁動かすことになります。

図10-029■256を掛ける処理は2進数で8桁左にずらすのと同じ
16進数で2桁繰り上げるのは、256を掛合わせる処理。2進数で8桁左にずらすのとおなじ結果になる。

ActionScriptでは、この2進数で桁をずらすというビット演算ができます。左に桁をずらすには、ビット単位の左シフト演算子<<を使います。ビット単位の左シフト演算子<<の左側オペランドには2進数としてビット演算する対象の式、右側オペランドには左に動かすビット(桁)数を指定します。すると、前掲の256階調RGBカラー成分値からカラー値を計算するスクリプトは、つぎのように記述することもできます。

var nR:int = 0xFF;
var nG:int = 0x33;
var nB:int = 0x99;
var nColor:int = nR << 16 | nG << 8 | nB;
trace(nColor, nColor.toString(16));   // 出力: 16724889 ff3399


16進数の2桁繰り上がりは、2進数で8桁左にずらせばよい。

逆に、RGBカラー値から各成分値を取出すには、ビット単位の右シフト演算子>>とビット単位の論理積演算子&を使います。以下のスクリプトの処理内容は、ビット演算の練習問題として考えていただくのにちょうどよいでしょう。理解しにくいときは、各値をそれぞれ2進数に変換(toString(2))して確かめ、演算の結果と比べてみることです。

var nColor:int = 0xFF3399;
var nB:int = nColor & 0xFF;
var nG:int = (nColor >> 8) & 0xFF;
var nR:int = (nColor >> 16) & 0xFF;
trace(nR.toString(16), nG.toString(16), nB.toString(16));   // 出力: ff 33 99

[*筆者用参考]「複数のフラグをひとつの整数で表す



Column 11 Eventのサブクラスでclone()とtoString()メソッドをオーバーライドする
[ヘルプ]の[ActionScript 3.0のプログラミング] > [イベントの処理] > [イベントオブジェクト]で「Eventクラスのサブクラス」の項を見ると、つぎのような注意があります。

Eventサブクラスを作成する場合は、clone()とtoString()メソッドをオーバーライドして、サブクラスに固有の機能を提供する必要があります。

これらのメソッドの意義とそのオーバーライドの仕方についてご説明します。

●Event.clone()メソッドがいつ必要か
まずは、Event.clone()メソッドです。[ヘルプ]の説明には、Eventのサブクラスのインスタンスを複製するメソッドだとされています。ただし、このメソッドは、通常とくに呼出す必要はありません。一度配信したイベントオブジェクトを再び配信すると、内部的に呼出されるメソッドだからです。それがどういうことなのか、RotationEventクラス(スクリプト10-008)で確かめてみましょう。

テスト用にクラスとFlashムービー(FLA)のフレームアクションをひとつずつ用意します。クラスは、RotationControllerクラス(スクリプト10-017)からrotate()メソッドの主要部分だけを抜取ったような定義内容です。クラス名をEventTestとします。なお、rotate()メソッドは、テスト用のフレームアクションから呼出しますので、アクセス制御の属性はpublicで指定しています。

package rotation{
  import flash.events.EventDispatcher;
  import flash.geom.Point;
  public class EventTest extends EventDispatcher {
    public function EventTest() {
    }
    public function rotate():void {
      var myEventObject:RotationEvent = new RotationEvent();
      myEventObject.setRotationInfo(0,1,new Point(2,3),new Point(4,5));
      dispatchEvent(myEventObject);
    }
  }
}

Flashムービー(FLA)ファイルのフレームアクションは、EventTestインスタンスにRotationEvent.ROTATEイベントのリスナー関数を登録し、そのインスタンスに対してイベントを配信するためのメソッドrotate()を2度呼出しています(スクリプト10-020)。

スクリプト10-020■EventTestインスタンスにリスナー関数を登録してイベント配信する

// フレームアクション
import rotation.RotationEvent;
import rotation.EventTest;
var test:EventTest = new EventTest();
test.addEventListener(RotationEvent.ROTATE, onRotate);
function onRotate(eventObject:RotationEvent):void {
  trace(eventObject);
}
test.rotate();
test.rotate();

[ムービープレビュー]で確かめると、とくに問題なく処理が実行されます。リスナー関数onRotate()はtrace()関数でイベントオブジェクトを[出力]しますので、[出力]パネルにはrotate()メソッドを呼出した2回分イベントオブジェクトの情報が表示されます(図10-030)。

図10-030■フレームアクションでEventTestインスタンスからイベントを配信する
rotate()メソッドを2度呼出しているので、イベントオブジェクトの情報が[出力]パネルに2行表示される。

クラスEventTestにひとつだけ修正を加えます。rotate()メソッドが呼出されるたびにローカル変数にRotationEventインスタンスを作成するのでなく、インスタンスプロパティmyEventObjectに予め設定しておくことにします(スクリプト10-021)。

スクリプト10-021■EventTestクラスのインスタンスプロパティにRotationEventインスタンスを設定

// ActionScript 3.0クラス定義ファイル: EventTest.as
package rotation{
  import flash.events.EventDispatcher;
  import flash.geom.Point;
  public class EventTest extends EventDispatcher {
    private var myEventObject:RotationEvent = new RotationEvent();
    public function EventTest() {
    }
    public function rotate():void {
      // var myEventObject:RotationEvent = new RotationEvent();
      myEventObject.setRotationInfo(0,1,new Point(2,3),new Point(4,5));
      dispatchEvent(myEventObject);
    }
  }
}

改めて[ムービープレビュー]を見ると、今度はランタイムエラーが起こります。注目していただきたいのは、フレームアクションにおけるrotate()メソッドの最初の呼出しはイベントオブジェクトの情報を正しく[出力]していることです。エラーを発生させているのは、その2度目の呼出しなのです。

図10-031■同じイベントを2度配信するとランタイムエラーが発生
rotate()メソッドの最初の呼出しは、イベントオブジェクトを正しく[出力]。2度目の呼出しがランタイムエラーを発生させている。

イベントの配信つまりEventDispatcher.dispatchEvent()メソッドの呼出しには、毎回新規のイベントオブジェクトを渡さなければならないのです。もし、そのいベントオブジェクトがすでに配信されていた場合、そのインスタンスに対してclone()メソッドが呼出されます。そして、メソッドが返した複製のインスタンスが、つぎの配信に使われる仕組みになっています。

●Event.clone()メソッドのオーバーライド
オーバーライドするclone()メソッドのやるべきことは、したがってイベントオブジェクトを新たに生成し、必要なプロパティを設定したうえで返すということになります。RotationEventクラスであれば、コンストラクタを呼出してインスタンスを作成したら、setRotationInfo()メソッドを使ってプロパティを設定すればよいでしょう。clone()メソッドをオーバーライドしたクラスRotationEventの定義は、つぎのスクリプト10-022のとおりです。

スクリプト10-022■RotationEventクラスでclone()メソッドをオーバーライド

// ActionScript 3.0クラス定義ファイル: RotationEvent.as
package rotation{
  import flash.events.Event;
  import flash.geom.Point;
  public class RotationEvent extends Event {
    public static const ROTATE:String = "rotate";
    public var speed:Number;
    public var altitude:Number;
    public var centerX:Number;
    public var centerY:Number;
    public var radiusX:Number;
    public var radiusY:Number;
    public function RotationEvent() {
      super(ROTATE);
    }
    public function setRotationInfo(mySpeed:Number, myAltitude:Number, center:Point, radius:Point):void {
      speed = mySpeed;
      altitude = myAltitude;
      centerX = center.x;
      centerY = center.y;
      radiusX = radius.x;
      radiusY = radius.y;
    }
    override public function clone():Event {   // オーバーライド
      var myEventObject:RotationEvent = new RotationEvent();
      var center:Point = new Point(centerX, centerY);
      var radius:Point = new Point(radiusX, radiusY);
      myEventObject.setRotationInfo(speed, altitude, center, radius);
      trace("called");   // 確認用
      return myEventObject;
    }
  }
}

オーバーライドしたclone()メソッドには、確認用にtrace()関数のステートメントを加えてあります。フレームアクション(スクリプト10-020)を[ムービープレビュー]で試すと、エラーが起こることなく、イベントオブジェクトの情報が[出力]パネルに2回表示されます(図10-032)。2回目の表示の前に、clone()メソッド内に確認のため記述したtrace()関数により"called"が[出力]されているのは、rotate()メソッドを2回目に実行したときclone()メソッドが呼出されたことを示します。

図10-032■すでに配信されたイベントオブジェクトを再び配信しようとするとclone()メソッドが呼出される
2回目のイベントオブジェクトの情報が表示される前に、clone()メソッド内のtrace()関数が実行され、"called"と[出力]されているのは、2度目にclone()メソッドが呼出されたことを示す。

●Event.toString()メソッドのオーバーライド
toString()メソッドを呼出すと、一般にインスタンスの文字列表現つまりインスタンスの情報を示す文字列が返されます。toString()メソッドは、trace()関数の引数に渡されるなどインスタンスの文字列表現が求められるとき、自動的に呼出されることもあります(Maniac! 03-002「trace()関数はインスタンスのtoString()メソッドを呼出す」参照)。

今、クラスRotationEventの定義ではtoString()メソッドをオーバーライドしていませんので、スーパークラスのEvent.toString()メソッドが呼出されて、つぎのような文字列が返されます。

[Event type="rotate" bubbles=false cancelable=false eventPhase=2]

そこで、RotationEventにtoString()メソッドを再定義して、RotationEventインスタンスのpublicプロパティを文字列表現に加えてみましょう(スクリプト10-023)。この修正については、新たに解説すべきことはありません。toString()メソッドは、super.toString()(Tips 10-010「オーバーライドされたスーパークラスのメソッドを呼出す」参照)でスーパークラスのEvent.toString()メソッドから戻り値を受取ったうえで、String.substring()メソッド(03-05「Stringクラスで文字列を操作する」参照)により文字列の加工を行っています。

スクリプト10-023■RotationEventクラスでtoString()メソッドをオーバーライド

// ActionScript 3.0クラス定義ファイル: RotationEvent.as
package rotation{
  import flash.events.Event;
  import flash.geom.Point;
  public class RotationEvent extends Event {
    public static const ROTATE:String = "rotate";
    public var speed:Number;
    public var altitude:Number;
    public var centerX:Number;
    public var centerY:Number;
    public var radiusX:Number;
    public var radiusY:Number;
    public function RotationEvent() {
      super(ROTATE);
    }
    public function setRotationInfo(mySpeed:Number, myAltitude:Number, center:Point, radius:Point):void {
      speed = mySpeed;
      altitude = myAltitude;
      centerX = center.x;
      centerY = center.y;
      radiusX = radius.x;
      radiusY = radius.y;
    }
    override public function clone():Event {
      var myEventObject:RotationEvent = new RotationEvent();
      var center:Point = new Point(centerX, centerY);
      var radius:Point = new Point(radiusX, radiusY);
      myEventObject.setRotationInfo(speed, altitude, center, radius);
      trace("called");
      return myEventObject;
    }
    override public function toString():String {   // オーバーライド
      var return_str:String = super.toString();
      return_str = return_str.substring(0, return_str.length-1);
      return_str += " speed="+speed;
      return_str += " altitude="+altitude;
      return_str += " centerX="+centerX;
      return_str += " centerY="+centerY;
      return_str += " radiusX="+radiusX;
      return_str += " radiusY="+radiusY;
      return_str += "]";
      return return_str;
    }
  }
}

[ムービープレビュー]を行うと、trace()関数で[出力]されるRotationEventインスタンスの文字列表現が、つぎのように変わります。

[Event type="rotate" bubbles=false cancelable=false eventPhase=2 speed=0 altitude=1 centerX=2 centerY=3 radiusX=4 radiusY=5]

Maniac! 10-009■Object.toString()メソッド
toString()メソッドは、Objectクラスに定義されています。つまり、すべてのクラスがこのメソッドを備えています。ただ、多くのクラスはObject.toString()メソッドをオーバーライドして、それぞれに適した処理を再定義しています。Event.toString()も、オーバーライドしたメソッドです。

カスタムクラスの定義でスーパークラスを指定(extends)しないとき、Objectクラスを直接継承します。その場合、Object.toString()メソッドをオーバーライドするには、override属性キーワードは指定しません(図10-033)。

図10-033■[ヘルプ]のObject.toString()メソッドの説明
ObjectのサブクラスでObject.toString()メソッドをオーバーライドするときは、override属性を指定しない。

[Prev/Next]


作成者: 野中文雄
作成日: 2008年7月4日


Copyright © 2001-2008 Fumio Nonaka.  All rights reserved.