【連載】Starlingフレームワークを用いたStage3Dによる2Dアニメーション 第5回 Starlingフレームワークでイベントリスナーを扱う [Edit]

前回の「StarlingフレームワークのTweenクラスにおける最適化とJugglerクラスの実装」では、Jugglerクラスを用いたコールバックについてご説明しました。けれど、もちろんStarlingフレームワークでも、定義済みActionScript 3.0と同じようにイベントリスナーが使えます。そして、基本的な考え方には、違いがありません。ただ、具体的なイベントの扱いが異なったり、Starlingフレームワーク独自の工夫が加えられた部分もあります。今回は、そのあたりを中心に解説します。

  • サンプルファイル: スクリプト002と004 (Starling_05.zip/Flash CS6形式/約49KB)
    ダウンロードファイルには、Starlingフレームワークのライブラリは含まれていません。予めインストールしてください。

01 イベントリスナーはイベントオブジェクトを受取らなくてもよい

まずは、[ライブラリ]のビットマップからImageインスタンスをつくって、ステージの真ん中に置きます(図001)。この処理は、すでに第1回「Starlingフレームワークで書く初めてのアニメーション」04「[ライブラリ]のビットマップをStarlingのステージに置く」でご説明しました。Starlingルートクラス(MySprite)は、後に掲げるスクリプト001のように定めました。

図001■[ライブラリ]のビットマップからつくられたインスタンスがステージ中央に置かれた
図001

基本的な中身は、第1回スクリプト004と同じです。ただ、後で手を加えやすいようにメソッドを小分けしたり、プロパティも多めに宣言してあります。しかし、ひとつだけ第1回スクリプト004とも定義済みActionScript 3.0とも大きく違うところがあります。それは、DisplayObject.addedToStageイベント(定数Event.ADDED_TO_STAGE)のリスナーメソッド(initialize())が引数をもたないことです。

public function MySprite() {
	addEventListener(Event.ADDED_TO_STAGE, initialize);
}
// private function initialize(eventObject:Event):void {
private function initialize():void {
	// ...[中略]...
}

定義済みActionScript 3.0では、リスナーがイベントオブジェクトを引数に受取らないとエラーになりました。Starlingフレームワークでも、第1回スクリプト004のようにイベントオブジェクトは受取れます。けれど、DisplayObject.addedToStageDisplayObject.enterFrameイベントでは、イベントオブジェクトをリスナーが使うことはほとんどないでしょう。そのようなときには、引数を省いてしまえるのです[*1]。Starlingルートクラス(MySprite)全体は、つぎのスクリプト001のとおりです。

スクリプト001■[ライブラリ]のビットマップからImageインスタンスをつくってステージ中央に置く

// ActionScript 3.0クラスファイル: MySprite.as
package  {
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.Event;
	import flash.display.BitmapData;
	public class MySprite extends Sprite {
		private var center:Image;
		private var stageWidth:int;
		private var stageHeight:int;
		private var centerX:Number;
		private var centerY:Number;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize():void {
			var myBitmapData:BitmapData = new Pen();
			var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
			stageWidth = stage.stageWidth;
			stageHeight = stage.stageHeight;
			centerX = stageWidth / 2;
			centerY = stageHeight / 2;
			center = createCenter(myTexture);
		}
		private function createCenter(myTexture:Texture):Image {
			var instance:Image = createImage(myTexture);
			instance.x = centerX;
			instance.y = centerY;
			return instance;
		}
		private function createImage(myTexture:Texture):Image {
			var instance:Image = new Image(myTexture);
			instance.pivotX = instance.width / 2;
			instance.pivotY = instance.height / 2;
			addChild(instance);
			return instance;
		}
	}	
}

[*1] EventDispatcherクラスが内部的にイベントリスナーを呼出すメソッド(invokeEvent())は、非ドキュメントのFunction.lengthプロパティでリスナーの引数の数を確かめ。値が0のときはイベントオブジェクトは渡さずにリスナーを呼出しています。なお、具体的な実装については「Function.lengthプロパティ」の「」としてご紹介しています。

02 Touchイベントでドラッグ操作を扱う

Starlingフレームワークで扱う基本的なイベントのうちActionScript 3.0と勝手が違うのは、マウスを使った操作です。マウスとタッチパネルの操作は、すべてDisplayObject.touchイベント(定数TouchEvent.TOUCH)で扱います。つまり、マウスボタンを押すのも放すのも、あるいはドラッグするのもDisplayObject.touchイベントひとつで捉えるということです。

