hrxdev
日本語

#3 マウスで刑務所を建てる、そして自分で見つかる部屋

· Nick

2つ目のマイルストーンが完了し、初めて目で見られるものができました。Godot のクライアントがシミュレーションコアの上に乗り、マウスで1階建ての刑務所を建てられます。床を敷き、壁を引き、ドアを置き、部屋を指定し、取り壊す。すべてただのキューブと単色で、中にはまだ誰もいません。それでも、そこは一つの場所で、カメラをぐるりと回して眺められます。

いまできること

新しいゲームを始めるときに、マップの大きさ(一辺64、128、256タイル)と、任意のシードを選ぶと、空のグリッドが現れます。カメラは固定の傾きを持つ正射影で、きっちり90° 刻みで回り、カーソルに向かってズームし、キーまたは中ボタンでパンでき、建設用の真上視点モードもあります。自由に回るカメラでは壁が部屋を隠してしまうため、あえてそうしていません。

床と取り壊しは矩形で指定します。壁は輪郭をなぞって引きます(Shift を押していれば直線)。ドアはクリックで置け、隣の壁から向きを決め、キーで回転できます。ドラッグ中、カーソル下の範囲はゴーストとして表示されます。緑のタイルは建設され、赤のタイルはスキップされ、ラベルがその数と理由を示します。指を離すまで世界には何も送られず、コアが「1タイルも変わらない」と答えたときは、何も送られません。右クリックまたは Escape でキャンセルです。

世界のモデルには複数の階層があるので、階層ピッカーがあり、いまの階より上の階は隠れます。部屋には専用のツールがあり、後述します。デバッグオーバーレイには、チャンクの境界、歩行可能なタイル、カーソル下のタイル、タイミングのパネルが表示されます。インターフェースの言葉は、すべて英語です。

プレビューは本物

結果と食い違うゴーストは、ないほうがましです。そこでクライアントは、建設コマンドが何をするかを推測しません。コアは、同じ範囲について「これを実行したらどうなるか」を、タイルごとに、拒否するタイルにはその理由を付けて答えられます。プレビューを埋めるチェックは、コマンドが実際に実行されるときに使うルールと同じなので、両者がずれることはありません。クライアントは答えを描くだけです。コアに再度尋ねるのは、答えに影響するもの、つまりツール、ドラッグした範囲、階層、マップ自体が変わったときだけです。

この原則は、クライアントの他の部分も形づくっています。世界の状態を読み、コマンドを送り、自前のシミュレーションロジックは持ちません。いまのところコアはメインスレッドでティックし、クリックによるコマンドは、次のフレームの頭、世界がティックする前に適用されます。世界が人でいっぱいになってティック時間が跳ね始めたら、コアを専用スレッドに移す計画で、そのタイミングには計測した閾値を設けます。

自分で見つかる部屋

部屋の作り方は2通り検討しました。Prison Architect では、ゾーンを描くと、それが独自のタイルを持つ独立したオブジェクトになります。分かりやすい反面、刑務所ではうまくいきません。房棟全体が1つのゾーンになってしまい、壁を作り直すたびに塗り直しが必要になります。もう一つは、囲まれた領域を自動で見つける方法ですが、これでは壁のない中庭を指定できません。

私が選んだのは両者の折衷です。保存するのは、各タイルの部屋タイプだけ。独房、食堂、浴場、医務室、面会室、運動場などです。あとはすべて壁から導き出します。スペースとは、同じ階層で辺が接する開いたタイルの集まりで、マップの端や空洞に接していなければ「囲まれている」とみなします。部屋とは、1つのスペースの中にある同じタイプのタイルすべてです。壁とドアは境界であり、壁の下に塗られたタイプは保持されるものの無視され、壁を取り除けば復活します。

