Fix: attack animation sank the model into the floor (Rukhanka partial-clip collapse)
An attack clip that keys ONLY the Root bone makes Rukhanka collapse every un-keyed bone (Hips/Spine/legs) to identity for the duration of the state -> the body folds halfway into the floor (player MeleeSwing + enemy Attack; writeDefaultValues does NOT prevent it -- confirmed since even a pure yaw, which is height-preserving, still sank). Fix: build the attack clips FROM the full idle pose (every bone keyed) + a Root YAW twist on top, so nothing is un-keyed. Applied to both runtime clips (PlayerMeleeSwing/EnemyAttackWindup) and both editor recipes (PlayerRigTools/EnemyRigTools) so rebuilds stay correct. Operator-verified in Play. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -118,17 +118,25 @@ namespace ProjectM.EditorTools
|
||||
[MenuItem("ProjectM/Animation/Enemy Rigs - 2 Build Controller")]
|
||||
public static void BuildControllerAndClip()
|
||||
{
|
||||
// Attack wind-up: pitch the Root bone forward (whole-body lunge) and back, non-looping. Root is the
|
||||
// top of every Synty Generic rig, so one clip fits all variants; the locomotion clips don't key Root,
|
||||
// so no conflict and Root returns to its authored Y-offset when the state exits.
|
||||
var clip = new AnimationClip { frameRate = 30f };
|
||||
var pitch = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.15f, 30f), new Keyframe(0.40f, 0f));
|
||||
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.x"), pitch);
|
||||
// Attack tell: built FROM the full idle pose (every bone keyed) + a Root YAW coil. A Root-ONLY clip makes
|
||||
// Rukhanka collapse every un-keyed bone to identity for the state's duration -> the body sinks into the
|
||||
// floor (writeDefaultValues does NOT prevent it; 2026-06-11 fix). Idle base = nothing un-keyed; the yaw is
|
||||
// height-preserving. Based on the player controller's Idle clip (the enemy controller is forked below).
|
||||
var srcAc = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
||||
AnimationClip idle = null;
|
||||
if (srcAc != null) foreach (var st in srcAc.layers[0].stateMachine.states) if (st.state.name == "Idle") idle = st.state.motion as AnimationClip;
|
||||
if (idle == null) { Debug.LogError("[EnemyRigTools] No Idle clip to base the attack on."); return; }
|
||||
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(AttackClip);
|
||||
if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, AttackClip); }
|
||||
clip.ClearCurves();
|
||||
foreach (var cb in AnimationUtility.GetCurveBindings(idle))
|
||||
{ if (cb.path == "Root") continue; AnimationUtility.SetEditorCurve(clip, cb, AnimationUtility.GetEditorCurve(idle, cb)); }
|
||||
var yaw = new AnimationCurve(new Keyframe(0f, 0f), new Keyframe(0.15f, -38f), new Keyframe(0.40f, 0f));
|
||||
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw);
|
||||
var s = AnimationUtility.GetAnimationClipSettings(clip);
|
||||
s.loopTime = false;
|
||||
AnimationUtility.SetAnimationClipSettings(clip, s);
|
||||
AssetDatabase.DeleteAsset(AttackClip);
|
||||
AssetDatabase.CreateAsset(clip, AttackClip);
|
||||
EditorUtility.SetDirty(clip);
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
if (AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController) == null)
|
||||
|
||||
Reference in New Issue
Block a user