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とかが実質使えなくなる

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

汎用ビデオゲーム記述言語VGDLとその処理系PyVGDL

2Dビデオゲームのメカクニスを記述するための言語VGDL (Video Game Description Language)というものを研究している人達がいる。

ゲーム内AIなどを研究するために必要な言語仕様の提案として始まったようだ。それを実際にパースして実行する処理系PyVGDLも作られている。

VGDLはパックマンとかフロッガーとかバルダーダッシュとかの古典的アクションゲームが簡単にかけるという触れ込みである。例えばフロッガーをVGDLで書いたコードは以下だ。

まずレベルの定義がある。

wwwwwwwwwwwwwwwwwwwwwwwwwwww
w           wGw            w
w00==000000===0000=====000=2
w0000====0000000000====00012
w00===000===000====0000===02
www   ww   www    www  wwwww
w   ----   ---   -  ----   w
w-     xxx       xxx    xx w
w -   ---     -   ---- --  w
w       A                  w
wwwwwwwwwwwwwwwwwwwwwwwwwwww

なんとなく見覚えのある画面がテキストで書いてある。次はゲーム内キャラの定義。

    SpriteSet
        forest > SpawnPoint stype=log prob=0.4  cooldown=10
        structure > Immovable
            water > color=BLUE
            goal  > color=GREEN
        log    > Missile   orientation=LEFT  speed=0.1 color=BROWN
        safety > Resource  limit=2 color=BROWN
        truck  > Missile   orientation=RIGHT 
            fasttruck  > speed=0.2  color=ORANGE
            slowtruck  > speed=0.1  color=RED
        wall > Immovable color=BLACK 

林とか水とかゴールとか丸太とか、フロッガー内のキャラクタ(スプライト)が定義されている。後で説明するが、SpawnPointとか、Missileとか、この辺の記述がキャラクタの動作を決めている。

その次はキャラ同士の衝突時に何が起こるかの定義。

    InteractionSet
        goal avatar  > killSprite
        avatar log   > changeResource resource=safety value=2
        avatar log   > pullWithIt
        avatar wall  > stepBack
        avatar water > killIfHasLess  resource=safety limit=0
        avatar water > changeResource resource=safety value=-1
        avatar truck > killSprite
        log    EOS   > killSprite
        truck  EOS   > wrapAround

avatarってのは自機キャラのことですなわちカエルだ。ゴールにカエルが突っ込んだらゴールを消す(killSprite)とか、壁に突っ込んだら戻る(stepBack)などの動作が書いてある。

次はゲームの終了条件。

    TerminationSet
        SpriteCounter stype=goal   limit=0 win=True
        SpriteCounter stype=avatar limit=0 win=False

画面上からゴールが無くなったら勝ち、カエルが無くなったら負け、ということだ。

最後は最初に書いたレベルがどのキャラに相当するか。

    LevelMapping
        G > goal
        0 > water
        1 > forest water
        2 > forest wall log
        - > slowtruck
        x > fasttruck
        = > log water

以上。非常にシンプルに書けている。

ただ本当にこれで汎用かと言うとちょっと何なところもある感じがする。SpriteSetとInteractionSetの部分がメカニクスというかゲームルール記述の肝なんだけど、ここにあるMissileとかstepBackとかの動作の定義はあらかじめ決められたセットから選ばれているようだ。

このコード内にある動作がその全てで、これの作り込みによって定義できるゲームのバリエーションは規定されているように思える。で、これら動作も汎用的かというと、どうかなあ、killIfSlowとか、これルナランダー専用動作ではないのかという気がしないでも。まあここの動作が十分な数揃っていればいいのかもしれないが。

最初に思ったのは、これPuzzleScriptに似ているなということ。

PuzzleScriptはその名の通りパズルを作るための専用言語。例えば倉庫番は以下で書ける。

LEVELS、RULES、WINCONDITIONS、LEGENDの順に見ればVGDLのサンプルに非常に似ている感じがする。PuzzleScriptのルール定義は本当にシンプルで、状態のパターンマッチと、その後の状態を記述するだけ。プレイヤーが荷物に向かって歩いたら、荷物を押す、は

[ > Player | Crate ] -> [ > Player | > Crate ]

こうだ。VISCUITのメガネにも少し似ている。この1行書くだけで、上下左右どこから押されても同じように動作するみたいなことはPuzzleScriptの処理系がよろしくやってくれるので、うまく使えば少ない記述量で複雑な動作も書ける。