何らかのマウス操作が行われたことだけは、DisplayObject.touchイベントでわかります。それがどういう操作かは、イベントリスナーが引数に受取ったTouchEventオブジェクトから情報を得ることになります。まず、TouchEvent.getTouch()メソッドでTouchオブジェクトを取出します。つぎに、そのTouch.phaseプロパティを調べると、マウス操作がわかるのです。前掲スクリプト001には、マウスドラッグの扱いを加えます。ドラッグに関わるマウス操作と、それぞれのTouch.phaseプロパティの値はつぎの表001のとおりです。

表001■ドラッグに関わるマウス操作とTouch.phaseプロパティの値
マウス操作 イベント Touch.phaseプロパティの値
マウスボタンを押す DisplayObject.touch TouchPhase.BEGAN
ドラッグする TouchPhase.MOVED
マウスボタンを放す TouchPhase.ENDED

Touch.phaseプロパティの値は文字列です。TouchPhaseクラスに各値が定数として定められていますので、それを用いるのがよいでしょう。マウスだけでなくタッチパネルの操作も併せて、TouchPhaseクラスの定数をつぎの表002にまとめました。

表002■TouchPhaseクラスの定数とタッチパネルおよびマウスの操作
TouchPhase
クラスの定数
操作
タッチパネル マウス
BEGAN 画面に触れる マウスボタンを押す
ENDED 画面から指を離す マウスボタンを放す
HOVER マウスポインタを重ねる
MOVED 画面に触れた指を動かす ボタンは押したままマウスを動かす
STATIONARY 画面に触れたまま動かさない ボタンを押したままマウスは動かさない

ここまで読まれて、Starlingフレームワークでマウス操作を扱うのは面倒そうに感じられたかもしれません。けれど、ドラッグは定義済みActionScript 3.0よりむしろ簡単です。

というのは、TouchPhaseというクラスの名前から想像できるように、マウスの操作が一連の流れの中の段階(phase)として捉えられています。そして、Touch.phaseプロパティが定数TouchPhase.MOVEDという値をとるのは、TouchPhase.BEGANの後の段階です。つまり、マウスボタンは押された後そのままマウスが動いているのですから、すなわちドラッグを意味します。定義済みActionScript 3.0のように、マウスを動かしているとき、ボタンが押されているかどうかなど調べずに済むのです[*2]

それでは、前掲スクリプト001にマウスのドラッグの扱いを加えます。まずは、3つのクラスTouchとTouchEventおよびTouchPhaseを、新たにimport宣言します。

import starling.events.Touch;
import starling.events.TouchEvent;
import starling.events.TouchPhase;

前述のとおり、インスタンスをマウスでドラッグして動かすだけでしたら、Touch.phaseプロパティの値が定数TouchPhase.MOVEDのとき処理すれば済んでしまいます(「Starlingフレームワークでインスタンスをドラッグ&ドロップする」をお読みください)。プロパティが他の値の場合も扱いに含めるため、ステージ上を水平ドラッグすると、その幅に応じてインスタンスが回るというお題で考えます(図002)。

図002■ステージ上を水平ドラッグするとその幅に応じてインスタンスが回る
図002

ステージ上でマウスボタンを押してドラッグし、放すまでの3つのTouch.phaseプロパティの値それぞれについて処理を加えるには、スクリプトはつぎのような組立てになります。第1に、Stageオブジェクト(DisplayObject.stageプロパティ)のDisplayObject.touchイベント(定数TouchEvent.TOUCH)にイベントリスナー(onTouch())を加えます。第2に、リスナーメソッドは引数のTouchEventオブジェクト(eventObject)から、TouchEvent.getTouch()メソッドでTouchオブジェクト(myTouch)を得ます。念のためオブジェクトがあるかを調べ、なければ処理を終えます。そして第3に、TouchオブジェクトのTouch.phaseプロパティによりマウス操作を調べるのです。

private function initialize():void {
	// ...[中略]...
	stage.addEventListener(TouchEvent.TOUCH, onTouch);
}
private function onTouch(eventObject:TouchEvent):void {
	var myTouch:Touch = eventObject.getTouch(stage);
	if (!myTouch) return;
	switch (myTouch.phase) {
		case TouchPhase.BEGAN:
			// マウスボタンを押したときの処理
			break;
		case TouchPhase.MOVED:
			// ドラッグ中の処理
			break;
		case TouchPhase.ENDED:
			// マウスボタンを放したときの処理
			break;
	}
}

