ES2015のProxyを使ってJavaScriptを改造して遊ぼう

ES2015にはProxyという仕組みがある。Proxyを使えばオブジェクトへの各種操作に割り込んで好き勝手な動作を定義できる。

ただProxyには問題が合って、サポートするプラットフォームが少ない。

どうもProxyはpolyfillやトランスパイラで実現するのが難しいらしく、BabelやTypeScriptではES5に変換できない。

なのでブラウザで対応してもらうしかないのだが、長らくFirefoxでしか動かなくて最近やっとChromeが49で対応した。Chromeで動くならギリギリ使っても良いか、というレベルにやっとなったところだ。

今回はProxyを使って関数を呼び出すだけでゲーム内アクターを生成できるようにすることで、アクター生成を簡単に書けるようにならないか、というのをやってみた。

Proxyは動作をフックした時の挙動を定義するオブジェクトを与えて作る。

            protoSpawn = new Proxy({}, functionToActorHook);

関数が呼ばれた時をフックするにはオブジェクトに対するgetをフックして、そのターゲットがfunctionの時の動作を書く。Actorを生成するとか。

const functionToActorHook = {
    get: function(target, name) {
        const targetObj = target[name];
        if (typeof targetObj === 'function') {
            return function(...args) {
                if (args.length > 0 && args[0].rename != null) {
                    name = args[0].rename;
                    args[0].rename = null;
                }
                const actor = new Actor(name);
                actor.generator = targetObj.apply(actor, args);
                return actor;
            }
        }
        return targetObj;
    }
}

こうしておけばこのProxyを介して関数呼び出しをした時に上記コードが呼ばれるようになる。

import {protoSpawn as ps, p5js as p, mech as m} from './protospawn';
import Actor from './actor';

function setPsCode() {
    ps.main = function*() {
        ps.ship({isPlayer: true, pos: {x: 50}});
        ps.ship({isPlayer: false});
    }
    ps.ship = function*(prop) {

ps.shipを呼ぶだけでActorが生成される。あとES2015を使っているのでついでにGeneratorも使ってyieldすることで関数途中で1フレーム待つのもできるようにした。

    ps.explosion = function*(prop) {
        this.set(prop);
        this.stroke = 'red';
        for (let i = 0; i < 15; i++) {
            this.size += 2;
            yield;
        }
        for (let i = 0; i < 15; i++) {
            this.size -= 2;
            yield;
        }
        this.remove();
    }

で、作ってみてなんなのだが、このアプローチはいろいろと問題が。

  • アクター生成を関数で置き換えること自体にそんなに価値がない。new Ship()とか書けばいいだけだし。あとアクター定義が複雑になってきたら素直にclassで書いたほうがコード補完とかの面で有利
  • yieldを使ってアクター動作を中断する仕組みを使う場面ってそんなにない。従来の1フレームごとのupdate関数がある方が便利なことも多い。必要ならtweeningっぽい仕組みを別途作れば良さそう
  • Generator関数の中のクロージャからyieldが呼べない。なのでlodashの_.timesとかが実質使えなくなる

別の方法を考えた方が良いね。