Matter.jsをプラグインで拡張する

lark-matter screenshot

ちょっと前にlark-matterっていう物理エンジンMatter.jsのレンダラをドット絵っぽくするプラグインを作ったんだけど、その時に覚えたはずのMatter.jsのプラグインによる拡張方法を忘れないうちにメモっておく。

基本的には以下のドキュメントを読めば良い。

プラグインの基本情報を書く所登録する所はだいたいサンプルの通り書けばいいとして、肝心なのはエンジンへのパッチの当て方だ。

基本的にはMatter.beforeMatter.afterという関数を使えばMatter.js内のあらゆる関数の実行前と実行後にプラグインとして実行したい処理を挟み込むことができるので、そこでゴニョゴニョすれば良い。

lark-matterの例で言えば、まずRenderを差し替えたいのでRender.createの後で独自レンダラの初期化などを行って、Render.runの後で既存レンダラを止めて新しいレンダラに差し替えている。

  matter.after('Render.create', initRender);
  matter.after('Render.run', runRender);
function runRender(render: Matter.Render) {
  matter.Render.stop(render);
  renderLm(render);
}

Render.runの引数とかはそのままフックする関数の引数にくるので、それを使ってrunしたレンダラを止めるとかいう処理もできる。

後はEngine.createの後でMatter.js内のイベントのリスナーを登録しておけば、特定のイベントが発生した時の処理を差し込むこともできる。例えば衝突発生時(collisionStart)にパーティクルを飛ばすとか。

  matter.after('Engine.create', initEngine);
function initEngine() {
  const engine: Matter.Engine = this;
  matter.Events.on(engine, 'collisionStart', e => {
    e.pairs.forEach(p => {
      p.activeContacts.forEach(ac => {
        const b = ac.vertex.body;
        const v = b.velocity;
        let ratio = (<any>p).collision.depth * matter.Vector.magnitude(v) * 0.1;
        if (ratio > 2) {
          ratio = 2;
        }
        if (ratio > 0.3) {
          ppe.emit(b.ppeTypeId,
            ac.vertex.x / LarkMatter.options.dotSize,
            ac.vertex.y / LarkMatter.options.dotSize,
            Math.atan2(-v.y, -v.x),
            { countScale: ratio, speed: 0.7 * ratio });
          if (LarkMatter.options.enableSes) {
            sss.play(b.sssTypeId, 2, null, ratio > 1 ? 1 : ratio);
          }
        }
      });
    });
  });
}

どのようなイベントが存在するかはMatter.jsのリファレンスを見れば分かる。例えばEngineで発生するイベントの一覧とか。

あとはプラグインのリストもあるので必要に応じて他の人の使ったプラグインを参考にすれば良い。

ブラウザ上でサーバサイドコードをエディット、実行できるGlitch

jsbinみたいなクライアントサイドJavaScriptをブラウザ上でエディット、共有するサイトはいろいろあるけど、このGlitchはサーバサイドでのコードをエディットして実行できるのが特徴。

とりあえずアクセスした人が適当にセルを埋められる多人数ライフゲームを作ってみた。Glitchのエディタ上でコードを書くだけで適当なURLが割り当てられてnode.jsでサーバサイド実行してくれる。

ドキュメントがあまり無いので結局何をしてくれてコードが実行されるのかがよく分からんのだが、たぶんこのエディタ上のルートディレクトリでnpm installnpm startした結果が動作しているのだと思う。だからpackage.json上のscriptsで実行させたいサーバサイドコードのエントリポイントを示しておいて、そのエントリポイントからexpressなどのサーバを起動、express.staticでクライアントサイドのJavaScriptやHTMLの置き場を示せば良い、のだと思われる。

ただブラウザ上エディタで昨今の近代的なJavaScript開発を行うのはなかなか限界があるので、その時はGitHub上にプロジェクトを置いてそれをインポートするのが現実的かもしれず。