Touch.phaseプロパティの値をswitchステートメントで3つのcaseに分けたうえで、それぞれつぎのような処理を行います。第1に、プロパティ値がTouchPhase.BEGANつまりマウスボタンを押したときはドラッグの始まりですので、現在のマウスポインタの水平座標(Touch.globalXプロパティ)の値をプロパティ(startX)に納めます。

第2に、Touch.phaseプロパティ値がTouchPhase.MOVEDのとき、ドラッグしている間の処理になります。マウスポインタの水平の動きから回転角(angle)を求め、インスタンスのDisplayObject.rotationプロパティに定めます。ステージ幅(stageWidth)一杯に動かすと1回転(2πラジアン)するように比率を決めました。なお、回転のラジアン角は±2πの範囲に収まるよう、2πラジアン(PI_2)の剰余%をとっています。

第3に、マウスボタンを放してドラッグを終えたTouchPhase.ENDEDがプロパティ値の場合です。現在の回転角をプロパティ(startAngle)に納めたうえで、その値をtrace()関数で[出力]して確かめました。


private const PI_2:Number = Math.PI * 2;
private var startAngle:Number = 0;
private var startX:Number;
// ...[中略]...
private function onTouch(eventObject:TouchEvent):void {
	var myTouch:Touch = eventObject.getTouch(stage);
	if (!myTouch) return;
	switch (myTouch.phase) {
		case TouchPhase.BEGAN:
			startX = myTouch.globalX;
			break;
		case TouchPhase.MOVED:
			var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
			angle %= PI_2;
			center.rotation = angle;
			break;
		case TouchPhase.ENDED:
			startAngle += PI_2 * (myTouch.globalX - startX) / stageWidth;
			startAngle %= PI_2;
			trace(startAngle);  // 確認用
			break;
	}
}

前掲スクリプト001を書替えたStarlingルートクラス(MySprite)は、以下のスクリプト002のとおりです。ステージを水平にドラッグすると、動かした方向と幅に応じて、真ん中に置かれたインスタンスが回ります。

スクリプト002■ステージ上のドラッグ幅に応じてインスタンスを回す

// ActionScript 3.0クラスファイル: MySprite.as
package  {
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.Event;
	import starling.events.Touch;
	import starling.events.TouchEvent;
	import starling.events.TouchPhase;
	import flash.display.BitmapData;
	public class MySprite extends Sprite {
		private const PI_2:Number = Math.PI * 2;
		private var center:Image;
		private var stageWidth:int;
		private var stageHeight:int;
		private var centerX:Number;
		private var centerY:Number;
		private var startAngle:Number = 0;
		private var startX:Number;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize():void {
			var myBitmapData:BitmapData = new Pen();
			var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
			stageWidth = stage.stageWidth;
			stageHeight = stage.stageHeight;
			centerX = stageWidth / 2;
			centerY = stageHeight / 2;
			center = createCenter(myTexture);
			stage.addEventListener(TouchEvent.TOUCH, onTouch);
		}
		private function onTouch(eventObject:TouchEvent):void {
			var myTouch:Touch = eventObject.getTouch(stage);
			if (!myTouch) return;
			switch (myTouch.phase) {
				case TouchPhase.BEGAN:
					startX = myTouch.globalX;
					break;
				case TouchPhase.MOVED:
					var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
					angle %= PI_2;
					center.rotation = angle;
					break;
				case TouchPhase.ENDED:
					startAngle += PI_2 * (myTouch.globalX - startX) / stageWidth;
					startAngle %= PI_2;
					trace(startAngle);  // 確認用
					break;
			}
		}
		private function createCenter(myTexture:Texture):Image {
			var instance:Image = createImage(myTexture);
			instance.x = centerX;
			instance.y = centerY;
			return instance;
		}
		private function createImage(myTexture:Texture):Image {
			var instance:Image = new Image(myTexture);
			instance.pivotX = instance.width / 2;
			instance.pivotY = instance.height / 2;
			addChild(instance);
			return instance;
		}
	}	
}

[*2] 逆に、Starlingフレームワークでは、単純なクリックを捉えるのは定義済みActionScript 3.0より手間がかかります。詳しくは、「Starlingフレームワークでインスタンスをクリックする」および「Starlingフレームワークでビットマップ上のクリックを検知する」をお読みください。

