Admittedly something small.

ちょっと小さいのはたしかですが。

モナドのまほう 第1話『画像が表示できました』

2016年10月29日 ( 8年前に投稿 )

JavaScript purescript Firebase 関数型プログラミング モナドのまほう

shot1.png

shot2.png

※画面は開発中のものです。実際の製品とはぜんぜん違います。

各話一覧

デモとリポジトリ

  • ~~https://aratama.github.io/cubbit/ ←現状のデモはこっちです。BGMが流れるのでボリュームを最大にして爆音でお聴きください。わりと読み込みに時間がかかるので、画面下のプログレスバーが伸びるまで気長にお待ちください。Chrome推奨です。FirefoxやEdgeでもたぶん動きますが、Firefoxだとホイールの挙動が違う問題があるのと、Edgeでも一人称視点モードでPointer Lockがちゃんと効いていないようです。WASDで移動、スペースでジャンプ、マウス右ドラッグやホイールで視点の変更です。画面下にあるホットバーのボタンを押すとブロックを置くモードになり、マウスクリックでブロックを置いたり取り除いたりできるようになります。~~ 申し訳ありませんがちょっと調整中です。というか、ちょっとアレがアレで開発が中断されています。

  • https://github.com/aratama/cubbit ←コードのリポジトリはこっちに移動です

というわけで、オンラインゲームを作ることにします!:innocent:

現状のコンセプトはこんな感じです。開発当初の目論見からずいぶん変わりました。

  • 開発途中でもワクワク感がありそうなので、プレイヤーが広大なフィールドを自由にうろうろできるオープンワールド:globe_with_meridians:でマインクラフトっぽいサンドボックスゲーム:golf:な感じで! 周囲数十キロメートルを自由に散策できます。地下や天空も数キロの高度まで移動可能。まだ地形生成が単純なので、どこまで行っても同じような風景ですが。
  • 簡単にアクセスできるように、ブラウザで動きます
  • グラフィックスはWebGL:gem:を使ったガチ3Dだよ! 3Dのゲームは作るのが大変ですが、babylonjsがきっとなんとかしてくれる! キャラクターの3DポリゴンモデルもBlender:art:で筆者が自分で作ってるよ!
  • Firebase:fire:を使ってマルチプレイヤーなオンラインゲームにします!(無謀) オンラインゲームは大変ですが、Firebaseがきっとなんとかしてくれる!
  • コードはPureScript:innocent:という純粋関数型プログラミング言語で! この筆者がJavaScriptなんかで書くわけないだろ! いいかげんにしろ!

このエントリはゲーム制作過程の日記的なものです。今回はとにかく自分が:sparkles::sparkles: $ {\bf {\Huge \color{orange}楽\color{lime}し\color{aqua}く} }$ :sparkles::sparkles:開発するのが目標! ゲームを作りたくなったから作る! Firebase使ってみたいから使う! PureScriptが好きだから使う! WebGL使いたくないけど使う! そんな感じで、このエントリは勢いだけで書きます! ついてこれる奴だけついてこいッ!!:smiling_imp::smiling_imp::smiling_imp:

~~ちなみにプロジェクト名がzombieなのはゾンビが出てくる的なゲームにしようかと検討しているからです。どうせみんなゾンビ好きでしょ!(偏見)。某潜入アクションゲームだって遂にゾンビアクションゲームになっちゃったし。まあ行き当たりばったりに作っているので、ゲームの方向性はまたそのうち変わると思います。~~ 開発当初に比べてもう完全にゲームの方向性が変わっていて、メルヘンな感じになってます。どこに向かっているのかは筆者にもよくわかりません。

プログラムのエントリポイント!:zap:

プログラムはmainから始まりmainに終わる! 基本中の基本!

main = do
    pure unit

よし出来た! ちなみに、pure unitというのは『何もしない』ってことです。ここまで書けたら、とりあえずビルドできることを確認! よしOK! 次っ!

画像の読み込み!:camera:

次はタイルチップ用の画像を読み込みます。今使っているライブラリではwithImageという関数で画像を読み込めるので、これを使いましょう。最初に引数に画像のパス、ふたつめの引数に読み込んだ画像を受け取るためのコールバック関数を渡せばOK!

