日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

play-clj のインストール、シンプルなウィンドウの表示

arcade-clj シリーズ 1 つ目。シリーズ全体の目次はこちら。 一部以前の記事と重複している所もあるけどご容赦を。

play-clj のインストール

インストールと言うか leiningen での新規プロジェクト作成時にテンプレートで play-clj を指定するだけで良い。

lein new play-clj arcade-clj

プロジェクトが作成されたら desktop/project.cljClojure のバージョンを 1.8.0 に変更して lein deps すれば Clojure 含め必要なライブラリは全てインストールされる。 Clojure は別にこれと言って新しくする理由もないんだけど何となく。 *1

ひとまず動かしてみる

play-clj は lein run するととりあえず Window が出るまでの最低限のテンプレートを生成するので まずは lein run で動かしてみる。黒いウィンドウが出てきて左下に "Hello world!" と出ていれば OK。

元記事の仕様に合わせる

元記事は、

  1. タイトルに "ArcadeRS Shooter"
  2. 画面解像度 横 800, 縦 600
  3. 真っ黒な Window
  4. 3 秒経ったら自動的に終了する

となっているので合わせる。

まず、1,2 を合わせる為に desktop/src/arcade_clj/core/desktop_launcher.clj を開く。 そこの

(LwjglApplication. arcade-clj-game "arcade-clj" 800 600)

(LwjglApplication. arcade-clj-game "arcade-clj Shooter" 800 600)

とする。解像度は生成されたテンプレートのデフォルトと一致してたのでそのまま。

REPL で動かしてみる

さて、Clojure を使っていて REPL を使わない手はない。 コマンドプロンプトを立ち上げて lein repl :headless として nREPL サーバを起動する。 (cider-jack-in しない理由は後述する)。

次に emacs から desktop/src-common/arcade_clj/core.clj を開き cider-connect(C-c M-c)でさっき起動した nREPL サーバにつなぐ。

そして REPL から

arcade-clj.core.desktop-launcher> (-main)

とタイプし起動。project.clj を見ると分かるが、main は ns が desktop-launcher にあるので 別の ns で実行しても動作しない為注意。実行した結果、lein run した時と同じ画面が出てくると思う。

正直ここまでだと lein run の方が楽じゃないのと思うだろうがここからが違う。

じゃ次に元記事に仕様を合わせるべく次に画面を真っ黒にするためラベルを消してみる。 それは簡単で defscreen:on-show で指定されている label の S 式全体を消してしまえば良い。

  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage)))

こんな感じに。

で、ソースを保存し C-c C-k(cider-load-buffer) した後、nREPL クライアントで namespace を arcade-clj.core に移動した後で以下のコマンドを実行する。

(on-gl (set-screen! arcade-clj-game main-screen))

上記はざっくりと言えばゲーム画面に変更を反映させる為のコマンドになる。 このコマンドを実行すると画面から"Hello World!"はなくなり真っ黒になったはず。

それでもまだ面倒

正直自分が面倒だったのでさっきの手順をひとまとめに実行する emacs のコマンドを作った。 init.el 辺りにこんなコードを入れておく。

(defun play-clj-reload ()
  "REPL へ play-clj への反映コマンドを投げる。
   arcade-clj-game と main-screen は適時書き換える事。"
  (interactive)
  (cider-interactive-eval
   "(on-gl (set-screen! arcade-clj-game main-screen))"))

(defun save->eval->reload ()
  "play-clj での開発用。バッファセーブ→バッファ評価→play-cljのリロードのコンボをまとめて実行する。
   sit-for で wait かましているのはあんまり意味ないかも。"
  (interactive)
  (save-buffer)
  (sit-for 1)
  (cider-load-buffer)
  (sit-for 1)
  (play-clj-reload))