03 イベントリスナーでインスタンスを回す

さて、本稿のお題は「イベントリスナー」です。そこで、ドラッグしたとき、イベントリスナーでインスタンスを回してみましょう。すると、スクリプトはこんな組立てになります。

  1. 「回す」イベントにリスナーメソッドを登録する
  2. ドラッグしたら「回す」イベントを配信する
  3. リスナーメソッドがインスタンスを回す

イベントの配信は、EventDispatcher.dispatchEvent()メソッドで行います。引数には新たなイベントオブジェクトを渡します。基本の構文はつぎのとおりです。

オブジェクト.dispatchEvent(new Event(イベント名))

すると、スクリプトにはつぎのような手を加えることになります。イベント名に決めた文字列("rotate")は、クラスの定数(ROTATE)にしました。これで、ドラッグしたときリスナーメソッド(rotateAtCenter())が呼出されます。

private const ROTATE:String = "rotate";
// ...[中略]...
private function initialize():void {
	// ...[中略]...
	addEventListener(ROTATE, rotateAtCenter);
}
private function onTouch(eventObject:TouchEvent):void {
	var myTouch:Touch = eventObject.getTouch(stage);
	if (!myTouch) return;
	switch (myTouch.phase) {
		// ...[中略]...
		case TouchPhase.MOVED:
			// ...回転角を求める
			var rotateEvent:Event = new Event(ROTATE);
			dispatchEvent(rotateEvent);
			break;
		// ...[中略]...
	}
}
private function rotateAtCenter(eventObject:Event):void {
	// ...インスタンスを回す
}

ここで考えなければならないのは、ドラッグの動きから求めた回転角を、リスナーメソッドにどうやって伝えるかです。ルートクラスのプロパティに入れて、リスナーメソッドからその値を参照する手はあります。けれど、リスナーメソッドにはイベントオブジェクトが送られるのですから、その中に加える方がスマートでしょう。

リスナーが使う値をイベントオブジェクトに入れて送るというのは、イベントリスナーの仕組みではたびたび用いられるやり方です。ただ、定義済みActionScript 3.0のEventクラスには、自由な値が加えられません。そこで、Eventのサブクラスを定めて、新たなプロパティを加え、そこに値を入れるのです。このイベントリスナーの仕組みにとくに問題はないものの、値をひとつ送りたいだけなのにサブクラスを定めるというのは、煩わしく感じられます。

そこで、StarlingフレームワークのEventクラスには、自由な値を入れられるプロパティがひとつ加わりました。それは、Event.dataプロパティです。もっとも、リファレンスには読取り専用とされています。実は、Event()コンストラクタでインスタンスをつくるとき、第3引数に渡す値がEvent.dataプロパティの値になるのです。なお、第2引数は、イベントを表示オブジェクトの親に送る(バブリング)かどうかのブール(論理)値です(デフォルト値はfalse)。

new Event(イベント名, バブリング, dataの値)

では、このEvent()コンストラクタの第3引数を使って、回転角をリスナー関数に送りましょう。前掲スクリプト002に、つぎのような手を加えます。まず、初期化のメソッド(initialize())で、回転のイベント(ROTATE)にリスナーメソッド(rotateAtCenter())を加えます。

つぎに、DisplayObject.touchイベントのリスナーメソッド(onTouch())は、Touch.phaseプロパティの値がTouchPhase.MOVEDつまりドラッグしていたら、EventDispatcher.dispatchEvent()メソッドで回転のイベントを配信します。このとき送るEventオブジェクトには、Event()コンストラクタの第3引数で回転角(angle)を加えてあります。

そして、回転のリスナーメソッド(rotateAtCenter())は、受取ったEventオブジェクトのEvent.dataプロパティから回転角の値を取出して、インスタンスを回すのです。なお、Event.dataプロパティは任意の値を納めるため、データ型がObjectに定められています。したがって、他の型指定をした変数に納めるには、as演算子でデータ型を評価し直します。

private const ROTATE:String = "rotate";
// ...[中略]...
private function initialize():void {
	// ...[中略]...
	addEventListener(ROTATE, rotateAtCenter);
}
private function onTouch(eventObject:TouchEvent):void {
	var myTouch:Touch = eventObject.getTouch(stage);
	if (!myTouch) return;
	switch (myTouch.phase) {
		// ...[中略]...
		case TouchPhase.MOVED:
			var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
			angle %= PI_2;
			// center.rotation = angle;
			var rotateEvent:Event = new Event(ROTATE, false, angle);
			dispatchEvent(rotateEvent);
			break;
		// ...[中略]...
	}
}
private function rotateAtCenter(eventObject:Event):void {
	var angle:Number = eventObject.data as Number;
	center.rotation = angle;
}

これで、前掲スクリプト002と同じように、ステージを水平にドラッグする方向と幅に応じて、真ん中に置かれたインスタンスが回ります。ただし、ドラッグしたときに、イベントリスナーの仕組みを使って、回転のイベント(ROTATE)をリスナーメソッド(rotateAtCenter())に送っています。このように書替えたStarlingルートクラス(MySprite)は、つぎのスクリプト003のとおりです。

スクリプト003■ステージ上でドラッグしたときイベントリスナーによりインスタンスを回す

// ActionScript 3.0クラスファイル: MySprite.as
package  {
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.Event;
	import starling.events.Touch;
	import starling.events.TouchEvent;
	import starling.events.TouchPhase;
	import flash.display.BitmapData;
	public class MySprite extends Sprite {
		private const PI_2:Number = Math.PI * 2;
		private const ROTATE:String = "rotate";
		private var center:Image;
		private var stageWidth:int;
		private var stageHeight:int;
		private var centerX:Number;
		private var centerY:Number;
		private var startAngle:Number = 0;
		private var startX:Number;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize():void {
			var myBitmapData:BitmapData = new Pen();
			var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
			stageWidth = stage.stageWidth;
			stageHeight = stage.stageHeight;
			centerX = stageWidth / 2;
			centerY = stageHeight / 2;
			center = createCenter(myTexture);
			stage.addEventListener(TouchEvent.TOUCH, onTouch);
			addEventListener(ROTATE, rotateAtCenter);
		}
		private function onTouch(eventObject:TouchEvent):void {
			var myTouch:Touch = eventObject.getTouch(stage);
			if (!myTouch) return;
			switch (myTouch.phase) {
				case TouchPhase.BEGAN:
					startX = myTouch.globalX;
					break;
				case TouchPhase.MOVED:
					var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
					angle %= PI_2;
					var rotateEvent:Event = new Event(ROTATE, false, angle);
					dispatchEvent(rotateEvent);
					break;
				case TouchPhase.ENDED:
					startAngle += PI_2 * (myTouch.globalX - startX) / stageWidth;
					startAngle %= PI_2;
					trace(startAngle);
					break;
			}
		}
		private function createCenter(myTexture:Texture):Image {
			var instance:Image = createImage(myTexture);
			instance.x = centerX;
			instance.y = centerY;
			return instance;
		}
		private function rotateAtCenter(eventObject:Event):void {
			var angle:Number = eventObject.data as Number;
			center.rotation = angle;
		}
		private function createImage(myTexture:Texture):Image {
			var instance:Image = new Image(myTexture);
			instance.pivotX = instance.width / 2;
			instance.pivotY = instance.height / 2;
			addChild(instance);
			return instance;
		}
	}	
}

04 イベントオブジェクトを使い回して配信するEventDispatcher.dispatchEventWith()メソッド

前掲スクリプト003は、ステージ上をドラッグしている間中、回転のイベントが配信されます。そして、そのたびに新たなEventオブジェクトがつくられることになります。前回の第4回「StarlingフレームワークのTweenクラスにおける最適化とJugglerクラスの実装」では、02「Tweenオブジェクトを使い回す」で「古いインスタンスを使い回した方がエコ、つまりお得になる」と説明しました。

すでにつくったEventオブジェクトはとっておいて、プロパティだけ書直せるとよさそうです。ところが、Eventクラスのプロパティは多くが読取り専用です[*3]。そこで、Starlingフレームワークには新たなイベント配信のメソッドEventDispatcher.dispatchEventWith()が備わりました。役目はEventDispatcher.dispatchEvent()と同じく、リスナーへのイベントの配信です。けれど、予めイベントオブジェクトをつくらなくてよいのです。引数は3つで、Event()コンストラクタと同じです。

