Common Lispでゲーム用の状態遷移マシン 完成?
やっと、コード書く時間がとれたので、記録用に書きます。
singletonクラス作成用のパッケージ
とりあえず、singletonをつくるためのパッケージを作成しました。
http://cadr.g.hatena.ne.jp/g000001/20081202/1228199756
この記事が超参考になりました。
closer-mopを使うことで、特定の実装に依存しないでsingletonが実現できますね。
素晴らしいです。
(defpackage :singleton (:use :cl ) (:export #:define-singleton-class )) (in-package :singleton) (defclass singleton-meta (standard-class) ((%the-singleton-instance :initform () ))) (defmethod make-instance ((class singleton-meta) &key) (with-slots (%the-singleton-instance) class (if %the-singleton-instance %the-singleton-instance (let ((ins (call-next-method))) (setf %the-singleton-instance ins) ins)))) (defmethod c2mop:validate-superclass ((class singleton-meta) (super standard-class)) 'T) (defmethod c2mop:validate-superclass ((class singleton-meta) (superclass standard-class)) ;; it's OK for a standard class to be a superclass of a singleton ;; class 'T) (defmethod c2mop:validate-superclass ((class singleton-meta) (superclass singleton-meta)) ;; it's OK for a singleton class to be a subclass of a singleton class 'T) (defmethod c2mop:validate-superclass ((class standard-class) (superclass singleton-meta)) ;; but it is not OK for a standard class which is not a singleton class ;; to be a subclass of a singleton class nil) (defmacro define-singleton-class (name supers &rest args) (and (assoc :metaclass args) (error "Metaclass already specified.")) `(defclass ,name ,supers ,@args (:metaclass singleton-meta)))
状態遷移マシン自体のパッケージ
状態遷移マシンは、こんな感じで実装してみました。
state-machine:stateを継承しているクラスを使うことを想定しています。
(in-package :cl) (defpackage :state-machine (:use :cl :singleton) (:export #:state #:state-machine #:set-current-state #:set-prev-state #:set-global-state #:change-state #:enter #:execute #:exit #:update #:current-state ) ) (in-package :state-machine) (singleton:define-singleton-class state () ()) (defmethod enter ((state-instance state) &rest arg) ) (defmethod execute ((state-instance state) &rest arg) ) (defmethod exit ((state-instance state) &rest arg) ) (defclass state-machine () ((owner :initarg :owner) (current-state :initform nil :accessor current-state) (prev-state :initform nil :accessor prev-state) (global-state :initform nil :accessor global-state))) (defmethod set-current-state ((obj state-machine) s) (setf (current-state obj) s)) (defmethod set-prev-state ((obj state-machine) s) (setf (prev-state obj) s)) (defmethod set-global-state ((obj state-machine) s) (setf (global-state obj) s)) (defmethod update ((obj state-machine)) (when (not (null (global-state obj))) (execute (make-instance (global-state obj)))) (when (not (null (current-state obj))) (execute (make-instance (current-state obj))))) (defmethod change-state ((obj state-machine) new-state) (setf (prev-state obj) (current-state obj)) (exit (make-instance (current-state obj))) (setf (current-state obj) new-state) (enter (make-instance (current-state obj))))
実際の例
こんな感じで、状態遷移マシンを構成してみます。
(singleton:define-singleton-class getup (state-machine:state) ()) (singleton:define-singleton-class goodnight (state-machine:state) ()) (defmethod state-machine:enter ((state-instance getup) &rest arg) (declare (ignore arg)) (format t "enter getup~%")) (defmethod state-machine:execute ((state-instance getup) &rest arg) (declare (ignore arg)) (format t "exec getup~%")) (defmethod state-machine:exit ((state-instance getup) &rest arg) (declare (ignore arg)) (format t "exit getup~%")) (defmethod state-machine:enter ((state-instance goodnight) &rest arg) (declare (ignore arg)) (format t "enter goodnight~%")) (defmethod state-machine:execute ((state-instance goodnight) &rest arg) (declare (ignore arg)) (format t "exec goodnight~%")) (defmethod state-machine:exit ((state-instance goodnight) &rest arg) (declare (ignore arg)) (format t "exit goodnight~%")) (setf fsm (make-instance 'state-machine:state-machine)) (state-machine:set-current-state fsm 'getup) (state-machine:update fsm)
さて、実際に実行してみると
CL-USER> (state-machine:update fsm) exec getup NIL CL-USER> (state-machine:update fsm) exec getup NIL CL-USER> (state-machine:update fsm) exec getup NIL CL-USER> (state-machine:change-state fsm 'goodnight) exit getup enter goodnight NIL CL-USER> (state-machine:update fsm) exec goodnight NIL
いい感じですねー。
課題
- 状態遷移のためのstateを継承したクラスの定義が面倒くさい。パッケージをわざわざ指定するのは面倒くさい。
解決方法をちょっと考えてみます...。
移植にあたっての課題とその解決方法
ここでは、FSM/State.hとStateMachine.hをCLOS上に移植する上での課題とその回避策について考えます。
移植上の2つの課題
簡単に移植できると思うのですが、CLOSで扱うのが面倒臭そうな技術上の課題がいくつかあります。
上記のコードでは、以下のテクニックが使われています。
- テンプレート
- singletonパターン
こんな手法を使っている理由を考えてみます
テンプレートは、いちいちStateMachineおよびStateを継承したマーカクラスを作成するのが面倒くさいというのが主たる目的でしょう。
singletonパターンは、StateMachineを愚直に実装すると、StateMachineごとにStateのインスタンスを生成する実装が考えられます。しかし、これはメモリの使用効率が良くないです。StateMachineは1万とかオーダーで生成されることを想定されます。このたびにStateを生成していては無駄となります。なので、静的に確保したいのでしょう。
余談
singletonは、最初はnamespaceでも良いのではと考えてしまいましたが、これではダメですね。statemachineクラスが現在の状態を管理するには、右辺値として扱えるクラスであって欲しいですね。statemachineクラスがenter,execute,exit関数を関数ポインタで保持して、管理する形式にすれば可能ですが、それはあまりにも煩雑でしょう。また、テンプレートを使っていることを考えるとトリッキーなコードになりそうですし...。
テンプレートについての解決方法
これは困った。CLOSにはテンプレートとかないです。Common Lispなので動的型付けだし、みなかったことにしたいですねw
ありえない型への遷移も許容してしまう実装になってしまいそうですねぇ...。
マクロにして型チェックをするコードを挿入するというのも手ですが、これでも動的チェックになってしまいますね。
静的チェックを実装する方法については、今後の課題にしておきます。
singletonパターンについての解決方法
これまた困りました。どうしてこんなに世の中生きていくのが辛いのでしょうか?
以下の2つが制御できるかどうかが課題ですね。
- setf
- make-instance
CLOSでsingletonクラスの実装をしている人はいるようで以下のような実装がありました。
http://www.tfeb.org/programs/lisp/singleton-class.lisp
make-instanceを制御しているようです
(make-instance 'foo)
で毎回同じオブジェクトが返ってくるようになりますね。
(setf (make-instance 'foo) nil)
とかしたらオワタなきがします。
やってみました。
The function (SETF MAKE-INSTANCE) is undefined. [Condition of type UNDEFINED-FUNCTION] Restarts: 0: [RETRY] Retry SLIME REPL evaluation request. 1: [*ABORT] Return to SLIME's top level. 2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {AB97279}>)
なるほどー。エラーがかえってきました。この実装でそのまま使えそうですねー。
わーい。
というわけで、singletonは大丈夫そうですね!
C++の場合はどうなるか?
いきなりCommon Lispで挑戦するのでもよかったのですが、ここは先人の知恵を借りたいところです。
C++でゲーム用の状態遷移クラスを作っているサンプルや枯れているものはたくさんあるので参考にしようと思います。
ここでは、以下の本に載っている状態遷移クラスを参考にしようと考えています。
- 作者: Mat Buckland,松田晃一
- 出版社/メーカー: オライリー・ジャパン
- 発売日: 2007/09/28
- メディア: 大型本
- 購入: 7人 クリック: 207回
- この商品を含むブログ (40件) を見る
ソースコードは以下のSamples & Additional Resourcesから入手可能です。
http://www.jblearning.com/Catalog/9781556220784/student/
具体的なファイルはCommon/FSM/State.h,Common/FSM/StateMachine.hです。
これをCommon Lispへと移植することにします。
ゲームに使える状態遷移マシン for Common Lisp
さて、前回の続きです。
クラス構成と要件を前回説明しました。
今回は実際にCommon Lisp上に状態遷移マシンを実装します。
ゲームに使える状態遷移マシン
備忘録的に吸闘紀で採用している状態遷移マシンの設計と実装について書いておきます。
ゲームに必要な状態遷移マシン
ゲームに使える状態遷移マシン。そこまで複雑な状態遷移マシンは必要とはなりません。
簡単に要件をまとめておきます。
まず、各遷移する状態を表す状態は以下の要件があれば十分です
- 各状態は特定のタイミングで指定した関数を実行する
- 各状態が定めている関数の引数の数や意味はそれぞれに異なる可能性がある
次に、上記の状態を管理し、遷移を適切に処理する状態遷移マシンクラスの要件です。
- 状態遷移マシンは状態を2種類もつ
各要件の詳細
各状態は特定のタイミングで関数を実行する
以下の3つのタイミングで、各状態ごとに定義されている関数を実行する必要があります。
- 状態に遷移する時に実行する関数(enter)
- 状態の更新関数(update)
- 次状態に遷移する時に実行する関数(leave)
各状態が定めている関数の引数の数や意味はそれぞれに異なる可能性がある
上記の関数の引数は、各状態ごとに異なる可能性があります。
これは、敵の更新関数、プレイヤーの更新関数、シーンの更新関数では必要な情報が異なる場合が少なくないためです。
使用上の注意
この設計では、各エンティティが自分の状態を管理する状態遷移マシンを持っています。また、状態遷移マシンは各エンティティへの参照を持っています。
このため、相互に所有している関係となっています。
エンティティが開放されると同時に状態遷移マシンも開放されるように、状態遷移マシンの参照はエンティティ内部に閉じるようにしておく必要があります。
実装
次回にまとめてソースは書きます。
Mac環境構築ガイド for 自分
MacBookPro(15インチ)を購入しちゃいました!!
Apple MacBook Pro 2.2GHz 15.4インチ MC723J/A
- 出版社/メーカー: アップル
- 発売日: 2011/02/25
- メディア: Personal Computers
- クリック: 10回
- この商品を含むブログ (2件) を見る
今後の備忘録もかねて環境構築ガイドを作成しようと思い記事にしてしまいます。
ランチャ(QuickSilver)のインストール
Macでは有名なキーボードで操作できるランチャです。
起動のキーバインドはCommand + Shit + Spaceを割り当てています。
マウスの設定
画面の四隅のショートカットをよく使うので設定します。
- 画面左上でエクスポゼが実行されるように
- 画面右下でデスクトップが表示されるように
Spotlightの設定
検索機能のspotlightの設定です。検索機能自体は便利なのですがバインドがEmacsとぶつかるので無効にします。
雑多なアプリケーションのインストール
AppStoreから以下のアプリをインストールします。
以下のアプリは適宜サイトからDLしインストール
- Skype
- Windows Liveメッセンジャー
- VLC
- コーデック(Perian)のインストール
XCodeのインストール
XCode4は有料なので、XCode3をインストールします。これはディスクからでも良いし、AppleのサイトからDLしてインストールしても問題ないです。
サイズが大きいので。お茶でも呑んで待ちます。
MacPortsのインストール
これも公式のサイトからインストールします。
ターミナルの設定
端末の色がデフォルトでは見づらいので、色を変更できるように以下の2つをインストールします。
- SIMBL
- Terminal Color
端末で日本語が表示できるように以下の設定を行います。
内容は後日追記します。
- inputrc
- MacOS/environments.plist
- zshrc
MacPortsで入れるもの
あとはMacPortsでほしい物をじゃんじゃんインストールしましょう。
- git-core +svn
なぜかしょっぱなからコケました。db46のコンパイルにjava developperがいるみたいです。
全くわからなくて時間を浪費してしまいました。
Carbon Emacsはもう古いのでこちらのEmacsを利用しています。Cocoaを利用するようになっていて、フォントなどの設定が楽になっています。
開発用に以下のライブラリをインストールします。
- libsdl
- libsdl-framework
- libsdl-image
- libsdl-image-framework
- libsdl-mixer
- libsdl-mixer-framework
- libsdl-ttf
- libsdl-ttf-framework
もちろんCommon Lispの環境もインストールします
quicklisp
これは、quicklispのサイトからDLして来ます。
Lispの環境設定についてはまた別の記事にでもしようかと思ういます。
設定完了!
以上が、自分のMacの環境です。
主に、開発にしか使わないので素っ気ない環境ですが気に入ってます。
ガンダムUC読了
とりあえずガンダムUCを10巻全部読了しますた。
なかなか良い話だった気がします。文句があるとすると筆者がガンダム好きすぎ。そのせいで、セリフのほとんどがガンダムのオマージュになっててちょっともったいない。そんなことしなくても魅力的な話なのに。
機動戦士ガンダムUC 1 ユニコーンの日(上) (角川コミックス・エース 189-1)
- 作者: 福井晴敏
- 出版社/メーカー: 角川書店
- 発売日: 2007/09/26
- メディア: コミック
- 購入: 11人 クリック: 90回
- この商品を含むブログ (206件) を見る