main = do
    withImage "grass.png" \grass → do
        pure unit

よし出来た!grass.pngという芝生っぽいタイルチップ画像を読み込んで、画像オブジェクトを変数grassで受け取っています。ここでやっているのは、JavaScriptで言えばこんな感じのコードです。

widthImage("grass.png", function(grass){
    return;
});

PureScriptなのでやけに見た目がスカスカしてますが、落ち着いて読めば別に難しくもなんともないですね。ちなみに読み込んだgrass.pngはこれ:

grass.png

筆者がpixlrで10秒で描きました。超ハイクオリティ!:v:

Canvasオブジェクトの取得!

次は画像を描画するためのキャンバスオブジェクトを取ってきましょう。このライブラリにはgetElementByIdの簡単なラッパgetCanvasElementByIdが付属しているのでこれを使います。canvasというID(直球)を文字列で指定して呼び出すだけです。

canvasMaybe ← getCanvasElementById "canvas"

これでgetCanvasElementByIdの結果が変数canvasMaybeに代入されます。それだけです。え?変な矢印は何かって?PureScriptは変数に代入するときに<-のほかにも使えるんですよ。なんか見た目が可愛いので筆者は最近を使うのがお気に入りなんです。Qiitaの構文ハイライトで思いっきり駄目出しされてますけど。

場合分け!

しかしここで筆者は大ピンチ!:fearful:なんとgetCanvasElementByIdの結果canvasMaybeはキャンバスオブジェクトそのものではありません。getElementByIdとは別モノじゃないか! 指定したIDのcanvas要素があるかどうかはわからないので、取得が成功した場合と失敗した場合で場合分けをしなくてはならないのです。場合分けにはcaseを使います。

case canvasMaybe of
     Nothing → pure unit
     Just canvas → pure unit

よし出来た!失敗した時はNothing、成功した時はJustのほうが実行され、Justのすぐ後ろにある変数canvasにキャンバスオブジェクトが入っています。まだ分岐後に何もしていない(pure unit)ですけどね。JavaScriptだと別に場合分けの必要はないですし、PureScriptのほうがちょっとだけ面倒ですが、ここは三匹の子豚精神:pig:で乗り切ります。生き残るのは苦労して<ruby>:wolf:<rt>エラー</ruby>に備えた奴なのです!<ruby>:wolf:<rt>エラー</ruby>への備えを怠る:pig:は食われてしまえばいいのです。

標準出力!

さて、エラーを握りつぶしてしまってはいけないので、エラー時にはコンソールにエラーの内容を出力しておきましょう。コンソールに文字を書くには、log関数を使います。要するにconsole.logと同じものです。

case canvasMaybe of
    Nothing → log "canvas not found."
    Just canvas → pure unit

よしできた!それだけ!:grinning:

グラフィックコンテキストの取得!

今使っているライブラリはHTML5 Canvas APIの薄いラッパなので、APIもほとんど同じです。キャンバスオブジェクトを取得できたら、次はグラフィックコンテキストを取得しましょう。JavaScriptではcanvas.getContext('2d')というようにgetContextメソッドを呼ぶところですが、よく考えたらPureScriptはオブジェクト指向プログラミング言語じゃない……!:scream::scream::scream: それじゃあcanvasオブジェクトのgetContextメソッドを呼べないじゃないですか! もうだめだぁ……おしまいだぁ……:sob:でも諦めるのはまだ早い! 実は普通にgetContextメソッドのラッパであるgetContext2D関数を呼ぶだけでOK!

context ← getContext2D canvas

なあんだ。え?これじゃあ語順的にcanvasgetContextメソッドを読んでいるように見えない?だったら#演算子を使えばいいんです!

context ← canvas # getContext2D

ほら!語順がひっくり返った! これで『canvasgetContext2Dのメッセージを送っている』とか言い張れるでしょ! #演算子はほんとに関数と引数の位置をひっくり返すだけのための演算子です。PureScriptは演算子も自由に定義できるので、こんな工夫も簡単です。

まずは画像をひとつだけ描画してみる!

次はdrawImageで画像を描画しましょう:sunglasses:

