Combat: melee swing animation + live range slash-arc VFX (MC-4 polish)
Rukhanka swing animation: PlayerRigTools builds a procedural Root-bone PlayerMeleeSwing.anim and adds an IsAttacking param + MeleeSwing state to AC_PlayerTopDown (mirroring the enemy attack recipe -- no authored Synty Generic melee clip exists). PlayerAnimationDriveSystem pulses IsAttacking from the replicated MeleeCombo swing window (local + remote, NetworkTick wrap-safe, re-triggers per chained hit). CombatFeedbackSystem flashes a procedural cone slash-arc mesh matching the LIVE cleave range + half-angle on each swing (finisher wider/warmer) -- the arc IS the range telegraph. Addresses 'range isn't clear + no animation'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,31 @@ AnimatorState:
|
|||||||
m_MirrorParameter:
|
m_MirrorParameter:
|
||||||
m_CycleOffsetParameter:
|
m_CycleOffsetParameter:
|
||||||
m_TimeParameter:
|
m_TimeParameter:
|
||||||
|
--- !u!1101 &-7833896337878999491
|
||||||
|
AnimatorStateTransition:
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name:
|
||||||
|
m_Conditions:
|
||||||
|
- m_ConditionMode: 1
|
||||||
|
m_ConditionEvent: IsAttacking
|
||||||
|
m_EventTreshold: 0
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: 4550302468649108848}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 0
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.05
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.75
|
||||||
|
m_HasExitTime: 0
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 0
|
||||||
--- !u!1101 &-1505565898738729052
|
--- !u!1101 &-1505565898738729052
|
||||||
AnimatorStateTransition:
|
AnimatorStateTransition:
|
||||||
m_ObjectHideFlags: 1
|
m_ObjectHideFlags: 1
|
||||||
@@ -172,6 +197,12 @@ AnimatorController:
|
|||||||
m_DefaultInt: 0
|
m_DefaultInt: 0
|
||||||
m_DefaultBool: 0
|
m_DefaultBool: 0
|
||||||
m_Controller: {fileID: 9100000}
|
m_Controller: {fileID: 9100000}
|
||||||
|
- m_Name: IsAttacking
|
||||||
|
m_Type: 4
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
m_AnimatorLayers:
|
m_AnimatorLayers:
|
||||||
- serializedVersion: 5
|
- serializedVersion: 5
|
||||||
m_Name: Base Layer
|
m_Name: Base Layer
|
||||||
@@ -237,6 +268,33 @@ AnimatorState:
|
|||||||
m_MirrorParameter:
|
m_MirrorParameter:
|
||||||
m_CycleOffsetParameter:
|
m_CycleOffsetParameter:
|
||||||
m_TimeParameter:
|
m_TimeParameter:
|
||||||
|
--- !u!1102 &4550302468649108848
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: MeleeSwing
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions:
|
||||||
|
- {fileID: 8638400773933187460}
|
||||||
|
m_StateMachineBehaviours: []
|
||||||
|
m_Position: {x: 50, y: 50, z: 0}
|
||||||
|
m_IKOnFeet: 0
|
||||||
|
m_WriteDefaultValues: 1
|
||||||
|
m_Mirror: 0
|
||||||
|
m_SpeedParameterActive: 0
|
||||||
|
m_MirrorParameterActive: 0
|
||||||
|
m_CycleOffsetParameterActive: 0
|
||||||
|
m_TimeParameterActive: 0
|
||||||
|
m_Motion: {fileID: 7400000, guid: c819793eb6a73914b9875b1fe58b8626, type: 2}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
--- !u!1101 &5014557051428573475
|
--- !u!1101 &5014557051428573475
|
||||||
AnimatorStateTransition:
|
AnimatorStateTransition:
|
||||||
m_ObjectHideFlags: 1
|
m_ObjectHideFlags: 1
|
||||||
@@ -280,9 +338,13 @@ AnimatorStateMachine:
|
|||||||
- serializedVersion: 1
|
- serializedVersion: 1
|
||||||
m_State: {fileID: 1998880016702917871}
|
m_State: {fileID: 1998880016702917871}
|
||||||
m_Position: {x: 270, y: 130, z: 0}
|
m_Position: {x: 270, y: 130, z: 0}
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: 4550302468649108848}
|
||||||
|
m_Position: {x: 305, y: 195, z: 0}
|
||||||
m_ChildStateMachines: []
|
m_ChildStateMachines: []
|
||||||
m_AnyStateTransitions:
|
m_AnyStateTransitions:
|
||||||
- {fileID: 5014557051428573475}
|
- {fileID: 5014557051428573475}
|
||||||
|
- {fileID: -7833896337878999491}
|
||||||
m_EntryTransitions: []
|
m_EntryTransitions: []
|
||||||
m_StateMachineTransitions: {}
|
m_StateMachineTransitions: {}
|
||||||
m_StateMachineBehaviours: []
|
m_StateMachineBehaviours: []
|
||||||
@@ -318,3 +380,28 @@ AnimatorState:
|
|||||||
m_MirrorParameter:
|
m_MirrorParameter:
|
||||||
m_CycleOffsetParameter:
|
m_CycleOffsetParameter:
|
||||||
m_TimeParameter:
|
m_TimeParameter:
|
||||||
|
--- !u!1101 &8638400773933187460
|
||||||
|
AnimatorStateTransition:
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name:
|
||||||
|
m_Conditions:
|
||||||
|
- m_ConditionMode: 2
|
||||||
|
m_ConditionEvent: IsAttacking
|
||||||
|
m_EventTreshold: 0
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: 0}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 1
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.1
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.9
|
||||||
|
m_HasExitTime: 0
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 1
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!74 &7400000
|
||||||
|
AnimationClip:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: PlayerMeleeSwing
|
||||||
|
serializedVersion: 7
|
||||||
|
m_Legacy: 0
|
||||||
|
m_Compressed: 0
|
||||||
|
m_UseHighQualityCurve: 1
|
||||||
|
m_RotationCurves: []
|
||||||
|
m_CompressedRotationCurves: []
|
||||||
|
m_EulerCurves:
|
||||||
|
- curve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve:
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
inSlope: {x: 0, y: 0, z: 0}
|
||||||
|
outSlope: {x: 0, y: 0, z: 0}
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: {x: 0, y: 0, z: 0.33333334}
|
||||||
|
outWeight: {x: 0, y: 0, z: 0.33333334}
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.06
|
||||||
|
value: {x: 6.297375, y: -22, z: 0}
|
||||||
|
inSlope: {x: 167.93002, y: 0, z: 0}
|
||||||
|
outSlope: {x: 167.93002, y: 0, z: 0}
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: {x: 0.33333334, y: 0, z: 0.33333334}
|
||||||
|
outWeight: {x: 0.33333334, y: 0, z: 0.33333334}
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.14
|
||||||
|
value: {x: 16, y: 28.176, z: 0}
|
||||||
|
inSlope: {x: 0, y: 537.60004, z: 0}
|
||||||
|
outSlope: {x: 0, y: 537.60004, z: 0}
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: {x: 0, y: 0.33333334, z: 0.33333334}
|
||||||
|
outWeight: {x: 0, y: 0.33333334, z: 0.33333334}
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.16
|
||||||
|
value: {x: 15.4513035, y: 34, z: 0}
|
||||||
|
inSlope: {x: -52.67489, y: 0, z: 0}
|
||||||
|
outSlope: {x: -52.67489, y: 0, z: 0}
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: {x: 0.33333334, y: 0, z: 0.33333334}
|
||||||
|
outWeight: {x: 0.33333334, y: 0, z: 0.33333334}
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.32
|
||||||
|
value: {x: 0, y: 0, z: 0}
|
||||||
|
inSlope: {x: 0, y: 0, z: 0}
|
||||||
|
outSlope: {x: 0, y: 0, z: 0}
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: {x: 0, y: 0, z: 0.33333334}
|
||||||
|
outWeight: {x: 0, y: 0, z: 0.33333334}
|
||||||
|
m_PreInfinity: 2
|
||||||
|
m_PostInfinity: 2
|
||||||
|
m_RotationOrder: 4
|
||||||
|
path: Root
|
||||||
|
m_PositionCurves: []
|
||||||
|
m_ScaleCurves: []
|
||||||
|
m_FloatCurves: []
|
||||||
|
m_PPtrCurves: []
|
||||||
|
m_SampleRate: 30
|
||||||
|
m_WrapMode: 0
|
||||||
|
m_Bounds:
|
||||||
|
m_Center: {x: 0, y: 0, z: 0}
|
||||||
|
m_Extent: {x: 0, y: 0, z: 0}
|
||||||
|
m_ClipBindingConstant:
|
||||||
|
genericBindings:
|
||||||
|
- serializedVersion: 2
|
||||||
|
path: 3066451557
|
||||||
|
attribute: 4
|
||||||
|
script: {fileID: 0}
|
||||||
|
typeID: 4
|
||||||
|
customType: 4
|
||||||
|
isPPtrCurve: 0
|
||||||
|
isIntCurve: 0
|
||||||
|
isSerializeReferenceCurve: 0
|
||||||
|
pptrCurveMapping: []
|
||||||
|
m_AnimationClipSettings:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_AdditiveReferencePoseClip: {fileID: 0}
|
||||||
|
m_AdditiveReferencePoseTime: 0
|
||||||
|
m_StartTime: 0
|
||||||
|
m_StopTime: 0.32
|
||||||
|
m_OrientationOffsetY: 0
|
||||||
|
m_Level: 0
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_HasAdditiveReferencePose: 0
|
||||||
|
m_LoopTime: 0
|
||||||
|
m_LoopBlend: 0
|
||||||
|
m_LoopBlendOrientation: 0
|
||||||
|
m_LoopBlendPositionY: 0
|
||||||
|
m_LoopBlendPositionXZ: 0
|
||||||
|
m_KeepOriginalOrientation: 0
|
||||||
|
m_KeepOriginalPositionY: 1
|
||||||
|
m_KeepOriginalPositionXZ: 0
|
||||||
|
m_HeightFromFeet: 0
|
||||||
|
m_Mirror: 0
|
||||||
|
m_EditorCurves:
|
||||||
|
- serializedVersion: 2
|
||||||
|
curve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve:
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0
|
||||||
|
value: 0
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.06
|
||||||
|
value: -22
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.16
|
||||||
|
value: 34
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.32
|
||||||
|
value: 0
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
m_PreInfinity: 2
|
||||||
|
m_PostInfinity: 2
|
||||||
|
m_RotationOrder: 4
|
||||||
|
attribute: localEulerAnglesRaw.y
|
||||||
|
path: Root
|
||||||
|
classID: 4
|
||||||
|
script: {fileID: 0}
|
||||||
|
flags: 16
|
||||||
|
- serializedVersion: 2
|
||||||
|
curve:
|
||||||
|
serializedVersion: 2
|
||||||
|
m_Curve:
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0
|
||||||
|
value: 0
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.14
|
||||||
|
value: 16
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
- serializedVersion: 3
|
||||||
|
time: 0.32
|
||||||
|
value: 0
|
||||||
|
inSlope: 0
|
||||||
|
outSlope: 0
|
||||||
|
tangentMode: 0
|
||||||
|
weightedMode: 0
|
||||||
|
inWeight: 0
|
||||||
|
outWeight: 0
|
||||||
|
m_PreInfinity: 2
|
||||||
|
m_PostInfinity: 2
|
||||||
|
m_RotationOrder: 4
|
||||||
|
attribute: localEulerAnglesRaw.x
|
||||||
|
path: Root
|
||||||
|
classID: 4
|
||||||
|
script: {fileID: 0}
|
||||||
|
flags: 16
|
||||||
|
m_EulerEditorCurves: []
|
||||||
|
m_HasGenericRootTransform: 0
|
||||||
|
m_HasMotionFloatCurves: 0
|
||||||
|
m_Events: []
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c819793eb6a73914b9875b1fe58b8626
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 7400000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -54,6 +54,12 @@ namespace ProjectM.Client
|
|||||||
ParticleSystem _muzzleFx;
|
ParticleSystem _muzzleFx;
|
||||||
ParticleSystem _dashFx;
|
ParticleSystem _dashFx;
|
||||||
ParticleSystem _swingFx;
|
ParticleSystem _swingFx;
|
||||||
|
Mesh _slashMesh;
|
||||||
|
MeshRenderer _slashMr;
|
||||||
|
Material _slashMat;
|
||||||
|
Color _slashTint;
|
||||||
|
float _slashAge, _slashLife;
|
||||||
|
bool _slashActive;
|
||||||
AudioClip _hitClip;
|
AudioClip _hitClip;
|
||||||
AudioClip _deathClip;
|
AudioClip _deathClip;
|
||||||
AudioClip _fireClip;
|
AudioClip _fireClip;
|
||||||
@@ -95,6 +101,7 @@ namespace ProjectM.Client
|
|||||||
_muzzleFx = MakeBurst("Muzzle", mat, new Color(0.6f, 2.4f, 3.2f), 0.12f, 5f, 0.20f, 128);
|
_muzzleFx = MakeBurst("Muzzle", mat, new Color(0.6f, 2.4f, 3.2f), 0.12f, 5f, 0.20f, 128);
|
||||||
_dashFx = MakeBurst("DashWhoosh", mat, new Color(0.7f, 2.6f, 3.0f), 0.16f, 4f, 0.30f, 256);
|
_dashFx = MakeBurst("DashWhoosh", mat, new Color(0.7f, 2.6f, 3.0f), 0.16f, 4f, 0.30f, 256);
|
||||||
_swingFx = MakeBurst("MeleeSwing", mat, new Color(3.0f, 2.6f, 0.9f), 0.14f, 6f, 0.28f, 256);
|
_swingFx = MakeBurst("MeleeSwing", mat, new Color(3.0f, 2.6f, 0.9f), 0.14f, 6f, 0.28f, 256);
|
||||||
|
BuildSlash();
|
||||||
|
|
||||||
for (int i = 0; i < NumberPoolSize; i++)
|
for (int i = 0; i < NumberPoolSize; i++)
|
||||||
_numbers.Add(CreateNumber());
|
_numbers.Add(CreateNumber());
|
||||||
@@ -104,6 +111,8 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
if (_fxRoot != null)
|
if (_fxRoot != null)
|
||||||
Object.Destroy(_fxRoot.gameObject);
|
Object.Destroy(_fxRoot.gameObject);
|
||||||
|
if (_slashMesh != null) Object.Destroy(_slashMesh);
|
||||||
|
if (_slashMat != null) Object.Destroy(_slashMat);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnUpdate()
|
protected override void OnUpdate()
|
||||||
@@ -268,7 +277,12 @@ namespace ProjectM.Client
|
|||||||
PlayClip(_swingClip, (Vector3)localPos, 0.45f);
|
PlayClip(_swingClip, (Vector3)localPos, 0.45f);
|
||||||
PrototypeCameraRig.AddShake(0.04f * step);
|
PrototypeCameraRig.AddShake(0.04f * step);
|
||||||
int comboLen = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? (int)math.clamp((int)tcfg.MeleeComboLength, 1, 3) : 3;
|
int comboLen = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? (int)math.clamp((int)tcfg.MeleeComboLength, 1, 3) : 3;
|
||||||
if (step >= comboLen) PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs); // finisher pop keyed off the live combo length (MC-4 review)
|
bool finisher = step >= comboLen;
|
||||||
|
float slashRange = tcfg.MeleeRange > 0f ? tcfg.MeleeRange : 2.6f;
|
||||||
|
float slashHalf = tcfg.MeleeConeHalfAngleRad > 0f ? tcfg.MeleeConeHalfAngleRad : 0.9f;
|
||||||
|
if (finisher) slashRange *= tcfg.MeleeFinisherMult > 0f ? tcfg.MeleeFinisherMult : 1.8f;
|
||||||
|
TriggerSlash((Vector3)localPos, new float2(face.x, face.z), slashRange, slashHalf, finisher); // the arc IS the range telegraph (MC-4 visual clarity)
|
||||||
|
if (finisher) PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs);
|
||||||
}
|
}
|
||||||
_lastLocalSwingTick = mc.SwingStartTick;
|
_lastLocalSwingTick = mc.SwingStartTick;
|
||||||
_swingTickInit = true;
|
_swingTickInit = true;
|
||||||
@@ -277,6 +291,7 @@ namespace ProjectM.Client
|
|||||||
UpdateProjectileTrails(cfg);
|
UpdateProjectileTrails(cfg);
|
||||||
PruneVfx();
|
PruneVfx();
|
||||||
AnimateNumbers(dt, cam);
|
AnimateNumbers(dt, cam);
|
||||||
|
UpdateSlash(dt);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- Authored VFX (GabrielAguiar prefabs via VFXConfig); fall back to the procedural burst ----
|
// ---- Authored VFX (GabrielAguiar prefabs via VFXConfig); fall back to the procedural burst ----
|
||||||
@@ -542,6 +557,87 @@ namespace ProjectM.Client
|
|||||||
return ps;
|
return ps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void BuildSlash()
|
||||||
|
{
|
||||||
|
var go = new GameObject("MeleeSlashArc");
|
||||||
|
go.transform.SetParent(_fxRoot, false);
|
||||||
|
_slashMesh = new Mesh { name = "MeleeSlashArc" };
|
||||||
|
var mf = go.AddComponent<MeshFilter>();
|
||||||
|
mf.sharedMesh = _slashMesh;
|
||||||
|
_slashMr = go.AddComponent<MeshRenderer>();
|
||||||
|
_slashMat = MakeParticleMaterial();
|
||||||
|
_slashMat.name = "MeleeSlashArc";
|
||||||
|
_slashMr.sharedMaterial = _slashMat;
|
||||||
|
_slashMr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
|
||||||
|
_slashMr.receiveShadows = false;
|
||||||
|
_slashMr.enabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rebuild the crescent (inner->outer arc) for the LIVE cone half-angle + range, in local +Z-forward space.
|
||||||
|
void BuildSlashMesh(float range, float halfAngle)
|
||||||
|
{
|
||||||
|
const int seg = 16;
|
||||||
|
float r1 = Mathf.Max(0.4f, range);
|
||||||
|
float r0 = r1 * 0.45f;
|
||||||
|
var verts = new Vector3[(seg + 1) * 2];
|
||||||
|
var cols = new Color[(seg + 1) * 2];
|
||||||
|
var uvs = new Vector2[(seg + 1) * 2];
|
||||||
|
var tris = new int[seg * 6];
|
||||||
|
for (int i = 0; i <= seg; i++)
|
||||||
|
{
|
||||||
|
float a = Mathf.Lerp(-halfAngle, halfAngle, i / (float)seg);
|
||||||
|
float sx = Mathf.Sin(a), cz = Mathf.Cos(a);
|
||||||
|
verts[i * 2] = new Vector3(sx * r0, 0f, cz * r0);
|
||||||
|
verts[i * 2 + 1] = new Vector3(sx * r1, 0f, cz * r1);
|
||||||
|
float across = 1f - Mathf.Abs(i / (float)seg * 2f - 1f); // 0 at edges, 1 at centre
|
||||||
|
cols[i * 2] = new Color(1f, 1f, 1f, 0.55f * (0.4f + 0.6f * across)); // inner brighter
|
||||||
|
cols[i * 2 + 1] = new Color(1f, 1f, 1f, 0f); // outer rim fades out
|
||||||
|
uvs[i * 2] = new Vector2(0.5f, 0.5f);
|
||||||
|
uvs[i * 2 + 1] = new Vector2(0.5f, 0.5f);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < seg; i++)
|
||||||
|
{
|
||||||
|
int b = i * 2;
|
||||||
|
tris[i * 6 + 0] = b; tris[i * 6 + 1] = b + 1; tris[i * 6 + 2] = b + 2;
|
||||||
|
tris[i * 6 + 3] = b + 1; tris[i * 6 + 4] = b + 3; tris[i * 6 + 5] = b + 2;
|
||||||
|
}
|
||||||
|
_slashMesh.Clear();
|
||||||
|
_slashMesh.vertices = verts;
|
||||||
|
_slashMesh.colors = cols;
|
||||||
|
_slashMesh.uv = uvs;
|
||||||
|
_slashMesh.triangles = tris;
|
||||||
|
_slashMesh.RecalculateBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flash a cone-shaped slash matching the LIVE melee range + half-angle, oriented along facing. The arc IS the
|
||||||
|
// range telegraph (MC-4 visual clarity): the player sees exactly how far + how wide the cleave reaches.
|
||||||
|
void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, bool finisher)
|
||||||
|
{
|
||||||
|
if (_slashMr == null || _slashMat == null) return;
|
||||||
|
BuildSlashMesh(range, halfAngle);
|
||||||
|
Vector3 f = math.lengthsq(facing) > 1e-6f ? new Vector3(facing.x, 0f, facing.y).normalized : Vector3.forward;
|
||||||
|
var tr = _slashMr.transform;
|
||||||
|
tr.position = pos + Vector3.up * 0.12f;
|
||||||
|
tr.rotation = Quaternion.LookRotation(f, Vector3.up);
|
||||||
|
tr.localScale = Vector3.one;
|
||||||
|
_slashTint = finisher ? new Color(3.2f, 2.3f, 0.7f) : new Color(1.6f, 2.4f, 3.2f); // finisher warm / light cool (HDR -> bloom)
|
||||||
|
_slashLife = finisher ? 0.26f : 0.17f;
|
||||||
|
_slashAge = 0f;
|
||||||
|
_slashActive = true;
|
||||||
|
_slashMat.color = _slashTint;
|
||||||
|
_slashMr.enabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UpdateSlash(float dt)
|
||||||
|
{
|
||||||
|
if (!_slashActive || _slashMr == null) return;
|
||||||
|
_slashAge += dt;
|
||||||
|
float u = _slashAge / Mathf.Max(1e-4f, _slashLife);
|
||||||
|
if (u >= 1f) { _slashActive = false; _slashMr.enabled = false; return; }
|
||||||
|
var c = _slashTint; c.a = 1f - u; _slashMat.color = c;
|
||||||
|
_slashMr.transform.localScale = Vector3.one * (1f + u * 0.12f);
|
||||||
|
}
|
||||||
|
|
||||||
static void EmitAt(ParticleSystem ps, Vector3 pos, int count)
|
static void EmitAt(ParticleSystem ps, Vector3 pos, int count)
|
||||||
{
|
{
|
||||||
if (ps == null) return;
|
if (ps == null) return;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using Unity.Burst;
|
|||||||
using Unity.Collections;
|
using Unity.Collections;
|
||||||
using Unity.Entities;
|
using Unity.Entities;
|
||||||
using Unity.Mathematics;
|
using Unity.Mathematics;
|
||||||
using Unity.NetCode; // GhostOwnerIsLocal
|
using Unity.NetCode; // GhostOwnerIsLocal, NetworkTime, NetworkTick
|
||||||
using Unity.Transforms; // LocalTransform
|
using Unity.Transforms; // LocalTransform
|
||||||
using Unity.CharacterController; // KinematicCharacterBody
|
using Unity.CharacterController; // KinematicCharacterBody
|
||||||
|
|
||||||
@@ -20,13 +20,17 @@ namespace ProjectM.Client
|
|||||||
/// deliberate, documented exception to the project's "all juice = PresentationSystemGroup" rule -- the
|
/// deliberate, documented exception to the project's "all juice = PresentationSystemGroup" rule -- the
|
||||||
/// params MUST be set before Rukhanka's same-frame controller eval (see DR-022).
|
/// params MUST be set before Rukhanka's same-frame controller eval (see DR-022).
|
||||||
///
|
///
|
||||||
|
/// Drives MoveX/MoveZ/Speed/IsDead from locomotion + IsDead, plus IsAttacking (MC-4) pulsed from the
|
||||||
|
/// replicated <see cref="MeleeCombo"/> swing window so the AC_PlayerTopDown MeleeSwing state plays per swing.
|
||||||
|
///
|
||||||
/// Two paths:
|
/// Two paths:
|
||||||
/// LOCAL (owner-predicted, GhostOwnerIsLocal ENABLED): realized CC RelativeVelocity (wall-aware).
|
/// LOCAL (owner-predicted, GhostOwnerIsLocal ENABLED): realized CC RelativeVelocity (wall-aware).
|
||||||
/// REMOTE (interpolated, GhostOwnerIsLocal DISABLED): KinematicCharacterBody is NOT a [GhostField] and
|
/// REMOTE (interpolated, GhostOwnerIsLocal DISABLED): KinematicCharacterBody is NOT a [GhostField] and
|
||||||
/// the CC processor is owner-only, so RelativeVelocity stays baked-zero on remotes -> derive
|
/// the CC processor is owner-only, so RelativeVelocity stays baked-zero on remotes -> derive
|
||||||
/// planar velocity from replicated LocalTransform.Position deltas. PlayerFacing.Direction is a
|
/// planar velocity from replicated LocalTransform.Position deltas. PlayerFacing.Direction is a
|
||||||
/// [GhostField] (valid on remotes); EffectiveCharacterStats.MoveSpeed is derived locally each
|
/// [GhostField] (valid on remotes); EffectiveCharacterStats.MoveSpeed is derived locally each
|
||||||
/// tick by StatRecomputeSystem (present on remotes). Cache prevPos per Entity, prune each frame.
|
/// tick by StatRecomputeSystem (present on remotes). MeleeCombo replicates so teammates' swings
|
||||||
|
/// animate too. Cache prevPos per Entity, prune each frame.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation | WorldSystemFilterFlags.ClientSimulation)]
|
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation | WorldSystemFilterFlags.ClientSimulation)]
|
||||||
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
|
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
|
||||||
@@ -35,10 +39,16 @@ namespace ProjectM.Client
|
|||||||
{
|
{
|
||||||
// Perfect-hash keys, built once (managed string ctor). Names MUST match AC_PlayerTopDown.controller
|
// Perfect-hash keys, built once (managed string ctor). Names MUST match AC_PlayerTopDown.controller
|
||||||
// parameter names exactly. Immutable readonly hashes -> domain-reload safe.
|
// parameter names exactly. Immutable readonly hashes -> domain-reload safe.
|
||||||
static readonly FastAnimatorParameter k_MoveX = new FastAnimatorParameter("MoveX");
|
static readonly FastAnimatorParameter k_MoveX = new FastAnimatorParameter("MoveX");
|
||||||
static readonly FastAnimatorParameter k_MoveZ = new FastAnimatorParameter("MoveZ");
|
static readonly FastAnimatorParameter k_MoveZ = new FastAnimatorParameter("MoveZ");
|
||||||
static readonly FastAnimatorParameter k_Speed = new FastAnimatorParameter("Speed");
|
static readonly FastAnimatorParameter k_Speed = new FastAnimatorParameter("Speed");
|
||||||
static readonly FastAnimatorParameter k_IsDead = new FastAnimatorParameter("IsDead");
|
static readonly FastAnimatorParameter k_IsDead = new FastAnimatorParameter("IsDead");
|
||||||
|
static readonly FastAnimatorParameter k_IsAttacking = new FastAnimatorParameter("IsAttacking");
|
||||||
|
|
||||||
|
// Ticks after a swing-start that IsAttacking stays true (drives the MeleeSwing state). Kept < the swing lock
|
||||||
|
// (MeleeRecoverTicks ~16) so a CHAINED swing re-pulses the bool false->true and re-triggers the Any State
|
||||||
|
// transition per hit. ~0.22s @ 60Hz. Presentation-only.
|
||||||
|
const uint k_AttackAnimTicks = 13;
|
||||||
|
|
||||||
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||||
NativeParallelHashMap<Entity, float3> _prevPos;
|
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||||
@@ -58,10 +68,14 @@ namespace ProjectM.Client
|
|||||||
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta is correct for presentation
|
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta is correct for presentation
|
||||||
if (dt < 1e-5f) dt = 1e-5f;
|
if (dt < 1e-5f) dt = 1e-5f;
|
||||||
|
|
||||||
|
// Current authoritative tick for the swing-window check (default = invalid -> IsAttacking stays false).
|
||||||
|
NetworkTick serverTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt) ? nt.ServerTick : default;
|
||||||
|
|
||||||
// --- LOCAL owner (CC velocity) ---
|
// --- LOCAL owner (CC velocity) ---
|
||||||
var localJob = new LocalDriveJob
|
var localJob = new LocalDriveJob
|
||||||
{
|
{
|
||||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||||
|
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||||
};
|
};
|
||||||
Dependency = localJob.ScheduleParallel(Dependency);
|
Dependency = localJob.ScheduleParallel(Dependency);
|
||||||
|
|
||||||
@@ -70,6 +84,7 @@ namespace ProjectM.Client
|
|||||||
var remoteJob = new RemoteDriveJob
|
var remoteJob = new RemoteDriveJob
|
||||||
{
|
{
|
||||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||||
|
isAttacking = k_IsAttacking, serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||||
dt = dt,
|
dt = dt,
|
||||||
prevPos = _prevPos,
|
prevPos = _prevPos,
|
||||||
seen = seen,
|
seen = seen,
|
||||||
@@ -88,6 +103,16 @@ namespace ProjectM.Client
|
|||||||
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// True while now is within [SwingStartTick, SwingStartTick + animTicks) -- a per-swing pulse that re-triggers
|
||||||
|
// on each chained swing. NetworkTick arithmetic (wrap-safe). Presentation-only, Burst-safe.
|
||||||
|
static bool SwingActive(in MeleeCombo mc, NetworkTick serverTick, uint animTicks)
|
||||||
|
{
|
||||||
|
if (mc.SwingStartTick == 0u || !serverTick.IsValid) return false;
|
||||||
|
var start = new NetworkTick(mc.SwingStartTick);
|
||||||
|
var end = new NetworkTick(TickUtil.NonZero(mc.SwingStartTick + animTicks));
|
||||||
|
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||||
|
}
|
||||||
|
|
||||||
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
||||||
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
|
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
|
||||||
// enable bit; do NOT take it as an `in` parameter (that matches on presence -> drives remotes too).
|
// enable bit; do NOT take it as an `in` parameter (that matches on presence -> drives remotes too).
|
||||||
@@ -96,7 +121,9 @@ namespace ProjectM.Client
|
|||||||
[WithPresent(typeof(Dead))]
|
[WithPresent(typeof(Dead))]
|
||||||
partial struct LocalDriveJob : IJobEntity
|
partial struct LocalDriveJob : IJobEntity
|
||||||
{
|
{
|
||||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
|
||||||
|
public NetworkTick serverTick;
|
||||||
|
public uint attackTicks;
|
||||||
|
|
||||||
void Execute(
|
void Execute(
|
||||||
AnimatorControllerParameterIndexTableComponent indexTable,
|
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||||
@@ -104,11 +131,13 @@ namespace ProjectM.Client
|
|||||||
in PlayerFacing facing,
|
in PlayerFacing facing,
|
||||||
in EffectiveCharacterStats stats,
|
in EffectiveCharacterStats stats,
|
||||||
in KinematicCharacterBody body,
|
in KinematicCharacterBody body,
|
||||||
|
in MeleeCombo melee,
|
||||||
EnabledRefRO<Dead> dead)
|
EnabledRefRO<Dead> dead)
|
||||||
{
|
{
|
||||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||||
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
|
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
|
||||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||||
|
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,7 +148,9 @@ namespace ProjectM.Client
|
|||||||
[WithPresent(typeof(Dead))]
|
[WithPresent(typeof(Dead))]
|
||||||
partial struct RemoteDriveJob : IJobEntity
|
partial struct RemoteDriveJob : IJobEntity
|
||||||
{
|
{
|
||||||
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking;
|
||||||
|
public NetworkTick serverTick;
|
||||||
|
public uint attackTicks;
|
||||||
public float dt;
|
public float dt;
|
||||||
public NativeParallelHashMap<Entity, float3> prevPos;
|
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||||
public NativeParallelHashSet<Entity> seen;
|
public NativeParallelHashSet<Entity> seen;
|
||||||
@@ -131,6 +162,7 @@ namespace ProjectM.Client
|
|||||||
in LocalTransform xform,
|
in LocalTransform xform,
|
||||||
in PlayerFacing facing,
|
in PlayerFacing facing,
|
||||||
in EffectiveCharacterStats stats,
|
in EffectiveCharacterStats stats,
|
||||||
|
in MeleeCombo melee,
|
||||||
EnabledRefRO<Dead> dead)
|
EnabledRefRO<Dead> dead)
|
||||||
{
|
{
|
||||||
seen.Add(e);
|
seen.Add(e);
|
||||||
@@ -143,6 +175,7 @@ namespace ProjectM.Client
|
|||||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||||
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
|
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
|
||||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||||
|
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
using UnityEditor;
|
||||||
|
using UnityEditor.Animations;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.EditorTools
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MC-4 — builds the player's procedural MELEE SWING clip and adds the IsAttacking param + MeleeSwing state to
|
||||||
|
/// AC_PlayerTopDown. Mirrors EnemyRigTools' attack recipe (a Root-bone procedural clip — no authored Synty
|
||||||
|
/// Generic melee-swing clip exists; the slash-arc VFX in CombatFeedbackSystem carries the blade read). The clip
|
||||||
|
/// keys ONLY Root rotation (a yaw twist into the swing + a slight forward pitch), leaving the rig's authored
|
||||||
|
/// Root Y offset (feet-on-ground) untouched. 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()
|
||||||
|
{
|
||||||
|
// Horizontal-cleave read: yaw the Root (wind back -> swing across -> recover) + a slight forward pitch.
|
||||||
|
// Root is the top of the Synty Generic rig; locomotion clips don't key Root, so no conflict and it
|
||||||
|
// returns to the authored pose on exit. Rotation-only (no position) so the baked Root Y offset stays.
|
||||||
|
var clip = new AnimationClip { frameRate = 30f };
|
||||||
|
var yaw = new AnimationCurve(
|
||||||
|
new Keyframe(0f, 0f), new Keyframe(0.06f, -22f), new Keyframe(0.16f, 34f), new Keyframe(0.32f, 0f));
|
||||||
|
var pitch = new AnimationCurve(
|
||||||
|
new Keyframe(0f, 0f), new Keyframe(0.14f, 16f), new Keyframe(0.32f, 0f));
|
||||||
|
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.y"), yaw);
|
||||||
|
AnimationUtility.SetEditorCurve(clip, EditorCurveBinding.FloatCurve("Root", typeof(Transform), "localEulerAnglesRaw.x"), pitch);
|
||||||
|
var s = AnimationUtility.GetAnimationClipSettings(clip);
|
||||||
|
s.loopTime = false;
|
||||||
|
AnimationUtility.SetAnimationClipSettings(clip, s);
|
||||||
|
AssetDatabase.DeleteAsset(SwingClip);
|
||||||
|
AssetDatabase.CreateAsset(clip, SwingClip);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
|
||||||
|
var ac = AssetDatabase.LoadAssetAtPath<AnimatorController>(PlayerController);
|
||||||
|
if (ac == null) { Debug.LogError($"[PlayerRigTools] Controller missing: {PlayerController}"); return; }
|
||||||
|
|
||||||
|
if (!HasParam(ac, "IsAttacking"))
|
||||||
|
ac.AddParameter("IsAttacking", AnimatorControllerParameterType.Bool);
|
||||||
|
|
||||||
|
var sm = ac.layers[0].stateMachine;
|
||||||
|
var swingClipAsset = AssetDatabase.LoadAssetAtPath<AnimationClip>(SwingClip);
|
||||||
|
|
||||||
|
var swing = FindState(sm, "MeleeSwing");
|
||||||
|
if (swing == null)
|
||||||
|
{
|
||||||
|
swing = sm.AddState("MeleeSwing");
|
||||||
|
swing.writeDefaultValues = true;
|
||||||
|
|
||||||
|
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 = swingClipAsset; // (re)point at the freshly-built clip
|
||||||
|
|
||||||
|
EditorUtility.SetDirty(ac);
|
||||||
|
AssetDatabase.SaveAssets();
|
||||||
|
Debug.Log("[PlayerRigTools] AC_PlayerTopDown: IsAttacking + MeleeSwing state built; PlayerMeleeSwing.anim created.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8727eff958d5ae94c9cf4acbfd2b0220
|
||||||
Reference in New Issue
Block a user