手続き脳人間がWeb向け関数型言語elmを使ってゲームを書こうとしてみた

がまだ私には難しすぎる気がするよ……

elmはHaskellに似た構文を持つ関数型言語のAltJS。コンパイルするとJavaScriptが生成されるのでブラウザ上で動くゲームも作れる。なのでごく簡単なミニゲームをelmで作ってみた。

ゲームライブラリ相当の部分を除くと250行強というところなので、コードの分量的にはCoffeeScriptで書くのと似たようなものかちょい長めというところかなあ。でもコードを書く際には関数型言語ならではのかなり違う発想が求められるので、なかなか苦労する点も多い。elmについて、このミニゲームを書いた時に気づいた点をメモしておく。

elmいいところ

サンプルにPongがある

なんでelmを使ってみようかと思ったかというとオフィシャルにPongを作るサンプルがあるからですよ(ただし上記のサンプルまだver.0.13の書き方なので注意。今の最新のelmは0.14)。elmは単なる関数型言語ではなくて、 Functional **Reactive** Programmingのための言語で、その特性を活かしてゲームを作るためのエッセンスが、このPongの作り方に書いてある。

elmではSignalと呼ばれる、時間で変化する値を関数型言語の枠組みで扱うための仕組みがある。時間で変化する値とは、例えばゲームのプレイヤーからのキー入力など。

まだ理解が追いついてないのだが、Signalでキー入力を扱うと、時系列のキー入力状態がリストとして管理される。キーが入力されるとその情報がこのリストの末尾に追加される。

Signalにはこのリストを過去から畳み込むための関数foldpがある。過去から畳み込む!もうこの時点でお前は何を言っているんだ感があるが、要はこれら入力に応じて今から次の未来への変換を行う関数を発火させて、別のSignalを作る仕組み。まあゲームでいうところの毎フレームのUpdate処理だ。

elmのmain関数は画面上のElementのSignalとして定義されている。なのでUpdate処理で生成されるSignalからElementのSignalを作れば、ゲームの画面出力が得られる。なので必要なのは入力や時間経過のSignalをfoldpでゲーム状態のSignalにした上でそれをElementのSignalにmapする関数、とか言い始めるとよく分からなくなるのでPongのサンプルを見て下さい。とにかくPongのサンプルを見ればelmでゲームを作る方法がすぐに分かって素晴らしい。

コアライブラリで基本的な表示と入力が扱える

コアライブラリにあるGraphics.Collageでキャンバス上に書く丸や四角や線、Mouseでマウス入力、Keyboardでキー入力が扱えるので、ゲームに必要な基本的な入出力は外部ライブラリの助けを借りなくても実現できる。関数型言語のProcessingっぽい位置づけとも言える。

Keyboard.wasdっていう関数があるのが、elmでゲームを作りましょうという意図が感じられていいね。

ランタイムエラーが少なくなる

関数型言語コンパイルが通ればランタイムエラーがほとんど無いとよく言われるが、それは確かに感じた。ただそれが型のチェックのおかげなのか、関数型言語のおかげなのかはよく分からん。型チェックが欲しいだけならTypeScriptでもいいしなあ。

タイムトラベルデバッガ

elmを使って楽しいのがこのタイムトラベルデバッガ。普通にゲーム書くだけで、それを自由にポーズしたり、時間を巻き戻したりすることができる。関数出力を監視するDebug.watchと組み合わせることで、妙な挙動をしたところとその前後を簡単に調べることができる。これは便利。

elmイマイチなところ

なんでも型エラー

ささいな間違いがなんでも型エラー (Type mismatch)になる。しかもエラーメッセージが不親切。

例えば関数の引数の数を間違えたというささいな間違いに対して長大な型エラーが出力されたりする。関数に対して一部の引数を与えることができるカリー化でそういった関数も評価できてしまうせいもあるとは思うのだが、そういった間違いも無理やり評価してその結果いろんなところに型エラーを発見したことになってしまっている感じ。よく見ると本質的なエラーが書いてあるところもあるのだが、それ以外のエラー指摘に埋もれてしまっていて発見が非常に困難。

特にelmのコンパイラは型エラーはこのへんの記述に関係あります、というメッセージの下に関数まるごとが表示されたりすることが頻発するので、発見がより困難になる。

関数型への考え方の転換コスト

