121 lines
3.4 KiB
GDScript
121 lines
3.4 KiB
GDScript
class_name TileLayerNode
|
|
extends Node3D
|
|
## A visual representation of a single tile layer within a chunk.
|
|
##
|
|
## This node is instantiated by [ChunkScene] for each tile layer in a chunk.
|
|
## It renders the layer's tiles as instanced meshes (one per tile definition
|
|
## in the palette) using [MultiMeshInstance3D] for efficiency.
|
|
##
|
|
## ## Structure
|
|
##
|
|
## ```
|
|
## TileLayerNode (Node3D)
|
|
## └── MultiMeshInstance3D # one per palette entry (tile definition)
|
|
## ```
|
|
|
|
## The tile layer data this node renders.
|
|
var tile_layer: TileLayer = null
|
|
|
|
## The chunk coordinate this layer belongs to (for world positioning).
|
|
var chunk_coord: Vector3i = Vector3i.ZERO
|
|
|
|
## Tile size in world units (each tile is 1x1x1).
|
|
const TILE_SIZE: float = 1.0
|
|
|
|
## Cache of MultiMeshInstance3D nodes, keyed by tile definition id.
|
|
var _mesh_instances: Dictionary = {}
|
|
|
|
|
|
func _ready() -> void:
|
|
if tile_layer == null:
|
|
return
|
|
_build_meshes()
|
|
|
|
|
|
## Build the MultiMesh instances for each tile definition in the palette.
|
|
func _build_meshes() -> void:
|
|
for tile_def in tile_layer.palette:
|
|
if tile_def.id == 0:
|
|
continue # Tile 0 is "empty/air", no mesh needed
|
|
_create_mesh_for_tile(tile_def)
|
|
|
|
|
|
func _create_mesh_for_tile(tile_def: TileDef) -> void:
|
|
# Count how many cells use this tile definition
|
|
var count := 0
|
|
for tile_id in tile_layer.tiles:
|
|
if tile_id == tile_def.id:
|
|
count += 1
|
|
|
|
if count == 0:
|
|
return
|
|
|
|
var multi_mesh := MultiMesh.new()
|
|
multi_mesh.transform_format = MultiMesh.TRANSFORM_3D
|
|
multi_mesh.mesh = _make_unit_cube_mesh()
|
|
multi_mesh.instance_count = count
|
|
|
|
var col_body: StaticBody3D = null
|
|
if tile_def.solid:
|
|
col_body = StaticBody3D.new()
|
|
col_body.name = "collision_%d" % tile_def.id
|
|
add_child(col_body)
|
|
|
|
var instance_idx := 0
|
|
for z in range(tile_layer.depth):
|
|
for y in range(tile_layer.height):
|
|
for x in range(tile_layer.width):
|
|
var tile_id := tile_layer.get_tile(x, y, z)
|
|
if tile_id != tile_def.id:
|
|
continue
|
|
var world_pos := _tile_to_world(x, y, z)
|
|
var xform := Transform3D(Basis(), world_pos)
|
|
multi_mesh.set_instance_transform(instance_idx, xform)
|
|
instance_idx += 1
|
|
if col_body != null:
|
|
var col_shape := CollisionShape3D.new()
|
|
var box := BoxShape3D.new()
|
|
box.size = Vector3(TILE_SIZE, TILE_SIZE, TILE_SIZE)
|
|
col_shape.shape = box
|
|
col_shape.position = world_pos
|
|
col_body.add_child(col_shape)
|
|
|
|
var mesh_instance := MultiMeshInstance3D.new()
|
|
mesh_instance.multimesh = multi_mesh
|
|
mesh_instance.name = "tiles_%d" % tile_def.id
|
|
add_child(mesh_instance)
|
|
_mesh_instances[tile_def.id] = mesh_instance
|
|
|
|
|
|
## 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:
|
|
return Vector3(
|
|
x * TILE_SIZE + TILE_SIZE * 0.5,
|
|
z * TILE_SIZE + TILE_SIZE * 0.5,
|
|
y * TILE_SIZE + TILE_SIZE * 0.5
|
|
)
|
|
|
|
|
|
## Create a simple unit cube mesh for tile rendering.
|
|
func _make_unit_cube_mesh() -> Mesh:
|
|
var mesh := BoxMesh.new()
|
|
mesh.size = Vector3(TILE_SIZE, TILE_SIZE, TILE_SIZE)
|
|
return mesh
|
|
|
|
|
|
## Get the tile definition at a grid position, or null if empty.
|
|
func get_tile_def_at(x: int, y: int, z: int = 0) -> TileDef:
|
|
if tile_layer == null:
|
|
return null
|
|
var tile_id := tile_layer.get_tile(x, y, z)
|
|
if tile_id == 0:
|
|
return null
|
|
return tile_layer.get_tile_def(tile_id)
|
|
|
|
|
|
## Clear all rendered meshes.
|
|
func clear() -> void:
|
|
for child in get_children():
|
|
child.queue_free()
|
|
_mesh_instances.clear()
|