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
3410532f
Commit
3410532f
authored
Jun 19, 2026
by
Gavin An
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
근접유닛 애니매이션 관련 수정
parent
9939b856
Changes
9
Show whitespace changes
Inline
Side-by-side
Showing
9 changed files
with
279 additions
and
185 deletions
+279
-185
docs/agents.md
docs/agents.md
+1
-1
docs/idea/design.md
docs/idea/design.md
+1
-0
src/autoload/data_manager.gd
src/autoload/data_manager.gd
+1
-1
src/core/main.tscn
src/core/main.tscn
+1
-1
src/entities/monsters/base_monster.gd
src/entities/monsters/base_monster.gd
+2
-2
src/entities/units/base_unit.gd
src/entities/units/base_unit.gd
+140
-179
src/entities/units/unit_data.gd
src/entities/units/unit_data.gd
+0
-1
src/entities/units/unit_helper.gd
src/entities/units/unit_helper.gd
+132
-0
src/entities/units/unit_helper.gd.uid
src/entities/units/unit_helper.gd.uid
+1
-0
No files found.
docs/agents.md
View file @
3410532f
docs/idea/design.md
0 → 100644
View file @
3410532f
매 10 라운드마다 보스 출현
src/autoload/data_manager.gd
View file @
3410532f
...
...
@@ -15,7 +15,7 @@ func _initialize_unit_templates() -> void:
striker
.
unit_id
=
"Unit_Striker"
striker
.
unit_name
=
"Striker"
striker
.
damage
=
12.0
striker
.
cooldown
=
0
.5
striker
.
cooldown
=
1
.5
striker
.
move_speed
=
6.0
striker
.
attack_range
=
1.5
striker
.
is_ranged
=
false
...
...
src/core/main.tscn
View file @
3410532f
...
...
@@ -2,7 +2,7 @@
[ext_resource type="Script" uid="uid://dd1xgi6hnpuvs" path="res://src/core/rts_camera.gd" id="1_rts_cam"]
[ext_resource type="Script" uid="uid://cq7r51whh6po" path="res://src/core/stage_manager.gd" id="2_stage_mgr"]
[ext_resource type="PackedScene" path="res://src/entities/monsters/noble_man/noble_man_unit.tscn" id="3_base_monster"]
[ext_resource type="PackedScene"
uid="uid://dimlohck6ebg7"
path="res://src/entities/monsters/noble_man/noble_man_unit.tscn" id="3_base_monster"]
[ext_resource type="Script" uid="uid://dkplkt6q8e3b5" path="res://src/core/unit_controller.gd" id="4_unit_ctrl"]
[ext_resource type="PackedScene" uid="uid://bqga1rvcjyydy" path="res://src/ui/hud.tscn" id="5_hud"]
...
...
src/entities/monsters/base_monster.gd
View file @
3410532f
...
...
@@ -4,7 +4,7 @@ extends PathFollow3D
## 기본 몬스터 클래스 (Path3D 상하위에서 경로를 따라 회전하며 피격/사망 처리됨)
# 몬스터 능력치 설정
@
export
var
speed
:
float
=
6
.0
@
export
var
speed
:
float
=
2
.0
var
max_hp
:
float
=
100.0
var
current_hp
:
float
=
100.0
var
gold_reward
:
int
=
15
...
...
@@ -158,7 +158,7 @@ func _create_hp_bar() -> void:
# 3. Sprite3D 생성 (Viewport의 2D 렌더링 텍스처를 3D 공간에 띄움)
var
sprite
:
Sprite3D
=
Sprite3D
.
new
()
sprite
.
billboard
=
BaseMaterial3D
.
BILLBOARD_ENABLED
# 카메라를 항상 마주하도록 설정
sprite
.
position
=
Vector3
(
0.0
,
1.2
,
0.0
)
# 몬스터 머리 위
오프셋
sprite
.
position
=
Vector3
(
0.0
,
0.15
,
0.0
)
# 몬스터 발밑
오프셋
sprite
.
texture
=
viewport
.
get_texture
()
sprite
.
cast_shadow
=
GeometryInstance3D
.
SHADOW_CASTING_SETTING_OFF
# 그림자 연산 제외
sprite
.
no_depth_test
=
false
# 지형이나 다른 장막 뒤에 가려지도록 설정
...
...
src/entities/units/base_unit.gd
View file @
3410532f
class_name
BaseUnit
extends
CharacterBody3D
const
UnitHelper
=
preload
(
"res://src/entities/units/unit_helper.gd"
)
## 플레이어 유닛 기본 클래스 (RTS 식 이동, 몬스터 자동 감지 및 공격 수행)
@
export
var
unit_data
:
UnitData
...
...
@@ -28,6 +30,8 @@ var _is_holding: bool = false
var
_model_instance
:
Node3D
=
null
var
_model_anim_player
:
AnimationPlayer
=
null
var
_last_anim_state
:
String
=
""
var
_is_currently_attacking
:
bool
=
false
var
_attack_lock_timer
:
float
=
0.0
# 공격 모션 완주 및 물리 이동 차단을 위한 타이머
func
_ready
()
->
void
:
# 유닛 그룹에 등록
...
...
@@ -60,13 +64,13 @@ func _ready() -> void:
_model_instance
=
detected_model_node
if
detected_model_name
==
"striker"
:
_apply_striker_skin
(
_model_instance
)
_attach_weapon
(
)
UnitHelper
.
apply_skin
(
_model_instance
,
"res://assets/models/units/striker_0.png"
)
UnitHelper
.
attach_weapon
(
_model_instance
,
unit_data
)
elif
detected_model_name
==
"guardian"
:
_apply_guardian_skin
(
_model_instance
)
_attach_weapon
(
)
UnitHelper
.
apply_skin
(
_model_instance
,
"res://assets/models/units/guardian/guardian_0.png"
)
UnitHelper
.
attach_weapon
(
_model_instance
,
unit_data
)
_model_anim_player
=
_
find_animation_player
(
_model_instance
)
_model_anim_player
=
UnitHelper
.
find_animation_player
(
_model_instance
)
if
_model_anim_player
:
print
(
"BaseUnit: Linked pre-configured model & AnimationPlayer: "
,
detected_model_name
)
...
...
@@ -77,6 +81,7 @@ func _ready() -> void:
if
lib
:
_model_anim_player
.
add_animation_library
(
detected_model_name
,
lib
)
print
(
"BaseUnit: Loaded and registered "
,
detected_model_name
,
" animations library successfully."
)
UnitHelper
.
clean_root_animation_tracks
(
_model_anim_player
)
...
...
@@ -88,6 +93,8 @@ func _physics_process(delta: float) -> void:
# 공격 타이머 및 자동 타겟팅 쿨타임 차감
if
_attack_timer
>
0.0
:
_attack_timer
-=
delta
if
_attack_lock_timer
>
0.0
:
_attack_lock_timer
-=
delta
if
_auto_target_cooldown
>
0.0
:
_auto_target_cooldown
-=
delta
...
...
@@ -95,10 +102,13 @@ func _physics_process(delta: float) -> void:
if
_target_monster
and
(
not
is_instance_valid
(
_target_monster
)
or
_target_monster
.
get
(
"is_dead"
)):
_target_monster
=
null
# 홀드 상태인데 타겟이 공격
사거리를
벗어나면 타겟팅 해제하여 추격 방지
# 홀드 상태인데 타겟이 공격
유지 한계선(chase_threshold)을
벗어나면 타겟팅 해제하여 추격 방지
if
_is_holding
and
_target_monster
:
var
dist
:
float
=
global_position
.
distance_to
(
_target_monster
.
global_position
)
if
dist
>
unit_data
.
attack_range
:
var
attack_range
:
float
=
unit_data
.
attack_range
var
buffer
:
float
=
0.5
if
not
unit_data
.
is_ranged
else
maxf
(
0.8
,
attack_range
*
0.1
)
var
chase_threshold
:
float
=
attack_range
+
buffer
if
dist
>
chase_threshold
:
_target_monster
=
null
# 2. 적 자동 탐색 (타겟이 없고 수동 강제 이동 방지 쿨다운이 완료된 경우)
...
...
@@ -114,31 +124,57 @@ func _physics_process(delta: float) -> void:
# 4. 3D 애니메이션 재생 업데이트
_update_animations
()
## 1. 공격 타겟이 존재할 때의 동작 로직
## 1. 공격 타겟이 존재할 때의 동작 로직
(진동 버벅임 방지 Hysteresis 적용 및 공격 중 잠금 추가)
func
_process_attack_logic
(
_delta
:
float
)
->
void
:
if
not
_target_monster
:
_is_currently_attacking
=
false
_attack_lock_timer
=
0.0
return
# 공격 모션 재생 중(공격 잠금 시간 동안)에는 무조건 이동하지 않고 멈춰서 공격 모션 유지
if
_attack_lock_timer
>
0.0
:
_is_currently_attacking
=
true
velocity
=
Vector3
.
ZERO
# 타겟 몬스터를 바라보도록 회전
_look_at_target
(
_target_monster
.
global_position
)
return
var
distance_to_target
:
float
=
global_position
.
distance_to
(
_target_monster
.
global_position
)
# 사거리 밖인 경우 추격 이동
if
distance_to_target
>
unit_data
.
attack_range
:
_nav_agent
.
target_position
=
_target_monster
.
global_position
_navigate_to_target
(
_delta
)
# 사거리 이내인 경우 정지 및 공격 수행
# 완충 사거리 마진 (Hysteresis): 이동-정지-이동 반복으로 틱틱 끊기는 버벅임 방지
var
attack_range
:
float
=
unit_data
.
attack_range
# 근접은 0.5m, 원거리는 사거리의 10%(최소 0.8m) 버퍼를 줌
var
buffer
:
float
=
0.5
if
not
unit_data
.
is_ranged
else
maxf
(
0.8
,
attack_range
*
0.1
)
var
chase_threshold
:
float
=
attack_range
+
buffer
# 공격 개시/유지 조건 판정
var
should_attack
:
bool
=
false
if
_is_currently_attacking
:
# 이미 때리고 있다면 마진 한계선(chase_threshold)을 넘어가기 전까지 계속 제자리 타격
should_attack
=
(
distance_to_target
<=
chase_threshold
)
else
:
# 추격 중이라면 원래의 사거리(attack_range) 이내로 들어와야 공격 모드 진입
should_attack
=
(
distance_to_target
<=
attack_range
)
if
should_attack
:
_is_currently_attacking
=
true
velocity
=
Vector3
.
ZERO
# 타겟 몬스터를 바라보도록 회전
_look_at_target
(
_target_monster
.
global_position
)
_try_attack
()
else
:
_is_currently_attacking
=
false
# 사거리 밖인 경우 추격 이동
_nav_agent
.
target_position
=
_target_monster
.
global_position
_navigate_to_target
(
_delta
)
## 2. 일반 대기 및 이동 상태의 로직
func
_process_normal_logic
(
_delta
:
float
)
->
void
:
# 이동 명령을 수행 중인 경우
if
not
_nav_agent
.
is_navigation_finished
():
_navigate_to_target
(
_delta
)
else
:
# 홀드 상태이거나 이동 명령이 끝난 경우 정지
if
_is_holding
or
_nav_agent
.
is_navigation_finished
():
velocity
=
Vector3
.
ZERO
else
:
_navigate_to_target
(
_delta
)
## 네비게이션 타겟 방향으로 캐릭터 이동
func
_navigate_to_target
(
_delta
:
float
)
->
void
:
...
...
@@ -191,8 +227,38 @@ func _try_attack() -> void:
if
_attack_timer
>
0.0
or
not
_target_monster
:
return
# 쿨타임 타이머 작동
_attack_timer
=
unit_data
.
cooldown
# 공격 비주얼 연출
_show_attack_visual
()
# 공격 애니메이션 재생 길이에 맞춘 이동 제어 락 타이머 설정
var
anim_length
:
float
=
0.5
# 기본값
var
play_speed
:
float
=
1.5
if
_model_instance
and
_model_anim_player
:
var
model_name
:
String
=
_model_instance
.
name
.
to_lower
()
var
library_prefix
:
String
=
model_name
+
"/"
var
attack_anim
:
String
=
library_prefix
+
"attack"
if
_model_anim_player
.
has_animation
(
attack_anim
):
anim_length
=
_model_anim_player
.
get_animation
(
attack_anim
)
.
length
elif
_model_anim_player
.
has_animation
(
"Armature|Armature|Basic_Jump|baselayer"
):
anim_length
=
_model_anim_player
.
get_animation
(
"Armature|Armature|Basic_Jump|baselayer"
)
.
length
if
unit_data
.
cooldown
<
0.6
:
play_speed
=
clampf
(
0.6
/
unit_data
.
cooldown
,
1.0
,
3.0
)
# 공격 락 시간 계산 (애니메이션 실재생 속도 반영)
_attack_lock_timer
=
anim_length
/
play_speed
# 락 타이머가 공격 쿨다운보다 길어지는 것은 방지
if
_attack_lock_timer
>
unit_data
.
cooldown
:
_attack_lock_timer
=
unit_data
.
cooldown
# 데미지 적용 및 발사체 발사 처리 분기
if
unit_data
.
is_ranged
:
# 원거리 유닛: 유도 발사체(Projectile) 생성하여 발사
# 원거리 유닛:
즉발로
유도 발사체(Projectile) 생성하여 발사
var
projectile_script
=
preload
(
"res://src/entities/units/projectile.gd"
)
var
proj
:
Node3D
=
projectile_script
.
new
()
as
Node3D
proj
.
set
(
"damage"
,
unit_data
.
damage
)
...
...
@@ -203,15 +269,21 @@ func _try_attack() -> void:
proj
.
global_position
=
global_position
+
Vector3
(
0.0
,
0.7
,
0.0
)
get_tree
()
.
current_scene
.
add_child
(
proj
)
else
:
# 근접 유닛: 즉발 공격 및 피해 적용
if
_target_monster
.
has_method
(
"take_damage"
):
_target_monster
.
call
(
"take_damage"
,
unit_data
.
damage
)
# 쿨타임 타이머 작동
_attack_timer
=
unit_data
.
cooldown
# 공격 비주얼 연출
_show_attack_visual
()
# 근접 유닛: 공격 애니메이션 중간 즈음(락아웃 시간의 50% 지점)에 데미지 지연 적용
var
target_ref
:
Node3D
=
_target_monster
var
damage_amount
:
int
=
unit_data
.
damage
var
damage_delay
:
float
=
_attack_lock_timer
*
0.5
if
damage_delay
>
0.0
:
get_tree
()
.
create_timer
(
damage_delay
)
.
timeout
.
connect
(
func
()
->
void
:
if
is_instance_valid
(
target_ref
)
and
not
target_ref
.
get
(
"is_dead"
):
if
target_ref
.
has_method
(
"take_damage"
):
target_ref
.
call
(
"take_damage"
,
damage_amount
)
)
else
:
if
target_ref
.
has_method
(
"take_damage"
):
target_ref
.
call
(
"take_damage"
,
damage_amount
)
## 공격 관련 비주얼 이펙트 연출
func
_show_attack_visual
()
->
void
:
...
...
@@ -232,6 +304,8 @@ func hold_position() -> void:
func
move_to
(
target_pos
:
Vector3
)
->
void
:
_is_holding
=
false
# 수동 조작 시 홀드 해제
_target_monster
=
null
# 이동 명령 시 강제 공격 해제
_attack_lock_timer
=
0.0
# 공격 애니메이션 진행 중이더라도 강제 이동 명령 시 즉시 캔슬하고 이동 (스타크래프트 식 무빙 캔슬)
_is_currently_attacking
=
false
_auto_target_cooldown
=
0.5
# 수동 강제 이동 시 0.5초 동안 자동 공격 타겟 지정을 비활성화하여 피신을 허용
_nav_agent
.
target_position
=
target_pos
...
...
@@ -264,133 +338,7 @@ func deselect() -> void:
if
_selection_ring
:
_selection_ring
.
visible
=
false
## FBX 모델의 자식 메쉬에 striker_0.png 텍스처를 씌우는 재귀 함수
func
_apply_striker_skin
(
node
:
Node
)
->
void
:
if
node
is
MeshInstance3D
:
var
mesh_inst
:
MeshInstance3D
=
node
as
MeshInstance3D
var
texture
:
Texture2D
=
load
(
"res://assets/models/units/striker_0.png"
)
as
Texture2D
if
texture
:
var
mat
:
StandardMaterial3D
=
StandardMaterial3D
.
new
()
mat
.
albedo_texture
=
texture
mat
.
roughness
=
0.5
mesh_inst
.
material_override
=
mat
for
child
:
Node
in
node
.
get_children
():
_apply_striker_skin
(
child
)
## FBX 모델의 자식 메쉬에 guardian_0.png 텍스처를 씌우는 재귀 함수
func
_apply_guardian_skin
(
node
:
Node
)
->
void
:
if
node
is
MeshInstance3D
:
var
mesh_inst
:
MeshInstance3D
=
node
as
MeshInstance3D
var
texture
:
Texture2D
=
load
(
"res://assets/models/units/guardian/guardian_0.png"
)
as
Texture2D
if
texture
:
var
mat
:
StandardMaterial3D
=
StandardMaterial3D
.
new
()
mat
.
albedo_texture
=
texture
mat
.
roughness
=
0.5
mesh_inst
.
material_override
=
mat
for
child
:
Node
in
node
.
get_children
():
_apply_guardian_skin
(
child
)
## 모델 자식 노드 중 AnimationPlayer 탐색
func
_find_animation_player
(
node
:
Node
)
->
AnimationPlayer
:
if
node
is
AnimationPlayer
:
return
node
as
AnimationPlayer
for
child
:
Node
in
node
.
get_children
():
var
res
:
AnimationPlayer
=
_find_animation_player
(
child
)
if
res
:
return
res
return
null
## 유닛의 오른손에 지정된 무기 모델을 런타임 장착
func
_attach_weapon
()
->
void
:
if
not
_model_instance
or
not
unit_data
:
return
# 스켈레톤 노드 탐색
var
skeleton
:
Skeleton3D
=
_find_skeleton
(
_model_instance
)
if
not
skeleton
:
return
# 오른손 본("RightHand")이 존재하는지 확인
var
bone_idx
:
int
=
skeleton
.
find_bone
(
"RightHand"
)
if
bone_idx
==
-
1
:
print
(
"BaseUnit: RightHand bone not found in skeleton."
)
return
# 이미 장착된 무기가 있는지 확인하여 제거 (중복 장착 방지)
var
existing_attachment
:
Node
=
skeleton
.
get_node_or_null
(
"RightHandAttachment"
)
if
existing_attachment
:
existing_attachment
.
queue_free
()
# 1. BoneAttachment3D 노드 동적 생성 및 본 동기화 설정
var
bone_attachment
:
BoneAttachment3D
=
BoneAttachment3D
.
new
()
bone_attachment
.
name
=
"RightHandAttachment"
bone_attachment
.
bone_name
=
"RightHand"
skeleton
.
add_child
(
bone_attachment
)
# 2. 유닛 종류에 따른 무기 설정 분기
var
weapon_scene_path
:
String
=
""
var
weapon_scale
:
Vector3
=
Vector3
.
ONE
var
weapon_rotation
:
Vector3
=
Vector3
.
ZERO
var
weapon_position
:
Vector3
=
Vector3
.
ZERO
var
texture_path
:
String
=
""
if
unit_data
.
unit_id
==
"Unit_Guardian"
:
# 가디언 창 (azure_spear)
weapon_scene_path
=
"res://assets/models/items/weapons/azure_spear.fbx"
texture_path
=
"res://assets/models/items/weapons/azure_spear_0.png"
# 가디언 창 피팅 보정 오프셋 (테스트 결과 기반 스케일 및 위치/회전 보정)
#weapon_scale = Vector3(0.8, 0.8, 0.8)
#weapon_rotation = Vector3(30.0, 95.0, 165.0)
#weapon_position = Vector3(0.0000, 0.1000, -0.8000)
else
:
# 기본값: Striker 검 (azure_blade)
weapon_scene_path
=
"res://assets/models/items/weapons/azure_blade.fbx"
texture_path
=
"res://assets/models/items/weapons/azure_blade_0.png"
#weapon_scale = Vector3(0.8, 0.7, 0.7)
#weapon_rotation = Vector3(30.0, 95.0, 165.0)
#weapon_position = Vector3(0.0000, 0.2000, -0.6000)
# 3. 무기 로드 및 인스턴스화
if
ResourceLoader
.
exists
(
weapon_scene_path
):
var
weapon_scene
:
PackedScene
=
load
(
weapon_scene_path
)
as
PackedScene
if
weapon_scene
:
var
weapon_inst
:
Node3D
=
weapon_scene
.
instantiate
()
as
Node3D
weapon_inst
.
scale
=
weapon_scale
weapon_inst
.
rotation_degrees
=
weapon_rotation
weapon_inst
.
position
=
weapon_position
# 무기 텍스처 매핑
_apply_weapon_skin
(
weapon_inst
,
texture_path
)
bone_attachment
.
add_child
(
weapon_inst
)
print
(
"BaseUnit: Attached weapon successfully. Unit: "
,
unit_data
.
unit_name
,
", Weapon: "
,
weapon_scene_path
.
get_file
())
## 무기 메쉬에 텍스처를 씌우는 재귀 함수
func
_apply_weapon_skin
(
node
:
Node
,
texture_path
:
String
)
->
void
:
if
node
is
MeshInstance3D
:
var
mesh_inst
:
MeshInstance3D
=
node
as
MeshInstance3D
if
texture_path
!=
""
and
ResourceLoader
.
exists
(
texture_path
):
var
texture
:
Texture2D
=
load
(
texture_path
)
as
Texture2D
if
texture
:
var
mat
:
StandardMaterial3D
=
StandardMaterial3D
.
new
()
mat
.
albedo_texture
=
texture
mat
.
roughness
=
0.4
mesh_inst
.
material_override
=
mat
for
child
:
Node
in
node
.
get_children
():
_apply_weapon_skin
(
child
,
texture_path
)
## 헬퍼: 자식 노드 중 Skeleton3D 탐색
func
_find_skeleton
(
node
:
Node
)
->
Skeleton3D
:
if
node
is
Skeleton3D
:
return
node
as
Skeleton3D
for
child
:
Node
in
node
.
get_children
():
var
res
:
Skeleton3D
=
_find_skeleton
(
child
)
if
res
:
return
res
return
null
## 상태(대기, 이동, 공격)별 3D 애니메이션 트랙 제어
## 상태(대기, 이동, 공격)별 3D 애니메이션 트랙 제어 및 크로스페이드 블렌딩
func
_update_animations
()
->
void
:
if
not
_model_anim_player
:
return
...
...
@@ -402,37 +350,50 @@ func _update_animations() -> void:
var
library_prefix
:
String
=
model_name
+
"/"
var
has_custom_anims
:
bool
=
_model_anim_player
.
has_animation
(
library_prefix
+
"idle"
)
var
target_anim
:
String
=
""
var
play_speed
:
float
=
1.0
var
blend_time
:
float
=
0.2
# 뚝뚝 끊기는 현상을 없애기 위한 0.2초 부드러운 크로스페이드
if
has_custom_anims
:
# 1. 공격 중 상태: 공격 타이머 차감 후 쿨타임 구간의 초입 0.4초간은 공격 애니메이션 우선 재생
if
_attack_timer
>
(
unit_data
.
cooldown
-
0.4
):
if
_last_anim_state
!=
"attack"
:
_last_anim_state
=
"attack"
_model_anim_player
.
play
(
library_prefix
+
"attack"
,
-
1
,
1.5
)
# 2. 이동 중 상태: 속도 벡터가 존재할 때 (이동 상태)
var
attack_anim
:
String
=
library_prefix
+
"attack"
var
run_anim
:
String
=
library_prefix
+
"run"
var
idle_anim
:
String
=
library_prefix
+
"idle"
# 1. 공격 락이 활성화되어 있는 동안에는 공격 애니메이션 강제 재생
if
_attack_lock_timer
>
0.0
:
var
base_speed
:
float
=
1.5
if
unit_data
.
cooldown
<
0.6
:
base_speed
=
0.6
/
unit_data
.
cooldown
play_speed
=
clampf
(
base_speed
,
1.0
,
3.0
)
target_anim
=
attack_anim
# 2. 이동 중 상태: 속도가 있을 때
elif
velocity
.
length
()
>
0.1
:
if
_last_anim_state
!=
"move"
:
_last_anim_state
=
"move"
_model_anim_player
.
play
(
library_prefix
+
"run"
,
-
1
,
1.2
)
# 3. 대기(Idle) 상태: 정지 상태일 때 대기 애니메이션 재생
target_anim
=
run_anim
play_speed
=
1.2
# 3. 대기 상태: 정지 상태 및 쿨다운 중 대기할 때
else
:
if
_last_anim_state
!=
"idle"
:
_last_anim_state
=
"idle"
_model_anim_player
.
play
(
library_prefix
+
"idle"
,
-
1
,
1.0
)
target_anim
=
idle_anim
play_speed
=
1.0
# 애니메이션 전환 및 크로스페이드 블렌딩 실행
if
target_anim
!=
""
and
_model_anim_player
.
current_animation
!=
target_anim
:
_last_anim_state
=
target_anim
.
replace
(
library_prefix
,
""
)
_model_anim_player
.
play
(
target_anim
,
blend_time
,
play_speed
)
else
:
# Fallback: 기존 Jump 애니메이션 처리
var
anim_name
:
String
=
"Armature|Armature|Basic_Jump|baselayer"
if
not
_model_anim_player
.
has_animation
(
anim_name
):
# Fallback: 기존
캡슐
Jump 애니메이션 처리
var
fallback_anim
:
String
=
"Armature|Armature|Basic_Jump|baselayer"
if
not
_model_anim_player
.
has_animation
(
fallback_anim
):
return
if
_attack_timer
>
(
unit_data
.
cooldown
-
0.4
):
if
_last_anim_state
!=
"attack"
:
_last_anim_state
=
"attack"
_model_anim_player
.
play
(
anim_name
,
-
1
,
2.5
)
if
_attack_lock_timer
>
0.0
:
target_anim
=
fallback_anim
play_speed
=
2.5
elif
velocity
.
length
()
>
0.1
:
if
_last_anim_state
!=
"move"
:
_last_anim_state
=
"move"
_model_anim_player
.
play
(
anim_name
,
-
1
,
1.5
)
target_anim
=
fallback_anim
play_speed
=
1.5
else
:
if
_last_anim_state
!=
"idle"
:
_last_anim_state
=
"idle"
target_anim
=
""
_model_anim_player
.
stop
()
if
target_anim
!=
""
and
_model_anim_player
.
current_animation
!=
target_anim
:
_model_anim_player
.
play
(
target_anim
,
blend_time
,
play_speed
)
src/entities/units/unit_data.gd
View file @
3410532f
...
...
@@ -2,7 +2,6 @@ class_name UnitData
extends
Resource
## 유닛의 기획 능력치를 정의하는 커스텀 리소스 (UnitData)
@
export
var
unit_id
:
String
=
""
@
export
var
unit_name
:
String
=
""
@
export
var
damage
:
float
=
10.0
...
...
src/entities/units/unit_helper.gd
0 → 100644
View file @
3410532f
class_name
UnitHelper
extends
Object
## 유닛 외형 렌더링 및 3D 장착 어태치먼트 제어를 위한 정적 유틸리티 헬퍼
## 자식 메쉬에 지정된 텍스처를 씌우는 재귀 함수
static
func
apply_skin
(
node
:
Node
,
texture_path
:
String
)
->
void
:
if
not
ResourceLoader
.
exists
(
texture_path
):
return
var
texture
:
Texture2D
=
load
(
texture_path
)
as
Texture2D
if
not
texture
:
return
_apply_skin_recursive
(
node
,
texture
)
static
func
_apply_skin_recursive
(
node
:
Node
,
texture
:
Texture2D
)
->
void
:
if
node
is
MeshInstance3D
:
var
mesh_inst
:
MeshInstance3D
=
node
as
MeshInstance3D
var
mat
:
StandardMaterial3D
=
StandardMaterial3D
.
new
()
mat
.
albedo_texture
=
texture
mat
.
roughness
=
0.5
mesh_inst
.
material_override
=
mat
for
child
:
Node
in
node
.
get_children
():
_apply_skin_recursive
(
child
,
texture
)
## 자식 노드 중 AnimationPlayer 탐색
static
func
find_animation_player
(
node
:
Node
)
->
AnimationPlayer
:
if
node
is
AnimationPlayer
:
return
node
as
AnimationPlayer
for
child
:
Node
in
node
.
get_children
():
var
res
:
AnimationPlayer
=
find_animation_player
(
child
)
if
res
:
return
res
return
null
## 자식 노드 중 Skeleton3D 탐색
static
func
find_skeleton
(
node
:
Node
)
->
Skeleton3D
:
if
node
is
Skeleton3D
:
return
node
as
Skeleton3D
for
child
:
Node
in
node
.
get_children
():
var
res
:
Skeleton3D
=
find_skeleton
(
child
)
if
res
:
return
res
return
null
## 유닛의 오른손에 지정된 무기 모델을 런타임 장착
static
func
attach_weapon
(
model_instance
:
Node3D
,
unit_data
:
UnitData
)
->
void
:
if
not
model_instance
or
not
unit_data
:
return
# 스켈레톤 노드 탐색
var
skeleton
:
Skeleton3D
=
find_skeleton
(
model_instance
)
if
not
skeleton
:
return
# 오른손 본("RightHand")이 존재하는지 확인
var
bone_idx
:
int
=
skeleton
.
find_bone
(
"RightHand"
)
if
bone_idx
==
-
1
:
print
(
"UnitHelper: RightHand bone not found in skeleton."
)
return
# 이미 장착된 무기가 있는지 확인하여 제거 (중복 장착 방지)
var
existing_attachment
:
Node
=
skeleton
.
get_node_or_null
(
"RightHandAttachment"
)
if
existing_attachment
:
existing_attachment
.
queue_free
()
# 1. BoneAttachment3D 노드 동적 생성 및 본 동기화 설정
var
bone_attachment
:
BoneAttachment3D
=
BoneAttachment3D
.
new
()
bone_attachment
.
name
=
"RightHandAttachment"
bone_attachment
.
bone_name
=
"RightHand"
skeleton
.
add_child
(
bone_attachment
)
# 2. 유닛 종류에 따른 무기 설정 분기
var
weapon_scene_path
:
String
=
""
var
weapon_scale
:
Vector3
=
Vector3
.
ONE
var
weapon_rotation
:
Vector3
=
Vector3
.
ZERO
var
weapon_position
:
Vector3
=
Vector3
.
ZERO
var
texture_path
:
String
=
""
if
unit_data
.
unit_id
==
"Unit_Guardian"
:
# 가디언 창 (azure_spear)
weapon_scene_path
=
"res://assets/models/items/weapons/azure_spear.fbx"
texture_path
=
"res://assets/models/items/weapons/azure_spear_0.png"
else
:
# 기본값: Striker 검 (azure_blade)
weapon_scene_path
=
"res://assets/models/items/weapons/azure_blade.fbx"
texture_path
=
"res://assets/models/items/weapons/azure_blade_0.png"
# 3. 무기 로드 및 인스턴스화
if
ResourceLoader
.
exists
(
weapon_scene_path
):
var
weapon_scene
:
PackedScene
=
load
(
weapon_scene_path
)
as
PackedScene
if
weapon_scene
:
var
weapon_inst
:
Node3D
=
weapon_scene
.
instantiate
()
as
Node3D
weapon_inst
.
scale
=
weapon_scale
weapon_inst
.
rotation_degrees
=
weapon_rotation
weapon_inst
.
position
=
weapon_position
# 무기 텍스처 매핑
apply_skin
(
weapon_inst
,
texture_path
)
bone_attachment
.
add_child
(
weapon_inst
)
print
(
"UnitHelper: Attached weapon successfully. Unit: "
,
unit_data
.
unit_name
,
", Weapon: "
,
weapon_scene_path
.
get_file
())
## 애니메이션 내에서 모델의 로컬 정면 방향을 강제 리셋하는 루트(Armature) 트랙 정화
static
func
clean_root_animation_tracks
(
anim_player
:
AnimationPlayer
)
->
void
:
if
not
anim_player
:
return
# AnimationPlayer에 바인딩된 모든 애니메이션 라이브러리의 트랙 정화
for
library_name
:
StringName
in
anim_player
.
get_animation_library_list
():
var
lib
:
AnimationLibrary
=
anim_player
.
get_animation_library
(
library_name
)
if
not
lib
:
continue
for
anim_name
:
StringName
in
lib
.
get_animation_list
():
var
anim
:
Animation
=
lib
.
get_animation
(
anim_name
)
if
anim
:
var
tracks_to_remove
:
Array
[
int
]
=
[]
for
track_idx
:
int
in
range
(
anim
.
get_track_count
()):
var
path_str
:
String
=
str
(
anim
.
track_get_path
(
track_idx
))
# 최상위 루트 노드(Armature 등)의 회전/위치 강제 덮어쓰기 트랙 검출
# Skeleton3D 하위의 실제 뼈대 애니메이션 트랙은 유지하고 오직 Armature 루트 트랜스폼 트랙만 제거
if
path_str
==
"Armature:rotation"
or
path_str
==
"Armature:position"
or
\
path_str
.
begins_with
(
"Armature:"
)
and
not
"Skeleton3D"
in
path_str
:
tracks_to_remove
.
append
(
track_idx
)
# 역순으로 정렬하여 트랙 제거 시 인덱스 밀림 방지
tracks_to_remove
.
reverse
()
for
track_idx
:
int
in
tracks_to_remove
:
anim
.
remove_track
(
track_idx
)
print
(
"UnitHelper: Cleaned root and Armature transform tracks to preserve character body rotation."
)
src/entities/units/unit_helper.gd.uid
0 → 100644
View file @
3410532f
uid://dxqa2rlcobrie
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