サイトトップ

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

Adobe Flash CS3 Professional ActionScript 3.0

□09 MovieClipシンボルにクラスを定義する

09-01 MovieClipシンボルに設定するクラス定義
07「三角関数を使って楕円軌道のアニメーションを作成する」では、楕円軌道に沿って3D風にアニメーションするフレームアクションを作成しました。その完成させた前掲スクリプト07-005(図09-001)を、クラスとして定義し直したうえで、アニメーションさせるMovieClipシンボルに設定してみます。

図09-001■インスタンスを楕円軌道で3D風にアニメーションさせるフレームアクション(スクリプト07-005)

MovieClipインスタンスを楕円軌道でアニメーションさせるとともに、水平スケールの変更や奥行きを示すぼかしを加えて、3D風の回転を表現した。

MovieClipシンボルに定義するクラスのスタイル
MovieClipシンボルに設定するクラスは、基本的には以下のような記述がベースになります。(1)import宣言の存在と(2)クラスのpublic属性の指定、および(3)extendsキーワードによるMovieClipクラスの継承が特徴です。クラス定義本体については、前章08「カスタムクラスを定義する」でご説明したとおりです。

【MovieClipシンボルに設定するクラス定義の基本】
// ActionScript 3.0クラス定義ファイル: クラス名.as
package [パッケージ名]{
  import flash.display.MovieClip;
  // 他のimport宣言
  public class クラス名 extends MovieClip {
    // クラス定義の本体
  }
}

import宣言については、継承の話をしてから触れることにします。そこでMovieClipシンボルに定義するクラスに必要な第1は、クラスをpublic属性で指定することです。MovieClipインスタンスは、配置されたタイムライン上で操作できる必要があります。つまり、外部からのアクセスを受入れるpublicなクラスのインスタンスでなければならないのです(08-01「空のクラスをつくる」「タイムラインから呼出せるクラス」参照)。

第2に、MovieClipクラスを継承することです。継承により、「スーバークラスのプロパティやメソッドは、すべてサブクラスに受継がれます」(Word 04-001「継承」参照)。継承を行うには、クラス名に続けてextends定義キーワードを記述し、その後に継承すべきスーパークラスを指定します。

MovieClipシンボルにカスタムクラスを設定すると、そのインスタンスはカスタムクラスのオブジェクトになります。もし継承をしなかったら、MovieClipのプロパティやメソッドが一切使えなくなります。DisplayObjectのサブクラスでもなくなりますから、DisplayObject.xDisplayObject.yプロパティの操作はできず、座標が動かせません。同じようにEventDispatcherクラスのaddEventListener()メソッドも呼出せなくなり、リスナー関数がまったく登録できなくなってしまいます。

テスト用に、MovieClipクラスを継承したコンストラクタだけのクラスMyClassを、以下のように定義してみます。コンストラクタ内では確認のために、プロパティDisplayObject.xDisplayObject.y、並びにメソッドEventDispatcher.addEventListenertrace()関数で[出力]します。なお、trace()関数の引数に渡すEventDispatcher.addEventListenerはメソッドそのものを参照しますので、呼出しの括弧()はつけないことにご注意ください。また、このクラスはMovieClipシンボルに設定する必要はありません。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  import flash.display.MovieClip;
  public class MyClass extends MovieClip {
    public function MyClass() {
      trace(x, y);
      trace(addEventListener);
    }
  }
}

FLAファイルのフレームアクションでクラスMyClassのコンストラクタを呼出せば、[ムービープレビュー]でプロパティDisplayObject.xDisplayObject.yの初期値0と、メソッドEventDispatcher.addEventListenerの参照が[出力]パネルに表示されます(図09-002)。メソッドはActionScript上は関数(function)として扱われますですので、[出力]は「function Function() {}」となります。

// フレームアクション
var myObject:MyClass = new MyClass();
// [出力]パネルの表示
0 0
function Function() {}
図09-002■MovieClipを継承したクラスMyClassのコンストラクタの呼出し

継承したプロパティDisplayObject.xDisplayObject.yの初期値0と、メソッドEventDispatcher.addEventListenerの参照が[出力]される。

試しにMovieClipクラスを継承するextends定義キーワードの記述をコメントアウトすると、MyClassはプロパティもメソッドもない空のクラスになります。したがって、同じようにコンストラクタを呼出すと、プロパティやメソッドが未定義である旨の[コンパイルエラー]が表示されることになります(図09-003)。

図09-003■継承の指定を外したクラスMyClassのコンストラクタの呼出し

プロパティやメソッドは継承されないので、[コンパイルエラー]が表示される。

ですから、カスタムクラスがMovieClipクラスおよびそのスーパークラスのプロパティやメソッドを使うためには、MovieClipクラスを継承しなければならないのです。

さらに、継承するMovieClipクラスを指定するには、第3にimport宣言が必要になります。前章08(Tips 08-002「パッケージはクラスをグループ分けする仕組み」参照)に述べたとおり、ActionScript 3.0の多くのクラスは、パッケージに属しています。そして、MovieClipクラスのパッケージは、flash.displayです(図09-002)。

図09-004■MovieClipクラスのパッケージはflash.display

パッケージ名を加えたflash.display.MovieClipがクラスの正式な名前(完全修飾名)。

パッケージに含まれるクラスは、正式な名前がつぎのような記述で表されます。

パッケージ名.クラス名

つまり、MovieClipクラスの「フルネーム」は、flash.display.MovieClipなのです。したがって、カスタムクラスにMovieClipクラスの継承を指定するとき、以下のように記述することもできます。なお、クラスのフルネームは、「完全修飾名」と呼ばれます。

