Feat/14 movement overhaul #20

Merged
Dotts merged 86 commits from feat/14-movement-overhaul into main 2026-07-17 10:44:23 -07:00
Showing only changes of commit 9e498370fb - Show all commits
+169 -138
View File
@@ -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