340 lines
11 KiB
GDScript
340 lines
11 KiB
GDScript
class_name InteriorMap extends Node3D
|
|
|
|
const _CHUNK_SIZE := 32
|
|
|
|
const _GRID_ORIGIN := Vector2(0.5, 0.5)
|
|
|
|
##
|
|
## Cardinal orientation referencing the direction of a tile.
|
|
##
|
|
enum Orientation {
|
|
NORTH,
|
|
EAST,
|
|
SOUTH,
|
|
WEST,
|
|
}
|
|
|
|
##
|
|
## Baked block of meshes making up a segment of the grid.
|
|
##
|
|
class Chunk:
|
|
var _floor_meshes: Array[Mesh] = []
|
|
|
|
var _floor_orientation := PackedInt32Array()
|
|
|
|
var _multi_mesh_instances: Array[MultiMeshInstance] = []
|
|
|
|
var _wall_meshes: Array[Mesh] = []
|
|
|
|
var _wall_orientation := PackedInt32Array()
|
|
|
|
func _init() -> void:
|
|
var buffer_size := _CHUNK_SIZE * _CHUNK_SIZE
|
|
|
|
_floor_meshes.resize(buffer_size)
|
|
_floor_orientation.resize(buffer_size)
|
|
_wall_meshes.resize(buffer_size)
|
|
_wall_orientation.resize(buffer_size)
|
|
|
|
##
|
|
## Invalidates the mesh block, re-baking its contents from the current mesh data set.
|
|
##
|
|
func invalidate(interior_map: InteriorMap, coordinate: Vector2i) -> void:
|
|
# TODO: Once this is all lowered into native code, look for ways to parallelize the loops.
|
|
for multi_mesh_instance in _multi_mesh_instances:
|
|
RenderingServer.free_rid(multi_mesh_instance._instance_rid)
|
|
RenderingServer.free_rid(multi_mesh_instance._multimesh_rid)
|
|
|
|
_multi_mesh_instances.clear()
|
|
|
|
# Normalize mesh instance data for the chunk.
|
|
var transforms_by_mesh := {}
|
|
var grid_size := interior_map.size
|
|
var half_pi := PI / 2
|
|
|
|
for i in _floor_meshes.size():
|
|
var mesh := _floor_meshes[i]
|
|
|
|
if mesh != null:
|
|
if not(mesh in transforms_by_mesh):
|
|
transforms_by_mesh[mesh] = []
|
|
|
|
transforms_by_mesh[mesh].append(Transform3D(
|
|
Basis.IDENTITY.rotated(Vector3.UP, half_pi * _floor_orientation[i]), Vector3(
|
|
(float(coordinate.x * _CHUNK_SIZE) + (i % _CHUNK_SIZE)) -
|
|
(float(grid_size.x) * _GRID_ORIGIN.x) + 0.5, 0.0,
|
|
(float(coordinate.y * _CHUNK_SIZE) + (i / _CHUNK_SIZE)) -
|
|
(float(grid_size.y) * _GRID_ORIGIN.y) + 0.5)))
|
|
|
|
for i in _wall_meshes.size():
|
|
var mesh := _wall_meshes[i]
|
|
|
|
if mesh != null:
|
|
if not(mesh in transforms_by_mesh):
|
|
transforms_by_mesh[mesh] = []
|
|
|
|
transforms_by_mesh[mesh].append(Transform3D(
|
|
Basis.IDENTITY.rotated(Vector3.UP, half_pi * _wall_orientation[i]), Vector3(
|
|
(float(coordinate.x * _CHUNK_SIZE) + (i % _CHUNK_SIZE)) -
|
|
(float(grid_size.x) * _GRID_ORIGIN.x) + 0.5, 0.0,
|
|
(float(coordinate.y * _CHUNK_SIZE) + (i / _CHUNK_SIZE)) -
|
|
(float(grid_size.y) * _GRID_ORIGIN.y) + 0.5)))
|
|
|
|
# (Re)-bake into multimesh instances for the chunk.
|
|
var scenario_rid := interior_map.get_world_3d().scenario
|
|
var global_transform := interior_map.global_transform
|
|
|
|
for chunk_mesh in transforms_by_mesh:
|
|
var multi_mesh_instance := MultiMeshInstance.new(
|
|
scenario_rid, chunk_mesh.get_rid(), transforms_by_mesh[chunk_mesh])
|
|
|
|
multi_mesh_instance.set_offset(global_transform)
|
|
_multi_mesh_instances.append(multi_mesh_instance)
|
|
|
|
##
|
|
## Sets the floor mesh in the chunk at the location relative [code]coordinatess[/code] to
|
|
## [code]mesh[/code].
|
|
##
|
|
## *Note* that [method Chunk.invalidate] must be called at some point after to visually update
|
|
## the chunk.
|
|
##
|
|
func set_floor_mesh(coordinates: Vector2i, orientation: Orientation, mesh: Mesh) -> void:
|
|
var index := (_CHUNK_SIZE * coordinates.y) + coordinates.x
|
|
|
|
_floor_orientation[index] = orientation
|
|
_floor_meshes[index] = mesh
|
|
|
|
##
|
|
## Sets the wall mesh in the chunk at the location relative [code]coordinatess[/code] to
|
|
## [code]mesh[/code].
|
|
##
|
|
## *Note* that [method Chunk.invalidate] must be called at some point after to visually update
|
|
## the chunk.
|
|
##
|
|
func set_wall_mesh(coordinates: Vector2i, orientation: Orientation, mesh: Mesh) -> void:
|
|
var index := (_CHUNK_SIZE * coordinates.y) + coordinates.x
|
|
|
|
_wall_orientation[index] = orientation
|
|
_wall_meshes[index] = mesh
|
|
|
|
##
|
|
## Specialized multi-mesh instance convenience for use within the tile grid and its chunks.
|
|
##
|
|
class MultiMeshInstance:
|
|
var _instance_rid := RID()
|
|
|
|
var _multimesh_rid := RID()
|
|
|
|
func _init(scenario_rid: RID, mesh_rid: RID, transforms: Array) -> void:
|
|
_multimesh_rid = RenderingServer.multimesh_create()
|
|
_instance_rid = RenderingServer.instance_create2(_multimesh_rid, scenario_rid)
|
|
|
|
RenderingServer.multimesh_set_mesh(_multimesh_rid, mesh_rid)
|
|
|
|
var transform_count := transforms.size()
|
|
|
|
RenderingServer.multimesh_allocate_data(_multimesh_rid,
|
|
transform_count, RenderingServer.MULTIMESH_TRANSFORM_3D, true)
|
|
|
|
for i in transform_count:
|
|
RenderingServer.multimesh_instance_set_transform(_multimesh_rid, i, transforms[i])
|
|
|
|
##
|
|
## Sets the transform of the instance to [code]offset[/code].
|
|
##
|
|
func set_offset(offset: Transform3D) -> void:
|
|
RenderingServer.instance_set_transform(_instance_rid, offset)
|
|
|
|
##
|
|
## Sets the visibility of the instance to [code]is_visible[/code].
|
|
##
|
|
func set_visible(is_visible: bool) -> void:
|
|
RenderingServer.instance_set_visible(_instance_rid, is_visible)
|
|
|
|
var _chunks: Array[Chunk] = []
|
|
|
|
var _chunks_size := Vector2i.ZERO
|
|
|
|
##
|
|
## Size of the tile grid (in engine units).
|
|
##
|
|
@export
|
|
var size: Vector2i:
|
|
set(value):
|
|
var chunk_size_factor := float(_CHUNK_SIZE)
|
|
|
|
_chunks.resize(int(ceilf(value.x / chunk_size_factor) * ceilf(value.y / chunk_size_factor)))
|
|
|
|
for i in _chunks.size():
|
|
_chunks[i] = Chunk.new()
|
|
|
|
_chunks_size = Vector2i((value / float(_CHUNK_SIZE)).ceil())
|
|
size = value
|
|
|
|
func _get_chunk(chunk_coordinate: Vector2i) -> Chunk:
|
|
return _chunks[(_chunks_size.x * chunk_coordinate.y) + chunk_coordinate.x]
|
|
|
|
func _notification(what: int) -> void:
|
|
match what:
|
|
NOTIFICATION_TRANSFORM_CHANGED:
|
|
for chunk in _chunks:
|
|
for multi_mesh_instance in chunk._multi_mesh_instances:
|
|
multi_mesh_instance.set_offset_transform(global_transform)
|
|
|
|
NOTIFICATION_VISIBILITY_CHANGED:
|
|
for chunk in _chunks:
|
|
for multi_mesh_instance in chunk._multi_mesh_instances:
|
|
multi_mesh_instance.set_visible(visible)
|
|
|
|
##
|
|
## Clears the entirety of the tile grid to only contain tiles of instance [code]tile[/code] pointing
|
|
## in the orientation of [code]orientation[/code].
|
|
##
|
|
## For clearing a specific region of the mesh grid, see [method fill_mesh].
|
|
##
|
|
func clear_tiles(orientation: Orientation, tile: InteriorTile) -> void:
|
|
if tile == null:
|
|
for y in size.y:
|
|
for x in size.x:
|
|
var coordinates := Vector2i(x, y)
|
|
var chunk := _get_chunk(coordinates / _CHUNK_SIZE)
|
|
var chunk_coordinates := coordinates % _CHUNK_SIZE
|
|
|
|
chunk.set_floor_mesh(chunk_coordinates, orientation, null)
|
|
chunk.set_wall_mesh(chunk_coordinates, orientation, null)
|
|
|
|
else:
|
|
var mesh := tile.mesh
|
|
|
|
match tile.kind:
|
|
InteriorTile.Kind.FLOOR:
|
|
for y in size.y:
|
|
for x in size.x:
|
|
var coordinate := Vector2i(x, y)
|
|
|
|
_get_chunk(coordinate / _CHUNK_SIZE).\
|
|
set_floor_mesh(coordinate % _CHUNK_SIZE, orientation, mesh)
|
|
|
|
InteriorTile.Kind.WALL:
|
|
for y in size.y:
|
|
for x in size.x:
|
|
var coordinate := Vector2i(x, y)
|
|
|
|
_get_chunk(coordinate / _CHUNK_SIZE).\
|
|
set_floor_mesh(coordinate % _CHUNK_SIZE, orientation, mesh)
|
|
|
|
for y in _chunks_size.y:
|
|
for x in _chunks_size.x:
|
|
var chunk_coordinate := Vector2i(x, y)
|
|
|
|
_get_chunk(chunk_coordinate).invalidate(self, chunk_coordinate)
|
|
|
|
##
|
|
## Clears the region of the tile grid at [code]area[/code] to only contain instances of
|
|
## [code]tile[/code] pointing in the orientation of [code]orientation[/code].
|
|
##
|
|
## *Note* that [code]area[/code] *must* be within the area of the of the mesh grid, where
|
|
## [code]Vector2i.ZERO[/code] is the top-left and [member size] [code]- 1[/code] is the bottom-
|
|
## right.
|
|
##
|
|
## For clearing the whole of the mesh grid, see [method clear_mesh].
|
|
##
|
|
func fill_tiles(area: Rect2i, orientation: Orientation, tile: InteriorTile) -> void:
|
|
assert(get_area().encloses(area), "area must be within grid")
|
|
|
|
var filled_chunks := BitMap.new()
|
|
|
|
filled_chunks.resize(_chunks_size)
|
|
|
|
if tile == null:
|
|
for y in range(area.position.y, area.end.y):
|
|
for x in range(area.position.x, area.end.x):
|
|
var coordinates := Vector2i(x, y)
|
|
var chunk_coordinates := coordinates / _CHUNK_SIZE
|
|
var chunk := _get_chunk(coordinates / _CHUNK_SIZE)
|
|
var local_coordinates := coordinates % _CHUNK_SIZE
|
|
|
|
chunk.set_floor_mesh(local_coordinates, orientation, null)
|
|
chunk.set_wall_mesh(local_coordinates, orientation, null)
|
|
filled_chunks.set_bitv(chunk_coordinates, true)
|
|
|
|
else:
|
|
var mesh := tile.mesh
|
|
|
|
if tile.fixed_orientation:
|
|
orientation = Orientation.NORTH
|
|
|
|
match tile.kind:
|
|
InteriorTile.Kind.FLOOR:
|
|
for y in range(area.position.y, area.end.y):
|
|
for x in range(area.position.x, area.end.x):
|
|
var coordinate := Vector2i(x, y)
|
|
var chunk_coordinate := coordinate / _CHUNK_SIZE
|
|
|
|
_get_chunk(chunk_coordinate).set_floor_mesh(
|
|
coordinate % _CHUNK_SIZE, orientation, mesh)
|
|
|
|
filled_chunks.set_bitv(chunk_coordinate, true)
|
|
|
|
InteriorTile.Kind.WALL:
|
|
for y in range(area.position.y, area.end.y):
|
|
for x in range(area.position.x, area.end.x):
|
|
var coordinate := Vector2i(x, y)
|
|
var chunk_coordinate := coordinate / _CHUNK_SIZE
|
|
|
|
_get_chunk(chunk_coordinate).set_wall_mesh(
|
|
coordinate % _CHUNK_SIZE, orientation, mesh)
|
|
|
|
filled_chunks.set_bitv(chunk_coordinate, true)
|
|
|
|
for y in _chunks_size.y:
|
|
for x in _chunks_size.x:
|
|
if filled_chunks.get_bit(x, y):
|
|
var chunk_coordinate := Vector2i(x, y)
|
|
|
|
_get_chunk(chunk_coordinate).invalidate(self, chunk_coordinate)
|
|
|
|
##
|
|
## Returns the area of the interior map based on its current size.
|
|
##
|
|
func get_area() -> Rect2i:
|
|
return Rect2i(Vector2i.ZERO, size)
|
|
|
|
##
|
|
## Plots a single tile at [code]coordinates[/code] to be an instance of [code]tile[/code] pointing
|
|
## in the orientation of [code]orientation[/code], overwriting whatever it previously contained.
|
|
##
|
|
## *Note* that [code]coordinates[/code] *must* be within the area of the of the mesh grid, where
|
|
## [code]Vector2i.ZERO[/code] is the top-left and [member size] [code]- 1[/code] is the bottom-
|
|
## right.
|
|
##
|
|
## For bulk-setting many meshes at once, see [method fill_mesh] and [method clear_mesh].
|
|
##
|
|
func plot_tile(coordinates: Vector2i, orientation: Orientation, tile: InteriorTile) -> void:
|
|
assert(get_area().has_point(coordinates), "coordinate must be within grid")
|
|
|
|
var chunk_coordinates := coordinates / _CHUNK_SIZE
|
|
var chunk := _get_chunk(chunk_coordinates)
|
|
|
|
if tile == null:
|
|
var local_coordinates := coordinates % _CHUNK_SIZE
|
|
|
|
chunk.set_floor_mesh(local_coordinates, orientation, null)
|
|
chunk.set_wall_mesh(local_coordinates, orientation, null)
|
|
chunk.invalidate(self, chunk_coordinates)
|
|
|
|
else:
|
|
var mesh := tile.mesh
|
|
|
|
if tile.fixed_orientation:
|
|
orientation = Orientation.NORTH
|
|
|
|
match tile.kind:
|
|
InteriorTile.Kind.FLOOR:
|
|
chunk.set_floor_mesh(coordinates % _CHUNK_SIZE, orientation, mesh)
|
|
chunk.invalidate(self, chunk_coordinates)
|
|
|
|
InteriorTile.Kind.WALL:
|
|
chunk.set_wall_mesh(coordinates % _CHUNK_SIZE, orientation, mesh)
|
|
chunk.invalidate(self, chunk_coordinates)
|