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
48b5d744
Commit
48b5d744
authored
Jul 20, 2026
by
Gavin An
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
조이스틱 구현
parent
b553c088
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
275 additions
and
0 deletions
+275
-0
src/ui/hero_panel/hud_hero_panel.gd
src/ui/hero_panel/hud_hero_panel.gd
+15
-0
src/ui/virtual_joystick/virtual_joystick.gd
src/ui/virtual_joystick/virtual_joystick.gd
+246
-0
src/ui/virtual_joystick/virtual_joystick.gd.uid
src/ui/virtual_joystick/virtual_joystick.gd.uid
+1
-0
src/ui/virtual_joystick/virtual_joystick.tscn
src/ui/virtual_joystick/virtual_joystick.tscn
+13
-0
No files found.
src/ui/hero_panel/hud_hero_panel.gd
View file @
48b5d744
...
@@ -8,6 +8,7 @@ extends RefCounted
...
@@ -8,6 +8,7 @@ extends RefCounted
const
HERO_SPAWN_COST
:
int
=
500
const
HERO_SPAWN_COST
:
int
=
500
const
DWARF_HERO_SCENE
:
PackedScene
=
preload
(
"res://src/entities/units/hero/dwarf/dwarf_hero.tscn"
)
const
DWARF_HERO_SCENE
:
PackedScene
=
preload
(
"res://src/entities/units/hero/dwarf/dwarf_hero.tscn"
)
const
DWARF_PORTRAIT_PATH
:
String
=
"res://assets/ui/hero/dwarf_portrait.jpg"
const
DWARF_PORTRAIT_PATH
:
String
=
"res://assets/ui/hero/dwarf_portrait.jpg"
const
VIRTUAL_JOYSTICK_SCENE
:
PackedScene
=
preload
(
"res://src/ui/virtual_joystick/virtual_joystick.tscn"
)
var
_hud
:
Control
=
null
var
_hud
:
Control
=
null
var
_placement_manager
:
Object
=
null
var
_placement_manager
:
Object
=
null
...
@@ -15,6 +16,7 @@ var _placement_manager: Object = null
...
@@ -15,6 +16,7 @@ var _placement_manager: Object = null
var
hero_spawn_button
:
Button
=
null
var
hero_spawn_button
:
Button
=
null
var
hero_panel
:
Panel
=
null
var
hero_panel
:
Panel
=
null
var
_active_hero
:
BaseHero
=
null
var
_active_hero
:
BaseHero
=
null
var
_virtual_joystick
:
Control
=
null
var
_skill_cooldown_overlays
:
Dictionary
=
{}
var
_skill_cooldown_overlays
:
Dictionary
=
{}
var
_skill_cooldown_labels
:
Dictionary
=
{}
var
_skill_cooldown_labels
:
Dictionary
=
{}
...
@@ -73,6 +75,7 @@ func _on_summon_hero_pressed() -> void:
...
@@ -73,6 +75,7 @@ func _on_summon_hero_pressed() -> void:
_active_hero
=
new_hero
_active_hero
=
new_hero
_setup_hero_ui
(
new_hero
)
_setup_hero_ui
(
new_hero
)
_setup_virtual_joystick
(
new_hero
)
## 일반 유닛 배치 대기 상태인지 placement_manager에 위임해 확인합니다.
## 일반 유닛 배치 대기 상태인지 placement_manager에 위임해 확인합니다.
func
_has_pending_unit_placement
()
->
bool
:
func
_has_pending_unit_placement
()
->
bool
:
...
@@ -80,6 +83,18 @@ func _has_pending_unit_placement() -> bool:
...
@@ -80,6 +83,18 @@ func _has_pending_unit_placement() -> bool:
return
_placement_manager
.
call
(
"has_pending_unit_placement"
)
return
_placement_manager
.
call
(
"has_pending_unit_placement"
)
return
false
return
false
## 영웅 생성 후 화면 하단의 가상 조이스틱 UI를 생성하거나 기존 UI에 새 영웅을 연결합니다.
## virtual_joystick.tscn은 HUD의 자식으로 붙어 화면 전체를 기준으로 좌/우 하단 컨트롤을 배치합니다.
func
_setup_virtual_joystick
(
hero
:
BaseHero
)
->
void
:
if
not
_virtual_joystick
or
not
is_instance_valid
(
_virtual_joystick
):
_virtual_joystick
=
VIRTUAL_JOYSTICK_SCENE
.
instantiate
()
as
Control
_virtual_joystick
.
name
=
"HeroVirtualJoystick"
_hud
.
add_child
(
_virtual_joystick
)
_virtual_joystick
.
move_to_front
()
if
_virtual_joystick
and
_virtual_joystick
.
has_method
(
"setup"
):
_virtual_joystick
.
call
(
"setup"
,
hero
)
## 영웅 전용 좌측 상단 HUD를 새로 구성합니다.
## 영웅 전용 좌측 상단 HUD를 새로 구성합니다.
## 기존 패널이 있으면 제거한 뒤, 새 영웅 인스턴스의 신호와 연결된 UI를 생성합니다.
## 기존 패널이 있으면 제거한 뒤, 새 영웅 인스턴스의 신호와 연결된 UI를 생성합니다.
func
_setup_hero_ui
(
hero
:
BaseHero
)
->
void
:
func
_setup_hero_ui
(
hero
:
BaseHero
)
->
void
:
...
...
src/ui/virtual_joystick/virtual_joystick.gd
0 → 100644
View file @
48b5d744
extends
Control
## 영웅 전용 가상 조이스틱 UI입니다.
## - 왼쪽 하단: 반투명 이동 조이스틱
## - 오른쪽 하단: 공격 버튼과 Q/W/E/R 스킬 버튼
## - HudHeroPanel에서 영웅이 생성될 때 setup(hero)를 호출해 실제 영웅 인스턴스와 연결합니다.
const
JOYSTICK_RADIUS
:
float
=
54.0
const
JOYSTICK_DEADZONE
:
float
=
0.16
const
MOVE_TARGET_DISTANCE
:
float
=
3.0
const
MOVE_COMMAND_INTERVAL
:
float
=
0.08
var
_hero
:
Node3D
=
null
var
_move_vector
:
Vector2
=
Vector2
.
ZERO
var
_is_dragging_move_stick
:
bool
=
false
var
_move_command_timer
:
float
=
0.0
var
_move_area
:
Control
=
null
var
_move_base
:
Panel
=
null
var
_move_knob
:
Panel
=
null
var
_attack_button
:
Button
=
null
var
_skill_buttons
:
Dictionary
=
{}
## HudHeroPanel에서 호출하는 초기화 함수입니다.
## 영웅 인스턴스가 바뀌어도 같은 UI 노드를 재사용할 수 있도록 참조만 교체합니다.
func
setup
(
hero
:
Node3D
)
->
void
:
_hero
=
hero
visible
=
is_instance_valid
(
_hero
)
set_process
(
visible
)
func
_ready
()
->
void
:
_build_runtime_controls
()
visible
=
false
set_process
(
false
)
func
_process
(
delta
:
float
)
->
void
:
if
not
is_instance_valid
(
_hero
)
or
_is_game_over
():
visible
=
false
set_process
(
false
)
return
_move_command_timer
-=
delta
if
_move_vector
.
length
()
>
JOYSTICK_DEADZONE
and
_move_command_timer
<=
0.0
:
_move_command_timer
=
MOVE_COMMAND_INTERVAL
_apply_hero_move_vector
()
## GameManager AutoLoad를 안전하게 찾아 게임 종료 여부를 반환합니다.
## ResourceSaver 같은 도구 실행 환경에서는 AutoLoad 전역 이름이 없을 수 있어 직접 식별자 참조를 피합니다.
func
_is_game_over
()
->
bool
:
var
game_manager
:
Node
=
get_node_or_null
(
"/root/GameManager"
)
if
not
game_manager
:
return
false
return
bool
(
game_manager
.
get
(
"is_game_over"
))
## 반투명 조이스틱과 버튼들을 런타임에 구성합니다.
## tscn 원본은 Root Control + Script만 저장하고, 세부 UI는 한곳의 코드에서 일관되게 생성합니다.
func
_build_runtime_controls
()
->
void
:
mouse_filter
=
Control
.
MOUSE_FILTER_IGNORE
anchors_preset
=
Control
.
PRESET_FULL_RECT
anchor_right
=
1.0
anchor_bottom
=
1.0
_build_move_joystick
()
_build_action_buttons
()
## 왼쪽 하단 이동 조이스틱을 생성합니다.
## move_area가 입력을 직접 받아 base/knob 위치를 갱신하고, 계산된 방향을 _move_vector에 저장합니다.
func
_build_move_joystick
()
->
void
:
_move_area
=
Control
.
new
()
_move_area
.
name
=
"MoveJoystickArea"
_move_area
.
anchor_left
=
0.0
_move_area
.
anchor_top
=
1.0
_move_area
.
anchor_right
=
0.0
_move_area
.
anchor_bottom
=
1.0
_move_area
.
offset_left
=
28.0
_move_area
.
offset_top
=
-
168.0
_move_area
.
offset_right
=
168.0
_move_area
.
offset_bottom
=
-
28.0
_move_area
.
mouse_filter
=
Control
.
MOUSE_FILTER_STOP
_move_area
.
gui_input
.
connect
(
_on_move_area_gui_input
)
add_child
(
_move_area
)
_move_base
=
Panel
.
new
()
_move_base
.
name
=
"MoveBase"
_move_base
.
anchor_left
=
0.5
_move_base
.
anchor_top
=
0.5
_move_base
.
anchor_right
=
0.5
_move_base
.
anchor_bottom
=
0.5
_move_base
.
offset_left
=
-
JOYSTICK_RADIUS
_move_base
.
offset_top
=
-
JOYSTICK_RADIUS
_move_base
.
offset_right
=
JOYSTICK_RADIUS
_move_base
.
offset_bottom
=
JOYSTICK_RADIUS
_move_base
.
mouse_filter
=
Control
.
MOUSE_FILTER_IGNORE
_move_base
.
add_theme_stylebox_override
(
"panel"
,
_create_circle_style
(
Color
(
0.35
,
0.65
,
0.95
,
0.24
),
Color
(
0.75
,
0.9
,
1.0
,
0.45
),
54
))
_move_area
.
add_child
(
_move_base
)
_move_knob
=
Panel
.
new
()
_move_knob
.
name
=
"MoveKnob"
_move_knob
.
anchor_left
=
0.5
_move_knob
.
anchor_top
=
0.5
_move_knob
.
anchor_right
=
0.5
_move_knob
.
anchor_bottom
=
0.5
_move_knob
.
offset_left
=
-
24.0
_move_knob
.
offset_top
=
-
24.0
_move_knob
.
offset_right
=
24.0
_move_knob
.
offset_bottom
=
24.0
_move_knob
.
mouse_filter
=
Control
.
MOUSE_FILTER_IGNORE
_move_knob
.
add_theme_stylebox_override
(
"panel"
,
_create_circle_style
(
Color
(
0.85
,
0.95
,
1.0
,
0.42
),
Color
(
1.0
,
1.0
,
1.0
,
0.65
),
24
))
_move_area
.
add_child
(
_move_knob
)
## 오른쪽 하단 공격/스킬 버튼 묶음을 생성합니다.
func
_build_action_buttons
()
->
void
:
var
action_root
:
Control
=
Control
.
new
()
action_root
.
name
=
"ActionButtonArea"
action_root
.
anchor_left
=
1.0
action_root
.
anchor_top
=
1.0
action_root
.
anchor_right
=
1.0
action_root
.
anchor_bottom
=
1.0
action_root
.
offset_left
=
-
260.0
action_root
.
offset_top
=
-
178.0
action_root
.
offset_right
=
-
24.0
action_root
.
offset_bottom
=
-
24.0
action_root
.
mouse_filter
=
Control
.
MOUSE_FILTER_IGNORE
add_child
(
action_root
)
_attack_button
=
_create_action_button
(
"AttackButton"
,
"ATK"
,
Vector2
(
156.0
,
76.0
),
Vector2
(
70.0
,
70.0
),
Color
(
0.85
,
0.18
,
0.12
,
0.58
))
_attack_button
.
pressed
.
connect
(
_on_attack_pressed
)
action_root
.
add_child
(
_attack_button
)
var
slots
:
Array
[
String
]
=
[
"Q"
,
"W"
,
"E"
,
"R"
]
var
positions
:
Dictionary
=
{
"Q"
:
Vector2
(
0.0
,
88.0
),
"W"
:
Vector2
(
54.0
,
46.0
),
"E"
:
Vector2
(
108.0
,
18.0
),
"R"
:
Vector2
(
164.0
,
0.0
)
}
for
slot
:
String
in
slots
:
var
slot_name
:
String
=
slot
var
button
:
Button
=
_create_action_button
(
"Skill"
+
slot
+
"Button"
,
slot
,
positions
[
slot
],
Vector2
(
46.0
,
46.0
),
Color
(
0.28
,
0.18
,
0.75
,
0.54
))
button
.
pressed
.
connect
(
func
()
->
void
:
_on_skill_pressed
(
slot_name
)
)
_skill_buttons
[
slot_name
]
=
button
action_root
.
add_child
(
button
)
## 이동 조이스틱 입력을 처리합니다.
## 터치/마우스 드래그 위치를 중심 기준 벡터로 변환하고 knob를 반지름 안에서 움직입니다.
func
_on_move_area_gui_input
(
event
:
InputEvent
)
->
void
:
if
event
is
InputEventMouseButton
:
var
mouse_event
:
InputEventMouseButton
=
event
as
InputEventMouseButton
if
mouse_event
.
button_index
==
MOUSE_BUTTON_LEFT
:
_is_dragging_move_stick
=
mouse_event
.
pressed
_update_move_stick
(
mouse_event
.
position
if
mouse_event
.
pressed
else
_move_area
.
size
*
0.5
)
if
not
mouse_event
.
pressed
:
_reset_move_stick
()
elif
event
is
InputEventMouseMotion
and
_is_dragging_move_stick
:
var
motion_event
:
InputEventMouseMotion
=
event
as
InputEventMouseMotion
_update_move_stick
(
motion_event
.
position
)
## 조이스틱의 현재 방향을 영웅 이동 명령으로 변환합니다.
## 기존 BaseUnit.move_to() 경로를 사용해 NavigationAgent 흐름과 충돌하지 않도록 합니다.
func
_apply_hero_move_vector
()
->
void
:
if
not
is_instance_valid
(
_hero
):
return
var
move_direction
:
Vector3
=
Vector3
(
_move_vector
.
x
,
0.0
,
_move_vector
.
y
)
var
target_position
:
Vector3
=
_hero
.
global_position
+
move_direction
*
MOVE_TARGET_DISTANCE
if
_hero
.
has_method
(
"move_to"
):
_hero
.
call
(
"move_to"
,
target_position
)
## 공격 버튼을 누르면 가장 가까운 살아있는 몬스터를 찾아 영웅의 공격 대상으로 지정합니다.
func
_on_attack_pressed
()
->
void
:
if
not
is_instance_valid
(
_hero
):
return
var
target
:
Node3D
=
_find_nearest_monster
()
if
target
and
_hero
.
has_method
(
"attack_target"
):
_hero
.
call
(
"attack_target"
,
target
)
## 스킬 버튼을 누르면 BaseHero.use_skill()에 그대로 위임합니다.
func
_on_skill_pressed
(
slot
:
String
)
->
void
:
if
is_instance_valid
(
_hero
)
and
_hero
.
has_method
(
"use_skill"
):
_hero
.
call
(
"use_skill"
,
slot
)
## move_area 내부 좌표를 조이스틱 방향 벡터로 갱신합니다.
func
_update_move_stick
(
local_position
:
Vector2
)
->
void
:
var
center
:
Vector2
=
_move_area
.
size
*
0.5
var
offset
:
Vector2
=
local_position
-
center
var
clamped_offset
:
Vector2
=
offset
.
limit_length
(
JOYSTICK_RADIUS
)
_move_vector
=
clamped_offset
/
JOYSTICK_RADIUS
_move_knob
.
position
=
center
+
clamped_offset
-
_move_knob
.
size
*
0.5
## 손을 떼면 조이스틱과 이동 벡터를 중앙으로 되돌립니다.
func
_reset_move_stick
()
->
void
:
_move_vector
=
Vector2
.
ZERO
_move_command_timer
=
0.0
if
_move_area
and
_move_knob
:
_move_knob
.
position
=
_move_area
.
size
*
0.5
-
_move_knob
.
size
*
0.5
## 현재 필드에서 영웅과 가장 가까운 살아있는 몬스터를 찾습니다.
func
_find_nearest_monster
()
->
Node3D
:
var
nearest_monster
:
Node3D
=
null
var
nearest_distance
:
float
=
INF
for
monster
:
Node
in
_hero
.
get_tree
()
.
get_nodes_in_group
(
"monsters"
):
if
not
is_instance_valid
(
monster
)
or
not
monster
is
Node3D
:
continue
if
monster
.
get
(
"is_dead"
)
==
true
:
continue
var
monster_3d
:
Node3D
=
monster
as
Node3D
var
distance
:
float
=
_hero
.
global_position
.
distance_to
(
monster_3d
.
global_position
)
if
distance
<
nearest_distance
:
nearest_distance
=
distance
nearest_monster
=
monster_3d
return
nearest_monster
## 반투명 원형 패널 스타일을 생성합니다.
func
_create_circle_style
(
fill_color
:
Color
,
border_color
:
Color
,
radius
:
int
)
->
StyleBoxFlat
:
var
style
:
StyleBoxFlat
=
StyleBoxFlat
.
new
()
style
.
bg_color
=
fill_color
style
.
border_color
=
border_color
style
.
border_width_left
=
2
style
.
border_width_top
=
2
style
.
border_width_right
=
2
style
.
border_width_bottom
=
2
style
.
set_corner_radius_all
(
radius
)
return
style
## 공격/스킬 버튼 공통 생성 함수입니다.
func
_create_action_button
(
button_name
:
String
,
text
:
String
,
position
:
Vector2
,
size
:
Vector2
,
color
:
Color
)
->
Button
:
var
button
:
Button
=
Button
.
new
()
button
.
name
=
button_name
button
.
text
=
text
button
.
position
=
position
button
.
custom_minimum_size
=
size
button
.
size
=
size
button
.
mouse_filter
=
Control
.
MOUSE_FILTER_STOP
button
.
add_theme_font_size_override
(
"font_size"
,
16
)
button
.
add_theme_stylebox_override
(
"normal"
,
_create_circle_style
(
color
,
Color
(
1.0
,
1.0
,
1.0
,
0.42
),
int
(
size
.
x
*
0.5
)))
button
.
add_theme_stylebox_override
(
"hover"
,
_create_circle_style
(
color
.
lightened
(
0.15
),
Color
(
1.0
,
1.0
,
1.0
,
0.58
),
int
(
size
.
x
*
0.5
)))
button
.
add_theme_stylebox_override
(
"pressed"
,
_create_circle_style
(
color
.
darkened
(
0.18
),
Color
(
1.0
,
1.0
,
1.0
,
0.72
),
int
(
size
.
x
*
0.5
)))
return
button
src/ui/virtual_joystick/virtual_joystick.gd.uid
0 → 100644
View file @
48b5d744
uid://ckc10hqcu852h
src/ui/virtual_joystick/virtual_joystick.tscn
0 → 100644
View file @
48b5d744
[gd_scene format=3 uid="uid://cnck8ouc8u72d"]
[ext_resource type="Script" uid="uid://ckc10hqcu852h" path="res://src/ui/virtual_joystick/virtual_joystick.gd" id="1_yf7xh"]
[node name="VirtualJoystick" type="Control" unique_id=1725233368]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("1_yf7xh")
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