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

第63回Starlingフレームワークでインスタンスのクリックをどう扱うか

最終回の今回は、Starlingフレームワークでインスタンスのクリックをどう扱うか解説したい。インスタンスのクリックは、もっとも基本的なマウスインタラクションだ。ところが、この扱いというのが、定義済みActionScript 3.0と勝手が違うだけでなく、思いのほかやっかいだったりする。

マウスボタンを放す操作はTouchPhase.ENDED

前回の第62回Starlingフレームワークで動的に置いたビットマップをドラッグするでは、インスタンスのドラッグについて学んだ。このときのスクリプト3「Starlingフレームワークでインスタンスをドラッグする」図1を、クリックのサンプルに書替えてみよう。

図1 Starlingフレームワークでインスタンスをドラッグするルートクラスの定義
図1Starlingフレームワークでインスタンスをドラッグするルートクラスの定義

まず、マウスボタンを放す操作のTouch.phaseプロパティ値は、定数TouchPhase.ENDEDで定められる(再掲第62回表2⁠。すると、TouchEvent.getTouch()メソッドの第2引数にこの定数TouchPhase.ENDEDを渡せば、マウスボタンを放す操作が捉えられる(前掲図1参照⁠⁠。

第62回表2 TouchPhaseクラスの定数とタッチスクリーンおよびマウスの操作(再掲)
TouchPhaseクラスの定数操作
タッチスクリーンマウス
BEGAN画面に触れるマウスボタンを押す
ENDED画面から指を離すマウスボタンを放す
HOVERマウスポインタを重ねる
MOVED画面に触れた指を動かすボタンは押したままマウスを動かす
STATIONARY画面に触れたまま動かさないボタンを押したままマウスは動かさない

インスタンスをクリックしたら、一定の角度回すことにしよう。すると、第62回スクリプト3のDisplayObject.touchイベント(定数TouchEvent.TOUCHのリスナーメソッド(mouseOperated())につぎのような手を加えればよさそうだ。

private var angle:Number = 15 * Math.PI / 180;

private function mouseOperated(eventObject:TouchEvent):void {
  // var myTouch:Touch = eventObject.getTouch(instance, TouchPhase.MOVED);
  var myTouch:Touch = eventObject.getTouch(instance, TouchPhase.ENDED);
  if (myTouch) {
    /*
    var nMoveX:Number = myTouch.globalX - myTouch.previousGlobalX;
    var nMoveY:Number = myTouch.globalY - myTouch.previousGlobalY;
    instance.x += nMoveX;
    instance.y += nMoveY;
    */
    instance.rotation += angle;
  }
}

試してみると、確かにインスタンスをクリックすれば回転する。ただし、TouchPhase.ENDEDというのは、マウスボタンを放すのがインスタンス上でなくともよい。つまり、外でマウスボタンを放してもインスタンスは回ってしまうのだ図2⁠。しかし、インスタンスの外でマウスボタンを放したのはクリックではない。

図2 インスタンスの外でマウスボタンを放してもTouchPhase.ENDEDとみなされる
図2 インスタンスの外でマウスボタンを放してもTouchPhase.ENDEDとみなされる 図2 インスタンスの外でマウスボタンを放してもTouchPhase.ENDEDとみなされる

マウスボタンがインスタンス上で放されたのかを調べる

マウスボタンをどこで放しても、TouchPhase.ENDEDが拾ってしまう。そのため、マウスボタンを放したときのポインタの座標が、インスタンス上かどうかは改めて確かめなければならない。このとき用いるのが、DisplayObject.hitTest()メソッドだ。

DisplayObjectオブジェクト.hitTest(Pointオブジェクト, マウス操作対象に限定)

第1引数には、参照するDisplayObjectオブジェクトから見た座標を、Pointオブジェクトで定める。第2引数にtrueを渡すと、マウス(またはタッチ)操作できる表示されたインスタンスだけを対象にして、座標の重なりが調べられる(デフォルト値はfalse⁠。戻り値は、座標と重なるDisplayObjectインスタンスだ。また、重なりはインスタンスの矩形領域(Rectangleオブジェクト)の内側かどうかで決まる。座標が重ならないときはnullが返される。

そこで、つぎのスクリプト1は、DisplayObject.touchイベントのリスナーメソッド(mouseOperated())で、マウスボタンを放したときのポインタの座標が、インスタンス上にあるかどうか調べるようにした。TouchPhase.ENDEDのTouchオブジェクトがTouchEvent.getTouch()メソッドで得られたら、さらにDisplayObject.hitTest()メソッドでマウスポインタの座標がインスタンス上にあるのを確かめたうえで、インスタンスを回している。Touch.getLocation()メソッドは、引数に渡したインスタンスから見たマウスポインタの座標を返す。

スクリプト1 Starlingフレームワークでインスタンスのクリックを扱う
// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
  import flash.display.BitmapData;
  import starling.display.Sprite;
  import starling.display.Image;
  import starling.textures.Texture;
  import starling.events.Event;
  import starling.events.TouchEvent;
  import starling.events.Touch;
  import starling.events.TouchPhase;
  import flash.geom.Point;
  public class MySprite extends Sprite{
    private var instance:Image;
    private var angle:Number = 15 * Math.PI / 180;
    public function MySprite() {
      addEventListener(Event.ADDED_TO_STAGE, initialize);
    }
    private function initialize(eventObject:Event):void {
      var myBitmapData:BitmapData = new Pen();
      var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
      instance = new Image(myTexture);
      addChild(instance);
      instance.addEventListener(TouchEvent.TOUCH, mouseOperated);
      instance.x = stage.stageWidth / 2;
      instance.y = stage.stageHeight / 2;
      instance.pivotX = instance.width / 2;
      instance.pivotY = instance.height / 2;
    }
    private function mouseOperated(eventObject:TouchEvent):void {
      var myTouch:Touch = eventObject.getTouch(instance, TouchPhase.ENDED);
      if (myTouch) {
        var mousePoint:Point = myTouch.getLocation(instance);
        if (instance.hitTest(mousePoint, true)) {
          instance.rotation += angle;
        }
      }
    }
  }
}

これで、マウスボタンをインスタンス上で押し、かつその上で放したとき、つまりクリックしたときだけインスタンスの角度が変わる。インスタンスが矩形であればこれでよい。しかし、前述のとおり、インスタンスのかたちにかかわらず、DisplayObject.hitTest()メソッドは矩形領域で座標との重なりを評価する。矩形でないインスタンスをどう扱えばよいか、次項で考えてみよう。

矩形でないビットマップのクリックを捉える

Starlingフレームワークでは、インスタンスと座標の重なりは、矩形領域で評価される。定義済みActionScript 3.0と異なり、矩形でないかたちについてピクセル単位で調べることはできない。だが、望みはある。定義済みActionScript 3.0が使えればよいのだ。ビットマップなら、まさにその手が使える。そこで、[ライブラリ]にアルファで抜いたビットマップを用意して、クラスを定めよう図3⁠。

図3 [ライブラリ]の矩形でないビットマップにクラスを定める
図3 [ライブラリ]の矩形でないビットマップにクラスを定める

もちろん、前掲スクリプト1をそのまま使うと、矩形領域内であればアルファで抜かれたピクセル上もクリックを捉えてしまう図4⁠。

図4 矩形領域内はアルファで抜かれていてもクリックを捉える
図4 矩形領域内はアルファで抜かれていてもクリックを捉える

しかし、BitmapDataクラスにはBitmapData.hitTest()メソッドがあり、アルファを識別した座標の重なりが確かめられる。このメソッドにはいくつかの使い方がある[1]⁠。今回は、つぎの構文で用いる。

BitmapDataオブジェクト.hitTest(左上角Pointオブジェクト, アルファしきい値, 対象Pointオブジェクト)

第1引数はインスタンスの左上角座標を、Pointオブジェクトで渡す。BitmapDataインスタンスはタイムラインに直には置けないため、xy座標をプロパティとしてもたないからだ。第2引数は、インスタンスのピクセルの有無を切り分けるアルファの値(しきい値)で、256階調(0~255)の数値によって定める。第3引数が、重なりを調べる座標のPointオブジェクトだ。

前掲スクリプト1に手を加えて、つぎのようにBitmapData.hitTest()メソッドを呼出してみよう。メソッドの第3引数には、インスタンスから見たマウスポインタの座標をPointオブジェクト(mousePoint)で渡した。すると、第1引数の左上角は原点(0, 0)(zeroPoint)でよい。第2引数のアルファは、完全に抜けたピクセルの値として0x0(0)を渡している。なお、BitmapDataインスタンスはリスナーメソッドからも参照するため、ローカル変数でなく、プロパティ(myBitmapData)としてvar宣言した。

private var myBitmapData:BitmapData;
private var zeroPoint:Point = new Point(0, 0);
private function initialize(eventObject:Event):void {
  // var myBitmapData:BitmapData = new Pen();
  myBitmapData = new Pen();
  // ...[中略]...
}
private function mouseOperated(eventObject:TouchEvent):void {
  var myTouch:Touch = eventObject.getTouch(instance, TouchPhase.ENDED);
  if (myTouch) {
    var mousePoint:Point = myTouch.getLocation(instance);
    // if (instance.hitTest(mousePoint, true)) {
    if (myBitmapData.hitTest(zeroPoint, 0x0, mousePoint)) {
      instance.rotation += angle;
    }
  }
}

これで、アルファの抜けた矩形領域内でマウスボタンを放しても、クリックとは捉えられないようになる。ただし、アルファの抜けた矩形領域でマウスボタンを押して、ビットマップイメージ上で放すとクリックになってしまう図5⁠。これは矩形領域内でマウスボタンを押す操作が、DisplayObject.touchイベントTouch.phaseプロパティはTouchPhase.BEGANを起こすからだ。

図5 矩形領域でマウスボタンを押してビットマップイメージ上で放すとクリックになる
図5 矩形領域でマウスボタンを押してビットマップイメージ上で放すとクリックになる

つまり、マウスボタンを押したときTouch.phaseプロパティがTouchPhase.BEGANも、アルファを識別してポインタ座標がビットマップ上かどうかを確かめなければならない。つぎのスクリプト2は、TouchEvent.getTouch()メソッドの第2引数なしにTouchオブジェクトを取出し[2]⁠、switchステートメントでTouch.phaseプロパティがTouchPhase.BEGANTouchPhase.ENDEDの処理をそれぞれ定めた。

スクリプト2 ビットマップのアルファも識別してクリックを捉える
// ActionScript 3.0クラス定義ファイル: MySprite.as
package {
  import flash.display.BitmapData;
  import starling.display.Sprite;
  import starling.display.Image;
  import starling.textures.Texture;
  import starling.events.Event;
  import starling.events.TouchEvent;
  import starling.events.Touch;
  import starling.events.TouchPhase;
  import flash.geom.Point;
  public class MySprite extends Sprite{
    private var instance:Image;
    private var angle:Number = 15 * Math.PI / 180;
    private var myBitmapData:BitmapData;
    private var zeroPoint:Point = new Point(0, 0);
    private var pressed:Boolean = false;
    public function MySprite() {
      addEventListener(Event.ADDED_TO_STAGE, initialize);
    }
    private function initialize(eventObject:Event):void {
      myBitmapData = new Pen();
      var myTexture:Texture = Texture.fromBitmapData(myBitmapData);
      instance = new Image(myTexture);
      addChild(instance);
      instance.addEventListener(TouchEvent.TOUCH, mouseOperated);
      instance.x = stage.stageWidth / 2;
      instance.y = stage.stageHeight / 2;
      instance.pivotX = instance.width / 2;
      instance.pivotY = instance.height / 2;
    }
    private function mouseOperated(eventObject:TouchEvent):void {
      var myTouch:Touch = eventObject.getTouch(instance);
      if (myTouch) {
        switch (myTouch.phase) {
          case TouchPhase.BEGAN:
            if (instanceIsTouched(myTouch)) {
              pressed = true;
            }
            break;
          case TouchPhase.ENDED:
            if (pressed && instanceIsTouched(myTouch)) {
              instance.rotation += angle;
            }
            pressed = false;
            break;
        }
      }
    }
    private function instanceIsTouched(myTouch:Touch):Boolean {
      var mousePoint:Point = myTouch.getLocation(instance);
      var touched:Boolean = myBitmapData.hitTest(zeroPoint, 0x0, mousePoint);
      return touched;
    }
  }
}

座標がアルファを識別したビットマップイメージ上かどうかは、別に定めた関数(instanceIsTouched())で調べることにした。DisplayObject.touchイベントのリスナーメソッド(mouseOperated())は、インスタンスの矩形領域でマウスボタンを押したときTouch.phaseプロパティがTouchPhase.BEGAN⁠、ポインタの座標がビットマップ上ならフラグのプロパティ(pressed)trueにする。そして、マウスボタンを放したときTouch.phaseプロパティはTouchPhase.ENDED⁠、このプロパティ値がtrueかつマウスポインタの座標がビットマップ上であることを確かめたうえで、クリックとして扱っている。

これで、ビットマップイメージのアルファも識別したマウスクリックが捉えられる。さて、少しばかり手間がかかってしまった。Starlingにかぎらず、フレームワークを使うときには、こうした得意・不得意や特徴を理解しておくことが大切だ。

最後に

Flash Player 9から始まった5年を超える本連載は今回で閉じことになる。5年間でFlashの技術も取り巻く状況も、大きく変わった。また、今なお変わり続けている。Flashを始めとするWebの技術については、筆者のサイトFumioNonaka.comで引続き発信していく。折に触れて、ご覧いただければ幸いだ。最後に改めて、長い間のご愛読に感謝したい。

おすすめ記事

記事・ニュース一覧