cocos2d-x 3.3 のタッチイベントの流れを追う

cocos2d-x にてタッチイベントを押されたまさにその瞬間が欲しかったので、
まずは下調べとして実際にタッチイベントが `cocos2d::EventDispatcher` に到着するまでの流れを追ってみました。

#プラットフォーム毎の処理
##Android 特有の処理
まずソースコード上、一番最初にタッチイベントを受け取るメソッドは java 側の
`org.cocos2dx.lib.Cocos2dxGLSurfaceView.onTouchEvent` です。

```java:Cocos2dxGLSurfaceView.java
@Override
public boolean onTouchEvent(final MotionEvent pMotionEvent) {
    // ...
}
```

ここで一旦タッチ識別番号をごにょごにょしたりしていますが、座標に関してはまだ何もいじりません。
この辺りですぐに JNI を叩いて native 側にタッチを通知する処理を書くとタッチした瞬間を瞬時に伝えられそうです。

この `onTouchEvent` では一旦 GLThread に載せ替え、
`org.cocos2dx.lib.Cocos2dxRenderer` の handle 系のメソッドを経由して、
nativeTouches 系のメソッドを呼び出しています。

それぞれのイベントで呼ばれる `Cocos2dxRenderer` のメソッドは以下のようになっています。

|イベントの種類    |呼ばれる `Cocos2dxRenderer` のメソッド|
|:----------------|:-----------------------------------|
|タッチの開始      |`nativeTouchesBegin`                |
|タッチの終了      |`nativeTouchesEnd`                  |
|タッチの移動      |`nativeTouchesMove`                 |
|タッチのキャンセル|`nativeTouchesCancel`                |

**注意: `nativeTouchesBegin` と `nativeTouchesEnd` すべての引数が `int` や `float` になっているのに対し、
`nativeTouchesMove` および `nativeTouchesCancel` はすべての引数が配列になっています**

JNI で呼ばれるこれらのメソッドの実装は、`cocos2d/cocos/platform/android/jni/TouchesJNI.cpp` にあります。
ここで、他のプラットフォーム同様 `cocos2d::Director` を通じて `cocos2d::GLVeiw` のインスタンスを取得し、対応する handleTouches 系メソッドを呼び出しています。
ここから先は、他のプラットフォームと共通の処理になっていきます。

##iOS 特有の処理
まずソースコード上、一番最初にタッチイベントを受け取るのは Objective-C 上の `[CCEAGLView touches****:withEvent:]` です。

|イベントの種類    |呼ばれる `CCEAGLView` のメソッド|
|:----------------|:-----------------------------|
|タッチの開始      |`touchesBegan`                |
|タッチの終了      |`touchesEnded`                |
|タッチの移動      |`touchesMoved`                |
|タッチのキャンセル|`touchesCancelled`             |

`CCEAGLView` の実装は `cocos2d/cocos/platform/ios/CCEAGLView-ios.mm` にあります。
これらのメソッドは一度 Objective-C のオブジェクトを C++ 用に載せ替え、他のプラットフォーム同様 `cocos2d::Director` を通じて `cocos2d::GLVeiw` のインスタンスを取得し、対応する handleTouches 系メソッドを呼び出しています。
ここから先は、他のプラットフォームと共通の処理になっていきます。

#各プラットフォーム共通の処理
##各プラットフォーム依存のコードからの受け口
各プラットフォームに依存する処理から呼び出された cocos2d::GLView::handleTouches 系メソッドの実装は、`cocos2d/cocos/platform/CCGLView.cpp` にあります。
だいたいの処理は以下のようになります。

```cpp
void GLView::handleTouches****(int num, intptr_t ids[], float xs[], float ys[])
{
    intptr_t id;
    float x = 0.0f;
    float y = 0.0f;
    EventTouch touchEvent;
    
    // 1 点ごとに処理
    for (int i = 0; i < num; i++)
    {
        id = ids[i];
        x = xs[i];
        y = ys[i];
        
        // 各タッチイベントに合わせた処理
    }
    
    // 最終的に送出されるタッチイベントに
    // 点 が 1 つも格納されなければ dispatch しない。
    if (touchEvent._touches.size() == 0) {
        CCLOG("touches****: size = 0");
        return;
    }
    
    // イベントを送出する
    touchEvent._eventCode = EventTouch::EventCode::****;
    auto dispatcher = Director::getInstance()->getEventDispatcher();
    dispatcher->dispatchEvent(&touchEvent);
}
```

すべてのイベント処理にて、タッチ座標系から DesignResolution 座標系への変換方法として使われている処理は以下のようになります。

```cpp
// 実際のコードでは GLView 内で行われる処理なので、直接メンバ変数にアクセスしています
GLView *view = cocos2d::Director::getInstance()->getOpenGLView();
const Rect &viewPortRect = view->getViewPortRect;

Vec2 locationInView {
    (x - viewPortRect.origin.x) / view->getScaleX(),
    (y - viewPortRect.origin.y) / view->getScaleY()
};
```

これにて皆さんご存知の `EventDispatcher` までイベントが届き、
やっとタッチイベントがお届けされました。やったね。

それでは「各タッチイベントに合わせた処理」に注目してみましょう。

##handleTouchesBegin
`handleTouchBegin` では、以下のような処理が渡された点の数だけ繰り返されます。

1. `g_touchIdReorderMap` を用いて native のタッチ識別子からすでに登録されている点で無いことを確認する
2. 未使用のインデックス値を取ってくる。
3. `Touch` インスタンスを生成して `g_touches` と `touchEvent` に詰め込む
4. `g_touchIdReorderMap` にインデックス値と native のタッチ識別子の関連付けを追加する

##handleTouchesMove
`handleTouchesMove` では、以下のような処理が渡された点の数だけ繰り返されます。

1. `g_touchIdReorderMap` を用いて native のタッチ識別子からインデックス値を得る
2. 対応するインデックス値が存在しない場合は、この点の処理を飛ばす
3. 取得したインデックス値を元に `g_touches` から対応する `Touch` インスタンスを取得する
4. 取得した `Touch` インスタンスに座標を追加し、`touchEvent` に詰め込む

##handleTouchesEnd および handleTouchesCancel
`handleTouchesEnd` と `handleTouchesCancel` は、
`handleTouchesEndOrCancel` というメソッドを呼び出す処理だけをしています。
それぞれの呼び出しの違いは `touchEvent` に設定されるイベントの違いのみで、それ以外は同じです。

`handleTouchesEndOrCancel` では、以下のような処理が渡された点の数だけ繰り返されます。

1. `g_touchIdReorderMap` を用いて native のタッチ識別子からインデックス値を得る
2. インデックス値を用いて `g_touches` から対応する `Touch` インスタンスを取得する
3. 取得した `Touch` インスタンスに座標を追加し、`touchEvent` に詰め込む
4. `g_touchIdReorderMap` から対応関係を削除する

なお、dispatchEvent をした後に `touchEvent` に追加されていた `Touch` インスタンスの `release` が呼び出されます。

#終わりに
これにて、やっとタッチイベントの流れが追えました。
各プラットフォームのタッチハンドラでうまく振舞えば、
フレーム単位ではなくまさに各プラットフォームでタッチを検出した瞬間を cocos2d-x 側で補足することができるかもしれませんね。
他のプラットフォームについては、手を出す必要が出てきたときに調べたいと思います。

それでは良いタッチイベントライフを。