Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
168 lines (100 sloc) 14 KB

core.asyncと「おしいれクエスト」

これは Clojure Advent Calendar 2014 の九日目の記事です。

Abstract

記事を全部読むのが面倒な人向けの要約です。

  • 今回の記事の為に作成したブラゲ(ブラウザゲーム)が http://vnctst.tir.jp/op0010/ にあります。タイトルの「おしいれクエスト」とはこれの事です。最後まで遊ぶ必要はないので、とりあえずどんな感じに動いているかだけ簡単に見ておいてください。

  • 上記ゲームのソースコードは src/cljs/op0010/core.cljs にあります。

    • このファイル内のmain関数に、このゲームのシステム本体があり、そこを見てもらえれば core.asyncを使えば、このように比較的素直なコードでゲームのロジックが書き下せる』 という事が分かってもらえると思います。筆者がこの記事で訴えたい一番のポイントはここです。
    • ゲームの状態遷移は、よくあるwebサービスや普通のアプリケーションソフトの状態遷移とは比べ物にならないぐらい、とんでもなく複雑になる事が多いです。しかし、core.asyncを上手く使えば、上記のように、その複雑さをある程度軽減する事ができます。
    • ちなみに、(限定)継続やモナドを使う事でも、このような用途(低レベルではバラバラに処理されなくてはならない処理を、コード上では素直に書き下す)に利用する事ができます。core.asyncはそれらとは全然違うアプローチ(チャンネルと非同期処理))なのに、なぜかこの用途が実現できてしまっている、というのが個人的に面白いポイントでした。
  • 今回の記事ではcore.asyncの解説はあまりしません。既に分かりやすい解説文書があるので、そちらを読むとよいでしょう。

  • 「おしいれクエスト」ではUIの構成に、外部JSライブラリとしてsweetAlertを利用しています。

    • sweetAlertがどんなものかは、 http://tristanedwards.me/sweetalert にあるデモを見てください。要はJavaScript標準のalert() confirm()の代替品です。
    • 現バージョンのsweetAlertでは以下の点に注意が必要です。
      1. 本物のalert() confirm()とは違い、制御(継続)がその場で止まらない
        • これについてはcore.asyncで適切にラッピングする事で使い勝手を改善できる。どのようにラッピングしているのは実際のコードを参照
      2. モーダルダイアログっぽさがいまいち不完全で、マウスからの操作こそきちんと遮断されているものの、キーボード操作(tabキーで移動して決定する等)の遮断がきちんと実現できてない
        • この問題があるので、一部の人しかさわらない管理画面とかで使うなら問題ないが、不特定多数の人がさわるようなwebアプリには、現段階のsweetAlertは向いていないように個人的には思っている
        • これについては仕方がないので、とりあえず今回の「おしいれクエスト」では「コンテンツ本体を空にし、ダイアログのみで内容を構成する」という手法で問題自体を回避した
      3. カスタマイズの自由度はあまりない
        • 指定画像サイズ変更の引数があるが、なんか上手く動いてない
        • もちろん、jsソースやcssをいじれば好きにはできるが…
  • 「自分も しょぼい ブラゲを手早く作りたい!」という人がいれば、このリポジトリをcloneして改造する事ができます。

    • シンプルな作りなのと、作成手順を以下に書いたので、Clojureプログラマであればcljs固有のノウハウ等はなくてもいじれるかもしれません。
    • ライセンスについてはREADME.mdを参照してください。

以上です。

Introduction

こんにちわ。ここ最近は株式会社テンクーにて、Clojureの仕事をさせてもらっている山田と申します。

以下では、実際に「おしいれクエスト」を作成した手順を示していきます。

プロジェクトの作成

まず、cljs向けの project.clj を用意する。

きちんとした本格的なテンプレを用意したい場合は、chestnutのがよい(らしい)

また、自分が以前に烏丸Clojureでスライドを作った時に一緒に書いたものが http://doc.tir.ne.jp/misc/karasumaclj/project.clj にある。

が、今回はどちらも使わず、極力シンプルなものを用意した。

実際の内容については、同梱の project.clj を参照。

index.html

今回は極力シンプルに行きたいので、ring等のhttpdも使わず、直接 index.html を用意する事にした。

  • resources/public/index.html にある。
    • この中で参照している sweet-alert.csssweet-alert.js は、sweetAlertの配布物から取ってきた。
    • cljs.js は前述の project.clj にてビルドされる。

上記の resources/public/ という場所は、 ring でhttpdを動かした時のデフォルトのコンテンツ置き場になる。今回はringは使わないが、一応揃えておいた。

  • よって、 index.html 以外に置きたいファイルがある場合も resources/public/ 内に置けばよい。最終的にデプロイする時には、このディレクトリをデプロイする形になる。
    • 今回、ゲーム内で利用する画像は resources/public/assets/img/ 内に置く事とした。その結果、これらの画像をindex.htmlから参照する場合は assets/img/hoge.png 等のように指定する事になる。

