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
312e1e41
Commit
312e1e41
authored
Jul 27, 2026
by
Gavin An
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
스크립트 정리
parent
33596b43
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
172 additions
and
382 deletions
+172
-382
assets/tile/README.md
assets/tile/README.md
+6
-0
assets/tile/desert/scenes/desert_mesh_library.tres
assets/tile/desert/scenes/desert_mesh_library.tres
+54
-49
src/core/unit_controller.gd
src/core/unit_controller.gd
+48
-327
src/map/stages/stage_1_map.tscn
src/map/stages/stage_1_map.tscn
+1
-1
src/ui/unit_placement/hud_unit_placement.gd
src/ui/unit_placement/hud_unit_placement.gd
+63
-5
No files found.
assets/tile/README.md
0 → 100644
View file @
312e1e41
## 3d MeshLibrary / GridMap 에는 커스텀 메타 데이터를 넣을 수 없음.
## 따라서, 스크립트를 통해 타일마다 메타데이터를 넣어줘야 함.
## 쓰이는 메타데이터가 있으면 여기다가 넣어야 함.
*
'unit_place_blocked': 해당 타일에 유닛을 설치못하는가
\ No newline at end of file
assets/tile/desert/scenes/desert_mesh_library.tres
View file @
312e1e41
This source diff could not be displayed because it is too large. You can
view the blob
instead.
src/core/unit_controller.gd
View file @
312e1e41
...
@@ -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,21 +21,17 @@ var _action_button_container: HBoxContainer = null
...
@@ -28,21 +21,17 @@ 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 구현 보완)
var
nav_region
:
NavigationRegion3D
=
get_node_or_null
(
"../NavigationRegion3D"
)
as
NavigationRegion3D
var
nav_region
:
NavigationRegion3D
=
get_node_or_null
(
"../NavigationRegion3D"
)
as
NavigationRegion3D
if
nav_region
:
if
nav_region
:
...
@@ -54,20 +43,20 @@ func _process(_delta: float) -> void:
...
@@ -54,20 +43,20 @@ func _process(_delta: float) -> void:
func
_unhandled_input
(
event
:
InputEvent
)
->
void
:
func
_unhandled_input
(
event
:
InputEvent
)
->
void
:
if
GameManager
.
is_game_over
:
if
GameManager
.
is_game_over
:
return
return
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
if
not
camera
:
if
not
camera
:
return
return
if
event
is
InputEventMouseButton
:
if
event
is
InputEventMouseButton
:
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_tile_move_destination
:
if
_is_waiting_for_tile_move_destination
:
_try_move_selected_unit_to_tile
(
mouse_event
.
position
)
_try_move_selected_unit_to_tile
(
mouse_event
.
position
)
else
:
else
:
_perform_selection
(
mouse_event
.
position
)
_perform_selection
(
mouse_event
.
position
)
# 마우스 오른쪽 버튼 클릭 (이동 및 공격 지시)
# 마우스 오른쪽 버튼 클릭 (이동 및 공격 지시)
elif
mouse_event
.
button_index
==
MOUSE_BUTTON_RIGHT
and
mouse_event
.
is_pressed
():
elif
mouse_event
.
button_index
==
MOUSE_BUTTON_RIGHT
and
mouse_event
.
is_pressed
():
_perform_action
(
mouse_event
.
position
)
_perform_action
(
mouse_event
.
position
)
...
@@ -96,7 +85,7 @@ func _find_selectable_unit_from_mouse(mouse_pos: Vector2) -> Node3D:
...
@@ -96,7 +85,7 @@ func _find_selectable_unit_from_mouse(mouse_pos: Vector2) -> Node3D:
var
raycast_unit
:
Node
=
_get_unit_node
(
hit_collider
)
var
raycast_unit
:
Node
=
_get_unit_node
(
hit_collider
)
if
raycast_unit
and
raycast_unit
.
is_in_group
(
"units"
)
and
raycast_unit
is
Node3D
:
if
raycast_unit
and
raycast_unit
.
is_in_group
(
"units"
)
and
raycast_unit
is
Node3D
:
return
raycast_unit
as
Node3D
return
raycast_unit
as
Node3D
return
_find_nearest_unit_on_screen
(
mouse_pos
)
return
_find_nearest_unit_on_screen
(
mouse_pos
)
## 화면 좌표 기준으로 마우스 근처의 가장 가까운 유닛을 반환합니다.
## 화면 좌표 기준으로 마우스 근처의 가장 가까운 유닛을 반환합니다.
...
@@ -105,24 +94,24 @@ func _find_nearest_unit_on_screen(mouse_pos: Vector2) -> Node3D:
...
@@ -105,24 +94,24 @@ func _find_nearest_unit_on_screen(mouse_pos: Vector2) -> Node3D:
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
if
not
camera
:
if
not
camera
:
return
null
return
null
var
nearest_unit
:
Node3D
=
null
var
nearest_unit
:
Node3D
=
null
var
nearest_screen_distance
:
float
=
UNIT_SELECTION_SCREEN_RADIUS
var
nearest_screen_distance
:
float
=
UNIT_SELECTION_SCREEN_RADIUS
var
units
:
Array
[
Node
]
=
get_tree
()
.
get_nodes_in_group
(
"units"
)
var
units
:
Array
[
Node
]
=
get_tree
()
.
get_nodes_in_group
(
"units"
)
for
unit
:
Node
in
units
:
for
unit
:
Node
in
units
:
if
not
is_instance_valid
(
unit
)
or
not
unit
is
Node3D
:
if
not
is_instance_valid
(
unit
)
or
not
unit
is
Node3D
:
continue
continue
var
unit_3d
:
Node3D
=
unit
as
Node3D
var
unit_3d
:
Node3D
=
unit
as
Node3D
if
camera
.
is_position_behind
(
unit_3d
.
global_position
):
if
camera
.
is_position_behind
(
unit_3d
.
global_position
):
continue
continue
var
screen_pos
:
Vector2
=
camera
.
unproject_position
(
unit_3d
.
global_position
)
var
screen_pos
:
Vector2
=
camera
.
unproject_position
(
unit_3d
.
global_position
)
var
screen_distance
:
float
=
screen_pos
.
distance_to
(
mouse_pos
)
var
screen_distance
:
float
=
screen_pos
.
distance_to
(
mouse_pos
)
if
screen_distance
<=
nearest_screen_distance
:
if
screen_distance
<=
nearest_screen_distance
:
nearest_screen_distance
=
screen_distance
nearest_screen_distance
=
screen_distance
nearest_unit
=
unit_3d
nearest_unit
=
unit_3d
return
nearest_unit
return
nearest_unit
## 선택 유닛을 하나로 고정합니다.
## 선택 유닛을 하나로 고정합니다.
...
@@ -131,13 +120,13 @@ func _select_single_unit(unit_node: Node3D) -> void:
...
@@ -131,13 +120,13 @@ func _select_single_unit(unit_node: Node3D) -> void:
if
_selected_unit
==
unit_node
:
if
_selected_unit
==
unit_node
:
_show_unit_action_buttons
()
_show_unit_action_buttons
()
return
return
_clear_selected_unit
()
_clear_selected_unit
()
_selected_unit
=
unit_node
_selected_unit
=
unit_node
selected_units
.
clear
()
selected_units
.
clear
()
selected_units
.
append
(
unit_node
)
selected_units
.
append
(
unit_node
)
_cancel_tile_move_mode
()
_cancel_tile_move_mode
()
if
unit_node
.
has_method
(
"select"
):
if
unit_node
.
has_method
(
"select"
):
unit_node
.
call
(
"select"
)
unit_node
.
call
(
"select"
)
_show_unit_action_buttons
()
_show_unit_action_buttons
()
...
@@ -157,31 +146,31 @@ func _perform_action(mouse_pos: Vector2) -> void:
...
@@ -157,31 +146,31 @@ func _perform_action(mouse_pos: Vector2) -> void:
return
return
if
_is_waiting_for_tile_move_destination
:
if
_is_waiting_for_tile_move_destination
:
return
return
# Raycast를 쏘아 바닥 또는 몬스터 감지
# Raycast를 쏘아 바닥 또는 몬스터 감지
var
ray_result
:
Dictionary
=
_raycast_from_mouse
(
mouse_pos
)
var
ray_result
:
Dictionary
=
_raycast_from_mouse
(
mouse_pos
)
if
ray_result
.
is_empty
():
if
ray_result
.
is_empty
():
return
return
var
hit_pos
:
Vector3
=
ray_result
[
"position"
]
var
hit_pos
:
Vector3
=
ray_result
[
"position"
]
var
hit_collider
:
Node
=
ray_result
[
"collider"
]
as
Node
var
hit_collider
:
Node
=
ray_result
[
"collider"
]
as
Node
# 충돌 대상이 몬스터인지 검사
# 충돌 대상이 몬스터인지 검사
var
monster_node
:
Node
=
_get_monster_node
(
hit_collider
)
var
monster_node
:
Node
=
_get_monster_node
(
hit_collider
)
if
monster_node
and
monster_node
.
is_in_group
(
"monsters"
):
if
monster_node
and
monster_node
.
is_in_group
(
"monsters"
):
# 선택된 유닛에게 공격 타겟 명령 하달
# 선택된 유닛에게 공격 타겟 명령 하달
for
unit
:
Node
in
selected_units
:
for
unit
:
Node
in
selected_units
:
if
is_instance_valid
(
unit
)
and
unit
.
has_method
(
"attack_target"
):
if
is_instance_valid
(
unit
)
and
unit
.
has_method
(
"attack_target"
):
unit
.
call
(
"attack_target"
,
monster_node
)
unit
.
call
(
"attack_target"
,
monster_node
)
# 공격 지시 클릭 표시 (빨간색) 스폰
# 공격 지시 클릭 표시 (빨간색) 스폰
_spawn_click_indicator
(
monster_node
.
global_position
,
true
)
_spawn_click_indicator
(
monster_node
.
global_position
,
true
)
else
:
else
:
var
selected_unit
:
Node
=
selected_units
[
0
]
var
selected_unit
:
Node
=
selected_units
[
0
]
if
is_instance_valid
(
selected_unit
)
and
selected_unit
.
has_method
(
"move_to"
):
if
is_instance_valid
(
selected_unit
)
and
selected_unit
.
has_method
(
"move_to"
):
selected_unit
.
call
(
"move_to"
,
hit_pos
)
selected_unit
.
call
(
"move_to"
,
hit_pos
)
# 지면 이동 클릭 표시 (초록색) 스폰
# 지면 이동 클릭 표시 (초록색) 스폰
_spawn_click_indicator
(
hit_pos
,
false
)
_spawn_click_indicator
(
hit_pos
,
false
)
...
@@ -191,11 +180,11 @@ func _spawn_click_indicator(pos: Vector3, is_attack: bool) -> void:
...
@@ -191,11 +180,11 @@ func _spawn_click_indicator(pos: Vector3, is_attack: bool) -> void:
var
indicator
:
Node3D
=
click_indicator_scene
.
instantiate
()
as
Node3D
var
indicator
:
Node3D
=
click_indicator_scene
.
instantiate
()
as
Node3D
if
not
indicator
:
if
not
indicator
:
return
return
# 지형 위에 살짝 띄워 배치 (Y=0.1m로 뎁스 충돌 방지)
# 지형 위에 살짝 띄워 배치 (Y=0.1m로 뎁스 충돌 방지)
indicator
.
global_position
=
pos
+
Vector3
(
0.0
,
0.1
,
0.0
)
indicator
.
global_position
=
pos
+
Vector3
(
0.0
,
0.1
,
0.0
)
get_tree
()
.
current_scene
.
add_child
(
indicator
)
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
)
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"
):
if
indicator
.
has_method
(
"setup_indicator"
):
...
@@ -206,15 +195,15 @@ func _raycast_from_mouse(mouse_pos: Vector2) -> Dictionary:
...
@@ -206,15 +195,15 @@ func _raycast_from_mouse(mouse_pos: Vector2) -> Dictionary:
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
if
not
camera
:
if
not
camera
:
return
{}
return
{}
var
from
:
Vector3
=
camera
.
project_ray_origin
(
mouse_pos
)
var
from
:
Vector3
=
camera
.
project_ray_origin
(
mouse_pos
)
var
to
:
Vector3
=
from
+
camera
.
project_ray_normal
(
mouse_pos
)
*
1000.0
var
to
:
Vector3
=
from
+
camera
.
project_ray_normal
(
mouse_pos
)
*
1000.0
var
space_state
:
PhysicsDirectSpaceState3D
=
camera
.
get_world_3d
()
.
direct_space_state
var
space_state
:
PhysicsDirectSpaceState3D
=
camera
.
get_world_3d
()
.
direct_space_state
var
query
:
PhysicsRayQueryParameters3D
=
PhysicsRayQueryParameters3D
.
create
(
from
,
to
)
var
query
:
PhysicsRayQueryParameters3D
=
PhysicsRayQueryParameters3D
.
create
(
from
,
to
)
query
.
collide_with_areas
=
true
query
.
collide_with_areas
=
true
query
.
collide_with_bodies
=
true
query
.
collide_with_bodies
=
true
return
space_state
.
intersect_ray
(
query
)
return
space_state
.
intersect_ray
(
query
)
## 충돌체로부터 유닛 노드를 식별하여 반환
## 충돌체로부터 유닛 노드를 식별하여 반환
...
@@ -261,7 +250,7 @@ func _get_monster_node(collider: Node) -> Node:
...
@@ -261,7 +250,7 @@ func _get_monster_node(collider: Node) -> Node:
func
_perform_hold_position
()
->
void
:
func
_perform_hold_position
()
->
void
:
if
selected_units
.
is_empty
():
if
selected_units
.
is_empty
():
return
return
for
unit
:
Node
in
selected_units
:
for
unit
:
Node
in
selected_units
:
if
is_instance_valid
(
unit
)
and
unit
.
has_method
(
"hold_position"
):
if
is_instance_valid
(
unit
)
and
unit
.
has_method
(
"hold_position"
):
unit
.
call
(
"hold_position"
)
unit
.
call
(
"hold_position"
)
...
@@ -275,11 +264,11 @@ func _create_unit_action_buttons() -> void:
...
@@ -275,11 +264,11 @@ func _create_unit_action_buttons() -> void:
_action_button_container
.
mouse_filter
=
Control
.
MOUSE_FILTER_STOP
_action_button_container
.
mouse_filter
=
Control
.
MOUSE_FILTER_STOP
_action_button_container
.
add_theme_constant_override
(
"separation"
,
int
(
UNIT_ACTION_BUTTON_GAP
))
_action_button_container
.
add_theme_constant_override
(
"separation"
,
int
(
UNIT_ACTION_BUTTON_GAP
))
add_child
(
_action_button_container
)
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
))
_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
))
_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
))
_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
)
_sell_button
.
pressed
.
connect
(
_on_sell_selected_unit_pressed
)
_move_button
.
pressed
.
connect
(
_on_move_selected_unit_pressed
)
_move_button
.
pressed
.
connect
(
_on_move_selected_unit_pressed
)
_upgrade_button
.
pressed
.
connect
(
_on_upgrade_selected_unit_pressed
)
_upgrade_button
.
pressed
.
connect
(
_on_upgrade_selected_unit_pressed
)
...
@@ -339,17 +328,17 @@ func _update_unit_action_buttons_position() -> void:
...
@@ -339,17 +328,17 @@ func _update_unit_action_buttons_position() -> 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
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
var
camera
:
Camera3D
=
get_viewport
()
.
get_camera_3d
()
if
not
camera
:
if
not
camera
:
_hide_unit_action_buttons
()
_hide_unit_action_buttons
()
return
return
var
anchor_world_position
:
Vector3
=
_selected_unit
.
global_position
+
Vector3
(
0.0
,
UNIT_ACTION_BUTTON_WORLD_HEIGHT
,
0.0
)
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
):
if
camera
.
is_position_behind
(
anchor_world_position
):
_action_button_container
.
visible
=
false
_action_button_container
.
visible
=
false
return
return
var
screen_position
:
Vector2
=
camera
.
unproject_position
(
anchor_world_position
)
var
screen_position
:
Vector2
=
camera
.
unproject_position
(
anchor_world_position
)
var
container_size
:
Vector2
=
_action_button_container
.
get_combined_minimum_size
()
var
container_size
:
Vector2
=
_action_button_container
.
get_combined_minimum_size
()
_action_button_container
.
size
=
container_size
_action_button_container
.
size
=
container_size
...
@@ -361,11 +350,11 @@ func _on_sell_selected_unit_pressed() -> void:
...
@@ -361,11 +350,11 @@ func _on_sell_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
var
refund_amount
:
int
=
int
(
float
(
GameManager
.
unit_spawn_cost
)
*
UNIT_SELL_REFUND_RATIO
)
var
refund_amount
:
int
=
int
(
float
(
GameManager
.
unit_spawn_cost
)
*
UNIT_SELL_REFUND_RATIO
)
if
refund_amount
>
0
:
if
refund_amount
>
0
:
GameManager
.
add_gold
(
refund_amount
)
GameManager
.
add_gold
(
refund_amount
)
var
unit_to_sell
:
Node3D
=
_selected_unit
var
unit_to_sell
:
Node3D
=
_selected_unit
_clear_selected_unit
()
_clear_selected_unit
()
unit_to_sell
.
queue_free
()
unit_to_sell
.
queue_free
()
...
@@ -385,16 +374,17 @@ func _enter_tile_move_mode() -> void:
...
@@ -385,16 +374,17 @@ 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
_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 셀로 변환하고, 배치 가능하면 선택 유닛을 그 타일로 이동시킵니다.
## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다.
## 유닛 생성 배치와 같은 판정을 써서 몬스터 길/점유 타일에는 이동 명령이 들어가지 않습니다.
...
@@ -402,21 +392,21 @@ func _try_move_selected_unit_to_tile(screen_pos: Vector2) -> void:
...
@@ -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
):
if
not
_selected_unit
or
not
is_instance_valid
(
_selected_unit
):
_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"
]
if
_selected_unit
.
has_method
(
"move_to_tile"
):
if
_selected_unit
.
has_method
(
"move_to_tile"
):
_selected_unit
.
call
(
"move_to_tile"
,
tile_center
,
tile_cell
)
_selected_unit
.
call
(
"move_to_tile"
,
tile_center
,
tile_cell
)
elif
_selected_unit
.
has_method
(
"move_to"
):
elif
_selected_unit
.
has_method
(
"move_to"
):
_selected_unit
.
call
(
"move_to"
,
tile_center
)
_selected_unit
.
call
(
"move_to"
,
tile_center
)
_spawn_click_indicator
(
tile_center
,
false
)
_spawn_click_indicator
(
tile_center
,
false
)
_cancel_tile_move_mode
()
_cancel_tile_move_mode
()
_show_unit_action_buttons
()
_show_unit_action_buttons
()
...
@@ -429,284 +419,15 @@ func _on_upgrade_selected_unit_pressed() -> void:
...
@@ -429,284 +419,15 @@ func _on_upgrade_selected_unit_pressed() -> void:
return
return
if
not
_selected_unit
is
BaseUnit
:
if
not
_selected_unit
is
BaseUnit
:
return
return
var
selected_base_unit
:
BaseUnit
=
_selected_unit
as
BaseUnit
var
selected_base_unit
:
BaseUnit
=
_selected_unit
as
BaseUnit
if
not
selected_base_unit
.
unit_data
:
if
not
selected_base_unit
.
unit_data
:
return
return
if
not
GameManager
.
spend_gold
(
UNIT_UPGRADE_COST
):
if
not
GameManager
.
spend_gold
(
UNIT_UPGRADE_COST
):
return
return
selected_base_unit
.
unit_data
=
selected_base_unit
.
unit_data
.
duplicate
(
true
)
as
UnitData
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
.
damage
*=
1.18
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/map/stages/stage_1_map.tscn
View file @
312e1e41
[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")
...
...
src/ui/unit_placement/hud_unit_placement.gd
View file @
312e1e41
...
@@ -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
:
return
true
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"
)
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
...
...
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