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:
2026-06-22 22:02:30 -04:00
parent c033247f75
commit a2956635c0
2 changed files with 73 additions and 23 deletions
+62 -19
View File
@@ -15,7 +15,6 @@ func _ready() -> void:
load_model(model_path)
func load_model(path: String) -> void:
# Clear existing children
for child in get_children():
child.queue_free()
@@ -32,36 +31,82 @@ func load_model(path: String) -> void:
add_child(scene)
# Debug: print the full scene tree
print("SkinnedPlayerModel: scene tree after instantiation:")
print("SkinnedPlayerModel: scene tree:")
_print_tree(scene, 0)
# Find animation player — search entire tree including the scene root itself
animation_player = find_animation_player(self)
if not animation_player:
animation_player = find_animation_player(scene)
# Find skeleton
skeleton = find_skeleton(self)
if not skeleton:
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:
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:
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
if animation_player.has_animation("Idle"):
animation_player.play("Idle")
print("SkinnedPlayerModel: playing 'Idle'")
elif anim_list.size() > 0:
animation_player.play(anim_list[0])
print("SkinnedPlayerModel: playing first anim: %s" % anim_list[0])
print("SkinnedPlayerModel: playing '%s'" % anim_list[0])
else:
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found in scene tree")
print("SkinnedPlayerModel: ERROR - no AnimationPlayer available")
func _print_tree(node: Node, depth: int) -> void:
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():
_print_tree(child, depth + 1)
@@ -74,11 +119,11 @@ func find_skeleton(node: Node) -> Skeleton3D:
return result
return null
func find_animation_player(node: Node) -> AnimationPlayer:
func find_animation_player_recursive(node: Node) -> AnimationPlayer:
if node is AnimationPlayer:
return node
for child in node.get_children():
var result = find_animation_player(child)
var result = find_animation_player_recursive(child)
if result:
return result
return null
@@ -93,28 +138,26 @@ func _process(delta: float) -> void:
if not animation_player:
return
# Debug: print animation state occasionally
# Debug output every 2 seconds
_anim_debug_timer += delta
if _anim_debug_timer > 2.0:
_anim_debug_timer = 0.0
if animation_player.is_playing():
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
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")
if not sm:
return
var state = sm.current_state
# Calculate speed from player's horizontal velocity
var speed = 0.0
if sm.player:
var vel = sm.player.velocity
speed = Vector2(vel.x, vel.z).length()
# Only change animation if it's different from current
var target_anim = "Idle"
match state:
"ground", "idle":
+11 -4
View File
@@ -748,11 +748,18 @@ func _process(_delta: float) -> void:
if not _player or not is_instance_valid(_player):
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:
var target_pos = _player.global_position + Vector3(0, 2.0, -4.0)
_third_person_camera.global_position = _third_person_camera.global_position.lerp(target_pos, 0.1)
_third_person_camera.look_at(_player.global_position + Vector3(0, 1.0, 0))
# Get the player's forward direction (on XZ plane)
var forward = -_player.global_transform.basis.z
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:
_debug_ui_panel.visible = SettingsManager.show_debug_ui