読者です 読者をやめる 読者になる 読者になる

機能モジュールのパイプラインでキャラを制御する

Phaserをthree.jsで描画ネタのサンプルコードでもう一つ実験したのは、ゲーム内のキャラを機能のモジュールを組み合わせて制御できないかということ。例えば、上から降ってくるバルーンは、以下のコードのようにした。

interface Baloon extends Phaser.Sprite, U.HasMesh, U.HasName { }
function setBaloon() {
    var x, y;
    var baloon = <Baloon> U.chain([
        U.Name.set('Baloon'),
        U.Position.set(
            x = (Math.random() * .8 + .1) * 512,
            y = -.1 * 512),
        U.Body.set,
        U.GeomertyMaterialBody.addSquares
            (15, ['0110', '1111', '1111', '0110'], [0xeeeeaa]),
        U.Mesh.set,
        U.Sprite.removeWhenOutOfWorldBounds(60, 60, true),
        U.Body.setCollisionGroup(shotCollidingCg),
        U.Body.collides([shotCg, shotCollidingCg]),
        U.Body.setNotCollidingWorldBounds,
        U.Sprite.setUpdate((o: Baloon) => {
            o.body.thrust(66);
            U.PositionMesh.update(o);
            U.BodyMesh.update(o);
        }),
    ], U.Sprite.get());
}

elmで関数型にかぶれてた後に作ったので関数合成っぽくかけるようにした、んだけど実態は全然関数合成ではなくて、U.chainっていう関数の中身は配列内の関数にobjを順に渡しているだけ。

export function chain(funcs: Function[], obj = {}) {
   _.forEach(funcs, (f) => f(obj));
   return obj;
}

objはゲーム内のキャラを表すインスタンスで、PhaserではだいたいPhaser.Spriteを使う。objを扱う関数は、例えばその位置を設定するU.Position.setは以下のコード。

export interface HasPosition {
    position: Phaser.Point;
}
export module Position {
    export function set(x: number, y: number) {
        return (obj: HasPosition) => {
            obj.position.x = x;
            obj.position.y = y;
        }
    }
}

positionを持つHasPositionなobjにxとyを設定する関数を返している。自力カリー化みたいなことをすることで、関数合成もどきみたいなことをできるようにしている。objを直接書き換えずに、位置を更新した新しいobjをreturnするようにすれば、lodashのflowで繋げられる行儀の良い関数になると思うけど、面倒だからこの方式で。

利点としてはパイプラインで書けるので流れるようなインタフェースっぽいのが実現できること。あと機能をモジュールで分離できるので、一つのクラスにすべての機能を詰め込むようなことをしなくて済む。物理エンジンのBodyと3DライブラリのMeshの両方を持つobjだけで使うユーティリティ関数とかも作れる。

export interface HasBodyMesh extends HasBody, HasMesh { }
export module BodyMesh {
    export function update(obj: HasBodyMesh) {
        obj.mesh.rotation.z = -obj.body.angle * Math.PI / 180;
    }
}

欠点としては各関数で手動のカリー化みたいなのをしなきゃいけないこと。Ramdaとかの自動カリー化をしてくれるライブラリが使えればいいんだけど、TypeScriptの型情報を保持したままカリー化するのは、インタフェースをうまいこと使ったりする必要があったりして難しそう。

あとデバッグが面倒。パイプラインでつないだ関数を実行する実態はすべてU.chainの中にいってしまうので、まともにブレークポイントが張れない。これが結構きつい。

TypeScriptにこだわらないのであれば、ES6とRamdaの組み合わせとかでもうちょっとマシに作れるかもしれないけど、型が無いのは個人的にはかなり魅了減なので難しいところだ。

この記事みたいに、さらにBacon.jsまで使えばより関数型でFRPっぽい書き方ができそうな気もするが、そこまでするんだったらもうelmにしとけという気もする。

ゲーム内のキャラ制御を機能別にモジュール化できればコードの再利用性は高まると思うけど、モジュールの作り方、それらの組み合わせ方、妥当な書き方はまだよく分からんというところでした。