Commit 48b5d744 authored by Gavin An's avatar Gavin An

조이스틱 구현

parent b553c088
...@@ -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:
......
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
[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")
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