elmは関数型言語であるからにして代入を許さない。参照透過性を保つためだ。

これはゲームの書き方にいろんな影響を与える。Update処理はゲーム内オブジェクトを書き換えるのでは無く、1フレーム前の状態を見て今のフレームの状態を生成する処理になる。また逐次的な処理を行う際の書き方も、前状態から次状態の値を生成する関数を作り、その値を入力としてさらに次の値を生成する関数、という書き方になりがちで、その受け渡しをするための一時変数名が大量に必要になったりする。

この辺の記述の無駄さ加減がコードを書いている際に気になる。まあそのような逐次的な処理をしないような作りにしましょう、というのが正しいプラクティスなのかもしれんが、手続き脳人間にはなかなかつらい。

パースエラー

elmのパーサーこなれてないよ。特にレコード操作。elmはレコードにすでにあるフィールドをアップデートする時の構文(<-)と、フィールドを追加する時の構文(=)が違うのだが、これを間違った時にそこを指摘せずにその後ろのカンマを指摘したりする。あとアップデートはカンマで複数書けるのに追加は複数書けないとかいう構文自体の謎仕様もある(外部ライブラリのFocusを使えばマシにはなるが)。しかもその仕様を踏んだ時のエラーメッセージがまた謎。

あと型の宣言でのエラーや変数名の重複などについて、その行番号を教えてくれないのは何なんですかね。

結局Signalとは何者だったのか

Signal、結局よく分かってない。今困っているのは乱数のシードに与えるための、ゲーム開始時の時刻を取る方法が分からないこと。Time.timestampでSignal (Time, a)は取り出せるっぽいのだが、このTimeをRandom.initialSeedに流し込むのはどうすればいいんだ。

elm、書き方の発想が手続き型言語と違って書いている分にはいろいろ楽しいけど苦しみも多い。これからどうしようかなあ。もうちょっと継続して使って見るか、やめて手続き脳の山に帰るか、悩ましいところだ。

今年面白かったゲーム

やけくそランク VS やけくそアイテムのインフレゲーRisk of Rain、タイムパイロット in 21世紀のLUFTRAUSERS、ミニマムA列車のMini Metroあたりは特にオススメ。そして2014年になってもまだスペースハリアーを遊んでいる未来。そして今でも面白い。

今年50のゲームを作って分かった面白いゲームを作る方法

なんてのは無いということが。

作ったものは上のページにまとめた。全ゲームのスクリーンショットがアニメGIFになっていて、クリックすればそのゲームが遊べる。個人的な意見としては、左上の方が楽しめて、右下のほうが退屈できます。

すべてブラウザで遊べる昔ながらのミニゲーム。半分Flash、半分HTML5HaxeCoffeeScriptで書いた。ソースも置いてあります。

1年で50作れば年の終わり頃には余裕で面白いゲームを狙って作れるようになるかなあと思ったけど、脳内で面白そうと思ったゲームが実際に作るとひどくつまらないということは相変わらず多発するので、やはりイケてるゲームを作る簡単なセオリーなんてものは無い。あるいはまだ見つかって無い。ひたすら作って、遊んで、面白くなるまで直すしかないね。

まあでも昔ながらのミニゲームを作るのに役立ちそうないくつかの方策はあったような気がするので、忘れないようメモしておく。

  • 斬新さを少しずつ削る

せっかく自作ミニゲーム作るんだから、今までに無かった面白くて斬新なギミックやフィーチャーが入ったゲームが作りたい、という方針で作るとだいたいとても斬新でとても面白くないゲームができる。大事なことはそこであきらめないで、ゲームに盛ってしまった斬新さを少しずつ削って、そこそこ面白くてそこそこ斬新なゲームに仕立てていくこと。保守的な作りによせる、ということが後から出来るようになるとゲーム作りははかどる。

  • 無理やり発想を広げるワンキーゲーム

削る前の斬新さを探すのもそこそこ大変である。そういった場合はとりあえずワンキーゲーム作りをトライ。ワンキーしか使えないという制約が強制的に新しいゲームを作るのに役立つ。ワンキーにどういう動作、仕組みを割り当てるか、という発想の起点が分かりやすいのもいいところ。

  • 誘爆は友達

困ったら何か誘爆させとけ。連鎖で何かが爆発すればみんな笑顔。うまく調理しないとありがちなゲームになる危険性もあるけど。

  • 重力も友達