EventDispatcher.dispatchEventWith()メソッドは、Event()コンストラクタと同じ3つの引数を使って、まだ使い回せるEventオブジェクトがなければコンストラクタで新たなインスタンスをつくって送ります。そして、使ったオブジェクトは、クラスに蓄えられます。すると、つぎにEventDispatcher.dispatchEventWith()メソッドを呼出したとき、すでにあるオブジェクトを取出し、コンストラクタでつくるときと同じように設定を書直して配信するのです。

オブジェクト.dispatchEventWith(イベント名, バブリング, dataの値)

前掲スクリプト003をこのEventDispatcher.dispatchEventWith()メソッドで書替えるのは、つぎのようにとても簡単です。Event()コンストラクタとEventDispatcher.dispatchEvent()メソッドがひとつになったと考えればよいからです。これだけで、この頃のエコ家電のように無駄は省かれ、Eventオブジェクトが自動的に使い回されます。

private function onTouch(eventObject:TouchEvent):void {
	var myTouch:Touch = eventObject.getTouch(stage);

	switch (myTouch.phase) {

		case TouchPhase.MOVED:
			var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
			angle %= PI_2;
			// var rotateEvent:Event = new Event(ROTATE, false, angle);
			// dispatchEvent(rotateEvent);
			dispatchEventWith(ROTATE, false, angle);
			break;
		case TouchPhase.ENDED:

	}
}

もっとも、インスタンスをひとつ回すだけなら、前のスクリプト002のように、プロパティ値を直に変えた方が手っ取り早いです。イベントリスナーは、イベントを受取るリスナーがいくつもあるときに役立ちます。そこで、インスタンスをもうひとつ加え、新たなリスナーメソッドで別の回し方をしてみましょう。

小さめのインスタンス(sub)を新たなメソッド(createSub())でつくり、ステージ真ん中上の方に置きます(図003)。TextureオブジェクトからImageインスタンスをつくって表示リストに加える処理は、すでに定めてあったメソッド(createImage())を使い回しました。

private var sub:Image;
// ...[中略]...
private function initialize():void {
		var myBitmapData:BitmapData = new Pen();
		var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
		// ...[中略]...
		sub = createSub(myTexture);
		// ...[中略]...
}
// ...[中略]...
private function createSub(myTexture:Texture):Image {
		var instance:Image = createImage(myTexture);
		instance.scaleX = instance.scaleY = 0.5;
		instance.x = centerX;
		instance.y = instance.height / 2;
		return instance;
}
private function createImage(myTexture:Texture):Image {
		var instance:Image = new Image(myTexture);
		instance.pivotX = instance.width / 2;
		instance.pivotY = instance.height / 2;
		addChild(instance);
		return instance;
}

図003■小さいインスタンスを中央上部に加えた
図003

新たに加えた小さいインスタンス(sub)も、回転(ROTATE)のイベントにリスナーメソッド(rotateAround())を定めて加えます。このインスタンスは、ステージの中心を軸にして、時計回りに位置を回転します。中心の座標を(x, y)、回転する円の半径をrとすると、角θ回転した位置(x', y')は、つぎの式で表されます。

x' = x + r cosθ
y' = y + r sinθ

小さいインスタンスのリスナーメソッド(rotateAround())は、この式にもとづいてインスタンスの位置を定めます。なお、回転の起点が時計の12時の方向なので、回転角は-π/2ラジアン(-90°)から始めます。


private const PI_HALF:Number = Math.PI / 2;
private var radius:Number;
// ...[中略]...
private function createSub(myTexture:Texture):Image {
	var instance:Image = createImage(myTexture);
	// ...[中略]...
	radius = (stageHeight - instance.height) / 2;
	addEventListener(ROTATE, rotateAround);
	return instance;
}
// ...[中略]...
private function rotateAround(eventObject:Event):void {
	var angle:Number = eventObject.data as Number;
	var radians:Number = angle - PI_HALF;
	sub.x = centerX + radius * Math.cos(radians);
	sub.y = centerY + radius * Math.sin(radians);
}

前掲スクリプト003のStarlingルートクラス(MySprite)にこれまでご説明した処理を加えると、以下のスクリプト004のようになります。ステージ上でドラッグすると、ふたつのインスタンスが異なるリスナーメソッドによりそれぞれステージ中央を軸に回ります。ふたつ目に加えた小さなインスタンスは、中心座標から一定の半径(radius)で位置を変えますので、ひとつ目のインスタンスの衛星のような動きになります(図004)。