public class クラス名 extends flash.display.MovieClip {

また、MovieClip型で指定するプロパティやローカル変数は、同じ書き方ではつぎのように宣言することになります。

var 識別子:flash.display.MovieClip;

しかし、クラス定義で使うMovieClipクラスの指定に、いちいちflash.display.MovieClipと記述するのは手間でしょう。しかも、パッケージに属するクラスは、MovieClipクラスだけではありません。それらすべてにフルネームを書いていたのでは、煩雑なだけでなく、スクリプトも読みにくくなります。

そこでActionScript 3.0では、カスタムクラス定義の中でパッケージに含まれるクラスを使う場合、その完全修飾名は冒頭にimportディレクティブ(指示子ともいいます)を用いて1度だけ宣言すればよいとされます。完全修飾名をimport宣言されたクラスは、そのスクリプト内では単に(パッケージを省いた)クラス名のみ記述するだけで、パッケージのクラスが正しく読込まれるということです。

たとえば、作曲家のバッハについてWikipediaで調べると(<http://ja.wikipedia.org/wiki/ヨハン・ゼバスティアン・バッハ>)、つぎのような説明で始まります。

ヨハン・ゼバスティアン・バッハ(以下バッハとする)はアイゼナハの町楽師ヨハン・アンブロジウスの末子として生まれた。

「ヨハン・ゼバスティアン・バッハ(以下バッハとする)」というのは、仮にjohann.sebastian.Bachというクラスがあったとしたら、つぎのimport宣言と同じ意味になります。

import johann.sebastian.Bach;   // 以下Bachとする

作曲家のバッハについてWikipediaの冒頭説明以降はフルネームを書く必要がないのと同じように、import宣言後はクラスをBachと記述するだけで指定できるからです。


寿限無のような長いフルネームも、初めに1度名乗ればOK!

なお、同じパッケージ内にあるすべてのクラスは、ワイルドカード*を使って指定することができます。たとえば、flash.displayパッケージには、MovieClipクラスのほかDisplayObjectやInteractiveObjectその他多くのクラスが含まれています。つぎの1行のステートメントで、それらすべてのクラスをimport宣言したことになります。

import flash.display.*;

Tips 09-001■import宣言についての注意
import宣言について注意すべき点をふたつ述べます。

第1に、ワイルドカード*で任意のパッケージを指定することはできません。たとえば、つぎのいずれの記述を行ってもimport宣言の効果はなく、flash以下のすべてのバッケージに属するクラスすべてをインポートすることにはなりません。

import flash.*;
import flash.*.*;

第2に、importディレクティブ自体は、クラスを読込む処理は行いません。宣言されたクラスが実際にスクリプトで参照されて初めて、そのクラスはSWFに書出されます。したがって、import宣言されても使われなかったクラスは、ロードの負荷やSWFファイルのサイズには影響を与えません。

なお、importディレクティブについて詳しくは、FumioNonaka.com「import指示子」(<http://www.fumiononaka.com/TechNotes/Flash/FN0607003.html>)をご参照ください。


AS1&2 Note 09-001■ActionScript 3.0ではimport宣言が必須
ActionScript 2.0では、パッケージに属するクラスをimport宣言しなくても、クラスは完全修飾名で記述しさえすれば読込まれました。しかし、ActionScript 3.0では、完全修飾クラス名を使う場合でも、import宣言は必ず行わなければなりません。

逆に、フレームアクションでは、ActionScript 3.0の定義済みのクラスは、パッケージに属する場合も、後述の理由でimportディレクティブの記述は要りません。しかし、2.0については、パッケージに含まれるクラスはimport宣言をするか、そうでなければ完全修飾名を記述する必要があります。


Maniac! 09-001■ディレクティブおよび完全修飾名という用語つにいて
importディレクティブの「ディレクティブ」(directive)は英語で「指示」を意味し、「指示子」とも呼ばれます。ActionScriptをコンパイル(SWF書出し)する際に、処理を指示するという役目だからです。たとえば、importディレクティブは、スクリプトに記述されたクラスの正確なパッケージを指示して、クラスが正しくSWFに書出せるようにします。

「完全修飾名」は英語の"fully qualified name"の訳で、「完全限定名」という呼び方もあります。「修飾」には「飾り立てる」という意味がありますし、「限定」は「範囲を狭める」と受取られがちです。しかし、それぞれには「詳しくする」とか「明確にする」という意味合いも込められています。

英語の"qualify"は、詳しい意義づけをして限定するといった意味です。たとえば、URLなどのパスを詳しく特定できるように正確に記述すると、完全パスとかフルパスと呼ばれます。ですから、"fully qualified name"も「正式名」とか「フルネーム」と捉えて差支えはないように思われます。

[*筆者用参考] アスキーデジタル用語辞典「ディレクティブ」。

ところで、前掲スクリプト07-005(図09-001)を始め、これまで作成してきたフレームアクションではimportディレクティブはとくに用いることなく、MovieClipおよびそのスーパークラスのプロパティやメソッドを使うことができました。フレームアクションには、import宣言は要らないのでしょうか。

実は、フレームアクションにもimport宣言は必要です。ただし、ActionScript 3.0のおもな定義済みクラスについては、私たちが直接記述しなくてもよいのです。なぜなら、Flash CS3が、それらのクラスを自動的にimport宣言してくれているからです。

Tips 09-002■タイムラインで自動的にimport宣言されるクラス
タイムラインに対して自動的にimport宣言されるクラスは、Flashアプリケーションの中にXMLファイルで指定されています。具体的には、Adobe Flash CS3アプリケーションフォルダ内のConfigration/ActionScript 3.0/implicitImports.xmlにその記述があります(図09-005)。

XMLファイルimplicitImports.xmlには、flash.displayを始め、ActionScript 3.0に定義済みのクラスのパッケージのパスが記載されています。これらのパッケージが、コンパイル(SWF書出し)時に、ファイル名が示すとおり黙示的(implicit)にimport宣言されます。

図09-005■implicitImports.xmlの内容

XMLファイルに指定されたパッケージが、コンパイル時に自動的にimport宣言される。

すでに見たとおり、ActionScript 3.0クラス定義ファイルには、この自動的なimport宣言は適用されませんので、importディレクティブを用いて明示的に記述する必要があります。また、フレームアクションでも、自分や他の人が定義してパッケージ化したクラスを使うときには、やはりimport宣言をしなければなりません。そこで、以降はフレームアクションの場合にも、パッケージを意識するために、import宣言を記述することがあります。

Maniac! 09-002■フレームアクションにおけるimport宣言の適用範囲
フレームアクションで行ったimport宣言は、そのタイムライン全体に適用されます。これは変数のvar宣言や関数のfunction定義が、記述したフレームに関わらず、タイムライン全体に効果を及ぼしたのと同じです(Column 04「変数宣言と関数定義の初期化時期」)。したがって、後のフレームアクションにimport宣言が記述されていれば、前のフレームでクラスにアクセスすることができます。

ただし、タイムラインが異なると、import宣言は別途行う必要があります。たとえば、メインタイムラインや他のMovieClipインスタンスでimport宣言されたクラスを、別のMovieClipのフレームアクションでimportせずに使うことはできません。

[ヘルプ]のimportディレクティブの項に「importディレクティブは、それを呼び出している現在のスクリプト(フレームまたはオブジェクト)にのみ適用されます」と説明されているのは誤りです。なお、ActionScript 2.0では、import宣言はそれを書いたスクリプトペイン内にのみ適用されます。[ヘルプ]の記述は、この2.0についての説明文が不当に残されたものでしょう。

フレームアクションを修正してMovieClipシンボルに設定するクラスとして定義する
それでは、前掲スクリプト07-005(図09-001)を、MovieClipシンボルに設定するクラスとして定義してみましょう。クラス名は、EllipticMotionとします(スクリプト09-001)。

すでに「MovieClipシンボルに定義するクラスのスタイル」で述べたおり、第1にクラスはpublic属性で指定します。第2に、extends定義キーワードを用いて、MovieClipクラスを継承します。第3は、import宣言です。継承するMovieClipクラスのほか、イベントを定義するEventクラスおよびぼかしフィルタを設定するBlurFilterクラスもパッケージに属しますので、importする必要があります。

スクリプト09-001■フレームアクションをMovieClipシンボルに設定するクラスとして定義

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  import flash.display.MovieClip;
  import flash.events.Event;
  import flash.filters.BlurFilter;
  public class EllipticMotion extends MovieClip {
    private const DEGREE_TO_RADIAN:Number = Math.PI/180;
    private var nDegree:Number = 0;
    private var nRadian:Number = 0;
    private var nSpeed:Number = 5;
    private var nCenterX:Number = stage.stageWidth/2;
    private var nCenterY:Number = stage.stageHeight/2;
    private var nRadiusX:Number = 100;
    private var nRadiusY:Number = 50;
    private var nCos:Number = Math.cos(nRadian);
    private var nSin:Number = Math.sin(nRadian);
    public function EllipticMotion() {
      addEventListener(Event.ENTER_FRAME, xMoveX);
      addEventListener(Event.ENTER_FRAME, xMoveY);
      addEventListener(Event.ENTER_FRAME, xScale);
      addEventListener(Event.ENTER_FRAME, xBlur);
      addEventListener(Event.ENTER_FRAME, xUpdate);
    }
    private function xMoveX(eventObject:Event):void {
      x = nCenterX+nCos*nRadiusX;
    }
    private function xMoveY(eventObject:Event):void {
      y = nCenterY+nSin*nRadiusY;
    }
    private function xUpdate(eventObject:Event):void {
      nDegree += nSpeed;
      nDegree = (nDegree%360+360)%360;
      nRadian = nDegree*DEGREE_TO_RADIAN;
      nCos = Math.cos(nRadian);
      nSin = Math.sin(nRadian);
    }
    private function xScale(eventObject:Event):void {
      scaleX = scaleY = xGetIndexZ(0.8, 1);
      scaleX *= xGetIndexZ();
    }
    private function xBlur(eventObject:Event):void {
      var nBlur:Number = xGetIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function xGetIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(nSin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

クラスEllipticMotionに定義したすべてのプロパティと、コンストラクタを除くすべてのメソッドには、アクセス制御の属性としてprivateを指定しました。また、イベントリスナーを登録する(EventDispatcher.addEventListener()メソッド呼出しの)ステートメントは、コンストラクタメソッド内に記述しています。

角度の度数をラジアンに変換する比率(Math.PI/180)を格納するNumber型の変数(DEGREE_TO_RADIAN)には、varでなくconstという新しいキーワードが指定されています。これは「定数」の宣言になります。定数も、値を格納する変数であることは、var宣言したプロパティと同じです。ただし、定数はその名前のとおり、数値が定まっており、1度設定した値は変更できません。

たとえば、以下のフレームアクションのようにconst宣言した定数に値を設定したうえで、その定数に別の値を代入しようとすると、「定数として指定した変数への代入が無効」であるという[コンパイルエラー]が表示されます(図09-006)。

// フレームアクション
const DEGREE_TO_RADIAN:Number = Math.PI/180;
DEGREE_TO_RADIAN = 0;   // コンパイルエラー
図09-006■定数に設定した値は変更できない

別の値を代入しようとすると、[コンパイルエラー]が生じる。

なお、前述(Tips 02-010「小文字で始めない識別子」)のとおり、定数には識別子としてすべて大文字を用いるのが一般的です。そのため、上記スクリプト09-001でも定数名を"DEGREE_TO_RADIAN"と、すべて大文字にしました。

Word 09-001■const定義キーワード
つぎのシンタックスで、定数を宣言します。

const 識別子:データ型

「定数」は、値が設定されたら変更のできない変数です。定数値を変更しようとすれば、[コンパイルエラー]が発生します。

定数にも、変数やプロパティと同じく、識別子の後にコロン(:)を記述してデータ型の指定ができます。なお一般に、定数の識別子には、すべて大文字を用います。

MovieClipシンボルにクラスを設定する
楕円軌道に沿って3D風にアニメーションさせるスクリプトがMovieClipを継承するクラスEllipticMotion(スクリプト09-001)として定義できましたので、これをMovieClipシンボルに設定しましょう。

[ライブラリ]パネルでMovieClipシンボルを選んで、[リンケージプロパティ]ダイアログボックスを開きます([ライブラリ]のオプションポップアップメニューまたはシンボルを右クリックして[リンケージ])。そして、[リンケージ]の[ActionScriptに書き出し]をチェックしたうえで、設定するクラス名EllipticMotionを[クラス]のフィールドに入力します(図09-007)。なお、[最初のフレームに書き出し]がデフォルトで選択されますので、この設定は変えないでください(後述参照 )。

図09-007■[リンケージプロパティ]ダイアログボックスで[クラス]を設定

[リンケージ]の[ActionScriptに書き出し]をチェックしたうえで、設定するクラス名を[クラス]のフィールドに入力。[最初のフレームに書き出し]はデフォルトのままにする。

[リンケージプロパティ]ダイアログボックスで[クラス]の設定ができたら、[ムービープレビュー]で動作を確かめます。EllipticMotionクラスを設定したMovieClipインスタンスは、スクリプト07-005をフレームアクションに記述したときと同じく、予めステージに配置しておく必要があります。クラスEllipticMotionの定義と、そのMovieClipシンボルへの設定が正しくできていれば、フレームアクション(スクリプト07-005)の場合とまったく同じように、インスタンスが楕円軌道に沿って3D風にアニメーションします(図09-008)。

なお、[リンケージプロパティ]ダイアログボックスで[クラス]にEllipticMotionを設定したMovieClipシンボルは、ActionScriptの観点からはEllipticMotionクラスのシンボルということになります。したがって、解説中ではこのシンボルのインスタンスをEllipticMotionインスタンスと呼ぶことがあります。ただ、とくにオーサリング時には、[ライブラリ]パネル内のシンボルは外観がMovieClipのまま変わりません。また、MovieClipはクラスEllipticMotionのスーパークラスですので、EllipticMotionクラスを設定したMovieClipシンボルという表現も用います。

図09-008■MovieClipシンボルに設定したクラスによりインスタンスが3D風にアニメーション
3D風に回転するアニメーションは、フレームアクションの場合と同じ。しかし、MovieClipシンボル自体には、スクリプトが記述されていない。

[リンケージプロパティ]ダイアログボックスのオプション[ActionScriptに書き出し](図09-009)は、英語版の表記が"Export for ActionScript"で、「ActionScript用に書出し」することを意味します。それは第1に、[リンケージプロパティ]ダイアログボックスで指定した[クラス]などのAcrionScript用の設定を、シンボルのデータとともにSWFファイルに書出すことを示します。

そして第2に、ActionScriptで使えるように、シンボルのインスタンスがフレームに配置されていなくても、必ずSWFファイルとして書出すという指定でもあります。

後述するように、ActionScriptを用いれば、シンボルのインスタンスを予めステージに配置することなく、動的に作成することができます。しかし、アニメーションツールとしてのFlashは、ファイルサイズを最適化するため、実際にタイムラインで使われたシンボルのデータのみをSWFに書き出す仕様になっています。[ActionScriptに書き出し]のオプションを選択すると、インスタンスがフレームで使われているかどうかを問わず、SWFファイルへの書出しが行われることになるのです。

図09-009■[リンケージプロパティ]ダイアログボックスの[ActionScriptに書き出し]と[最初のフレームに書き出し]
[リンケージプロパティ]ダイアログボックスで指定した[クラス]などのAcrionScript用の設定をシンボルとともに、メインタイムラインの第1フレームで使われるデータとしてSWFファイルに書出す。

[ActionScriptに書き出し]をチェックして選択すると、デフォルトで[最初のフレームに書き出し]が選ばれます。これはシンボルが、メインタイムラインの第1フレームで使われるデータとして、SWFファイルに書出されることを意味します。逆にいえば、Flashコンテンツ(SWF)の第1フレームが表示される前に、シンボルのデータはダウンロードされるのです。つまり、最初のフレームが表示された瞬間に、すでにシンボルのデータはActionScriptで使える準備が整っていることになります。

Tips 09-003■[最初のフレームに書き出し]しない場合
[最初のフレームに書き出し]すると、ActionScriptでいつでも使える反面、コンテンツの第1フレームを表示する前にダウンロードの負荷が生じます。このようなデータが多いと、ステージは背景色だけの状態のまま、コンテンツがなかなか表れないこともあります。第1フレームさえ表示されないのですから、プリローダ(「Now Loading...」などの読込み待ち表示)も役に立ちません。

そこで、[最初のフレームに書き出し]は、チェックを外して、選択しないことができます。その場合、SWFファイルへの書出しは、原則に戻ります。つまり、インスタンスをタイムラインに置かなければ、データがSWFファイルに含まれないということです。

インスタンスの配置先は、それを最初に使うフレームかそれよりも手前であれば、どのフレームでも構いません。できれば、他のデータのダウンロードやアニメーションの負荷が少ない、メニュー画面などに置くとよいでしょう。インスタンスはステージに表示する必要はなく(DisplayObject.visibleプロパティをfalseに設定してよく)、ステージ外に配置しても差支えありません。


09-02 クラスの中身を整理する
前節で定義したクラスEllipticMotion(スクリプト09-001)は、フレームアクションに対してMovieClipシンボルに設定するクラスとしての最低限の修正を施したものです。このクラスには、この後さらに拡張を加えていく予定です。それに先立ち、クラスEllipticMotionのプロパティやメソッドを少し整理しておきましょう。

メンバー名とプロパティの整理
まず第1に、メンバー名つまりプロパティやメソッドの識別子は、接頭辞を除きます。この作業は必須ではありません。しかし、クラスを汎用化して、自分以外の人が利用する場合も考えると、ActionScript定義済みクラスのプロパティやメソッドに準じた名前を用いた方がよいでしょう。

第2に、角度を保持するプロパティは、度数かラジアンのどちらか一方があれば、定数DEGREE_TO_RADIANを使っていつでも値が得られます。そこで、位置決めのしやすい度数のプロパティをdegreeとして残し、ラジアン値のプロパティnRadianは削除することにします。この変更にともなうスクリプト09-001の修正部分は、つぎのとおりです(なお、識別子のみの変更箇所は割愛します)。

// private var nRadian:Number = 0;
// ...[中略]...
// private var nCos:Number = Math.cos(nRadian);
// private var nSin:Number = Math.sin(nRadian);

private var cos:Number = Math.cos(degree*DEGREE_TO_RADIAN);
private var sin:Number = Math.sin(degree*DEGREE_TO_RADIAN);
// ...[中略]...
private function update(eventObject:Event):void {
  degree += speed;
  degree = (degree%360+360)%360;
  // nRadian = degree*DEGREE_TO_RADIAN;
  var nRadian:Number = degree*DEGREE_TO_RADIAN;
  cos = Math.cos(nRadian);
  sin = Math.sin(nRadian);
}

スクリプト09-001の変更されたステートメントは、コメントアウトして示しました。修正内容としては、cosとsinの初期値が上述のとおりプロパティdegreeに定数DEGREE_TO_RADIANを掛合わせて求められていることと、メソッドupdate()内でラジアン値をプロパティでなくローカル変数に納めたことです。

Pointクラスでxyの値を管理する
整理点の第3は、楕円軌道の中心座標やx軸y軸の半径など、xとyの値を一緒に管理するということです。Pointクラスは、xとyの値をプロパティとして持ち、簡単なベクトル演算も行うことができます。パッケージは、幾何学(geometry)的な処理を行うクラスが納められたflash.geomに属します。

Pointインスタンスは、ふたつの引数として(x, y)座標を渡して作成します。各座標値は、それぞれPoint.xおよびPoint.yプロパティにより、取得および設定することができます(Word 09-002「Pointコンストラクタ」参照)。

Word 09-002■Point()コントラクタ
つぎのシンタックスで、Pointクラスのインスタンスを作成します。

new Point(x座標:Number=0, y座標:Number=0)

指定した(x, y)座標値を持つPointインスタンスが生成されます。ふたつの引数は、オプションです。インスタンスの(x, y)座標値は、それぞれPoint.xおよびPoint.yプロパティを用いて、取得・設定することができます。

x座標 ー 水平座標値を指定する浮動小数点数値。デフォルト値は0です。

y座標 ー 垂直座標値を指定する浮動小数点数値。デフォルト値は0です。

楕円軌道の中心座標およびx軸とy軸方向の半径をPointインスタンスで扱うと、以下のような修正が必要になります。プロパティ名は、それぞれcenterとradiusにしました。

import flash.geom.Point;
public class EllipticMotion extends MovieClip {
  // ...[中略]...
  // private var nCenterX:Number = stage.stageWidth/2;
  // private var nCenterY:Number = stage.stageHeight/2;

  private var center:Point = new Point(stage.stageWidth/2, stage.stageHeight/2);
  // private var nRadiusX:Number = 100;
  // private var nRadiusY:Number = 50;

  private var radius:Point = new Point(100, 50);
  // ...[中略]...
  private function moveX(eventObject:Event):void {
    // x = nCenterX+nCos*nRadiusX;
    x = center.x+cos*radius.x;
  }
  private function moveY(eventObject:Event):void {
    // y = nCenterY+nSin*nRadiusY;
    y = center.y+sin*radius.y;
  }

数値で設定していた各(x, y)座標値は、Point()コンストラクタに引数として渡すことにより、Pointインスタンスのプロパティ値になります。したがって、各座標の数値が必要なときは、Point.xまたはPoint.yプロパティにより取出します。

以上3点の修正を前掲スクリプト09-001に加えたのが、以下のEllipticMotionクラス(スクリプト09-002)です。プロパティやメソッドなどの内部的な扱い方を整理しただけで、動作は変えていません。したがって、[ムービープレビュー]を見れば、前のスクリプト09-001と同じアニメーションになります。

スクリプト09-002■プロパティを整理したクラスEllipticMotion

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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(stage.stageWidth/2, stage.stageHeight/2);
    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);
    public function EllipticMotion() {
      addEventListener(Event.ENTER_FRAME, moveX);
      addEventListener(Event.ENTER_FRAME, moveY);
      addEventListener(Event.ENTER_FRAME, scale);
      addEventListener(Event.ENTER_FRAME, blur);
      addEventListener(Event.ENTER_FRAME, update);
    }
    private function moveX(eventObject:Event):void {
      x = center.x+cos*radius.x;
    }
    private function moveY(eventObject:Event):void {
      y = center.y+sin*radius.y;
    }
    private function update(eventObject:Event):void {
      degree += speed;
      degree = (degree%360+360)%360;
      var nRadian:Number = degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
    }
    private function scale(eventObject:Event):void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur(eventObject:Event):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;
    }
  }
}

メソッドの整理
プロパティに続いて、メソッドも少し整理しましょう。第1は、リスナー関数をまとめることです。前述(Tips 07-002「リスナー関数の利点」)のとおり、複数のリスナー関数をそれぞれ同じイベントに登録すると、それらの組換えがたやすく行えます。しかし、今回はこの後、複数のインスタンスを配置して、それらの連携したアニメーションに発展させる予定です。そうなると、アニメーションを構成するそれぞれのメソッド(moveX()/moveY()/scale()/blur()/update())は、まとめて扱う方が都合はよいといえます。

そこで、新たにメソッドrotate()を定義して、その中からアニメーションの各メソッド(moveX()/moveY()/scale()/blur()/update())を呼出すことにします。その際、とくに各メソッドに渡すべき引数はありません。しかし、これらのメソッドはリスナー関数だったために、すべてイベントオブジェクトが引数として指定されています。引数のある関数(function)を、引数なしに呼出せばエラーになってしまいます。そこで、これらのメソッドに宣言されたイベントオブジェクトの引数は、すべて削除します。前掲スクリプト09-002に対する修正箇所は、つぎのとおりです。

public function EllipticMotion() {
  /*
  addEventListener(Event.ENTER_FRAME, moveX);
  addEventListener(Event.ENTER_FRAME, moveY);
  addEventListener(Event.ENTER_FRAME, scale);
  addEventListener(Event.ENTER_FRAME, blur);
  addEventListener(Event.ENTER_FRAME, update);
  */

  addEventListener(Event.ENTER_FRAME, rotate);
}
private function rotate(eventObject:Event):void {
  update();
  moveX();
  moveY();
  scale();
  blur();
}
// private function moveX(eventObject:Event):void {
private function moveX():void {
  // ...[中略]...
// private function moveY(eventObject:Event):void {

private function moveY():void {
  // ...[中略]...
// private function update(eventObject:Event):void {

private function update():void {
  // ...[中略]...
// private function scale(eventObject:Event):void {

private function scale():void {
  // ...[中略]...
// private function blur(eventObject:Event):void {

private function blur():void {

Tips 09-004■リスナー関数を引数なしに呼出す
「関数に引数が指定されている場合、引数を渡さずに関数を呼出すとエラーになります」(Tips 07-011「指定された引数を渡さずに関数を呼出すと」)。しかし、イベントリスナーに指定した関数・メソッドを、イベントとは関わりなく、初期設定などで別途引数なしに呼出したい場合があります。

そのようなときには、関数(function)に引数のデフォルト値を指定すれば、引数を渡さずに呼出すことができました(前掲Tips 07-011)。そして、任意のクラスで指定したデータ型には、特別な値としてnullが適合しました(Tips 03-013「Timerインスタンスの動作開始時にリスナー関数を呼出す」)。

したがって、イベントオブジェクトの引数にデフォルト値としてnullを指定すると、リスナー関数は引数なしに呼出すことができます。

// フレームアクション
function xTest(eventObject:Event=null):void {
  trace(eventObject);   // 出力: null
}
xTest();

メソッドを整理する第2の点として、度数の角度を保持するプロパティdegreeはget/setアクセサメソッドとして定義することにします。角度に連動して値が設定されるsinおよびcosは、角度のsetアクセサメソッド内で設定してしまいます。すると、元のクラスEllipticMotion(スクリプト09-002)のメソッドupdateの処理が、基本的に角度のsetアクセサメソッドに移行できそうです。

getおよびsetアクセサメソッドの名前は、改めてdegreeとしましょう。そして、内部的に用いるprivateな角度のプロパティは、_degreeとして宣言し直します。すると、スクリプト09-002のクラスEllipticMotionには、さらにつぎのような修正を加えることになります。

// private var degree:Number = 0;
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 cos:Number = Math.cos(_degree*DEGREE_TO_RADIAN);
private var sin:Number = Math.sin(_degree*DEGREE_TO_RADIAN);
// ...[中略]...
private function get degree():Number {
  return _degree;
}
// private function update():void {
private function set degree(nDegree:Number):void {
  // degree += speed;
  // degree = (degree%360+360)%360;

  _degree = (nDegree%360+360)%360;
  // var nRadian:Number = degree*DEGREE_TO_RADIAN;
  var nRadian:Number = _degree*DEGREE_TO_RADIAN;
  cos = Math.cos(nRadian);
  sin = Math.sin(nRadian);
}
private function rotate(eventObject:Event):void {
  // update();
  degree += speed;
  moveX();
  moveY();
  scale();
  blur();
}

以上2点の修正を前掲スクリプト09-002に加えると、新たなクラスEllipticMotionはつぎのように定義されます(スクリプト09-003)。

スクリプト09-003■メソッドを整理したクラスEllipticMotion

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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(stage.stageWidth/2, stage.stageHeight/2);
    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);
    public function EllipticMotion() {
      addEventListener(Event.ENTER_FRAME, rotate);
      setRotation();
    }
    private function get degree():Number {
      return _degree;
    }
    private function set degree(nDegree:Number):void {
      _degree = (nDegree%360+360)%360;
      var nRadian:Number = _degree*DEGREE_TO_RADIAN;
      cos = Math.cos(nRadian);
      sin = Math.sin(nRadian);
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x;
    }
    private function moveY():void {
      y = center.y+sin*radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

上記のクラスEllipticMotion(スクリプト09-003)には、さらにメソッドsetRotation()が新たに加えられています。処理内容は、元のメソッドrotate()に記述されていた4つのメソッド(moveX()/moveY()/scale()/blur())の呼出しです(図09-010)。これら4つのメソッドは、現在設定されている角度(_degreeプロパティの値)にもとづいて、楕円軌道上に配置する処理を行っています。

メソッドsetRotation()を別に定義することにより、メソッドrotate()の処理は(1)角度の加算(setアクセサメソッドdegree()の呼出し)と(2)楕円軌道上の配置のふたつに整理することができました。

図09-010■メソッドsetRotation()を定義
元のメソッドrotate()から、インスタンスの楕円軌道上への配置の処理を、新たにメソッドsetRotation()として定義した。

そして、メソッドsetRotation()は、クラスEllipticMotionのコンストラクタからも呼出しています(スクリプト09-003および図09-010)。この呼出しがなかったとき(スクリプト09-002)は、最初のDisplayObject.enterFrameイベントが発生してリスナー関数rotate()が呼ばれるまで、インスタンスは楕円軌道上に配置されませんでした。そのため、[ムービープレビュー]で見ると、初めに一瞬予めステージに配置されたインスタンスが表示されていました。コンストラクタからsetRotation()メソッドを呼出すことにより、インスタンスは楕円軌道上に置かれた状態で表示されるようになります。


09-03 インスタンスを動的に配置する
カスタムクラスを設定したMovieClipシンボルについて、インスタンスは予めステージに置かず、スクリプトで動的に作成して、配置してみましょう。

シンプルに試す
まずは、コンストラクタだけのごくシンプルなクラスMyClassを、以下のように定義します。そのうえで、クラスMyClassは、MovieClipシンボルに[クラス]として設定します(図09-011)。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  import flash.display.MovieClip;
  public class MyClass extends MovieClip {
    public function MyClass() {
    }
  }
}
図09-011■シンボルの[リンケージプロパティ]ダイアログボックスで[クラス]を設定
コンストラクタのみのシンプルなクラスMyClassを[クラス]に入力。

クラスMyClassを設定したMovieClipシンボルのインスタンスは、原則どおり、コンストラクタを呼出して作成します。以下のフレームアクションをMyClass.asと同階層のFLAファイルに記述すると、生成されたインスタンスを[出力]パネルで確認できます。ただし、ステージにはインスタンスは表示されません(図09-012)。

// フレームアクション
var my_mc:MyClass = new MyClass();
trace(my_mc);
図09-012■コンストラクタで作成したインスタンスを[出力]パネルに表示
インスタンスは、ステージには表示されない。

第6章で、Flash Playerの表示階層は、Stageオブジェクトを頂点とするツリー構造だと説明しました(06-01「キーイベントを受取る」)。このツリー構造でインスタンスを管理する仕組みは、「表示(ディスプレイ)リスト」と呼ばれます。そして、表示リストに加えられたインスタンスが、ツリー構造に従ってFlash Playerの画面に表示されるのです。

コンストラクタでインスタンスを作成しただけでは、表示リストには加わりません。親となるインスタンスに対してDisplayObjectContainer.addChild()メソッドを呼出し、表示リストに加えたいインスタンスを引数として渡す必要があります。上記フレームアクションにこのDisplayObjectContainer.addChild()メソッド呼出しのステートメントを加えれば、インスタンスをステージ上に表示することができます。

親インスタンス.addChild(子インスタンス)

表示リストに加えられた子のインスタンスは、親インスタンスの座標(0, 0)に配置されます。したがって、クラスMyClassのインスタンスをメインタイムラインの子として加え、ステージ中央に配置するには、メインタイムラインに記述した上記フレームアクションをつぎのように修正します(インスタンスの生成を確認するためのtrace()関数のステートメントは除きました)。

// フレームアクション
var my_mc:MyClass = new MyClass();
addChild(my_mc);
my_mc.x = stage.stageWidth/2;
my_mc.y = stage.stageHeight/2;

[ムービープレビュー]を確認すると、クラスMyClassを設定したMovieClipシンボルのインスタンスがステージ中央に表示されます。MyClassインスタンスは、DisplayObjectContainer.addChild()メソッドでメインタイムラインの子として加えられたことによりステージ上に描画され、その座標がステージ中央に設定されたからです。

図09-013■コンストラクタで作成したインスタンスをステージ中央に配置
DisplayObjectContainer.addChild()メソッドにより、ターゲットであるメインタイムラインの子として、MyClassインスタンスが表示される。

DisplayObjectContainer.addChild()メソッド
改めてDisplayObjectContainer.addChild()メソッドについて、確認してみましょう。シンタックスおよび処理内容は、下記Word 09-003に示すとおりです。

Word 09-003■DisplayObjectContainer.addChild()メソッド
つぎのシンタックスで、DisplayObjectインスタンスを子として加えます。

インスタンス.addChild(子インスタンス:DisplayObject):DisplayObject

DisplayObjectContainerインスタンスに対して、引数で渡したDisplayObjectインスタンスを子として加えます。引数のDisplayObjectインスタンスは、親のDisplayObjectContainerインスタンスが管理する子の表示リストの最後に加えられます。メソッドの戻り値は、引数として渡したDisplayObjectインスタンスです。

子のリスト内のDisplayObjectインスタンスには、配列エレメントと同じく、加えられた順に0から始まる整数インデックスが与えられます。インスタンスのインデックスは大きいほど、子のリスト内の重ね順が手前になります。つまり、DisplayObjectContainer.addChild()メソッドで追加されたDisplayObjectインスタンスは、その時点では子のリスト内の最前面に配置されます。

なお、ひとつのDisplayObjectインスタンスは、Flash Player上のすべてのDisplayObjectContainerインスタンスの子リストの中にひとつしか存在し得ません。したがって、あるDisplayObjectContainerインスタンスの子リストにすでに納められているDisplayObjectインスタンスを、別のDisplayObjectContainerインスタンスにDisplayObjectContainer.addChild()メソッドで加えると、前のDisplayObjectContainerインスタンスの子リストからは削除されます。

ステージ上に表示できるのは、DisplayObjectおよびそのサブクラスのインスタンスです。DisplayObjectのサブクラスには、MovieClipのほかTextFieldやボタンのSimpleButtonクラスなど、多くのクラスがあります。

しかし、自分が親となって、子のインスタンスを持てるのは、その中の一部のクラスです。その機能を備えるのが、DisplayObjectContainerクラスになります。DisplayObjectContainerクラスも、もちろんステージに表示できますので、DisplayObjectクラスを継承します(図09-014)。

図09-014■[ヘルプ]のDisplayObjectContainerクラス冒頭の説明
DisplayObjectContainerクラスは、DisplayObjectクラスを継承する。

DisplayObjectContainerクラスは、子の表示リストを持ち、表示リストや子インスタンスを操作するためのプロパティとメソッドを備えています。これまでご紹介した中では、まずMovieClipクラスがDisplayObjectContainerのサブクラスです。メインタイムラインは、すでにご説明したとおり、MovieClipインスタンスです。それから、StageクラスもDisplayObjectContainerクラスを継承します(図09-015)。

図09-015■DisplayObjectインスタンスのツリー構造で表示リストが構成される
子を持てるのは、DisplayObjectContainerのサブクラス。

DisplayObjectContainerを継承するクラスは、MovieClipやStageクラス以外にもいくつかあります。それは、後の章で一部ご紹介します。

DisplayObjectContainerインスタンスが保持する子インスタンスのリストは、配列と似た仕組みで管理されます。子インスタンスには、0から始まる整数の連番が与えられ、値が大きいほど前面に配置されます。子リストを操作するためのプロパティやメソッドには、たとえば下表09-001のようなものがあります。

表09-001■DisplayObjectContainerクラスの子リストを操作するおもなプロパティとメソッド
プロパティ
numChildren:int 子リスト内の子インスタンスの数を整数値で返します(読取り専用)。
メソッド 処理
addChild(子インスタンス:DisplayObject):DisplayObject DisplayObjectContainerインスタンスの子リストの最後に、子のDisplayObjectインスタンスを追加します。
addChildAt(子インスタンス:DisplayObject, インデックス:int):DisplayObject DisplayObjectContainerインスタンスの子リストの指定されたインデックスに、子のDisplayObjectインスタンスを追加します。
getChildAt(インデックス:int):DisplayObject DisplayObjectContainerインスタンスの子リスト内の、指定されたインデックスにある子のDisplayObjectインスタンスを返します。
getChildIndex(子インスタンス:DisplayObject):int DisplayObjectContainerインスタンスの子リストの中で、指定されたDisplayObjectインスタンスが格納されたインデックスを返します。
removeChild(子インスタンス:DisplayObject):DisplayObject DisplayObjectContainerインスタンスの子リストの中で、指定されたDisplayObjectインスタンスを削除します。
removeChildAt(インデックス:int):DisplayObject DisplayObjectContainerインスタンスの子リスト内の、指定されたインデックスにある子のDisplayObjectインスタンスを削除します。
setChildIndex(子インスタンス:DisplayObject, インデックス:int):void DisplayObjectContainerインスタンスの子リストの中で、指定されたDisplayObjectインスタンスの位置を指定されたインデックスに変更します。

なお、表09-001に掲げたのはDisplayObjectContainerクラスのプロパティとメソッドの一部です。他のプロパティとメソッドや各プロパティ・メソッドの内容について詳しくは、[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド]のDisplayObjectContainerクラスの項をお読みください。

デバッグは犯罪の捜査と同じ
ここで、前節09-02「クラスの中身を整理する」で作成したクラスEllipticMotion(スクリプト09-003)に戻ります。このクラスの設定されたMovieClipインスタンスをステージから削除し、フレームアクションでインスタンスを動的に作成してみましょう。

処理は基本的に、前掲MyClassをMovieClipシンボルに設定して、インスタンスをステージに配置した場合のスクリプト(図09-013)と同じでよいはずです。インスタンスの座標設定は、クラスEllipticMotionのsetRotation()メソッドで行っていますので、フレームアクションには必要ありません。すると、2行のステートメントで足りそうです(図09-016)。

// フレームアクション
var my_mc:EllipticMotion = new EllipticMotion();
addChild(my_mc);
図09-016■ステージ上のインスタンスは削除してフレームアクションで動的に作成
フレームアクションの内容は、前に試したシンプルなクラスMyClassのインスタンスを動的に配置した場合と同じ。

ところが、[ムービープレビュー]で確認するとランタイムエラーが表示されます(図09-016)。しかも、今回のエラーは、やっかいなことに、発生した具体的なステートメントなどの情報が[コンパイルエラー]パネルに示されません。つまり、クラス定義のどこでエラーが起こったのか、ほとんど手がかりがないということです。

図09-017■[出力]パネルに表示されたランタイムエラー
[コンパイルエラー]パネルには、エラーの発生したステートメントなどの情報が何も表示されない。

エラーがどこでなぜ発生したのかを探る「デバッグ」は、刑事ドラマの犯罪捜査と似た手順を踏みます(表09-002。なお、F-site「トラブルの『捜査手順』」<http://f-site.org/articles/2004/10/05174315.html>参照)。

表09-002■犯罪捜査とデバッグの手順
犯罪捜査 デバッグ
被害者の特定 具体的に何が起こったのか、問題を特定します。
犯行時刻の推定 どこまでの処理は問題がなく、その後のどの処理までの間に現象が発生しているのかを絞り込みます。
アリバイ捜査 問題を発生させている可能性のあるステートメントのひとつひとつについて、どれが現象の発生に関与し、どれは関係ないのかを振り分けます。
犯行の手口と動機 問題がどのようなプロセスで発生し、その原因が何なのかを探ります。

まず第1に、問題の特定です。エラーが発生する場合には、何が具体的な問題かは、比較的把握しやすいでしょう。しかし、ひとつの原因から複数のエラーが示されることもあります。そうしたときは、その内のどれが主犯つまり発端なのかを切り分けなければなりません。

さらに、エラーが起こるのではなく、意図しない動作になる場合には、どのような結果が生じているのかを、詳しく見極める必要があります。たとえば、プロパティや変数の値、あるいは演算結果に問題があると推測されるときは、極端な値を与えてみたり、処理を単純化してみるなどして、動作の変化を確認します。

今回はエラーがひとつだけ発生し、つぎのようなメッセージが表示されました。

TypeError: Error #1009: nullのオブジェクト参照のプロパティまたはメソッドにアクセスすることはできません。

このエラー#1009の内容は、[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド] > [付録] > [ランタイムエラー]に説明されています。日本語が少しわかりにくいので、英語版のドキュメントから邦訳すると、つぎのとおりです。

オブジェクト(の参照)がnullと評価されると、プロパティは持てません。このエラーは、予期できない(しかし有効とはされる)状況で起こりえます。

nullは、オブジェクトが存在しないことを示す特別な値です(Word 09-004)。したがって上記の説明は、オブジェクトの参照つまりプロパティや変数に何らかの理由でオブジェクトが設定されていないにも拘らず、オブジェクトがある前提でそのプロパティやメソッドにアクセスしていることを示唆します。

Word 09-004■nullプライマリ式キーワード
nullは、クラスなどで型指定されたリファレンス(複合)型データに、値がないときに返されます(リファレンス型とプリミティブ型データについては、Column 06「値と参照」を参照)。nullをただひとつの値として持つNull型のデータです([ヘルプ]の[ActionScript 3.0のプログラミング] > [ActionScript言語とシンタックス] > [データ型] > [データ型の記述]参照)。ただし、Nullをデータ型の指定に用いることはできません。

変数をクラスなどリファレンス型データで指定すると、デフォルト値はnullになります(なお、プリミティブ型データでもStringだけは、デフォルト値がnullです)。値が存在しない、あるいはまだ設定されていないことを示します。

似た値に、未定義値を示すundefinedがあります。この値をnullと等価演算子==で比較すれば、trueが返されます。しかし、undefinedは、基本的にデータ型が指定されていない未定義の値を示します。したがって、nullundefinedとを厳密な等価演算子===(Tips 05-002「等価と厳密な等価」参照)で比較すれば、falseが返ります。


Maniac! 09-003■ランタイムエラー#1009
[ヘルプ]の[ActionScript 3.0コンポーネントリファレンスガイド] > [付録] > [ランタイムエラー]には、エラーコード#1009は以下のように説明されています。日本語としては、少々意味が取りにくいように思われます。

評価結果がnullになるオブジェクトは、プロパティを持つことができません。このエラーは、有効ではあるが予期しない状況で発生します。

英語版のドキュメントは、本来LiveDocs(Column 03「LiveDocsオンラインヘルプ」参照)の英語版で確認できます。しかし、英語版の[Runtime Errors]のページ(<http://livedocs.adobe.com/flash/9.0/ActionScriptLangRefV3/runtimeErrors.html>)には、1000番代のエラーコードの説明が掲載されていません。

そこで上述本文の邦訳に際しては、Flex 2の英語版ドキュメントを参照しました。[ActionScript 3.0 Language and Components Reference]については、[Adobe Flex 2 Language Reference](<http://livedocs.adobe.com/flex/2/langref/>)でも内容はほぼ同じです。

Flex 2の英語版リファレンス(<http://livedocs.adobe.com/flex/2/langref/runtimeErrors.html>)には、エラーコード#1009はつぎのように説明されています。

An object that evaluates to null can have no properties. This error can occur in some unexpected (though valid) situations.

エラー「捜査」の第2は、問題が処理のどの時点で発生しているのかを絞り込むことです。この点では、残念ながら[コンパイルエラー]パネルに該当のステートメントなどの情報が表示されていないため、自力で調査しなければなりません。このときの指針は、できるかぎり問題をシンプルにすることです。

フレームアクションには、2行のステートメントが記述されています。まずは、そのどちらでエラーが起こっているのかを確かめます。第2ステートメントをコメントアウトしても、同じランタイムエラーが生じます。したがって、クラスEllipticMotionのコンストラクタを呼出したときに、問題が発生していると推測できます。

さらにEllipticMotionのクラス定義を見ると、コンストラクタからふたつのメソッドを呼出しています。この2行もコメントアウトすれば、問題がコンストラクタを呼出す初期化時にあるのか、コンストラクタからのメソッドの呼出しが原因なのかを絞り込めます。念のため、クラスEllipticMotionのコンストラクタ以外のメソッドも、すべてコメントアウトしておきます(図09-020)。

図09-020■EllipticMotionの空のコンストラクタを残してすべてのメソッドをコメントアウト
クラスEllipticMotionのコンストラクタのステートメントと他のメソッドは、すべてコメントアウト。

すると、それでも同じエラーが発生することを確認できます。メソッドは空のコンストラクタ以外、すべてコメントアウトされています。したがって、インスタンスの初期化時に問題が生じていると推測できます。

エラー「捜査」の第3は、アリバイを洗うことです。しかし、デバッグが犯罪の捜査とひとつ大きく違うのは、容疑者となる処理をひとつひとつ外して、結果が同じように起こるかどうかが試せることです。もちろん犯罪の取調べのように、怪しいプロパティや変数などがあれば、trace()関数でその値を確かめることもできます。

すでにコンストラクタは空ですので、プロパティおよびimport宣言をひとつひとつコメントアウトして結果を確認します。すると、中心座標のPointインスタンスを設定するプロパティcenterの宣言が、エラーの原因らしいと突き止められます(図09-021)。

図09-021■プロパティcenterの宣言がエラーの原因らしい
このvar宣言のステートメントをコメントアウトすると、エラーが起こらなくなる。

このステートメントでは、Pointクラスのコンストラクタを呼出し、DisplayObject.stageにアクセスしてそのプロパティ値から計算した座標値を引数として渡しています。このふたつの処理のどちらが原因かを絞り込むには、DisplayObject.stageは使わず、コンストラクタに別の値を渡してみればよいでしょう。

たとえば、プロパティcenterのvar宣言を以下のように修正して、Pointコンストラクタの引数に直接数値を指定してみます。すると、ランタイムエラーは発生しません。ここで、DisplayObject.stageプロパティが重要参考人という結論に達します。

private var center:Point = new Point(120, 90);

さて、するとエラー「捜査」の最後の第4として、問題がどのように発生し、その理由は何なのかを探ることになります。とろこで、前に試したシンプルなクラスMyClassのインスタンスをステージ中央に配置したとき(図09-013)には、今回のようなエラーは発生しませんでした。このときのフレームアクションでも、ステージ中央の座標を計算するため、DisplayObject.stageプロパティを使っていました。クラスMyClassとEllipticMotionとの違いは何なのでしょう。

この段階になると、ヘルプその他のドキュメントで問題のDisplayObject.stageプロパティについて調べる必要があり、場合によってはネットで検索してみることも有効です。しかしここではあえて、ふたつのクラスMyClassとEllipticMotionを、もう少し詳しく比べてみることにします。

DisplayObject.stageプロパティへのアクセスについて、クラスMyClassがEllipticMotionと明らかに違うのは、クラス定義内では何のプロパティも操作しておらず、フレームアクションでインスタンスの座標を設定する際にプロパティ値を取得しただけだということです(図09-022)。

図09-022■クラスMyClassは空のコンストラクタが定義されているだけ
DisplayObject.stageプロパティへのアクセスは、フレームアクションから行った。

そこで試みに、クラスMyClassの定義を以下のように修正し、コンストラクタの中でDisplayObject.stageプロパティを取得して、ステージ中央の座標にインスタンスを設定します。なお、クラスEllipticMotionでは、インスタンスプロパティの宣言時にDisplayObject.stageプロパティの値を取得していますので、アクセスのタイミングは厳密には少し異なります。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  import flash.display.MovieClip;
  public class MyClass extends MovieClip {
    public function MyClass() {
      x = stage.stageWidth/2;
      y = stage.stageHeight/2;
    }
  }
}

上記のように修正したクラスMyClassのコンストラクタを、フレームアクションから呼出すと、#1009のランタイムエラーが再現します(図09-023)。デバッグでは、このようにできるかぎりシンプルなスクリプトで問題を再現することがとても大切です。この結果により、原因はDisplayObject.stageプロパティで、クラス内からアクセスしていることが問題と関わっているらしいと推測できます。

図09-023■クラスMyClassのコンストラクタからDisplayObject.stageにアクセスするとランタイムエラー
DisplayObject.stageプロパティへのアクセスを、フレームアクションからクラスMyClassのコンストラクタ内に移行すると、ランタイムエラーが発生。

少し慎重ないいまわしを選びました。というのは、DisplayObject.stageプロパティにはクラス内からアクセスできないようだ、と結論づけるのは早計だからです。なぜエラーが起こったかの理由は、まだ明らかではありません。ですから、クラス内からアクセスする方法はあるかもしれません。また、クラス外からアクセスすればエラーが発生しないという保障もないのです。

DisplayObject.stageプロパティへのアクセスに問題が生じる理由を明らかにするには、取りあえずその値を確認してみるべきでしょう。クラスMyClassのコンストラクタ内で座標を設定するのは止め、替わりにtrace()関数にDisplayObject.stageプロパティを渡して呼出してみると、エラーは発生せずnullが[出力]されます。どうやら、原因がはっきりしてきました。

// ActionScript 3.0クラス定義ファイル: MyClass.as
package {
  import flash.display.MovieClip;
  public class MyClass extends MovieClip {
    public function MyClass() {
      trace(this, stage);   // 出力 : [object MyClass] null
    }
  }
}

さらに詳しく処理経過を調べるため、フレームアクションにDisplayObjectContainer.addChild()メソッドの呼出しを復活させ、プロパティのターゲットも変えつつ何箇所か、trace()関数でDisplayObject.stageプロパティの値を[出力]してみることにします。複数の[出力]結果を見分けやすいように、trace()関数の引数にはターゲットの参照も加えてあります。すると、つぎのような結果になりました(図09-024)。

// フレームアクション
var my_mc:MyClass = new MyClass();
trace(this, stage);   // 出力1 : [object MainTimeline] [object Stage]
trace(my_mc, my_mc.stage);   // 出力2 : [object MyClass] null
addChild(my_mc);
trace(my_mc, my_mc.stage);   // 出力3 : [object MyClass] [object Stage]
図09-024■MyClassインスタンスの処理過程でDisplayObject.stageプロパティの値を[出力]
MyClassインスタンスを表示リストに加えると、DisplayObject.stageプロパティがStageオブジェクトの参照を返す。

まず、フレームアクションを記述したメインタイムラインをターゲットにすると、DisplayObject.stageプロパティで正しくStageオブジェクトを取得できます(出力1)。しかし、MyClassインスタンスを参照した場合には、フレームアクションからでもプロパティ値としてnullが返され、Stageオブジェクトは取得できません(出力2)。したがって、プロパティへのアクセスがクラス内かフレームアクションかは、DisplayObject.stagenullになる直接の原因ではないということです。

しかし、DisplayObjectContainer.addChild()メソッドでMyClassインスタンスをメインタイムラインの子リストに加えると、このインスタンスに対するDisplayObject.stageプロパティがStageオブジェクトを返すようになります。考えてみると、メインタイムラインは、Flash Playerが初期化された時点でStageオブジェクトの表示リストに含まれています。

つまり、真の原因はDisplayObjectインスタンス(メインタイムラインやMyClassインスタンス)がStageオブジェクトを頂点とした表示リストに含まれていないと、DisplayObject.stageプロパティは値がnullとなり、Stageオブジェクトの参照を返さないということのようです。そのnullのプロパティ値をターゲットにしてStageオブジェクトのプロパティ(Stage.stageWidthstage.stageHeight)にアクセスしようとしたために、問題の#1009ランタイムエラーが生じたと考えられます。

もっとも今回の問題については、実はヘルプのDisplayObject.stageプロパティの説明に、つぎのように手がかりが示されています。

表示オブジェクト(筆者注: DisplayObjectインスタンス)が表示リストに追加されていない場合、stageプロパティはnullに設定されます。

しかし、ドキュメントにつねに情報があるとはかぎりません。また、本ボシと目をつけた容疑者が実はシロで、真犯人は別にいたということもありえます。ですから、聴込み(ドキュメントの調査や検索)と併せて、さらに詳しく取調べ(テスト)を行っておくことが、事件の早期解決の鍵となるのです。


捜査の基本は、ガイシャ、犯行時刻、アリバイ、手口だ。


Tips 09-005■DisplayObject.rootプロパティと表示リスト
DisplayObject.rootプロパティも、インスタンスがStageオブジェクトの表示リストに加えられるまでは、値がnullになります。前掲クラスMyClassのインスタンスについて、メインタイムラインのフレームアクションで確認するとつぎのような結果です。

// フレームアクション
var my_mc:MyClass = new MyClass();
trace(my_mc.root);   // 出力: null
addChild(my_mc);
trace(my_mc.root);   // 出力: [object MainTimeline]

Maniac! 09-004■DisplayObject.rootプロパティには例外事項がある
DisplayObject.rootプロパティの値の決まり方には、細かな例外が少なくありません。

たとえば、後述のLoaderクラスを使って外部ファイルをロードした場合、ロードされたコンテンツ(SWFや画像)のインスタンス(MovieClipやBitmap)には、そのインスタンス自身がDisplayObject.rootプロパティの値として設定されます。ファイルをロードしたLoaderインスタンスが、Stageオブジェクトの表示リストに加わっている必要はありません。

DisplayObject.rootプロパティについて詳しくは、[ヘルプ]のDisplayObject.rootプロパティの項、あるいは筆者サイトFumioNonaka.comの「DisplayObject.rootプロパティ」をご参照ください。

DisplayObject.addedToStageイベント
改めてクラスEllipticMotionを、インスタンスが動的に配置できるように修正してみます。クラス内からでも、インスタンスが表示リストに加えられた後であれば、DisplayObject.stageプロパティにアクセスして座標を指定することはできるはずです。Stageオブジェクトを頂点とする表示リストにインスタンスが加えられると、イベントDisplayObject.addedToStageが発生します(Word 09-005)。このイベントにリスナーを登録し、そのメソッド内で座標は設定することにしましょう。

Word 09-005■DisplayObject.addedToStageイベント
DisplayObjectインスタンスが、ステージに表示されたとき、つまりStageを頂点とする表示リストに加えられたときに発生するイベントです。

これには、大きくふたつの場合がありえます。第1に、DisplayObjectインスタンスが、Stageオブジェクトの表示リストに直接追加されたときです。第2は、DisplayObjectインスタンスがStageの表示リストにないDisplayObjectContainerの表示リスト(子リストのツリーのいずれか)に格納されていて、その上位のDisplayObjectContainerインスタンスがStageオブジェクトの表示リストに新たに加えられた場合です。

このイベントを発生させるメソッドは、DisplayObjectContainer.addChild()メソッド(Word 09-003)あるいは DisplayObjectContainer.addChildAt()のいずれかです。また、EventDispatcher.addEventListener()メソッドなどで、イベントリスナーを扱う際にこのイベントを指定する定数は、Event.ADDED_TO_STAGEになります。

修正箇所は、大きく3点です(図09-025)。第1に、プロパティcenterのvar宣言から、初期値の代入を除きます。EllipticMotionインスタンスが初期化時では、まだDisplayObject.stageプロパティがStageオブジェクトの参照を得られないからです。

private var center:Point;   // = new Point(stage.stageWidth/2, stage.stageHeight/2);

第2に、コンストラクタメソッドに記述されていたDisplayObject.enterFrameイベントへのリスナー登録と座標設定の処理を含むsetRotation()メソッドの呼出しの2行は外します。そして替わりに、DisplayObject.addedToStageイベントへのリスナー登録のステートメントを加えます。リスナーのメソッド名はsetPositionとしましょう。

public function EllipticMotion() {
  addEventListener(Event.ADDED_TO_STAGE, setPosition);
  // addEventListener(Event.ENTER_FRAME, rotate);
  // setRotation();

}

第3に、DisplayObject.addedToStageイベントのリスナーとして新たなメソッドsetPosition()を、以下のように定義します。処理の内容としては、3つあります。

まずプロパティ宣言で未設定だったcenterの値の代入です。DisplayObject.stageプロパティがStageオブジェクトの参照を返しますので、そのプロパティ値から計算した座標をPointインスタンスに渡して設定します。つぎに、コンストラクタメソッドから除いた2行のステートメントを移行します。そして3つ目に、忘れていけないのは、DisplayObject.addedToStageイベントに登録したリスナーの削除(DisplayObject.removeEventListener()メソッドの呼出し)です。

private function setPosition(eventObject:Event):void {
  removeEventListener(Event.ADDED_TO_STAGE, setPosition);
  center = new Point(stage.stageWidth/2, stage.stageHeight/2);
  addEventListener(Event.ENTER_FRAME, rotate);
  setRotation();
}
図09-025■DisplayObject.addedToStageイベントに登録したリスナーメソッドで座標を設定する
修正は、(1)centerプロパティの初期値を削除し、(2)コンストラクタでDisplayObject.addedToStageイベントのリスナーを登録、(3)そのリスナーメソッド内で初期化の処理を行う。

念のため、DisplayObject.addedToStageイベントのリスナーが正しく削除されているかどうか、EventDispatcher.hasEventListener()メソッドで確かめることにします。EventDispatcher.hasEventListener()メソッドにはイベント定数を引数として渡すと、そのイベントに登録されているリスナーが存在するかどうかをブール(論理)値で返します(Word 09-006)。

Word 09-006■EventDispatcher.hasEventListener()メソッド
つぎのシンタックスで、インスタンスにリスナーが登録されているかどうかをブール(論理)値で返します。

インスタンス.hasEventListener(イベント:String):Boolean

EventDispatcherインスタンスあるいはそのサブクラスのインスタンスに対して、引数で渡したイベントのリスナー関数を調べ、リスナーが存在すればtrue、存在しなければfalseを返します。

イベント ー イベントの種類を文字列で指定します。Eventまたはそのサブクラスに定義されたイベント定数を用いることが推奨されます(イベント定数については、02-07「イベントのことをもう少し知ろう」参照)。

イベント定数Event.ADDED_TO_STAGEを引数として呼出したEventDispatcher.hasEventListener()メソッドの戻り値は、trace()関数によりDisplayObject.removeEventListener()メソッド呼出しの前後にそれぞれ[出力]します(図09-025)。戻り値は、イベントリスナーを削除する前がtrue、削除後はfalseになるべきです。

それでは試してみましょう。FLAファイルのムービー内容やフレームアクションは、デバッグをする際に試したとき(図09-016)のまま変更は要りません。前述のとおり、ステージにはインスタンスを置かず、[ライブラリ]のMovieClipシンボルにクラスEllipticMotionを設定します。クラスと同階層に保存したFLAファイルのメインタイムラインには、以下のフレームアクションを記述して、[ムービープレビュー]を試します。

// フレームアクション
var my_mc:EllipticMotion = new EllipticMotion();
addChild(my_mc);

今度は、EllipticMotionインスタンスがStageオブジェクトの表示リストに追加されるのを待って、DisplayObject.stageプロパティにアクセスしていますので、エラーなくインスタンスがステージ上に配置され、楕円軌道を描いてアニメーションが再生されます。

EventDispatcher.hasEventListener()メソッドの戻り値が、trace()関数によりつぎのように[出力]されれば、リスナーは正しく削除されています(図09-026)。

true
false
図09-026■DisplayObject.addedToStageイベントに登録したリスナーが削除されていることを確認
[ムービープレビュー]で、EventDispatcher.hasEventListener()メソッドの戻り値が、リスナーの削除前と後の2回[出力]。インスタンスはエラーなくステージに配置されて、楕円軌道のアニメーションが行われる。

イベント定数Event.ADDED_TO_STAGEを引数として呼出したEventDispatcher.hasEventListener()メソッドの戻り値によりリスナーの削除が確認できたら、trace()関数のステートメントはコメントアウトか削除して構いません。

修正後のクラスEllipticMotionは、つぎのスクリプト09-004のように定義されます。

スクリプト09-004■クラスEllipticMotionにDisplayObject.addedToStageイベントのリスナーを追加

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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;
    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);
    public function EllipticMotion() {
      addEventListener(Event.ADDED_TO_STAGE, setPosition);
    }
    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);
    }
    private function setPosition(eventObject:Event):void {
      // trace(hasEventListener(Event.ADDED_TO_STAGE));   // 確認用
      removeEventListener(Event.ADDED_TO_STAGE, setPosition);
      // trace(hasEventListener(Event.ADDED_TO_STAGE));   // 確認用
      center = new Point(stage.stageWidth/2, stage.stageHeight/2);
      addEventListener(Event.ENTER_FRAME, rotate);
      setRotation();
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x;
    }
    private function moveY():void {
      y = center.y+sin*radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}


09-04 複数のインスタンスを配置する ー forステートメント
前節09-03でMovieClipシンボルに定義したクラスEllipticMotion(スクリプト09-004)のインスタンスを、複数配置してみることにしましょう。本節では、同じ処理を繰返すforステートメントについて解説します。

インスタンスの配置を指定する
複数のインスタンスは、同じ場所に重ねて置いても意味がありません。複数のインスタンスを配置する前に、その位置を指定できるようにクラスEllipticMotionを修正する必要があります。

今回のアニメーションでEllipticMotionインスタンスの位置は、(1)中心座標と(2)楕円軌道のx軸・y軸の各半径、および(3)回転角度で決まります。そこで、取りあえず前2者の(1)と(2)は固定として、最後の(3)をコンストラクタの引数で指定するようにしてみます。つまり、Flashムービー(FLA)ファイルのフレームアクションからEllipticMotionインスタンスをつぎのように生成すると、楕円軌道のアニメーションは角度90度の位置から開始することになります。

// フレームアクション
var my_mc:EllipticMotion = new EllipticMotion(90);
addChild(my_mc);

クラスEllipticMotionに対して加える修正は、コンストラクタメソッドに受取る引数を指定して、get/setアクセサメソッドdegreeに設定するだけです。

public function EllipticMotion(nDegree:Number) {
  degree = nDegree;
  addEventListener(Event.ADDED_TO_STAGE, setPosition);
}

前に定義したクラスEllipticMotion(スクリプト09-004)は、以下のスクリプト09-005のように変更されます()。コンストラクタの引数には、デフォルト値として0を指定しました。

スクリプト09-005■クラスEllipticMotionのコンストラクタに引数として角度を指定

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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;
    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);
    public function EllipticMotion(nDegree:Number=0) {
      degree = nDegree;
      addEventListener(Event.ADDED_TO_STAGE, setPosition);
    }
    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);
    }
    private function setPosition(eventObject:Event):void {
      removeEventListener(Event.ADDED_TO_STAGE, setPosition);
      center = new Point(stage.stageWidth/2, stage.stageHeight/2);
      addEventListener(Event.ENTER_FRAME, rotate);
      setRotation();
    }
    private function setRotation():void {
      moveX();
      moveY();
      scale();
      blur();
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x;
    }
    private function moveY():void {
      y = center.y+sin*radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

これで、スクリプトから楕円軌道上の任意の位置にEllipticMotionインスタンスを配置して、3D風の回転のアニメーションが実行できます。90度間隔で4つのインスタンスを生成して回転させるには、フレームアクションをつぎのように記述すればよいでしょう。

// フレームアクション
var my_mc:EllipticMotion;
my_mc = new EllipticMotion();
addChild(my_mc);
my_mc = new EllipticMotion(90);
addChild(my_mc);
my_mc = new EllipticMotion(180);
addChild(my_mc);
my_mc = new EllipticMotion(270);
addChild(my_mc);

[ムービープレビュー]を確かめると、4つのインスタンスが楕円軌道上を等間隔で回転します(図09-027)。ただし、アニメーションをよく観察すると、各インスタンスの重ね順が変わらないので、楕円軌道の上(つまり仮想3Dの奥)に移動したインスタンスが、下(仮想3Dの手前)のインスタンスの前に表示されることもあります(図09-028)。この重ね順については、本節の最後に対応する処理を加えます。

図09-027■EllipticMotionクラスのコンストラクタに引数として角度の数値を追加
フレームアクションで90度間隔に4つのインスタンスを生成すると、それぞれが楕円軌道のアニメーションを行う。

図09-028■インスタンスの重ね順は楕円軌道の上下で変わらない
仮想3Dで奥になる楕円軌道上部のインスタンスが、仮想3Dでは手前である軌道下部のインスタンスより前に表示されることもある。

Tips 09-006■変数の使い回し
上記フレームアクションでは、ひとつの変数my_mcを使い回して、複数のインスタンスの格納に用いています。インスタンスのデータ型がみな同じMovieClipですので、この使い回し自体はとくに問題はありません。

しかし、変数に対して各インスタンスの参照を上書きしていますので、最後のインスタンス以外の参照はどこにも保持されません。そうすると、DisplayObjectContainer.removeChild()メソッド(表09-001参照)により表示リストからインスタンスを削除する必要が生じた場合に、引数として渡す参照がなくなってしまうことになります(インデックスが特定できれば、DisplayObjectContainer.removeChildAt()メソッドで取出す途はあります)。

かといって、あまり雑多に変数を宣言することも避けたいです。これは後に、配列でインスタンスを管理することにより解決します。

上記フレームアクションで4つのインスタンスをタイムラインに配置する処理は、基本的に同じ2行のステートメントの繰返しです。つまり、ひとつはコンストラクタによりインスタンスを生成するステートメントで、引数に渡す角度の値以外は同じです。もうひとつは、インスタンスをStageオブジェクトの表示リストに加えるステートメントです。こちらは、インスタンスによる違いはまったくありません。

ActionScriptには、このような繰返しの処理を行う構文が用意されています。その代表であるforステートメントを、次項で使ってみましょう。

forステートメント
forステートメントは、指定された条件(継続条件)が満たされる間、予め決められたステートメントのまとまり(「ステートメントブロック」と呼びます)を繰返し処理します。条件を評価して一定の処理を行う点は、ifステートメントと同じです。しかし、条件が満たされなくなるまで処理を繰返すところが、forステートメントの特徴です。そのシンタックスは、つぎのとおりです。

for (初期化処理; 継続条件; 更新処理) {
   // 繰返す処理
}

初期化処理は、繰返し処理(「ループ処理」ともいいます)に先立って行っておくべき設定です。通常は変数を宣言し、その初期値を代入します。ループ処理は、継続条件が満たされるかぎり繰返されます。条件の指定の仕方はifステートメントと同じです。終了条件ではありませんので、ご注意ください。更新処理は、ループ処理がひとつ終わるごとに実行されます。変数値の更新を行うことが多いでしょう。

簡単な例として、配列に0から9まで10個の整数を連番で格納してみましょう。forステートメントは、以下のように記述します。フレームアクションに記述して、[ムービープレビュー]で確かめると、0から9までの配列エレメントがカンマ区切りで[出力]されます。

for (var i:int = 0, _array:Array = new Array(); i<10; i++) {
  _array.push(i);
}
trace(_array);   // 出力: 0,1,2,3,4,5,6,7,8,9

第1に、初期化の処理ではふたつの変数、int型のiとArray型の_arrayを宣言し、それぞれに初期値として整数0と空の配列を代入しています。複数の変数を初期化するときは、このように間をカンマ(,)で区切り、varキーワードはその先頭にだけ記述します。

Tips 09-007■1ステートメントで複数の変数を宣言する
複数の変数宣言をカンマ(,)区切りで1ステートメントに記述することは、forステートメントだけでなく、通常のフレームアクションやクラスでもできます。

// フレームアクション
var i:int = 0, _array:Array = new Array();
_array.push(i);
trace(_array);

しかし、この書き方は、1ステートメントにまとめられるという以外には、とくに利点がありません。むしろ、各変数の宣言を確認するときには、見づらい結果になりがちです。

forステートメントの初期化処理では、セミコロン(;)でステートメントを分けることができません(継続条件の記述と認識されてしまいます)ので、カンマ(,)区切りで記述する必要が生じます。

第2に、継続条件は関係演算子を使うなどして、ifステートメントの条件と同じように指定します。上記フレームアクションでは変数iの値が10未満の間繰返し処理が行われ、iが10以上になったときに処理を止めてforループから抜けます。継続条件が不適切な場合には、無限ループ(継続条件がつねにtrue)になるおそれがありますので気をつけましょう。

Tips 09-008■15秒ルール
スクリプトの処理が無限ループになっても、Flashコンテンツがハングアップする訳ではありません。バスケットボールの審判のようにFlash Playerが時間を計り、デフォルトではスクリプトが 15秒以上処理を持ち続けると、反則を取られエラーで停止します(図09-029)。これは、悪意のあるプログラムを排除するセキュリティ上の考慮です。

図09-029■15秒のスクリプトタイムアウト
セキュリティ上の考慮から、15秒以上処理が続くとエラーになる。

無限ループでなくても、大きな処理で15秒以上経過してしまうと、やはりエラーになります。そのような処理は分割したうえで、DisplayObject.enterFrameイベントやTimerクラスを利用して順に実行するなど工夫が必要になります。

第3の更新の処理では、ループ処理をひとつ終えるたびに、変数iの値に1加算します。++は、変数(あるいはプロパティ)に整数1を加算するインクリメント演算子です。加算後代入演算子を使った「変数 += 1」と同じ処理になります。なお、整数1を差引くには、デクリメント演算子--が使えます。

Tips 09-009■プリインクリメントとポストインクリメント
インクリメント演算子++は、オペランド(Word 03-005「オペランド」参照)の前後どちらにつけることもできます。前に置くのは「プリインクリメント」、後が「ポストインクリメント」と呼ばれます。オペランドに対する演算は、1加算することに変わりはありません。

ただし、プリインクリメントとポストインクリメントでは、演算子の返す値が変わります。プリインクリメントはオペランドの加算後の値を返すのに対して、ポストインクリメントでは加算前の値が返ります。

// フレームアクション
// プリインクリメント
var i:int = 0;
var j:int = ++i;   // 加算後の値が代入される
trace(i, j);   出力: // 1 1

// ポストインクリメント
var i:int = 0;
var j:int = i++;   // 加算前の値が代入される
trace(i, j);   // 出力: 1 0

これらは、プリインクリメントまたはポストインクリメントした式の戻り値をそのまま代入なり、他の処理に用いた場合の差です。上記フレームアクションの更新処理では、変数iを単にポストインクリメントしただけで、その戻り値をとくに使ってはいません。したがって、プリインクリメントに書替えても、同じ処理になります。

なお、デクリメント演算子--にも、プリデクリメントとポストデクリメントとがあり、インクリメントと同じように戻り値が異なります。


Tips 09-010■forステートメントの初期化と更新で複数の処理を行う
forステートメントでは、初期化だけでなく更新についても、カンマ(,)区切りで複数の処理を記述することができます。たとえば、以下のフレームアクションは、0度以上360度未満の角度の値を、30度刻みで度数とラジアンの値の組合わせにして[出力]します(図09-029)。

// フレームアクション
for (var nDegree:int = 0, nRadian:Number = 0; nDegree<360; nDegree += 30, nRadian = nDegree/180*Math.PI) {
  trace(nDegree, nRadian);
}
図09-030■forステートメントの初期化と更新に複数の処理を記述
0度以上360度未満の角度と、対応するラジアン値を、30度刻みで[出力]。

whileステートメント
whileステートメントは、継続条件の指定のみで繰返し処理を行います。初期化や継続の処理が必要なときは、別途そのステートメントを加えなければなりません。forステートメントの例と同じ、配列に0から9までの整数を格納するには、つぎのようなフレームアクションを記述します。

// フレームアクション
var i:int = 0;
var _array:Array = new Array();
while (i<10) {
  _array.push(i++);
}
trace(_array);   // 出力: 0,1,2,3,4,5,6,7,8,9

まず、ふたつの変数、int型のiとArray型の_arrayは、whileステートメントの前に宣言し、初期値を与えています。つぎに、継続条件の指定は、forステートメントの場合と同じです。そして、更新処理は変数iのインクリメントで、その戻り値をArray.push()メソッドの引数として指定しました。ポストインクリメントですので、メソッドには加算前のiの値が渡されます(Tips 09-008参照)。

一般的には、forステートメントの方が、初期化処理・継続条件・更新処理が明らかに示されるので、スクリプトとして見やすくなります。ですから、とくに初期化や更新の処理が要らないという場合でなければ、forステートメントの利用をお勧めします。

Maniac! 09-005■短く記述する
配列に0から9までの整数を格納するループ処理は、本文で紹介したスクリプトより、もっと短く記述することができます。たとえば、whileステートメントを使った場合には、つぎのようにループ処理のステートメントブロック(中括弧{}内のステートメント)はなくしてしまえます。

// フレームアクション
var i:int = 0;
var _array:Array = new Array();
while (_array.push(i++)<10) {}
trace(_array);   // 出力: 0,1,2,3,4,5,6,7,8,9

Array.push()メソッドは、エレメントを追加した後の配列の長さ(Array.lengthプロパティの値)を返すからです。さらに、forステートメントを使えば、ステートメントブロックなしの1行にすることもできます。

// フレームアクション
for (var i:int = 0, _array:Array = new Array(); _array.push(i)<10; i++) {}
trace(_array);   // 出力: 0,1,2,3,4,5,6,7,8,9

もっとも、本文で紹介したサンプルスクリプトとの違いは、継続条件に変数iの値を使うかArray.push()メソッドの戻り値を使うかということだけで、実質的な処理に差はありません。入力の手間が少しだけ減ることと、短く書いて気分がいいという以外メリットはほとんどなく、スクリプトとしてはむしろ見にくいとさえいえます。したがって、このような記述をとくにお勧めするものではありません。

なお、ステートメントプロックの処理がない場合、中括弧{}は書かなくても済みます。

for (var i:int = 0, _array:Array = new Array(); _array.push(i)<10; i++);

ただ、ループ処理がどこなのかわかりにくくなり、要らぬ誤解を招くもとですので、中括弧{}は明示的に記述することをお勧めします。

複数のインスタンスをループ処理で配置する
それでは、EllipticMotionインスタンスを90度間隔で4つ配置するフレームアクション(図09-027)の例に戻ります。forステートメントを使えば、フレームアクションはつぎのように記述することができます。

// フレームアクション
var my_mc:EllipticMotion;
for (var i:int = 0; i<4; i++) {
  my_mc = new EllipticMotion(i*90);
  addChild(my_mc);
}

しかし、インスタンスの数が4個に決まっている訳ではなく、たとえば6個になったりするかもしれないというときは、数を変数に設定し、角度も数に応じて計算する方がよいでしょう。その場合のフレームアクションは、つぎのスクリプト09-006のようになります。

スクリプト09-006■クラスEllipticMotionのコンストラクタに引数として角度を指定

// フレームアクション
var nCount:int = 6;
var my_mc:EllipticMotion;
for (var i:int = 0; i<nCount; i++) {
  my_mc = new EllipticMotion(i*360/nCount);
  addChild(my_mc);
}

インスタンスの数を納めるint型変数cCountには6を設定しました。したがって、[ムービープレビュー]で確かめると、6個のEllipticMotionインスタンスが60度間隔で回転します(図09-031)。

図09-031■指定された数のインスタンスを等間隔で配置して回転する
変数nCountにインスタンスの数として6を指定したので、60度間隔でインスタンスが配置されて回転のアニメーションを行う。

機能の拡張に備える
EllipticMotionクラスは、次章でさらに拡張する予定です。それに備えて、インスタンスの配置について、もう少し自由度を高めておくことにします。

まず、インスタンスをつくるときに角度の指定が必要ですと、予め配置する数は決めておかなければなりません。これは、後からインスタンスを加えて、配置し直せるようにしたいです。つぎに、楕円軌道のx軸・y軸方向の半径は、次章ではマウス操作に応じてインタラクティブに変化させるつもりです。中心座標も、変えられるようにしておきましょう。

したがって、第1に、クラスEllipticMotionには、インスタンスを配置するためのメソッドが必要になります。その際、引数として指定するのは、角度に加えて、楕円軌道の半径とその中心座標です。第2に、中心座標をクラス外(フレームアクション)から指定するということは、EllipticMotionクラス内でDisplayObject.stageプロパティにアクセスしなくてよいということです。すると、DisplayObject.addedToStageイベントのリスナーは要らなくなります。

そこで、DisplayObject.addedToStageイベントのリスナー関数だったsetPosition()は、改めてインスタンスを配置するためのメソッドとして定義し直すことにします。引数は、角度と楕円軌道の中心座標、および半径です。このメソッドはタイムラインのフレームアクションから呼出したいので、アクセス制御の属性はprivateからpublicに変更します。修正を加えたメソッドsetPosition()は、つぎのとおりです。

// private function setPosition(eventObject:Event):void {
public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
  degree = nDegree;
  // removeEventListener(Event.ADDED_TO_STAGE, setPosition);
  // center = new Point(stage.stageWidth/2, stage.stageHeight/2);

  if (myCenter is Point) {
    center = myCenter;
  }
  if (myRadius is Point) {
    radius = myRadius;
  }
  addEventListener(Event.ENTER_FRAME, rotate);
  setRotation();
}

引数のデータ型は、角度nDegreeをNumber型、楕円軌道の中心座標myCenterとその半径myRadiusはPointクラスで指定しました。引数3つともデフォルト値を定めましたので、メソッドsetPosition()には引数なし(0)から3つまでの任意の数の値が渡せます。まず、第1引数の指定がない、つまりメソッドが引数なしで呼ばれると、nDegreeにはデフォルト値0が設定され、setPosition()内ではその値0がsetアクセサメソッドdegreeに渡されます。

つぎに、第2引数myCenterと第3引数myRadiusのデフォルト値はともにnullです。setPosition()メソッドにこれらの引数が渡されなければ、nullがデフォルトとして引数の値となります。しかし、nullは、前述(Word 09-004「nullプライマリ式キーワード」)のとおり、「値が存在しない」ことを示す値ですので、このままプロパティに設定しても意味がありません。そこで、第2、第3引数が指定されないとき、すなわちデフォルト値nullの場合には、すでにプロパティに設定されている値をそのまま変更しないという処理にしています。

第2引数myCenterと第3引数myRadiusの値を判定するifステートメントは、基本的に同じ構造です。条件として、引数値がnullではないという(不等価!=)比較の式にはなっていません。条件に使われているのは、新しいis演算子(Word 09-007)です。この演算子は、つぎのシンタックス(構文)で、式が指定したクラスのインスタンスであるかどうかを、trueまたはfalseのブール(論理)値で返します。

式 is クラス

式には、もちろん変数も含まれます。その値が、指定したクラスまたはそのサブクラスのインスタンスであればtrue、そうでなければfalseを返します。たとえば、MovieClipインスタンスmy_mcをis演算子で調べると、つぎのような結果が返されます。

trace(my_mc is MovieClip);   // 出力: true
trace(my_mc is DisplayObject);   // 出力: true
trace(my_mc is Object);   // 出力: true
trace(my_mc is Array);   // 出力: false

そして、ここで大事なのは、nullは一般にクラスで型指定された変数やプロパティのデフォルト値でありながら、データはNull型で、クラスのインスタンスとはみなされないということです。つまり、初期化されていないか、あるいは値としてnullが代入された変数は、あるクラスで型指定されていても、is演算子でそのクラスのインスタンスかどうかを調べればfalseが返されるのです。

var my_mc:MovieClip;
trace(my_mc);   // 出力: null
trace(my_mc is MovieClip);   // 出力: false

Word 09-007■is演算子
つぎのシンタックスで、式の値が指定のデータ型として扱えるかどうかを、ブール(論理)値で返します。

式 is データ型

式(Word 03-004「」参照)の値が指定のデータ型で扱えるのは、値がそのデータ型のインスタンスであるか、そのデータ型のサブクラスのインスタンスである場合、あるいはそのインスタンスの属するクラスが後述(10-07「クラスに資格を与える ー インターフェイス」)のインターフェイスを実装する場合です。

式の値が指定のデータ型として扱えればtrueを、扱えないときはfalseを返します。式をあるデータ型の値として処理する前に、データ型を確かめるために用いることができます。

[*筆者用参考]「isとas


AS1&2 Note 09-002■is演算子とinstanceof演算子
is演算子は、ActionScript 2.0のinstanceof演算子に替わるものです。ActionScript 3.0でinstanceof演算子を使うと、デフォルトでは替わりにis演算子を用いるよう警告(Warning)されます。is演算子がinstanceof演算子と異なるのは、インターフェイスを実装しているかどうかが調べられることです。


Tips 09-011■データ型とデフォルト値
ActionScript 3.0で使われるデータ型と、そのデータ型を指定した場合のデフォルト値および取りうる値は、次表09-003のとおりです(ヘルプの[ActionScript 3.0のプログラミング] > [ActionScript言語とシンタックス] > [データ型] > [データ型の記述]参照)。なお、数値のデータ型(intとuintおよびNumber)については、Tips 02-015「数値のデータ型」ですでに解説しました。

表09-003■データ型とデフォルト値
データ型 デフォルト値
Boolean false trueまたはfalse
int 0 32ビット符号付き整数
Null 型指定に用いることはできない null
Number NaN 64ビット浮動小数点数
String null 16ビット文字列
uint 0 32ビット符号なし整数
void undefined undefined
クラス(リファレンス型データ) null クラスのインスタンス

したがって、前記スクリプトのsetPosition()メソッドに渡されたふたつの引数(myCenterとmyRadius)がPointインスタンスでなければ、ifステートメントのプロパティ(centerとradius)を設定する処理が行われず、現在のプロパティ値がそのまま保たれることになるのです。

Maniac! 09-006■nullではないという条件
setPosition()メソッドでis演算子を用いたif条件は、以下のようにnullでないことを不等価演算子!=で確認しても、処理としてはとくに問題はありません。第2および第3引数はPointで型指定されていますので、Point以外のインスタンスを引数に渡せばエラーが生じます。ですから、エラーにならないnullundefinedif条件で排除すれば、それで足ります。

public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
  degree = nDegree;
  // if (myCenter is Point) {
  if (myCenter != null) {
    center = myCenter;
  }
  // if (myRadius is Point) {
  if (myRadius != null) {
    radius = myRadius;
  }
  addEventListener(Event.ENTER_FRAME, rotate);
  setRotation();
}

しかし、うっかり引数の型指定を忘れたとしても、if条件でis演算子を使ってPointインスタンスであることを確かめていれば、他のクラスのインスタンスが紛れ込むおそれはありません。ただし、nullとの不等価比較と比べると、is演算子による評価の方が処理は遅くなります。

コンストラクタメソッドでは、角度の指定もDisplayObject.addedToStageイベントのリスナー登録も必要がなくなります。ですから、コンストラクタは空でよいことになります。それから、プロパティcenterのvar宣言には、初期値を与えておくことにしましょう。以上の修正を加えたクラスEllipticMotionの定義は、以下のスクリプト09-007のようになります(図09-032)。

図09-032■クラスEllipticMotionのコンストラクタとsetPosition()メソッドを修正
プロパティcenterは、var宣言で初期値を与えた。

スクリプト09-007■コンストラクタとsetPosition()メソッドに修正を加えたクラスEllipticMotion

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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);
    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);
    }
    public function setPosition(nDegree:Number=0, myCenter:Point=null, myRadius:Point=null):void {
      degree = nDegree;
2);
      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();
    }
    private function rotate(eventObject:Event):void {
      degree += speed;
      setRotation();
    }
    private function moveX():void {
      x = center.x+cos*radius.x;
    }
    private function moveY():void {
      y = center.y+sin*radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function getIndexZ(nMin:Number=-1, nMax:Number=1):Number {
      if (isNaN(nMin) || isNaN(nMax)) {
        return NaN;
      }
      var nIndexZ:Number = (nMax-nMin)*(sin+1)/2+nMin;
      return nIndexZ;
    }
  }
}