地面に落ちるとか惑星の引力に引かれるとかの、ゲームに重力を取り入れたり、重力を操作できるギミックを入れたりするのは安直だがいい方策。磁力とかもいい。

昔の名作は名作たる所以があるだけあって楽しいギミックがてんこ盛りである。そういったギミックをつまみ食いしてゲームに取り入れたり、レトロゲーの一部分だけを別ゲーとして仕立てるというのは、ミニゲームを作る際には役立つ。

これは面白いゲームを作るというよりはどうやって手早くゲームを作るかという話だが、プログラムにレベルデザインを自動生成させることに慣れるとミニゲームを短時間で作るにはとても役立つ。自動生成する仕組みそのものも重要だが、自動生成したレベルが理不尽なものにならないように、あらかじめ正解を作っておいてそれ以外の部分を後から埋めるとか、作ったレベルから正解のルートだけを安全に加工するとか、そういうロジックまで書ける方が望ましい。

  • 納得できる終わりをもたらす難度曲線

ミニゲームはゲームがどんどん難しくなってやがてプレイヤーの能力を超えたところでプレイヤーを負かして終わり、という作りが多くなる。その場合、いきなり難しくなって理不尽に負ける、では無くてあともうちょっと頑張れば勝てたのに負けた、くらいの感覚になるように難しくなり具合、いわゆる難度曲線を調整できているのが望ましい。

  • リスク駆動開発

かと言ってゲーム序盤がやたら簡単なのもヒマだ。そういった時は、プレイヤーになんらかのリスキーな行動をさせてそれにボーナスを与えるべし。リスクへのボーナスを適切に与えることで、ゲーム開始1秒から納得性の高いゲームオーバーが提供できる。どういったリスクをプレイヤーに与えようか、という点から新たなギミックを考えるリスク駆動開発もオススメ。

今思いつくのはこんなところ。今年作ったゲームを遊んでくれたり感想をくれたりした方々に感謝いたします。

最新ゲーム機の裸眼立体視をマイコン時代のBASICで堪能できるプチコン3号

ニンテンドーDS用プログラミング環境プチコンの第3弾は3DS用である。

しかも裸眼立体視対応である。1000円を握りしめてニンテンドーeショップに駆け込み、プチコン3号を買って、以下のプログラムを打ち込もう。

@1:LOCATE RND(48),RND(30),RND(500)-250:PRINT "バカ":GOTO @1

文字列の表示位置を指定するLOCATEの引数が3つあることに注目。3つ目の引数のZ座標で奥行きを設定することができ、このプログラムで3DSの上画面に飛び出す「バカ」が表示できる。

立体視を手軽に試すことができるという点では、プチコン3号はかなりイケている環境である。コンソール上のテキストだけでなく、スプライトやBGなどもZ座標を設定することで画面から飛び出した位置や奥まった位置に配置できる。ムダに立体なゲームが作りたくなる。

作ったゲームはサーバ上にアップロード、公開することができ、公開キーを打ち込むことでダウンロードできる。作ったゲームを配るのも簡単だ。

いくつか作ってみた。

3DS用のソフトなので、スクリーンショット付きで3DSWii用のSNSであるMiiverseに投稿できる。3DS上で見ればスクリーンショットもちゃんと立体だ。

プチコン3号の前のプチコンmkIIの時は、

みたいな感じであまり若者が見当たらなかった印象だが、Miiverseを見ている限り今回は若手がたくさんいる感じ。

小4でmkIIから触ってるという人もいるから、単にMiiverseで若手が可視化されただけかもしれん。

プチコン3号にはあらかじめ用意されたキャラクタや背景、BGMやSEもあるので、ちょっとしたゲームを作るのは簡単で、プログラミング入門環境としてはかなり優秀だ。

言語はBASICなので最近の言語みたいな凝ったことはできないが、複数文をくくれるIF〜ENDIFはあるし、昔ながらのサブルーチン作成手段のGOSUB〜RETURN、関数定義のDEF〜ENDはあるから、頑張ればそこそこきれいで大規模なコードもかけそう。手軽ミニゲームからマジ大作まで、いろんなゲームがプチコン3号上で作られるといいね。

リワードがスコアだけとはなんという古めかしいゲームじゃ

