Commit 3410532f authored by Gavin An's avatar Gavin An

근접유닛 애니매이션 관련 수정

parent 9939b856
......@@ -32,4 +32,4 @@
에이전트는 아래의 명령을 수행하여 프로젝트를 관리할 수 있습니다.
- `run game`: `godot --main-pack project.pck` 또는 에디터 실행을 통해 게임을 시작합니다.
- `lint`: GDScript 내장 linter를 실행하여 코드 오류를 점검합니다.
- `clean`: `.godot/` 임시 캐시 폴더를 비웁니다.
\ No newline at end of file
- `clean`: `.godot/` 임시 캐시 폴더를 비웁니다.
매 10 라운드마다 보스 출현
......@@ -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
......
......@@ -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"]
......
......@@ -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 # 지형이나 다른 장막 뒤에 가려지도록 설정
......
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)
# 근접 유닛: 공격 애니메이션 중간 즈음(락아웃 시간의 50% 지점)에 데미지 지연 적용
var target_ref: Node3D = _target_monster
var damage_amount: int = unit_data.damage
var damage_delay: float = _attack_lock_timer * 0.5
# 쿨타임 타이머 작동
_attack_timer = unit_data.cooldown
# 공격 비주얼 연출
_show_attack_visual()
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"
_model_anim_player.stop()
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)
......@@ -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
......
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.")
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