こうすると、何も教えなくても自然な挙動になります。部屋に壁を通せば、同じタイプの2つの部屋になります。壁を取り除けば、合体します。壁があった場所にドアを置いても、何も変わりません。有効でない部屋は削除されません。それは「ストレスなく」とは正反対だからです。代わりに赤で表示され、理由が添えられます。囲まれていない、他のタイプと共有している、狭すぎる、ドアがない、のいずれかです。矩形を塗ることも、囲まれたスペース全体をワンクリックで塗りつぶすことも、消すこともできます。

部屋の一覧は導出データなので、保存されず、ワールドのハッシュにも入りません。保存されるのは各タイルのタイプだけです。コマンドの後とシステムの後に再構築され、変化のない世界のチェックにはコストがかかりません。このマイルストーンでは、変更のあった階層をまるごと再計算していて、256×256 のマップで約0.5ミリ秒です。チャンクごとの処理は、経路のリージョンが同じ仕組みを必要とするまで待てます。守っているルールは、差分再構築が、全体再構築と同じ結果にならなければならない、というものです。

変わったところだけ作り直し、階層の切り替えはタダ

マップは、壁1枚ごとのオブジェクトではなく、階層ごと、チャンクごとに1つのメッシュとして描画されます。コアの各チャンクにはバージョンカウンタがあり、レンダラーは最後に描いたバージョンを覚えています。世界が変わると、より新しいバージョンのチャンクだけがキューに入り、キューはフレームごとの時間予算の範囲で処理されます。大きな変更は、一度に固まるのではなく、数フレームに分散されます。部屋タイプを塗ってもマップのバージョンは上がらないので、メッシュビルダーを起こすことはありません。

階層の切り替えも、同じ「やる量を減らす」考え方です。現在より上の階は、単に非表示にします。現在の階の壁は、シェーダーで1メートルの高さまで押し潰します。共有のマテリアル1つと高さの値1つなので、階層を切り替えたり、フルハイトの壁を切り替えたりしても、何も再構築されません。4つの階層を切り替えても、メッシュの再構築は0回です。

10センチの隙間

最終チェックで最初の1階建ての刑務所を試したとき、上の階に床があり、横から見ると、壁の上端とその上のスラブのあいだに細い明るい線が見えました。角では、床が宙に浮いているのが見えました。階層の切り替え表示を疑い、次にZファイティング、さらにスムージングを疑いました。

どれも違いました。床は厚みのない1枚の平らなクアッドで、その階の高さに10センチを足した位置に描かれていました。下の階の壁は、ちょうど3メートルまで伸びていました。次の階の床は、3メートル10センチから始まっていました。10センチ幅の隙間があり、そこから壁の明るい上面が見えていたのです。水平方向はすべて完璧に揃っていたので、気づくまでに時間がかかりました。

修正は、床を本物の厚みを持つスラブにして、その階の始まりまで下ろすことです。上面の高さは同じなので、マウスによるタイルの選択も、すべてのオーバーレイも変わりません。側面は、同じチャンク内で床や壁が接している場所では省略され、チャンクの境界では描かれるので、チャンクのメッシュが隣に依存することはありません。その結果、スラブの端に暗い10センチの縁が残ります。修正前後のスクリーンショットを見比べて、残すことにしました。床の端に見えるからで、実際にそのとおりです。

使い物になるか

このマイルストーンの最後のタスクは、マウスで1階建ての刑務所を建てて、苦痛かどうかを確かめることでした。おおむね、苦痛ではありませんでした。上の隙間以外でこの実験が見つけたのは、小さいけれど煩わしいことです。マウスで別のツールを選んでも、ツールボタンのハイライトが消えなかったこと、そして Escape で「新規ゲーム」ダイアログから戻れなかったこと。どちらも直しました。ポーズメニューは、あとで検討するアイデアのリストに入っています。

次は

次は M2、移動と日課です。世界にはリージョンと到達可能性、階をまたぐ経路探索、群衆のためのフローフィールドが加わります。コアにはようやく毎ティック本物の仕事ができ、ベンチマークのゲートも眠りから覚めます。ロードマップをご覧ください。

← #2 · 開発ログ一覧