Initial import
This commit is contained in:
commit
ab891a315c
773 changed files with 257255 additions and 0 deletions
550
Release/docs/OPEN_WORLD_CHUNK_V1.md
Normal file
550
Release/docs/OPEN_WORLD_CHUNK_V1.md
Normal file
|
|
@ -0,0 +1,550 @@
|
|||
# Open World Chunking V1
|
||||
|
||||
This document proposes a concrete v1 data model and editor/runtime architecture for replacing the current finite `map` model with a streamed open world.
|
||||
|
||||
The goal is not "fake endless maps glued together." The goal is:
|
||||
|
||||
- one continuous world coordinate space
|
||||
- chunked storage and loading
|
||||
- seamless editing across chunk boundaries
|
||||
- sparse height overrides
|
||||
- chunk-local save/write operations
|
||||
- renderer and editor surfaces that only load and draw what is nearby
|
||||
|
||||
## Plain-English Model
|
||||
|
||||
Think of the world as graph paper that goes on forever.
|
||||
|
||||
- The player and editor work in one global coordinate space: `(worldX, worldY)`.
|
||||
- That graph paper is cut into fixed-size square sheets called `chunks`.
|
||||
- Each chunk stores only the tiles, height patches, and instances that belong to that square.
|
||||
- The game and editor only load the nearby chunks.
|
||||
|
||||
The user should never feel like they are "switching maps." They should feel like they are scrolling around one world.
|
||||
|
||||
## V1 Recommendation
|
||||
|
||||
V1 should introduce a new top-level concept: `world`.
|
||||
|
||||
Current:
|
||||
|
||||
- `maps.json`
|
||||
- one map entry owns rows, layers, height layers, instances
|
||||
|
||||
Proposed:
|
||||
|
||||
- `worlds.json`
|
||||
- one world entry defines chunk rules and metadata
|
||||
- chunk files hold actual terrain/layer/instance data
|
||||
|
||||
Keep support for multiple worlds. Even in an "open world" game, multiple worlds are still useful for:
|
||||
|
||||
- overworld
|
||||
- interiors or special dimensions
|
||||
- test worlds
|
||||
- developer sandboxes
|
||||
|
||||
## Goals
|
||||
|
||||
- Seamless editing across arbitrary world coordinates
|
||||
- Automatic chunk creation when painting or placing far away
|
||||
- No giant monolithic world file for tile data
|
||||
- Chunk-local save/load for performance
|
||||
- Reuse current room-layer and height-patch ideas where possible
|
||||
- Replace minimap with world navigation, coordinates, and bookmarks
|
||||
|
||||
## Non-Goals For V1
|
||||
|
||||
- Infinite procedural terrain generation
|
||||
- Networked world streaming
|
||||
- Region compression or advanced binary storage
|
||||
- Fully general LOD terrain synthesis
|
||||
- Rewriting the whole engine to 3D or voxel storage
|
||||
|
||||
## Chosen Chunk Size
|
||||
|
||||
The chosen v1 chunk size is `32x32`.
|
||||
|
||||
Reason:
|
||||
|
||||
- simpler mental math in tooling and code
|
||||
- slightly better fit for common viewport batching
|
||||
- cleaner chunk index math
|
||||
- easier future optimization
|
||||
|
||||
`25x25` would also work, but v1 should standardize on `32x32` and avoid making chunk size configurable until the new architecture is stable.
|
||||
|
||||
## Coordinate Model
|
||||
|
||||
The world uses two coordinate spaces:
|
||||
|
||||
1. World coordinates
|
||||
- Absolute tile coordinates in the world
|
||||
- Example: `(73, -12)`
|
||||
|
||||
2. Chunk-local coordinates
|
||||
- Tile coordinates within a chunk
|
||||
- Example: chunk `(2, -1)` local tile `(9, 20)`
|
||||
|
||||
Conversion:
|
||||
|
||||
```txt
|
||||
chunkX = floor(worldX / chunkWidth)
|
||||
chunkY = floor(worldY / chunkHeight)
|
||||
|
||||
localX = worldX - (chunkX * chunkWidth)
|
||||
localY = worldY - (chunkY * chunkHeight)
|
||||
```
|
||||
|
||||
This is the core of the whole system.
|
||||
|
||||
## Proposed File Structure
|
||||
|
||||
```txt
|
||||
content/
|
||||
worlds.json
|
||||
worlds/
|
||||
overworld/
|
||||
world.json
|
||||
bookmarks.json
|
||||
chunks/
|
||||
0_0.json
|
||||
1_0.json
|
||||
0_1.json
|
||||
-1_0.json
|
||||
```
|
||||
|
||||
### `content/worlds.json`
|
||||
|
||||
Index of available worlds.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worlds": [
|
||||
{
|
||||
"id": "overworld",
|
||||
"name": "Overworld",
|
||||
"worldDir": "worlds/overworld"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### `content/worlds/overworld/world.json`
|
||||
|
||||
World-level metadata only.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "overworld",
|
||||
"name": "Overworld",
|
||||
"chunkWidth": 32,
|
||||
"chunkHeight": 32,
|
||||
"tileSize": 32,
|
||||
"defaultBackgroundTileId": "tile_grass_01",
|
||||
"spawn": { "x": 0, "y": 0 },
|
||||
"editor": {
|
||||
"defaultZoom": 1,
|
||||
"gridVisible": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### `content/worlds/overworld/bookmarks.json`
|
||||
|
||||
Replaces the current minimap's "jump around the finite map" role with saved navigation targets.
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worldId": "overworld",
|
||||
"bookmarks": [
|
||||
{ "id": "town_center", "label": "Town Center", "x": 120, "y": 84 },
|
||||
{ "id": "north_tower", "label": "North Tower", "x": 145, "y": 32 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Chunk file
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"worldId": "overworld",
|
||||
"chunkX": 2,
|
||||
"chunkY": -1,
|
||||
"width": 32,
|
||||
"height": 32,
|
||||
"backgroundTileId": "tile_grass_01",
|
||||
"roomLayers": [
|
||||
{
|
||||
"layer": 0,
|
||||
"name": "Ground",
|
||||
"rows": ["................................", "..."],
|
||||
"instanceIds": []
|
||||
},
|
||||
{
|
||||
"layer": 1,
|
||||
"name": "Walls",
|
||||
"rows": [" ", "..."],
|
||||
"instanceIds": ["inst_gatekeeper_001"]
|
||||
}
|
||||
],
|
||||
"heightLayers": [
|
||||
{
|
||||
"id": "height_001",
|
||||
"name": "Tower Level 1",
|
||||
"z": 1,
|
||||
"x": 10,
|
||||
"y": 8,
|
||||
"rows": [
|
||||
" 4444 ",
|
||||
" 4 4 ",
|
||||
" 4444 "
|
||||
]
|
||||
}
|
||||
],
|
||||
"instances": [
|
||||
{
|
||||
"id": "inst_gatekeeper_001",
|
||||
"templateId": "npc_gatekeeper_bubbles",
|
||||
"layer": 1,
|
||||
"x": 12,
|
||||
"y": 9,
|
||||
"record": {
|
||||
"name": "Bubbles",
|
||||
"spriteId": "npc_human_style_13",
|
||||
"faction": "dangerous_gatekeeper"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Why This Structure
|
||||
|
||||
This splits data by ownership:
|
||||
|
||||
- `world.json`: rules and metadata
|
||||
- `bookmarks.json`: editor navigation helpers
|
||||
- chunk files: actual editable world content
|
||||
|
||||
That keeps save operations local and avoids turning one file into a bottleneck.
|
||||
|
||||
## Editor Data Model
|
||||
|
||||
The editor runtime should stop treating one "map" as the active document.
|
||||
|
||||
Instead it should have:
|
||||
|
||||
```ts
|
||||
type OpenWorldSession = {
|
||||
worldId: string;
|
||||
chunkWidth: number;
|
||||
chunkHeight: number;
|
||||
tileSize: number;
|
||||
cameraWorldX: number;
|
||||
cameraWorldY: number;
|
||||
loadedChunks: Record<string, WorldChunk>;
|
||||
dirtyChunks: Record<string, boolean>;
|
||||
activeLayer: number;
|
||||
activeHeightLayerId: string;
|
||||
editingTargetKind: "room" | "height";
|
||||
bookmarks: WorldBookmark[];
|
||||
};
|
||||
```
|
||||
|
||||
The main change is that the editor should think in `world coordinates` first and `chunk ownership` second.
|
||||
|
||||
## Editing Rules
|
||||
|
||||
### Painting tiles
|
||||
|
||||
When the user paints:
|
||||
|
||||
1. Convert pointer position to `(worldX, worldY)`
|
||||
2. Resolve chunk and local coordinate
|
||||
3. Create chunk if it does not exist
|
||||
4. Edit the right chunk layer row
|
||||
5. Mark that chunk dirty
|
||||
6. Patch only the visible surfaces touched by the stroke
|
||||
|
||||
If a rectangle, circle, or fill crosses chunk boundaries, the brush simply touches multiple chunks in one action.
|
||||
|
||||
### Placing instances
|
||||
|
||||
When the user drops an instance:
|
||||
|
||||
1. Resolve world tile
|
||||
2. Resolve chunk
|
||||
3. Create chunk if missing
|
||||
4. Add instance to that chunk
|
||||
5. Mark chunk dirty
|
||||
|
||||
### Painting far away
|
||||
|
||||
Yes, the editor should create all needed empty chunks automatically if the user paints or places content in a region that has never been created before.
|
||||
|
||||
It should not create a giant range of intermediate chunks just because the camera moved there. Chunks should be created lazily:
|
||||
|
||||
- create on first edit
|
||||
- optionally create on explicit "stamp empty region" tooling
|
||||
|
||||
## Runtime Load Model
|
||||
|
||||
The game should not load the whole world.
|
||||
|
||||
Instead:
|
||||
|
||||
1. Determine the player's current chunk
|
||||
2. Load a chunk radius around the player
|
||||
3. Keep nearby chunks hot
|
||||
4. Unload distant chunks
|
||||
|
||||
Example:
|
||||
|
||||
- player is in chunk `(10, 4)`
|
||||
- active chunk radius is `1`
|
||||
- load chunks from `(9..11, 3..5)`
|
||||
|
||||
That gives a `3x3` neighborhood.
|
||||
|
||||
This is the same core idea the editor should use for panning and visible drawing.
|
||||
|
||||
## Renderer Model
|
||||
|
||||
The renderer should operate in visible chunk slices, not full-world arrays.
|
||||
|
||||
Suggested v1 pipeline:
|
||||
|
||||
1. Determine visible world tile bounds from camera
|
||||
2. Determine which chunks intersect that area
|
||||
3. Ensure those chunks are loaded
|
||||
4. Draw visible base layers from those chunks
|
||||
5. Apply active height patches for current Z
|
||||
6. Draw instances
|
||||
7. Draw overlays and cursors
|
||||
|
||||
## Surface Caching
|
||||
|
||||
The current tile-surface work already points in the right direction.
|
||||
|
||||
V1 should move to:
|
||||
|
||||
- one cached surface per visible chunk layer
|
||||
- one cached patch surface per height patch
|
||||
- redraw only invalidated chunks or patches
|
||||
|
||||
That is much better than treating the whole world as one giant canvas.
|
||||
|
||||
## Height Layer Model In Open World
|
||||
|
||||
The current direction of sparse patch-based height overrides is still the right one.
|
||||
|
||||
V1 should keep height layers separate from ordinary draw layers.
|
||||
|
||||
Normal chunk data:
|
||||
|
||||
- base room layers define the world at Z0
|
||||
|
||||
Height patch data:
|
||||
|
||||
- sparse overrides define what appears at higher Z values
|
||||
|
||||
When the player goes up a ladder:
|
||||
|
||||
- current Z changes
|
||||
- renderer continues drawing visible Z0 world
|
||||
- renderer applies any visible patch data matching that Z
|
||||
|
||||
This is exactly the kind of thing chunking helps with, because only nearby patches need to be considered.
|
||||
|
||||
## Instance Model
|
||||
|
||||
Instances should remain data-driven and chunk-owned.
|
||||
|
||||
Recommended v1 storage:
|
||||
|
||||
- store instances inside the chunk that owns their tile origin
|
||||
- keep instance `x` and `y` local to the chunk file
|
||||
- derive world position in memory
|
||||
|
||||
Alternative:
|
||||
|
||||
- store world `x` and `y` directly in the chunk file
|
||||
|
||||
Either can work. For consistency with chunk-local tile rows, chunk-local instance positions are cleaner on disk.
|
||||
|
||||
Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "inst_blacksmith_001",
|
||||
"templateId": "npc_blacksmith",
|
||||
"layer": 1,
|
||||
"x": 18,
|
||||
"y": 7,
|
||||
"record": {
|
||||
"name": "Mera"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## World Navigation UI
|
||||
|
||||
The current minimap should be replaced.
|
||||
|
||||
Recommended v1 navigation tools:
|
||||
|
||||
- current world coordinate display
|
||||
- jump to X/Y
|
||||
- bookmarks / waypoints
|
||||
- "center on player"
|
||||
- chunk grid overlay toggle
|
||||
- optional chunk overview panel showing loaded and dirty chunks
|
||||
|
||||
This is much more useful for an open world than a single minimap squeezed into a corner.
|
||||
|
||||
## Save Model
|
||||
|
||||
The editor should save only dirty chunks and world metadata.
|
||||
|
||||
Recommended split:
|
||||
|
||||
- save `world.json` only when world-level settings change
|
||||
- save `bookmarks.json` only when bookmarks change
|
||||
- save only touched chunk files when content changes
|
||||
|
||||
This is the core performance and safety advantage.
|
||||
|
||||
## Undo / History Model
|
||||
|
||||
History can no longer assume one finite map snapshot as the main unit.
|
||||
|
||||
Recommended v1 history operation format:
|
||||
|
||||
```ts
|
||||
type ChunkEditOperation = {
|
||||
type: "chunk_edit";
|
||||
chunkKey: string;
|
||||
before: Partial<WorldChunk>;
|
||||
after: Partial<WorldChunk>;
|
||||
};
|
||||
```
|
||||
|
||||
For multi-chunk brushes:
|
||||
|
||||
```ts
|
||||
type MultiChunkEditOperation = {
|
||||
type: "multi_chunk_edit";
|
||||
chunks: ChunkEditOperation[];
|
||||
};
|
||||
```
|
||||
|
||||
This is important because world editing will regularly cross chunk boundaries.
|
||||
|
||||
## API Shape
|
||||
|
||||
Suggested API direction:
|
||||
|
||||
- `GET /api/worlds`
|
||||
- `GET /api/world/:worldId`
|
||||
- `GET /api/world/:worldId/bookmarks`
|
||||
- `GET /api/world/:worldId/chunks?x=10&y=4&radius=1`
|
||||
- `POST /api/world/:worldId/chunk/:chunkX/:chunkY`
|
||||
- `POST /api/world/:worldId/chunks/batch-save`
|
||||
- `POST /api/world/:worldId/bookmarks`
|
||||
|
||||
The important change is that APIs become chunk-aware instead of map-aware.
|
||||
|
||||
## Migration From Current Map Model
|
||||
|
||||
The easiest migration path is not "rewrite everything at once."
|
||||
|
||||
Recommended migration steps:
|
||||
|
||||
1. Introduce `world` and `chunk` data structures alongside current maps
|
||||
2. Build an importer that converts one existing map into one or more chunks
|
||||
3. Keep existing tile/layer/height patch formats as similar as possible
|
||||
4. Build a separate open-world editor mode first
|
||||
5. Retire the old map model only after the new one is stable
|
||||
|
||||
For migration:
|
||||
|
||||
- one `100x100` map can become `4x4` chunks at `25x25`
|
||||
- or `4x4` chunks at `32x32` with padded edges
|
||||
|
||||
## What Must Be Rewritten
|
||||
|
||||
This is a major architecture change. The following systems would need meaningful refactors:
|
||||
|
||||
- popup bootstrap and document loading
|
||||
- editor runtime state
|
||||
- renderer surface caching
|
||||
- minimap/navigation UI
|
||||
- history model
|
||||
- persistence API
|
||||
- map switcher UX
|
||||
|
||||
The following ideas can be reused:
|
||||
|
||||
- tile symbol rendering
|
||||
- tile/instance palettes
|
||||
- sparse height patch logic
|
||||
- layer editing concepts
|
||||
- chunk/surface invalidation patterns
|
||||
|
||||
## V1 Risks
|
||||
|
||||
- Trying to preserve too much of the finite-map abstraction
|
||||
- Storing too much redundant empty chunk data
|
||||
- Keeping full-world snapshots in history
|
||||
- Letting chunk creation happen implicitly on camera movement
|
||||
- Rebuilding all visible chunk surfaces too often
|
||||
|
||||
## Recommended V1 Boundaries
|
||||
|
||||
To keep this realistic, v1 should do only this:
|
||||
|
||||
- one open world
|
||||
- fixed chunk size
|
||||
- chunk-local room layers
|
||||
- sparse chunk-local height patches
|
||||
- chunk-owned instances
|
||||
- bookmarks instead of minimap
|
||||
- batch save of dirty chunks
|
||||
- visible-chunk-only rendering
|
||||
|
||||
That is already a large and meaningful overhaul.
|
||||
|
||||
## Implementation Order
|
||||
|
||||
The safest order is:
|
||||
|
||||
1. Add new world/chunk schemas and API routes without removing current map routes
|
||||
2. Build a converter that turns one current map into one chunked world
|
||||
3. Add a chunk-aware loader in parallel with the current popup bootstrap
|
||||
4. Replace the popup minimap with coordinates, bookmarks, and jump controls
|
||||
5. Convert rendering from one-map surfaces to per-chunk surfaces
|
||||
6. Convert tile painting, height painting, and instance placement to world-coordinate edits
|
||||
7. Convert save/history to batch dirty chunks
|
||||
8. Only then remove the old finite-map assumptions
|
||||
|
||||
## Final Recommendation
|
||||
|
||||
This direction makes sense.
|
||||
|
||||
The strongest version of it is:
|
||||
|
||||
- stop thinking in "many maps stitched together"
|
||||
- start thinking in "one world partitioned into chunks"
|
||||
|
||||
That keeps the user experience seamless while keeping storage and rendering manageable.
|
||||
|
||||
If we choose to proceed, the next best step is:
|
||||
|
||||
1. finalize the exact chunk JSON schema
|
||||
2. build the map-to-world importer
|
||||
3. add a parallel open-world editor path before deleting the current map editor
|
||||
Loading…
Add table
Add a link
Reference in a new issue