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

ということを昔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なりムービーなりで記録してサーバに叩き送るのが昨今だと正解じゃないですかね。

テキストエディタで完結したゲーム開発環境は無いかね

と書いたが要はテキストエディタだけを入力として完結したゲーム開発環境が欲しいなという話。グラフィカルなドット絵エディタや3Dモデラー、ミュージックシーケンサなどは無し、さらにいうと外部リソースインポートも無し、ゲームに必要なリソースは全てコードで示す。

ここまで制約を加えるとなかなか所望のものは見つからない。そもそもグラフィックスをコードで示そうとなると、ドット絵を文字列の配列で示すとか、モデルの頂点を数値配列で示すとか、えらく前時代的な作りになって、それはそれでどうかと思う。

他のアプローチとしては、グラフィックスを生成することだ。ドット絵で言えば

Procedurally generated enemies

Identicon

のように何もないところから敵っぽい形やアイコンを生成したり、

pixel-sprite-generator

のように大まかな形を与えるとそれっぽい形にしてくれる方法がある。

3Dでも

Spaceship Generator

のような宇宙船ジェネレータがあったりする。あとは

Supershapes Generator

のような数式による形状作成という方法もあるな。シェーダもこの手の話の一種。

POV-Ray

POV-Rayのようにプリミティブの配置をテキストで記述する方法もある。

こういった方法を使えば、ランダムシードや数式、または種となる簡単なパターンのみをコードに記述して、そこからキャラクタのグラフィックスを生成することができる。ただ作られる形のバリエーションにはかなりの制約があり、人手で作られたコンテンツのレベルには遠くおよばない。

音の方はどうだろう。テキストでの音記述と言えば、もちろんMMLがある。

Music Macro Language

音符はもちろん、ものによってはFM音源パラメタや波形データなども書けたはず。歴史のある伝統的な手法だ。

音をごくコンパクトな数式で表す方法もある。

Bytebeat

デモとかで使われることのあるBytebeat。時間を入力とした数式の出力をほぼそのまま波形にするという乱暴な方法だが、数式を工夫することで長時間の展開を含む複雑な曲を作ることもできる。ただちゃんと曲の鳴る数式を作るのはかなり大変。適当な式を入れると大抵はとんでもない騒音が出てくる。もうちょっと整った方法だとSuperColliderなど。

SuperCollider

効果音だけならパラメタの組み合わせで簡単に作れる方法がいろいろある。

jsfx

ChipTone

この辺の話はまとめるとGenerative artの一種ということにはなるんだよな。

Generative art

ただGenerative artはゲームに限った文脈ではないから、キャラクタとか効果音とかいう観点でのコンテンツ生成についてはそれほどカバーされてないかもしれん。

この辺の方法をうまく取り入れれは、コード一つでゲームロジックから絵から音から全て書けるようになって、プログラマにとってのお気楽なゲーム開発環境が手に入る。それを実現している一例としては、PuzzleScriptがあるな。

PuzzleScript (Objects, Sounds)

この手のお気楽さが他のジャンルのゲームでも使えるようになっくると嬉しいのだが。

お手軽にゲームプレイAIを試してみる

最近はゲームもAIがプレイしてくれる時代だ。

ゲーム攻略で人間を超えた人工知能、その名は「DQN」

有名なDQN。フルネームはdeep Q-networkと呼ばれる強化学習の一種だ。こういう機械学習系の仕組みはマシンパワーでもって学習をぶん回して動かさないといけないので、それなりの準備が必要なのが普通だ。だけど最近はこの手の物をブラウザ上で簡単に試せるようになっている。

REINFORCEjs

例えばREINFORCEjs。これはDQNJavaScriptで実装したもの。使い方もえらく簡単。

// DQNエージェントにゲームの状態を与えると
var action = agent.act(state); 
// アクションとしてどう行動すればよいかが帰ってくるので
// それに従って行動して
// その行動が正しかったどうかを示す報酬をDQNエージェントに教える
agent.learn(reward);