図004■水平ドラッグすると真ん中のインスタンスが自転して小さいインスタンスは周囲を回る
図004

スクリプト004■ステージ上でドラッグしたときふたつのイベントリスナーでそれぞれのインスタンスを回す


// ActionScript 3.0クラスファイル: MySprite.as
package  {
	import starling.display.Sprite;
	import starling.display.Image;
	import starling.textures.Texture;
	import starling.events.Event;
	import starling.events.Touch;
	import starling.events.TouchEvent;
	import starling.events.TouchPhase;
	import flash.display.BitmapData;
	public class MySprite extends Sprite {
		private const ROTATE:String = "rotate";
		private const PI_2:Number = Math.PI * 2;
		private const PI_HALF:Number = Math.PI / 2;
		private var center:Image;
		private var sub:Image;
		private var stageWidth:int;
		private var stageHeight:int;
		private var centerX:Number;
		private var centerY:Number;
		private var startAngle:Number = 0;
		private var startX:Number;
		private var radius:Number;
		public function MySprite() {
			addEventListener(Event.ADDED_TO_STAGE, initialize);
		}
		private function initialize():void {
			var myBitmapData:BitmapData = new Pen();
			var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
			stageWidth = stage.stageWidth;
			stageHeight = stage.stageHeight;
			centerX = stageWidth / 2;
			centerY = stageHeight / 2;
			center = createCenter(myTexture);
			sub = createSub(myTexture);
			stage.addEventListener(TouchEvent.TOUCH, onTouch);
		}
		private function onTouch(eventObject:TouchEvent):void {
			var myTouch:Touch = eventObject.getTouch(stage);
			if (!myTouch) return;
			switch (myTouch.phase) {
				case TouchPhase.BEGAN:
					startX = myTouch.globalX;
					break;
				case TouchPhase.MOVED:
					var angle:Number = startAngle + PI_2 * (myTouch.globalX - startX) / stageWidth;
					angle %= PI_2;
					dispatchEventWith(ROTATE, false, angle);
					break;
				case TouchPhase.ENDED:
					startAngle += PI_2 * (myTouch.globalX - startX) / stageWidth;
					startAngle %= PI_2;
					trace(startAngle);
					break;
			}
		}
		private function createCenter(myTexture:Texture):Image {
			var instance:Image = createImage(myTexture);
			instance.x = centerX;
			instance.y = centerY;
			addEventListener(ROTATE, rotateAtCenter);
			return instance;
		}
		private function rotateAtCenter(eventObject:Event):void {
			var angle:Number = eventObject.data as Number;
			center.rotation = angle;
		}
		private function createSub(myTexture:Texture):Image {
			var instance:Image = createImage(myTexture);
			instance.scaleX = instance.scaleY = 0.5;
			instance.x = centerX;
			instance.y = instance.height / 2;
			radius = (stageHeight - instance.height) / 2;
			addEventListener(ROTATE, rotateAround);
			return instance;
		}
		private function rotateAround(eventObject:Event):void {
			var angle:Number = eventObject.data as Number;
			var radians:Number = angle - PI_HALF;
			sub.x = centerX + radius * Math.cos(radians);
			sub.y = centerY + radius * Math.sin(radians);
		}
		private function createImage(myTexture:Texture):Image {
			var instance:Image = new Image(myTexture);
			instance.pivotX = instance.width / 2;
			instance.pivotY = instance.height / 2;
			addChild(instance);
			return instance;
		}
	}	
}

このように、あるイベントに対して複数のインスタンスに異なる振舞いをさせたいとき、イベントリスナーの仕組みが役立ちます。もっとも、前掲スクリプト004の場合なら、わざわざイベントリスナーに加えなくても、ドラッグしているときにそれぞれのメソッドを直接呼出せば済みます。

イベントリスナーのよいところは、イベントを配信する側がリスナーの中身を知らなくてよいことです。今回のお題でいえば、「回転」をひたすら伝えるだけで、それぞれのインスタンス(centerとsub)がどう「回転」するかは気にとめません。そういうことでいえば、回転するインスタンスにはそれぞれクラスが定められていて、クラスによって異なるメソッドをリスナーとして加えるというのが本領を発揮する場面です。

