1985年5月29日、そのゲームは、全国のゲーマーに衝撃を与えた。
らしい。
僕はよく知らない。そのときまだ生まれたばかりだったからだ。 そんな僕の小学校にはPC98があったし、中学校にはFM-TOWNS、 高校にはAT互換機があった。
あたりまえのように存在するゲームとプログラミング環境の中で育った少年が、 進化したプログラミング言語からのメッセージを伝えるために、 20周年をむかえたグラディウスと、 ゲームを、ゲームプログラミングを愛する人たちに感謝を込めて贈る。
Monadiusは、そんなたいしたことはないWindows用横スクロールシューティングゲームです。
Haskellで書かれているためゲーム全体がモナド で出来ています。だからモナディウス。
Monadiusは、グラディウス20周年企画であり、Haskellでゲームを作るときの問題点を 洗い出すための実験でもあります。
基本的にグラディウスです。
詳しくはreadmeをごらんください。
MonadiusはGPLライセンスで配布されるフリーウェアです。
実行には、新しめのWindowsとOpenGLが必要です。
なぜHaskellなんかでゲームを作りたがる人がいるのか、 実際にHaskellでプログラムを書くのはどういう感じなのか、 プログラミングをしらない人も意識して説明してみようと思います。
Haskellingの雰囲気を見てみたい人はまず実験編を読んでください。
関数型言語でゲームを書くといろいろ楽になるんじゃないか。 関数型言語は、あまり普及してないし、そのせいであまり使いこなされていないのが現状です。 が、枠組みさえ整えば、ゲームのアイデアを持っている人にとって命令型でやるより ずっと豊かな道具となるはず。
なんですが、詳しいことは自分で実際に何か作ってみないと…。
そんなときグラディウス20周年の話を耳にして、Haskellへの移植を思い立ったのです。 既存のゲームのフィーチャーを一定期間内でひととおりなぞってみることになり、 Haskellならほんとうに速く書けるのか、 どんなことがHaskellではやりにくいのか、 オリジナルなものを作るだけでは分からないことも分かるかと。
いわば、写経です。
Monadiusを作るのに使ったもの。
GHCはHaskellのコンパイラ。 Haskellやりたかったら、いまんところ インタプリタHugsかコンパイラGHCの2択になると 思いますが、GHCにはインタプリタもついてるので通常はこちらがお勧めです。 Windowsな人ならmsiファイル形式のインストーラーをダウンロードして実行するだけ。 これで今日からあなたもHaskeller。
EclipseはIBMが提供するフリーの多目的開発環境です。 デバッグなどが楽になります。
Eclipseを手に入れたらEclipseFPを導入することで Eclipseが関数型言語に対応するようになります。 今のところHaskellとOCamlに対応しているようです。
ハスケル階層ライブラリをしょっちゅう引いていました。 GHCの全機能がアルファベット順と、機能の階層順の2通りで網羅されていて、簡単な解説もついています。 GHCをインストールすればもれなく付いてきます。
しかし辞書だけでは学べないんで、本か、人間の指導者が必要です。 The Craft of Functional Programming はHaskellの手に入れ方や 関数という概念、あたりから丁寧に解説してあるので独習に向いていると思います。
はじめにお断りしますと、以下のコードは説明のために実際のものと若干変えてあるところがあります。
さてHaskellでは、ふだんは設計ノートに書いたり頭の中で覚えておくようなことを、ソースに書いてしまえます。 まず全体の流れをつくってから、足りない詳細を埋めていく、というトップダウンなやり方が自然にできるのです。
というわけでいきなり、ゲームとは何ぞ?を定義しましょう。
--game.hs module Game( Game(..) )where import Graphics.UI.GLUT.Callbacks.Window class Game g where -- gがゲームであるとは、 update::[Key]->g->g -- キー入力にしたがって定まるgからgへの変換によって更新することができ、 render::g->IO () -- gを描画することができ、 isGameover::g->Bool -- gがゲームオーバーかどうか判定することができることである。
ステートマシンがあっさり抽象化できました。renderingというのは3DCGを描くという意味の用語です。
つぎにMonadiusをつくり、それがGameの例になっていることを宣言します。
Monadiusはdata、Gameはclassです。 dataとclassの関係は構造とその具体例の関係です。(行列,行列積)と群の関係、(整数,和,積)と環の関係。
instance Game Monadius where update = updateMonadius render = renderMonadius isGameover = isMonadiusOver data Monadius = Monadius (GameVariables,[GameObject]) updateMonadius::[Key]->Monadius->Monadius renderMonadius::Monadius->IO () isMonadiusOver::Monadius->Bool
Monadiusはデータ的には (GameVariables,[GameObject]) つまり、ゲーム変数(スコアとか)と、ゲームキャラクタ(プレイヤー機とか敵とか)のリストと の組になっています。代数っぽくいうと、そういうデータの一塊であるMonadiusが、 updateMonadius, renderMonadius, isMonadiusOverという演算に関してGameを成す、と宣言されています。
updateMonadius, renderMonadius, isMonadiusOverの型はclass Gameの条件を ちゃんと満たしていますね。
キー入力に対しMonadiusというゲームがどのように振舞えばいいか、updateMonadiusの中身を書きます。
updateMonadius::[Key]->Monadius->Monadius
updateMonadius keys (Monadius (variables,objects))
= Monadius (newVariables,newObjects) where
newObjects = -- キー入力後のゲームのキャラクタたちは
(issueTag. -- キャラ識別タグを発行する
(loadObjects ++). -- 新たに登場するキャラを読み込む
(filterJust.map scroll). -- キャラの位置をスクロールさせる
concatMap updateGameObject. -- それぞれのキャラを運動させる
collide) -- キャラ同士の衝突を判定する
objects -- らの合成関数を入力前のゲームのキャラクタにかけたもの
newVariables = variables{ -- キー入力後のゲームの変数たちは
... -- 適宜更新(^^;)
} .(ピリオド)は関数合成をあらわす演算子です。 いかにも関数型言語らしい表現ですね。 このあと、合成されてる関数のそれぞれ中身を書かないといけないですが、これ以上の詳細はちょっと退屈なので省略。
Monadiusというゲームを画面に描く、renderMonadiusのほうを見てみましょう。renderMonadius::Monadius->IO () renderMonadius (Monadius (variables,objects)) = do mapM_ renderGameObject objectsすっきりしてます。それぞれのオブジェクトを描くだけですね。
ここでHaskellは初めてだけどプログラミング言語は知っているという方々に、 このすっきりしている1行が実はいかに凄いかをお教えしましょう。上のソースコードで、
このように、Haskellなどの関数型言語では、命令(ソースコードの地の文と思って。)も値の一種であり、関数に渡し、関数から返し、変数に代入し、クラスそのほかの要素とし… などが自由にできます。ソースコードを綺麗にするべく特殊な構文が大量に用意されているのではありません。
- objects はゲームオブジェクトのリストです。
- renderGameObject はゲームオブジェクトを引数にとり、オブジェクトを描画する命令を返す関数です。
- map は与えられた関数をリストの各要素に適用し、結果のリストを返す関数です。
- mapM_ はmapして、(出てきたものが命令のリストであることを前提に)命令のリストを結合してひとつの命令にする関数です。
- renderMonadius は、総じて、ゲームMonadiusのある時刻の状態を引数にとり、ゲームを描画する命令を返す関数です。
int値を整理するためのあらゆる方法が、そのまま命令を整理するために使えるので、ソースコードは構造化され、読みやすくなるのです。
ではMonadiusというゲームの終了を判定する、isMonadiusOverは。
isMonadiusOver::Monadius->Bool isMonadiusOver (Monadius (variables,objects)) = flagGameover variablesflagGameoverは、variablesというゲーム変数の総体から、ゲームオーバーかどうかの情報だけをとりだす 射影演算子です。 Monadius (variables,objects) がゲームオーバーなのは、 flagGameover variablesが真のときだと。
モナディウスにはもうひとつGameの例が含まれています。リプレイ記録装置です:
instance Game Recorder where update = updateRecorder render = renderRecorder isGameover = recorderIsGameover
こうやって書いてから、updateRecorderなどの内容をあとで書きます。
ある程度書きかけのソースコードをGlasgow Haskell Compilerに渡すと、 未定義の関数や、型エラー(物理でいえば、左辺と右辺の単位が違っていること)を調べてくれます。 これを参考にしながら、どんどん定義を埋めていきます。
いかがでしょう。Haskellは、あなたが抱いていたプログラミング言語のイメージとは違ったかもしれませんね。 僕は、Haskell書きは、直感的に明らかなたぐいの数学の定理を フォーマルに証明する過程に似ている感じがします。
コンパイラーが警告を出さなくなるまで定義を詰めれば、プログラムは完成します。
完成したもの:
今回は問題点が挙がればいい、解決するのは次以降という実験なので、問題点はほったらかしです。 あまり考えて書いてないので、関数型言語といいながら中身はほとんど古き悪しきステートマシンです。 決してソースを見て、模範的なHaskellのコードだと思わないように(誰も思わない)。
発見:
課題:
Haskellでもどうにかゲームは作れるぞということは示せましたが、 実行してたら遅くなるとかいった、Haskellの仕組みをよく知らないと分からない問題はお手上げです。 もっともっとゲームを作りやすくする方法があるはず。 つぎは、みんなにまかせた!
スポンサードリンク