5分間くらいアクセスが無いと自動的にサーバは止まるので、データを永続化したい時は外部DBに頼るか、sqliteなどを使ってファイルに記録しておく必要がある。そのほかにもいろいろ制約はあるけど、今のところサービスは無料で使えるようになっているので、簡単なnode.jsアプリを作って公開するにはお手軽かも。

Twitter上でそのまま遊べるアクションゲーム

はタイムライン上でJavaScriptが動かないので原理的にムリだ。なので、

こうなる。プレイヤーの良心に期待だ。

この方式の欠点はくさるほどあるが、一番の問題は避けゲーしか作れないこと。プレイヤーが能動的に何かを取ったり撃ったりすることはできない。Gifだから。

あとはこういったタントアール的な方向に走る方向もあるけど、できればアクションゲームが作りたいなあ。なにかいい方法はないかね。

ソースコードは以下にある。

not-interactive

無限ミニゲーム生成器を今度は遺伝的プログラミングで作ろうかと

思っていたのだけどやはりうまくいかん。

game-combinator

前回の無限ランダムひどいアクションゲーム生成器への道ではボタンを押した時にゲームに与える影響をランダムに変化させてゲームを生成しようとしていたけど、いまいちゲームにバリエーションが出ないのが欠点だった。

もうちょっとドラスティックにゲームの構成を変えないとバリエーションが得られないかなと思って、今度は遺伝的プログラミングっぽくゲームのコードを組み合わせて新しいゲームを作るアプローチを試してみた。

例えば、

game-combinator-helmet

のような上から降ってくる物を避けるゲームを

(game helmet
  (actor stage
    (if initial (spawn player))
    (if (random frequently) (spawn enemy))
  )
  (actor player
    (if initial (place bottom_left))
    (if (key left) (move left))
    (if (key right) (move right))
    (if (touch out_of_screen_right) (
      score
      (place bottom_left)
    ))
    (if (touch out_of_screen) (move step_back))
  )
  (actor enemy
    (if initial (place top))
    (accelerate down normal)
    (if (touch player) miss)
    (if (touch out_of_screen) remove)
  )
)

のようなDSLで書く。同様にこの手のゲームを10個ほど作り、それらの構文木を適当に混ぜ合わせる、と以下のようにゲーム、か?みたいなものがいくつができる。

game-combinator-generated

上記の謎のDSLは、ゲームを混ぜ合わせたときでもそれほど破綻のないよう、

  • 構文が簡単
  • 数値を使わない
  • ゲーム中のアクターは自機とショットと敵と敵弾、あとステージに限定

という方針で作ったもの。お陰で一応混ぜたものはだいたい実行できる。実行はできるのだが……

問題は、できたものがかろうじてゲームと呼べるものになることが極めてまれということだ。ゲームと呼べるとは、

  • キー入力すると何かが起こる
  • プレイヤーが意図してスコアを得る手段がある
  • プレイヤーが何かを失敗してミスすることができる

という極めて意識の低い設定によるものだが、このハードルすら越えられないひどいものがほとんどである。

ごくまれにハードルを越えてゲームと呼んでやってもいいものもできる。

rainy day

game-combinator-rainy-day

これは上から降ってくる雨をよけるゲームだと思われる。この青いキャラは本来はプレイヤーのショットなのだがそれが降ってくる障害物っぽい挙動に変わった上でプレイヤーに当たるとミス扱いになるようにもコードがたまたま変わったのでゲームとして成り立っているっぽい。上端にあるキャラは謎。

racket fire

racket_fire

最初の10個のゲームの中にゲーム&ウォッチのファイアっぽいゲームがあったのだが、それはプレイヤーの幅が1キャラ分しかなかったので同時に複数の人が落ちてくるとさばききれないという問題があった。この生成されたバージョンはプレイヤーを横に長くしてちゃんと受け取れるようにしてくれている。ありがとうございます。でもなんでプレイヤーを画面中央に置き直しているんですかね。元のゲームの通り画面下でよかったんですけど。

とまあ、ぎりぎりゲームと呼べるものがたまにできるが、それもベースの10個のゲームがそれほど混ぜられてなくて遊べる状態で残っている、みたいなものが多く、全く新しいものができてしかもそれがゲームになっている、みたいな感じはあまりしない。