ビルドプロセスの起動

本体のソースは src/cljs/op0010/core.cljs に書く。

  • index.html の中に <body onload="op0010.core.main();"> と書いてあり、これをClojure流に翻訳するならば「(op0010.core/main)」が実行されるのに相当する。よって、前述のsrc/cljs/op0010/core.cljs内にmain関数を書く、という事になる。また、このmain関数には「JavaScript側から参照される」印として、^:exportメタ属性を付与しておく。

別コンソールを開き lein cljsbuild clean && lein cljsbuild auto を実行しておく。これで、上記のソースファイルがエディタから更新される毎にcljs.jsが動的に生成され直すので、あとはindex.htmlをブラウザで開いて適当にリロードしつつ動作を見ながら、インクリメンタル開発を行う。

  • このビルドプロセスだが、マクロ追加だか何だかのタイミングでコンパイル等がおかしくなるっぽい事があるので、たまにctrl-Cで停止させて再度 lein cljsbuild clean && lein cljsbuild auto を実行し直した方がよい(この為に、最初に明示的にcleanするようにしている)。
    • このあたりのバッドノウハウは、以前に http://vnctst.tir.jp/karasumaclj/ にも少し書いているので、興味のある人は目を通しておいてもよい

なお、前述のchestnutを使えば、いちいちブラウザをリロードしなくても反映されたり、普通にnREPL経由でエディタからページに対してREPL接続が使えたりする。が、今回はchestnutは使っていないので、REPLなしでの開発手順となっている。

sweetAlertを呼ぶラッパーを書く

今回はsweetAlertだけを使ってUIを作成する。 しかしそのままでは扱いづらいので、以下の仕様のラッパー関数を用意する。

  • 引数としてmapを受け取り、デフォルト値のmapにmergeしたものをjs-obj化してからsweetAlertに渡す
  • 実行結果として、core.asyncのチャンネルを返す。

この関数には swal$ という名前を付けた。

ゲーム本体を作成

あとは実際にゲーム自体の内容を作っていくだけだ!

一番大変な部分だが、この部分はもはやClojureとは全然関係ないので、この記事では省略。

ここで、core.asyncを最大限に利用する。

  • goブロックがチャンネルを返す」という性質が意外と重要で、この性質によって「別のgoブロックの返り値」を<!で素直に受け取る事ができる。
  • 逆に、core.asyncの弱点として「goブロックの中で気軽に関数を生成できない」という点に注意する必要がある。goブロックの中であっても、その中の(fn [] ...)ブロック内では<!等が直接は利用できないからだ。(fn [] (go ...))のよう別のgoブロックにする必要がある(そして<!で受け取る必要がある)。
    • 上記のようなコード(defnfnのすぐ内側にgoを書くコード)は多いので、go-loopのようにマクロ化してもよいかもしれない

デプロイする

ゲーム本体が完成し、動作確認も一通り終わったなら、完成したゲームをどこかのwebサーバに設置して公開する。

基本的には先に書いたように resources/public/ 配下を丸ごとコピーするだけだが、その前にcljsbuildの最適化オプションを変更しておいた方がよい。

  • 具体的には以下の手順を実行する

    1. ビルドプロセスが起動しているなら、停止させておく
    2. project.clj:optimizations :whitespace #_:simple になっているところを :optimizations #_:whitespace :simple に変更する
    3. lein cljsbuild clean && lein cljsbuild once を実行する
    4. resources/public/ 配下を丸ごとデプロイ先にコピーする
    5. デプロイが完了したので、(2)で変更したproject.cljを元に戻す
    6. 念の為 lein cljsbuild clean も実行しておく
  • 上記の手順にて、いちいちproject.cljを書き換えているが、これは本来であればprofileを分けるべき。今回は極力シンプルにする方針にした悪影響でこうなってしまったが…

  • なお、:optimizationsオプションにはもっと最適化する:advancedオプションもあるのだが、こっちは:externs指定がほぼ必須になったり、色々と大変なので最初の内はおすすめしない。

完成?

作成手順はこんな感じです。 core.asyncを使う事で、ゲームのロジックを素直に書く事ができました。

しかし「簡潔に書ける事」と「ゲームの面白さ」には、直接の因果関係はないのです。 なんということでしょう。

(一応、簡潔に書ければその分だけゲーム内容の調整に使える時間が増える等の間接的な効果はあるでしょうけど。)


なお、この後は以下の選択肢があります。

  • ゲーム内容のボリュームを増やしたり、ゲーム内容の完成度を上げたりする
  • SEやBGMをつけたり、セーブ機能をつけたりの改善をしたりしてみる。sweetAlertを、もっときちんとしたUIを提供しているライブラリに変更したりしてみる
  • 次回のニコニコ自作ゲームフェスが始まったら登録してみる。他のゲーム系イベントも探して参加してみる
  • core.async の実際の使い方が分かったので、別の新しいプロジェクトを作り、また別のゲーム/アプリを作成してみる。cljsに限定する必要はない

おしまい