作成日

23 January 2011

本連載ではActionScript 3.0の初心者の方を対象に、ActionScriptを使って表現を広げるためのテクニックを紹介していきます。ActionScript 3.0初心者向けに考え方や原理を解説するという目的のため、基本的には次のようなスタイルをとります。
・フレームアクションを使う。
・ActionScriptライブラリは使わない。

第1回 スクリプトによるアニメーションの基本01
第2回 スクリプトによるアニメーションの基本02
第3回 複数のオブジェクトの制御方法
第4回 複数のオブジェクトの制御方法:スクリプトのバリエーション
第5回 複数のオブジェクトの制御方法:sin値によるアニメーションと組み合わせる
第6回 ウィンドウのリサイズに合わせてオブジェクトのレイアウトを変更する

今回は、これまでにやってきた「sin値によるアニメーション」と「複数のオブジェクトの制御」を組み合わせたサンプルを作成します。作成するサンプルは、以下のアニメーションです。

  1. ステージ上に9個の円があり、数字の「1」の形に配置してあります。
  2. [1→2]ボタンをクリックすると9個の円が移動して数字の「2」の形に変化します。
  1. [2→1]ボタンをクリックした場合も同じように9個の円が移動して数字の「1」の形に変化します。

※サンプルを見る:sample05.swf

それでは、このアニメーションを作成する手法を解説していきましょう。また、手法についていくつかのバリエーションも紹介していきます。

処理の概要を確認する

今回は、ボタンクリックで、9個のムービークリップインスタンスを同時に開始位置から終了位置に移動させます。まず必要になるのは、それぞれの開始位置(「1」を形作る座標位置)と終了位置(「2」を形作る座標位置)です。開始位置はもともと「1」の形に配置したインスタンスの初期位置を使用します。終了位置となる「2」の形の座標は実際にFlash上でインスタンスを配置して決定しました。

これを踏まえて処理を考えると、概要は次のようになります。

  1. 開始位置と終了位置を各インスタンスのカスタムプロパティとして設定する。
  2. [1→2]/[2-1]ボタンをクリックすると次の処理を行うように設定する。
    2-1. アニメーションの初期処理として、開始位置から終了位置までの距離を算出する。
    2-2. entarFrameイベントハンドラでsin値の変化に応じてアニメーションを進行させる。
    2-3. 終了位置に到着したら、enterFrameイベントハンドラを削除し、開始位置と終了位置を入れ替える。

ムービークリップインスタンスのアニメーションについて少し仕組みを考えてみましょう。まず第4回で扱ったように、ムービークリップシンボル内にアニメーション開始用のメソッドを定義し、同じくシンボル内で定義したenterFrameイベントハンドラを追加する方法です。

この方法はシンプルで分かりやすいのですが、やや気になる点があります。1つはアニメーションに使用するsin値の問題です。今回は9個のムービークリップインスタンスが同期してアニメーションします。つまり、全てのインスタンスは同一のsin値を元に移動すべきですが、シンボル内で「アニメーションの処理を行うentarFrameイベントハンドラ」を定義すると、インスタンスごとにentarFrameイベントハンドラが実行され、sin値も個々に算出されることになります。これは1箇所で計算した値を各ムービークリップインスタンスで参照する形が望ましいでしょう。

2つ目は処理負荷の問題です。シンボル内でentarFrameイベントハンドラを定義すると、インスタンスの数の分だけ個別のenterFrameイベントが発生します(それぞれに異なるイベントオブジェクトが生成されます)。イベントオブジェクトの生成は比較的負荷のかかる処理です(今回のサンプルでは、扱うインスタンス数が少ないので、実際には気にするほどではありませんが)。そのため、メインタイムラインにentarFrameイベントハンドラを定義し、その中から各ムービークリップインスタンスにアニメーションを更新するためのカスタムイベントMOVEを発行する形にします。

この考え方で作成したのが次のサンプルです。

