Files
Project-M/Assets/_Project/Scripts/Editor/PlayerRigTools.cs
T
kronic 1224fd97f8 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>
2026-06-11 00:35:39 -07:00

96 lines
4.6 KiB
C#

using UnityEditor;
using UnityEditor.Animations;
using UnityEngine;
namespace ProjectM.EditorTools
{
/// <summary>
/// MC-4 — builds the player's MELEE SWING clip + adds the IsAttacking param + MeleeSwing state to AC_PlayerTopDown.
/// The swing clip is built FROM the full idle pose (every bone keyed) + a Root YAW twist on top. A clip that keys
/// ONLY the Root makes Rukhanka collapse every un-keyed bone (Hips/Spine/legs) to identity for the state's
/// duration -> the body sinks into the floor; writeDefaultValues does NOT prevent it (2026-06-11 fix). Basing on
/// idle leaves nothing un-keyed; the Root yaw is a vertical-axis (height-preserving) twist that reads as a
/// horizontal slash, paired with the CombatFeedbackSystem slash-arc VFX. PlayerAnimationDriveSystem drives
/// IsAttacking from MeleeCombo. Idempotent / re-runnable (menu: ProjectM/Animation).
/// </summary>
public static class PlayerRigTools
{
const string SwingClip = "Assets/_Project/Animation/PlayerMeleeSwing.anim";
const string PlayerController = "Assets/_Project/Animation/AC_PlayerTopDown.controller";
[MenuItem("ProjectM/Animation/Player - Build Melee Swing")]
public static void BuildPlayerMeleeSwing()
{
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; }
var idle = FindIdleClip(ac);
if (idle == null) { Debug.LogError("[PlayerRigTools] No Idle-state clip to base the swing on."); return; }
// Full idle pose (every bone keyed) + a Root yaw twist. See the class summary for why a Root-only clip sinks.
var clip = AssetDatabase.LoadAssetAtPath<AnimationClip>(SwingClip);
if (clip == null) { clip = new AnimationClip { frameRate = 30f }; AssetDatabase.CreateAsset(clip, SwingClip); }
clip.ClearCurves();
foreach (var b in AnimationUtility.GetCurveBindings(idle))
{
if (b.path == "Root") continue; // the Root is driven below
AnimationUtility.SetEditorCurve(clip, b, AnimationUtility.GetEditorCurve(idle, b));
}
var yaw = new AnimationCurve(
new Keyframe(0f, 0f), new Keyframe(0.06f, -25f), new Keyframe(0.16f, 48f), new Keyframe(0.30f, 0f));
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw);
var s = AnimationUtility.GetAnimationClipSettings(clip);
s.loopTime = false;
AnimationUtility.SetAnimationClipSettings(clip, s);
EditorUtility.SetDirty(clip);
AssetDatabase.SaveAssets();
if (!HasParam(ac, "IsAttacking"))
ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool);
var sm = ac.layers[0].stateMachine;
var swing = FindState(sm, "MeleeSwing");
if (swing == null)
{
swing = sm.AddState("MeleeSwing");
swing.writeDefaultValues = false;
var toSwing = sm.AddAnyStateTransition(swing);
toSwing.hasExitTime = false;
toSwing.duration = 0.05f;
toSwing.canTransitionToSelf = false;
toSwing.AddCondition(AnimatorConditionMode.If, 0f, "IsAttacking");
var fromSwing = swing.AddExitTransition();
fromSwing.hasExitTime = false;
fromSwing.duration = 0.10f;
fromSwing.AddCondition(AnimatorConditionMode.IfNot, 0f, "IsAttacking");
}
swing.motion = clip;
EditorUtility.SetDirty(ac);
AssetDatabase.SaveAssets();
Debug.Log("[PlayerRigTools] AC_PlayerTopDown: IsAttacking + idle-based MeleeSwing built (no un-keyed-bone collapse).");
}
static AnimationClip FindIdleClip(AnimatorController ac)
{
foreach (var c in ac.layers[0].stateMachine.states)
if (c.state.name == "Idle") return c.state.motion as AnimationClip;
return null;
}
static bool HasParam(AnimatorController ac, string name)
{
foreach (var p in ac.parameters) if (p.name == name) return true;
return false;
}
static AnimatorState FindState(AnimatorStateMachine sm, string name)
{
foreach (var c in sm.states) if (c.state.name == name) return c.state;
return null;
}
}
}