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

筆者の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の当たり判定は凸面のポリゴンにしか対応してないので、凹面の多角形は適当な凸面ポリゴンに分割する必要がある。今回は安直に多角形の中心点と各辺から成る三角形に分割した。

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

Elmのエラーメッセージが分かりやすくなっている

だいぶ前にElm触ってゲーム作った時はElmに色々と不満があったんだけど、あれからElmもだいぶバージョンアップして0.15.1ではエラーが人フレンドリーになったらしい。前回の不満がどれだけ解決されているか調べてみよう。

関数の引数の数間違いが謎の型エラーになったりする

module Test where

import Html exposing (text)

message2: String -> String -> String
message2 world world2 = "Hello, " ++ world ++ " " ++ world2

main = text (message2 "World?" "World!")

というメッセージを表示するだけのコードを書いて、

main = text (message2 "World?")

と引数の数を間違うと、

The 1st argument to function `text` has an unexpected type.

8|        text (message2 "World?")
                ^^^^^^^^^^^^^^^^^
As I infer the type of values flowing through your program, I see a conflict
between these two types:

    String

    String -> String

8行目のここの引数がStringで無くString -> Stringだぞと出る。素晴らしい。抜群に分かりやすくなっとる。

レコードのアップデートと追加構文を間違った時のパーサーのエラーが謎だった

main関数を

main =
  let
     world = {str = "World??"}
     world2 = {world | str2 = "World!!"}
  in text (message2 world2.str world2.str2)

とし、

     world2 = {world | str2 = "World!!", str3 = "World?!"}

と許可されていない複数のフィールド追加を行うと、

11|      world2 = {world | str2 = "World!!", str3 = "World?!"}
                                           ^
I am looking for one of the following things:

    a closing bracket '}'
    an expression
    an infix operator like (+)
    whitespace

とちゃんとここで'}'を閉じろと言う。素晴らしい。

型の宣言でのエラーや変数名の重複でその行番号を教えてくれない

message2: String -> String -> String2

とすると

Cannot find type `String2`

5| message2: String -> String -> String2
                                 ^^^^^^^
Maybe you want one of the following?

    String

こう。

     world = {str = "World??"}
     world = {world | str2 = "World!!"}

とすると

Naming multiple values `world` in a single let-expression makes
things ambiguous. When you say `world` which one do you want?

11|      world = {world | str2 = "World!!"}
         ^^^^^
Find all the values named `world` in this let-expression and
do some renaming. Make sure the names are distinct!

こう。完璧ではないか。

乱数シードに現在時刻が欲しい

これはエラーメッセージとは関係ないのだが、乱数の初期化のために現在時刻が欲しかったのだが、その方法が良く分からなかった。

Elmの中だけでなんとかしようと思わないでportを使ってJavaScriptから供給すればいいらいいぞ。

portはElmとJavaScriptの間で値をやりとりする仕組みで、JavaScriptからsendされた値をElmのSignalとして受け取ることができる。

Elm.fullscreen(Elm.Test, {beginTime: Date.now()});

JavaScriptからElmを起動する時に引数で与えた値を、

port beginTime: Float

main =
  let
     world = {str = toString beginTime}

とportを介してもらえば良い。portってSignalじゃなくてもいいのね。たぶん不変な値限定だけど。

requestAnimationFrame対応

ElmのPongの例とかだと画面の更新はSignal.map inSeconds (fps 35)とか使っていてタイマーベースだけど、できればこれはrequestAnimationFrameにしたい。

それにはelm-animation-frameを使えば良い。elm package install jwmerrill/elm-animation-frameでパッケージをインストールし、

main =
  let
     frameNumSignal = Signal.map toString 
       (Signal.foldp (+) beginTime AnimationFrame.frame)
     messageSignal = Signal.map (message2 "World?!") frameNumSignal
  in
    Signal.map text messageSignal

のようにAnimationFrame.frameをトリガに画面を更新すれば良いらしいぞ。

Elmイケているではないか

バージョンアップで着実に欠点が埋められている感じ。portを使ったJavaScriptとの連携がうまく使えれば、ゲーム作りにもいいかも。あとは良いIDEが欲しいよな。

物理エンジンMatter.jsをテキストでレンダリング

CRTっぽいテキスト画面をWebGLで作るLocatePrintを使って物理エンジンをレンダリングするというデモも作った。

consolephysics

物理エンジンMatter.jsを使っている。Matter.jsはもちろん自前のレンダラを持っているんだけどそれを他のレンダラで差し替えられる

差し替える独自レンダラはMatter.jsのRender.jsを参考に作れば良い。特にRender.bodyWireframes関数にボディの頂点を線でつなぐ一番単純な描画方法が実装されているので、これを真似るのが簡単だ。

デモのコードでは、頂点を線でつなぐ部分でテキストを書くようにした。画面をテキストの幅と高さ(40キャラ x 20キャラ)のグリッドに分割し、線とグリッドが交わるポイントをリストアップする。グリッド内の各キャラについて、それらポイントがどこにあるかに応じて書くテキストを変える。例えば上端右と下端左にポイントがあれば/、右端上と下端左にあればFという具合に。それらのパターンを全部配列で記述した。

後はボディごとに設定された色に従ってcolor, locate, printするだけ。昔ながらの画面で近代的な物理エンジンが動く妙な絵が作れる。楽しい。