例1:メインタイムラインでenterFrameイベントハンドラを実行する

スクリプト例1:05_01.fla シンボル内のスクリプト

//▼プロパティ定義 var pStart:Point;//開始位置 var pGoal:Point;//終了位置 var pDistans:Point;//開始位置と終了位置の距離 var myParent:MovieClip; //▼メソッド定義 //初期化処理 function xInit(pos2X, pos2Y):void { //メインタイムラインをムービークリップとして取得 myParent = parent as MovieClip; //ターゲットを登録 myParent.xAddTarget(this); //開始位置と終了位置を設定 pStart = new Point(this.x, this.y); pGoal = new Point(pos2X, pos2Y); } //▼カスタムイベントハンドラ //開始時 function startHandler(eventObj:Event):void{ pDistans = pGoal.subtract(pStart); } //位置更新 function moveHandler(eventObj:Event):void{ var nSin:Number = myParent.xGetSin(); this.x = pStart.x + pDistans.x * nSin; this.y = pStart.y + pDistans.y * nSin; } //終了時(開始位置、終了位置入れ替え) function moveEndHandler(eventObj:Event):void { var pTemp:Point = pStart; pStart = pGoal; pGoal = pTemp; }

スクリプト例1:05_01.fla メインタイムラインのスクリプト

//▼イベント用定数定義 const START:String = "start"; const MOVE:String = "move"; const MOVE_END:String = "moveEnd"; //▼変数定義 const DURATION:Number = 500;//アニメーションの長さ(ミリ秒) const START_RADIAN:Number = -Math.PI * 0.5;//開始角度(ラジアン) const END_RADIAN:Number = Math.PI * 0.5;//終了角度(ラジアン) const RANGE_RADIAN:Number = END_RADIAN - START_RADIAN;//変化範囲(ラジアン) const START_SIN:Number = Math.sin(START_RADIAN);//開始時のサイン値 const END_SIN:Number = Math.sin(END_RADIAN);//終了時のサイン値 var aTargets:Array = [];//配列で制御対象を管理 var nStartTime:Number;//開始時間 var nSin:Number;//sin値を加工した0~1の値 var toEnabledBtn:Button;//アニメーション終了後に使用可能にするボタン xInit(); //▼関数定義 //初期化 function xInit():void { //各初期化処理 xInitMC();//ムービークリップ xInitButton();//ボタン } //ムービークリップ初期化 function xInitMC():void { //各MCの初期化処理 a_mc.xInit(232, 200); b_mc.xInit(200, 200); c_mc.xInit(168, 200); d_mc.xInit(185, 162); e_mc.xInit(210, 123); f_mc.xInit(232, 85); g_mc.xInit(216, 51); h_mc.xInit(184, 51); i_mc.xInit(168, 84); } //ボタン初期化 function xInitButton():void { bt1To2.addEventListener(MouseEvent.CLICK, btn_clickHandler); bt2To1.addEventListener(MouseEvent.CLICK, btn_clickHandler); } //sin値を0~1に直して返す function xNormalize(sin_:Number):Number { var sinNormal:Number = (sin_ - START_SIN) / (END_SIN - START_SIN); return sinNormal; } //カスタムイベント設定 function xAddTarget(targetMc:MovieClip):void { aTargets.push(targetMc);//カスタムイベントのターゲットを追加 //イベント処理 this.addEventListener(START, targetMc.startHandler); this.addEventListener(MOVE, targetMc.moveHandler); this.addEventListener(MOVE_END, targetMc.moveEndHandler); } //ムービークリップからのsin値取得用 function xGetSin():Number { return nSin; } //▼イベントハンドラ定義 //ボタン共用clickイベントハンドラ function btn_clickHandler(eventObj:MouseEvent):void { //自分を無効にし、他方を移動終了後に有効にできるよう保持 switch (eventObj.target) { case bt1To2: bt1To2.enabled = false; toEnabledBtn = bt2To1; break; case bt2To1: bt2To1.enabled = false; toEnabledBtn = bt1To2; break; } //アニメーション開始 nStartTime = getTimer();//開始時間取得 this.dispatchEvent(new Event(START)); this.addEventListener(Event.ENTER_FRAME, enterFrameHandler); } //enterFrameイベントハンドラ function enterFrameHandler(eventObj:Event):void { var nPastTime:Number = getTimer() - nStartTime;//経過時間取得 var nTimeRate:Number = nPastTime / DURATION;//進行度を取得 if (nTimeRate > 1) nTimeRate = 1; var nCurrentRadian:Number = START_RADIAN + RANGE_RADIAN * nTimeRate; nSin = xNormalize(Math.sin(nCurrentRadian));//sin値を取得 this.dispatchEvent(new Event(MOVE)); if (nTimeRate >= 1){ this.removeEventListener(Event.ENTER_FRAME, enterFrameHandler); this.dispatchEvent(new Event(MOVE_END)); toEnabledBtn.enabled = true;//他方のボタンを有効に } }