昨日ゲームのリスク/リワードのうちリスクの話だけ (http://d.hatena.ne.jp/ABA/20141017#p1)したけど、リワードの話はしなかった。いやしなかったというか、私が最近作ってるミニゲームにおけるリワードは「スコアが入る」以外なにも無いので、しようがなかったというか。

最近のゲームだとそもそもスコアという概念が無いので、別の形でプレイヤーに報いるリワードがあるのが普通だ。アンロックされるキャラクタやステージだったり、プレイヤーを強化できる経験値やカードだったり、あと実績やメダルも。

ゲームそのものは昔ながらの作りでも、これら近代的なリワードを入れることで現代に通用するゲームにすることができる。最近遊んだゲームの中で、これにとても成功しているなあと思ったのは、LUFTRAUSERS。

LUFTRAUSERSはゲーム自体は「これタイムパイロットじゃん」という古典的全方位シューティングなのだが、コンボ中に8つのボートを倒せ!とかいうチャレンジをクリアしていくごとに、自機の武器、機体、エンジンのパーツがいろいろともらえる。死ぬ時に大爆発する機体とか、海面に突っ込んでもダメージが無いエンジンとか、一風変わったパーツをカスタマイズして組み合わせると、ノーマル自機とはかなり違った遊び方ができるようになっている。

ゲーム本体にいろんなチャレンジと、アンロックされるパーツを組み合わせるという作りにすれば、ゲーム本体は昔ながらの古典的な内容でも今風なゲームにすることができるので、みなさんなるべくそういうふうにゲームを作りましょう。

と言ってもこれ作るの面倒なんだよな。適切なチャレンジを設定する、組み合わせて面白いパーツを作りこむ、両方ともそれなりに手間がかかる。あと、チャレンジを閲覧したりパーツをカスタマイズするUIを作るのもおっくうだ。チャレンジが設定できるだけのゲームの幅を持たせるのも難しい。

でも少しは現代風なゲームにしたいと思ったら、プロトタイピングにおいてもこういったアンロックされるなにかも含んだ遊び心地まで確認できることが必要だろうし、ミニゲームにもなんらかのチャンレンジ&アンロックの仕組みが入っているのが理想なのかもしれん。これらをうまいこと手抜きして作れる方策を考えたいね。

ミニゲームに適切なリスクとリワードにはどんな種類があるかね

ゲームにおけるリスクとリワード、つまりプレイヤーがリスクを取るとリワードが得られるという仕組みは、ゲームの面白さを増すのに重要と言われている。

Risk/reward is a system established by the arcade generation that rewards the player for taking a risk that goes beyond what they are asked to do normally.

'arcade generation'とあるように、リスク/リワードはアーケードゲームによって確立されたものなので、現代のゲームにおいても必須かどうかは分からんが、アーケードライクなミニゲームにおいてはあったほうが望ましい。特にゲーム序盤が簡単で徐々に難しくなるタイプのゲームにおいては、序盤の簡単な時にプレイヤーがリスクを取れるようになってないと、やることが無くなってとても暇なゲームになってしまう。

So the first and most obvious aspect of r/r in Pacman is the blue ghost multiplier. When the player eats a power pellet the four ghosts chasing you become edible for a small amount of time and attempt to avoid you. Eating said ghosts will result in score points and with each ghost eaten after the first that score is multiplied.

The risk here of course is the fact that the ghosts will turn back to normal very quickly, so eating them becomes a race against the clock, if you're too close to one when they become normal you usually die.

代表的なのはパックマンでパワーエサを食べたあとに青点滅しているゴーストをぎりぎりまで食いにいく場面。パワーエサの効力が切れてゴーストが反転して食われるかもがリスク、切れる前に食ってしまえば1600点がリワード。

こういったリスクのバリエーションを色々と揃えておくと、ゲームに彩りを加えるのに役立つ。なので自分で今まで作ったゲームを眺めて、リスクの種類を整理してみたいと思った。自作ゲームだとリワードは「点が入る」くらいしか作ってないので、それはまた別に考えないと。

ボーナスアイテムを取りに行く

例えばこれで使っているボーナスアイテム。

上からくる黄色いボーナスアイテムを取るとボーナススコアが入る。足場がないところにボーナスが現れたりしたら無理して足場を作って取りに行かないといけない。また、アイテムを逃さずに取り続けるとスコアが+100→+200→...→+1000と増えるので、逃さないようにしたほうがお得。バトルガレッガの勲章と似たような感じ。

これとかガレッガ勲章そのもの。

ボーナスアイテムはあらゆるゲームに簡単に導入できる上にリスク/リワードも分かりやすいのでとても便利だが、それ故に安易に頼ってしまいがち。

敵に近づく

パックマンの例は思いっきりこれ。バリエーションとして、敵の近くに長時間いるとスコアが増す、という実現方法もあるよ。

これはミサイルに長い時間追っかけられると点が上がるようにした。

これは敵の背後を長時間取るとボーナス。

うまく使えばゲームにヒリヒリ感を与えるのにとても役立つが、プレイヤーのフラストレーションと表裏一体なので加減が重要。サイヴァリアの、敵弾の真横ギリギリでレバーをぐるぐる回すというとんでもないリスクに、一定時間無敵を与えるというとんでもないリワードを与えるとかが、ある意味理想型。

敵を意図的に増やす

次々に出現する敵をすぐには倒さずに、多数出現させてから倒すとボーナススコアというタイプ。敵が増えることはもちろん危険に直結する。

敵をまとめて誘爆させると高得点、とかがよくある。

「まとめて倒すと高得点!」は昔のアーケードのインストカードに書いてある代表的文句であった。

死に近づく何かを貯める

敵を増やすの類型。

での大きく囲おうとする行為とか、

での数字パネルを積み上げる行為とか。

ゲームスピードを上げる

アクセルを踏み続けるとスコアが入るというチキンレースタイプ。速度が上がると制御が効かなくなって危なくなるのは現実と同じ。

ゲームスピード上昇はゲーム&ウォッチ時代からの伝統的な難度上昇手段だが、それをプレイヤーに意図的にやらせるという方式。

リスク低減に役立たない何かをする

という分類名が正しいのかよく分からんが、破壊不能な敵弾を撃つなど、やったところで別に自分が安全になるわけではない行為に点をあげること。

有名なジェミニ誘導。

同色の敵のみを撃つ

赤、青、黄色の敵の、同色3つを続けて撃つとボーナス点。

シルバーガンじゃん。

このタイプのリスクは「シルバーガンじゃん」か「斑鳩じゃん」と言われるのがリスク。

とりあえずこんくらいの種類が思いついた。あんまりバリエーションないな。まあ「死に近づく何かを貯める」とかいうのは、それをリスクと言うのだ、というレベルでちょっとざっくりすぎる感じはする。こういったゲーム向けリスク種別をまとめている記事があったら読んでみたいのだが、どっかのゲームデザイン教科書にはあったりするのかしらん。

HTML5ミニゲーム作り向けライブラリMGL.COFFEEを作った

またゲームエンジンを作っているのか、きみは。

前にHaxeFlashミニゲームというかプロトタイプを作るためにmgl (https://github.com/abagames/mgl)というゲームエンジンというかライブラリを作っていたが、そのCoffeeScript版を作った。CoffeeScriptのやたら短いコード記述ができるのが好きなので。

作るものがきっちり決まっていれば1時間以内、漠然としたアイデアからでも3時間以内に、いちおうタイトルとスコアと音が付いたゲームが作れる、ということが目標。その分グラフィックスやら音やらは最低限でしょぼいが。

メソッドチェインと短縮記法を使うと、例えば上方向へのスピード0.05、サイズ0.05のパーティクル20個を、以下の短い記述で出せる。

@np
	.w 0, 90
	.s .05
	.sz .05
	.n 20

可読性は最悪である。

ドラムパターンと音色を適当に自動生成するので、とにかく何でも音が付けばいいやという場合は、

Sound.sd 4321
@drums = []
@drums.push G.ns.d().dp() for i in [1..4]
d.pp for d in @drums

でBGMは鳴るし、

Spring.bs = @ns.v(4).d()
Spring.bs.p

で効果音代わりのドラムがクォンタイズされて鳴る。安直である。ランダムシードの4321を適当な値に変えれば出る音はいろいろ変わる。

あとはキャラクタのグラフィックスを自動生成できればさらに手抜きができるのだが、あまりいい方法を思いついてない。ロールシャッハ的な適当な左右対称キャラなら作れそうだが、それだけでは使い勝手は良くなさそう。今はごく少数の四角の組み合わせをキャラにしているが、とりあえずこれでしのぐか。