Kimetron Isle という 2D 配置エンジンの上に、ユニット移動の仲裁層「SAGBAL」を作っています。前回は三層分離(App = 意味 / SAGBAL = 事象 / Kimetron = 物理の真実)の話を書きました。
今回は中身です。毎ティック、全ユニットから「この1歩を取りたい」という意図が集まってくる。これをどう裁くか——機構の設計を書きます。
総意: 行き先の取り合いはスコアで競る
基本はシンプルです。各ユニットの意図を「進行ベクトル + 行き先の矩形」として集約し、行き先の矩形が重なるペアを「競合中」とします。そして関係者のスコアを比較して、最大者に通過権。これを層内では「総意」と呼んでいます。
スコア自体は App が決める数値です。プレイヤー操作の P は 90、徘徊スライムは 10、といった具合です。層は「誰が偉いか」を知りません。知っているのは数値の比較だけ。
譲りヒステリシス: 一度負けたらしばらく譲る
競合に負けた側はどうなるか。何もしないと毎ティック同じ競りが起きて、勝者・敗者が高速で入れ替わる振動になります。これを防ぐため、敗者は元の意図を一時抑制します。抑制は時間で減衰して回復しますが、阻害の原因が消えた時点で即回復——相手が去ったあとも譲り続けるのは不自然なので、譲りは原因と紐付いています。
「一度譲ったらしばらく譲り続ける」だけで、勝者は開いた隙間を通り抜け、渋滞は進んでいきます。
行き詰まりの検出と相互退避
意図の矩形が重なってもスコアでは決着がつかないことがあります。お互いがお互いの占有域を踏もうとする「閉路(デッドロックサイクル)」です。
検出は依存グラフでやります。A が B を塞ぎ、B が A を塞ぐ——そういう2体閉路を見つけたら、両者敗北として各々を「来た方向への反射 + 微小角」で退避させます。
この微小角はランダムではなく幾何から取るのがポイントです。「相手→自分ベクトルの進行方向垂直成分」つまり自分がすでに逸れている側へ倒す。デッドオン(真正面からの衝突)では「相手方向の左側」が互いに反対のワールド方向になるので、idハッシュも使わず決定的に非対称になれます。乱数を使わないので、同じ初期状態からのリプレイが完全に再現します。
右側通行: 世界規約としての keepSide
v0.2.0 で deadlock を実装した直後、デモの門下で対面衝突が頻発しました。当初「ユニットの重み(スコア)が等しいから競り負けが続くのでは」と疑ったのですが、解析してみると違いました。デッドロックはスコアを見ない幾何関係で、発生要因は「全ユニットが門の中央という同一の via 点を目指す」ことでした。
そこで v0.3.0 で入れたのが Policy.keepSide です。行き詰まった2体が退避する向きを「それぞれ自分の進行方向に対して右(または左)側」に固定します。ペアごとの交渉ではなく世界の通行規則にしてしまうと、対向する流れは自然にレーンへ分かれます。デモでは 'right' を採用して、60秒走行で deadlock が激減するのを確認しました。
「総意がカバーしない範囲を世界規約がカバーする」——この棲み分けが後の設計を支えます。
閉路にも総意を: 爆発した者は押し通す
v0.4.0 で設計をひとつ修正しました。従来、閉路検出はオークションをバイパスして常に「両者退避」していました。これが残念な挙動を生みます——たとえば一方が「爆発」(後述)してスコアが +100 されているのに、閉路だからと両者が退避して、また同じ場所で衝突し直す。
今は閉路も総意に戻します。スコアに差があれば負けた方だけが逸れて、勝者は空いた隙間を通り抜けます。同点のときだけ従来通りの両者退避です。イベントログでは deadlock → winner と deadlock — both sidestep の違いとして見えます。
爆発: 耐えた末の加点
詰まり続けたユニットはストレスを溜めます。閾値(12)を超えると爆発——スコアに +100 の臨時加点を得て、次の競りを必ず押し通します。「じっと耐えた末に押し通る」を機構化したものです。
重要なのは、単に元のスコアに戻すだけでは永久に負け続ける、ということです。加点でなければ飢餓(starvation)は解消されません。爆発は「いつか勝てる」ことを構造的に保証する飢餓対策でもあります。
実機では爆発しても閉塞が解けず、減衰→再蓄積→再爆発を繰り返すユニットも観測できました。爆発が「出口」になったからこそ、次の問題が見えます——壁に囲まれた完全閉塞は、爆発しても解けません。
完全閉塞は拡散でほぐす
四方すべてが塞がって「動きたいのに動けない」ユニットは、周囲へ「動けません」という信号を発信します(boxed 事象)。信号を受けた近くのユニットは、距離減衰する斥力場で少しずつ離れていきます(diffuse 事象)。押し出されたユニットが隙間を開けると、boxed だったユニットがそこへ動ける。
ゲーム的に言うと「周囲の NPC がそっとどく」です。層が組み立てているのは発信と斥力の仕組みまでで、「それは会話なのか威嚇なのか」の解釈は App 側です。
プレイヤーも競りの参加者
最後に、外部からの手の扱いです。ドラッグ中のプレイヤーキャラ P の移動は setIntent で意図として層に登録されます(スコア 90)。層は外部の手を直接動かしません——意図は競りに参加するが、層には動かせない。徘徊者は他のユニットと同じように P をブロッカーとして認識し、道を譲り、逸れ、場合によっては手前で爆発します。
「プレイヤーだけが特別な近道」を作らずに、同じ仲裁を通す。物理の真実は全員に等しい、という Kimetron 側の思想をそのまま層に持ち上げた形です。
つづく
第3回は実験の話です。建物で区切られた街路網——つまり迷路——にスライムを放つと何が起きるか。設計が壊れるか、機能するか。「迷路デモのストレステストで分かった、層の境界は健全だった」という、実は少し意外な結論を書きます。