スクリプトのポイントを解説しましょう。まず、9個のムービークリップインスタンスのインスタンス名は「a_mc」~「i_mc」としています。

カスタムイベントは、アニメーション開始時に発行するSTARTイベント、enterFrameイベントハンドラからアニメーションを進行させるために発行するMOVEイベント、移動終了時に発行するMOVE_ENDイベントを使用します。

メインタイムラインのxInitMC()メソッドでは、各ムービークリップインスタンスの終了位置(「2」の形)のX座標とY座標を引数としてxInit()メソッドを実行し、初回のアニメーションの開始位置と終了位置を設定する初期化処理を行っています。開始位置は、SWFロード時に9個のムービークリップインスタンスは「1」の形に配置してあるため、その座標となります。ボタンクリックによる2回目以降のアニメーションでは、この初期化処理は実行されず、前回のアニメーション終了時にmoveEndHandler()メソッドで再設定さる開始位置と終了位置をもとに移動するようになっています。

また、シンボルのxInit()メソッド内では、メインタイムラインのxAddTarget()メソッドを実行して、カスタムイベントのターゲットを登録しています。このようにシンボル内から親であるメインタイムラインのカスタムメソッドを実行するには、parentプロパティで参照するメインタイムラインをMovieClipとして認識させる必要があります。そのためas演算子を使ってMovieClipに型変換したメインタイムラインへの参照を変数myParentに保持しています。

enterFrameイベントハンドラのenterFrameHandler()メソッドではメインタイムラインのグローバル変数nSinにsin値を格納しています。このsin値をムービークリップインスタンスから参照するためにxGetSin()メソッドを用意してあります。xGetSin()メソッドはシンボル内のmoveEndHandler()メソッド内で参照しています。

例2:ムービークリップシンボルをコンポーネント化する

例1では、各インスタンスの終了位置を固定値で指定しましたが、このような場合コンポーネント定義を使うこともできます。コンポーネント定義を行うことで、[プロパティ]インスペクター(CS4では[コンポーネントインスペクタ])でパラメーターの指定を行えるようになります。

パラメーターで指定した値は、ムービークリップに定義された対応する変数に自動的に代入されます。コンポーネント定義を行うには[ライブラリ]で対象となるムービークリップシンボルを選択し、オプションメニューから[コンポーネント定義...]実行します。
[コンポーネント定義]ダイアログで[+]ボタンでパラメータを追加します。プログラム内で使用する変数名は[変数]欄で指定します。

各ムービークリップインスタンスの終了位置をプログラム内で指定する必要が無くなるので、スクリプトは次のように変更できます。

スクリプト例2:05_02.fla シンボル内のスクリプト