これで、インスタンスの作成とその配置とは、処理を分けることが可能になります。そこで、Flashムービー(FLA)ファイルのフレームアクションも、インスタンスの作成とその配置を分けてみることにします。まず、必要な数のインスタンスを、表示リストには入れずに、一旦配列に格納します。そしてつぎに、配列内のインスタンス数を確かめたうえで、インスタンスを表示リストに加えるとともに等間隔で配置します。そのフレームアクションは、つぎのスクリプト09-008のとおりです(図09-033)。

スクリプト09-008■インスタンスの作成と配置をふたつのforループに分離

// フレームアクション
var nCount:int = 6;
var center:Point = new Point(stage.stageWidth/2, stage.stageHeight/2);
var instances_array:Array = new Array();
var my_mc:EllipticMotion;
for (var i:int = 0; i<nCount; i++) {
  my_mc = new EllipticMotion();
  instances_array.push(my_mc);
}
var nLength:int = instances_array.length;
for (var j:int = 0; j<nLength; j++) {
  my_mc = instances_array[j];
  addChild(my_mc);
  my_mc.setPosition(j*360/nCount, center);
}


図09-033■作成したインスタンスは配列に格納したうえで表示と配置を行う
配列の長さ(Array.lengthプロパティの値)は、予めローカル変数に設定しておくことで最適化をはかる。