これだけ。状態、アクション、報酬ってのは例えば以下のようなもの。

  • 状態:プレイヤーから見た敵の方向と距離、ボーナスアイテムの方向と距離
  • アクション:上下左右どっちに動くか
  • 報酬:敵にぶつかると-1、ボーナスアイテムを取ると+1

ディープマインドがAtariのゲームを攻略した時みたいに、画面のピクセル全体を入力として操作を学習する、みたいなことをしようとするとそれなりの学習時間とマシンパワーが必要だが、入力する状態を限定すればそれほど頑張らなくても学習できる。

DQNとは異なるアプローチもある。

Neuroevolution

Neuroevolution。ニューラルネットワークにスコアを与え、それを元に遺伝的アルゴリズムを適用しネットワークを進化させていく。これもブラウザ実装がある。

Flappy Learning

Flappy BirdをNeuroevolutionを使って攻略している。NeuroEvolution.js部分に学習アルゴリズムがまとまっている。これも使い方は簡単。

// 複数のニューラルネットワークを取得して
var networks = ne.nextGeneration();
...
// ネットワークにゲームの状態を与えると、
var action = network.compute(state);
// アクションとしてどう行動すればよいかが帰ってくるので
// それに従って行動
...
// 一通り行動したらそれぞれのネットワークにスコアを与える
ne.networkScore(network, totalReward);

NeuroEvolution.jsは一通り行動してからその行動にまとめてスコア付けして進化、REINFORCEjsは行動のたびに報酬を与えて学習、というふうに動作サイクルは若干異なるけど、両方とも強化学習の一種なので外からみれば使い方はほとんど同じだ。

だからこいつらを一つのゲームに混ぜて競わせることもできる。

dqn-cross-road

dqn-cross-road

いわゆるハイウェイをクロスさせてみた。

  • 状態として周辺3車線の直近の車までの距離と車線のタイプ(普通、スタート地点、ゴール)を与える
  • アクションは前に進む、後ろに戻る、何もしないの3つ
  • 報酬は渡りきると+1、車に当たると-5
  • REINFORCEjsはデフォルト設定のまま、NeuroEvolution.jsはネットワークを入力ノード数6(3車線*距離とタイプ)、中間ノードを10ノード2層、出力ノード数2(前、後ろ)とする
  • 最初にREINFORCEjs5つ、NeuroEvolution.js5つのプレイヤーを作る
  • 2秒ごとに報酬が一番低いプレイヤーが排除される。全プレイヤーが排除されると次の世代へ
  • REINFORCEjsは最後まで生き残っていたエージェントのクローンが次の世代で作られる

こういう学習して動作する系のものってバグ無くうまく動いているかどうかが分からないのが困り者なのだが、一応ちょっとは車を避けているっぽい動作をしているので動いているのではないだろうか。あと状態とか報酬とかネットワーク構成とかは適当。本当はチューニングが必要なのだろうが。

ちょっと動かしてみるとREINFORCEjsはフラフラしながらも一応避けて進めているっぽい。NeuroEvolution.jsは立ち止まる、突っ込むみたいな単純な動作になりがち。これは単純にネットワークの複雑さの差なのかしらん。だけどNeuroEvolution.jsでもたまに賢げに振る舞うのが出てきたりするのが面白い。ただ放っておけばどんどん賢くなっているかと言われるとあんまりそんな風には見えないね。

という感じに簡単にゲームプレイAIを試すことができるように最近はなってきている。これどう使おうかね。とりあえずデモプレイを代わりにやってもらおうか。

世の中にワンボタンゲームってどんなのがあるの

って聞いて教えてもらった。ワンボタンゲームってのはプレイ中に使う操作がボタン一つだけのゲーム。

スタートリゴン

星の周りをグルグル回るホシ・ワタル。ボタンを押すと飛び出して他の星へラインを張って移動、三角ネットが出来たらOk。

バッドランズ

LDゲーム。ボタンでタイミングよく拳銃を撃ち敵を倒す。LDゲームは基本タイミングゲーなので、それを極限まで簡略化するとワンボタンになる。

