Files
godot-playground/scenes/Grid/grid_plane_manager.gd
T

356 lines
9.9 KiB
GDScript

@tool
extends Node3D
class_name GridPlaneManager
enum GridType { SQUARE, HEX }
signal grid_layout_changed(cells: Dictionary)
var _grid_type: GridType = GridType.SQUARE
@export var grid_type: GridType = GridType.SQUARE:
set(value):
if value == _grid_type:
return
_grid_type = value as GridType
_queue_rebuild()
get:
return _grid_type
var _columns: int = 10
@export_range(1, 256, 1) var columns: int:
set(value):
value = max(value, 1)
if value == _columns:
return
_columns = value
_queue_rebuild()
get:
return _columns
var _rows: int = 10
@export_range(1, 256, 1) var rows: int:
set(value):
value = max(value, 1)
if value == _rows:
return
_rows = value
_queue_rebuild()
get:
return _rows
var _cell_size: float = 1.0
@export_range(0.1, 10.0, 0.1) var cell_size: float:
set(value):
value = max(value, 0.25)
if is_equal_approx(value, _cell_size):
return
_cell_size = value
_queue_rebuild()
get:
return _cell_size
var _ground_thickness: float = 0.4
@export_range(0.1, 2.0, 0.05) var ground_thickness: float:
set(value):
value = clamp(value, 0.1, 2.0)
if is_equal_approx(value, _ground_thickness):
return
_ground_thickness = value
_queue_rebuild()
get:
return _ground_thickness
var _ground_padding: float = 20.0
@export_range(0.0, 200.0, 1.0) var ground_padding: float:
set(value):
value = max(value, 0.0)
if is_equal_approx(value, _ground_padding):
return
_ground_padding = value
emit_signal("grid_layout_changed", _cells)
get:
return _ground_padding
var _show_grid_lines: bool = true
@export var show_grid_lines: bool:
set(value):
if value == _show_grid_lines:
return
_show_grid_lines = value
if is_node_ready():
_update_grid_visibility()
get:
return _show_grid_lines
var _grid_line_color: Color = Color(0.2, 0.7, 1.0, 0.65)
@export var grid_line_color: Color:
set(value):
if value == _grid_line_color:
return
_grid_line_color = value
_queue_rebuild()
get:
return _grid_line_color
const _LINE_HEIGHT_OFFSET := 0.02
const _SQRT3 := 1.7320508075688772
@onready var _grid_lines: MeshInstance3D = %GridLines
var _grid_size: Vector2 = Vector2.ZERO
var _cells: Dictionary = {}
var _queued_rebuild: bool = false
var _hex_center_offset_local: Vector3 = Vector3.ZERO
func _ready() -> void:
_queue_rebuild()
func get_cells() -> Dictionary:
return _cells.duplicate()
func get_grid_size() -> Vector2:
return _grid_size
func get_grid_type() -> GridType:
return _grid_type
func get_cell_center(index: Vector2i) -> Vector3:
return _cells.get(index, Vector3.ZERO)
func has_cell(index: Vector2i) -> bool:
return _cells.has(index)
func cell_to_world(index: Vector2i) -> Vector3:
return get_cell_center(index)
func world_to_cell(world_position: Vector3, clamp_to_bounds: bool = true) -> Vector2i:
var local_position: Vector3 = to_local(world_position)
if _grid_type == GridType.HEX:
return _world_to_hex_cell(local_position, clamp_to_bounds)
return _world_to_square_cell(local_position, clamp_to_bounds)
func for_each_cell(callback: Callable) -> void:
for index in _cells.keys():
callback.call(index, _cells[index])
func get_cell_size_value() -> float:
return _cell_size
func get_ground_thickness() -> float:
return _ground_thickness
func get_ground_padding() -> float:
return _ground_padding
func make_highlight_mesh() -> Mesh:
if _grid_type == GridType.HEX:
return _build_hex_highlight_mesh()
var plane := PlaneMesh.new()
plane.size = Vector2(_cell_size, _cell_size)
return plane
func _queue_rebuild() -> void:
if _queued_rebuild:
return
_queued_rebuild = true
call_deferred("_process_rebuild_request")
func _process_rebuild_request() -> void:
_queued_rebuild = false
if not is_node_ready():
return
_rebuild_grid()
func _rebuild_grid() -> void:
match _grid_type:
GridType.HEX:
_generate_hex_grid()
_:
_generate_square_grid()
_update_grid_lines()
emit_signal("grid_layout_changed", _cells)
func _generate_square_grid() -> void:
_hex_center_offset_local = Vector3.ZERO
_cells.clear()
_grid_size = Vector2(_columns * _cell_size, _rows * _cell_size)
var start_x := -_grid_size.x * 0.5 + _cell_size * 0.5
var start_z := -_grid_size.y * 0.5 + _cell_size * 0.5
for row in range(_rows):
for column in range(_columns):
var center := Vector3(
start_x + float(column) * _cell_size,
0.0,
start_z + float(row) * _cell_size
)
_cells[Vector2i(column, row)] = center
func _generate_hex_grid() -> void:
_cells.clear()
if _columns <= 0 or _rows <= 0:
_grid_size = Vector2.ZERO
return
var radius: float = max(_cell_size * 0.5, 0.001)
var min_corner_x: float = INF
var max_corner_x: float = -INF
var min_corner_z: float = INF
var max_corner_z: float = -INF
for row in range(_rows):
for column in range(_columns):
var x := radius * 1.5 * float(column)
var z := radius * _SQRT3 * (float(row) + float(column) * 0.5)
var center := Vector3(x, 0.0, z)
_cells[Vector2i(column, row)] = center
var corners := _compute_hex_corners(center, radius)
for corner in corners:
min_corner_x = min(min_corner_x, corner.x)
max_corner_x = max(max_corner_x, corner.x)
min_corner_z = min(min_corner_z, corner.z)
max_corner_z = max(max_corner_z, corner.z)
if min_corner_x == INF:
_grid_size = Vector2.ZERO
return
var size_x: float = max_corner_x - min_corner_x
var size_z: float = max_corner_z - min_corner_z
var center_offset: Vector3 = Vector3(min_corner_x + size_x * 0.5, 0.0, min_corner_z + size_z * 0.5)
for key in _cells.keys():
_cells[key] -= center_offset
_hex_center_offset_local = center_offset
_grid_size = Vector2(size_x, size_z)
func _update_grid_lines() -> void:
if not _grid_lines:
return
if not _show_grid_lines:
_grid_lines.visible = false
_grid_lines.mesh = null
return
_grid_lines.visible = true
if _grid_type == GridType.HEX:
_grid_lines.mesh = _build_hex_grid_mesh()
else:
_grid_lines.mesh = _build_square_grid_mesh()
func _update_grid_visibility() -> void:
if not _grid_lines:
return
_grid_lines.visible = _show_grid_lines
func _build_square_grid_mesh() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_LINES)
var half_width := _grid_size.x * 0.5
var half_depth := _grid_size.y * 0.5
var color := _grid_line_color
for column in range(_columns + 1):
var x := -half_width + float(column) * _cell_size
st.set_color(color)
st.add_vertex(Vector3(x, _LINE_HEIGHT_OFFSET, -half_depth))
st.set_color(color)
st.add_vertex(Vector3(x, _LINE_HEIGHT_OFFSET, half_depth))
for row in range(_rows + 1):
var z := -half_depth + float(row) * _cell_size
st.set_color(color)
st.add_vertex(Vector3(-half_width, _LINE_HEIGHT_OFFSET, z))
st.set_color(color)
st.add_vertex(Vector3(half_width, _LINE_HEIGHT_OFFSET, z))
var mesh := st.commit()
return mesh
func _build_hex_grid_mesh() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_LINES)
var color := _grid_line_color
var radius: float = max(_cell_size * 0.5, 0.001)
for center in _cells.values():
var center_vec: Vector3 = center
var corners := _compute_hex_corners(center_vec, radius)
for i in range(corners.size()):
var a: Vector3 = corners[i]
var b: Vector3 = corners[(i + 1) % corners.size()]
st.set_color(color)
st.add_vertex(a + Vector3(0, _LINE_HEIGHT_OFFSET, 0))
st.set_color(color)
st.add_vertex(b + Vector3(0, _LINE_HEIGHT_OFFSET, 0))
var mesh := st.commit()
return mesh
func _compute_hex_corners(center: Vector3, radius: float) -> Array[Vector3]:
var corners: Array[Vector3] = []
for i in range(6):
var angle_deg := 60.0 * i
var angle := deg_to_rad(angle_deg)
var x := center.x + radius * cos(angle)
var z := center.z + radius * sin(angle)
corners.append(Vector3(x, 0.0, z))
return corners
func _update_highlight_mesh() -> void:
# Deprecated internally; kept for compatibility.
pass
func _build_hex_highlight_mesh() -> ArrayMesh:
var st := SurfaceTool.new()
st.begin(Mesh.PRIMITIVE_TRIANGLES)
var radius: float = max(_cell_size * 0.5, 0.001)
var corners := _compute_hex_corners(Vector3.ZERO, radius)
for i in range(1, corners.size() - 1):
st.add_vertex(corners[0])
st.add_vertex(corners[i])
st.add_vertex(corners[i + 1])
var mesh := st.commit()
return mesh
func _world_to_square_cell(local_position: Vector3, clamp_to_bounds: bool) -> Vector2i:
var half_width := _grid_size.x * 0.5
var half_depth := _grid_size.y * 0.5
var normalized_x := (local_position.x + half_width) / _cell_size
var normalized_z := (local_position.z + half_depth) / _cell_size
var x := int(floor(normalized_x))
var z := int(floor(normalized_z))
if clamp_to_bounds:
x = clamp(x, 0, _columns - 1)
z = clamp(z, 0, _rows - 1)
return Vector2i(x, z)
func _world_to_hex_cell(local_position: Vector3, clamp_to_bounds: bool) -> Vector2i:
var adjusted: Vector3 = local_position + _hex_center_offset_local
var axial: Vector2 = _local_hex_to_axial(adjusted)
var cube: Vector3 = _axial_to_cube(axial)
var rounded: Vector3 = _cube_round(cube)
var q: int = int(rounded.x)
var r: int = int(rounded.z)
if clamp_to_bounds:
q = clamp(q, 0, _columns - 1)
r = clamp(r, 0, _rows - 1)
elif q < 0 or q >= _columns or r < 0 or r >= _rows:
return Vector2i(-1, -1)
return Vector2i(q, r)
func _local_hex_to_axial(local_position: Vector3) -> Vector2:
var radius: float = max(_cell_size * 0.5, 0.0001)
var q: float = (2.0 / 3.0 * local_position.x) / radius
var r: float = ((-1.0 / 3.0) * local_position.x + (_SQRT3 / 3.0) * local_position.z) / radius
return Vector2(q, r)
func _axial_to_cube(axial: Vector2) -> Vector3:
var x: float = axial.x
var z: float = axial.y
var y: float = -x - z
return Vector3(x, y, z)
func _cube_round(cube: Vector3) -> Vector3:
var rx: float = round(cube.x)
var ry: float = round(cube.y)
var rz: float = round(cube.z)
var x_diff: float = abs(rx - cube.x)
var y_diff: float = abs(ry - cube.y)
var z_diff: float = abs(rz - cube.z)
if x_diff > y_diff and x_diff > z_diff:
rx = -ry - rz
elif y_diff > z_diff:
ry = -rx - rz
else:
rz = -rx - ry
return Vector3(rx, ry, rz)