PuzzleScriptはキャラクタの形や効果音などもちゃんと記述することができて、これだけでちゃんと体裁が整ったゲームを作ることができる。VGDLはあくまでゲームルールを記述するだけなのでこれだけではゲームとして色々足りない部分がある。

この2つどっちが先にできたのかと言うと、PyVGDLのinitial commitが2012年6月でPuzzleScriptのinitial commitが2013年9月だからPuzzleScriptのほうが後発だ。それらの間に関係があるかというと、よく分からない。よく分からないが、比較記事があった。

PuzzleScriptはターンベースのパズルゲーム記述にかなり特化、対してVGDLはもっと広くアクションゲームを書こうとしている点で異なるけど、ゲームの形式化という点でアプローチは似ているよね、という内容。アクションやパズル系ゲームの簡易記述を考えると、この2つのようなフォーマットに落ちるのが一つの妥当なアプローチなんだろう。

ゲームのルールのエッセンスさえ記述できれば良いという割り切りのもと、どれだけ簡易なゲーム記述言語を作ることができるか、というのを考えてみるのも楽しそうだ。VGDLの発展形や、それとは全く別な記述方式、いろんなアプローチが思いつけるのが理想だが、簡易だけど汎用的というバランスの良い言語を考えるのはたぶんそう簡単では無いね。

斬新なゲームメカニクスを目指した時の「やらかし」と「もがき」の制作過程が分かる本「組み立て×分解!ゲームデザイン」

筆者のkuniさんから献本いただいた。

ひどくおおざっぱに言うと、斬新なルール、ゲームメカニクスを持つゲームを作るに向けて、これは面白いだろうと思って作ったルールがイマイチな時、そこからどうやって工夫することで面白くすることができるか、それを書いた本。

題材のゲームはkuniさんが作ったいくつかのゲームを用いている。

例えば2章はmosserことまるぼうしかく内のしかくことフレイムテイルが題材。炎が燃え広がるというアルゴリズムを元に、単にクリックしたところが燃える凡ゲーから、炎をつける自機の導入、燃やせるテトロミノの導入などの様々な試行錯誤を経て、最終形のお尻に火が着いたスネークゲームというところに到達するまでが丁寧に解説されている。

こういった新しいルールを導入したパズルゲームやアクションゲームを作るためには何回もの試行錯誤を経てルールが出来上がっていくものだが、その制作過程を細かに語っている本は珍しく思う。こういったところは明文化されない個々人のノウハウになりがちなので、それが具体的に記載されていることがありがたい。その他にも、ルールを引き算や足し算で作る方法、ゲーム内の緩急の付け方、制約を設けることでゲームをゲームとして仕立てていく方法などなどが、具体的なゲーム制作体験に沿って書かれている。5章はみんな大好きパネキットでのいくつかの制約がなぜ必要だったのかにもちょいと触れられているよ。

定番のゲームではなく、ゲームの根幹のメカニクスがちょいと変わったゲームを作りたい人にはオススメ。記載が具体的な分、ゲームデザインに対して網羅的な内容では無いし、引用された豊富な既存ゲームに対する説明はおっさんゲーマーにとっては自明の内容で無駄な感じもするところはちょっと欠点だけど、それを差し引いても読んでいてとても楽しい本です。あとパネキットを代表とするkuniゲーが好きな人はぜひ手に取ると良いと思うよ。

ES2015のProxyを使った関数呼び出しのフック

ES2015にはProxyという仕組みがある。

Proxyを使うことでオブジェクトへの書き込みや読み込みをフックすることができ、メタプログラミング的なことがJavaScript上で実現できるようになる。

例えば、関数呼び出しをフックして呼び出し前に何か別の処理をしたい、と思ったら以下のように書ける。

const obj = {
  foo: (x) => {
    console.log(x);
  }
};

const proxy = new Proxy(obj, {
  get: function(target, name) {
    const targetObj = target[name];
    if (typeof targetObj === 'function') {
      return function(...args) {
        console.log(`call ${name} ${args}`);
        const result = targetObj.apply(this, args);
        return result;
      };
    } else {
      return targetObj;
    }
  }
});

window.onload = () => {
  proxy.foo(42);
}