今回のデモでは100個のゲームを生成した上でそれから有望そうなのを10個表示して、それが同時に遊べるようにした。

有望そうってのを判定するためにいわゆるゲームとしての適応度みたいのを一応算出しようとしている。対象のゲームを複数同時に動かして、それぞれに操作をランダムに与えた時の

の差分を算出して、それが最初の10個のゲームと似たような傾向にあればゲームっぽいという判定をしてそれを適応度とした。ゲームなんだから違う操作をした時にはゲーム展開になんらかの差分が出るべきだろうという基準。

また[GENERATE FROM LIKED]ボタンを押すと表示した10個のうちLikeチェックボックスがオンのものから次のゲームを生成するという、Interactive Genetic Algorithmっぽいこともできるようにした。

現時点の問題点としては、

  • ゲームのバリエーションがまだまだ確保できてない
  • できたものからゲームっぽいものが正しく選別できてない

あたり。

バリエーションについては種になる10個のゲームをもっと増やしたり、DSLの命令セットを増やすことで少しはましになるかと思われる。

ゲームの選別基準は、難しいね。今回使った適応度は操作しても何もおこらないみたいな明らかにダメなものを弾くには使えるけど、ちゃんと遊べるゲームを選ぶという点ではあまり期待できない。DQNみたいなゲームAIに遊んでもらってAIがちゃんと上達するかを見る、みたいなアプローチもありそうだけど、ちょっと計算コストが高すぎて気軽に試すのは難しそう。

とりあえずもう少し種ゲーム増やしてみて様子見してみることにします。

難度曲線をいじっていい具合のプレイ感覚を探る

ということを昔tweetして、今でも基本こんな感じで問題ないとは思っている。ただ全てのミニゲームでこの難度曲線でうまくいくかというとそうとも限らない。場合によってはもっととっとと難度を上げたほうが緊張感が出て良かったり。

この辺の感覚は実際にゲームとして遊べるものにしないと分かりにくい。なので難度曲線を可視化&調整した上でゲームに反映する物を作ってみた。

diffi-tween

screenshot

右で曲線を調整して左をクリックしてプレイ。上から落ちてくる岩を避けて下さい。難度の上昇具合が分かりやすいようtweetの式よりは難度の上がり方はだいぶ速い。デフォルトだと30秒で2倍になる。

曲線はその計算式 (sqrt, linear, pow)と上昇具合 (climb)、あとGame & Watch式に一定時間ごとにがくんと難度が下がるノコギリ波 (saw)の調整ができるようになっている。また複数のパラメタに対して難度が設定できる。今回の例だと岩の落下速度 (speed)と大きさ (size)。

難度に影響を受けるパラメタが複数あると調整はより難しい。また、パラメタには難しくなってもプレイヤーの技量でなんとかなりそうと思えるものと、難しくなるとどうしようもない感が出るものがあって、今回の例だと前者が落下速度、後者が大きさ。速く落ちてくる分にはマウスさばきでなんとかなりそうだけど、デカイのが降ってくるのはどうしようもなく理不尽な印象を受ける。

緊張感を持たせるためには前者はlinearでゲーム序盤からどんどん難しくしてしまってたまにノコギリ波でゆるめる、後者はsqrtで頭打ちするようにして不条理感を減らす。例えば以下のグラフのように。

speed: linear, size: sqrt

ただこのへんは好みの問題なのでその方針が一意にいいとはいえず。落下速度も十分に上がってしまえば理不尽な難度感が出てしまうのは結局同じだし。

speed: pow

これは極端にpowで上げたけど20秒くらいから無理ゲー。そもそもpowで難度曲線を書く必要がある場面ってあるのかしらんという根本的な疑問も。

このような調整UIが必要かはともかく、ポチポチ変更して体感を試してみるくらいしか適切な難度曲線を書く手法はないのかねえ。

WebAssemblyのゲームをアセンブリ直書きで作る

