diff --git a/scenes/Grid/grid_field.tscn b/scenes/Grid/grid_field.tscn new file mode 100644 index 0000000..0ccc0f8 --- /dev/null +++ b/scenes/Grid/grid_field.tscn @@ -0,0 +1,82 @@ +[gd_scene load_steps=9 format=4 uid="uid://qd6xvgwrd00g"] + +[ext_resource type="Script" uid="uid://3goclr4srt7k" path="res://scenes/Grid/grid_plane_manager.gd" id="1_72cxc"] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_ground"] +albedo_color = Color(0.211765, 0.364706, 0.211765, 1) + +[sub_resource type="PlaneMesh" id="PlaneMesh_ground"] +size = Vector2(10, 10) + +[sub_resource type="BoxShape3D" id="BoxShape3D_ground"] +size = Vector3(10, 0.4, 10) + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_grid"] +no_depth_test = true +shading_mode = 0 +vertex_color_use_as_albedo = true +albedo_color = Color(0.811765, 0.917647, 1, 0.74902) + +[sub_resource type="ArrayMesh" id="ArrayMesh_q3k88"] +_surfaces = [{ +"aabb": AABB(-5, 0.02, -5, 10, 1e-05, 10), +"attribute_data": PackedByteArray("M7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6Uzsv+lM7L/pTOy/6U="), +"format": 34359738377, +"primitive": 1, +"uv_scale": Vector4(0, 0, 0, 0), +"vertex_count": 44, +"vertex_data": PackedByteArray("AACgwArXozwAAKDAAACgwArXozwAAKBAAACAwArXozwAAKDAAACAwArXozwAAKBAAABAwArXozwAAKDAAABAwArXozwAAKBAAAAAwArXozwAAKDAAAAAwArXozwAAKBAAACAvwrXozwAAKDAAACAvwrXozwAAKBAAAAAAArXozwAAKDAAAAAAArXozwAAKBAAACAPwrXozwAAKDAAACAPwrXozwAAKBAAAAAQArXozwAAKDAAAAAQArXozwAAKBAAABAQArXozwAAKDAAABAQArXozwAAKBAAACAQArXozwAAKDAAACAQArXozwAAKBAAACgQArXozwAAKDAAACgQArXozwAAKBAAACgwArXozwAAKDAAACgQArXozwAAKDAAACgwArXozwAAIDAAACgQArXozwAAIDAAACgwArXozwAAEDAAACgQArXozwAAEDAAACgwArXozwAAADAAACgQArXozwAAADAAACgwArXozwAAIC/AACgQArXozwAAIC/AACgwArXozwAAAAAAACgQArXozwAAAAAAACgwArXozwAAIA/AACgQArXozwAAIA/AACgwArXozwAAABAAACgQArXozwAAABAAACgwArXozwAAEBAAACgQArXozwAAEBAAACgwArXozwAAIBAAACgQArXozwAAIBAAACgwArXozwAAKBAAACgQArXozwAAKBA") +}] + +[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_highlight"] +transparency = 1 +no_depth_test = true +shading_mode = 0 +albedo_color = Color(0.976471, 0.843137, 0.333333, 0.6) + +[sub_resource type="PlaneMesh" id="PlaneMesh_8dkrf"] +size = Vector2(1, 1) + +[node name="GridField" type="Node3D"] +script = ExtResource("1_72cxc") +columns = 10 +rows = 10 +cell_size = 1.0 +ground_thickness = 0.4 +show_grid_lines = true +grid_line_color = Color(0.2, 0.7, 1, 0.65) + +[node name="GroundMesh" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +material_override = SubResource("StandardMaterial3D_ground") +cast_shadow = 0 +mesh = SubResource("PlaneMesh_ground") + +[node name="GroundBody" type="StaticBody3D" parent="."] + +[node name="GroundCollision" type="CollisionShape3D" parent="GroundBody"] +unique_name_in_owner = true +shape = SubResource("BoxShape3D_ground") + +[node name="GridLines" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +material_override = SubResource("StandardMaterial3D_grid") +cast_shadow = 0 +mesh = SubResource("ArrayMesh_q3k88") + +[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."] +transform = Transform3D(0.707107, -0.5, 0.5, 0, 0.707107, 0.707107, -0.707107, -0.5, 0.5, 0, 5, 0) +shadow_enabled = true + +[node name="Camera3D" type="Camera3D" parent="."] +unique_name_in_owner = true +transform = Transform3D(0.70710677, -0.40824828, 0.57735026, 0, 0.8164966, 0.57735026, -0.70710677, -0.40824828, 0.57735026, 6, 6, 6) +current = true +fov = 60.0 + +[node name="HoverHighlight" type="MeshInstance3D" parent="."] +unique_name_in_owner = true +visible = false +material_override = SubResource("StandardMaterial3D_highlight") +cast_shadow = 0 +mesh = SubResource("PlaneMesh_8dkrf") diff --git a/scenes/Grid/grid_plane_manager.gd b/scenes/Grid/grid_plane_manager.gd new file mode 100644 index 0000000..6b9ae1f --- /dev/null +++ b/scenes/Grid/grid_plane_manager.gd @@ -0,0 +1,394 @@ +@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 _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 _ground_mesh: MeshInstance3D = %GroundMesh +@onready var _ground_collision: CollisionShape3D = %GroundCollision +@onready var _grid_lines: MeshInstance3D = %GridLines +@onready var _camera: Camera3D = %Camera3D +@onready var _hover_highlight: MeshInstance3D = %HoverHighlight + +var _grid_size: Vector2 = Vector2.ZERO +var _cells: Dictionary = {} +var _queued_rebuild: bool = false +var _hovered_cell: Vector2i = Vector2i(-1, -1) +var _hex_center_offset_local: Vector3 = Vector3.ZERO + +func _ready() -> void: + set_process(true) + _queue_rebuild() + +func get_cells() -> Dictionary: + return _cells.duplicate() + +func get_cell_center(index: Vector2i) -> Vector3: + return _cells.get(index, Vector3.ZERO) + +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 _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_ground_assets() + _update_grid_lines() + _update_highlight_mesh() + _update_camera_view() + emit_signal("grid_layout_changed", _cells) + +func _update_ground_assets() -> void: + if _ground_mesh and _ground_mesh.mesh is PlaneMesh: + var plane_mesh := _ground_mesh.mesh as PlaneMesh + plane_mesh.size = _grid_size + if _ground_collision and _ground_collision.shape is BoxShape3D: + var box := _ground_collision.shape as BoxShape3D + box.size = Vector3(_grid_size.x, _ground_thickness, _grid_size.y) + +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: + if not _hover_highlight: + return + if _grid_type == GridType.HEX: + _hover_highlight.mesh = _build_hex_highlight_mesh() + else: + var plane := PlaneMesh.new() + plane.size = Vector2(_cell_size, _cell_size) + _hover_highlight.mesh = plane + +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) + +func _update_camera_view() -> void: + if not _camera: + return + var extent: float = max(_grid_size.x, _grid_size.y) + var half_extent: float = extent * 0.5 + var distance: float = max(half_extent * 1.1, 6.0) + var height: float = max(half_extent * 0.8, 6.0) + var offset: Vector3 = Vector3(distance, height, distance) + var target: Vector3 = global_transform.origin + var offset_global: Vector3 = global_transform.basis * offset + var desired_position: Vector3 = target + offset_global + _camera.look_at_from_position(desired_position, target, Vector3.UP) + +func _process(_delta: float) -> void: + if Engine.is_editor_hint(): + return + var mouse_position: Vector2 = get_viewport().get_mouse_position() + _update_hovered_cell(mouse_position) + +func _update_hovered_cell(mouse_position: Vector2) -> void: + if not _camera or not _hover_highlight: + return + var ray_origin: Vector3 = _camera.project_ray_origin(mouse_position) + var ray_direction: Vector3 = _camera.project_ray_normal(mouse_position) + var plane_normal: Vector3 = global_transform.basis.y.normalized() + var plane_point: Vector3 = global_transform.origin + var denominator: float = plane_normal.dot(ray_direction) + if abs(denominator) < 0.0001: + _clear_hover() + return + var t: float = plane_normal.dot(plane_point - ray_origin) / denominator + if t <= 0.0: + _clear_hover() + return + var world_point: Vector3 = ray_origin + ray_direction * t + var cell_index: Vector2i = world_to_cell(world_point, false) + if cell_index.x < 0 or cell_index.y < 0 or not _cells.has(cell_index): + _clear_hover() + return + if cell_index == _hovered_cell: + return + _hovered_cell = cell_index + var local_center: Vector3 = cell_to_world(cell_index) + var target_position: Vector3 = to_global(local_center) + _hover_highlight.global_position = target_position + plane_normal * 0.02 + _hover_highlight.visible = true + +func _clear_hover() -> void: + if _hover_highlight: + _hover_highlight.visible = false + _hovered_cell = Vector2i(-1, -1) diff --git a/scenes/Grid/grid_plane_manager.gd.uid b/scenes/Grid/grid_plane_manager.gd.uid new file mode 100644 index 0000000..f3acd4d --- /dev/null +++ b/scenes/Grid/grid_plane_manager.gd.uid @@ -0,0 +1 @@ +uid://3goclr4srt7k