Canyon Bomber

自機は左右に自動移動。ボタンを押すと爆弾投下。得点岩をたくさん破壊できればOk。

魚ポコ

正確にはワンボタンゲームでは無くレバー下だけゲーム。ピンボールのプランジャーのように引いて玉を撃ち出す。

あとはハイパーオリンピックみたいな連打系があるかもしれないけどそれは今回は除外。

わざわざゲーセンに限定して探したのは、ブラウザゲームスマホゲームに広げるとおそらく無数にあるだろうから。フラッピーバードのような酔っぱらいゲーム系はたくさんありそうだし、

One Button games on Kongregate

KongregateにもONE BUTTONタグが付いたゲームはたくさんある。

Canabalt

ワンボタンジャンプゲームとしてとても有名なCanabalt。

10 More Bullets

ワンボタンでショットして連鎖ゲー。これ楽しいな。

あと個人的に好きなワンキーゴルフとワンキーフロッガー

1B Nanogolf

F

昔いくつか自作もした。

DOT CAR

FIGURE OF EIGHT

ボタンに割り当てられたアクションは、撃つ、ジャンプ、加減速くらいのものだけど、その制約があってもこれだけバリエーションのあるゲームが作れるんだから人類は偉大だ。どこかワンボタンゲーム機とか酔狂なもの作ってくれませんかね。

セルオートマトンでゲームは作れるか

作れそうだけど大変そう。

consomaton-game-lib

screenshot

前にセルオートマトンプログラミングパズルゲームconsomaton作った時から、セルオートマトンのルールを書くだけでゲームが作れたらお気軽ではないかと思っていた。ただセルオートマトンのルールは、あるセルとその周辺の状態の変化のみしか書けないという強烈な縛りがあるので、書けるゲームはかなり限られるだろうという予想はあった。でもまあ試すだけ試してみた。

ルールは左3列がbefore、右3列がafterとしてconsomatonと同様に書く。'==='はルール間のセパレータ。

===
v
   v

こう書くとvが毎フレーム一つ下に落ちるようになる。

ゲームだからランダムに出現する敵とか必要。なのでルールをランダムに発火させるための'r'コマンドってのを作った。

===r10
.  .
   v

コマンドはセパレータの右に書く。'r10'って書くと1/10の確率で発火する。

あとゲームにはスコアも必要。なので次はスコアを追加する's'コマンド。

===r10-s1
.  .
   v

's1'って書くとこのルールが発火したときに1点入る。

パッドやキーの入力を受け付けてそれに応じて発火するのも必要。'p'コマンドってのを作って'p>'で右入力で発火にした。

===p>
@   @

@を自機として右に動くルールができた。

同様に'p<'で左、だけど右のルールがすでに発火している時はこのルールは発火しなくても良い。なので'---'っていうセパレータにすると前のルールが発火済みの時は発火しないようにした。

---p<
 @ @

あとゲームオーバー条件も必要。プレイヤーがいなくなったらを表すための'n'コマンドと、発火したらゲームオーバになる'o'コマンドってのを追加。

===n@-o

ここまで書くとこれができる。

FALL V

このようにお気軽にゲームが書ける。

お気軽ではない。ただでさえルールで書けることに限りがあるのにさらにそれを謎のコマンド群で拡張しないとごく簡単なゲームも作れない。ルールの書く順番や謎のセパレータの使い分けも必要だったりして、手軽に書けると言えるかというと微妙である。

ついでに前に作ったサウンドジェネレータsounds-some-soundsで音が付けられるようにして、ドット絵ジェネレータpixel-art-genで絵が付けられるようにした。それらを駆使して、一番上のスクリーンショットにあるクロスハイウェイみたいのを作るソースコードは以下。

CROSSMAN source code

長いね。これを書くこと自体がパズルゲームだね。パズルゲームを解く苦行とゲームを作る苦行が同時に味わえるという点ではお得感があると言えましょう。