真の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のプロパティ変更時の自動アニメーションを使っている。一方でドラッグ中はトランザクションを使ってコマ毎に全体を描画している。自動アニメーションの方は全レイヤにバラバラにアニメーションを走らせている都合で結構ちらつく。自分でフレーム単位にトランザクション使って描けばちらつかなくなると思うけど、それはつまらないのでやっていない。

プログラムの説明 - クラス構成

True3DMazeAppDelegate
アプリケーションのdelegate
MazeController
迷路ウィンドウと関連オブジェクトの制御。Maze.nibのFile's Owner。
MazeView
描画、イベント受付およびレイヤの制御
Maze3D
迷路を表すモデル
ViewPoint
視点を表すモデル。中身は只のtransform。
MazeSetting
設定内容を保持するモデル。設定シートにCocoa Bindingsで貼付けている。
MazeLayer
壁一枚ごとのレイヤ。CALayerのサブクラス。

プログラムの説明 - 処理のおおまかな流れ

初期化時は大体以下の流れで処理が進む

  1. アプリケーション起動時に基本nibファイルを読み込むその際、NSApplicationのdelegateのTrue3DMazeAppDelegateが作られる。
  2. 「New Maze」メニューがTrue3DMazeAppDelegateの-newMaze:アクションにバインドされていて、メニューを押すとMazeControllerを生成して迷路ウィンドウ用のnibを読み込んでいる。MazeControllerがFile's Ownerになっていて、
    • nib上で迷路用のwindowとその上のMazeViewが初期化されてMazeControllerにセットされる
    • MazeControllerは以下のクラスのオブジェクトを生成して保持する
      • 視点の情報を持つ ViewPoint クラス
      • 迷路の地図情報を持つ Maze3D クラス
      • 設定情報を持つ MazeSettingクラス
    • これらを相互にnotificationのobservationを登録することで関連づける
      • Maze3Dの初期化通知をMazeViewで受ける(CALayerで壁を構築するため)
      • Maze3Dの初期化通知をViewPointで受ける(スタート位置に初期化する為)
      • ViewPointの視点移動通知をMazeViewで受ける(再描画のため)
      • MazeSettingの見た目設定変更通知をMazeViewで受ける(再描画のため)
  3. 迷路の生成をおこなう
    • Maze3Dが迷路を作成し、迷路生成完了の通知を投げる
    • 迷路生成完了の通知を受けて、MazeViewが透視変換レイヤの上にMazeLayer(CALayerのサブクラス)を壁一枚毎に生成して設置して行く
    • 迷路生成完了の通知を受けて、ViewPointがスタート地点に初期化される
    • ViewPointの初期化を受けて、MazeView上のレイヤのtransformがスタート位置からの視点で初期化される
  4. 表示したウィンドウ上に迷路が表示される。


ユーザ入力時は以下の流れで処理が進む

  1. MazeView上でマウスイベントを受ける
  2. MazeViewは入力内容をMazeControllerに伝える
    • ドラッグ中
      • MazeControllerがドラッグ中の視点変更通知を投げる
      • MazeViewが視点変更通知を拾ってMazeView上のレイヤのtransformを変更して画面に反映
    • マウスアップ
      • MazeViewが入力内容をMazeControllerに伝える(回転 or 前進)
      • MazeControllerがMaze3Dの内容をみながらViewPointを変更し、視点変更通知を投げる
      • MazeViewが視点変更通知を拾ってMazeView上のレイヤのtransformを変更して画面に反映


設定変更は、設定パネルがNSObjectController経由でMazeSettingにバインドされていて、パネルの入力内容がMazeSettingにリアルタイムに反映される

  1. 「Setting...」メニューのアクションがFirst Responderに投げられる
  2. WindowのdelegateであるMazeControllerが受け取ってパネル(シート)を表示する
  3. ユーザ入力で描画系の設定変更があれば、その都度MazeSetting設定変更通知を投げる
    • MazeViewが設定変更通知を拾ってMazeView上のレイヤの属性を変更する
  4. 「Hide Settigs」ボタンを押すと、シートを引っ込める

迷路の再作成は、ボタンがMazeControllerのアクションにバインドされていて、押すと初期化のときと同じ流れで処理が走る

  1. MazeControllerがMaze3Dの迷路作成処理を呼び出す
    • Maze3Dが迷路を再作成し、迷路生成完了の通知を投げる
    • 迷路生成完了の通知を受けて、MazeView上のLayer再作成
    • 迷路生成完了の通知を受けて、ViewPointが初期化される
    • ViewPointの初期化を受けて、MazeView上のレイヤがスタート位置からの視点にリセットされる


MazeControllerさんがコントローラと一部モデルもやってしまっている感じ。

感想

実は大学時代にNeXT上のCherry Basic作ったやつのリメイクだったりする。当時はアニメーションじゃなくてカーソルで移動してたけど。


OPENSTEPぐらいまでは結構アプリ作ってたけど、当時はUI周りについてはメモリ管理とかあんまりちゃんと考えてなかったので、今回はちょっと悩んだ。というかまだリークあるかも。

おまけ

2D迷路から3D迷路にする途中のバージョンの動画載せとく。
http://terazzo.tumblr.com/post/14031568494

*1:多分「MSX POCKET BANK」あたり