Three.jsなWebアプリをCommon Lispで書く話

前書き

Lisp Advent Calendar 2016の20日目の記事です。

13日目の記事「フロントエンドもサーバーサイドもCommon Lispで書く試み - @peccul is peccu」のタイトルを見た瞬間「あっ」と思ったのですが、テーマがダダ被りです。しかも、pecculさんの方はメンテされているjsclをベースにしている一方で、こちらはメンテされていないParenscriptをベースに頑張っ(てしまっ)た記事です。

冒頭から残念感あふれますが、jsclに乗り換えても(未定)考え方は使えると思い、気を取り直して進めます。さて、こんなものを作りました。

f:id:eshamster:20161218174621p:plain

斑鳩という素敵STGの3面中ボスである「鴫(シギ)」に関するシミュレータ(絶賛未完成)ですが、Qiitaから来られた方の10割はなんのことか分からないと思うのでこれ自体の話はまた別に記事を起こせたらと思います(ちなみに、Twitterから来られた方の8割はなんのことか分かると思います)。

ポイントはThree.js(WebGLを簡単に利用するためのライブラリ)を利用したWeb2Dゲームアプリであり、サーバサイドからフロントエンドまで*2Lispで記述されているという点です。これを実現するためにどのようなベースを作り、その上でどのような開発サイクルを回していったかが記事の焦点です。

一応動かし方

github.com

これを動かすこと自体に興味のある人がどれだけいるか甚だ疑問ですが、一番簡単なのはDockerHubに上げてあるイメージを使うことです。

$ docker pull eshamster/app-cl-shigi-simulator
$ docker run -p 5000:8080 -d eshamster/app-cl-shigi-simulator

これでhttp://localhost:5000でアクセスできるはずです(docker runコマンドはすぐに返ってきますが、アクセス可能になるまでに10~20秒ほどかかります)。

開発環境にロードする場合は次のような感じです。quicklispに登録していない自作ライブラリに依存しているため、qlot installgithubから拾わないとql:quickloadできません…*3

# bash側
$ ros install eshamster/cl-shigi-simulator
$ cd .roswell/local-projects/eshamster/cl-shigi-simulator/
$ ros install qlot
$ qlot install
----
;; REPL側
$ (ql:quickload :cl-shigi-simulator)
$ (cl-shigi-simulator:start :port 5000)

概要

次のような階層で実現しています。

それぞれのライブラリ(自作物のみ)

上のライブラリを使ってどのように開発を回してきたかという話を淡々と書いていきます。

caveman-skeltons

Caveman2は非Lisperとの協業を視野に入れており、フロントエンド側は無理にLispにされていません。デフォルトのテンプレートエンジンとしてはDjulaを採用し、JavaScriptについてはそのままであり、必ずしもLispに親しくない人を驚かさないようになっています。

が、一人で書く分には全部Lispでも問題ありません。殊に今回のようにサーバエンドが軽い場合はただのJavaScript開発になってしまうので悲しい限りです。というわけで、Caveman2のスケルトンを下敷きにフロントエンド側もLispで記述するスケルトンを用意したものがcaveman-skeltonsです。

Gitのブランチで複数のスケルトンを管理しています*4。必要なブランチに切り替えたのち、以下のようにしてスケルトンからプロジェクトを作成します(この作成操作は純正のCaveman2と基本同じです)。

> (ql:quickload :caveman-skeltons)
> (caveman-skeltons:make-project #p"/path/to/project")

現状3つ(事実上2つ)のブランチがあります*5

  • master: これはCaveman2そのまま
  • with_cl_markup: Djulaに代わり、CL-Markupをテンプレートエンジンに採用したスケルト
  • with_parenscript: with_cl_markupをベースに、さらにJavaScript側をParenscriptで書くための準備を施したスケルト

以下はマニュアルの英語もどきに少し肉付けしたものです。

まず、HTML部分の開発サイクル(新しいページの追加)は次の流れです*6。(with_cl_markupブランチ or with_parenscriptブランチ)

  1. <プロジェクト名>.asdの編集:"templates"モジュールの下にテンプレート名を追加
  2. Lispファイルの追加と編集:templates/<テンプレート名>.lispを追加
    • パッケージ定義:<プロジェクト名>.templates.<テンプレート名>の名前で作成
    • render関数の作成:HTMLコードを文字列として返す関数
      • このHTMLコード作成のためにCL-Markupを使う想定
      • 引数は任意
  3. テンプレートを利用:Caveman2でルーティングの定義を行うsrc/web.lispで作業
    • <your project name>.view:render関数を利用
      • (render :<テンプレート名> <引数(あれば)>)

templates/index.lispがこのサンプルになっています。テンプレートエンジン感を出すため、templates/layouts/default.lispで定義したデフォルトのテンプレートを利用するという形をとっています。このwith-default-layoutはただのマクロなので、必要であれば引数などは好きに追加できます。

;; templates/index.lisp
(in-package :cl-user)
(defpackage <% @var name %>.templates.index
  (:use :cl
        :cl-markup)
  (:import-from :<% @var name %>.templates.layouts.defaults
                :with-default-layout))
(in-package :<% @var name %>.templates.index)

(defun render ()
  (with-default-layout (:title "Welcome to Caveman2")
    (:div :id "main"
          "Welcome to " (:a :href "http://8arrow.org/caveman/" "Caveman2") "!")))

これを使う側(ルーティング側)は次のような感じです(src/web.lisp抜粋)。

(defroute "/" ()
  (render :index))

次はJavaScript側の開発サイクルです。(with_parenscriptブランチ)

  1. <プロジェクト名>.asdの編集:"static/js"モジュールの下にファイル名を追加
  2. Lispファイルの追加と編集:static/js/<name>.lispを追加
    • パッケージ定義:<プロジェクト名>.static.js.<name>の名前で作成
    • js-main関数の作成:JavaScriptコードを文字列として返す関数
      • このJavaScriptコード作成のためにParenscriptを使う想定
      • 引数はなし
  3. テンプレート側での読み出し:<プロジェクト名>.static.js.utils:load-js関数を利用する
    • (load-js :<name>)
      • static/js/_<name>.jsを作成する
      • 返り値は文字列'_<name>.js'

static/js/index.lispJavaScriptコードを作成する側の例です。

(in-package :cl-user)
(defpackage <% @var name %>.static.js.index
  (:use :cl
        :parenscript))
(in-package :<% @var name %>.static.js.index)

(defun js-main ()
  (ps (alert "Hello Parenscript!!")))

これを使う側はtemplates/index.lisp @ with_parenscriptのようになります(load-jsしている部分)。

(in-package :cl-user)
(defpackage <% @var name %>.templates.index
  (:use :cl
        :cl-markup)
  (:import-from :<% @var name %>.templates.layouts.defaults
                :with-default-layout)
  (:import-from :<% @var name %>.static.js.utils
                :load-js))
(in-package :<% @var name %>.templates.index)

(defun render ()
  (with-default-layout (:title "Welcome to Caveman2")
    (:div :id "main"
          "Welcome to " (:a :href "http://8arrow.org/caveman/" "Caveman2") "!")
    (:script :src (load-js :index) nil)))

ちなみに、load-jsこんな感じです(関連関数は一部のみ抜粋)。

(defun write-to-js-file (name)
  (with-open-file (out (make-js-full-path name)
                       :direction :output
                       :if-exists :supersede
                       :if-does-not-exist :create)
    (format t "(re-)load js: ~A" name)
    (format out
            (funcall (intern "JS-MAIN"
                             (string-upcase
                              (concatenate 'string
                                           "<% @var name %>.static.js."
                                           name)))))))

(defun load-js (js-name &key (base-path nil))
  (check-type js-name keyword)
  (let ((name (string-downcase (symbol-name js-name))))
    (when (or *force-reload-js*
              (is-js-older name))
      (write-to-js-file name))
    (make-js-load-path name base-path)))

一応is-js-olderでファイルの新旧を見てコンパイルするか判断などやっているのですが、開発中は*force-reload-js*をずっとtにしています*7。上記サイクルの2番目でプロジェクト名を指定している理由はwrite-to-jsにあります。export, importの手間を省くためにパッケージが上記の命名に従っていることを仮定してjs-mainを呼び出すということをしています*8

ps-experiment

ps-experimentはParenscriptの不便だと思ったところを気まぐれに拡張しているライブラリです。

ここまでの開発サイクルに関する話題としては、パッケージもどきシステムを備えている点が重要です。上記のwith_parenscriptテンプレートの利用方法ではParenscriptコードの複数ファイルへの分割方法に言及していませんが、そこを補うものになってきます。

helloという関数を別のパッケージ(ファイル)で作成して一緒にロードする例を下記に示します。なお、通常のCommon Lisp開発と同じくsome-packageの方も.asdファイルに追加しておく必要があります。

;; static/js/some-package.lisp
(in-package :cl-user)
(defpackage sample.static.js.some-package
  (:use :cl
        :parenscript
        :ps-experiment)
  (:export :hello))
(in-package :sample.static.js.some-package)

;; 普通のdefunのように関数を定義
(defun.ps+ hello (name)
  (concatenate 'string "Hello " name "!"))
;; static/js/index.lisp
(in-package :cl-user)
(defpackage sample.static.js.index
  (:use :cl
        :parenscript
        :ps-experiment
        :sample.static.js.some-package))
(in-package :sample.static.js.index)

(defvar.ps+ *my-name* "eshamster")

(defun.ps+ main ()
  (hello *my-name*))

(defun js-main ()
  ;; def~.ps[+]で定義したものも含めてJavaScriptを出力
  (pse:with-use-ps-pack (:this)
    (alert (main))))

主なポイントは次の通りです。

  • def~.ps+は同等のCommon Lispマクロdef~と同じように使えます
    • def~.ps+Common Lisp用の定義とJavaScript用の定義を同時に行います
      • 可能な限りこちらを使っておくと、シンボルの参照や関数の引数チェックなどCommon Lisp相当のコンパイル時チェックができて嬉しいです
    • def~.ps+がない)バージョンはJavaScript用の定義だけを行います
      • JavaScriptのライブラリに依存している部分や、Parenscriptやps-experimentで未対応であるためにCommon Lispままではコンパイルできない部分は止むを得ずこちらを使う感じです
    • defun, defvar, defmacro, defstructがこの形で利用できます
  • def~.ps[+]で定義したものはwith-use-packageでまとめてJavaScriptコードとして出力します
    • 第1引数で指定したパッケージに属する定義が対象です
    • 指定がなくとも、useしているパッケージは再帰的に探して定義を出力します(上記で:sample.static.js.some-packageを省略できるのはそのため)*9
    • また、:thisは自身(上記では:sample.static.js.index)のエイリアスです
  • js-mainは上で解説したものです
  • 残念ながらJavaScript側では名前空間を分けることができていません…
    • 単純にグループ化しているに過ぎないので「パッケージもどき」と言っています

js-main関数を直接呼んでみると下記のようなJavaScriptコードが(文字列として)出力されます。

function hello(name) {
    return 'Hello ' + name + '!';
};
var MYNAME = 'eshamster';
function main() {
    return hello(MYNAME);
};
alert(main());

細かい部分の話。

  • def~.ps[+]は共通して各単位でのコンパイルが可能です。つまり、SlimeであればC-c C-cで定義を更新できます
    • といっても、ブラウザ側へ反映させるためには、さらにブラウザ側でのリロードが必要になってしまいますが
  • 出力されるJavaScriptコードを確認する一番手っ取り早い方法はdef~.ps[+]に対するマクロ全展開(SlimeでC-c M-m)です。周囲に直接関係のないCommon Lispコードも出てしまいますが、JavaScript部分は文字列としてまとまっているので、目視で見分けるのは簡単です。
  • 上記以外も含めps-experimentは全体として以下のような機能を持ちます*10
    • 上述のパッケージもどき(グループ化)機能
    • defstructサブセットの提供
    • ドット記法のサポート
    • キャメルケース用のリードマクロ(Ex. #j.div.innerHTML#
    • src/utils: car, cdr, findその他、Common Lispとしては欲しい関数をParenscriptで使うためのマクロ群
    • src/common-macros.lisp: Parenscriptコードを書いていてよく出てくるパターンをマクロ化したもの
      • ps-experimentの趣旨と少しずれるので、ps-experiment.common-macrosを明示的にインポートしないと使えないようにしています

cl-ps-ecs

ps-experimentまでは基盤よりのライブラリでしたが、ここからはアプリよりのライブラリです。

cl-ps-ecsCommon Lisp兼Parenscript用のEntity Component System(ECS)ライブラリです*11。ECSがどの領域にどの程度知られているか良く分からないのですが、個人的には[GDC 2015]エンジンとツールがないなら自作しよう。「World of Tanks Blitz」ローンチまでの道のりを開発者が振り返る - 4Gamer.net」の記事で名前を知って以来一度作ってみたいと思っていました。

良い解説は調べれば出てくる(Understanding Component-Entity-Systems - Game Programming - Articles - Articles - GameDev.netとか。英語記事ですが図を見るだけでも問題意識は伝わると思います)ので、ECSについては簡単で適当な解説だけします。Unity知っている人はそのイメージで大体良い気がします*12

パッと見誤解しやすいですが、「EntityとComponentからなるSystem」ではなく「EntityとComponentとSystemからなるアーキテクチャ」です。それぞれ次のようなものです。

  • Entity: 識別子と複数のComponentを持つ
  • Component: 型とデータを持つ
  • System: 特定のComponent(の組み合わせ)を持つEntityを認識して処理を行う
    • 例えば、「当たり判定」Systemは「物理」Componentを持ったEntityを処理する

保持するComponentによってEntityが分類される = 適切なSystemに認識されるという点が重要です。ここがクラス継承による型ベースのオブジェクト表現(GameObjectクラスがあって、それを継承したPlayerクラスとEnemyクラスがあって、さらにEnemyを継承したFlyingEnemyクラスがあって…というもの)と大きく異なる点です。多重継承の罠に陥ることなく、必要なComponentを付け外しするだけでEntityに機能を柔軟に追加・削除できる点が、試行錯誤が多く、柔軟性が求められるゲーム開発に向いていると言われています。

さて、このライブラリの使い方ですが、こんな感じになります。まずはecs-componentを継承してComponentを適当に定義します。

(defstruct (vector-2d (:include ecs-component)) (x 0) (y 0))
(defstruct (position-2d (:include vector-2d)))
(defstruct (velocity-2d (:include vector-2d)))

次にecs-systemを継承してSystemを定義して登録(register-ecs-system)します。このmove-systemは位置(point-2d)と速度(velocity-2d)を持ったEntityに対して、位置を速度の分だけ更新します。

(defun process-move-system (entity)
  (with-ecs-components ((pos position-2d) (vel velocity-2d)) entity
    (incf (position-2d-x pos) (velocity-2d-x vel))
    (incf (position-2d-y pos) (velocity-2d-y vel))))

(defstruct (move-system
             (:include ecs-system
                       ;; どのコンポーネントを持つEntityを処理するか
                       (target-component-types '(position-2d velocity-2d))
                       ;; 対象Entityに対してどのような処理をするか
                       (process #'process-move-system))))

;; 第1引数の:moveは単なる識別子なので適当に
(register-ecs-system :move (make-move-system))

そして、ecs-entity型のEntityを生成し、add-ecs-component[-list]で必要なComponentを追加します。Systemに認識させるためにこれをグローバルに登録(add-ecs-entity)します。

(let ((entity (make-ecs-entity)))
  (add-ecs-component-list
   entity
   (make-position-2d :x 0 :y 0)
   (make-velocity-2d :x 1 :y 0))
  (add-ecs-entity entity))

(let ((entity (make-ecs-entity)))
  (add-ecs-component-list
   entity
   (make-position-2d :x 0 :y 0)
   (make-velocity-2d :x 0 :y -1))
  (add-ecs-entity entity))

;; velocity-2dを持たないEntity
(let ((entity (make-ecs-entity)))
  (add-ecs-component-list
   entity
   (make-position-2d :x 0 :y 0))
  (add-ecs-entity entity))

ecs-mainを呼び出すと登録済みのSystemが一度走ります。

(defun print-all-entities ()
  (do-ecs-entities entity
    (with-ecs-components (position-2d) entity
      (format t "ID = ~D, pos = (~A, ~A)~%"
              (ecs-entity-id entity)
              (vector-2d-x position-2d)
              (vector-2d-y position-2d)))))

(progn (print-all-entities)
       (format t "--- Run ecs-main ---~%")
       ;; ↓これ
       (ecs-main)
       (print-all-entities))

出力は次のような感じです。「速度」Componentを持たないEntity(ID = 3)は移動していないことが分かります。

ID = 3, pos = (0, 0)
ID = 2, pos = (0, 0)
ID = 1, pos = (0, 0)
--- Run ecs-main ---
ID = 3, pos = (0, 0)
ID = 2, pos = (0, -1)
ID = 1, pos = (1, 0)

cl-web-2d-game

次のcl-web-2d-gameはアプリ側のライブラリで、Web上で2Dのゲームを作るためのライブラリになる…といいですが、まだ柔らか過ぎて使い方など提示できる感じではありません…。もともとcl-shigi-simulatorと一緒に育てたのち分離する予定だったのですが、まだ間に合っておらず、cl-shigi-simulator/js-libフォルダに内蔵されたままです。

主な特徴は次の通りです。

  • 描画周りはThree.jsをある程度抽象化する形で書いている
    • このため両対応なcl-ps-ecsとは異なりあくまでParenscript用ライブラリです
  • cs-ps-ecsを用いたECSなアーキテクチャである
    • ベーシックなComponentやSystemを提供している
  • その他便利そうな関数群を提供している

まだ柔らかなので紹介しづらいです…。記事の方も力尽き気味なので、気の向いたところだけ説明する形にします(そのうちまた記事にしたい…)。

f:id:eshamster:20161218185622p:plain

このキャプチャに写っているのは、cl-shigi-simulatorというタイトルにも入っている中ボス「鴫(シギ)」を模したものです。本体部分2パーツとビット4つの計6パーツからなります。全部は長いので、ビット作成部分とそれらをまとめて鴫を構成する部分から抜粋して見ていきます。全体はstatic/js/shigi.lispです。

まずはビットの作成部分です。以降も含めてですが、必要な定義を全て記載しているわけではないので、コメントから雰囲気を察してください。

;; ビットEntityeを4つ作成し、リストにして返す
(defun.ps make-shigi-bits ()
  (let ((result '())
        (num-bit 4)
        (rot-speed (get-param :shigi :bit :rot-speed))
        (r (get-param :shigi :bit :r))
        (dist (get-param :shigi :bit :dist)))
    (dotimes (i num-bit)
      (let* ((bit (make-ecs-entity))
             (angle (* 2 PI i (/ 1 num-bit)))
             (model-offset (make-vector-2d :x (* -1 r) :y (* -1 r) :angle 0))
             (point (make-point-2d)))
        ;; --- 円周上に並ぶように位置調整 --- ;;
        (adjustf-point-by-rotate point dist angle)
        ;; --- タグを付与 --- ;;
        (add-entity-tag bit "shigi-part" "shigi-bit")
        ;; --- 各種コンポーネントを持たせる部分 --- ;;
        (add-ecs-component-list
         bit
         ;; 描画用コンポーネント(円…を作成する関数を作っていないので、辺の多い正多角形)
         (make-model-2d :model (make-wired-regular-polygon :r r :n 100
                                                           :color (get-param :shigi :color))
                        :depth (get-param :shigi :depth)
                        :offset model-offset)
         ;; 当たり判定用コンポーネント
         (make-physic-circle :r r
                             :on-collision #'toggle-shigi-part-by-mouse
                             :target-tags *shigi-collision-targets*)
         ;; 位置コンポーネント(ローカル座標)
         point
         ;; 回転移動用コンポーネント
         (make-rotate-2d :speed rot-speed
                         :angle angle
                         :radious dist)
         ;; Key-Valueパラメータ用コンポーネント
         (init-entity-params :color (nth i (get-param :color-chip :colors))
                             :display-name (+ "Bit" (1+ i))
                             :bit-id i
                             :enable t)))
      (push bit result))
    result))

かいつまんで見ていきます。

まず、一番特徴的なのはやはりECSがベースになっていることです。描画をするのか(model-2d)や当たり判定をするのか(physic)はComponentの有無によって変わってきます。ここにはないですが、あらゆる操作に対していちいちSystemを作成することは現実的ではないため、script-2dという、Entityを引数にとる任意の関数を登録するためのComponentも存在します。

次に、本システムで用意しているパラメータ管理機構には大きくglobalレベルのものとentityレベルのものがあります。globalレベルのものは上記でも所々で利用されているget-paramです。cl-web-2d-gameライブラリとして提供しているのは下記です。

  • convert-to-layered-hash: 階層的なKey-Value構造(ハッシュ)を作るためのDSL
  • get-layered-hash: 上記で作成した構造から値を取り出す(もしくは関数を実行する)
;; 下記の(+ 10 20)のようにリストを書いた部分は
;; get時に関数として評価
(defvar *hash*
  (convert-to-layered-hash
   (:position (:x 12 :y (+ 10 20))
    :size (:width (* 2 3) :height 100)
    :some-list (list 1 2 3))))

(get-layered-hash *hash* :position :x)  => 12
(get-layered-hash *hash* :position :y)  => 30
(get-layered-hash *hash* :size :width)  => 6
(get-layered-hash *hash* :size :height) => 100
(get-layered-hash *hash* :some-list) => (1 2 3)

本アプリではglobalなハッシュはせいぜい一つしか必要でないため、get-paramget-layered-paramをラップしてハッシュの指定を省略しています。

もう一つのEntityレベルのパラメータはinit-entity-paramsコンポーネントとして持たせています。中身は単なる1階層のハッシュです。抜粋部分にはありませんが、利用はget-entity-paramset-entity-paramによって行います(setfには対応できておらず…)。

だいぶおざなりな解説をしている自覚はありますが、ここで鴫全体を構成するためのコードへ向かいます。

(defun.ps make-shigi ()
  ;; centerは全体をまとめる親となるEntityでグラフィックを持たない
  ;; bodiesは鴫本体の2パーツ
  (let ((center (make-shigi-center))
        (bodies (make-shigi-bodies))
        (bit-list (make-shigi-bits)))
    (add-ecs-entity center)
    ;; centerを親にしてbodyとbitをadd-ecs-entityしていく
    (dolist (body bodies)
      (add-ecs-entity body center)
      ;; markerは上記画像の各パーツ中心にある四角のこと
      (add-ecs-entity (make-center-point-marker) body))
    (dolist (bit bit-list)
      (add-ecs-entity bit center)
      (add-ecs-entity (make-center-point-marker) bit)
      (when (oddp (get-entity-param bit :bit-id))
        (toggle-shigi-part bit)))))

ここで特徴的なのはEntityの親子関係の登録です。cl-ps-ecs:add-ecs-entityは第2引数で親となるEntityを指定できます。cl-web-2d-gameではこの親子関係を主に座標の管理に利用しています。具体的には、子は座標データ(位置と回転)を親に対する相対座標として持っています*13

この方法をとると嬉しいのは、親と子の移動処理を独立して書けるという点です。例えば、一番親となるcenterから見ると、子供であるbodybitのことを気にせずに移動しても(今回は固定位置ですが)、子供は勝手についてきてくれます。子供、例えばbitの方から見ると、親の中心点に対して回転するという動作だけ記述しておけば、親がどこにいるかを気にする必要はありません。今回の例で極端なものはmake-center-point-markerから作っているマーカで、マーカ自身は一切移動の処理(Component)を持ちませんが、勝手に親のbodybitについていっています。

あとはjs-libフォルダ配下のファイルレベルで何ができているかをざっと見て終わりにします。

  • Three.jsへの依存が強い部分
    • 2d-geometry.lisp: 描画用のモデル作成。丸や四角形や多角形を作れます
    • draw-model-system.lisp: 描画用のSystem
    • camera.lisp: 2Dに十分な形で3Dカメラを初期化・管理
  • その他のJavaScriptライブラリに依存する部分
    • input.lisp: マウス、キーボード、タップといった入力関係の処理
    • gui.lisp: アプリ右上の操作パネル。dat.GUIのラッパー
  • JavaScriptへの依存がない部分
    • basic-components.lisp: 2Dのベクタなど基本的なComponent群
    • calc.lisp: 主にベクタ関連の計算
    • collision.lisp: 衝突計算。現状、円と円、円と任意凸多角形の判定ができます
      • 任意凸多角形同士の判定はまだ実装していません
  • utils.lisp: その他整理できていない諸々
    • ゲームの初期化・スタート(ここはThree.jsに依存)
    • wtf-trace用のラッパー
    • 上記のglobalなパラメータの作成・読み込み(ここは依存なし)

まとめ

見ての通りだいぶ荒削りですが、どうにかオールCommon LispでThree.jsなWebアプリを作れるようになりました。基盤となるライブラリを整えつつでしたが、軌道に乗ってくると全部Common Lisp(もどき)で書けて嬉しいです。また、前々から気になっていたECSを作れたのも良かったです。

今後としては、jsclやClojureScriptといったところをちゃんと調べて反映させていかないとだめですね…。その前に、今回作ったシミュレータはもう少し仕上げておきたいと思います。他、触った方は分かると思いますがまだまだパフォーマンスが悪いのでその改善も…。

最後に、もっと小分けにして少しずつ記事にしておけば良かったと思いました まる


*1:さくらのDockerホスティングサービスArukasのプレビュー版を使っています。仮置きなので、メンテのために適当に止まったり、いつの間にか消滅していたりするかもしれません。

*2:CSSを除く

*3:qlotに限らず新しくlocal-project配下にプログラムを置いたときなのですが、自身の環境ではなぜか大本のbashをいったん落とさないとql:quickloadから見えるようになりません…。前掲のDockerイメージを作るDockerfileではそれができないので、ql:*local-project-directories*にcl-shigi-simulator配下にできたquicklispフォルダをpushすることでどうにか回避しています

*4:もっと良いやり方がありそうですが、ベースとなるスケルトンの更新の反映を考えると、他に良い方法が思いつきませんでした…

*5:CSS部分をLisp化したものがないのは単にCSSヘビーなものを書いていないからです…。

*6:ここは以前記事(Caveman2でCL-Markupを使う準備 - eshamster’s diary)にしたものを少し整理してスケルトン化したものです

*7:理由は後述のps-experimentに関連します。1つ目は関数コンパイルだけで定義更新できるので、ファイルの保存を一々したくないこと。もう1つは、load-js関数の定義されたファイルしか見ていないため、ps-experimentで実現するファイル分割に対応できていないことです…。

*8:Dirtyかもしれません

*9:useは汚いのでimportしているシンボルの属するパッケージを探す形の方がまだ良さそうですね…。少し重そうなのが気になりますが。

*10:基本的には以前「Parenscriptで遊んでみる」シリーズで記事にしていた内容です

*11:細かな定義論が分かっていないので「ECSもどき」が適切かもしれません

*12:自身はUnity少ししか触ったことがないですが…。また、Unity社はECSとは似て非なるものだと言っているはずです

*13:割と普通の設計だとは思うのですが、以前書いたライブラリはそうしていなかったため、個人的には印象の強いポイントです