feat: stream chunks in-game, render 3D catalog props, and ground chunk coordinates
checkpoint / checkpoint (push) Successful in 5m54s

This commit is contained in:
2026-09-16 21:31:33 +00:00
parent e54ff6eeb2
commit d2b62c5d25
5 changed files with 412 additions and 56 deletions
+14 -17
View File
@@ -54,20 +54,16 @@ var is_dirty: bool = false
func _ready() -> void:
# Position this chunk scene at its world-space origin
global_position = bounds.position
# Set up collision shape for the chunk bounds
_setup_collision()
# Position this chunk scene at its 3D world ground coordinates
global_position = Vector3(
float(coord.x * Chunk.CHUNK_SIZE_X),
float(coord.z * Chunk.CHUNK_SIZE_Z),
float(coord.y * Chunk.CHUNK_SIZE_Y)
)
func _setup_collision() -> void:
# Add a collision shape for the chunk's bounding box
var collision := CollisionShape3D.new()
var shape := BoxShape3D.new()
shape.size = bounds.size
collision.shape = shape
collision.name = "ChunkBounds"
add_child(collision)
pass
## Add a tile layer to this chunk scene.
@@ -91,9 +87,10 @@ func add_object(obj: WorldObject) -> void:
func _add_static_object(obj: WorldObject) -> void:
var static_obj := StaticObjectNode.new()
static_obj.world_object = obj
static_obj.position = obj.position
static_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w)
static_obj.scale = obj.scale
# Map chunk coordinates (X_ground, Y_ground, Z_height) to Godot 3D (X, Y_height, Z_ground)
static_obj.position = Vector3(obj.position.x, obj.position.z, obj.position.y)
static_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.z, obj.rotation.y, obj.rotation.w)
static_obj.scale = Vector3(obj.scale.x, obj.scale.z, obj.scale.y)
static_obj.name = "static_%s" % obj.id
add_child(static_obj)
@@ -101,9 +98,9 @@ func _add_static_object(obj: WorldObject) -> void:
func _add_dynamic_object(obj: WorldObject) -> void:
var dynamic_obj := DynamicObjectNode.new()
dynamic_obj.world_object = obj
dynamic_obj.position = obj.position
dynamic_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.y, obj.rotation.z, obj.rotation.w)
dynamic_obj.scale = obj.scale
dynamic_obj.position = Vector3(obj.position.x, obj.position.z, obj.position.y)
dynamic_obj.quaternion = Quaternion(obj.rotation.x, obj.rotation.z, obj.rotation.y, obj.rotation.w)
dynamic_obj.scale = Vector3(obj.scale.x, obj.scale.z, obj.scale.y)
dynamic_obj.name = "dynamic_%s" % obj.id
add_child(dynamic_obj)
+92 -13
View File
@@ -37,12 +37,65 @@ var _status_label: Label = null
var _build_editor: InGameEditor = null
var _stream_manager: ChunkStreamManager = null
func _ready() -> void:
_show_build_stamp()
if Session.editing:
_open_editor_for_world()
elif Session.active:
_setup_networked()
else:
if Session.active:
_setup_networked()
_setup_chunk_streaming()
func _setup_chunk_streaming() -> void:
if Session.world_id == null or int(Session.world_id) <= 0:
return
if _stream_manager != null and is_instance_valid(_stream_manager):
return
_stream_manager = ChunkStreamManager.new()
_stream_manager.name = "ChunkStreamManager"
_stream_manager.player = _player
_stream_manager.streaming_radius = 2
add_child(_stream_manager)
var chunk_source := HttpChunkSource.new()
chunk_source.name = "HttpChunkSource"
chunk_source.world_id = str(Session.world_id)
chunk_source.room_slug = "main"
chunk_source.stream_manager = _stream_manager
_stream_manager.add_child(chunk_source)
_stream_manager.set_server_rpc(chunk_source)
_stream_manager.is_active = true
_stream_manager.chunk_loaded.connect(_on_chunk_loaded)
_stream_manager._load_chunk(Vector3i.ZERO)
func _on_chunk_loaded(_coord: Vector3i, chunk_scene: Node) -> void:
if chunk_scene is ChunkScene:
var cs := chunk_scene as ChunkScene
if not cs.world_objects.is_empty() or not cs.tile_layers.is_empty():
_hide_default_test_props()
func _hide_default_test_props() -> void:
var props := get_node_or_null("Level/Props")
if props != null and props is Node3D:
(props as Node3D).visible = false
for child in props.get_children():
if child is CollisionObject3D:
(child as CollisionObject3D).set_collision_layer_value(1, false)
(child as CollisionObject3D).set_collision_mask_value(1, false)
var chest := get_node_or_null("Level/ScriptedChest")
if chest != null and chest is Node3D:
(chest as Node3D).visible = false
if chest is CollisionObject3D:
(chest as CollisionObject3D).set_collision_layer_value(1, false)
(chest as CollisionObject3D).set_collision_mask_value(1, false)
func _unhandled_input(event: InputEvent) -> void:
@@ -89,7 +142,29 @@ func _open_build_mode() -> void:
var room_slug := "main"
var coord := Vector3i.ZERO
var model := ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_player.set_physics_process(false)
_player.set_process_unhandled_input(false)
var save_service: ChunkSaveService = null
var model: ChunkEditorModel = null
var base_revision := 0
if int(world_id_str) > 0:
save_service = ChunkSaveService.new()
save_service.world_id = world_id_str
save_service.room_slug = room_slug
add_child(save_service)
var fetch_result: Dictionary = await save_service.fetch_chunk(coord)
if fetch_result.get("ok", false) and fetch_result.get("model") != null:
model = fetch_result["model"]
base_revision = int(fetch_result.get("revision", 0))
if model == null:
model = ChunkEditorModel.create_empty(32, 32, world_id_str, room_slug, coord)
base_revision = 0
var catalog := EditorObjectCatalog.new()
var err := catalog.load_catalog()
if err != OK:
@@ -97,20 +172,12 @@ func _open_build_mode() -> void:
var core := EditorCore.new(model, catalog)
_build_editor = InGameEditor.new(core)
if int(world_id_str) > 0:
var save_service := ChunkSaveService.new()
save_service.world_id = world_id_str
save_service.room_slug = room_slug
add_child(save_service)
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, 0)
if save_service != null:
_build_editor.setup_persistence(save_service, world_id_str, room_slug, coord, base_revision)
_build_editor.closed.connect(_close_build_mode)
add_child(_build_editor)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
_player.set_physics_process(false)
_player.set_process_unhandled_input(false)
## Shared teardown for both close paths (F2 again, or the editor's own
## Close button/`closed` signal) -- safe to call twice: `queue_free()` on an
@@ -127,6 +194,14 @@ func _close_build_mode() -> void:
_player.set_process_unhandled_input(true)
_player._refresh_mouse_mode()
if _stream_manager == null:
_setup_chunk_streaming()
elif is_instance_valid(_stream_manager):
var source := _stream_manager.get_node_or_null("HttpChunkSource") as HttpChunkSource
if source != null and Session.world_id != null and int(Session.world_id) > 0:
source.world_id = str(Session.world_id)
_stream_manager.force_reload_chunk(Vector3i.ZERO)
## "Edit world" entry point from the world browser
## (world_persistence_impl.md §6 D4 / §9 "Client/editor task"). Reached only
@@ -198,6 +273,10 @@ func _leave_world() -> void:
## Close the ENet connection and stop it from calling us back mid-teardown.
func _teardown_net() -> void:
if _stream_manager != null and is_instance_valid(_stream_manager):
_stream_manager.clear_all_chunks()
_stream_manager.queue_free()
_stream_manager = null
if _net == null or not is_instance_valid(_net):
return
if _net.server_disconnected.is_connected(_on_server_lost):
+27 -8
View File
@@ -263,9 +263,9 @@ func _build_chunk_collision(parent: Node3D, coord: Vector3i, data: ChunkDataDeco
var body := StaticBody3D.new()
body.name = "chunk_%d_%d_%d" % [coord.x, coord.y, coord.z]
var chunk_origin := Vector3(
coord.x * Chunk.CHUNK_SIZE_X,
coord.y * Chunk.CHUNK_SIZE_Y,
coord.z * Chunk.CHUNK_SIZE_Z
float(coord.x * Chunk.CHUNK_SIZE_X),
float(coord.z * Chunk.CHUNK_SIZE_Z),
float(coord.y * Chunk.CHUNK_SIZE_Y)
)
for layer in data.tile_layers:
@@ -282,20 +282,39 @@ func _build_chunk_collision(parent: Node3D, coord: Vector3i, data: ChunkDataDeco
var box := BoxShape3D.new()
box.size = Vector3.ONE
shape.shape = box
shape.position = chunk_origin + Vector3(x + 0.5, y + 0.5, z + 0.5)
shape.position = chunk_origin + Vector3(x + 0.5, z + 0.5, y + 0.5)
body.add_child(shape)
_collision_shapes_built += 1
for obj in data.objects:
if obj.dynamic:
continue # dynamic templates are spawnable entities, not static geometry
if bool(obj.properties.get("walkable", true)):
if obj.type == "spawn_point" or bool(obj.properties.get("walkable", false)):
continue
var shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = obj.scale
shape.shape = box
shape.position = obj.position
var aid := str(obj.asset_id).to_lower()
var cat_id := str(obj.properties.get("catalog_id", "")).to_lower()
if "lamp" in aid or "lamp" in cat_id:
box.size = Vector3(0.5, 3.2, 0.5)
shape.shape = box
shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 1.6, obj.position.y)
elif "tree" in aid or "tree" in cat_id:
box.size = Vector3(0.8, 4.0, 0.8)
shape.shape = box
shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 2.0, obj.position.y)
elif "chest" in aid or "chest" in cat_id:
box.size = Vector3(1.0, 0.8, 0.6)
shape.shape = box
shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 0.4, obj.position.y)
elif "rock" in aid or "rock" in cat_id:
box.size = Vector3(1.2, 1.0, 1.2)
shape.shape = box
shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + 0.5, obj.position.y)
else:
box.size = Vector3(obj.scale.x, obj.scale.z, obj.scale.y)
shape.shape = box
shape.position = chunk_origin + Vector3(obj.position.x, obj.position.z + obj.scale.y * 0.5, obj.position.y)
body.add_child(shape)
_collision_shapes_built += 1
+275 -9
View File
@@ -29,16 +29,282 @@ func _ready() -> void:
## Load and attach the object's mesh.
func _load_mesh() -> void:
_mesh_instance = MeshInstance3D.new()
_mesh_instance.name = "mesh"
# 1. Try to load the asset directly if it exists on disk
var asset_node := _try_load_asset(world_object.asset_id)
if asset_node != null:
add_child(asset_node)
if asset_node is MeshInstance3D:
_mesh_instance = asset_node as MeshInstance3D
return
# Try to load the asset by its id (path). Fall back to a placeholder.
var mesh := _load_asset_mesh(world_object.asset_id)
if mesh == null:
mesh = _make_placeholder_mesh()
# 2. Build a stylized, recognizable 3D representation based on catalog ID / asset ID
var cat_id := _get_catalog_id()
_build_procedural_object(cat_id)
_mesh_instance.mesh = mesh
add_child(_mesh_instance)
func _get_catalog_id() -> String:
if world_object == null:
return ""
if world_object.properties is Dictionary and world_object.properties.has("catalog_id"):
return str(world_object.properties["catalog_id"]).to_lower()
return world_object.asset_id.get_file().get_basename().to_lower()
func _try_load_asset(asset_id: String) -> Node:
if asset_id.is_empty():
return null
var path := "res://assets/%s" % asset_id
if ResourceLoader.exists(path):
var res := load(path)
if res is PackedScene:
return (res as PackedScene).instantiate()
elif res is Mesh:
var mi := MeshInstance3D.new()
mi.name = "mesh"
mi.mesh = res as Mesh
return mi
return null
func _build_procedural_object(cat_id: String) -> void:
var container := Node3D.new()
container.name = "visuals"
add_child(container)
if "lamp" in cat_id:
# Lamp post: dark iron vertical pole + glowing lantern + omni light
var pole := MeshInstance3D.new()
pole.name = "pole"
var pole_mesh := CylinderMesh.new()
pole_mesh.height = 3.2
pole_mesh.top_radius = 0.07
pole_mesh.bottom_radius = 0.1
var iron_mat := StandardMaterial3D.new()
iron_mat.albedo_color = Color(0.2, 0.22, 0.25)
iron_mat.metallic = 0.85
iron_mat.roughness = 0.35
pole_mesh.material = iron_mat
pole.mesh = pole_mesh
pole.position = Vector3(0, 1.6, 0)
container.add_child(pole)
_mesh_instance = pole
var cap := MeshInstance3D.new()
cap.name = "cap"
var cap_mesh := CylinderMesh.new()
cap_mesh.height = 0.15
cap_mesh.top_radius = 0.05
cap_mesh.bottom_radius = 0.3
cap_mesh.material = iron_mat
cap.mesh = cap_mesh
cap.position = Vector3(0, 3.2, 0)
container.add_child(cap)
var lantern := MeshInstance3D.new()
lantern.name = "lantern"
var lantern_mesh := BoxMesh.new()
lantern_mesh.size = Vector3(0.32, 0.45, 0.32)
var glow_mat := StandardMaterial3D.new()
glow_mat.albedo_color = Color(1.0, 0.95, 0.7)
glow_mat.emission_enabled = true
glow_mat.emission = Color(1.0, 0.9, 0.4)
glow_mat.emission_energy_multiplier = 4.0
lantern_mesh.material = glow_mat
lantern.mesh = lantern_mesh
lantern.position = Vector3(0, 2.9, 0)
container.add_child(lantern)
var light := OmniLight3D.new()
light.name = "light"
light.light_color = Color(1.0, 0.93, 0.72)
light.light_energy = 1.6
light.omni_range = 10.0
light.position = Vector3(0, 2.9, 0)
container.add_child(light)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var cyl := CylinderShape3D.new()
cyl.height = 3.2
cyl.radius = 0.2
col_shape.shape = cyl
col_shape.position = Vector3(0, 1.6, 0)
col.add_child(col_shape)
container.add_child(col)
elif "crate" in cat_id:
var crate := MeshInstance3D.new()
crate.name = "crate"
var crate_mesh := BoxMesh.new()
crate_mesh.size = Vector3(1.0, 1.0, 1.0)
var wood_mat := StandardMaterial3D.new()
wood_mat.albedo_color = Color(0.55, 0.36, 0.18)
wood_mat.roughness = 0.8
crate_mesh.material = wood_mat
crate.mesh = crate_mesh
crate.position = Vector3(0, 0.5, 0)
container.add_child(crate)
_mesh_instance = crate
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 1.0, 1.0)
col_shape.shape = box
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
elif "rock" in cat_id:
var rock := MeshInstance3D.new()
rock.name = "rock"
var rock_mesh := SphereMesh.new()
rock_mesh.radius = 0.65
rock_mesh.height = 1.0
rock_mesh.radial_segments = 8
rock_mesh.rings = 6
var stone_mat := StandardMaterial3D.new()
stone_mat.albedo_color = Color(0.42, 0.44, 0.46)
stone_mat.roughness = 0.9
rock_mesh.material = stone_mat
rock.mesh = rock_mesh
rock.position = Vector3(0, 0.5, 0)
container.add_child(rock)
_mesh_instance = rock
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var sphere := SphereShape3D.new()
sphere.radius = 0.65
col_shape.shape = sphere
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
elif "tree" in cat_id:
var trunk := MeshInstance3D.new()
trunk.name = "trunk"
var trunk_mesh := CylinderMesh.new()
trunk_mesh.height = 1.4
trunk_mesh.top_radius = 0.2
trunk_mesh.bottom_radius = 0.25
var bark_mat := StandardMaterial3D.new()
bark_mat.albedo_color = Color(0.35, 0.22, 0.12)
trunk_mesh.material = bark_mat
trunk.mesh = trunk_mesh
trunk.position = Vector3(0, 0.7, 0)
container.add_child(trunk)
var leaves1 := MeshInstance3D.new()
leaves1.name = "leaves1"
var leaves1_mesh := CylinderMesh.new()
leaves1_mesh.height = 2.0
leaves1_mesh.top_radius = 0.1
leaves1_mesh.bottom_radius = 1.4
var leaves_mat := StandardMaterial3D.new()
leaves_mat.albedo_color = Color(0.12, 0.38, 0.15)
leaves1_mesh.material = leaves_mat
leaves1.mesh = leaves1_mesh
leaves1.position = Vector3(0, 2.0, 0)
container.add_child(leaves1)
_mesh_instance = leaves1
var leaves2 := MeshInstance3D.new()
leaves2.name = "leaves2"
var leaves2_mesh := CylinderMesh.new()
leaves2_mesh.height = 1.6
leaves2_mesh.top_radius = 0.0
leaves2_mesh.bottom_radius = 0.9
leaves2_mesh.material = leaves_mat
leaves2.mesh = leaves2_mesh
leaves2.position = Vector3(0, 3.2, 0)
container.add_child(leaves2)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var cyl := CylinderShape3D.new()
cyl.height = 4.0
cyl.radius = 0.35
col_shape.shape = cyl
col_shape.position = Vector3(0, 2.0, 0)
col.add_child(col_shape)
container.add_child(col)
elif "chest" in cat_id:
var chest := MeshInstance3D.new()
chest.name = "chest"
var chest_mesh := BoxMesh.new()
chest_mesh.size = Vector3(1.0, 0.8, 0.6)
var chest_mat := StandardMaterial3D.new()
chest_mat.albedo_color = Color(0.48, 0.26, 0.12)
chest_mesh.material = chest_mat
chest.mesh = chest_mesh
chest.position = Vector3(0, 0.4, 0)
container.add_child(chest)
_mesh_instance = chest
var trim := MeshInstance3D.new()
trim.name = "trim"
var trim_mesh := BoxMesh.new()
trim_mesh.size = Vector3(0.2, 0.25, 0.64)
var gold_mat := StandardMaterial3D.new()
gold_mat.albedo_color = Color(0.9, 0.75, 0.2)
trim_mesh.material = gold_mat
trim.mesh = trim_mesh
trim.position = Vector3(0, 0.4, 0)
container.add_child(trim)
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 0.8, 0.6)
col_shape.shape = box
col_shape.position = Vector3(0, 0.4, 0)
col.add_child(col_shape)
container.add_child(col)
elif "spawn" in cat_id or (world_object != null and world_object.type == "spawn_point"):
var pad := MeshInstance3D.new()
pad.name = "pad"
var pad_mesh := CylinderMesh.new()
pad_mesh.height = 0.08
pad_mesh.top_radius = 1.2
pad_mesh.bottom_radius = 1.2
var spawn_mat := StandardMaterial3D.new()
spawn_mat.albedo_color = Color(0.2, 0.75, 0.95)
spawn_mat.emission_enabled = true
spawn_mat.emission = Color(0.1, 0.6, 0.85)
spawn_mat.emission_energy_multiplier = 2.0
pad_mesh.material = spawn_mat
pad.mesh = pad_mesh
pad.position = Vector3(0, 0.04, 0)
container.add_child(pad)
_mesh_instance = pad
else:
var mi := MeshInstance3D.new()
mi.name = "mesh"
mi.mesh = _make_placeholder_mesh()
mi.position = Vector3(0, 0.5, 0)
container.add_child(mi)
_mesh_instance = mi
var col := _create_static_collision()
var col_shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = Vector3(1.0, 1.0, 1.0)
col_shape.shape = box
col_shape.position = Vector3(0, 0.5, 0)
col.add_child(col_shape)
container.add_child(col)
func _create_static_collision() -> StaticBody3D:
var sb := StaticBody3D.new()
sb.name = "collision"
sb.set_collision_layer_value(1, true)
sb.set_collision_mask_value(1, true)
return sb
## Attempt to load a mesh from the asset path.
@@ -49,7 +315,7 @@ func _load_asset_mesh(asset_id: String) -> Mesh:
if ResourceLoader.exists(path):
var res := load(path)
if res is Mesh:
return res
return res as Mesh
return null
+4 -9
View File
@@ -74,17 +74,12 @@ func _create_mesh_for_tile(tile_def: TileDef) -> void:
_mesh_instances[tile_def.id] = mesh_instance
## Convert a tile grid coordinate to a world-space position (center of tile).
## Convert a tile grid coordinate to a chunk-local 3D position (center of tile).
func _tile_to_world(x: int, y: int, z: int) -> Vector3:
var origin := Vector3(
chunk_coord.x * tile_layer.width,
chunk_coord.y * tile_layer.height,
chunk_coord.z * tile_layer.depth
)
return origin + Vector3(
return Vector3(
x * TILE_SIZE + TILE_SIZE * 0.5,
y * TILE_SIZE + TILE_SIZE * 0.5,
z * TILE_SIZE + TILE_SIZE * 0.5
z * TILE_SIZE + TILE_SIZE * 0.5,
y * TILE_SIZE + TILE_SIZE * 0.5
)