このフレームアクションはとくに新しい知識は使っていませんので、forループを使った練習として作成していただくとよいでしょう。ひとつだけ補足するとすれば、インスタンスが格納される配列instances_arrayの長さ(Array.lengthプロパティの値)をローカル変数nLengthに設定したことです。

配列の長さは、ふたつ目のforループで継続条件として用いられています。継続条件は、ループ処理がひとつ終わるたびに評価されます。つまり、配列の長さはループ処理の回数分だけアクセスされることになるのです。すでに述べたとおり(04-07「パラメータを加える」)、「2回以上使うプロパティの値は、ローカル変数に入れた方がお得」でした。そこで、その値をローカル変数に入れて、処理スピードの最適化をはかったのです。

フレームアクションの実行結果は、クラスEllipticMotionにsetPosition()メソッドを定義する前と、今のところ変わりはありません。しかし、インスタンスの作成とその配置を処理として分けたことにより、後からインスタンスを追加したり、楕円軌道を動的に変更したりしやすくなりました。


09-05 重ね順と遠近法
クラスEllipticMotionのアニメーションに、もうふたつ修正を加えます。ひとつは、すでに問題点を指摘してあったインスタンスの重ね順です。インスタンスは、表示リストに加えられた順に前面に重なり(Word 09-003「DisplayObjectContainer.addChild()メソッド」参照)、何もしなければ順番は変わりません。したがって、仮想3Dで奥になる楕円軌道上部に移動しても、下部のインスタンスより手前に表示されるものが出てきてしまいます(図09-034)。この重ね順をコントロールしなければなりません。

