Commit 312e1e41 authored by Gavin An's avatar Gavin An

스크립트 정리

parent 33596b43
## 3d MeshLibrary / GridMap 에는 커스텀 메타 데이터를 넣을 수 없음.
## 따라서, 스크립트를 통해 타일마다 메타데이터를 넣어줘야 함.
## 쓰이는 메타데이터가 있으면 여기다가 넣어야 함.
* 'unit_place_blocked': 해당 타일에 유닛을 설치못하는가
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -3,8 +3,7 @@ extends Control ...@@ -3,8 +3,7 @@ extends Control
## RTS 유닛 선택 및 이동을 통제하는 컨트롤러 (UnitController) ## RTS 유닛 선택 및 이동을 통제하는 컨트롤러 (UnitController)
## 화면 전체를 덮는 Control 노드로 작동하여 마우스 입력과 선택 유닛 액션 버튼을 처리합니다. ## 화면 전체를 덮는 Control 노드로 작동하여 마우스 입력과 선택 유닛 액션 버튼을 처리합니다.
const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn") const HudUnitPlacementScript: GDScript = preload("res://src/ui/unit_placement/hud_unit_placement.gd")
const INVALID_TILE_CELL: Vector3i = Vector3i(2147483647, 2147483647, 2147483647)
const UNIT_SELECTION_SCREEN_RADIUS: float = 54.0 const UNIT_SELECTION_SCREEN_RADIUS: float = 54.0
const UNIT_ACTION_BUTTON_SIZE: Vector2 = Vector2(86.0, 34.0) const UNIT_ACTION_BUTTON_SIZE: Vector2 = Vector2(86.0, 34.0)
const UNIT_ACTION_BUTTON_GAP: float = 8.0 const UNIT_ACTION_BUTTON_GAP: float = 8.0
...@@ -12,12 +11,6 @@ const UNIT_ACTION_BUTTON_WORLD_HEIGHT: float = 1.55 ...@@ -12,12 +11,6 @@ const UNIT_ACTION_BUTTON_WORLD_HEIGHT: float = 1.55
const UNIT_SELL_REFUND_RATIO: float = 0.5 const UNIT_SELL_REFUND_RATIO: float = 0.5
const UNIT_UPGRADE_COST: int = 75 const UNIT_UPGRADE_COST: int = 75
## MonsterPath와 가까운 타일을 "몬스터 길"로 판정할 때 추가로 더해 주는 여유 폭입니다.
## 실제 판정 거리는 `타일 반폭 + 타일 크기 * 이 비율`입니다.
## 예를 들어 GridMap 셀 크기가 2m이고 값이 0.15라면, 타일 중심이 Path3D에서 1.3m 이내일 때 길 타일로 취급합니다.
## Path3D가 타일 정중앙을 지나지 않거나 모델/그리드가 조금 어긋나도 몬스터 길에 유닛이 재배치되지 않게 막는 안전 마진입니다.
const MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO: float = 0.15
## 현재 선택된 유닛 리스트입니다. ## 현재 선택된 유닛 리스트입니다.
## 예전 다중 선택 API를 참조하는 코드가 있을 수 있어 배열 형태는 유지하지만, 실제로는 항상 0개 또는 1개만 담습니다. ## 예전 다중 선택 API를 참조하는 코드가 있을 수 있어 배열 형태는 유지하지만, 실제로는 항상 0개 또는 1개만 담습니다.
var selected_units: Array[Node] = [] var selected_units: Array[Node] = []
...@@ -28,19 +21,15 @@ var _action_button_container: HBoxContainer = null ...@@ -28,19 +21,15 @@ var _action_button_container: HBoxContainer = null
var _sell_button: Button = null var _sell_button: Button = null
var _move_button: Button = null var _move_button: Button = null
var _upgrade_button: Button = null var _upgrade_button: Button = null
var _placement_available_material: StandardMaterial3D = null var _tile_placement_helper: Object = null
var _placement_occupied_material: StandardMaterial3D = null
var _overlay_pool: Array[Node] = []
var _active_overlays: Array[Node] = []
var _overlay_pool_parent: Node = null
var _monster_path_tile_cache_grid_map: GridMap = null
var _monster_path_tile_cache: Dictionary = {}
func _ready() -> void: func _ready() -> void:
# 화면 전체를 덮도록 설정 # 화면 전체를 덮도록 설정
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
mouse_filter = MOUSE_FILTER_IGNORE # UI가 아닌 unhandled input으로 동작하게 통과 처리 mouse_filter = MOUSE_FILTER_IGNORE # UI가 아닌 unhandled input으로 동작하게 통과 처리
_tile_placement_helper = HudUnitPlacementScript.new()
_tile_placement_helper.call("setup_tile_helper", self)
_create_unit_action_buttons() _create_unit_action_buttons()
# 런타임에 NavigationRegion3D를 찾아 맵을 베이크시킴 (4.2 구현 보완) # 런타임에 NavigationRegion3D를 찾아 맵을 베이크시킴 (4.2 구현 보완)
...@@ -388,13 +377,14 @@ func _enter_tile_move_mode() -> void: ...@@ -388,13 +377,14 @@ func _enter_tile_move_mode() -> void:
_is_waiting_for_tile_move_destination = true _is_waiting_for_tile_move_destination = true
_hide_unit_action_buttons() _hide_unit_action_buttons()
_show_placement_tile_overlays() _tile_placement_helper.call("show_placement_tile_overlays", _selected_unit)
## 타일 이동 모드를 종료하고 표시 중인 타일 오버레이를 풀로 반환합니다. ## 타일 이동 모드를 종료하고 표시 중인 타일 오버레이를 풀로 반환합니다.
## 선택이 유지되는 경우에는 액션 버튼을 다시 띄울 수 있도록 별도 show 호출은 호출자가 결정합니다. ## 선택이 유지되는 경우에는 액션 버튼을 다시 띄울 수 있도록 별도 show 호출은 호출자가 결정합니다.
func _cancel_tile_move_mode() -> void: func _cancel_tile_move_mode() -> void:
_is_waiting_for_tile_move_destination = false _is_waiting_for_tile_move_destination = false
_clear_placement_tile_overlays() if _tile_placement_helper:
_tile_placement_helper.call("clear_placement_tile_overlays")
## 타일 이동 모드에서 클릭한 화면 좌표를 GridMap 셀로 변환하고, 배치 가능하면 선택 유닛을 그 타일로 이동시킵니다. ## 타일 이동 모드에서 클릭한 화면 좌표를 GridMap 셀로 변환하고, 배치 가능하면 선택 유닛을 그 타일로 이동시킵니다.
## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다. ## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다.
...@@ -403,12 +393,12 @@ func _try_move_selected_unit_to_tile(screen_pos: Vector2) -> void: ...@@ -403,12 +393,12 @@ func _try_move_selected_unit_to_tile(screen_pos: Vector2) -> void:
_clear_selected_unit() _clear_selected_unit()
return return
var tile_info: Dictionary = _get_tile_info_from_screen(screen_pos) var tile_info: Dictionary = _tile_placement_helper.call("get_tile_info_from_screen", screen_pos) as Dictionary
if tile_info.is_empty(): if tile_info.is_empty():
return return
var tile_cell: Vector3i = tile_info["cell"] var tile_cell: Vector3i = tile_info["cell"]
if _is_tile_occupied(tile_cell, _selected_unit): if _tile_placement_helper.call("is_tile_occupied", tile_cell, _selected_unit) == true:
return return
var tile_center: Vector3 = tile_info["center"] var tile_center: Vector3 = tile_info["center"]
...@@ -441,272 +431,3 @@ func _on_upgrade_selected_unit_pressed() -> void: ...@@ -441,272 +431,3 @@ func _on_upgrade_selected_unit_pressed() -> void:
selected_base_unit.unit_data.attack_range *= 1.04 selected_base_unit.unit_data.attack_range *= 1.04
selected_base_unit.unit_data.cooldown = maxf(0.15, selected_base_unit.unit_data.cooldown * 0.94) selected_base_unit.unit_data.cooldown = maxf(0.15, selected_base_unit.unit_data.cooldown * 0.94)
selected_base_unit.unit_data.move_speed *= 1.04 selected_base_unit.unit_data.move_speed *= 1.04
## 화면 좌표에 대응하는 GridMap 셀과 타일 중심 좌표를 계산합니다.
## 유닛 생성 배치와 동일하게, 카메라 레이캐스트 후 충돌 지점 주변의 유효 GridMap 셀을 찾습니다.
func _get_tile_info_from_screen(screen_pos: Vector2) -> Dictionary:
var camera: Camera3D = get_viewport().get_camera_3d()
var grid_map: GridMap = _get_stage_grid_map()
if not camera or not grid_map:
return {}
var ray_origin: Vector3 = camera.project_ray_origin(screen_pos)
var ray_end: Vector3 = ray_origin + camera.project_ray_normal(screen_pos) * 1000.0
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(ray_origin, ray_end)
var result: Dictionary = camera.get_world_3d().direct_space_state.intersect_ray(query)
if result.is_empty() or not result.has("position"):
return {}
var hit_position: Vector3 = result["position"]
var tile_cell: Vector3i = _get_valid_tile_cell(grid_map, hit_position)
if tile_cell == INVALID_TILE_CELL:
return {}
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
return {
"cell": tile_cell,
"center": tile_center
}
## 레이 충돌 위치와 가장 가까운 유효 GridMap 셀을 반환합니다.
## 타일 메시 높이나 클릭 위치가 조금 어긋나도 같은 x/z 주변 y 셀을 확인해 타일 선택이 쉽게 실패하지 않게 합니다.
func _get_valid_tile_cell(grid_map: GridMap, world_position: Vector3) -> Vector3i:
var local_position: Vector3 = grid_map.to_local(world_position)
var tile_cell: Vector3i = grid_map.local_to_map(local_position)
if grid_map.get_cell_item(tile_cell) != -1:
return tile_cell
var ground_cell: Vector3i = Vector3i(tile_cell.x, 0, tile_cell.z)
if grid_map.get_cell_item(ground_cell) != -1:
return ground_cell
for y: int in range(tile_cell.y - 4, tile_cell.y + 5):
var nearby_cell: Vector3i = Vector3i(tile_cell.x, y, tile_cell.z)
if grid_map.get_cell_item(nearby_cell) != -1:
return nearby_cell
return INVALID_TILE_CELL
## 현재 전장에 동적으로 붙은 StageMap GridMap을 찾습니다.
## main.tscn에서는 StageMap이 NavigationRegion3D 아래에 붙으므로, 고정 경로 대신 재귀 탐색을 사용합니다.
func _get_stage_grid_map() -> GridMap:
var current_scene: Node = get_tree().current_scene
if not current_scene:
return null
return _find_grid_map(current_scene)
## 자식 노드를 재귀 순회하며 첫 번째 GridMap을 반환합니다.
func _find_grid_map(node: Node) -> GridMap:
if node is GridMap:
return node as GridMap
for child: Node in node.get_children():
var found_grid: GridMap = _find_grid_map(child)
if found_grid:
return found_grid
return null
## 해당 타일이 이동 목적지로 막혀 있는지 확인합니다.
## selected_unit은 현재 움직이려는 유닛이므로, 자기 자신이 점유한 기존 타일 때문에 재배치가 막히지 않도록 제외합니다.
func _is_tile_occupied(tile_cell: Vector3i, selected_unit: Node = null) -> bool:
var grid_map: GridMap = _get_stage_grid_map()
if grid_map and _is_monster_path_tile(grid_map, tile_cell):
return true
var units: Array[Node] = get_tree().get_nodes_in_group("units")
for unit_node: Node in units:
if unit_node == selected_unit:
continue
if unit_node is BaseUnit:
var unit: BaseUnit = unit_node as BaseUnit
if unit.is_tile_bound and unit.occupied_tile_cell == tile_cell:
return true
return false
## 몬스터 이동 경로로 판단되는 타일인지 확인합니다.
## 길 타일은 선택 오버레이에서 아예 제외하고, 실제 클릭 이동 목적지로도 허용하지 않습니다.
func _is_monster_path_tile(grid_map: GridMap, tile_cell: Vector3i) -> bool:
_ensure_monster_path_tile_cache(grid_map)
return _monster_path_tile_cache.has(tile_cell)
## 현재 GridMap 기준으로 몬스터 길 타일 캐시를 준비합니다.
## 전체 타일 오버레이를 만들 때 매 셀마다 Path3D 거리 계산을 반복하지 않도록 맵 단위로 한 번만 계산합니다.
func _ensure_monster_path_tile_cache(grid_map: GridMap) -> void:
if _monster_path_tile_cache_grid_map == grid_map:
return
_monster_path_tile_cache_grid_map = grid_map
_monster_path_tile_cache.clear()
var used_cells: Array = grid_map.get_used_cells()
if used_cells.is_empty():
return
var cell_bounds: Dictionary = _get_used_cell_xz_bounds(used_cells)
var monster_path: Path3D = _get_monster_path()
for cell: Vector3i in used_cells:
if _is_outer_edge_cell(cell, cell_bounds) or _is_cell_close_to_monster_path(grid_map, monster_path, cell):
_monster_path_tile_cache[cell] = true
## GridMap 사용 셀의 x/z 최소/최대 범위를 계산합니다.
## 현재 맵은 몬스터 길이 외곽을 따라 돌기 때문에 외곽 셀을 빠르게 차단하는 데 사용합니다.
func _get_used_cell_xz_bounds(used_cells: Array) -> Dictionary:
var first_cell: Vector3i = used_cells[0] as Vector3i
var min_x: int = first_cell.x
var max_x: int = first_cell.x
var min_z: int = first_cell.z
var max_z: int = first_cell.z
for cell: Vector3i in used_cells:
min_x = mini(min_x, cell.x)
max_x = maxi(max_x, cell.x)
min_z = mini(min_z, cell.z)
max_z = maxi(max_z, cell.z)
return {
"min_x": min_x,
"max_x": max_x,
"min_z": min_z,
"max_z": max_z
}
## 사용 셀 범위의 가장자리 타일인지 확인합니다.
func _is_outer_edge_cell(tile_cell: Vector3i, cell_bounds: Dictionary) -> bool:
return (
tile_cell.x == cell_bounds["min_x"]
or tile_cell.x == cell_bounds["max_x"]
or tile_cell.z == cell_bounds["min_z"]
or tile_cell.z == cell_bounds["max_z"]
)
## 타일 중심이 MonsterPath에 가까우면 몬스터 길로 간주합니다.
## 외곽 길이 아닌 스테이지가 추가되어도 Path3D를 따라 재배치 금지 영역이 자동으로 생기도록 보강합니다.
func _is_cell_close_to_monster_path(grid_map: GridMap, monster_path: Path3D, tile_cell: Vector3i) -> bool:
if not monster_path or not monster_path.curve:
return false
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
var local_tile_center: Vector3 = monster_path.to_local(tile_center)
var closest_local_point: Vector3 = monster_path.curve.get_closest_point(local_tile_center)
var closest_world_point: Vector3 = monster_path.to_global(closest_local_point)
# 재배치 판정은 바닥 평면 기준이므로 y 높이 차이는 무시합니다.
# half_tile_width는 타일 중심에서 타일 가장자리까지의 거리이고,
# path_margin은 MonsterPath가 타일 중앙선에서 조금 벗어난 경우까지 길 타일로 묶기 위한 추가 여유입니다.
tile_center.y = 0.0
closest_world_point.y = 0.0
var half_tile_width: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * 0.5
var path_margin: float = maxf(grid_map.cell_size.x, grid_map.cell_size.z) * MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO
return tile_center.distance_to(closest_world_point) <= half_tile_width + path_margin
## 현재 전장 씬의 MonsterPath를 찾습니다.
func _get_monster_path() -> Path3D:
var current_scene: Node = get_tree().current_scene
if not current_scene:
return null
var path_node: Node = current_scene.find_child("MonsterPath", true, false)
if path_node is Path3D:
return path_node as Path3D
return null
## 선택 유닛 재배치 모드에 필요한 타일 오버레이를 표시합니다.
## 유닛 생성 배치 화면처럼 가능한 타일과 이미 점유된 타일을 구분하고, 몬스터 길 타일은 투명하게 남깁니다.
func _show_placement_tile_overlays() -> void:
_clear_placement_tile_overlays()
var grid_map: GridMap = _get_stage_grid_map()
var parent_scene: Node = get_tree().current_scene
if not grid_map or not parent_scene:
return
var used_cells: Array = grid_map.get_used_cells()
_ensure_monster_path_tile_cache(grid_map)
_ensure_overlay_pool_capacity(parent_scene, used_cells.size())
for cell: Vector3i in used_cells:
_add_placement_tile_overlay(grid_map, cell)
## 표시 중인 타일 오버레이를 삭제하지 않고 풀로 반환합니다.
func _clear_placement_tile_overlays() -> void:
for overlay: Node in _active_overlays:
if is_instance_valid(overlay):
if overlay.has_method("release_to_pool"):
overlay.call("release_to_pool")
_overlay_pool.append(overlay)
_active_overlays.clear()
## 타일 하나에 대응하는 오버레이 씬을 풀에서 꺼내 위치/크기/재질을 설정합니다.
func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void:
if _is_monster_path_tile(grid_map, tile_cell):
return
var is_occupied: bool = _is_tile_occupied(tile_cell, _selected_unit)
var overlay: Node = _take_overlay_from_pool()
if not overlay:
return
var tile_center: Vector3 = grid_map.to_global(grid_map.map_to_local(tile_cell))
var tile_size: Vector2 = Vector2(grid_map.cell_size.x, grid_map.cell_size.z)
if overlay.has_method("configure"):
overlay.call("configure", tile_center, tile_size, _get_placement_material(is_occupied), is_occupied)
_active_overlays.append(overlay)
## 오버레이 풀이 필요한 수량만큼 준비되어 있는지 확인합니다.
func _ensure_overlay_pool_capacity(parent_scene: Node, required_count: int) -> void:
_reset_overlay_pool_if_parent_changed(parent_scene)
var total_count: int = _overlay_pool.size() + _active_overlays.size()
var missing_count: int = maxi(0, required_count - total_count)
for index: int in range(missing_count):
_add_overlay_instance_to_pool(parent_scene)
## placement_tile_overlay.tscn 인스턴스 하나를 생성해 현재 씬에 붙이고 비활성 풀에 보관합니다.
func _add_overlay_instance_to_pool(parent_scene: Node) -> void:
var overlay: Node = PLACEMENT_TILE_OVERLAY_SCENE.instantiate()
if not overlay:
return
parent_scene.add_child(overlay)
if overlay.has_method("release_to_pool"):
overlay.call("release_to_pool")
_overlay_pool.append(overlay)
## 현재 씬이 바뀌면 이전 씬에 붙어 있던 풀 참조와 길 타일 캐시를 버립니다.
func _reset_overlay_pool_if_parent_changed(parent_scene: Node) -> void:
if _overlay_pool_parent == parent_scene:
return
_overlay_pool.clear()
_active_overlays.clear()
_monster_path_tile_cache_grid_map = null
_monster_path_tile_cache.clear()
_overlay_pool_parent = parent_scene
## 비활성 오버레이 하나를 풀에서 꺼냅니다.
func _take_overlay_from_pool() -> Node:
if _overlay_pool.is_empty():
return null
return _overlay_pool.pop_back()
## 배치 가능/점유 상태에 맞는 오버레이 머티리얼을 반환합니다.
## 현재 오버레이 씬은 내부에서 파츠별 색을 처리하지만, 기존 configure() 시그니처 호환을 위해 머티리얼을 전달합니다.
func _get_placement_material(is_occupied: bool) -> StandardMaterial3D:
if is_occupied:
if not _placement_occupied_material:
_placement_occupied_material = _create_placement_material(Color(1.0, 0.05, 0.02, 0.38))
return _placement_occupied_material
if not _placement_available_material:
_placement_available_material = _create_placement_material(Color(0.05, 1.0, 0.2, 0.32))
return _placement_available_material
## 타일 오버레이 호환용 반투명 머티리얼을 생성합니다.
func _create_placement_material(color: Color) -> StandardMaterial3D:
var material: StandardMaterial3D = StandardMaterial3D.new()
material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
material.cull_mode = BaseMaterial3D.CULL_DISABLED
material.no_depth_test = true
return material
[gd_scene format=3 uid="uid://doj12ce3mmosn"] [gd_scene format=3 uid="uid://doj12ce3mmosn"]
[ext_resource type="MeshLibrary" uid="uid://bh17fgvq0gpu4" path="res://assets/tile/desert/scenes/desert_mesh_library.tres" id="1_byiru"] [ext_resource type="MeshLibrary" uid="uid://dh2qv7hjdu846" path="res://assets/tile/desert/scenes/desert_mesh_library.tres" id="1_byiru"]
[node name="Stage1Map" type="GridMap" unique_id=875122487] [node name="Stage1Map" type="GridMap" unique_id=875122487]
mesh_library = ExtResource("1_byiru") mesh_library = ExtResource("1_byiru")
......
...@@ -12,6 +12,7 @@ const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit ...@@ -12,6 +12,7 @@ const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit
const WARRIOR_LV1_SCENE: PackedScene = preload("res://src/entities/units/warrior/lv1/warrior_unit.tscn") const WARRIOR_LV1_SCENE: PackedScene = preload("res://src/entities/units/warrior/lv1/warrior_unit.tscn")
const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn") const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn")
const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16 const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16
const UNIT_PLACE_BLOCKED_METADATA_KEY: String = "unit_place_blocked"
## MonsterPath와 가까운 타일을 "몬스터 길"로 판정할 때 추가로 더해 주는 여유 폭입니다. ## MonsterPath와 가까운 타일을 "몬스터 길"로 판정할 때 추가로 더해 주는 여유 폭입니다.
## 실제 판정 거리는 `타일 반폭 + 타일 크기 * 이 비율`입니다. ## 실제 판정 거리는 `타일 반폭 + 타일 크기 * 이 비율`입니다.
...@@ -35,6 +36,7 @@ var _overlay_pool: Array[Node] = [] ...@@ -35,6 +36,7 @@ var _overlay_pool: Array[Node] = []
var _active_overlays: Array[Node] = [] var _active_overlays: Array[Node] = []
var _overlay_pool_parent: Node = null var _overlay_pool_parent: Node = null
var _is_overlay_pool_warming_up: bool = false var _is_overlay_pool_warming_up: bool = false
var _overlay_ignored_unit: Node = null
var _monster_path_tile_cache_grid_map: GridMap = null var _monster_path_tile_cache_grid_map: GridMap = null
var _monster_path_tile_cache: Dictionary = {} var _monster_path_tile_cache: Dictionary = {}
...@@ -58,6 +60,34 @@ func setup(hud: Control, unit_spawn_button: Button, base_unit_scene: PackedScene ...@@ -58,6 +60,34 @@ func setup(hud: Control, unit_spawn_button: Button, base_unit_scene: PackedScene
_base_unit_scene = base_unit_scene _base_unit_scene = base_unit_scene
_spawn_button_default_text = _unit_spawn_button.text _spawn_button_default_text = _unit_spawn_button.text
## UnitController처럼 HUD 버튼 없이 타일 선택/오버레이 기능만 필요한 코드에서 사용하는 간단한 초기화입니다.
## 이 파일이 타일 레이캐스트, GridMap 탐색, 몬스터 길 캐시, 오버레이 풀을 한곳에서 관리하게 만들어
## 유닛 생성 배치와 선택 유닛 재배치가 같은 판정을 공유하도록 합니다.
func setup_tile_helper(owner_control: Control) -> void:
_hud = owner_control
## 외부 컨트롤러가 화면 좌표를 GridMap 셀 정보로 바꿀 때 사용하는 공개 wrapper입니다.
## 내부 구현은 HUD의 유닛 생성 배치와 동일한 `_get_tile_info_from_screen()`을 그대로 사용합니다.
func get_tile_info_from_screen(screen_pos: Vector2) -> Dictionary:
return _get_tile_info_from_screen(screen_pos)
## 외부 컨트롤러가 타일 배치 가능 여부를 검사할 때 사용하는 공개 wrapper입니다.
## ignored_unit은 재배치 중인 자기 자신을 점유 검사에서 제외하기 위한 값입니다.
func is_tile_occupied(tile_cell: Vector3i, ignored_unit: Node=null) -> bool:
return _is_tile_occupied(tile_cell, ignored_unit)
## 외부 컨트롤러가 선택 유닛 재배치 모드에 들어갈 때 사용하는 공개 wrapper입니다.
## ignored_unit은 현재 움직이려는 유닛이며, 자기 기존 타일이 빨간색 점유 타일로 표시되지 않게 합니다.
func show_placement_tile_overlays(ignored_unit: Node=null) -> void:
_overlay_ignored_unit = ignored_unit
_show_placement_tile_overlays()
## 외부 컨트롤러가 재배치 모드를 끝낼 때 사용하는 공개 wrapper입니다.
## 오버레이 노드는 삭제하지 않고 풀에 반납해 다음 유닛 생성/이동 배치 때 끊김 없이 재사용합니다.
func clear_placement_tile_overlays() -> void:
_overlay_ignored_unit = null
_clear_placement_tile_overlays()
## 전장 진입 직후 타일 오버레이 풀을 미리 준비합니다. ## 전장 진입 직후 타일 오버레이 풀을 미리 준비합니다.
## HUD _ready()에서 호출되며, 한 프레임 뒤 StageMap/GridMap이 준비된 시점에 사용 타일 수만큼 씬 인스턴스를 만들어 둡니다. ## HUD _ready()에서 호출되며, 한 프레임 뒤 StageMap/GridMap이 준비된 시점에 사용 타일 수만큼 씬 인스턴스를 만들어 둡니다.
func warmup_overlay_pool_deferred() -> void: func warmup_overlay_pool_deferred() -> void:
...@@ -326,21 +356,48 @@ func _find_grid_map(node: Node) -> GridMap: ...@@ -326,21 +356,48 @@ func _find_grid_map(node: Node) -> GridMap:
return null return null
## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다. ## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다.
## - MeshLibrary 타일 Mesh에 unit_place_blocked=true metadata가 있으면 "점유됨"으로 취급해 배치를 막습니다.
## - 몬스터가 지나다니는 길 타일은 유닛이 없어도 "점유됨"으로 취급해 배치를 막습니다. ## - 몬스터가 지나다니는 길 타일은 유닛이 없어도 "점유됨"으로 취급해 배치를 막습니다.
## - BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다. ## - BaseUnit.place_on_tile()에서 기록한 occupied_tile_cell을 기준으로 중복 배치를 막습니다.
func _is_tile_occupied(tile_cell: Vector3i) -> bool: func _is_tile_occupied(tile_cell: Vector3i, ignored_unit: Node=null) -> bool:
var grid_map: GridMap = _get_stage_grid_map() var grid_map: GridMap = _get_stage_grid_map()
if grid_map and _is_monster_path_tile(grid_map, tile_cell): if grid_map:
if _is_tile_unit_place_blocked(grid_map, tile_cell):
return true
if _is_monster_path_tile(grid_map, tile_cell):
return true return true
var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units") var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units")
for unit_node: Node in units: for unit_node: Node in units:
if unit_node == ignored_unit:
continue
if unit_node is BaseUnit: if unit_node is BaseUnit:
var unit: BaseUnit = unit_node as BaseUnit var unit: BaseUnit = unit_node as BaseUnit
if unit.is_tile_bound and unit.occupied_tile_cell == tile_cell: if unit.is_tile_bound and unit.occupied_tile_cell == tile_cell:
return true return true
return false return false
## GridMap 셀에 배치된 MeshLibrary item의 Mesh metadata를 읽어 배치 금지 타일인지 확인합니다.
## MeshLibrary item 자체에는 Godot API로 노출되는 metadata 슬롯이 없어서,
## desert_mesh_library.tres 작업 시 각 item이 참조하는 ArrayMesh에 unit_place_blocked 값을 저장해 둡니다.
## metadata가 없거나 MeshLibrary/item/mesh를 찾지 못하면 기본값은 false로 두어 기존 타일 동작을 보존합니다.
func _is_tile_unit_place_blocked(grid_map: GridMap, tile_cell: Vector3i) -> bool:
var item_id: int = grid_map.get_cell_item(tile_cell)
if item_id == -1:
return false
var mesh_library: MeshLibrary = grid_map.mesh_library
if mesh_library == null:
return false
var item_mesh: Mesh = mesh_library.get_item_mesh(item_id)
if item_mesh == null:
return false
return item_mesh.get_meta(UNIT_PLACE_BLOCKED_METADATA_KEY, false) == true
## 몬스터 이동 경로로 판단되는 타일인지 확인합니다. ## 몬스터 이동 경로로 판단되는 타일인지 확인합니다.
## 길 타일은 "이미 유닛이 있다"는 의미의 점유와는 성격이 다르지만, 최종 배치 가능 여부에서는 똑같이 막혀야 합니다. ## 길 타일은 "이미 유닛이 있다"는 의미의 점유와는 성격이 다르지만, 최종 배치 가능 여부에서는 똑같이 막혀야 합니다.
func _is_monster_path_tile(grid_map: GridMap, tile_cell: Vector3i) -> bool: func _is_monster_path_tile(grid_map: GridMap, tile_cell: Vector3i) -> bool:
...@@ -460,8 +517,9 @@ func _clear_placement_tile_overlays() -> void: ...@@ -460,8 +517,9 @@ func _clear_placement_tile_overlays() -> void:
func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void: func _add_placement_tile_overlay(grid_map: GridMap, tile_cell: Vector3i) -> void:
if _is_monster_path_tile(grid_map, tile_cell): if _is_monster_path_tile(grid_map, tile_cell):
return return
if _is_tile_unit_place_blocked(grid_map, tile_cell):
var is_occupied: bool = _is_tile_occupied(tile_cell) return
var is_occupied: bool = _is_tile_occupied(tile_cell, _overlay_ignored_unit)
var overlay: Node = _take_overlay_from_pool() var overlay: Node = _take_overlay_from_pool()
if not overlay: if not overlay:
return return
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment