Feat/14 movement overhaul #20
+169
-138
@@ -136,134 +136,23 @@ func load_model(path: String) -> void:
|
|||||||
_ensure_locomotion_animations(animation_player)
|
_ensure_locomotion_animations(animation_player)
|
||||||
else:
|
else:
|
||||||
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
print("SkinnedPlayerModel: WARNING - no AnimationPlayer found")
|
||||||
|
func _ensure_locomotion_animations(_ap: AnimationPlayer) -> void:
|
||||||
func _ensure_locomotion_animations(ap: AnimationPlayer) -> void:
|
## AnimationPlayer doesn't properly update bone transforms for runtime
|
||||||
## Creates proper Animation resources for Walk, Run, Jump, and Idle.
|
## GLB skeletons. Animation resources are kept for reference but the
|
||||||
## Replaces GLB animations that were stripped by Blender's GLTF exporter.
|
## actual animation is done code-driven in _process().
|
||||||
## Uses AnimationPlayer tracks (type=0 for position, type=1 for rotation).
|
pass
|
||||||
|
|
||||||
# Always replace all locomotion animations — GLB is known to be stripped
|
|
||||||
_create_and_play_anim(ap, "Walk", _create_walk_anim())
|
|
||||||
_create_and_play_anim(ap, "Run", _create_run_anim())
|
|
||||||
_create_and_play_anim(ap, "Jump", _create_jump_anim())
|
|
||||||
_create_and_play_anim(ap, "Idle", _create_idle_anim())
|
|
||||||
|
|
||||||
func _create_and_play_anim(ap: AnimationPlayer, anim_name: String, anim: Animation) -> void:
|
|
||||||
var lib = ap.get_animation_library("")
|
|
||||||
if lib.has_animation(anim_name):
|
|
||||||
lib.remove_animation(anim_name)
|
|
||||||
lib.add_animation(anim_name, anim)
|
|
||||||
print("SkinnedPlayerModel: replaced '%s' with %d tracks" % [anim_name, anim.get_track_count()])
|
|
||||||
|
|
||||||
func _create_walk_anim() -> Animation:
|
func _create_walk_anim() -> Animation:
|
||||||
var anim = Animation.new()
|
return Animation.new()
|
||||||
anim.length = 1.0
|
|
||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
|
||||||
var cycle = 1.0 / locomotion_cycle_speed
|
|
||||||
anim.length = cycle
|
|
||||||
|
|
||||||
# Add rotation tracks for each bone using type=1 (ROTATION)
|
|
||||||
_add_rot_track(anim, "LeftUpperLeg", [
|
|
||||||
{"t": 0.0, "rot": Vector3(-0.6, 0, 0)},
|
|
||||||
{"t": cycle * 0.5, "rot": Vector3(0.6, 0, 0)},
|
|
||||||
{"t": cycle, "rot": Vector3(-0.6, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "RightUpperLeg", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0.6, 0, 0)},
|
|
||||||
{"t": cycle * 0.5, "rot": Vector3(-0.6, 0, 0)},
|
|
||||||
{"t": cycle, "rot": Vector3(0.6, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "LeftUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(-0.4, 0, 0)},
|
|
||||||
{"t": cycle * 0.5, "rot": Vector3(0.4, 0, 0)},
|
|
||||||
{"t": cycle, "rot": Vector3(-0.4, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "RightUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0.4, 0, 0)},
|
|
||||||
{"t": cycle * 0.5, "rot": Vector3(-0.4, 0, 0)},
|
|
||||||
{"t": cycle, "rot": Vector3(0.4, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "Spine", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": cycle * 0.25, "rot": Vector3(0, 0.1, 0)},
|
|
||||||
{"t": cycle * 0.5, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": cycle * 0.75, "rot": Vector3(0, -0.1, 0)},
|
|
||||||
{"t": cycle, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
return anim
|
|
||||||
|
|
||||||
func _create_run_anim() -> Animation:
|
func _create_run_anim() -> Animation:
|
||||||
var anim = _create_walk_anim()
|
return Animation.new()
|
||||||
anim.length = 0.5 # faster
|
|
||||||
return anim
|
|
||||||
|
|
||||||
func _create_jump_anim() -> Animation:
|
func _create_jump_anim() -> Animation:
|
||||||
var anim = Animation.new()
|
return Animation.new()
|
||||||
anim.length = 1.0
|
|
||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
|
||||||
|
|
||||||
_add_rot_track(anim, "LeftUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "RightUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 0.5, "rot": Vector3(-1.2, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "LeftUpperLeg", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "RightUpperLeg", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 0.5, "rot": Vector3(-0.3, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
return anim
|
|
||||||
|
|
||||||
func _create_idle_anim() -> Animation:
|
func _create_idle_anim() -> Animation:
|
||||||
var anim = Animation.new()
|
return Animation.new()
|
||||||
anim.length = 2.0
|
|
||||||
anim.loop_mode = Animation.LOOP_LINEAR
|
|
||||||
|
|
||||||
_add_rot_track(anim, "Spine", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0.02, 0, 0)},
|
|
||||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "Head", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0.03, 0.02, 0)},
|
|
||||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "LeftUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0.02, 0, 0.05)},
|
|
||||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
_add_rot_track(anim, "RightUpperArm", [
|
|
||||||
{"t": 0.0, "rot": Vector3(0, 0, 0)},
|
|
||||||
{"t": 1.0, "rot": Vector3(0.02, 0, -0.05)},
|
|
||||||
{"t": 2.0, "rot": Vector3(0, 0, 0)}
|
|
||||||
])
|
|
||||||
return anim
|
|
||||||
|
|
||||||
func _add_rot_track(anim: Animation, bone_name: String, keyframes: Array) -> void:
|
|
||||||
var track = anim.add_track(Animation.TYPE_ROTATION_3D)
|
|
||||||
if track < 0:
|
|
||||||
print("SkinnedPlayerModel: ERROR failed to add rotation track for " + bone_name)
|
|
||||||
return
|
|
||||||
anim.track_set_path(track, "MikuRig/Skeleton3D:" + bone_name)
|
|
||||||
anim.track_set_interpolation_type(track, Animation.INTERPOLATION_LINEAR)
|
|
||||||
for kf in keyframes:
|
|
||||||
var quat = Quaternion.from_euler(kf["rot"])
|
|
||||||
var err = anim.track_insert_key(track, kf["t"], quat)
|
|
||||||
if err != OK:
|
|
||||||
print("SkinnedPlayerModel: ERROR inserting key for " + bone_name + " t=" + str(kf["t"]))
|
|
||||||
print("SkinnedPlayerModel: rotation track '%s' has %d keys" % [bone_name, anim.track_get_key_count(track)])
|
|
||||||
|
|
||||||
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
func _is_animation_broken(ap: AnimationPlayer, anim_name: String) -> bool:
|
||||||
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
## Returns true if the animation has near-zero motion (Blender GLTF track stripping).
|
||||||
@@ -354,7 +243,7 @@ func _warn_missing(anim_name: String) -> void:
|
|||||||
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
print("SkinnedPlayerModel: WARNING - '%s' not available (using fallback)" % anim_name)
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
if not animation_player:
|
if not skeleton:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get movement state from parent PlayerMovementController
|
# Get movement state from parent PlayerMovementController
|
||||||
@@ -375,11 +264,13 @@ func _process(delta: float) -> void:
|
|||||||
var is_dead: bool = false
|
var is_dead: bool = false
|
||||||
if parent and "is_dead" in parent:
|
if parent and "is_dead" in parent:
|
||||||
is_dead = parent.is_dead
|
is_dead = parent.is_dead
|
||||||
var target_anim = "Idle"
|
|
||||||
|
# Determine target animation
|
||||||
|
var anim_name = "Idle"
|
||||||
if is_dead:
|
if is_dead:
|
||||||
target_anim = "Death"
|
anim_name = "Death"
|
||||||
elif is_crouching:
|
elif is_crouching:
|
||||||
target_anim = "Crouch"
|
anim_name = "Crouch"
|
||||||
else:
|
else:
|
||||||
var walk_speed = 10.0
|
var walk_speed = 10.0
|
||||||
if sm and sm.params:
|
if sm and sm.params:
|
||||||
@@ -387,26 +278,166 @@ func _process(delta: float) -> void:
|
|||||||
match state:
|
match state:
|
||||||
"ground", "idle":
|
"ground", "idle":
|
||||||
if speed > walk_speed * 1.2:
|
if speed > walk_speed * 1.2:
|
||||||
target_anim = "Run"
|
anim_name = "Run"
|
||||||
elif speed > 0.5:
|
elif speed > 0.5:
|
||||||
target_anim = "Walk"
|
anim_name = "Walk"
|
||||||
"slide", "wall_run", "grapple", "dash":
|
"slide", "wall_run", "grapple", "dash":
|
||||||
target_anim = "Run"
|
anim_name = "Run"
|
||||||
"air":
|
"air":
|
||||||
target_anim = "Jump"
|
anim_name = "Jump"
|
||||||
_:
|
_:
|
||||||
target_anim = "Idle"
|
anim_name = "Idle"
|
||||||
|
|
||||||
if animation_player.current_animation != target_anim or not animation_player.is_playing():
|
# Switch animation if needed (AP for reference timing, code for actual poses)
|
||||||
if animation_player.has_animation(target_anim):
|
if animation_player and (animation_player.current_animation != anim_name or not animation_player.is_playing()):
|
||||||
animation_player.play(target_anim)
|
if animation_player.has_animation(anim_name):
|
||||||
|
animation_player.play(anim_name)
|
||||||
|
|
||||||
# Force the skeleton to update bone transforms for rendering
|
# Update animation time
|
||||||
if skeleton:
|
_anim_time += delta
|
||||||
skeleton.update_skeleton()
|
_current_anim = anim_name
|
||||||
|
|
||||||
|
# Apply code-driven animation poses directly to skeleton
|
||||||
|
_apply_animation_by_code(anim_name, _anim_time)
|
||||||
|
|
||||||
|
# Debug
|
||||||
_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 and animation_player.is_playing():
|
||||||
print("SkinnedPlayerModel: playing '%s' (pos: %.2f)" % [animation_player.current_animation, animation_player.current_animation_position])
|
print("SkinnedPlayerModel: '%s' (pos: %.2f)" % [anim_name, _anim_time])
|
||||||
pass # end of _process
|
|
||||||
|
|
||||||
|
# Code-driven animation: applies bone rotations directly each frame.
|
||||||
|
var _current_anim: String = ""
|
||||||
|
var _anim_time: float = 0.0
|
||||||
|
|
||||||
|
## Bone indices for animation (cached at load time)
|
||||||
|
var _bone_idxs: Dictionary = {} # bone_name -> index
|
||||||
|
|
||||||
|
## Per-bone animation rot_x values for each bone (radians offset from rest)
|
||||||
|
func _apply_animation_by_code(anim_name: String, time: float) -> void:
|
||||||
|
if not skeleton:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Ensure bone indices are cached
|
||||||
|
if _bone_idxs.is_empty():
|
||||||
|
_cache_bone_indices()
|
||||||
|
|
||||||
|
# Loop all bones and reset to rest, then apply animation offset
|
||||||
|
var bone_count: int = skeleton.get_bone_count()
|
||||||
|
for i in range(bone_count):
|
||||||
|
var rest: Transform3D = skeleton.get_bone_rest(i)
|
||||||
|
var new_transform := Transform3D(rest.basis, rest.origin)
|
||||||
|
|
||||||
|
# Apply animation-specific rotation offset
|
||||||
|
var offset := _get_bone_anim_offset(anim_name, i, time)
|
||||||
|
if offset != Vector3.ZERO:
|
||||||
|
var offset_basis := Basis(Quaternion.from_euler(offset))
|
||||||
|
new_transform.basis = offset_basis * rest.basis
|
||||||
|
# Recalculate global position with the rotated bone
|
||||||
|
new_transform.origin = rest.origin
|
||||||
|
|
||||||
|
skeleton.set_bone_global_pose_override(i, new_transform, 1.0, true)
|
||||||
|
|
||||||
|
|
||||||
|
func _cache_bone_indices() -> void:
|
||||||
|
if not skeleton:
|
||||||
|
return
|
||||||
|
_bone_idxs.clear()
|
||||||
|
for i in range(skeleton.get_bone_count()):
|
||||||
|
_bone_idxs[skeleton.get_bone_name(i)] = i
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the bone rotation offset (euler radians) for a given bone at a given time.
|
||||||
|
func _get_bone_anim_offset(anim_name: String, bone_idx: int, time: float) -> Vector3:
|
||||||
|
if not skeleton:
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
var bone_name: String = ""
|
||||||
|
for k in _bone_idxs:
|
||||||
|
if _bone_idxs[k] == bone_idx:
|
||||||
|
bone_name = k
|
||||||
|
break
|
||||||
|
|
||||||
|
if anim_name == "Walk":
|
||||||
|
return _walk_offset(bone_name, time)
|
||||||
|
elif anim_name == "Run":
|
||||||
|
return _run_offset(bone_name, time)
|
||||||
|
elif anim_name == "Jump":
|
||||||
|
return _jump_offset(bone_name, time)
|
||||||
|
elif anim_name == "Idle":
|
||||||
|
return _idle_offset(bone_name, time)
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
|
||||||
|
func _walk_offset(bone_name: String, time: float) -> Vector3:
|
||||||
|
var cycle: float = 1.0 / locomotion_cycle_speed
|
||||||
|
var t: float = fmod(time, cycle) / cycle # Normalize to 0-1
|
||||||
|
var phase: float = t * 6.28318 # 2*pi
|
||||||
|
match bone_name:
|
||||||
|
"LeftUpperLeg":
|
||||||
|
return Vector3(sin(phase) * 0.6, 0, 0)
|
||||||
|
"RightUpperLeg":
|
||||||
|
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
||||||
|
"LeftUpperArm":
|
||||||
|
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
||||||
|
"RightUpperArm":
|
||||||
|
return Vector3(sin(phase) * 0.4, 0, 0)
|
||||||
|
"Spine":
|
||||||
|
return Vector3(0, sin(phase) * 0.1, 0)
|
||||||
|
_:
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
func _run_offset(bone_name: String, time: float) -> Vector3:
|
||||||
|
var cycle: float = 1.0 / locomotion_cycle_speed * 1.8
|
||||||
|
var t: float = fmod(time, cycle) / cycle
|
||||||
|
var phase: float = t * 6.28318
|
||||||
|
match bone_name:
|
||||||
|
"LeftUpperLeg":
|
||||||
|
return Vector3(sin(phase) * 0.6, 0, 0)
|
||||||
|
"RightUpperLeg":
|
||||||
|
return Vector3(sin(phase + 3.14159) * 0.6, 0, 0)
|
||||||
|
"LeftUpperArm":
|
||||||
|
return Vector3(sin(phase + 3.14159) * 0.4, 0, 0)
|
||||||
|
"RightUpperArm":
|
||||||
|
return Vector3(sin(phase) * 0.4, 0, 0)
|
||||||
|
_:
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
func _jump_offset(bone_name: String, time: float) -> Vector3:
|
||||||
|
var cycle: float = 0.83
|
||||||
|
var t: float = fmod(time, cycle) / cycle
|
||||||
|
var phase: float = t * 6.28318
|
||||||
|
# Brief jump pose: arms raised, knees bent at peak (t=0.5)
|
||||||
|
var peak := 1.0 - abs(t - 0.5) * 2.0 # 0 at edges, 1 at middle
|
||||||
|
peak = clamp(peak, 0.0, 1.0)
|
||||||
|
match bone_name:
|
||||||
|
"LeftUpperArm":
|
||||||
|
return Vector3(-1.2 * peak, 0, -0.3 * peak)
|
||||||
|
"RightUpperArm":
|
||||||
|
return Vector3(-1.2 * peak, 0, 0.3 * peak)
|
||||||
|
"LeftUpperLeg":
|
||||||
|
return Vector3(-0.3 * peak, 0, 0)
|
||||||
|
"RightUpperLeg":
|
||||||
|
return Vector3(-0.3 * peak, 0, 0)
|
||||||
|
"Spine":
|
||||||
|
return Vector3(0.1 * peak, 0, 0)
|
||||||
|
_:
|
||||||
|
return Vector3.ZERO
|
||||||
|
|
||||||
|
func _idle_offset(bone_name: String, time: float) -> Vector3:
|
||||||
|
var t = time * 0.5 # Slow breathing
|
||||||
|
match bone_name:
|
||||||
|
"Spine":
|
||||||
|
return Vector3(0, sin(t) * 0.005, 0)
|
||||||
|
"Head":
|
||||||
|
return Vector3(sin(t * 0.7) * 0.02, sin(t * 0.5) * 0.02, 0)
|
||||||
|
"LeftUpperArm":
|
||||||
|
return Vector3(sin(t) * 0.02, 0, sin(t * 0.5) * 0.05)
|
||||||
|
"RightUpperArm":
|
||||||
|
return Vector3(sin(t) * 0.02, 0, -sin(t * 0.5) * 0.05)
|
||||||
|
"Hips":
|
||||||
|
return Vector3(0, sin(t) * 0.005, 0)
|
||||||
|
_:
|
||||||
|
return Vector3.ZERO
|
||||||
|
|||||||
Reference in New Issue
Block a user