図09-034■インスタンスの重ね順は楕円軌道の下が手前になるべき
仮想3Dで奥になる楕円軌道上部のインスタンスと、手前である軌道下部のインスタンスの重ね順が、適切に設定されていない。

もうひとつは、イラストでいわゆる「パース」(パースペクティブ: perspective)と呼ばれる遠近感を、より精密に表現するための修正です。遠近感が平面でどのように表現されるのかを、アニメーションでもう1度確かめてみる必要があります。

重ね順を変える
まず、重ね順は、表示リスト内のインデックスによって決まります。DisplayObjectContainer.addChild()メソッドは、子リストの最後にインスタンスを加えます。後に追加したインスタンスほどインデックスの整数は大きくなり、インデックスの数値が大きいほど手前に表示されます(Word 09-003「DisplayObjectContainer.addChild()メソッド」参照)。

したがって、インスタンスの重ね順を動かすには、表示リスト内におけるインデックスの値を変えればよいということになります。そのためのメソッドが、DisplayObjectContainer.setChildIndex()です。子のインスタンスと整数インデックスのふたつの引数をメソッドに渡して、表示リスト内のインデックスつまり重ね順を変更します(Word 09-008)。

Word 09-008■DisplayObjectContainer.setChildIndex()メソッド
つぎのシンタックスで、表示リストに含まれている子のDisplayObjectインスタンスを指定したインデックスに移動します。

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