左右矢印キーでスタートして移動、降ってくる岩を避けて下さい。

コードは以下。

ブラウザ上のアセンブリ言語ことWebAssemblyChromeFirefoxで動くようになってきたので何か作ろうと思った。普通はUnityとかRustとかのWebAssemblyを出力できるエンジンや言語を使って生成するのだが、せっかくだから直に書いてみた。

WebAssemblyはS式で書ける(wast形式)。

WebAssemblyはスタックマシンなので、2つの値を足す場合、それら値をスタックに順に積んでAddを呼び出す、というふうに書く。

get_local $p
get_local $p
i32.add

でも引数が先に来て後に命令がくるのはあまり直感的な感じはしない。なので同じものをS式で

(i32.add (get_local $p) (get_local $p))

という具合に書けるようになっている。

wastで書かれたコードはブラウザ上で実行可能なwasm形式にして、それをJavaScriptから呼び出す必要がある。wasmを作るにはWebpackローダーのwast-loaderを使う。wast-loaderは内部でwast2wasmを呼び出してwasmを生成してくれる。

JavaScriptとの連携方法は以下に詳しい。

wasmはWebAssembly.instantiateを使ってインスタンスを生成できる。

function instantiate(wastBuffer, imports) {
  return WebAssembly.instantiate(wastBuffer, imports).
    then(result => result.instance);
}

ゲームを作るからには画面への出力やキー入力などを扱いたいが、今のWebAssembly実装はDOMが触れない。

なのでそれらはJavaScript経由で行う必要がある。今回はJavaScriptとWebAssembly双方から触ることのできるMemoryを使って画面上のピクセルやキー状態のやり取りをすることにした。雑な図は以下の通り。

WebAssembly.Memoryを使ってMemoryを確保すればJavaScript上からはUint8Arrayとして見ることができる。そこにキー状態を入れたり、ピクセル状態を取り出してCanvasに反映したりする。

    new WebAssembly.Memory({ initial: 512 / 4 });
    ram = new Uint8Array(instance.exports.ram.buffer);
    document.onkeydown = e => {
      ram[e.keyCode + 256] = 1;
    };
    document.onkeyup = e => {
      ram[e.keyCode + 256] = 0;
    };
...snip...
  for (let i = 0; i < 256; i++) {
    const x = i % 16;
    const y = Math.floor(i / 16);
    if (ram[i] > 0) {
      context.fillRect(x, y, 1, 1);
    }
  }

wast上からはloadやstore命令でMemoryの読み書きができる。

  (memory (export "ram") 1)
