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
## RTS 유닛 선택 및 이동을 통제하는 컨트롤러 (UnitController)
## 화면 전체를 덮는 Control 노드로 작동하여 마우스 입력과 선택 유닛 액션 버튼을 처리합니다.
const PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn")
const INVALID_TILE_CELL: Vector3i = Vector3i(2147483647, 2147483647, 2147483647)
const HudUnitPlacementScript: GDScript = preload("res://src/ui/unit_placement/hud_unit_placement.gd")
const UNIT_SELECTION_SCREEN_RADIUS: float = 54.0
const UNIT_ACTION_BUTTON_SIZE: Vector2 = Vector2(86.0, 34.0)
const UNIT_ACTION_BUTTON_GAP: float = 8.0
......@@ -12,12 +11,6 @@ const UNIT_ACTION_BUTTON_WORLD_HEIGHT: float = 1.55
const UNIT_SELL_REFUND_RATIO: float = 0.5
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개만 담습니다.
var selected_units: Array[Node] = []
......@@ -28,21 +21,17 @@ var _action_button_container: HBoxContainer = null
var _sell_button: Button = null
var _move_button: Button = null
var _upgrade_button: Button = null
var _placement_available_material: StandardMaterial3D = 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 = {}
var _tile_placement_helper: Object = null
func _ready() -> void:
# 화면 전체를 덮도록 설정
anchor_right = 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()
# 런타임에 NavigationRegion3D를 찾아 맵을 베이크시킴 (4.2 구현 보완)
var nav_region: NavigationRegion3D = get_node_or_null("../NavigationRegion3D") as NavigationRegion3D
if nav_region:
......@@ -54,20 +43,20 @@ func _process(_delta: float) -> void:
func _unhandled_input(event: InputEvent) -> void:
if GameManager.is_game_over:
return
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera:
return
if event is InputEventMouseButton:
var mouse_event: InputEventMouseButton = event as InputEventMouseButton
if mouse_event.button_index == MOUSE_BUTTON_LEFT and mouse_event.is_pressed():
if _is_waiting_for_tile_move_destination:
_try_move_selected_unit_to_tile(mouse_event.position)
else:
_perform_selection(mouse_event.position)
# 마우스 오른쪽 버튼 클릭 (이동 및 공격 지시)
elif mouse_event.button_index == MOUSE_BUTTON_RIGHT and mouse_event.is_pressed():
_perform_action(mouse_event.position)
......@@ -96,7 +85,7 @@ func _find_selectable_unit_from_mouse(mouse_pos: Vector2) -> Node3D:
var raycast_unit: Node = _get_unit_node(hit_collider)
if raycast_unit and raycast_unit.is_in_group("units") and raycast_unit is Node3D:
return raycast_unit as Node3D
return _find_nearest_unit_on_screen(mouse_pos)
## 화면 좌표 기준으로 마우스 근처의 가장 가까운 유닛을 반환합니다.
......@@ -105,24 +94,24 @@ func _find_nearest_unit_on_screen(mouse_pos: Vector2) -> Node3D:
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera:
return null
var nearest_unit: Node3D = null
var nearest_screen_distance: float = UNIT_SELECTION_SCREEN_RADIUS
var units: Array[Node] = get_tree().get_nodes_in_group("units")
for unit: Node in units:
if not is_instance_valid(unit) or not unit is Node3D:
continue
var unit_3d: Node3D = unit as Node3D
if camera.is_position_behind(unit_3d.global_position):
continue
var screen_pos: Vector2 = camera.unproject_position(unit_3d.global_position)
var screen_distance: float = screen_pos.distance_to(mouse_pos)
if screen_distance <= nearest_screen_distance:
nearest_screen_distance = screen_distance
nearest_unit = unit_3d
return nearest_unit
## 선택 유닛을 하나로 고정합니다.
......@@ -131,13 +120,13 @@ func _select_single_unit(unit_node: Node3D) -> void:
if _selected_unit == unit_node:
_show_unit_action_buttons()
return
_clear_selected_unit()
_selected_unit = unit_node
selected_units.clear()
selected_units.append(unit_node)
_cancel_tile_move_mode()
if unit_node.has_method("select"):
unit_node.call("select")
_show_unit_action_buttons()
......@@ -157,31 +146,31 @@ func _perform_action(mouse_pos: Vector2) -> void:
return
if _is_waiting_for_tile_move_destination:
return
# Raycast를 쏘아 바닥 또는 몬스터 감지
var ray_result: Dictionary = _raycast_from_mouse(mouse_pos)
if ray_result.is_empty():
return
var hit_pos: Vector3 = ray_result["position"]
var hit_collider: Node = ray_result["collider"] as Node
# 충돌 대상이 몬스터인지 검사
var monster_node: Node = _get_monster_node(hit_collider)
if monster_node and monster_node.is_in_group("monsters"):
# 선택된 유닛에게 공격 타겟 명령 하달
for unit: Node in selected_units:
if is_instance_valid(unit) and unit.has_method("attack_target"):
unit.call("attack_target", monster_node)
# 공격 지시 클릭 표시 (빨간색) 스폰
_spawn_click_indicator(monster_node.global_position, true)
else:
var selected_unit: Node = selected_units[0]
if is_instance_valid(selected_unit) and selected_unit.has_method("move_to"):
selected_unit.call("move_to", hit_pos)
# 지면 이동 클릭 표시 (초록색) 스폰
_spawn_click_indicator(hit_pos, false)
......@@ -191,11 +180,11 @@ func _spawn_click_indicator(pos: Vector3, is_attack: bool) -> void:
var indicator: Node3D = click_indicator_scene.instantiate() as Node3D
if not indicator:
return
# 지형 위에 살짝 띄워 배치 (Y=0.1m로 뎁스 충돌 방지)
indicator.global_position = pos + Vector3(0.0, 0.1, 0.0)
get_tree().current_scene.add_child(indicator)
# 이펙트 색상 설정 및 연출 개시
var color: Color = Color(0.1, 0.9, 0.1, 0.9) if not is_attack else Color(0.9, 0.1, 0.1, 0.9)
if indicator.has_method("setup_indicator"):
......@@ -206,15 +195,15 @@ func _raycast_from_mouse(mouse_pos: Vector2) -> Dictionary:
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera:
return {}
var from: Vector3 = camera.project_ray_origin(mouse_pos)
var to: Vector3 = from + camera.project_ray_normal(mouse_pos) * 1000.0
var space_state: PhysicsDirectSpaceState3D = camera.get_world_3d().direct_space_state
var query: PhysicsRayQueryParameters3D = PhysicsRayQueryParameters3D.create(from, to)
query.collide_with_areas = true
query.collide_with_bodies = true
return space_state.intersect_ray(query)
## 충돌체로부터 유닛 노드를 식별하여 반환
......@@ -261,7 +250,7 @@ func _get_monster_node(collider: Node) -> Node:
func _perform_hold_position() -> void:
if selected_units.is_empty():
return
for unit: Node in selected_units:
if is_instance_valid(unit) and unit.has_method("hold_position"):
unit.call("hold_position")
......@@ -275,11 +264,11 @@ func _create_unit_action_buttons() -> void:
_action_button_container.mouse_filter = Control.MOUSE_FILTER_STOP
_action_button_container.add_theme_constant_override("separation", int(UNIT_ACTION_BUTTON_GAP))
add_child(_action_button_container)
_sell_button = _create_unit_action_button("SellUnitButton", "팔기", Color(0.48, 0.08, 0.08, 0.94), Color(1.0, 0.32, 0.22, 0.86))
_move_button = _create_unit_action_button("MoveUnitButton", "이동", Color(0.08, 0.22, 0.42, 0.94), Color(0.35, 0.72, 1.0, 0.86))
_upgrade_button = _create_unit_action_button("UpgradeUnitButton", "업그레이드", Color(0.18, 0.20, 0.08, 0.94), Color(1.0, 0.82, 0.24, 0.86))
_sell_button.pressed.connect(_on_sell_selected_unit_pressed)
_move_button.pressed.connect(_on_move_selected_unit_pressed)
_upgrade_button.pressed.connect(_on_upgrade_selected_unit_pressed)
......@@ -339,17 +328,17 @@ func _update_unit_action_buttons_position() -> void:
if not _selected_unit or not is_instance_valid(_selected_unit):
_clear_selected_unit()
return
var camera: Camera3D = get_viewport().get_camera_3d()
if not camera:
_hide_unit_action_buttons()
return
var anchor_world_position: Vector3 = _selected_unit.global_position + Vector3(0.0, UNIT_ACTION_BUTTON_WORLD_HEIGHT, 0.0)
if camera.is_position_behind(anchor_world_position):
_action_button_container.visible = false
return
var screen_position: Vector2 = camera.unproject_position(anchor_world_position)
var container_size: Vector2 = _action_button_container.get_combined_minimum_size()
_action_button_container.size = container_size
......@@ -361,11 +350,11 @@ func _on_sell_selected_unit_pressed() -> void:
if not _selected_unit or not is_instance_valid(_selected_unit):
_clear_selected_unit()
return
var refund_amount: int = int(float(GameManager.unit_spawn_cost) * UNIT_SELL_REFUND_RATIO)
if refund_amount > 0:
GameManager.add_gold(refund_amount)
var unit_to_sell: Node3D = _selected_unit
_clear_selected_unit()
unit_to_sell.queue_free()
......@@ -385,16 +374,17 @@ func _enter_tile_move_mode() -> void:
if not _selected_unit or not is_instance_valid(_selected_unit):
_clear_selected_unit()
return
_is_waiting_for_tile_move_destination = true
_hide_unit_action_buttons()
_show_placement_tile_overlays()
_tile_placement_helper.call("show_placement_tile_overlays", _selected_unit)
## 타일 이동 모드를 종료하고 표시 중인 타일 오버레이를 풀로 반환합니다.
## 선택이 유지되는 경우에는 액션 버튼을 다시 띄울 수 있도록 별도 show 호출은 호출자가 결정합니다.
func _cancel_tile_move_mode() -> void:
_is_waiting_for_tile_move_destination = false
_clear_placement_tile_overlays()
if _tile_placement_helper:
_tile_placement_helper.call("clear_placement_tile_overlays")
## 타일 이동 모드에서 클릭한 화면 좌표를 GridMap 셀로 변환하고, 배치 가능하면 선택 유닛을 그 타일로 이동시킵니다.
## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다.
......@@ -402,21 +392,21 @@ func _try_move_selected_unit_to_tile(screen_pos: Vector2) -> void:
if not _selected_unit or not is_instance_valid(_selected_unit):
_clear_selected_unit()
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():
return
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
var tile_center: Vector3 = tile_info["center"]
if _selected_unit.has_method("move_to_tile"):
_selected_unit.call("move_to_tile", tile_center, tile_cell)
elif _selected_unit.has_method("move_to"):
_selected_unit.call("move_to", tile_center)
_spawn_click_indicator(tile_center, false)
_cancel_tile_move_mode()
_show_unit_action_buttons()
......@@ -429,284 +419,15 @@ func _on_upgrade_selected_unit_pressed() -> void:
return
if not _selected_unit is BaseUnit:
return
var selected_base_unit: BaseUnit = _selected_unit as BaseUnit
if not selected_base_unit.unit_data:
return
if not GameManager.spend_gold(UNIT_UPGRADE_COST):
return
selected_base_unit.unit_data = selected_base_unit.unit_data.duplicate(true) as UnitData
selected_base_unit.unit_data.damage *= 1.18
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.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"]
[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]
mesh_library = ExtResource("1_byiru")
......
......@@ -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 PLACEMENT_TILE_OVERLAY_SCENE: PackedScene = preload("res://src/ui/unit_placement/placement_tile_overlay.tscn")
const OVERLAY_POOL_WARMUP_BATCH_SIZE: int = 16
const UNIT_PLACE_BLOCKED_METADATA_KEY: String = "unit_place_blocked"
## MonsterPath와 가까운 타일을 "몬스터 길"로 판정할 때 추가로 더해 주는 여유 폭입니다.
## 실제 판정 거리는 `타일 반폭 + 타일 크기 * 이 비율`입니다.
......@@ -35,6 +36,7 @@ var _overlay_pool: Array[Node] = []
var _active_overlays: Array[Node] = []
var _overlay_pool_parent: Node = null
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: Dictionary = {}
......@@ -58,6 +60,34 @@ func setup(hud: Control, unit_spawn_button: Button, base_unit_scene: PackedScene
_base_unit_scene = base_unit_scene
_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이 준비된 시점에 사용 타일 수만큼 씬 인스턴스를 만들어 둡니다.
func warmup_overlay_pool_deferred() -> void:
......@@ -326,21 +356,48 @@ func _find_grid_map(node: Node) -> GridMap:
return null
## 이미 타일에 고정 배치된 유닛이 해당 셀을 점유 중인지 확인합니다.
## - MeshLibrary 타일 Mesh에 unit_place_blocked=true metadata가 있으면 "점유됨"으로 취급해 배치를 막습니다.
## - 몬스터가 지나다니는 길 타일은 유닛이 없어도 "점유됨"으로 취급해 배치를 막습니다.
## - 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()
if grid_map and _is_monster_path_tile(grid_map, tile_cell):
return true
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
var units: Array[Node] = _hud.get_tree().get_nodes_in_group("units")
for unit_node: Node in units:
if unit_node == ignored_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
## 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:
......@@ -460,8 +517,9 @@ func _clear_placement_tile_overlays() -> void:
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)
if _is_tile_unit_place_blocked(grid_map, tile_cell):
return
var is_occupied: bool = _is_tile_occupied(tile_cell, _overlay_ignored_unit)
var overlay: Node = _take_overlay_from_pool()
if not overlay:
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