物理エンジンp2を使ったジャンプゲームの公式サンプル"p2-jump-game"を読む (2) ゲームの基本部分

[物理エンジンp2を使ったジャンプゲームの公式サンプル"p2-jump-game"を読む (1) 起動処理](https://qiita.com/motoyasu-yamada/items/ba1727229438d9affcf5)の続きです。
# デモ

[Egret Developer Center(白鹭 开发者 中心) > Example(示例) > Demo(功能演示) > 2D](http://developer.egret.com/cn/list/example/id/13)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/302078/3038a443-7ab1-7244-3dd6-a068257975ea.png)


![image.png](https://qiita-image-store.s3.amazonaws.com/0/302078/058b6e1a-1f77-0e8c-8253-94f403e188dc.png)

# ゲーム本体はどこで実装されているか？

```ts:Bootstrap.ts
    class Bootstrap {
    :
        private createGameScene():void {
            GameCore.$i.launch(this);
        }
    :
```

```ts:GameCore.ts
        public static get $i():GameCore{
            if( this._i == null ){
                this._i = new GameCore;
            }
            return this._i;
        }

        private static _i:GameCore;

        constructor(){
            super();
:
        }
```

ゲームの状態を管理する`GameCore`クラスは`static`ではなくシングルトンを使って上のように管理しているようです。ブラウザーベースのTypeScriptの場合って`static`使わないでシングルトンのほうがいいんですかね？いつか調べてみてQiitaします。

# ゲームの起動

```ts
   export class GameCore extends egret.Sprite {
       :
        public launch( container:egret.DisplayObjectContainer ):void{
            container.addChild( this );
:
            this.createWorldSystem( );
:
            this.stage.frameRate = 20;
            this.addEventListener( egret.Event.ENTER_FRAME, this.run, this );
        }
   }
```

ゲームを管理する`GameCore`クラスは`egret.Sprite`クラスを継承して作られています。`egret.Sprite`クラスを継承しなくてもいい気がします。メリットは、プレイヤーや地面などのゲーム内オブジェクトを、すべてこのクラスの下に一律ぶらさげて隔離するぐらいでしょか。

`container.addChild( this );`は自分を画面`container`の下に追加しています。

次に`createWorldSystem`でプレイヤーや壁などのオブジェクトを生成しています。

`frameRate`は`index.html`で設定するだけではなくプログラムからも指定できるようです。

そして`this.addEventListener( egret.Event.ENTER_FRAME`でゲームループが開始されます


# ゲーム内オブジェクトの生成
``` ts
        private createWorldSystem( ):void{
            :
            city.phys.P2Space.initSpace( 1, new egret.Rectangle( 0, 0, this.stage.stageWidth, this.stage.stageHeight ) );

            this._pw = new p2.World();
            this._pw.defaultContactMaterial.friction = 0.5;
            this._pw.setGlobalStiffness( 1000 );
            this._pw.gravity[1] = this.GRAVITY;
:


            this._vcGroundsFixed = [
                this.createGround( this._pw, this, 1, 0, 640, 50, "rects.rect-" + "9", 0, 750, "地面" )
                , this.createGround( this._pw, this, 2, 0, 50, 750, "rects.rect-" + "1", 0, 0, "左墙面" )
                , this.createGround( this._pw, this, 3, 0, 50, 750, "rects.rect-" + "1", 430, 0, "右墙面" )
            ];

            this._vcGroundsFloating = [
                this.createGround( this._pw, this, 4, 0.6, 120, 20, "rects.rect-" + "0", this._p2FloatingLimitLeft, 600 )
                ,this.createGround( this._pw, this, 5, -0.8, 90, 20, "rects.rect-" + "8", this._p2FloatingLimitRight, 450 )
                ,this.createGround( this._pw, this, 6, 1.2, 80, 20, "rects.rect-" + "10", this._p2FloatingLimitLeft, 300 )
            ];
:
                city.phys.P2Space.syncDisplay( this._vcGroundsFixed[0], "地面" );
                city.phys.P2Space.syncDisplay( this._vcGroundsFixed[1] );
                city.phys.P2Space.syncDisplay( this._vcGroundsFixed[2] );

:
            this._pbPlayer
                = this.createPlayer( this._pw, this, 0, "kuchipatchi1.png", 200, 700 );

:
            this.stage.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.touchProcess, this, false );

        }
```

egret社内のライブラリ的な`city.phys.P2Space`というクラスで、物理演算エンジンp2のメトリクス(メートル)とegretの画面サイズのメトリクスを管理しています。そのために最初に`city.phys.P2Space.initSpace( 1, new egret.Rectangle( 0, 0, this.stage.stageWidth, this.stage.stageHeight ) );`を行い初期化しています。

`createGround`で、地面や空中に浮いている床などを複数生成しています。

`city.phys.P2Space.syncDisplay`というメソッドで、`p2.Body`の物理演算上の位置や回転と、それに紐づいたegret上の表示オブジェクト`p2.Body#displays`の位置や回転情報を同期させています。

```ts:
        public static syncDisplay( body:p2.Body, comment:string = ""  ):void {
            :
            var disp:egret.DisplayObject = body.displays[0];
            if (disp) {
                var loc:Array<any> = P2Space.getEgretLoc( body, comment );
                disp.x = loc[0];
                disp.y = loc[1];
                disp.rotation = 360 - body.angle * 180 / Math.PI;
                :
            }
        }
```

`syncDisplay`のコードはegretとp2を使う際の定番コードになりますので覚えておいてください。

最後にタップされた時の処理のためにハンドラーを追加しています。
どのサンプルコードを観ても、タップ等のイベントハンドラーは基本的には、メインの`DisplayContainerObject`の`stage`の`addEventListener`を呼び出して追加するようです。

`this.stage.addEventListener(egret.TouchEvent.TOUCH_BEGIN, this.touchProcess, this, false )`

# タップ処理
```ts
        private touchProcess( e:egret.TouchEvent ):void{
            :
                if( city.phys.P2Space.checkIfCanJump( this._pw, this._pbPlayer ) ){
                    this._pbPlayer.velocity[0] = this.PLAYER_VX * ( e.stageX - 240 )/ 200 ;
                    this._pbPlayer.velocity[1] = this.PLAYER_VY;
                    console.log( "jump speed:", this._pbPlayer.velocity );
                }
            :
        }
```

`touchProcess`メソッド内では、ジャンプゲームで必須な「空中ではジャンプできないけど床の上にいる時だけジャンプできる」のための判定処理がおもしろそうです。

判定にはegretの社内ライブラリ的な`city.phys.P2Space`の`checkIfCanJump`が使われています。

```ts
        public static checkIfCanJump( world:p2.World, body:p2.Body ):boolean{
            var result = false;
            for(var i=0; i<world.narrowphase.contactEquations.length; i++){
                var c = world.narrowphase.contactEquations[i];
                if(c.bodyA === body || c.bodyB === body){
                    var d = p2.vec2.dot(c.normalA, this.yAxis); // Normal dot Y-axis
                    if(c.bodyA === body) d *= -1;
                    console.log( "checkIfCanJump:", d );
                    if(d < -0.5) result = true;
                }
            }
            return result;
        }
```

このメソッドはジャンプゲームを作る際には参考にできそうです。

このサンプルゲーム内のモジュール群`city.phys.*`はegretでのゲーム開発の参考になりそうなユーティリティーメソッドが多いのでソースコードを追っても面白そうです。

# ゲームループ
ゲームループは定番の３処理になります。

1. `p2.World.#step`を呼び出し物理演算を行う。
2. 各オブジェクトの処理を行い
3. 表示処理（物理演算上の位置と画面上の位置を同期）

このサンプルコートの注意点としては不具合があるところです。

APIDOC上では、`ENTER_FRAME`イベントの呼び出し時の引数は、`dt:number`ではなく`e:egret.Event`なはず。
また、`p2.World#step'(http://schteppe.github.io/p2.js/docs/classes/World.html)の第一引数が秒なはずです。
ところが`private WORLD_STEP_DT:number = 2.5;`とコーディングされていますので、フレームレート20にあいません。本来であれば`WORLD_STEP_DT:number = 1/20`なはずです。

```Ts
        private run( dt ):void{

            this._pw.step( this.WORLD_STEP_DT );
            :
            // 動く床がスクロールアウトしたら反対側に移動する処理
            if( this._vcGroundsFloating[0].position[0] > this._p2FloatingLimitRight ){
                this._vcGroundsFloating[0].position[0] = this._p2FloatingLimitLeft;
            }
            :
            // プレイヤーや動く床の物理演算上の位置と画面上の位置を同期させる
            city.phys.P2Space.syncDisplay( this._pbPlayer );
            :
        }
        
```