前掲スクリプト004は、イベントの流れをひと目で見て、イベントリスナーの仕組みがわかりやすいように、あえてルートクラスにすべての処理を書きました。ただ、処理結果はとくに変わらないものの、前掲スクリプト003から書替えて、イベントリスナーはインスタンスをつくるメソッド(createCenter()とcreateSub())から登録するようにしました(インスタンスにクラスを定めるときは、このかたちになることが多いでしょう)。

private function initialize():void {
	var myBitmapData:BitmapData = new Pen();
	var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
	// ...[中略]...
	center = createCenter(myTexture);
	sub = createSub(myTexture);
	stage.addEventListener(TouchEvent.TOUCH, onTouch);
	// addEventListener(ROTATE, rotateAtCenter);
}
// ...[中略]...
private function createCenter(myTexture:Texture):Image {
	var instance:Image = createImage(myTexture);
	// ...[中略]...
	addEventListener(ROTATE, rotateAtCenter);
	return instance;
}
// ...[中略]...
private function createSub(myTexture:Texture):Image {
	var instance:Image = createImage(myTexture);
	// ...[中略]...
	addEventListener(ROTATE, rotateAround);
	return instance;
}

[*3] さらに、定義済みActionScript 3.0では、EventDispatcher.dispatchEvent()メソッドで配信するのは、新たなイベントオブジェクトでなければなりません。使い回しのイベントオブジェクトを引数に渡すと、自動的にその複製がつくられてしまう(内部的にEvent.clone()メソッドが呼出される)のです。

05 イベントリスナーの受取る第2引数

Starlingフレームワークでは、イベントリスナーは第2引数を受取ることもできます。EventDispatcher.dispatchEvent()EventDispatcher.dispatchEventWith()メソッドでイベントを送ったときは、Event.dataプロパティの値が第2引数になります。したがって、前掲スクリプト004のふたつのリスナーメソッド(rotateAtCenter(()とrotateAround())は、つぎのように書くこともできます。

// private function rotateAtCenter(eventObject:Event):void {
private function rotateAtCenter(eventObject:Event, angle:Number):void {
	// var angle:Number = eventObject.data as Number;
	center.rotation = angle;
}

// private function rotateAround(eventObject:Event):void {
private function rotateAround(eventObject:Event, angle:Number):void {
	// var angle:Number = eventObject.data as Number;
	var radians:Number = angle - PI_HALF;
	sub.x = centerX + radius * Math.cos(radians);
	sub.y = centerY + radius * Math.sin(radians);
}

定義済みのイベントについても、役に立つ第2引数を受取れるものがあります。たとえば、Event.ENTER_FRAMEでは経過秒数[*4]KeyboardEvent.KEY_DOWNなら押したキーのコードが第2引数として渡されます。

addEventListener(Event.ENTER_FRAME, onEnterFrame);
function onEnterFrame(event:Event, passedTime:Number):void {
	// 第2引数のpassedTimeで経過時間がわかる
}
addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
function onKeyDown(event:Event, keyCode:uint):void {
	// 第2引数のkeyCodeでキーコードがわかる
}

[*4] 内部的には、第4回「StarlingフレームワークのTweenクラスにおける最適化とJugglerクラスの実装」04「Starlingオブジェクトに備わるJugglerオブジェクト」でご説明したとおり、StarlingオブジェクトがFlash PlayerからEvent.ENTER_FRAMEイベントを受取って、Starling.advanceTime()メソッドを呼出し、そこからさらにStage.advanceTime()メソッドが呼出されます。

すると、Stage.advanceTime()メソッドは、つぎのようにDisplayObjectContainer.broadcastEvent()メソッドにより、Event.ENTER_FRAMEイベントを表示リストの子に向けて配信します。このとき、メソッドが引数に受取った秒数を、イベントオブジェクトのEvent.dataプロパティに加えているのです。なお、reset()メソッドはEventクラスに内部的に定められていて、すでにあるEventオブジェクトを初期化し、第3引数はEvent.dataプロパティの値になります(「StarlingフレームワークのEventDispatcher.dispatchEventWith()メソッドと新しいイベント配信」の「実装」参照)

private var mEnterFrameEvent:EnterFrameEvent = new EnterFrameEvent(Event.ENTER_FRAME, 0.0);

public function advanceTime(passedTime:Number):void {
	mEnterFrameEvent.reset(Event.ENTER_FRAME, false, passedTime);
	broadcastEvent(mEnterFrameEvent);
}

 

コメント

この記事にコメントを書く

記事に対するテクニカルな質問はご遠慮ください(利用規約)。

その他の記事