(define-key cider-mode-map (kbd "<f5>") 'save->eval->reload)

これで F5 キーを押すだけで「セーブ→セーブしたバッファの評価→ゲーム画面に反映」まで ひとまとめにやってくれる。

ただこれだとこの記事で作ったプロジェクト以外にする度に書き換える必要があるのが課題ではあるが どうすればもう少し汎用的になるかはまだ調べていない。

emacs の設定書き換えるの嫌なんですが

これはもう emacs から離れて NightMod 辺りを使うしかないと思う。 こちらは先のコマンド登録相当の事を機能として実装してるとの事なので試してみてはどうだろうか。

自分は NightMod 自体を試してない事から比較判断はできない。

最後の機能追加

さて、最後に 3 秒経ったら終了するコードを追加する。

(defscreen main-screen
  :on-show
  (fn [screen entities]
    (update! screen :renderer (stage))
    (add-timer! screen :auto-dispose 3)           ;; (1)
    entities)                                     ;; ??

  :on-timer                                       ;; (2)
  (fn [screen entities]
    (case (:id screen)
      :auto-dispose (app! :exit)))
  
  :on-render
  (fn [screen entities]
    (clear!)
    (render! screen entities)))

これを評価すると 3 秒経った後画面が勝手に閉じる。 元記事と少し違ってタイマーを使って終了するようにした。

(1) で :auto-dispose と言う keyword で 3 秒後に実行されるタイマーを登録し、 タイマーが登録されると (2) の :on-timer イベントハンドラ関数が実行されるようになり、 screen の中にある :id が :auto-dispose ならアプリを終了するコードを追加している。

また、今回の機能追加に直接関係は無いが、?? の部分を補足する。

play-clj は各イベントハンドラ関数の戻り値としては entity が返される事を想定しており、 add-timer! 関数が返す Timer は entity としては扱えないので最後に持ってくるとエラーになる。 play-clj の leiningen テンプレートでは label を返すようになってたが、 これは entity として扱われるので OK だった。 とりあえず今回は怒られないようにする為だけにイベントハンドラで受け取った entities を そのまま返すようにした。中身は何も無いんだけどね。

entity system は play-clj 固有の概念なのだが、この辺りは 実際にもう少しちゃんと扱うようになったらまた改めて説明しようと思う。

play-clj REPL の制限

さて、アプリが自動終了した後 REPL でもう一度 (-main) として起動させようとして nil だけ返ってきてあれ? と思ったかもしれない。これは play-clj と言うか libGDX の制限で、 play-clj のゲーム画面と言うのは OpenGL コンテキストスレッド上で動作するのだが、 OpenGL コンテキストスレッドは 1 つのプロセスに対し 1 回しか起動を許していないという制限がある。

コマンドプロンプトの方を見るとそれらしき事を示す例外が吐き出されている。

Exception in thread "LWJGL Application" java.lang.RuntimeException: No OpenGL context found in the current thread.

自動終了するようコードを追加しておいてアレだが、アプリが終了した後は nREPL サーバを再起動しないともう起動できない。

nREPL サーバは jack-in しないで connect する事のススメ

また、この記事の最初の方でわざわざコマンドプロンプトを立ち上げて nREPL サーバを別に立ち上 げたのには理由があり、cider-jack-in した場合だと先のアプリが終了した時などの 例外を見えるようにする為というのが大きい。アプリ終了後に(-main)としても cider の nREPL クライアント側では nil としか返ってなかった事からも分かると思う。

例外が全く見えなくなってしまうのはさすがに辛いので play-clj で開発する際は コマンドプロンプトで nREPL サーバを立ち上げてから cider-connect する方法を強くオススメする。

また、アプリは終了せずとも :on-render などで大量に例外が吐き出されるような事も 開発中は起きてくるだろう(と言うか自分が体験した)。 例外が少ない場合はまだいいが、大量に例外が出続けると最初の方の例外はコマンドプロンプトだと 流れてこれも見えなくなってしまう。 このような場合は nREPL サーバ起動時にエラー出力をリダイレクトさせておくと良い。

> lein repl :headless 2> err.txt

こんな感じで。

github スナップショット

今回の記事終了時点のソースは以下から取得可能。

Release v0.1.0 · shinmuro/arcade-clj · GitHub

*1:強いて上げれば 1.8.0 で追加された direct linking が play-clj で使えるのか試してみたいくらい