...snip...
    (if (i32.load8_u (i32.const 293))

また、wast上の関数をexportしてJavaScriptから呼び出すことができる。これを使ってJavaScript上のrequestAnimationFrameからwast上の$update関数を呼び出す。

function update(instance) {
  instance.exports.update();
...snip...
  requestAnimationFrame(() => update(instance));
}
  (func $update (export "update")

またJavaScript上の関数をimportしてwast上から呼び出すこともできる。これを使ってMath.random()をwast上から呼び出して使う。

  instantiate(game, {
    imports: {
      random: () => Math.random()
    }
  }).then(instance => {
  (func $random (import "imports" "random") (result f32))
...snip...
    (set_local $rnd 
      (i32.trunc_u/f32 (f32.mul (call $random) (f32.const 16))))

ここまで準備できればあとはゲームのロジックをアセンブリ言語で書くだけだ。使える命令群は以下のページにある。

Z80とかでブイブイいわせてた人には楽勝だろう。WebAssemblyは膨大のメモリ、膨大なレジスタがあって浮動小数点数も使えて制御構文もある高級アセンブリ言語のようなものだ。レジスタが枯渇して裏レジスタを使わねば、みたいな葛藤は必要ない。

例えば岩を下にスクロールさせる処理は以下の通り。

    (set_local $from (i32.const 224))
    (set_local $to (i32.const 240))
    (block $slide_break
      (loop $slide
        (set_local $from (i32.sub (get_local $from) (i32.const 1)))
        (set_local $to (i32.sub (get_local $to) (i32.const 1)))
        (i32.store8 (get_local $to) (i32.load8_u (get_local $from)))
        (br_if $slide_break (i32.eqz (get_local $from)))
        (br $slide)
      )
    )

……なんか面倒な気が。プレイヤーを左右に動かすのは以下。

    (if (i32.load8_u (i32.const 293))
      (then 
        (set_global $player_x (f32.sub (get_global $player_x)
          (get_global $player_speed)))
      )
    )
    (if (i32.load8_u (i32.const 295))
      (then 
        (set_global $player_x (f32.add (get_global $player_x)
          (get_global $player_speed)))
      )
    )

やっぱり面倒だ。昨今の高級言語に慣れた身にはえらく煩雑に思える。

あとwastの書き方として変数を読み書きするのにいちいちget_localとか書かないといけないとか定数にもi32.constとかの記述が必要だとかいう問題もある。まあ手書きする想定のものではないからなあ。手書きするんだとするとこの辺を簡単に書けるシンタックスシュガーが欲しいところ。

WebAssemblyのおかげでブラウザ上でも低レベルなアセンブリ言語遊びができるようになったのは大変うれしい。でも現時点だとDOMが触れない制約のせいでJavaScriptに依存しないゲームループ実現が難しく、そのパフォーマンスを十分に享受することができないのが残念。早いところCanvasWebGLと直接やり取りできるようになって欲しい。そうなればブラウザ上の低レベルゲーム制作が楽しめるようになりそう。

ゲームのリプレイデータをURLへ埋め込む

URLには2000文字を詰め込むことができるので、頑張ればここにいろんなデータを埋め込むことができる。例えば、ゲームのリプレイデータ。この前作った例。

URLにリプレイデータが埋め込められればそのURLをTwitterとかで共有することができる。サーバレスで。いやTwitterのサーバには頼っているんだけど、自前のサーバはいらない。

でもまあたかだか2000文字分の情報量しか入らないので、入れるリプレイをどういう物にするのかは考えないといけない。

  1. ゲームオーバー直前の数秒だけをリプレイ
  2. プレイヤーからの入力を限定してデータ量を削減しつつゲーム時間を短く

1.はゲームのタイプに限らずリプレイができるけどリプレイ時間は短くなるし、作る側から見て非常に面倒なことにゲーム各フレームのスナップショットを記録しなければならない。ゲームを最初からリプレイするならゲーム初期状態としてランダムシードくらいを記録しておけばいいんだけど、1.みたいにゲーム中のどのタイミングからリプレイが始まるか分からない場合、毎フレームのスナップショットとして画面上のオブジェクト状態を正確に保存する必要がある。しかも今回はそのスナップショットも小さく最低限のデータ量にシリアライズしないといけない。何か記録漏れしたらすぐにリプレイは壊れる。とても面倒。

2.は初期状態記録+プレイヤー入力を全部記録だけで済むので開発側から見ると楽。でもゲーム中のプレイヤー全入力を2000文字に入れ込まなければいけないので、ゲームのタイプはだいぶ限られる。上記の例はワンボタンゲームで1分以内にまずプレイヤーがやられるという前提だから成り立っていた。

上記条件を満たしていれば、ゲームの初期状態やプレイヤーからの入力イベント、毎フレームのスナップショットを記録し、それをlz-stringとか使ってURL文字列にすればOk。実装の一例

リプレイ対応ゲーム作成の注意点としては以下がある。

  • JavaScriptのMath.randomはシードが与えられないのでシードが与えられる乱数を使う。Xorshiftとか
  • Array.sortを使わない。この実装が同じ比較値を持つ要素の順序が変わらない安定ソートかどうかはブラウザに依存するのでブラウザ間のリプレイデータの整合性が取れなくなる。lodash.sortByなどで代替すること

まあでもリプレイの実装はいろいろと面倒なのでサーバ使えるならgifなりムービーなりで記録してサーバに叩き送るのが昨今だと正解じゃないですかね。