DisplayObjectContainerインスタンスに対する処理として、すでに表示リスト内に存在する子のDisplayObjectインスタンスのインデックスを、指定した整数に変更します。子インスタンスのインデックスは数値が大きいほど、表示リスト内の重ね順が手前になります。

表示リスト内のインスタンスのインデックスは、つねに連番になっています。したがって、子インスタンスが予め配置されていたインデックスから除かれることにより、以降の子インスタンスの番号はひとつずつ繰り上がります。そして、新たなインデックスに移されるため、その番号以降の子インスタンスはインデックスがひとつずつ繰り下がります。

第1引数として表示リスト内に存在しないインスタンスを指定したり、第2引数に連番の範囲外のインデックス番号を渡すとエラーになります。


Tips 09-012■DisplayObjectContainer.addChild()とDisplayObjectContainer.addChildAt()メソッドで重ね順を変える
インスタンスの重ね順を最前面にするには、DisplayObjectContainer.addChild()メソッドで、改めて表示リストの最後に加える方法もあります。そのインスタンスを予め表示リストから抜取っておく必要はありません。なぜなら、DisplayObjectインスタンスは「Flash Player上のすべてのDisplayObjectContainerインスタンスの子リストの中にひとつしか存在し得」ないからです(Word 09-003「DisplayObjectContainer.addChild()メソッド」)。したがって、DisplayObjectContainer.addChild()メソッドでインスタンスを子リストの最後に追加すれば、すでにその表示リスト内にあったインスタンスは消えてなくなります。

同じように、インスタンスを再背面にもっていくには、DisplayObjectContainer.addChildAt()メソッド(表09-001「DisplayObjectContainerクラスの子リストを操作するおもなプロパティとメソッド」参照)により、インスタンスを表示リストのインデックス0に追加すればよいことになります。

この段階では、表示リスト内のインスタンスの重ね順を、ひとつひとつ正確に決めることまではしません。楕円軌道の上部(仮想3Dの奥)か下部(仮想3Dの手前)かだけを考え、インスタンスが 上部から下部に移ったとき重ね順を最前面にし、下部から上部に動いたら最背面に送るという処理にします(図09-035)。

図09-035■インスタンスが楕円軌道の上部と下部を移動するときに重ね順を変える
インスタンスが楕円軌道の上部から下部に移ったとき重ね順を最前面にし、下部から上部に動くときに最背面に送る。

インスタンスが楕円軌道の上部と下部の間で移動したときに重ね順を変えるためには、インスタンスがどちらの軌道にあるかを示すフラグが必要です(Tips 08-011「フラグ」参照)。そこで、EllipticMotionクラスには、Boolean型のインスタンスプロパティfrontを宣言します。値は仮想3D手前の軌道下部がtrueで、仮想3D奥になる軌道上部をfalseとします。

インスタンスの位置が楕円軌道の上部か下部かは、sin値がマイナスかプラスかで確かめることができました。sin値を取得するには、メソッドgetIndexZ()を呼出します。引数を渡さなければ、sin値がそのまま返され、楕円軌道上部が0から-1、下部は0から1までの値になります。

なお、表示リスト内の子インスタンスの数は、DisplayObjectContainer.numChildrenプロパティで調べられます(表09-001「DisplayObjectContainerクラスの子リストを操作するおもなプロパティとメソッド」参照)。また、インスタンスが属する表示リストをもつ親のDisplayObjectContainer(MovieClip)インスタンスは、DisplayObject.parentプロパティで参照が得られました。

これくらい前提知識の確認ができれば、クラスEllipticMotionの修正はできるでしょう。重ね順を変更するメソッド名はsetOrder()とします。そうすると、スクリプト09-007につぎのような追加をすれば、よさそうです。

// frontプロパティの宣言を追加
private var front:Boolean = false;

// setRotation()メソッドにsetOrder()メソッドの呼出しを追加
private function setRotation():void {
  moveX();
  moveY();
  scale();
  blur();
  setOrder();   // 追加
}

// setOrder()メソッドを新たに定義
private function setOrder():void {
  if (getIndexZ()>0) {
    if (front == false) {
      front = true;
      parent.setChildIndex(this, parent.numChildren-1);
    }
  } else {
    if (front == true) {
      front = false;
      parent.setChildIndex(this, 0);
    }
  }
}

Flashムービー(FLA)ファイルのフレームアクションは前と同じ(スクリプト09-008)まま、[ムービープレビュー]を確かめると、楕円軌道の上部と下部とで重ね順が変わり、前後の表示も正しい3D風のアニメーションになります。しかし、上記setOrder()メソッドには問題があります。それは、前述スクリプト09-008で、DisplayObjectContainer.addChild()メソッドの呼出しをEllipticMotionクラスのメソッドsetPosition()の後のステートメントに移動してみるとわかります。[ムービープレビュー]で、#1009の「nullのオブジェクト参照」のエラーが発生してしまいます(図09-036)。

図09-036■DisplayObjectContainer.addChild()メソッドをsetPosition()の後に呼出す
[ムービープレビュー]で、#1009の「nullのオブジェクト参照」のエラーが発生。

原因は、setOrder()メソッドの中で用いられているDisplayObject.parentプロパティです。このプロパティは、インスタンスがいずれかのDisplayObjectContainerインスタンスの表示リストに加えられるまでは、値としてnullを返します。nullに対しては、DisplayObjectContainerクラスのメソッド(DisplayObjectContainer.addChild()DisplayObjectContainer.addChildAt())は呼出せませんので、#1009のエラーが発生することになるのです。

Tips 09-013■DisplayObject.parentとDisplayObject.rootプロパティ
前述(Tips 09-005「DisplayObject.rootプロパティと表示リスト」)のとおり、DisplayObject.rootプロパティはStageオブジェクトを頂点とした表示リストに含まれていないと、値としてnullを返します。しかし、DisplayObject.parentプロパティは、Stageオブジェクトの表示リストでなくても、いずれかのDisplayObjectContainerインスタンスの子リストにさえ加えられれば、その親インスタンスの参照を返します。

したがって、エラー#1009を避けるには、setOrder()メソッド本体の処理を行う前に、インスタンスのDisplayObject.parentプロパティがnullでないかどうかを確かめなければなりません。このエラー回避の判定まで含めて修正を加えたクラスEllipticMotionは、つぎのスクリプト09-009のとおりです。

スクリプト09-009■クラスEllipticMotionにsetPosition()メソッドを追加

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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;
    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);
    }
    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;
    }
    private function moveY():void {
      y = center.y+sin*radius.y;
    }
    private function scale():void {
      scaleX = scaleY = getIndexZ(0.8, 1);
      scaleX *= getIndexZ();
    }
    private function blur():void {
      var nBlur:Number = getIndexZ(4, 0);
      var myBlur:BlurFilter = new BlurFilter(nBlur, nBlur/2);
      filters = [myBlur];
    }
    private function 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;
    }
  }
}

