fix: third-person camera orbits player facing + robust animation handling
Third-person camera: - Camera offset now follows player's forward direction using basis.z - Smoothly orbits behind player when they turn - Looks at player's head area (y+1.5) SkinnedPlayerModel: - Creates AnimationPlayer programmatically if GLB doesn't have one - Copies animations from armature's animation_data to the player - Handles both direct actions and NLA tracks - More detailed debug output for scene tree and animation state
This commit is contained in:
@@ -15,7 +15,6 @@ func _ready() -> void:
|
|||||||
load_model(model_path)
|
load_model(model_path)
|
||||||
|
|
||||||
func load_model(path: String) -> void:
|
func load_model(path: String) -> void:
|
||||||
# Clear existing children
|
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
child.queue_free()
|
child.queue_free()
|
||||||
|
|
||||||
@@ -32,36 +31,82 @@ func load_model(path: String) -> void:
|
|||||||
add_child(scene)
|
add_child(scene)
|
||||||
|
|
||||||
# Debug: print the full scene tree
|
# Debug: print the full scene tree
|
||||||
print("SkinnedPlayerModel: scene tree after instantiation:")
|
print("SkinnedPlayerModel: scene tree:")
|
||||||
_print_tree(scene, 0)
|
_print_tree(scene, 0)
|
||||||
|
|
||||||
# Find animation player — search entire tree including the scene root itself
|
# Find skeleton
|
||||||
animation_player = find_animation_player(self)
|
|
||||||
if not animation_player:
|
|
||||||
animation_player = find_animation_player(scene)
|
|
||||||
|
|
||||||
skeleton = find_skeleton(self)
|
skeleton = find_skeleton(self)
|
||||||
if not skeleton:
|
if not skeleton:
|
||||||
skeleton = find_skeleton(scene)
|
skeleton = find_skeleton(scene)
|
||||||
|
|
||||||
|
# Find existing AnimationPlayer in the GLB scene
|
||||||
|
animation_player = find_animation_player_recursive(scene)
|
||||||
|
|
||||||
|
# If no AnimationPlayer found, create one and add it to the armature
|
||||||
|
if not animation_player:
|
||||||
|
print("SkinnedPlayerModel: no AnimationPlayer in GLB, creating one...")
|
||||||
|
animation_player = AnimationPlayer.new()
|
||||||
|
animation_player.name = "AnimationPlayer"
|
||||||
|
|
||||||
|
# Add to the skeleton/armature node if found, otherwise to self
|
||||||
|
if skeleton:
|
||||||
|
skeleton.add_child(animation_player)
|
||||||
|
print("SkinnedPlayerModel: added AnimationPlayer to skeleton")
|
||||||
|
else:
|
||||||
|
add_child(animation_player)
|
||||||
|
print("SkinnedPlayerModel: added AnimationPlayer to self")
|
||||||
|
|
||||||
|
# Try to copy animations from the armature's animation_data
|
||||||
|
if skeleton and skeleton.animation_data:
|
||||||
|
var src_data = skeleton.animation_data
|
||||||
|
# Create animation library
|
||||||
|
var anim_lib = AnimationLibrary.new()
|
||||||
|
|
||||||
|
# Copy the action from the armature
|
||||||
|
if src_data.action:
|
||||||
|
var anim = src_data.action.copy()
|
||||||
|
anim_lib.add_animation(anim.name, anim)
|
||||||
|
print(f"SkinnedPlayerModel: copied animation '{anim.name}' ({anim.length:.2f}s)")
|
||||||
|
|
||||||
|
# Also check NLA tracks
|
||||||
|
if src_data.nla_tracks:
|
||||||
|
for track in src_data.nla_tracks:
|
||||||
|
for strip in track.strips:
|
||||||
|
if strip.action:
|
||||||
|
var anim = strip.action.copy()
|
||||||
|
anim_lib.add_animation(anim.name, anim)
|
||||||
|
print(f"SkinnedPlayerModel: copied NLA animation '{anim.name}'")
|
||||||
|
|
||||||
|
animation_player.add_animation_library("", anim_lib)
|
||||||
|
else:
|
||||||
|
print("SkinnedPlayerModel: WARNING - skeleton has no animation_data")
|
||||||
|
|
||||||
|
# List available animations
|
||||||
if animation_player:
|
if animation_player:
|
||||||
var anim_list = animation_player.get_animation_list()
|
var anim_list = animation_player.get_animation_list()
|
||||||
print("SkinnedPlayerModel: found AnimationPlayer with %d animations:" % anim_list.size())
|
print("SkinnedPlayerModel: %d animations available:" % anim_list.size())
|
||||||
for anim in anim_list:
|
for anim in anim_list:
|
||||||
print(" - %s (length: %.2f)" % [anim, animation_player.get_animation(anim).length])
|
var a = animation_player.get_animation(anim)
|
||||||
|
print(f" - {anim} ({a.length:.2f}s, loop={a.loop_mode})")
|
||||||
|
|
||||||
# Auto-play idle
|
# Auto-play idle
|
||||||
if animation_player.has_animation("Idle"):
|
if animation_player.has_animation("Idle"):
|
||||||
animation_player.play("Idle")
|
animation_player.play("Idle")
|
||||||
print("SkinnedPlayerModel: playing 'Idle'")
|
print("SkinnedPlayerModel: playing 'Idle'")
|
||||||
elif anim_list.size() > 0:
|
elif anim_list.size() > 0:
|
||||||
animation_player.play(anim_list[0])
|
animation_player.play(anim_list[0])
|
||||||
print("SkinnedPlayerModel: playing first anim: %s" % anim_list[0])
|
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found in scene tree")
|
print("SkinnedPlayerModel: ERROR - no AnimationPlayer available")
|
||||||
|
|
||||||
func _print_tree(node: Node, depth: int) -> void:
|
func _print_tree(node: Node, depth: int) -> void:
|
||||||
var indent = " ".repeat(depth)
|
var indent = " ".repeat(depth)
|
||||||
print("%s%s (%s)" % [indent, node.name, node.get_class()])
|
var extra = ""
|
||||||
|
if node is AnimationPlayer:
|
||||||
|
extra = " [ANIMATION PLAYER]"
|
||||||
|
elif node is Skeleton3D:
|
||||||
|
extra = " [SKELETON]"
|
||||||
|
print("%s%s (%s)%s" % [indent, node.name, node.get_class(), extra])
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
_print_tree(child, depth + 1)
|
_print_tree(child, depth + 1)
|
||||||
|
|
||||||
@@ -74,11 +119,11 @@ func find_skeleton(node: Node) -> Skeleton3D:
|
|||||||
return result
|
return result
|
||||||
return null
|
return null
|
||||||
|
|
||||||
func find_animation_player(node: Node) -> AnimationPlayer:
|
func find_animation_player_recursive(node: Node) -> AnimationPlayer:
|
||||||
if node is AnimationPlayer:
|
if node is AnimationPlayer:
|
||||||
return node
|
return node
|
||||||
for child in node.get_children():
|
for child in node.get_children():
|
||||||
var result = find_animation_player(child)
|
var result = find_animation_player_recursive(child)
|
||||||
if result:
|
if result:
|
||||||
return result
|
return result
|
||||||
return null
|
return null
|
||||||
@@ -93,28 +138,26 @@ func _process(delta: float) -> void:
|
|||||||
if not animation_player:
|
if not animation_player:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Debug: print animation state occasionally
|
# Debug output every 2 seconds
|
||||||
_anim_debug_timer += delta
|
_anim_debug_timer += delta
|
||||||
if _anim_debug_timer > 2.0:
|
if _anim_debug_timer > 2.0:
|
||||||
_anim_debug_timer = 0.0
|
_anim_debug_timer = 0.0
|
||||||
if animation_player.is_playing():
|
if animation_player.is_playing():
|
||||||
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: NOT playing any animation")
|
print("SkinnedPlayerModel: NOT playing")
|
||||||
|
|
||||||
# Get movement state from the state machine
|
# Get movement state
|
||||||
var sm = get_parent().get_node_or_null("MovementStateMachine")
|
var sm = get_parent().get_node_or_null("MovementStateMachine")
|
||||||
if not sm:
|
if not sm:
|
||||||
return
|
return
|
||||||
|
|
||||||
var state = sm.current_state
|
var state = sm.current_state
|
||||||
# Calculate speed from player's horizontal velocity
|
|
||||||
var speed = 0.0
|
var speed = 0.0
|
||||||
if sm.player:
|
if sm.player:
|
||||||
var vel = sm.player.velocity
|
var vel = sm.player.velocity
|
||||||
speed = Vector2(vel.x, vel.z).length()
|
speed = Vector2(vel.x, vel.z).length()
|
||||||
|
|
||||||
# Only change animation if it's different from current
|
|
||||||
var target_anim = "Idle"
|
var target_anim = "Idle"
|
||||||
match state:
|
match state:
|
||||||
"ground", "idle":
|
"ground", "idle":
|
||||||
|
|||||||
@@ -748,11 +748,18 @@ func _process(_delta: float) -> void:
|
|||||||
if not _player or not is_instance_valid(_player):
|
if not _player or not is_instance_valid(_player):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Third person camera follow
|
# Third person camera follow — orbit behind player's facing direction
|
||||||
if _third_person and _third_person_camera and _third_person_camera.current:
|
if _third_person and _third_person_camera and _third_person_camera.current:
|
||||||
var target_pos = _player.global_position + Vector3(0, 2.0, -4.0)
|
# Get the player's forward direction (on XZ plane)
|
||||||
_third_person_camera.global_position = _third_person_camera.global_position.lerp(target_pos, 0.1)
|
var forward = -_player.global_transform.basis.z
|
||||||
_third_person_camera.look_at(_player.global_position + Vector3(0, 1.0, 0))
|
forward.y = 0
|
||||||
|
forward = forward.normalized()
|
||||||
|
# Position camera behind and above player
|
||||||
|
var cam_offset = forward * -4.0 + Vector3.UP * 2.5
|
||||||
|
var target_pos = _player.global_position + cam_offset
|
||||||
|
_third_person_camera.global_position = _third_person_camera.global_position.lerp(target_pos, 0.12)
|
||||||
|
# Look at player's head area
|
||||||
|
_third_person_camera.look_at(_player.global_position + Vector3.UP * 1.5)
|
||||||
|
|
||||||
if _debug_ui_panel:
|
if _debug_ui_panel:
|
||||||
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
_debug_ui_panel.visible = SettingsManager.show_debug_ui
|
||||||
|
|||||||
Reference in New Issue
Block a user