真の3D迷路
前回やってみた、「CALayerのみで3Dっぽい表示にする」というのを使って、折角だし立体迷路アプリを作ってみた。
ちなみにOSバージョンはSnow Leopard(10.6.6)です。
説明
3D迷路って普通こういうのをイメージすると思う。
確かに表示は立体的だけど、迷路自体は2Dだよね?
本当の3D迷路はこうだと思う。
ちょっと分かりにくいかな。左右だけじゃなくて上下にも方向転換出来る。
- 操作は全てマウス
- クリックが前進
- マウスダウン&ドラッグすると方向を変えられる(壁をつかんで動かす感じ)
- 緑のコマがスタート、赤いコマがゴール
- 「Setting...」で設定が変えられる
- X、Y、Zのサイズが変更可能。(多分15x15x15ぐらいで限界)
- 壁の透明度、枠線の太さ、コーナーの大きさが変更可能
- 壁の色も変更可能
- 壁をすり抜け出来るモード付き
プログラムの説明 - 迷路作成
迷路の作成は、Maze3Dというクラスに実装している。アルゴリズムは、昔MSX-BASICの本*1に載ってたヤツを3Dにアレンジした。
元のアルゴリズムは、棒倒し法と穴堀法のハイブリッドのような感じで、
- マップ用の配列を用意し、1で壁、0で通路を表すことにする。
- まず最初に全て壁で埋める
- (1,1)から順に一つ飛ばし(つまり(奇数,奇数)になるところ)に現在のポジションを決め、
- 現在のポジションを通路にする
- 現在のポジションの上下左右(二歩先)に通路がある場合はランダムに選んで繋げる(=間も通路にする)
- さらに上下左右(二歩先)に壁があれば、そちらに穴を掘って進む
- これをまわりに掘り進める壁が無くなるまで繰り返す
というやつ
上下左右にさらにZ軸を追加したのと、開始点を(0,0,0)にしたの以外は変更していない。(赤字追記)
プログラムの説明 - 描画系
描画についてはほぼCore Animationの機能のみで実装している。(さすがにNSWindowとNSViewは使っているけど)
前回の要領で透視変換用のレイヤを用意し、そこに壁にあたるレイヤを立てていく。座標をうまくずらして、回転の中心が視点と重なるようにする。視点背後に回ったレイヤはhiddenフラグを立ててスクリーンに出ないようにしている。(後方の判定は、中心点のZ座標でおこなっている。具体的には、transformのm43成分を見ている。)
操作時は、マウスの移動量をみながら回転 or 前進をおこなっている。マウスダウン時に位置を覚えておき、ドラッグせずにマウスアップした場合は前進とみなす。ドラッグが発生した場合は、マウスの移動の度に適当に回転量を計算して、全壁のCALayerのtransformを変更している。ドラッグ後にマウスを放すと、90度単位で一番近い方向に自動的に向く。
前進とドラッグ後の方向調整はCore Animationのプロパティ変更時の自動アニメーションを使っている。一方でドラッグ中はトランザクションを使ってコマ毎に全体を描画している。自動アニメーションの方は全レイヤにバラバラにアニメーションを走らせている都合で結構ちらつく。自分でフレーム単位にトランザクション使って描けばちらつかなくなると思うけど、それはつまらないのでやっていない。
プログラムの説明 - クラス構成
プログラムの説明 - 処理のおおまかな流れ
初期化時は大体以下の流れで処理が進む
- アプリケーション起動時に基本nibファイルを読み込むその際、NSApplicationのdelegateのTrue3DMazeAppDelegateが作られる。
- 「New Maze」メニューがTrue3DMazeAppDelegateの-newMaze:アクションにバインドされていて、メニューを押すとMazeControllerを生成して迷路ウィンドウ用のnibを読み込んでいる。MazeControllerがFile's Ownerになっていて、
- nib上で迷路用のwindowとその上のMazeViewが初期化されてMazeControllerにセットされる
- MazeControllerは以下のクラスのオブジェクトを生成して保持する
- 視点の情報を持つ ViewPoint クラス
- 迷路の地図情報を持つ Maze3D クラス
- 設定情報を持つ MazeSettingクラス
- これらを相互にnotificationのobservationを登録することで関連づける
- 迷路の生成をおこなう
- 表示したウィンドウ上に迷路が表示される。
ユーザ入力時は以下の流れで処理が進む
- MazeView上でマウスイベントを受ける
- MazeViewは入力内容をMazeControllerに伝える
- ドラッグ中
- MazeControllerがドラッグ中の視点変更通知を投げる
- MazeViewが視点変更通知を拾ってMazeView上のレイヤのtransformを変更して画面に反映
- マウスアップ
- MazeViewが入力内容をMazeControllerに伝える(回転 or 前進)
- MazeControllerがMaze3Dの内容をみながらViewPointを変更し、視点変更通知を投げる
- MazeViewが視点変更通知を拾ってMazeView上のレイヤのtransformを変更して画面に反映
- ドラッグ中
設定変更は、設定パネルがNSObjectController経由でMazeSettingにバインドされていて、パネルの入力内容がMazeSettingにリアルタイムに反映される
- 「Setting...」メニューのアクションがFirst Responderに投げられる
- WindowのdelegateであるMazeControllerが受け取ってパネル(シート)を表示する
- ユーザ入力で描画系の設定変更があれば、その都度MazeSetting設定変更通知を投げる
- MazeViewが設定変更通知を拾ってMazeView上のレイヤの属性を変更する
- 「Hide Settigs」ボタンを押すと、シートを引っ込める
迷路の再作成は、ボタンがMazeControllerのアクションにバインドされていて、押すと初期化のときと同じ流れで処理が走る
- MazeControllerがMaze3Dの迷路作成処理を呼び出す
MazeControllerさんがコントローラと一部モデルもやってしまっている感じ。
感想
実は大学時代にNeXT上のCherry Basic作ったやつのリメイクだったりする。当時はアニメーションじゃなくてカーソルで移動してたけど。
OPENSTEPぐらいまでは結構アプリ作ってたけど、当時はUI周りについてはメモリ管理とかあんまりちゃんと考えてなかったので、今回はちょっと悩んだ。というかまだリークあるかも。
おまけ
2D迷路から3D迷路にする途中のバージョンの動画載せとく。
http://terazzo.tumblr.com/post/14031568494