setOrder()メソッドに加えた修正(スクリプト09-009)については、3つ補足があります(図09-037)。第1は、すでに述べたとおり、メソッド内の最初のステートメントでDisplayObject.parentプロパティがnullでないことを確かめていることです。この判定は、もちろんつぎのように不等価比較の式で行っても構いません。

if (parent != null) {

しかし、前記スクリプト09-009では、if条件にDisplayObject.parentプロパティのみを直接指定しました。このプロパティは値がDisplayObjectContainer型で定義されていますので、DisplayObjectContainerインスタンスかnullのいずれかを返します。そして、if条件に指定した式(変数やプロパティも含む)は、クラスのインスタンスであればtrueと評価され、nullfalseとして扱われるのです(Tips 05-003「if条件の評価」参照)。したがって、このif条件で、プロパティ値がDisplayObjectContainerインスタンスかnullかは正しく判定できます。

図09-037■setOrder()メソッドの修正
[修正前]
[修正後(スクリプト09-009)]
DisplayObject.parentプロパティがnullでないことの確認に加え、軌道上部と下部の重ね順を切替える処理も一部変更。

第2に、ふたつ目のif条件で、インスタンスの位置が楕円軌道の上部から下部、あるいは下部から上部に変わったことを判定しています。前述(図09-037)修正前のif条件の式であり、修正後は不等価比較(!=演算子)の左辺になったつぎの論理式は、インスタンスが今現在、楕円軌道の上部にあるときはfalse、下部になるとtrueを返します。

getIndexZ()>0

他方、EllipticMotionクラスのインスタンスプロパティfrontは、直前の処理のとき楕円軌道の上部にあったらfalse、下部ならtrueが設定されています。上の論理式の返す値とこのプロパティ値が一致しないのは、軌道の上部と下部の間をインスタンスが移った瞬間だということになります。したがって、このときインスタンスの重ね順を変えなければなりません。

そこで第3は、この場合にインスタンスの重ね順をどう変えるかです。まず、frontプロパティの値は、インスタンスの楕円軌道上の位置が上部と下部の間を移動しましたので、それに合わせてtruefalseとを切替えます。つぎに、重ね順を変えるためには、最前面(parent.numChildren-1)と最背面(0)のどちらの値を、インデックスとしてDisplayObjectContainer.setChildIndex()メソッドに渡すべきか決めなければなりません。前記スクリプト09-009では、条件演算子?:により、つぎのようにインデックス値を定めています(Tips 05-008「条件演算子?:を使う」参照)。

var nIndex:int = (front=!front) ? parent.numChildren-1 : 0;

ローカル変数nIndexへの代入式の右辺に、条件演算式が用いられています。その第1項が条件です。ただし、条件式が、不等価比較(!=)の論理式でなく、論理否定演算子!(Word 08-002「論理否定演算子!」参照)で反転したブール(論理)値の代入式であることにご注意ください。代入式は代入された値を返しますので、truefalseを反転して代入した後のfrontプロパティ値が条件として評価されます(Tips 08-010「if条件に指定した代入式」参照)。

つまり、インスタンスが楕円軌道を上部から下部に移れば、frontプロパティはfalseからtrueに反転しますので、条件はtrueと評価されて条件演算式の第2項(parent.numChildren-1)が、その逆の場合つまりfrontプロパティがfalseに変わったときは第3項(0)が、変数nIndexに代入されます。

この変数nIndexの値を、DisplayObjectContainer.setChildIndex()メソッドの第2引数のインデックスに指定して呼出します。すると、インスタンスが楕円軌道上部から下部に移るとき最前面に、逆に下部から上部に移動すれば最背面に、その重ね順が変わります。つまり、現在とその前とで楕円軌道の上下が変わったとき、最前面または最背面にインスタンスの重ね順を変えるという処理が行われる訳です。

Tips 09-014■処理のわかりやすさ
スクリプト09-009のEllipticMotionクラスでsetOrder()メソッドは、つぎのように定義しても、ことさら処理に不利な点はありません。ifステートメントが3重構造になってはいるものの、内容はシンプルだといえます。しかし、スクリプト09-009のsetOrder()メソッドの方が、明らかに短い記述です。

private function setOrder():void {
  if (parent) {
    if (getIndexZ()>0) {
      if (front == false) {
        front = true;
        parent.setChildIndex(this, parent.numChildren-1);
      }
    } else {
      if (front == true) {
        front = false;
        parent.setChildIndex(this, 0);
      }
    }
  }
}

もっとも、これまで何度か指摘してきたとおり、短くてもわかりにくい記述になっては管理しづらい結果となります。それに、短い記述が最適化された処理とはかぎりません。

今回の場合、処理速度の点では、両者に差はないと考えられます。そして、わかりやすさの面でも、スクリプト09-009のsetOrder()メソッドは、楕円軌道におけるインスタンスの上下位置が切り替ったとき、その上下に応じて重ね順を最背面または最前面のインデックスに変える、という処理の流れは明らかです。

そこで本書では、とくにトリッキーな処理になっていなければ、記述の短さもわかりやすさにはプラスになると考えて、スクリプト09-009の記述を採用しました。



真ん中の線を越えたとき、重ね順を変える。

さて、新しいEllipticMotionクラス(スクリプト09-009)で、フレームアクションのDisplayObjectContainer.addChild()メソッドをsetPosition()メソッドの後に呼出して、正しく動作するかどうかを確かめてみましょう。[ムービープレビュー]を試すと、#1009のエラーを発生させることなく、インスタンスが楕円軌道を描いてアニメーションします(図09-038)。

図09-038■DisplayObjectContainer.addChild()メソッドをsetPosition()の後に呼出す ー 修正後
setPosition()メソッドの後にDisplayObjectContainer.addChild()を呼出しても、エラーなしに重ね順の変更が行われる。

遠近法をより正確に
楕円軌道のアニメーションに3D風の遠近感を与えるため、すでにふたつ手を加えてありました。ひとつは、blur()メソッドで、インスタンスが軌道上部つまり仮想3Dの奥にいくほど、ぼかしを強くかけたことです。ふたつ目は、scale()メソッドにより、インスタンスが軌道上部に移動するに従いサイズを小さくしていることです。しかし、後者のサイズ変更が、アニメーションをよく見ると不自然です(図09-039)。

図09-039■楕円軌道の上部と下部とでインスタンスの間隔が異なる
軌道下部ではインスタンス同士の間がほぼくっついているのに、上部では間隔が開いている。

上図09-039では、インスタンスの数を増やして、楕円軌道の最下部でインスタンス同士がほぼくっつくようにアニメーションさせてみました。ところが、軌道の最上部を見ると、インスタンスの間隔が明らかに開いています。これは、軌道上の位置に合わせて変えているのが、インスタンスのサイズだけだからです。

3D風の表現として奥にいくほどサイズを小さくするのは、同じ大きさが遠くなるほど小さく見えるためです。たとえば、横断歩道のように、同じ幅の水平線が等間隔で前方に並んでいたとしましょう(図09-004)。平面で表現するとき、同じ幅の線つまり2点間の水平距離は、遠くなるほど短くなります。また、線と線との間隔も遠くにいくほど詰まって見えます。したがって、楕円軌道のアニメーションでも、遠近感をより正確に表現するには、インスタンスのサイズだけでなく、座標についても同じように変化させる必要があるのです。

図09-040■同じ幅の水平線が等間隔で並ぶ平面図
水平線の長さは遠いほど短く、線と線との間隔も詰まって見える。

では、具体的に座標は、どのように変えたらよいでしょう。以下の図09-041は、3次元の空間を上から見て、奥行き(z軸)と水平方向の広がり(x軸)の2軸で表したものです。平面として表現する投影面に、奥行きをもって(z位置に)配置されたオブジェクトが表示されるとき、視点から投影面への焦点距離と投影面からオブジェクトまでのz方向距離が投影する大きさを決めます。

図09-041■焦点距離とz方向距離が投影像の大きさを決める
投影像の大きさ/オリジナルの大きさ = 焦点距離/(焦点距離+z方向距離)

相似な三角形と辺の比の関係から、オリジナルの大きさと投影像の大きさとの比はつぎのとおりです。

投影像の大きさ/オリジナルの大きさ = 焦点距離/(焦点距離+z方向距離)

この式から、投影像の大きさが求められます。

投影像の大きさ = オリジナルの大きさ×焦点距離/(焦点距離+z方向距離)

ここまで「大きさ」と書きましたけれど、座標についてももちろん同じように考えられます。投影面のz座標を0として、z座標値がzの点(x, y, z)を投影面に変換したときの点(x', y, 0)のx座標x'も、まったく同じ比例式で求められます(図09-042。y座標は、ここでは問題とならず、任意です)。つまり、焦点距離をFとすれば、座標x'は以下のように導くことができます。

図09-042■z軸座標zのx座標を投影面の座標x'に変換する
比例の関係から、x' = x×F/(F+z)が導かれる。
x'/x = F/(F+z)
x' = x×F/(F+z)

上図09-042を3次元の横から見た図と考えれば(この場合x座標が任意です)、z軸y軸平面上の点と投影面への座標変換も同じ比例式になります。したがって、点(x, y, z)を投影面に変換したときの点(x, y', 0)のy座標y'はつぎのとおりです。

y' = y×F/(F+z)

結局、座標もサイズも、「F/(F+z)」あるいは「焦点距離/(焦点距離+z方向距離)」という同じ比率を、元の座標やサイズに掛け合わせれば、遠近法の効果が与えられた値に変換できるということです。そこで、具体的に「焦点距離」と「z方向距離」を決める必要があります。

今、EllipticMotionクラス(スクリプト09-009)のscale()メソッドは、つぎのようにgetIndexZ()メソッドによりインスタンスのサイズ(DisplayObject.scaleXDisplayObject.scaleYプロパティ)を簡易的に変えています。このときのgetIndexZ()メソッドの戻り値は、楕円軌道最下部の1(実寸)から最上部の0.8までの値です。この比率は、基本的に維持しましょう。

private function scale():void {
  scaleX = scaleY = getIndexZ(0.8, 1);

そうすると、軌道最下部つまりz軸方向で最前面のとき、比率は1にするということです。これは簡単です。最前面のz座標を0と定めればよいでしょう。z座標値が0であれば、z軸方向の距離は0で、焦点距離がいくつであろうと(ただし0は除きます)、分子分母の値が等しくなり、1になるからです。

焦点距離/(焦点距離+z軸方向距離) = F/(F+z)
z = 0を代入する。
F/(F+0) = F/F = 1

それでは、最背面のz座標はいくつにすべきでしょうか。実は、この値はいくつでも構いません。実際に表示するのはxy軸の2次元ですから、zは最前面と最背面の間の位置が相対的にわかりさえすればよいのです。もう少し具体的には、つぎの焦点距離を求めるとき、最背面のz座標を大きくすれば、必要な焦点距離も値が大きくなります。しかし、焦点距離もz座標もF/(F+z)の比率を求めるのに使われるだけですので、具体的な値までは問題とならないのです。だとすれば、最前面のz座標を0としましたので、最背面は値を1とするのが簡単でしょう。

そこで、つぎに焦点距離を求めます。z座標が最背面の値1を取ったとき、比率は0.8になるようにします。ただ、スクリプトでは0.8と値を決め打ちにせず、変数を使いたいと思います。ですから、この比率を「最少比率」として、式を解くことにします。

焦点距離/(焦点距離+z軸方向距離) = F/(F+z)
z = 1のとき最少比率Sとする。
F/(F+1) = S
F = S(F+1)
(1-S)F = S
F = S/(1-S)

これで、スクリプトに修正を加える準備が整いました。修正の要点を述べます。第1に、インスタンスプロパティを、以下のとおり新たに3つ宣言します。最少比率がminScale、焦点距離はfocalLength、そして座標やサイズに掛合わせる比率をcurrentScaleとしましょう。最初のふたつのプロパティは予め値が決まっていますので、宣言時に初期値を代入しておきます。最後のプロパティは、インスタンスの仮想のz座標の変化にともなって、刻々と値が変わります。ですから、宣言のみで初期値は設定しません。

private var minScale:Number = 0.8;
private var focalLength:Number = minScale/(1-minScale);
private var currentScale:Number;

第2に、座標やサイズに掛合わせる比率currentScaleの計算です。角度が変わるたびに値は変わりますので、setアクセサメソッドdegreeに以下のように処理を加えることにします。最前面が0、最背面を1とする仮想のz座標値は、getIndexZ()メソッドで取得します。ここまでの修正は、下図09-043のとおりです。

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));
}
図09-043■プロパティを新たに3つ宣言してsetアクセサメソッドdegreeで比率の計算
プロパティminScale(最少比率)、focalLength(焦点距離)、currentScale(投射比率)の宣言を加え、setアクセサメソッドdegreeでcurrentScaleを計算。

最後に修正の第3として、座標(DisplayObject.xDisplayObject.yプロパティ)とサイズ(DisplayObject.scaleXDisplayObject.scaleYプロパティ)に投射の比率currentScaleを乗じます。修正対象のメソッドは、以下のとおりmoveX()とmoveY()およびscale()の3つになります(図09-044)。注意すべき点として、moveX()とmoveY()メソッド内で座標を変換するとき、currentScaleは中心座標(center.xおよびcenter.y)には掛合わせません。中心座標は楕円軌道の起点ですので、この位置は動かさないからです。

private function moveX():void {
  // x = center.x+cos*radius.x;
  x = center.x+cos*radius.x*currentScale;
}
private function moveY():void {
  // y = center.y+sin*radius.y;
  y = center.y+sin*radius.y*currentScale;
}
private function scale():void {
  // scaleX = scaleY = getIndexZ(0.8, 1);
  scaleX = scaleY = currentScale;
  scaleX *= getIndexZ();
}
図09-044■座標とサイズを処理する3つのメソッドに投射比率を反映させる
moveX()とmoveY()およびscale()内の処理で、座標(DisplayObject.xDisplayObject.yプロパティ)とサイズ(DisplayObject.scaleXDisplayObject.scaleYプロパティ)に投射の比率currentScaleを乗じる。

遠近法をより正確に表現するための以上3点の修正が加わったクラスEllipticMotionは、以下のスクリプト09-010のとおりです。前に試したように(図09-039)フレームアクション(スクリプト09-008)でインスタンスの数を調整して、間隔が詰まるようにしたうえで[ムービープレビュー]を確かめると、今度は軌道上部に移っても、インスタンスの間隔が開きません。座標にも遠近法を適用しましたので、仮想z軸の奥つまり座円軌道の上部にインスタンスが移動しても、大きさだけでなく相対的な間隔も一緒に変わるからです。

スクリプト09-010■クラスEllipticMotionにより正確な遠近法の処理を加える

// ActionScript 3.0クラス定義ファイル: EllipticMotion.as
package {
  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;
    }
  }
}


図09-045■楕円軌道の上部と下部とでインスタンスの間隔は変わらない
座標にも遠近法を適用したので、仮想z軸で奥になる楕円軌道上部でも、インスタンスの間隔が相対的に保たれる。


大きさを縮めるとき、一緒に位置も詰めれば、より正確な3Dになる。


Tips 09-015■楕円軌道が小さくなった?
座標にも遠近法の効果を加えたことで、楕円軌道上部(z軸の奥)にいくほど、x座標は軌道中心に向けて、y座標は下方に位置が詰まる結果になります。したがって、遠近法の修正を行う前(図09-039)よりも、楕円軌道の見かけは小さくなります。

楕円軌道のx軸y軸方向の半径は、EllipticMotionクラスのプロパティradiusの初期値を修正することにより変えられます。

private var radius:Point = new Point(100, 50);   // Pointコンストラクタに渡す引数xとyの値を修正する

Column 09 [ライブラリ]アイテムから動的にインスタンスをつくる
MovieClipシンボルやサウンド、ビットマップなど[ライブラリ]アイテムのインスタンスを、スクリプトで動的に作成する方法について簡単にご紹介します。

MovieClipシンボル
09章ですでにご説明したとおり、MoiveClipシンボルにクラスを設定すれば、そのコンストラクタの呼出しによりインスタンスが生成できました。では、とくにプロパティやメソッドを加える必要がなく、素のMovieClipシンボルのままインスタンスをつくりたいときはどうしたらよいでしょう。この場合にも、クラスは設定しなければなりません。

そうなると、MovieClipクラスを継承した空のクラスを定義して、それをMovieClipシンボルに設定すればよさそうです。しかし、空のクラスをActionScript(AS)ファイルとしてわざわざ記述する作業は要りません。とくにクラスは定義せずに、[シンボルプロパティ]ダイアログボックスの[クラス]フィールドに存在しないクラス名たとえば"Pen"と入力してみましょう(図09-046)。

図09-046■[リンケージプロパティ]ダイアログボックスの[クラス]に未定義のクラス名を入力
Penというクラスは、定義されていない。

