Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Sign in / Register
Toggle navigation
L
ldefense
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Packages
Packages
Container Registry
Analytics
CI / CD Analytics
Repository Analytics
Value Stream Analytics
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Gavin An
ldefense
Commits
d8eba5c2
Commit
d8eba5c2
authored
Jul 27, 2026
by
Gavin An
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
작업중
parent
24121fe1
Changes
3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
363 additions
and
20 deletions
+363
-20
src/core/unit_controller.gd
src/core/unit_controller.gd
+328
-18
src/entities/units/base/base_unit.gd
src/entities/units/base/base_unit.gd
+28
-0
src/ui/unit_placement/hud_unit_placement.gd
src/ui/unit_placement/hud_unit_placement.gd
+7
-2
No files found.
src/core/unit_controller.gd
View file @
d8eba5c2
...
@@ -3,6 +3,8 @@ extends Control
...
@@ -3,6 +3,8 @@ 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
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
...
@@ -10,16 +12,29 @@ const UNIT_ACTION_BUTTON_WORLD_HEIGHT: float = 1.55
...
@@ -10,16 +12,29 @@ 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
]
=
[]
var
_selected_unit
:
Node3D
=
null
var
_selected_unit
:
Node3D
=
null
var
_is_waiting_for_move_destination
:
bool
=
false
var
_is_waiting_for_
tile_
move_destination
:
bool
=
false
var
_action_button_container
:
HBoxContainer
=
null
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
_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
:
# 화면 전체를 덮도록 설정
# 화면 전체를 덮도록 설정
...
@@ -48,8 +63,8 @@ func _unhandled_input(event: InputEvent) -> void:
...
@@ -48,8 +63,8 @@ func _unhandled_input(event: InputEvent) -> void:
var
mouse_event
:
InputEventMouseButton
=
event
as
InputEventMouseButton
var
mouse_event
:
InputEventMouseButton
=
event
as
InputEventMouseButton
if
mouse_event
.
button_index
==
MOUSE_BUTTON_LEFT
and
mouse_event
.
is_pressed
():
if
mouse_event
.
button_index
==
MOUSE_BUTTON_LEFT
and
mouse_event
.
is_pressed
():
if
_is_waiting_for_move_destination
:
if
_is_waiting_for_
tile_
move_destination
:
_
perform_selected_unit_mov
e
(
mouse_event
.
position
)
_
try_move_selected_unit_to_til
e
(
mouse_event
.
position
)
else
:
else
:
_perform_selection
(
mouse_event
.
position
)
_perform_selection
(
mouse_event
.
position
)
...
@@ -121,7 +136,7 @@ func _select_single_unit(unit_node: Node3D) -> void:
...
@@ -121,7 +136,7 @@ func _select_single_unit(unit_node: Node3D) -> void:
_selected_unit
=
unit_node
_selected_unit
=
unit_node
selected_units
.
clear
()
selected_units
.
clear
()
selected_units
.
append
(
unit_node
)
selected_units
.
append
(
unit_node
)
_
is_waiting_for_move_destination
=
false
_
cancel_tile_move_mode
()
if
unit_node
.
has_method
(
"select"
):
if
unit_node
.
has_method
(
"select"
):
unit_node
.
call
(
"select"
)
unit_node
.
call
(
"select"
)
...
@@ -133,13 +148,15 @@ func _clear_selected_unit() -> void:
...
@@ -133,13 +148,15 @@ func _clear_selected_unit() -> void:
_selected_unit
.
call
(
"deselect"
)
_selected_unit
.
call
(
"deselect"
)
_selected_unit
=
null
_selected_unit
=
null
selected_units
.
clear
()
selected_units
.
clear
()
_
is_waiting_for_move_destination
=
false
_
cancel_tile_move_mode
()
_hide_unit_action_buttons
()
_hide_unit_action_buttons
()
## 우클릭 액션 (이동 목적지 지정 또는 적 공격 타겟팅)
## 우클릭 액션 (이동 목적지 지정 또는 적 공격 타겟팅)
func
_perform_action
(
mouse_pos
:
Vector2
)
->
void
:
func
_perform_action
(
mouse_pos
:
Vector2
)
->
void
:
if
selected_units
.
is_empty
():
if
selected_units
.
is_empty
():
return
return
if
_is_waiting_for_tile_move_destination
:
return
# Raycast를 쏘아 바닥 또는 몬스터 감지
# Raycast를 쏘아 바닥 또는 몬스터 감지
var
ray_result
:
Dictionary
=
_raycast_from_mouse
(
mouse_pos
)
var
ray_result
:
Dictionary
=
_raycast_from_mouse
(
mouse_pos
)
...
@@ -354,29 +371,54 @@ func _on_sell_selected_unit_pressed() -> void:
...
@@ -354,29 +371,54 @@ func _on_sell_selected_unit_pressed() -> void:
unit_to_sell
.
queue_free
()
unit_to_sell
.
queue_free
()
## 이동 버튼 처리입니다.
## 이동 버튼 처리입니다.
## 버튼을 누른 뒤 다음 좌클릭 위치를 선택 유닛의 이동 목적지로 사용합니다.
## 버튼을 누르면 일반 바닥 클릭 이동이 아니라, 유닛 생성 배치와 같은 타일 선택 모드로 들어갑니다.
## 배치 가능한 타일에는 초록 오버레이가 뜨고, 이미 유닛이 있는 타일은 빨간 오버레이로 표시됩니다.
func
_on_move_selected_unit_pressed
()
->
void
:
func
_on_move_selected_unit_pressed
()
->
void
:
if
not
_selected_unit
or
not
is_instance_valid
(
_selected_unit
):
if
not
_selected_unit
or
not
is_instance_valid
(
_selected_unit
):
_clear_selected_unit
()
_clear_selected_unit
()
return
return
_
is_waiting_for_move_destination
=
true
_
enter_tile_move_mode
()
##
이동 버튼 후속 좌클릭 처리입
니다.
##
선택 유닛의 타일 이동 모드에 진입합
니다.
##
바닥을 찍으면 선택 유닛에 move_to()를 호출하고, 클릭 인디케이터를 표시한 뒤 이동 대기 상태를 해제
합니다.
##
액션 버튼은 타일 선택을 가리지 않도록 잠시 숨기고, 선택 가능한 전체 타일 오버레이를 표시
합니다.
func
_
perform_selected_unit_move
(
mouse_pos
:
Vector2
)
->
void
:
func
_
enter_tile_move_mode
(
)
->
void
:
if
not
_selected_unit
or
not
is_instance_valid
(
_selected_unit
):
if
not
_selected_unit
or
not
is_instance_valid
(
_selected_unit
):
_clear_selected_unit
()
_clear_selected_unit
()
return
return
var
ray_result
:
Dictionary
=
_raycast_from_mouse
(
mouse_pos
)
_is_waiting_for_tile_move_destination
=
true
if
ray_result
.
is_empty
()
or
not
ray_result
.
has
(
"position"
):
_hide_unit_action_buttons
()
_show_placement_tile_overlays
()
## 타일 이동 모드를 종료하고 표시 중인 타일 오버레이를 풀로 반환합니다.
## 선택이 유지되는 경우에는 액션 버튼을 다시 띄울 수 있도록 별도 show 호출은 호출자가 결정합니다.
func
_cancel_tile_move_mode
()
->
void
:
_is_waiting_for_tile_move_destination
=
false
_clear_placement_tile_overlays
()
## 타일 이동 모드에서 클릭한 화면 좌표를 GridMap 셀로 변환하고, 배치 가능하면 선택 유닛을 그 타일로 이동시킵니다.
## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다.
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
return
var
hit_pos
:
Vector3
=
ray_result
[
"position"
]
var
tile_info
:
Dictionary
=
_get_tile_info_from_screen
(
screen_pos
)
if
_selected_unit
.
has_method
(
"move_to"
):
if
tile_info
.
is_empty
():
_selected_unit
.
call
(
"move_to"
,
hit_pos
)
return
_spawn_click_indicator
(
hit_pos
,
false
)
_is_waiting_for_move_destination
=
false
var
tile_cell
:
Vector3i
=
tile_info
[
"cell"
]
if
_is_tile_occupied
(
tile_cell
,
_selected_unit
):
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
()
_show_unit_action_buttons
()
## 업그레이드 버튼 처리입니다.
## 업그레이드 버튼 처리입니다.
...
@@ -399,4 +441,272 @@ func _on_upgrade_selected_unit_pressed() -> void:
...
@@ -399,4 +441,272 @@ 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
src/entities/units/base/base_unit.gd
View file @
d8eba5c2
...
@@ -12,6 +12,9 @@ const INVALID_PLACEMENT_CELL: Vector3i = Vector3i(2147483647, 2147483647, 214748
...
@@ -12,6 +12,9 @@ const INVALID_PLACEMENT_CELL: Vector3i = Vector3i(2147483647, 2147483647, 214748
var
is_selected
:
bool
=
false
var
is_selected
:
bool
=
false
var
is_tile_bound
:
bool
=
false
var
is_tile_bound
:
bool
=
false
var
occupied_tile_cell
:
Vector3i
=
INVALID_PLACEMENT_CELL
var
occupied_tile_cell
:
Vector3i
=
INVALID_PLACEMENT_CELL
var
_has_pending_tile_rebind
:
bool
=
false
var
_pending_tile_center
:
Vector3
=
Vector3
.
ZERO
var
_pending_tile_cell
:
Vector3i
=
INVALID_PLACEMENT_CELL
# 전투 관련 상태
# 전투 관련 상태
var
_target_monster
:
Node3D
=
null
var
_target_monster
:
Node3D
=
null
...
@@ -95,6 +98,8 @@ func _physics_process(delta: float) -> void:
...
@@ -95,6 +98,8 @@ func _physics_process(delta: float) -> void:
if
_is_moving
and
_has_reached_manual_move_target
():
if
_is_moving
and
_has_reached_manual_move_target
():
_is_moving
=
false
_is_moving
=
false
if
_has_pending_tile_rebind
:
_complete_pending_tile_rebind
()
# 2. 적 자동 탐색 (타겟이 없고 수동 강제 이동 방지 쿨다운이 완료, 공격 쿨다운이 끝난 경우)
# 2. 적 자동 탐색 (타겟이 없고 수동 강제 이동 방지 쿨다운이 완료, 공격 쿨다운이 끝난 경우)
if
not
_is_moving
and
not
_target_monster
and
_auto_target_cooldown
<=
0.0
and
_attack_lock_timer
<=
0.0
:
if
not
_is_moving
and
not
_target_monster
and
_auto_target_cooldown
<=
0.0
and
_attack_lock_timer
<=
0.0
:
...
@@ -336,6 +341,9 @@ func hold_position() -> void:
...
@@ -336,6 +341,9 @@ func hold_position() -> void:
func
place_on_tile
(
tile_center
:
Vector3
,
tile_cell
:
Vector3i
)
->
void
:
func
place_on_tile
(
tile_center
:
Vector3
,
tile_cell
:
Vector3i
)
->
void
:
is_tile_bound
=
true
is_tile_bound
=
true
occupied_tile_cell
=
tile_cell
occupied_tile_cell
=
tile_cell
_has_pending_tile_rebind
=
false
_pending_tile_center
=
Vector3
.
ZERO
_pending_tile_cell
=
INVALID_PLACEMENT_CELL
_is_holding
=
true
_is_holding
=
true
_is_moving
=
false
_is_moving
=
false
_target_monster
=
null
_target_monster
=
null
...
@@ -348,6 +356,17 @@ func place_on_tile(tile_center: Vector3, tile_cell: Vector3i) -> void:
...
@@ -348,6 +356,17 @@ func place_on_tile(tile_center: Vector3, tile_cell: Vector3i) -> void:
_nav_agent
.
target_position
=
tile_center
_nav_agent
.
target_position
=
tile_center
velocity
=
Vector3
.
ZERO
velocity
=
Vector3
.
ZERO
## 타일에 고정된 유닛을 다른 배치 가능 타일로 이동시킵니다.
## 일반 move_to()는 타일 고정 유닛을 움직이지 않게 막으므로, 유닛 재배치 버튼 전용 경로를 따로 둡니다.
## 이동 중에는 현재 타일 점유를 해제하고, 목적지에 도착하면 place_on_tile()을 다시 호출해 새 타일에 고정합니다.
func
move_to_tile
(
tile_center
:
Vector3
,
tile_cell
:
Vector3i
)
->
void
:
_has_pending_tile_rebind
=
true
_pending_tile_center
=
tile_center
_pending_tile_cell
=
tile_cell
is_tile_bound
=
false
occupied_tile_cell
=
INVALID_PLACEMENT_CELL
_apply_move_command
(
tile_center
)
## 외부에서 호출하는 수동 이동 명령
## 외부에서 호출하는 수동 이동 명령
func
move_to
(
target_pos
:
Vector3
)
->
void
:
func
move_to
(
target_pos
:
Vector3
)
->
void
:
if
is_tile_bound
:
if
is_tile_bound
:
...
@@ -382,6 +401,15 @@ func _apply_move_command(target_pos: Vector3) -> void:
...
@@ -382,6 +401,15 @@ func _apply_move_command(target_pos: Vector3) -> void:
_auto_target_cooldown
=
0.5
# 수동 강제 이동 시 0.5초 동안 자동 공격 타겟 지정을 비활성화하여 피신을 허용
_auto_target_cooldown
=
0.5
# 수동 강제 이동 시 0.5초 동안 자동 공격 타겟 지정을 비활성화하여 피신을 허용
_nav_agent
.
target_position
=
target_pos
_nav_agent
.
target_position
=
target_pos
## move_to_tile()로 시작한 재배치 이동이 끝났을 때 새 타일에 유닛을 고정합니다.
## NavigationAgent 도착 판정은 약간의 허용 거리를 갖기 때문에, 마지막에는 타일 중심으로 스냅해 배치 그리드와 정확히 맞춥니다.
func
_complete_pending_tile_rebind
()
->
void
:
if
_pending_tile_cell
==
INVALID_PLACEMENT_CELL
:
_has_pending_tile_rebind
=
false
return
place_on_tile
(
_pending_tile_center
,
_pending_tile_cell
)
## 근접 공격의 타격 확정 전 구간인지 확인합니다.
## 근접 공격의 타격 확정 전 구간인지 확인합니다.
## - 원거리 유닛은 공격 모션 타이머가 있어도 이동 가능해야 하므로 false를 반환합니다.
## - 원거리 유닛은 공격 모션 타이머가 있어도 이동 가능해야 하므로 false를 반환합니다.
func
_is_melee_attack_locked
()
->
bool
:
func
_is_melee_attack_locked
()
->
bool
:
...
...
src/ui/unit_placement/hud_unit_placement.gd
View file @
d8eba5c2
...
@@ -12,6 +12,11 @@ const MAGE_SCENE: PackedScene = preload("res://src/entities/units/mage/mage_unit
...
@@ -12,6 +12,11 @@ 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
## MonsterPath와 가까운 타일을 "몬스터 길"로 판정할 때 추가로 더해 주는 여유 폭입니다.
## 실제 판정 거리는 `타일 반폭 + 타일 크기 * 이 비율`입니다.
## 예를 들어 GridMap 셀 크기가 2m이고 값이 0.15라면, 타일 중심이 Path3D에서 1.3m 이내일 때 길 타일로 취급합니다.
## Path3D가 타일 정중앙을 지나지 않거나 모델/그리드가 조금 어긋나도 몬스터 길에 유닛이 배치되지 않게 막는 안전 마진입니다.
const
MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO
:
float
=
0.15
const
MONSTER_PATH_TILE_EXTRA_MARGIN_RATIO
:
float
=
0.15
var
_hud
:
Control
=
null
var
_hud
:
Control
=
null
...
@@ -276,7 +281,6 @@ func _get_tile_info_from_screen(screen_pos: Vector2) -> Dictionary:
...
@@ -276,7 +281,6 @@ func _get_tile_info_from_screen(screen_pos: Vector2) -> Dictionary:
return
{}
return
{}
var
tile_center
:
Vector3
=
grid_map
.
to_global
(
grid_map
.
map_to_local
(
tile_cell
))
var
tile_center
:
Vector3
=
grid_map
.
to_global
(
grid_map
.
map_to_local
(
tile_cell
))
tile_center
.
y
+=
1.0
return
{
return
{
"cell"
:
tile_cell
,
"cell"
:
tile_cell
,
"center"
:
tile_center
"center"
:
tile_center
...
@@ -406,7 +410,8 @@ func _is_cell_close_to_monster_path(grid_map: GridMap, monster_path: Path3D, til
...
@@ -406,7 +410,8 @@ func _is_cell_close_to_monster_path(grid_map: GridMap, monster_path: Path3D, til
var
closest_world_point
:
Vector3
=
monster_path
.
to_global
(
closest_local_point
)
var
closest_world_point
:
Vector3
=
monster_path
.
to_global
(
closest_local_point
)
# 배치 판정은 바닥 평면 기준이므로 y 높이 차이는 무시합니다.
# 배치 판정은 바닥 평면 기준이므로 y 높이 차이는 무시합니다.
# half_tile_width에 작은 여유를 더해 경로가 타일 중앙에서 조금 벗어나 있어도 해당 길 타일이 잠기도록 합니다.
# half_tile_width는 타일 중심에서 타일 가장자리까지의 거리이고,
# path_margin은 MonsterPath가 타일 중앙선에서 조금 벗어난 경우까지 길 타일로 묶기 위한 추가 여유입니다.
tile_center
.
y
=
0.0
tile_center
.
y
=
0.0
closest_world_point
.
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
half_tile_width
:
float
=
maxf
(
grid_map
.
cell_size
.
x
,
grid_map
.
cell_size
.
z
)
*
0.5
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment