Open2

Cosmos 開発日誌

cddadrcddadr

開発中のゲームエンジン兼汎用アプリケーションフレームワークについて書いていきます。
元々はゲームエンジンとして作っていましたが、普通にGUIアプリを作るのに使えそうな様相になってきたので汎用アプリケーションフレームワークを自称しています。

名前は、混沌(chaos)としがちなゲーム開発に秩序(cosmos)をもたらしたいということで Cosmos です。Common Lisp製のアプリケーションはなぜか宇宙っぽい名前が好まれるというのも理由としてちょっとあったりします。

cddadrcddadr

Cosmos の設計とか

Cosmosはノードベースのフレームワークです。
HTMLのようにあらゆる構成要素はノードであり、その積み重ねで効果を発揮します。

ノードベースのゲームエンジンといえばGodotというのもありますが、Cosmosはより汎用化を進めてウィンドウやレンダーパスもノードとして表現しています。現行のコードは以下のような感じです。

(defnode <app> ()
  ;; スロット定義
  ((count 0 :type integer))
  ;; ツリー定義
  ((my-window <window> (:title "Cosmos Application"
                        :width 800 :height 600
                        :on-close #'cycle:kill)
     (my-resources <node> ()
       (my-font <managed-font> (:path "assets/monospace.ttf"
                                :font-size 20.0)))
     (window-coord <window-coord> (:children-target t)
       ;; (add-child <app>) でこのノードに子が追加される
       ;; 子のトランスフォームをウィンドウ座標系で計算する
       (my-text <ui-text> (:font my-font
                           :content "Hello, world!"
                           :translation (v3:make 400.0
                                                 300.0
                                                 0.0))))
     (renderer <pass-slot> (:target-node window-coord)
       (shape-pass <shape2d-pass> ())))))

(defmethod node:update ((node <app>) delta-time)
  (with-slots (count my-text)
      node
    (incf count)
    (setf (ui:text my-text)
          (format nil "Count: ~a" count))))

ノードはただのクラスなので make-instance すれば実体化できます。

ノードベースのシステムはモジュール化しやすいためコード再利用性が高く、Lisp の適したボトムアッププログラミング向きです。また、似たところにあるWebフロントエンドのノウハウを流用出来る可能性があります。

DOMと異なるのはノードがコンテキストを持つことです。コンテキストはノードの子となった時に親からコピーされ、ハッシュテーブルとして表現されます。深い階層のノードほど様々な情報を持つことになります。例えばウィンドウノードはコンテキストにスワップチェインなどの情報を格納し、レンダーパスノードがこれを利用するようになっています。

イベントシステムはDOMイベントを踏襲してキャプチャリング・バブリングフェーズを備えたものを実装しています。Godotのようにノードからノードに直接シグナルを送るシステムも用意していますが、こちらは今のところ活用できていません…

;; nil 以外を返すことで子へのイベント伝播を止める
(defmethod node:capture ((node <app>) (event window:key-down))
  t)

;; nil 以外を返すことでバブリングを停止する
(defmethod node:handle ((node <app>) (event window:key-down))
  (when (eq :escape (window:key event))
    (cycle:kill node))
  t)

基本的な仕組みはこんな感じで、ノードのライフサイクルとして今のところ次の6つがあります。

  • Initialize: make-instance 後、子ノードの構築が完了した後に実行される。
  • Stage: ノードの子として追加された時に実行される。親のコンテキストは保証されない。
  • Pulse: 動的にノードの子として追加された時に実行される。ルートノードからコンテキストが繋がっていることが保証される。
  • Update: 毎サイクル実行される。
  • Sync Frame: 毎サイクル Update 後に子ノードから順に実行される。
  • Draw Frame: 毎フレーム上記ライフサイクルと並列に実行される。

並列に動かしているところはあまり安定していないので今後変わる可能性があります。