[OK]ボタンで[リンケージプロパティ]ダイアログボックスを閉じようとすると、Penというクラスは定義されていないので、警告が表示されます(図09-047)。これは[クラス]フィールドに指定したクラス名で、MovieClipを継承した空のクラスが、Flash CS3によって自動的にSWF内に作成されることを示します。ここで、[OK]ボタンをクリックして、この警告のダイアログボックスを閉じます。

Tips 09-016■[リンケージプロパティ]ダイアログボックスの[基本クラス]
[リンケージプロパティ]ダイアログボックスの[基本クラス](図09-046)は、[クラス]フィールドに入力したクラスが継承するスーパークラスを示します(「基本クラス」という呼び方については、Word 04-001「継承」参照)。MovieClipシンボルの場合、デフォルトではflash.display.MovieClipが指定されいます。


図09-047■クラス定義が自動生成されることを告げる警告
[リンケージプロパティ]ダイアログボックスの[クラス]フィールドに入力したクラスが未定義なので、SWFファイル書出し時にFlash CS3が自動的にクラスを生成する。

Maniac! 09-007■自動生成されるクラス
自動生成されるのは、MovieClipクラスを継承した空のクラスです。本文の例のように、[リンケージプロパティ]ダイアログボックスの[クラス]フィールドに"Pen"と入力した場合は、つぎのようなクラスがFlash CS3によってSWFファイル内に作成されます([ヘルプ]の[ActionScript 3.0のプログラミング] > [ムービークリップの操作] > [ActionScriptでのMovieClipオブジェクトの作成] > [ActionScriptに対するライブラリシンボルの書き出し]参照)。

package {
  import flash.display.MovieClip;
  public class Pen extends MovieClip {
    public function Pen() {
    }
  }
}

クラスPenはFlashによって自動生成されることになりましたので、クラスは定義されている前提でインスタンスがつくれます。つまり、クラスのコンストラクタを呼出せばよいのです。クラスPenの設定されたMovieClipシンボルを[ライブラリ]に持つFlashムービー(FLA)ファイルのフレームアクションに、たとえばつぎのようなステートメントを記述すれば、クラスPenを設定したMovieClipシンボルのインスタンスがタイムラインに配置されます(図09-048)。

var _mc:MovieClip = new Pen();
addChild(_mc);
図09-048■[クラス]を設定したMovieClipシンボルのインスタンスはコンストラクタの呼出しで作成できる
[リンケージプロパティ]ダイアログボックスの[クラス]に指定したクラスは自動生成されるので、コンストラクタを呼出せばMovieClipシンボルのインスタンスが作成できる。インスタンスの座標をステージ中央に設定するステートメントが加えられている。

Tips 09-017■スーパークラスによる型指定
インスタンスを代入する変数(プロパティ)には、スーパークラスで型指定することもできます。スーパークラスのプロパティとメソッドは、すべてサブクラスのインスタンスから使うことができるからです。逆に、サブクラスで型指定した変数に、スーパークラスのインスタンスを格納することはできません。スーパークラスにはないプロパティあるいはメソッドを、サブクラスは備えられるためです。

前記フレームアクションでは、クラスPenのインスタンスを納める変数_mcは、MovieClipクラスで型指定しています。もちろん、クラスPenを型指定に用いても構いません。しかし、インスタンスを動的に生成するためだけにつけたクラス名ですし、中身は空ですから実質はMovieClipクラスです。それを明らかにするため、前述フレームアクションでは、変数をMovieClipクラスで型指定しました。

[ライブラリ]に読込んだサウンド
[ライブラリ]に読込んだサウンドも、スクリプトで動的にインスタンスを作成することができます([ライブラリ]へのサウンドの読込みについては、[ヘルプ]の[Flashユーザーガイド] > [サウンドの操作] > [Flashでのサウンドの使用] > [サウンドの読み込み]以下をお読みください)。この場合も、[リンケージプロパティ]ダイアログボックスで[クラス]を指定します(図09-049)。そのクラスが定義されていないと、MovieClipシンボルのときと同じように(図09-047)、自動的に生成されることを示す警告が表示されます。ダイアログボックスの[OK]ボタンをクリックすれば、Soundクラスを継承するクラスがSWFファイルに生成されます。

図09-049■[リンケージプロパティ]ダイアログボックスでサウンドに[クラス]を指定
[リンケージプロパティ]ダイアログボックスで指定した[クラス]が未定義であれば、MovieClipシンボルのときと同じ警告(図09-047)が示され、クラスを自動生成することができる。

Tips 09-018■サウンドの[基本クラス]
サウンドの[リンケージプロパティ]ダイアログボックスには、[基本クラス]にデフォルトでflash.media.Soundが指定されます。つまり、生成されるクラスは、Soundクラスを継承します。したがって、もしサウンドに設定するカスタムクラスを定義する場合には、そのクラスはSoundクラスを継承する必要があります。

[ライブラリ]のサウンドに[リンケージプロパティ]ダイアログボックスで[クラス]を設定したら、インスタンスは原則どおりコンストラクタを呼出して生成します。もっとも、Soundクラスは、ステージには表示されません。インスタンスがつくられたかどうかは、trace()関数で[出力]してみるなど、別途確かめた方がよいでしょう。生成されたインスタンスは、たとえばクラスMySoundが設定されていれば、[object MySound]と[出力]されます(図09-050)。

図09-050■[クラス]の設定されたサウンドのインスタンスを生成して[出力]
サウンドの[リンケージプロパティ]ダイアログボックスで[クラス]にMySoundが設定されていれば、trace()関数の引数にインスタンスを渡すと[object MySound]と[出力]される。

Sound(を継承したクラスの)インスタンスは、ステージに表示するといった処理は要りませんので、インスタンスが生成されればただちに利用できます。サウンドを再生するには、つぎのフレームアクションのようにSound.play()メソッドを呼出します。

var my_sound:Sound = new MySound();
var myChannel:SoundChannel = my_sound.play();

Tips 09-019■Sound.play()メソッドはSoundChannelインスタンスを返す
Sound.play()メソッドを呼出すと、戻り値としてそのサウンドを制御するSoundChannelインスタンスが返されます(図09-051)。再生しているサウンドをコントロールするには、このSoundChannelインスタンスが必要となりますので、上記フレームアクションのように、これを変数(またはプロパティ)に取得しておくようにしましょう。

図09-051■Sound.play()メソッドの返すSoundChannelインスタンスは変数に取得しておく
戻り値の格納された変数を[出力]すると、SoundChannelインスタンスが確認できる。

たとえば、再生しているサウンドを停止するには、Soundクラスのメソッドではなく、SoundChannel.stop()メソッドを呼出します。


Tips 09-020■[ライブラリ]に読込んだサウンドとSWFのサイズ
[ライブラリ]に読込んだサウンドは、SWFファイルに埋込まれます。そのため、[ヘルプ]では「埋込みサウンド」と呼ばれることがあります([ActionScript 3.0のプログラミング] > [サウンドの操作] > [埋め込みサウンドの操作]参照)。サウンドデータは容量が大きくなりがちですので、サウンドを扱ったSWFファイルのサイズには注意する必要があります。

とくに、サウンドの[リンケージプロパティ]ダイアログボックスで[クラス]を設定すれば、デフォルトでは[最初のフレームに書き出し]のチェックがついています(図09-049参照)。この場合、MovieClipシンボルについて説明したのと同じく、データのダウンロードが済むまでメインタイムラインの第1フレームさえ表示されないことになります(Tips 09-003「[最初のフレームに書き出し]しない場合」参照)。

サウンドデータのためにSWFファイルの初期ロードに時間がかかる場合は、[最初のフレームに書き出し]のチェックを外して後のフレームにダウンロードを分散するか、後の章でご紹介する外部サウンドファイルをロードする方法が検討の対象になるでしょう。

[ライブラリ]に読込んだビットマップ
[ライプラリ]に読込まれたビットマップも、MovieClipシンボルやサウンドと基本的に同じ考え方でインスタンスがつくれます。つまりまずは、[リンケージプロパティ]ダイアログボックスで[クラス]を指定することです(図09-052)。指定したのが未定義のクラスであれば、警告が表示されて、そのクラスはSWFファイルに自動的に生成されます。

図09-052■[リンケージプロパティ]ダイアログボックスでビットマップに[クラス]を指定
未定義のクラスを指定すれば、警告が表示されて、クラスはSWFファイルに自動生成される。

クラスが設定できたら、以下のフレームアクションでコンストラクタを呼出して、ビットマップのインスタンスをつくってみましょう。インスタンスはtrace()関数に引数として渡し、その生成を確かめます。ところが、[ムービープレビュー]を試すと、[コンパイルエラー]が表示されてしまいます(図09-053)。

var myData:BitmapData = new MyBitmapData();
trace(myData);
図09-053■ビットマップに指定したクラスのコンストラクタを呼出す
[コンパイルエラー]が表示されて、コンストラクタに引数はふたつ必要だと告げる。

[コンパイルエラー]は、「引数の数」が正しくないと告げます。そして、コンストラクタメソッドMyBitmapData()には、ふたつの引数が必要だと指摘しています。クラスMyBitmapDataは定義していませんので空のクラスが自動生成され、その実質は[リンケージプロパティ]ダイアログボックスの[基本クラス]に指定されていたスーパークラスBitmapDataです。したがって、BitmapDataクラスを確かめてみなければなりません。

BitmapDataクラスのコンストラクタを[ヘルプ]で調べてみると、シンタックスが以下のように記載されています。引数は4つあり、後のふたつにはデフォルト値が指定されているものの、初めのふたつの引数にはデフォルト値がありません(図09-054)。つまり、第1引数の幅(width)と第2引数の高さ(height)は、省略できないということになります。

public function BitmapData(width:int, height:int, transparent:Boolean = true, fillColor:uint = 0xFFFFFFFF)
図09-054■[ヘルプ]のBitmapDataクラスのBitmapData()コンストラクタの説明冒頭
4つの引数の内、後のふたつにはデフォルト値が指定され、初めのふたつwidthとheightにはデフォルト値がない。

ビットマップの[リンケージプロパティ]ダイアログボックスで[クラス]に指定し、自動生成されるクラスMyBitmapDataのコンストラクタも、継承するBitmapDataクラスのコンストラクタに渡すふたつの引数を受取る必要があるのです。では、[ライブラリ]に読込まれたビットマップの幅と高さは、どのように取得すればよいでしょう。[ライブラリ]のビットマップを、予め調べておかなければならないのでしょうか。

結論から言えば、幅と高さのふたつの引数は、ともに0で構いません。インスタンスを生成すれば、ビットマップの実際の幅と高さが、改めてBitmapData.widthBitmapData.heightプロパティに設定されます。したがって、[リンケージプロパティ]ダイアログボックスで[クラス]を指定したビットマップのインスタンスは、つぎのフレームアクションのように生成します(図09-055)。

var myData:BitmapData = new MyBitmapData(0, 0);
trace(myData, myData.width, myData.height);   // 出力: [object MyBitmapData] 幅 高さ
図09-055■ビットマップに指定したクラスのコンストラクタには引数をふたつ渡して呼出す
引数で渡した幅と高さは0でも、インスタンスのBitmapData.widthBitmapData.heightプロパティにはビットマップの幅と高さが設定される。

Maniac! 09-008■BitmapData()コンストラクタと引数の0
BitmapDataクラスのコンストラクタに、第1引数の幅と第2引数の高さとして0を指定して呼出すとランタイムルエラーになります(図09-056)。コンストラクタに渡す引数の幅や高さは、正の整数でなければなりらないからです。

図09-056■BitmapDataクラスのコンストラクタに幅と高さの引数として0を渡すとランタイムエラー
コンストラクタメソッドBitmapData()の第1および第2引数に引数は幅と高さなので、正の整数でなければならない。

しかし、ビットマップの[リンケージプロパティ]ダイアログボックスで[クラス]に指定したクラスがBitmapDataクラスを継承し、そのコンストラクタからスーパークラスであるBitmapDataのコンストラクタを呼出す場合には、ふたつの引数として0を渡してもランタイムエラーにはなりません。

それでは[ライブラリ]のビットマップからインスタンスが作成できたので、つぎにそれをステージに表示します。しかし、DisplayObjectContainer.addChild()メソッドに、MyBitmapDataインスタンスを引数として渡すと、今度は[コンパイルエラー]が示されます(図09-057)。エラーの[説明]は、つぎのとおりです。

1067: 型 flash.display:BitmapDataの値が、関連しない型flash.display:DisplayObjectに暗黙で型変換されています。
図09-057■DisplayObjectContainer.addChild()メソッドにビットマップのインスタンスを渡すと[コンパイルエラー]が発生
エラーの[説明]は、「BitmapDataの値」が「DisplayObject」に「型変換されて」いるという。

DisplayObjectは、DisplayObjectContainer.addChild()メソッドの引数に指定されている型です(Word 09-003「DisplayObjectContainer.addChild()メソッド」)。その引数としてBitmapDataクラスのサブクラスMyBitmapDataのインスタンスを渡しましたので、これをDisplayObjectのデータ型に変換するのはとくに不思議はありません。

[ヘルプ]の[コンパイルエラー]の項([ActionScript 3.0コンポーネントリファレンスガイド] > [付録])で、エラーコード1067の説明を見てみましょう。すると、「変換できない型にオブジェクトをキャストしようとしてい」る場合のエラーとあります。「キャスト」というのは、データ型を変換することです(後述Tips 10-012「キャスト」参照)。つまり、DisplayObjectContainer.addChild()メソッドの引数として指定されているデータ型のDisplayObjectに、BitmapData(またはそのサブクラスの)インスタンスは変換できないという意味なのです。

図09-058■[ヘルプ]の[コンパイルエラー]のエラーコード1067の説明
処理中のインスタンスが、指定されたデータ型に変換できないことを示す。

実際、[ヘルプ]で[BitmapData]クラスの説明冒頭に示されている「継承」を確かめると、Objectクラスを直接継承していて、DisplayObjectはスーパークラスに含まれていません。したがって、DisplayObjectクラスのプロパティやメソッドを備えていないので、インスタンスをDisplayObjectにデータ型変換することもできないのです。

図09-059■[ヘルプ]の[BitmapData]クラスの説明冒頭
BitmapDataクラスは、Objectクラスを直接継承する。

Maniac! 09-009■BitmapDataクラスのwidthとheightプロパティ
BitmapDataクラスがDisplayObjectクラスを継承しないということは、幅と高さのプロパティwidthheightはDisplayObjectのものではないということを意味します。これらはBitmapDataクラスに直接定義されたプロパティで、DisplayObject.widthDisplayObject.heightとは異なり、読取り専用とされています。

BitmapData(またはそのサブクラスの)インスタンスをDisplayObjectとして扱えるようにするには、Bitmapクラスを用います。BitmapクラスのコンストラクタにBitmapDataインスタンスを引数として渡すと、BitmapDataのイメージを含んだBitmapインスタンスが生成できます。そして、BitmapクラスはDisplayObjectクラスを継承します(図09-060)。

図09-060■[ヘルプ]の[Bitmap]クラスの説明冒頭
Bitmapクラスは、DisplayObjectクラスを継承する。

以上をまとめると、[ライブラリ]のビットマップに[リンケージプロパティ]ダイアログボックスで[クラス]を指定して、そのインスタンスをタイムラインに配置するには、つぎの3つの処理が必要になります。

第1は、指定した[クラス]のコンストラクタを呼出して、ビットマップのインスタンスを生成することです。このとき、幅と高さのふたつの引数を渡さなければならないことに注意しましょう。第2に、生成されたビットマップのインスタンスを引数にして、Bitmapインスタンスをつくります。そして最後に、Bitmapインスタンスを、DisplayObjectContainer.addChild()メソッドに渡して、タイムラインに配置します。この処理は、つぎのフレームアクションのようになります。

var myData:BitmapData = new MyBitmapData(0, 0);
var myBitmap:Bitmap = new Bitmap(myData);
addChild(myBitmap);

[ムービープレビュー]で確かめると、[ライブラリ]で[クラス]を設定したビットマップがタイムラインに配置されて表示されます(図09-061。図のフレームアクションでは、Bitmapインスタンスの座標も調整しています)。

図09-061■クラスの設定された[ライブラリ]内のビットマップをステージに表示
BitmapDataを継承するビットマップのインスタンスは、Bitmapクラスのコンストラクタに引数として渡し、BitmapインスタンスをDisplayObjectContainer.addChild()メソッドでタイムラインに配置する。

[Prev/Next]


作成者: 野中文雄
更新: 2008年7月16日 Column 09「[ライブラリ]アイテムから動的にインスタンスをつくる」のスクリプトを1箇所修正。
作成日: 2008年5月9日


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