//▼プロパティ定義 var pStart:Point;//開始位置 var pGoal:Point;//終了位置 var pDistans:Point;//開始位置と終了位置の距離 var myParent:MovieClip; //コンポーネントパラメータとして取得 var goalX:Number var goalY:Number; xInit(); //▼メソッド定義 //初期化処理 function xInit():void { //メインタイムラインをムービークリップとして取得 myParent = parent as MovieClip; //ターゲットを登録 myParent.xAddTarget(this); //開始位置と終了位置を設定 pStart = new Point(this.x, this.y); pGoal = new Point(goalX, goalY); } //▼カスタムイベントハンドラ //開始時 function startHandler(evt:Event):void{ pDistans = pGoal.subtract(pStart); } //位置更新 function moveHandler(evt:Event):void{ var nSin:Number = myParent.xGetSin(); this.x = pStart.x + pDistans.x * nSin; this.y = pStart.y + pDistans.y * nSin; } //終了時(開始位置、終了位置入れ替え) function moveEndHandler(evt:Event):void { var pTemp:Point = pStart; pStart = pGoal; pGoal = pTemp; }

スクリプト例2:05_02.fla メインタイムラインのスクリプト

//▼イベント用定数定義 const START:String = "start"; const MOVE:String = "move"; const MOVE_END:String = "moveEnd"; //▼変数定義 const DURATION:Number = 500;//アニメーションの長さ(ミリ秒) const START_RADIAN:Number = -Math.PI * 0.5;//開始角度(ラジアン) const END_RADIAN:Number = Math.PI * 0.5;//終了角度(ラジアン) const RANGE_RADIAN:Number = END_RADIAN - START_RADIAN;//変化範囲(ラジアン) const START_SIN:Number = Math.sin(START_RADIAN);//開始時のサイン値 const END_SIN:Number = Math.sin(END_RADIAN);//終了時のサイン値 var aTargets:Array = [];//配列で制御対象を管理 var nStartTime:Number;//開始時間 var nSin:Number;//sin値を加工した0~1の値 var toEnabledBtn:Button;//アニメーション終了後に使用可能にするボタン xInit(); //▼関数定義 //初期化 function xInit():void { //ボタン初期化 bt1To2.addEventListener(MouseEvent.CLICK, btn_clickHandler); bt2To1.addEventListener(MouseEvent.CLICK, btn_clickHandler); } //sin値を0~1に直して返す function xNormalize(sin_:Number):Number { var sinNormal:Number = (sin_ - START_SIN) / (END_SIN - START_SIN); return sinNormal; } //カスタムイベント設定 function xAddTarget(targetMc:MovieClip):void { aTargets.push(targetMc);//カスタムイベントのターゲットを追加 //イベント処理 this.addEventListener(START, targetMc.startHandler); this.addEventListener(MOVE, targetMc.moveHandler); this.addEventListener(MOVE_END, targetMc.moveEndHandler); } //ムービークリップからのsin値取得用 function xGetSin():Number { return nSin; } //▼イベントハンドラ定義 //ボタン共用clickイベントハンドラ function btn_clickHandler(eventObj:MouseEvent):void { //自分を無効にし、他方を移動終了後に有効にできるよう保持 switch (eventObj.target) { case bt1To2: bt1To2.enabled = false; toEnabledBtn = bt2To1; break; case bt2To1: bt2To1.enabled = false; toEnabledBtn = bt1To2; break; } //アニメーション開始 nStartTime = getTimer();//開始時間取得 this.dispatchEvent(new Event(START)); this.addEventListener(Event.ENTER_FRAME, enterFrameHandler); } //enterFrameイベントハンドラ function enterFrameHandler(eventObj:Event):void { var nPastTime:Number = getTimer() - nStartTime;//経過時間取得 var nTimeRate:Number = nPastTime / DURATION;//進行度を取得 if (nTimeRate > 1) nTimeRate = 1; var nCurrentRadian:Number = START_RADIAN + RANGE_RADIAN * nTimeRate; nSin = xNormalize(Math.sin(nCurrentRadian));//sin値を取得 this.dispatchEvent(new Event(MOVE)); if (nTimeRate >= 1){ this.removeEventListener(Event.ENTER_FRAME, enterFrameHandler); this.dispatchEvent(new Event(MOVE_END)); toEnabledBtn.enabled = true;//他方のボタンを有効に } }

