地形生成システム設計
概要
本ドキュメントは、島の地形生成システムに関する設計と実装をまとめたものです。
GameElementDefinition.md が掲げる「ハードコードしない」「汎用エンジンに
任せる」「ファイル追加だけで拡張できる」という設計方針(2 節)に準拠し、以下を目的とします。
- シード値ありのランダム生成で無人島の地形を生成する
- 地形・構造物はすべて YAML でオブジェクト定義し、パック作成者がコードに触れずに拡張できる状態を保つ
本書が扱うのは、島の座標・軸・LocationType・パスネットワークを生成するアルゴリズムそのもの
(Domain.Generation、WorldObject に一切触れない生成時点だけの純粋な計算)です。生成された
Location(土地)が生成されたあとにどう振る舞うか(スロット構成・探索・道の発見・移動)は
ExplorationSystem.md が扱います。
本書は設計判断(なぜこのアルゴリズムか)を扱い、実際のクラス名・メソッド名を使った実装の呼び出し関係は
TerrainGenerationImplementation.md に切り出しています。
コードを読む・変更する際はそちらを参照してください。
実装は src/domain/generation/ 以下、定義データは
src/assets/world-codex/terrain_generation.yaml(生成パラメータ)・locations.yaml(土地・道の
object_defs)にあります。
1. 用語定義
| 用語 | 説明 |
|---|---|
| Axis(軸) | 標高・湿り気など、地点が持つ連続値パラメータの 1 次元。ジェネレータ(ノイズ/距離場など)によって値が決まる。値は 0〜100 の整数(後述) |
| Site(地点) | 座標と軸ベクトルのみを持つ、LocationType 未確定の中間状態のノード |
| 環境(Environment) | 軸ベクトルが表す性質そのもの。データ構造として実体化はしない(クラスや YAML 定義を持たない)。Site 同士の軸ベクトルの近さという関係性としてのみ存在する概念 |
| LocationType | 「草原」「洞窟」など、配置の定義(YAML)。プレイヤーには見えない設計者側の語彙 |
| Location(土地) | 実際に生成・命名された実体。「草原」「花咲く草原」のようにゲーム内で識別可能な名前を持つ。パスネットワークのノードでもある |
| Structure(構造物) | 内部に子 Location のグラフを持つ、特殊な Location。「島の中に埋め込まれた入れ子の島」として扱う(いつか、3.7 節参照) |
| Coordinate/Position | 単なる座標値。Site/Location が持つプロパティの一つ |
2. 生成パイプライン全体像
[generation_scopes (YAML)]
↓ サイト数の抽選 + 座標配置(外周リング + 内陸への散布)
[Site群] (座標のみ)
↓ Axis定義(YAML)のジェネレータ(距離場/ノイズ)を各座標でサンプリング
[Site群] (座標 + 軸ベクトル)
↓ guarantees(軸カバレッジの強制割当) → LocationType の axis_preferences との最近傍マッチング
↓ (フォールバック処理)
[Location群] (LocationType確定)
↓ Delaunay三角形分割 → MSTで間引き → 迂回率に応じて一部の辺を復活
[Locationネットワーク] (距離・移動時間つきパスで接続)
↓ LocationTypeの識別子(同型が複数ならvariants)で命名
[Location群] (命名済み)
↓ IslandSpawner が object_defs を spawn し、world.locations へ配置
↓ 辺ごとに path を両端へ生成し、undiscovered_fixtures(隠しスロット)へ配置(ExplorationSystem.md)
[実体化済みの島]
座標に依存する軸のジェネレータ(距離場・座標ベースのノイズ)は座標なしでは評価できないため、 パイプラインの順序は「座標決定が最上流」に統一しています。
構造物内部の生成はまだ動いていません(3.7 節)。島の生成と構造物内部の生成は、同一の生成ロジックを
スコープ別パラメータ(generation_scopes)で共有する設計です。
3. 各ステップの詳細仕様
3.1 Axis定義とジェネレータプリミティブ
Axis は汎用プリミティブの重み合成(generator.blend)で値を生成します。軸の値は 0〜100 の整数です
(ジェネレータの内部計算には実数を使い、AxisDef.range へ量子化する時点で整数に丸めます)。
実装済みのプリミティブは次のとおりです。
distance_field(reference: edge): 島の縁からの距離場(縁 = 0、中心 = 1)layered_noise: シード付きの格子値ノイズ(octaves/frequency/seed_offsetを持つ)。座標をSitePlacer.ISLAND_RADIUSで正規化してからfrequencyを掛けるため、frequencyは「島の直径あたりの 起伏の数の目安」として機能する
blob_scatter(局所的に濃い領域を作る散布)・modifiers(他の軸との合成演算)は、実際の10土地種の定義には
まだ必要にならなかったため未実装です(6 節)。
axes:
elevation:
range:
min: 0
max: 100
stretch_sites_to_range: true
generator:
blend:
- {type: distance_field, reference: edge, weight: 90}
- type: layered_noise
octaves: 3
frequency: 2
seed_offset: 11
weight: 10
stretch_sites_to_range: 1 回の生成で出たサンプルの最小・最大が range の両端へ来るよう、
量子化の前に引き伸ばします。宣言した値域が実際に現れることを保証できるのはこれだけです
——ジェネレータの値はサイトの座標で決まり、どこにサイトが置かれるかは事前に決まらないため、
式をどう組んでも「上端に届くサイトがある」とは言えません。値域が現実の単位へ読み替えられる軸
(標高のメートル、3.5 節)で要ります。
引き伸ばしは値の幅だけを保証し、分布の形は元の式のままです。elevation でノイズの重みが
距離場に対して低いのはそのためで、加算で乗せた起伏はそのまま海岸の高さのばらつきになります
(重み 30 では、海に接する土地が海抜 100 m を超えます)。
設計上の注意: 軸の種類・数はハードコードしません。Axis 定義自体が YAML で完結し、LocationType 側は
「言及した軸だけ気にする」設計にすることで、軸の増減に対して LocationType 定義が壊れないようにします
(3.2 節参照)。実際の定義は src/assets/world-codex/terrain_generation.yaml の axes を参照してください。
3.2 LocationTypeマッチング(軸ベース)
各 LocationType は、軸空間上の「理想点+許容範囲」として定義します。
location_types:
jungle:
object_def: jungle # 実体化に使う型(locations.yamlのobject_defsのid)。土地も他の
# あらゆる要素と同じobject_defsで表現される
applicable_scopes: [island]
# 移動コストの倍率(1 = 等倍。3.5節のtravel_minutesに使う)
move_cost: 1.6
axis_preferences:
humidity:
ideal: 90
tolerance: 20
weight: 120
elevation:
ideal: 30
tolerance: 30
weight: 60
# 言及しない軸は自動的に「無関心」(マッチング距離の計算に一切寄与しない)
hard_limits:
# 絶対的な除外条件。海岸帯には出ない
coastal_distance: {min: 16}
humidity: {min: 65}
マッチングは、言及した軸だけを対象に正規化した重み付きユークリッド距離による最近傍探索です。
D(type, site) = sqrt( Σ_i w_i * ((v_i - ideal_i) / tolerance_i)^2 / Σ_i w_i )
(i は type が axis_preferences に持つ軸のみを走る。v_i は site の軸値)
この式の設計判断:
toleranceは距離のスケールであり、閾値ではありません。「toleranceを超えたら除外」という ゲートの意味は持たせていません(絶対的な除外はhard_limitsだけが担います)。1つの軸でtolerance分 ずれると正規化距離に 1 単位分寄与する、というスケール変換だけを行います。Σw_iによる正規化がないと、言及する軸が少ない型ほど和の項数が減って距離が構造的に小さくなり常に 有利になる(軸を1つだけ言及する型が島全体を侵食する類の事故)ため、Σw_iで割って次元数バイアスを 取り除いています。- 同点(同じ距離)の場合は、
location_typesの宣言順で先に書かれた型が採用されます(決定的な挙動のため)。
stages(GameElementDefinition.md 6 節)は1次元の区間判定である一方、こちらは複数軸にまたがる多次元の
最近傍探索であるため、専用のマッチング処理(Domain.Generation.LocationTypeMatcher)として実装しています。
3.3 マッチできない場合の処理(フォールバック)
- 絶対的な除外は
hard_limitsだけが行います。hard_limitsを満たさないLocationTypeはマッチング候補 から外れます(axis_preferencesのtoleranceは除外条件ではないため、これ単独ではどの候補も除外しません)。 - フォールバック
LocationType:is_fallback: true。hard_limitsによって全ての候補が除外されたSiteはこれが受けます(自身のhard_limitsも無視する、最後の受け皿)。複数のis_fallback型がある場合、priorityが最大のものが選ばれます。 - フォールバックの機能化: 特定の軸にのみ強くマッチする専用
LocationTypeを パック側で追加すれば、 フォールバックに落ちていた領域へ自動的に誘導できます。「マッチしない領域」は不具合ではなく「新しいLocationTypeを定義すべき場所」というシグナルとして扱います。
3.4 出現数について(同じ型の抑制とguaranteesによる保証)
同じ LocationType は max_sites_per_type 個までにします。 同じ地形は環境も発見物も見た目も同じなので、
並べても島は広くなりません。上限で急に打ち切るのではなく、既に置いた同じ型の個数に応じてマッチング距離へ
割増(crowding_penalty、率)を掛け、上限へ届く前から他の型へ譲らせます。
- 割り当ての順番は最良距離の昇順です。「その型らしさ」が濃い
Siteから決まるので、譲るのは環境の 境目にいるSiteになります(Siteの並び順で決めると、譲る側が環境と無関係に決まってしまいます)。 - 上限は置けるかどうかの条件ではありません。 上限を守るとどの型も選べない
Siteは、上限を無視して 選び直します。絶対の条件はhard_limitsだけ、という 3.3 節の原則を崩さないためです。 guarantees(後述)で強制割当したぶんも個数に数えます。
これは重複を減らすだけでなく、島に出る地形の種類を増やします。上限が無いと軸空間の中央付近に理想点を持つ
型が大半の Site を取り、端に寄った型がほとんど出ません(実測値は
stats/terrain.yaml の location_type_counts)。
下限(最低出現数)のレンジ管理はしません。 軸ベースの最近傍マッチングは各 Site が独立に
判定されるため、個数の厳密な制御には本質的に向いていません。
一方で「島には必ず山が1つある」のような要求は、軸の分布(カバレッジ)を事後チェックするだけでは保証できません
(カバレッジがあっても、実際にそこへ Site が配置され、かつマッチングでその型が選ばれるとは限らないため)。
そこで、generation_scopes に guarantees(明示的な強制割当)を持たせ、最近傍マッチングの前に
確定的に処理します。
generation_scopes:
island:
guarantees:
- {location_type: mountain_peak, axis: elevation, pick: max, count: 1}
axis が pick(max/min)側の値を持つ Site から count 個を選び、location_type を強制的に割り当てます
(hard_limits を満たす Site を優先し、足りなければ満たさない Site からも補います。保証は絶対のため)。
確率的な再生成を使わないため、決定性(同じシード → 同じ島)と停止性(無限に再生成しない)の両方を
保っています。
どの保証も hard_limits を満たす Site を得られる配り方が在るなら、必ずそれを採ります。 保証を 1 件ずつ
順に見て、そのたびに候補の先頭から固定すると、先に見た保証が取った Site のせいで、後の保証だけが
hard_limits を満たさない Site へ回ります(最高標高の Site を山頂が取り、そこにしか置けない火口が
山腹へ降りる)。そうならないよう、取ると後の保証が置き場を失う候補は飛ばします(当てられる数は
二部グラフの最大マッチングで数えます。材料の割り当てと同じ形で、GameElementDefinition.md 13.1 節)。
それでも置き場の無い保証だけが、hard_limits を満たさない Site へ降ります。
取り合いになったときは、先に宣言した保証が優先します(宣言順は、書き手が決められる唯一の順序です)。
どちらが取っても両方が成り立つなら、先に宣言したほうが pick 側の Site を取ります。優先が効くのは
上の「置き場を失わせない」範囲までで、先の保証の望みより、後の保証が置けることが先です。
3.5 座標配置とパスネットワーク生成
島の大きさと歩く速さ【確定】
- 島の直径は 6.7 km(面積およそ 35 km²)。山がひとつある島として自然な大きさで、ロビンソン・ クルーソー島(約 48 km²)よりやや小さい大きさです。
- 島の最高点は海抜 400 m。 同じ縮尺で自然な高さです(青ヶ島は 8.7 km² で 423 m、ボラボラ島は 30 km² で 727 m)。
- 道の無い熱帯の地面を歩く速さは 4 km/h。
出どころ: #656(島の大きさと歩く速さ)
大きさと速さは、それぞれ別のキーで宣言する
上の値をどう持つかを決めたもので、覆すのに人間の判断は要りません。
generation_scopes.island の diameter_meters・elevation_top_meters・walk_meters_per_hour
として、それぞれ現実の単位で別々に宣言します。速さが当たるのは move_cost が 1.0 の土地です。
1つの値に縮尺と速さを兼ねさせると、どちらも外の
知識と突き合わせて検算できなくなるためです
(DesignPrinciples.md の「現実に単位があるものは、その単位で持つ」
「1つの原因に、複数の結果を兼ねさせない」)。
3.5.1 座標配置(海岸に囲まれ、かつ海岸過多にならない配置)
島は単純な円盤(半径 SitePlacer.ISLAND_RADIUS)とみなします。抽象座標系の直径(ISLAND_RADIUS の
2 倍)が diameter_meters に当たり、抽象 1 単位はそれをこの直径で割った長さになります。 ノイズも距離場も
ISLAND_RADIUS で正規化しているので、縮尺を変えても地形の見た目は変わりません。要求「海岸に囲まれた島を、海岸が多くなり
すぎないように生成する」を、次の配置枠の分離によって実現しています(円盤へ一様に散布すると、
面積比の関係で外周付近のサイトが多数を占めてしまい、単純な後処理だけでは制御しづらいため、配置そのものを
分けています)。
- 外周リング: 島を囲む海岸候補を、半径85%〜95%の円環上へ、等間隔+ジッタで配置します。個数は
generation_scopes.islandの想定サイト数の約35%(ただし4〜7個にクランプ)です。この個数の上限・下限が、 「島を囲むのに十分な数」と「海岸が多くなりすぎない」の両方を同時に満たす調整弁になっています。 - 内陸: 残りのサイトを、半径75%以内へベストキャンディデート法(Mitchell's best-candidate algorithm)で 散布します。候補は枠へ面積あたり一様に引き、既存のどのサイトからも最も離れた候補を毎回選ぶことで、 サイト同士が均等に散らばります。Poisson-disk sampling ではなくこの方式なのは、生成するサイト数を 直接指定できるためです(Poisson-disk は半径と面積から結果的な個数が決まるサンプラで、個数を 指定できません)。
海岸の数を決めているのは、この枠の分離そのものです。 内陸の枠(半径75%以内)と外周リング (85〜95%)の間に隙間があるので、内陸のサイトは海岸帯へ届きません。
内陸のサイトを中心へ寄せる調整弁は、宣言に持ちません。 候補の半径分布を中心寄りへ歪めても、
採るのは「既存のどのサイトからも最も離れた候補」なので、寄せたぶんは間隔の側で戻されます
——採られたサイトの半径は、候補の半径ほどには動きません。端から端まで振っても、海岸帯の土地の
割合は動かず、道の網目も島の顔が変わるほどには動きません。動くのはどの型の土地が島に出るかの
ほうで、
これは「中心へ寄せる」という名前からは読めない効き方です。名乗った働きが名乗りに見合わず、見合う
働きは名前から読めない弁になるので、置きません(実測は
DesignNotes.md「地形生成」)。内陸の散布を中心寄りへ振りたくなったら、
候補の分布ではなくベストキャンディデート法が候補を採る基準の側を変えます。
外周リングに配置されたサイトは、coastal_distance 軸のサンプリング後に generation_scopes.island.coast_band
以下へクランプされます(hull_coast: true の場合)。これにより、海岸型 LocationType(hard_limits で
coastal_distance が海岸帯以下と定めている型)が必ずこの位置に配置され、「島が海岸で囲まれる」ことを
配置の構造そのもので保証しています。
3.5.2 パスネットワーク
-
全
Siteに対して Delaunay 三角形分割を実施します(Domain.Generation.DelaunayTriangulator、 Bowyer-Watson 法)。数学的に辺が交差しないという性質を、交差なしパスネットワークの土台に使います。 -
間引きは以下の順で行います。
- 最小全域木(MST、Kruskal法)は必ず残します(到達性の保証)。
- MST 以外の Delaunay 辺は、距離の短い順に走査し、「現在のグラフでのその2点間最短距離(Dijkstra)が、
直結距離の
generation_scopes.island.extra_edge_detour_factor(倍率)を超える」場合だけ復活させます (大回りを強いられている場合に、近道・分岐を作る)。復活させる辺も Delaunay 辺の部分集合であるため、 グラフは常に交差なし(平面)のままです。この閾値は上げるほど道が減り、下限は MST だけが残る 平均次数 ≒1.9 です。実測値はstats/terrain.yamlのsite_degree(npm run stats:terrainで再生成)にあり、閾値を動かしたら再生成します。
-
各エッジには
distance_meters(2 地点間のユークリッド距離、m)とtravel_minutes(移動時間、分)を 持たせます。距離が先にあり、速さで割ると時間が出ます。歩く分 = distance_meters × (moveCostA + moveCostB) / 2 / walk_meters_per_hour × 60 登り下り = |elevationA − elevationB| × metersPerElevationUnit / climb_meters_per_hour × 60 travel_minutes = round_to_tick( 歩く分 + 登り下り )(
minutes_per_tick(core.yaml)単位に丸め、tick 単位の時間経過と噛み合う粒度に揃えます。 最低 1 tick——どんなに近い土地の間でも 1 刻みはかかります。)move_costはその土地を進む遅さの倍率です(1.0 が開けた土地=walk_meters_per_hourそのままの速さ、密林 1.6、山頂 2.5)。metersPerElevationUnitはelevation_top_meters ÷ 標高軸の値域です(島のelevation_top_metersは 400 m)。 どの軸を標高として読むかはelevation_axisが指します(エンジンは軸の名前を知りません)。 軸の両端が実際に出る(3.1 節のstretch_sites_to_range)ので、島の最低点は必ず海抜 0 m、 最高点は必ず 400 m になります。- 登り下りは対称です。道は両端に2つあるので向きは表せますが、行きと帰りで時間が変わると往復の
勘定が全部2倍に複雑になります。
climb_meters_per_hourは 600 m/h(ネイスミスの法則)で、 海岸の土地から山頂まで最短経路で登ると、水平距離とは別に登るぶんの時間がかかります。 - 道1本あたりの距離・登り・所要時間の実測の分布は
stats/terrain.yamlのedge。
辺同士の交差を解決する処理はありません。採用する辺は常に Delaunay 辺の部分集合であるため、交差は数学的に 起こりません。Delaunay に含まれない任意のショートカット辺を追加したい要求が出てきた場合に、その時点で 改めて設計します。
道オブジェクト自体(path object_def)の生成・探索による発見・移動アクションは ExplorationSystem.md を
参照してください。
回り道の量は、行き止まりが消える下の線で決まる
上の extra_edge_detour_factor をどの値にするかを決めたもので、覆すのに人間の判断は要りません。
2 とします。決めているのは下の線で、上の線は今のところ遠くにあります(どちらも500シードの実測)。
- 下から: 1.5 では行き止まりを持つ島が半分を切り、1.2 ではほぼ消えます。行き止まりが無い島は
どこへでも回り道があるので、島ごとの形の差が移動の勘定に現れません(
DesignPrinciples.md「難易度は、数値ではなく配置の散らばりで作る」)。2 では島の9割以上が行き止まりを持ちます。 測った刻みの中で、ここを満たすいちばん下が 2 で、上げるほど行き止まりは増えます (1.5 と 2 の間は測っていないので、2 が下限だとは言えません)。 - 上から: 道が減るほど最短経路は伸び、いずれ拠点から最も遠い土地への往復が1日の屋外の枠に
収まらなくなります(
ContentSkeleton.md8.3.1節の確定が破れる)。 ただし今の消耗では 8 倍まで上げてようやく届かない島が出ます。
上の線は生存の採取の量で動きます。 往復に使えるのは屋外の枠から1日を賄う採取を引いた残り
(stats/terrain.yaml の daily_budget)なので、採取が重くなれば上限は下りてきます——採取が
枠の3分の1を占めるほどになると、上限は 2.5 あたりまで下ります。この節の 2 は下の線から出ているので、
そこが動いても揺れません。
往復の実測は stats/terrain.yaml の base_farthest_round_trip
で、上の線を割ったときに落ちるのは tests/generation/terrainGenerator.test.ts の
「どの島でも、拠点から最も遠い土地まで日帰りで往復できる」です。
3.5.3 砂浜と岸壁を分ける高さ
海岸の型が名乗る高さを決めたもので、覆すのに人間の判断は要りません。
砂浜は標高軸の 5 まで、岸壁は 6 からとします。 どちらも hard_limits の elevation で、
同じ高さには出ません。
引く理由は分布の調整ではなく、名前が嘘になるからです。線を引く前は、砂浜も岸壁も海岸帯の高さ
(海抜 0〜68 m)のどこにでも出ていました——海抜 60 m の砂浜と、海抜 0 m の岸壁が同じ島に並びます。
岸壁とは海へ落ち込む崖のことなので、高さは名前そのものです。 砂浜も同じで、浜は海面の高さに
あります。砂浜の上限は浜の背後の段丘まで含めた高さで、岸壁の下限はその1つ上の目盛りです(軸の1目盛りが何 m にあたるかは
3.5.2 節の metersPerElevationUnit)。
岩海岸には線を引きません。 「岩だらけ」は高さではなく地面の性質なので、高さを名乗っていません。 海岸帯のどの高さにも出られる型が1つあることで、線を引いても行き場を失うサイトは生じません。
代償は測ってあります。 海岸帯のサイトが選べる型が3つから2つへ減るぶん、max_sites_per_type
(3.4 節)を守るとどの型も選べないサイトが出て、同じ海岸の型が上限より1つ多く並ぶ島が出ます
(500 シードのうち 2.2%、超えるのは常に1つまで)。3.4 節のとおり上限は絶対の条件ではないので、
これは仕様どおりの逃げ道です。逃げ道が常用されていないことは
tests/generation/terrainGenerator.test.ts の「同じ地形が並びすぎず、上限を外れるのは例外に留まる」が
見張ります。
型ごとの海抜の実測は stats/terrain.yaml の site_elevation_by_location
で、線が消されたときに落ちるのは同ファイルの「砂浜と岸壁は、同じ高さには出ない」です。
3.6 命名処理
SiteをLocationTypeごとにまとめます。- その型が島に 1 つだけなら、型の識別子だけが名前になります(表示は「草原」)。
- 複数あるなら、その型の
variants(亜種)をシャッフルして 1 つずつ配ります(表示は「木苺の草原」)。 variantsが足りなければ、残りに通し番号を付けます(表示は「草原(第四)」)。名前として読めないので、 亜種は想定される個数ぶん用意します。
決まるのは識別子の組み合わせだけで、表示文字列は持ちません。 WorldCodex は識別子だけを持つという
規約(Localization.md)に従い、Site.name(LocationName)は「型・亜種・通し番号」
を持ち、文字列の組み立ては Localization.locationName が行います。
名前に島のどこに在るかを出しません。 地形の把握はプレイヤー自身の仕事なので、位置が分かる修飾語を 付けると、行き先の名前を見ただけで島の形が割れてしまいます。亜種の名前も同じ制約に従います (対応表の側で自動テストが見張ります)。
亜種(variants)
亜種は識別子と、実体化した土地へ書き込むプロパティの上書きの組です。IslandSpawner が spawn した
土地へ props を書き込み、探索の抽選がその値を候補の weight として読みます
(locations.yaml の weight: {prop: ...}。ExplorationSystem.md 2 節)。表示名は対応表が持ちます
(Localization.md の location_texts)。
sandy_beach:
object_def: sandy_beach
variants:
# 素の重みは13<!-- codex: locations.yaml object_defs.sandy_beach.props.palm_find.value -->
- {id: palm, props: {palm_find: 26}}
- {id: white_sand} # 素の亜種(名前だけが変わる)
- 名前と中身を合わせます。 「木苺の森」で木苺が増えないなら、名前はただの飾りです。合っていれば、 プレイヤーは名前から中身を覚えられます(地図を持たないゲームなので、名前が土地を指す唯一の手掛かりです)。
- その亜種にしか無いものは作りません。 同じ型は島に高々
max_sites_per_type個で、型ごとの出現率も 5 割前後です(3.4 節)。亜種に固有の発見物を紐づけると「島のどこにも無い」が普通に起きます。倍率も 控えめ(素の重みの 0.5〜2 倍程度)にとどめます。 - 上書きするプロパティがその土地の
object_defに無ければロード時にエラーにします(持たない プロパティへの書き込みは黙って消えるため)。 - 亜種は同じ地形の中の個体差なので、絵は型で共通です。見た目まで変えたくなったら、
location_typesを分けるか、亜種を絵の切り替えにも使うかの選択になります(後者は未実装)。
3.7 構造物(Structure)生成【いつか: 洞窟内部】
generation_scopes にスコープごとのパラメータプリセットを持たせる仕組み自体は実装済み
(現状 island のみ定義)で、structure_interior のようなスコープを追加すれば、同じ TerrainGenerator
一式が同一ロジックのまま再帰生成できる設計です。探索の発見物 shallow_cave(fixture かつ location)は
それ自体が入れる場所ですが、そこから奥へ続く道と、内部の子 Location グラフはまだ生成しません
(Someday.md)。
4. これまでに合意し、実装した設計原則
- 地形定義(
LocationType)と実体(Location)は必ず呼び分けます。裸の名詞=実体、〜Typeサフィックス=定義、 という命名規則を全概念で徹底します。 - 「似た環境が隣り合う」は、隣接しやすさテーブル(affinity、不採用)ではなく、軸の空間的連続性そのもの
(
layered_noiseが空間的に連続な値を返すこと自体)によって自然に実現しています。 - Axis の生成ロジックは汎用プリミティブ(
distance_field/layered_noise)の組み合わせに還元しています。 新しい軸・新しいLocationTypeは YAML の追加だけで導入でき、Domain.Generation側のコード変更は不要です。 - 島全体の生成と構造物内部の生成は、同一の生成ロジックをスコープ別パラメータで使い分ける設計です (構造物内部スコープの追加自体は 3.7 節のとおり「いつか」)。
- 軸の値は 0〜100 の整数です(3.1 節)。
稀な極端環境(weirdness 軸)は未実装です。「高周波ノイズで稀な異常環境を作る」という当初案は、
高周波ノイズが本質的に「多数の小さな異常点」を作るもので「稀」にはならないという矛盾を含むため、
実装方針を保留にしています(6 節)。
5. 生成パイプラインの前段・後段(範囲外の確認事項)
本書が扱うのは Axis 定義から Location の実体化・命名までの地形生成パイプラインそのものです。生成された
Location に対する探索アクション・道の発見・移動、キャラクタ・アイテム・設置物のスロット構成は
ExplorationSystem.md で扱います。Structure(洞窟等)内部の生成は 3.7 節の
とおり「いつか」で、範囲外です。
6. 未決事項・今後の検討課題
weirdness軸の具体設計: 稀な異常環境をどの程度の頻度・強度で発生させるか(4 節参照。「高周波ノイズ」 という当初案は矛盾を含むため再検討が必要)。blob_scatter・modifiersプリミティブ: 局所的に濃い領域(汚染地帯等)を作る散布や、軸同士の合成演算 は、必要になった時点でDomain.Generation.AxisSampler/GeneratorLayerTypeへ追加する。- Axis の計算コスト: 軸の数・
Site数が増えた際のサンプリングコスト、キャッシュ戦略(現状はSite数が高々20のため未検討)。 generation_scopes.island以外の生成スコープのパラメータ:islandの値は測って決まりました (extra_edge_detour_factorは 3.5.2 節)が、structure_interior(3.7 節)のような他のスコープの 値は、そのスコープが動くまで決まりません。