Proxyのgetハンドラで値の読み込みをフックし、関数を読み込もうとしたら別の関数に差し替える。差し替えた関数の中ではapplyで元の関数を呼び出すけど、その前や後に任意の処理を挟み込むことができる。

ちなみにES5の制約でBabelはProxy未サポートなのでブラウザが対応しないかぎりProxyは使えない。

今までProxyはFirefoxかEdgeでしか使えなかったのだけど、Chromeも49から対応してくれるらしい。

だとするとそろそろProxyを使ったメタプログラミングに手を出してもいいかなあとも思う。あとTypeScriptでもES6ターゲットにすればProxy書いても文句言わないので使えそうだ。

クリックだけでプログラムが作れる夢のプログラミング環境作った

ウソです。いやウソではないか……誇張です。

screenshot

上のデモ開いて、左クリックでコード生成、右クリックでコード削除。運が良いと何かのグラフィックスを描くプログラムができる。あまりに何も描かないようだったら一旦右下の[Reset]を押して下さい。グラフィックスAPIp5.js利用。

左クリックで生成されるコードはRecurrentJSを使ったLSTMで作られている。LSTMやRNNをつかった文書生成はいろんなところでやられていて、有名どころだとThe Unreasonable Effectiveness of Recurrent Neural Networksがある。この記事ではLinuxソースコードを食わせてCのプログラムを作る例もある。ただ、自動生成でできる文やプログラムはいわゆるワードサラダで、文には意味が無いし、プログラムはコンパイルできない。

ならワードサラダなプログラムでも実行できる処理系を作ればいいんではないか、と思って作ったのがsarad。スタック指向ポーランド記法な言語。文を右から見ていって数値や変数をスタックに積み、演算子や関数を処理する時にスタックから取り出して引数にする。引数が足りない場合は強制的に0を割り当てることでエラーを吐かないようにしている。'if'や'while'などの基本的なフロー制御はあるが、突然'else'とかが出てきてもエラーは出さずに無視する。そうすることで、ワードサラダなプログラムでもエラーにはならずに無理やり実行される。

LSTMの学習に使う元データはprocessingのサンプルコードを使った。ただそのまま持ってくると自動生成の元にするには無駄にバリエーションがあるので、

  • 数は一旦すべて'D'に置換して学習し、コードを生成してからランダムに'0'~'9'を割り当てる
  • 変数名は'V0'から'V4'に強制変換する

として複数のコード片を混ぜあわせても破綻しにくいようにした。

現時点で課題は山積みで、

  • 意味のあるプログラムが生成される確率があまりに低い
  • なにも処理をしていない大量のデッドコードが生成される
  • プログラムの可読性が低く生成されたコードのどこを削ればいいか分からない
  • 'if'や'while'などブロックを使った長い文脈で意味のあるものを生成するのは大変

などなど。可読性の点からいうと、独自言語よりは既存のプログラミング言語が生成できて、デッドコード削除もされてた方が良いと思われる。もうちょっと考えます。

無限ランダムひどいアクションゲーム生成器への道

ゲームそれ自体を自動生成してくれる機械が欲しい。開発者はその機械が生成するゲームを遊んで良ゲーなら採用、クソゲーなら捨てる、その作業だけでゲームが作れる。夢の機械だ。

人が後で見て取捨選択する前提なら、出来上がるものの大半ではクソゲーでもいい。それよりも大切なのはゲームの持つルールというか、仕組みというか、ギミックというか、そういったものが十分なバリエーションを持って生成されること。似たようなゲームしか作られないのではつまらない。

どうすればそういったことができるか。一例として、プレイヤーがボタンを押した時に起こることを乱数で適当に作って組み合わせる、という方法が考えられる。例えばボタンを押した時の自機の加速度の変動パターンを適当に設定することで、上に加速すればジャンプ、右に加速すればスライディング、などの動きをいろいろ作れそうである。そういったことを自機だけでなく敵にも設定すれば、いろんなバリエーションのゲームができるかも。

そういったアプローチを採っている既存のものとして、Mechanic Minerがある。

この論文はゲームを作るAIとして有名なANGELINAプロジェクトのものである。Mechanic MinerではToggleable Game Mechanics (TGM)と呼ばれるボタンが押されるたびに自機の状態が切り替わる機構を開発し、リフレクションを使ってクラス内のフィールドに対して適用している。題材としてはレベル内のスタート地点からゴール地点へ到達するパズルジャンプアクションを使っており、自機の位置や重力に対して、ボタンが押されると倍になる、半分になる、+-が入れ替わるというTGMが用意されている。