drawImage context grass 0.0 0.0

よしできた!これでプログラムを実行すると、左上に画像が表示されるはずです!

ss.png

よっしゃああああああああ!:flushed::flushed::flushed: きたあああああああ! 『純粋関数型』なんていう変なジャンルのプログラミング言語でもグラフィックスのプログラミングできてるああああああ!:smiling_imp::smiling_imp::smiling_imp: ちなみに画面サイズは1280 * 720ピクセル固定にしてます。

繰り返し!

芝生一枚じゃあまりに寂しい:disappointed:ので、繰り返し描画して、もっとたくさん縦横に並べましょう。Javascriptで言えば、forループを使ってこんな感じをやりたいわけです。

for(var y = 0; y <= 15; y++){
    for(var y = 0; y <= 15; y++){
        context.drawImage(grass, 36 * x, 36 * y);
    }
}

でも……噂によれば関数型言語にはループがなくて、繰り返しには再帰を使うらしいじゃないですか……。ひいいいい! 再帰とか難しすぎる:scream::scream::scream:……ダメだぁ……お終いだぁ……。でも諦めるのはまだ早い! PureScriptにはfor関数という見た目も使い方もfor文そのまんまの関数があります。for文というよりは、イメージ的にはJavaScriptのArray.prototype.forEachメソッドに近いでしょうか。コードはこんな感じ。

for (0 .. 15) \y -> do
    for (0 .. 15) \x -> do
        drawImage context grass (36.0 * toNumber x) (36.0 * toNumber y)

よしできた!:smile:0 .. 150から15までの16個の整数が格納された配列を表します。これに対してfor関数で繰り返しを行っているわけです。

PureScriptだとtoNumberで型を明示的に変換しなくちゃいけないのがちょっと面倒ですが、ここも三匹の子豚精神:pig_nose::pig_nose::pig_nose:で乗り切ります。実行するとこんな感じに表示されます。

ss2.png

いやっっほおおおう!:cactus:ふっさふさ!:cactus:かなり草がふっさふさだよ!:cactus::cactus::cactus: 関数型プログラミングではあんまりループを使わない(?)と主張する人もいます。確かにfor関数はあくまで関数であってループではないものの、コードの見た目や考え方としてはforループそのものです。少なくとも筆者はfor関数はforループの代わりくらいにしか思ってません。別に関数型プログラミングだからってそこまで脳みそを大幅に切り替える必要はないのです。

つーか誰だよ!純粋関数型プログラミング言語は副作用を扱うのが難しいって大嘘をぶっこいた奴は!キャンバスに画像を描画するとか思いっっっっっきり副作用だけど、実際にコードを書いてみるとJavaScriptと大して変わらねーじゃねーか! さっきも平気でコンソールに文字書いてたし! これじゃあオブジェクト指向が絡んでくるJavaScriptのほうがよっぽど難しいだろ!:rage:

ここまでの全体像!

main = do
    withImage "grass.png" \grass → do
        canvasMaybe ← getCanvasElementById "canvas"
        case canvasMaybe of
            Nothinglog "canvas not found."
            Just canvas → void do
                context ← getContext2D canvas
                for (0 .. 9) \y → do
                    for (0 .. 9) \x → do
                        drawImage context grass (36.0 * toNumber x) (36.0 * toNumber y)

モジュールのインポートの部分とかは省略しました。まだ10行です。

今後の方針ですが:chicken:

開発方針は完全に勢いだけ&いきあたりばったりです! またまだ続きます! 最初はもっと簡単なミニゲームを作ろうかと思ったんですが、そういう簡単なゲームって正直遊んでも一瞬で飽きるんですよね。ゲームを作るからには、ある程度は継続的に楽しく遊べてほしいわけです。その点、こういうオープンなワールドなゲームなら、後付けで幾らでも機能を実装し続けられます。それに、ゲームとしての機能やバランスがガバガバでも、フィールドをうろうろしているだけで何となくワクワク感がありますからね。何より作っていて楽しいです!:chicken::chicken::chicken:

次のお話:chicken:

(この記事は同じ筆者が Qiita に投稿した記事の複製です。オリジナル記事)