168 lines
5.1 KiB
GDScript
168 lines
5.1 KiB
GDScript
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 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:
|
|
pass
|
|
|
|
|
|
## 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
|
|
# 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)
|
|
|
|
|
|
func _add_dynamic_object(obj: WorldObject) -> void:
|
|
var dynamic_obj := DynamicObjectNode.new()
|
|
dynamic_obj.world_object = obj
|
|
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)
|
|
|
|
|
|
## 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: int = abs(coord.x - other_coord.x)
|
|
var dy: int = abs(coord.y - other_coord.y)
|
|
var dz: int = abs(coord.z - other_coord.z)
|
|
return dx <= 1 and dy <= 1 and dz <= 1 and (dx + dy + dz) > 0
|