Address review feedback: Implement chunk streaming system in GDScript #20
@@ -0,0 +1,170 @@
|
||||
class_name ChunkScene
|
||||
extends Node3D
|
||||
## A visual representation of a single loaded world chunk.
|
||||
##
|
||||
## This node is instantiated by [ChunkStreamManager] when a chunk is loaded.
|
||||
## It holds the tile layers, world objects, and metadata for one chunk of the
|
||||
## world. The scene is responsible for rendering the chunk's visual content
|
||||
## and exposing its gameplay objects to the rest of the game.
|
||||
##
|
||||
## ## Structure
|
||||
##
|
||||
## ```
|
||||
## ChunkScene (Node3D)
|
||||
## ├── TileLayer (Node3D) # one per tile layer (ground, walls, decor, ...)
|
||||
## ├── StaticObject (Node3D) # props, geometry, interactables
|
||||
## └── DynamicObject (Node3D) # spawnable entities (templates)
|
||||
## ```
|
||||
##
|
||||
## ## Lifecycle
|
||||
##
|
||||
## 1. [ChunkStreamManager] requests chunk data from the server.
|
||||
## 2. Server responds with binary payload.
|
||||
## 3. [ChunkStreamManager] decodes the payload and creates a [ChunkScene].
|
||||
## 4. [ChunkScene] populates itself with tile layers and objects.
|
||||
## 5. When the player moves out of range, [ChunkStreamManager] calls
|
||||
## [method ChunkScene.queue_free] to unload it.
|
||||
|
||||
## The chunk coordinate this scene represents.
|
||||
var coord: Vector3i = Vector3i.ZERO
|
||||
|
||||
## The canonical chunk key.
|
||||
var key: String = ""
|
||||
|
||||
## World-space bounding box of this chunk.
|
||||
var bounds: AABB = AABB()
|
||||
|
||||
## Format version of the chunk data.
|
||||
var format_version: int = 1
|
||||
|
||||
## Revision counter from the server.
|
||||
var revision: int = 0
|
||||
|
||||
## List of tile layers in this chunk.
|
||||
var tile_layers: Array[TileLayer] = []
|
||||
|
||||
## List of world objects (static + dynamic) in this chunk.
|
||||
var world_objects: Array[WorldObject] = []
|
||||
|
||||
## Content moderation flags for this chunk.
|
||||
var content_flags: ChunkDataDecoder.ContentFlags = ChunkDataDecoder.ContentFlags.new()
|
||||
|
||||
## Whether this chunk has been modified locally (not yet synced to server).
|
||||
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()
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
## Add a tile layer to this chunk scene.
|
||||
func add_tile_layer(layer: TileLayer) -> void:
|
||||
tile_layers.append(layer)
|
||||
var layer_node := TileLayerNode.new()
|
||||
layer_node.tile_layer = layer
|
||||
layer_node.chunk_coord = coord
|
||||
add_child(layer_node)
|
||||
|
||||
|
||||
## Add a world object to this chunk scene.
|
||||
func add_object(obj: WorldObject) -> void:
|
||||
world_objects.append(obj)
|
||||
if obj.is_dynamic:
|
||||
_add_dynamic_object(obj)
|
||||
else:
|
||||
_add_static_object(obj)
|
||||
|
||||
|
||||
func _add_static_object(obj: WorldObject) -> void:
|
||||
var static_obj := StaticObjectNode.new()
|
||||
static_obj.world_object = obj
|
||||
static_obj.position = obj.position
|
||||
static_obj.rotation = obj.rotation
|
||||
static_obj.scale = obj.scale
|
||||
static_obj.name = "static_%s" % obj.id
|
||||
add_child(static_obj)
|
||||
|
||||
|
||||
func _add_dynamic_object(obj: WorldObject) -> void:
|
||||
var dynamic_obj := DynamicObjectNode.new()
|
||||
dynamic_obj.world_object = obj
|
||||
dynamic_obj.position = obj.position
|
||||
dynamic_obj.rotation = obj.rotation
|
||||
dynamic_obj.scale = obj.scale
|
||||
dynamic_obj.name = "dynamic_%s" % obj.id
|
||||
add_child(dynamic_obj)
|
||||
|
||||
|
||||
## Get a world object by its ID.
|
||||
func get_object_by_id(object_id: String) -> WorldObject:
|
||||
for obj in world_objects:
|
||||
if obj.id == object_id:
|
||||
return obj
|
||||
return null
|
||||
|
||||
|
||||
## Get all static objects in this chunk.
|
||||
func get_static_objects() -> Array[WorldObject]:
|
||||
var result: Array[WorldObject] = []
|
||||
for obj in world_objects:
|
||||
if not obj.is_dynamic:
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
## Get all dynamic object templates in this chunk.
|
||||
func get_dynamic_templates() -> Array[WorldObject]:
|
||||
var result: Array[WorldObject] = []
|
||||
for obj in world_objects:
|
||||
if obj.is_dynamic:
|
||||
result.append(obj)
|
||||
return result
|
||||
|
||||
|
||||
## Mark this chunk as dirty (locally modified).
|
||||
func mark_dirty() -> void:
|
||||
is_dirty = true
|
||||
|
||||
|
||||
## Clear all objects and tile layers from this chunk.
|
||||
func clear() -> void:
|
||||
for child in get_children():
|
||||
if child is TileLayerNode or child is StaticObjectNode or child is DynamicObjectNode:
|
||||
child.queue_free()
|
||||
tile_layers.clear()
|
||||
world_objects.clear()
|
||||
|
||||
|
||||
## Get the world-space position of a tile within this chunk.
|
||||
func get_tile_world_position(tile_x: int, tile_y: int, tile_z: int = 0) -> Vector3:
|
||||
return bounds.position + Vector3(
|
||||
tile_x * 1.0, # Tile size is 1 world unit
|
||||
tile_y * 1.0,
|
||||
tile_z * 1.0
|
||||
)
|
||||
|
||||
|
||||
## Check if a world-space position is within this chunk's bounds.
|
||||
func contains_position(pos: Vector3) -> bool:
|
||||
return bounds.has_point(pos)
|
||||
|
||||
|
||||
## Check if a chunk coordinate is adjacent to this chunk.
|
||||
func is_adjacent(other_coord: Vector3i) -> bool:
|
||||
var dx := abs(coord.x - other_coord.x)
|
||||
var dy := abs(coord.y - other_coord.y)
|
||||
var dz := abs(coord.z - other_coord.z)
|
||||
return dx <= 1 and dy <= 1 and dz <= 1 and (dx + dy + dz) > 0
|
||||
+273
-204
@@ -1,229 +1,339 @@
|
||||
class_name ChunkStreamManager
|
||||
extends Node
|
||||
## Manages chunk load/unload based on player position.
|
||||
## Manages loading and unloading of world chunks based on player position.
|
||||
##
|
||||
## Chunks are loaded from the server and instantiated as [ChunkScene] nodes.
|
||||
## The manager maintains a view radius around the player and only loads chunks
|
||||
## within that radius. Chunks outside the radius are unloaded to save memory.
|
||||
## This is the central orchestrator for the chunk streaming system. It monitors
|
||||
## the player's position, determines which chunks should be loaded, requests
|
||||
## chunk data from the server, instantiates ChunkScene nodes, and unloads
|
||||
## chunks that fall outside the streaming radius.
|
||||
##
|
||||
## This implements the streaming system described in docs/architecture/world.md
|
||||
## section 5.6 (Streaming & Caching).
|
||||
## ## Design
|
||||
##
|
||||
## - **Streaming radius** defines how many chunks around the player are kept
|
||||
## loaded. Chunks beyond this radius are unloaded to save memory and CPU.
|
||||
## - **Load priority** ensures chunks closer to the player are loaded first,
|
||||
## preventing visual pop-in at the edge of the view.
|
||||
## - **Server communication** uses the RPCs defined in docs/architecture/world.md
|
||||
## (§5.3): `request_chunk`, `chunk_data`, `chunk_invalidated`.
|
||||
##
|
||||
## ## Usage
|
||||
##
|
||||
## Add this node to your scene tree and set the `player` reference:
|
||||
## ```gdscript
|
||||
## var stream_manager: ChunkStreamManager = $ChunkStreamManager
|
||||
## stream_manager.player = $Player
|
||||
## stream_manager.streaming_radius = 4 # chunks
|
||||
## ```
|
||||
|
||||
## Radius (in chunks) around the player within which chunks are loaded.
|
||||
const VIEW_RADIUS: int = 4
|
||||
## Number of chunks (radius) to keep loaded around the player.
|
||||
## A radius of 2 means a 5x5 grid of chunks centered on the player.
|
||||
## Higher values increase memory usage but reduce streaming pop-in.
|
||||
@export var streaming_radius: int = 3
|
||||
|
||||
## Minimum distance (in chunks) the player must move before the manager
|
||||
## re-evaluates which chunks need to be loaded/unloaded. This prevents
|
||||
## excessive chunk churn when the player is standing still.
|
||||
const MIN_MOVE_DISTANCE: float = 1.0
|
||||
## Maximum number of chunks to load per frame. Prevents frame spikes during
|
||||
## rapid movement or when entering a new area.
|
||||
@export var max_loads_per_frame: int = 4
|
||||
|
||||
## Number of chunks to load/unload per frame (prevents frame spikes).
|
||||
const CHUNKS_PER_FRAME: int = 4
|
||||
## Maximum number of chunks to unload per frame.
|
||||
@export var max_unloads_per_frame: int = 4
|
||||
|
||||
## Signal emitted when a chunk is successfully loaded.
|
||||
signal chunk_loaded(chunk_key: String, chunk_scene: Node3D)
|
||||
## Distance (in chunks) from the streaming radius edge before a chunk is
|
||||
## considered for unloading. Prevents rapid load/unload cycling.
|
||||
@export var unload_margin: int = 1
|
||||
|
||||
## Signal emitted when a chunk is unloaded and freed.
|
||||
signal chunk_unloaded(chunk_key: String)
|
||||
## Emitted when a chunk finishes loading.
|
||||
signal chunk_loaded(chunk_coord: Vector3i, chunk_scene: Node)
|
||||
|
||||
## Signal emitted when a chunk load fails.
|
||||
signal chunk_load_failed(chunk_key: String, error: String)
|
||||
## Emitted when a chunk is unloaded.
|
||||
signal chunk_unloaded(chunk_coord: Vector3i)
|
||||
|
||||
## The player node whose position drives chunk streaming.
|
||||
## Emitted when a chunk fails to load (e.g., server error).
|
||||
signal chunk_load_failed(chunk_coord: Vector3i, error: String)
|
||||
|
||||
## Emitted when the player moves far enough to require a streaming update.
|
||||
signal streaming_updated
|
||||
|
||||
## Reference to the player node. Must be set before the manager starts.
|
||||
var player: Node3D = null
|
||||
|
||||
## Set of currently loaded chunk keys.
|
||||
var loaded_chunks: Dictionary = {}
|
||||
## The current player chunk coordinate.
|
||||
var _player_chunk_coord: Vector3i = Vector3i.ZERO
|
||||
|
||||
## Set of chunk keys that are currently being loaded (async).
|
||||
var loading_chunks: Dictionary = {}
|
||||
## Map of loaded chunk coordinates to their scene nodes.
|
||||
var _loaded_chunks: Dictionary = {}
|
||||
|
||||
## The player's position at the last evaluation.
|
||||
var last_player_position: Vector3 = Vector3.ZERO
|
||||
## Queue of chunk coordinates pending load, sorted by distance from player.
|
||||
var _load_queue: Array[Vector3i] = []
|
||||
|
||||
## Whether streaming is active (player exists and is in-game).
|
||||
## Queue of chunk coordinates pending unload.
|
||||
var _unload_queue: Array[Vector3i] = []
|
||||
|
||||
## Whether streaming is currently active (player is set and in game).
|
||||
var is_active: bool = false
|
||||
|
||||
## Reference to the server connection for chunk requests.
|
||||
var server: NetworkedMultiplayerENet = null
|
||||
## The server RPC interface for chunk data. Set by the networking layer.
|
||||
var _server_rpc: Object = null
|
||||
|
||||
## Internal: track which chunks are currently being loaded (to avoid duplicates).
|
||||
var _loading_in_progress: Dictionary = {}
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
last_player_position = Vector3.ZERO
|
||||
_process_thread = Thread.create(_streaming_loop, Thread.PRIORITY_LOW)
|
||||
|
||||
|
||||
## Set the player node that drives chunk streaming.
|
||||
func set_player(p_player: Node3D) -> void:
|
||||
player = p_player
|
||||
if player != null:
|
||||
last_player_position = player.global_position
|
||||
is_active = true
|
||||
else:
|
||||
is_active = false
|
||||
func _streaming_loop() -> void:
|
||||
while is_inside_tree():
|
||||
_update_streaming()
|
||||
_process_load_queue()
|
||||
_process_unload_queue()
|
||||
streaming_updated.emit()
|
||||
await get_tree().create_timer(0.1).timeout # Check every 100ms
|
||||
|
||||
|
||||
## Set the network server reference for chunk requests.
|
||||
func set_server(p_server: NetworkedMultiplayerENet) -> void:
|
||||
server = p_server
|
||||
|
||||
|
||||
## Called every frame to evaluate chunk loading/unloading.
|
||||
func _process(_delta: float) -> void:
|
||||
if not is_active or player == null:
|
||||
func _update_streaming() -> void:
|
||||
if player == null or not is_inside_tree():
|
||||
return
|
||||
|
||||
# Check if player has moved enough to warrant re-evaluation.
|
||||
var current_pos := player.global_position
|
||||
var move_distance := (current_pos - last_player_position).length()
|
||||
if move_distance < MIN_MOVE_DISTANCE:
|
||||
var current_coord := Chunk.coord_for_position(player.global_position)
|
||||
if current_coord == _player_chunk_coord:
|
||||
return
|
||||
|
||||
last_player_position = current_pos
|
||||
|
||||
# Evaluate which chunks need to be loaded/unloaded.
|
||||
_evaluate_chunks()
|
||||
_player_chunk_coord = current_coord
|
||||
_rebuild_queues()
|
||||
|
||||
|
||||
## Evaluate which chunks to load and which to unload.
|
||||
func _evaluate_chunks() -> void:
|
||||
if player == null:
|
||||
return
|
||||
func _rebuild_queues() -> void:
|
||||
var target_set := _compute_target_chunks()
|
||||
|
||||
var player_chunk_coord := Chunk.coord_for_position(player.global_position)
|
||||
var chunks_to_load: PackedStringArray = []
|
||||
var chunks_to_unload: PackedStringArray = []
|
||||
# Determine which chunks need to be loaded
|
||||
var to_load: Array[Vector3i] = []
|
||||
for coord in target_set:
|
||||
if not _loaded_chunks.has(coord) and not _loading_in_progress.has(coord):
|
||||
to_load.append(coord)
|
||||
|
||||
# Determine which chunks should be loaded.
|
||||
for loaded_key in loaded_chunks.keys():
|
||||
var coord := _parse_chunk_key(loaded_key)
|
||||
if coord == null:
|
||||
continue
|
||||
# Sort by distance from player (closest first)
|
||||
to_load.sort_custom(_compare_distance)
|
||||
_load_queue = to_load
|
||||
|
||||
var dist := _chunk_distance(player_chunk_coord, coord)
|
||||
if dist > VIEW_RADIUS:
|
||||
chunks_to_unload.append(loaded_key)
|
||||
# Determine which chunks need to be unloaded
|
||||
var to_unload: Array[Vector3i] = []
|
||||
for coord in _loaded_chunks:
|
||||
if not target_set.has(coord):
|
||||
to_unload.append(coord)
|
||||
|
||||
# Determine which chunks should be loaded.
|
||||
for dx in range(-VIEW_RADIUS, VIEW_RADIUS + 1):
|
||||
for dy in range(-VIEW_RADIUS, VIEW_RADIUS + 1):
|
||||
for dz in range(-1, 2):
|
||||
var target_coord := Vector3i(
|
||||
player_chunk_coord.x + dx,
|
||||
player_chunk_coord.y + dy,
|
||||
player_chunk_coord.z + dz
|
||||
to_unload.sort_custom(_compare_distance)
|
||||
_unload_queue = to_unload
|
||||
|
||||
|
||||
func _compute_target_chunks() -> Dictionary:
|
||||
"""Compute the set of chunk coordinates that should be loaded."""
|
||||
var target := {}
|
||||
var half := streaming_radius
|
||||
for dx in range(-half, half + 1):
|
||||
for dy in range(-half, half + 1):
|
||||
for dz in range(-Chunk.CHUNK_SIZE_Z, Chunk.CHUNK_SIZE_Z + 1):
|
||||
var coord := Vector3i(
|
||||
_player_chunk_coord.x + dx,
|
||||
_player_chunk_coord.y + dy,
|
||||
_player_chunk_coord.z + dz
|
||||
)
|
||||
|
||||
# Skip chunks outside the spherical view radius.
|
||||
if _chunk_distance(player_chunk_coord, target_coord) > VIEW_RADIUS:
|
||||
continue
|
||||
|
||||
var world_id := _get_world_id()
|
||||
var room_id := _get_room_id()
|
||||
var key := Chunk.build_key(world_id, room_id, target_coord)
|
||||
|
||||
if key not in loaded_chunks and key not in loading_chunks:
|
||||
chunks_to_load.append(key)
|
||||
|
||||
# Unload chunks (up to CHUNKS_PER_FRAME per frame).
|
||||
var unload_count := 0
|
||||
for key in chunks_to_unload:
|
||||
if unload_count >= CHUNKS_PER_FRAME:
|
||||
break
|
||||
_unload_chunk(key)
|
||||
unload_count += 1
|
||||
|
||||
# Load chunks (up to CHUNKS_PER_FRAME per frame).
|
||||
var load_count := 0
|
||||
for key in chunks_to_load:
|
||||
if load_count >= CHUNKS_PER_FRAME:
|
||||
break
|
||||
_load_chunk(key)
|
||||
load_count += 1
|
||||
target[coord] = coord
|
||||
return target
|
||||
|
||||
|
||||
## Load a chunk by requesting it from the server and instantiating it.
|
||||
func _load_chunk(chunk_key: String) -> void:
|
||||
if server == null:
|
||||
# No server available; create a placeholder chunk.
|
||||
_instantiate_chunk(chunk_key)
|
||||
func _compare_distance(a: Vector3i, b: Vector3i) -> bool:
|
||||
"""Compare chunk coordinates by distance from player chunk."""
|
||||
var dist_a := _chunk_distance_squared(_player_chunk_coord, a)
|
||||
var dist_b := _chunk_distance_squared(_player_chunk_coord, b)
|
||||
return dist_a < dist_b
|
||||
|
||||
|
||||
func _chunk_distance_squared(a: Vector3i, b: Vector3i) -> int:
|
||||
var dx := a.x - b.x
|
||||
var dy := a.y - b.y
|
||||
var dz := a.z - b.z
|
||||
return dx * dx + dy * dy + dz * dz
|
||||
|
||||
|
||||
func _process_load_queue() -> void:
|
||||
var count := 0
|
||||
while count < max_loads_per_frame and not _load_queue.is_empty():
|
||||
var coord := _load_queue.pop_front()
|
||||
_load_chunk(coord)
|
||||
count += 1
|
||||
|
||||
|
||||
func _process_unload_queue() -> void:
|
||||
var count := 0
|
||||
while count < max_unloads_per_frame and not _unload_queue.is_empty():
|
||||
var coord := _unload_queue.pop_front()
|
||||
_unload_chunk(coord)
|
||||
count += 1
|
||||
|
||||
|
||||
func _load_chunk(coord: Vector3i) -> void:
|
||||
if _loading_in_progress.has(coord):
|
||||
return
|
||||
|
||||
loading_chunks[chunk_key] = true
|
||||
_loading_in_progress[coord] = true
|
||||
|
||||
# Request chunk from server via RPC.
|
||||
server.rpc("request_chunk", chunk_key)
|
||||
# Request chunk data from server via RPC
|
||||
if _server_rpc != null and _server_rpc.has_method("request_chunk"):
|
||||
_server_rpc.request_chunk(Chunk.build_key(
|
||||
"", "", coord
|
||||
))
|
||||
else:
|
||||
# Fallback: try to load from local cache or create empty chunk
|
||||
_instantiate_chunk_scene(coord)
|
||||
|
||||
|
||||
## Handle the server's chunk_data response.
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _rpc_chunk_data(chunk_key: String, payload: PackedByteArray, checksum: String) -> void:
|
||||
loading_chunks.erase(chunk_key)
|
||||
_instantiate_chunk(chunk_key, payload, checksum)
|
||||
|
||||
|
||||
## Handle chunk load failure from server.
|
||||
@rpc("any_peer", "call_local", "reliable")
|
||||
func _rpc_chunk_load_error(chunk_key: String, error_msg: String) -> void:
|
||||
loading_chunks.erase(chunk_key)
|
||||
chunk_load_failed.emit(chunk_key, error_msg)
|
||||
|
||||
|
||||
## Instantiate a chunk scene from data (either cached or from server).
|
||||
func _instantiate_chunk(chunk_key: String, payload: PackedByteArray = PackedByteArray(), checksum: String = "") -> void:
|
||||
var coord := _parse_chunk_key(chunk_key)
|
||||
func _on_chunk_data_received(key: String, binary_data: PackedByteArray) -> void:
|
||||
"""Called by the networking layer when chunk data arrives from server."""
|
||||
var coord := _parse_coord_from_key(key)
|
||||
if coord == null:
|
||||
push_error("Invalid chunk key: %s" % chunk_key)
|
||||
return
|
||||
|
||||
var world_id := _get_world_id()
|
||||
var room_id := _get_room_id()
|
||||
_loading_in_progress.erase(coord)
|
||||
|
||||
# Create the chunk scene.
|
||||
# Decode the binary chunk data
|
||||
var chunk_data := ChunkDataDecoder.decode_binary(binary_data)
|
||||
if chunk_data == null:
|
||||
chunk_load_failed.emit(coord, "Failed to decode chunk data")
|
||||
return
|
||||
|
||||
# Instantiate the chunk scene with the decoded data
|
||||
_instantiate_chunk_scene_with_data(coord, chunk_data)
|
||||
|
||||
|
||||
func _on_chunk_load_error(coord: Vector3i, error_msg: String) -> void:
|
||||
"""Called when the server reports a chunk load error."""
|
||||
_loading_in_progress.erase(coord)
|
||||
chunk_load_failed.emit(coord, error_msg)
|
||||
|
||||
|
||||
func _instantiate_chunk_scene(coord: Vector3i) -> void:
|
||||
"""Create an empty chunk scene for the given coordinate."""
|
||||
var chunk_scene := ChunkScene.new()
|
||||
chunk_scene.chunk_key = chunk_key
|
||||
chunk_scene.coord = coord
|
||||
chunk_scene.world_id = world_id
|
||||
chunk_scene.room_id = room_id
|
||||
chunk_scene.key = Chunk.build_key("", "", coord)
|
||||
chunk_scene.bounds = Chunk.compute_bounds(coord)
|
||||
_add_loaded_chunk(coord, chunk_scene)
|
||||
|
||||
# Decode payload if available.
|
||||
if not payload.is_empty():
|
||||
chunk_scene.decode_payload(payload, checksum)
|
||||
|
||||
# Add to world tree.
|
||||
func _instantiate_chunk_scene_with_data(coord: Vector3i, data: ChunkDataDecoder.ChunkPayload) -> void:
|
||||
"""Create a chunk scene populated with server data."""
|
||||
var chunk_scene := ChunkScene.new()
|
||||
chunk_scene.coord = coord
|
||||
chunk_scene.key = data.key
|
||||
chunk_scene.bounds = data.bounds
|
||||
chunk_scene.format_version = data.format_version
|
||||
chunk_scene.revision = data.revision
|
||||
|
||||
# Add tiles
|
||||
for layer in data.tile_layers:
|
||||
var tile_layer := TileLayer.new()
|
||||
tile_layer.name = layer.name
|
||||
tile_layer.layer_id = layer.layer_id
|
||||
tile_layer.width = layer.width
|
||||
tile_layer.height = layer.height
|
||||
tile_layer.depth = layer.depth
|
||||
tile_layer.palette = layer.palette
|
||||
tile_layer.tiles = layer.tiles
|
||||
chunk_scene.add_tile_layer(tile_layer)
|
||||
|
||||
# Add objects
|
||||
for obj_data in data.objects:
|
||||
var world_obj := WorldObject.new()
|
||||
world_obj.id = obj_data.id
|
||||
world_obj.type = obj_data.type
|
||||
world_obj.asset_id = obj_data.asset_id
|
||||
world_obj.position = obj_data.position
|
||||
world_obj.rotation = obj_data.rotation
|
||||
world_obj.scale = obj_data.scale
|
||||
world_obj.properties = obj_data.properties
|
||||
world_obj.is_dynamic = obj_data.dynamic
|
||||
world_obj.flags = obj_data.flags
|
||||
chunk_scene.add_object(world_obj)
|
||||
|
||||
# Add content flags
|
||||
chunk_scene.content_flags = data.flags
|
||||
|
||||
_add_loaded_chunk(coord, chunk_scene)
|
||||
|
||||
|
||||
func _add_loaded_chunk(coord: Vector3i, chunk_scene: Node) -> void:
|
||||
_loaded_chunks[coord] = chunk_scene
|
||||
add_child(chunk_scene)
|
||||
loaded_chunks[chunk_key] = chunk_scene
|
||||
loading_chunks.erase(chunk_key)
|
||||
|
||||
chunk_loaded.emit(chunk_key, chunk_scene)
|
||||
_loading_in_progress.erase(coord)
|
||||
chunk_loaded.emit(coord, chunk_scene)
|
||||
|
||||
|
||||
## Unload a chunk by freeing its scene.
|
||||
func _unload_chunk(chunk_key: String) -> void:
|
||||
var chunk_scene = loaded_chunks.get(chunk_key)
|
||||
if chunk_scene == null:
|
||||
func _unload_chunk(coord: Vector3i) -> void:
|
||||
if not _loaded_chunks.has(coord):
|
||||
return
|
||||
|
||||
loaded_chunks.erase(chunk_key)
|
||||
|
||||
var chunk_scene = _loaded_chunks[coord]
|
||||
if is_instance_valid(chunk_scene):
|
||||
chunk_scene.queue_free()
|
||||
# Wait for tree removal before freeing.
|
||||
await chunk_scene.tree_exited
|
||||
|
||||
chunk_unloaded.emit(chunk_key)
|
||||
_loaded_chunks.erase(coord)
|
||||
chunk_unloaded.emit(coord)
|
||||
|
||||
|
||||
## Get the distance between two chunk coordinates (Manhattan distance in chunk space).
|
||||
static func _chunk_distance(a: Vector3i, b: Vector3i) -> int:
|
||||
var dx := absi(a.x - b.x)
|
||||
var dy := absi(a.y - b.y)
|
||||
var dz := absi(a.z - b.z)
|
||||
# Use Chebyshev distance (max of axis distances) for square view radius.
|
||||
return maxi(dx, maxi(dy, dz))
|
||||
func _on_player_position_changed() -> void:
|
||||
"""Call this when the player position changes significantly."""
|
||||
if player != null:
|
||||
_update_streaming()
|
||||
|
||||
|
||||
## Parse a chunk key string into its coordinate components.
|
||||
static func _parse_chunk_key(key: String) -> Vector3i:
|
||||
func set_server_rpc(rpc_interface: Object) -> void:
|
||||
"""Set the server RPC interface for chunk requests."""
|
||||
_server_rpc = rpc_interface
|
||||
|
||||
|
||||
func get_loaded_chunk(coord: Vector3i) -> Node:
|
||||
"""Get a loaded chunk scene by coordinate, or null if not loaded."""
|
||||
return _loaded_chunks.get(coord, null)
|
||||
|
||||
|
||||
func is_chunk_loaded(coord: Vector3i) -> bool:
|
||||
"""Check if a chunk is currently loaded."""
|
||||
return _loaded_chunks.has(coord)
|
||||
|
||||
|
||||
func get_loaded_chunk_count() -> int:
|
||||
"""Get the number of currently loaded chunks."""
|
||||
return _loaded_chunks.size()
|
||||
|
||||
|
||||
func get_all_loaded_coords() -> Array[Vector3i]:
|
||||
"""Get all currently loaded chunk coordinates."""
|
||||
var coords: Array[Vector3i] = []
|
||||
for key in _loaded_chunks:
|
||||
coords.append(key)
|
||||
return coords
|
||||
|
||||
|
||||
func force_reload_chunk(coord: Vector3i) -> void:
|
||||
"""Force reload a specific chunk (e.g., after server update)."""
|
||||
if _loaded_chunks.has(coord):
|
||||
_unload_chunk(coord)
|
||||
if not _loading_in_progress.has(coord):
|
||||
_load_chunk(coord)
|
||||
|
||||
|
||||
func clear_all_chunks() -> void:
|
||||
"""Unload all chunks immediately."""
|
||||
var coords := get_all_loaded_coords()
|
||||
for coord in coords:
|
||||
_unload_chunk(coord)
|
||||
_load_queue.clear()
|
||||
_unload_queue.clear()
|
||||
|
||||
|
||||
func _parse_coord_from_key(key: String) -> Vector3i:
|
||||
"""Parse chunk coordinate from a canonical chunk key."""
|
||||
var parts := key.split(":")
|
||||
if parts.size() < 5:
|
||||
return null
|
||||
@@ -232,44 +342,3 @@ static func _parse_chunk_key(key: String) -> Vector3i:
|
||||
int(parts[3]),
|
||||
int(parts[4])
|
||||
)
|
||||
|
||||
|
||||
## Get the current world ID (override in subclass or set via property).
|
||||
func _get_world_id() -> String:
|
||||
return "w_0001"
|
||||
|
||||
|
||||
## Get the current room ID (override in subclass or set via property).
|
||||
func _get_room_id() -> String:
|
||||
return "r_0001"
|
||||
|
||||
|
||||
## Get the list of currently loaded chunk keys.
|
||||
func get_loaded_chunk_keys() -> PackedStringArray:
|
||||
return loaded_chunks.keys()
|
||||
|
||||
|
||||
## Check if a specific chunk is currently loaded.
|
||||
func is_chunk_loaded(chunk_key: String) -> bool:
|
||||
return chunk_key in loaded_chunks
|
||||
|
||||
|
||||
## Force reload a specific chunk (unloads and re-loads it).
|
||||
func reload_chunk(chunk_key: String) -> void:
|
||||
if chunk_key in loaded_chunks:
|
||||
_unload_chunk(chunk_key)
|
||||
if chunk_key not in loading_chunks:
|
||||
_load_chunk(chunk_key)
|
||||
|
||||
|
||||
## Clear all loaded chunks (useful for world transitions).
|
||||
func clear_all_chunks() -> void:
|
||||
var keys := loaded_chunks.keys()
|
||||
for key in keys:
|
||||
_unload_chunk(key)
|
||||
loading_chunks.clear()
|
||||
|
||||
|
||||
## Get the number of currently loaded chunks.
|
||||
func get_loaded_chunk_count() -> int:
|
||||
return loaded_chunks.size()
|
||||
|
||||
Reference in New Issue
Block a user