コンポーネントパラメータとして定義した終了位置を表す変数goalX、goalYはシンボル内でグローバル変数として宣言しておく必要があります。メインタイムラインではムービークリップインスタンスに終了位置を設定する必要が無くなったのでその部分の処理は削ってあります。

さて、ここでは紹介のためにコンポーネント定義を使ってムービークリップシンボルをコンポーネント化しましたが、コンポーネントは本来コンポーネントの制作者と使用者が異なる場合に有効です。使用者はプログラムコードを変更することなくコンポーネントの振る舞いを変えることができるので、安全に使うことができるからです。それ以外の場合には、ともすれば処理の全貌が見えにくくなることがあるので効果を検討した上で使うのがよいでしょう。

例3:カスタムイベントをクラスとして定義する

本連載ではカスタムクラスは使用しない方針ですが、今回は少しだけ使ってみようと思います。その目的は、MOVEイベントハンドラ(シンボル内のmoveHandler()メソッド)で、sin値を取得する際にメインタイムラインのxGetSin()メソッドを通して取得しているのを、イベントオブジェクトのカスタムプロパティとして受け取れるようにすることです。

ここまではカスタムイベントを発行する際に、Eventインスタンスを引数としてEventDispatcher.dispatchEvent()メソッドを実行してきましたが、Eventインスタンスにはカスタムプロパティを追加できません。そこで、カスタムイベント用のクラスを作成して、そこでsin値を保持するためのカスタムプロパティを定義しようと思います。ここではクラス名はAnimationEventクラス、sin値を保持するプロパティはsinプロパティとします。なお、sinプロパティを使用するのはMOVEイベントだけですが、今回使っている3つのカスタムイベントは全てAnimationEventクラスとして定義しようと思います。

以下、スクリプトコードです。

スクリプト例3:AnimationEvent.as クラス定義ファイルのスクリプト