Mechanic Minerでは遺伝的アルゴリズムを用いてランダムなTGMの組み合わせから適切なTGM群を選択している。選択のための適応度 (fitness)は、自機を適当に動かしてゴールに到達するまでにレベルのどの程度の範囲を移動できたかを用いている。レベルのあちこちを移動してやっとゴールに到達できる、というのが質の良いゲームと判定しているようだ。もちろんゴールに到達不能なものは排除される。

Mechanic Minerはこのジャンプアクションに比較的特化した作りになっている感じだったので、アクションゲーム全般にもうちょっと拡張可能なものが作れないかと思って、別のものを作ってみた。

デモゲームは以下で遊べます。

クソゲーを自動的に作る、という点のみクリアして、あとは失敗している気がする。自機や敵の動作はいろんなバリエーションが出ているが、ゲームとしてのルールとしてのバリエーションとまでは呼べない印象である。あとクソゲーを超えたプレイ不能ゲームがたくさん出てくる。ボタンを押しても何も起きないとか。

GameMechRandomizerではボタンを押した時に起こることとして、数値がn倍になる、nを足す、nになるという、より広範な動作を用意した。これらの動作がボタンを押した時、押している間、押すとトグルで切り替わる、押している間トグルで切り替わる、いずれかの条件で発生する。

適応度は2つのプレイヤーAIを作ることで評価した。一つは賢いもの、もう一つはバカなもの。それぞれのAIが自動生成されたゲームを遊んで、バカなものは多く死に、賢いものはそれほど死なない、そういったものが比較的遊べるゲームだという判断をして、適応度を高くする。逆にプレイヤーが賢かろうがバカだろうが死に方に大差ないのはゲームとして成り立ってないと考えることとした。賢いAIといっても、例えば自機を8の字で動かして敵弾に当たりにくくするとか、そのレベルの賢さだが。

GameMechRandomizerを使うことで、ある程度は汎用的なアクションゲームに対して新たな仕組みを導入することはできる。ある程度は。

課題は満載である。

  • 新しいゲームが生成されている、とはとても言えない。箱避けゲームとか箱撃ちゲームとか、もともとあるゲームに対してちょっとバリエーションを与えている、程度である

  • まともなゲームができない。クソゲーで良いといっても遊べすらしないゲームはいかがなものか

  • 遊べすらしないゲームを遊べるゲームへ調整する仕組みが必要だが、現状の遺伝的アルゴリズムベースのアプローチが妥当かどうかは不明である

  • 多分、ゲームの楽しさというものを測るもうちょっとまともな指標が必要

もうちょっと考えます。

SVGのPathの当たり判定を取る方法

Pathで囲まれたSVGの当たり判定を取ることができれば、ベクター形状の間で正確に当たり判定が取れる。現代版1ドットのエクスタシーを魅せることが可能だ。以下の様なコードを書いた。

stars stars

SVG自体にはバウンディングボックスを使ったおおまかな当たり判定の仕組みしかないので、厳密な当たり判定は自前でなんとかするしかない。

この記事にあるようにまずバウンディングボックスでの衝突を判定し、衝突している場合のみパスを使った厳密な当たり判定を取るのがパフォーマンスの面から見て望ましい。

バウンディングボックスはこのスレにあるように、getBBox()で取ったバウンディングボックスを自力でtransformすることで得た。

パスを使った当たり判定は、まじめにパスとパスの交差を計算するととても大変なので、いくつかの線分で近似するのが現実的だろう。SVGPathElementにはgetPointAtLength()というパスの始点から特定の長さの位置にある点を取る関数があるのでこれを利用する。getTotalLength()で取ったパス全体の長さを適当な数で割ってその長さごとの点で囲まれた多角形を当たり判定とする。

実際に当たり判定を行うのはsat-jsに丸投げ。ただsat-jsのPolygonの当たり判定は凸面のポリゴンにしか対応してないので、凹面の多角形は適当な凸面ポリゴンに分割する必要がある。今回は安直に多角形の中心点と各辺から成る三角形に分割した。

こうすればそこそこ正確に当たり判定を取れるはずだが、処理としては結構重たいかも。ゲームで使っていいスピードが出るかどうかは微妙かもしれず。