package { //▼インポート import flash.events.Event; public class AnimationEvent extends Event{ //▼イベント用定数定義 public static const START:String = "start"; public static const MOVE:String = "move"; public static const MOVE_END:String = "moveEnd"; //▼MOVEイベント用sin値 public var sin:Number; public function AnimationEvent(type:String) { super(type); } } }

スクリプト例3:05_03.fla シンボル内のスクリプト

//▼プロパティ定義 var pStart:Point;//開始位置 var pGoal:Point;//終了位置 var pDistans:Point;//開始位置と終了位置の距離 var myParent:MovieClip; //コンポーネントパラメータとして取得 var goalX:Number var goalY:Number; xInit(); //▼メソッド定義 //初期化処理 function xInit():void { //メインタイムラインをムービークリップとして取得 myParent = parent as MovieClip; //ターゲットを登録 myParent.xAddTarget(this); //開始位置と終了位置を設定 pStart = new Point(this.x, this.y); pGoal = new Point(goalX, goalY); } //▼カスタムイベントハンドラ //開始時 function startHandler(eventObj:AnimationEvent):void{ pDistans = pGoal.subtract(pStart); } //位置更新 function moveHandler(eventObj:AnimationEvent):void{ this.x = pStart.x + pDistans.x * eventObj.sin; this.y = pStart.y + pDistans.y * eventObj.sin; } //終了時(開始位置、終了位置入れ替え) function moveEndHandler(eventObj:AnimationEvent):void { var pTemp:Point = pStart; pStart = pGoal; pGoal = pTemp; }

スクリプト例3:05_03.fla メインタイムラインのスクリプト

//▼変数定義 const DURATION:Number = 500;//アニメーションの長さ(ミリ秒) const START_RADIAN:Number = -Math.PI * 0.5;//開始角度(ラジアン) const END_RADIAN:Number = Math.PI * 0.5;//終了角度(ラジアン) const RANGE_RADIAN:Number = END_RADIAN - START_RADIAN;//変化範囲(ラジアン) const START_SIN:Number = Math.sin(START_RADIAN);//開始時のサイン値 const END_SIN:Number = Math.sin(END_RADIAN);//終了時のサイン値 var aTargets:Array = [];//配列で制御対象を管理 var nStartTime:Number;//開始時間 var toEnabledBtn:Button;//アニメーション終了後に使用可能にするボタン xInit(); //▼関数定義 //初期化 function xInit():void { //ボタン初期化 bt1To2.addEventListener(MouseEvent.CLICK, btn_clickHandler); bt2To1.addEventListener(MouseEvent.CLICK, btn_clickHandler); } //sin値を0~1に直して返す function xNormalize(sin_:Number):Number { var sinNormal:Number = (sin_ - START_SIN) / (END_SIN - START_SIN); return sinNormal; } //カスタムイベント設定 function xAddTarget(targetMc:MovieClip):void { aTargets.push(targetMc);//カスタムイベントのターゲットを追加 //イベント処理 this.addEventListener(AnimationEvent.START, targetMc.startHandler); this.addEventListener(AnimationEvent.MOVE, targetMc.moveHandler); this.addEventListener(AnimationEvent.MOVE_END, targetMc.moveEndHandler); } //▼イベントハンドラ定義 //ボタン共用clickイベントハンドラ function btn_clickHandler(eventObj:MouseEvent):void { //自分を無効にし、他方を移動終了後に有効にできるよう保持 switch (eventObj.target) { case bt1To2: bt1To2.enabled = false; toEnabledBtn = bt2To1; break; case bt2To1: bt2To1.enabled = false; toEnabledBtn = bt1To2; break; } //アニメーション開始 nStartTime = getTimer();//開始時間取得 this.dispatchEvent(new AnimationEvent(AnimationEvent.START)); this.addEventListener(Event.ENTER_FRAME, enterFrameHandler); } //enterFrameイベントハンドラ function enterFrameHandler(eventObj:Event):void { var nPastTime:Number = getTimer() - nStartTime;//経過時間取得 var nTimeRate:Number = nPastTime / DURATION;//進行度を取得 if (nTimeRate > 1) nTimeRate = 1; var nCurrentRadian:Number = START_RADIAN + RANGE_RADIAN * nTimeRate; //AnimationEvent.MOVEイベントオブジェクト生成 var moveEventObj:AnimationEvent = new AnimationEvent(AnimationEvent.MOVE); //sin値をイベントオブジェクトのプロパティとして設定 moveEventObj.sin = xNormalize(Math.sin(nCurrentRadian));//sin値を取得 this.dispatchEvent(moveEventObj); if (nTimeRate >= 1){ this.removeEventListener(Event.ENTER_FRAME, enterFrameHandler); this.dispatchEvent(new AnimationEvent(AnimationEvent.MOVE_END)); toEnabledBtn.enabled = true;//他方のボタンを有効に } }

カスタムイベント用の定数は全てクラス定義ファイル内でパブリック定数として定義したので「AnimationEvent.START」のように記述します。組み込みのイベントっぽくなりました。ポイントは、メインタイムラインのenterFrameイベントハンドラ内で、AnimationEvent.MOVEイベントを発行する際にAnimationEventインスタンスのsinプロパティにsin値を設定している点です。この値はシンボル内のmoveHandler()メソッド内でイベントオブジェクトのプロパティとして参照できています。

なお、今回のカスタムイベントクラスは最小限の記述にしていますが、本来はEvent.clone()メソッドやEvent.toString()メソッドをオーバーライドすべきとされています。詳しくはオンラインヘルプ「Eventクラス」などをご確認ください。

次号予告

次回は、ブラウザウィンドウのサイズ変更に合わせてオブジェクトのレイアウトを変更するサンプルを作成する予定です。