Animate the player: Rukhanka skeletal locomotion (client-derived)
Replace the capsule with a rigged Synty SciFiSpace soldier driven by Rukhanka 2.9 (netcode replication off; animation derived client-side from replicated state). Adds a slim top-down AnimatorController (idle / 2D-strafe locomotion / death) from Synty clips; client-only PlayerAnimationDriveSystem (local CC-velocity + remote position-delta paths); AnimParamMath (+10 EditMode tests); ServerStripAnimationSystem (disables Rukhanka on the server, zero server-side animation). Client.asmdef gains Rukhanka.Runtime/CharacterController/Physics. EditMode 204/204; Play-validated. See DR-022. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: c15b1956fd3eb8c4ab4ad2052d18bf59
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,320 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!1102 &-8102868851794693864
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Locomotion
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions:
|
||||||
|
- {fileID: 785731946183937007}
|
||||||
|
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: -481359581180607483}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
|
--- !u!1101 &-1505565898738729052
|
||||||
|
AnimatorStateTransition:
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name:
|
||||||
|
m_Conditions:
|
||||||
|
- m_ConditionMode: 3
|
||||||
|
m_ConditionEvent: Speed
|
||||||
|
m_EventTreshold: 0.1
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: -8102868851794693864}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 0
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.15
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.9
|
||||||
|
m_HasExitTime: 0
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 1
|
||||||
|
--- !u!206 &-481359581180607483
|
||||||
|
BlendTree:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Locomotion
|
||||||
|
m_Childs:
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: a4b8f2deec6a99945987cfc59b1b4e54, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 0, y: 0}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: 4f79281e6eb3df54889369eba3aa1c67, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 0, y: 1}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: b982f3686334e9248b6b2020e5682d9e, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 0.7, y: 0.7}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: 4699ac7f1156bfc4dba616d9b3a05234, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 1, y: 0}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: ae899eb338d4f7443b77e2cc71e0b29e, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 0.7, y: -0.7}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: b28d41dc2454f694fac9ab766e54323a, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: 0, y: -1}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: d466621781c6a7449aa01e5ac0dd2c0f, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: -0.7, y: -0.7}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: d294a4226e928db4fbbd933c19ccf5a1, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: -1, y: 0}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
- serializedVersion: 2
|
||||||
|
m_Motion: {fileID: 1827226128182048838, guid: 4c272e8ffeefc6a4ab141f84a6712559, type: 3}
|
||||||
|
m_Threshold: 0
|
||||||
|
m_Position: {x: -0.7, y: 0.7}
|
||||||
|
m_TimeScale: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_DirectBlendParameter: Blend
|
||||||
|
m_Mirror: 0
|
||||||
|
m_BlendParameter: MoveX
|
||||||
|
m_BlendParameterY: MoveZ
|
||||||
|
m_MinThreshold: 0
|
||||||
|
m_MaxThreshold: 1
|
||||||
|
m_UseAutomaticThresholds: 0
|
||||||
|
m_NormalizedBlendValues: 0
|
||||||
|
m_BlendType: 2
|
||||||
|
--- !u!91 &9100000
|
||||||
|
AnimatorController:
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: AC_PlayerTopDown
|
||||||
|
serializedVersion: 6
|
||||||
|
m_AnimatorParameters:
|
||||||
|
- m_Name: MoveX
|
||||||
|
m_Type: 1
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
- m_Name: MoveZ
|
||||||
|
m_Type: 1
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
- m_Name: Speed
|
||||||
|
m_Type: 1
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
- m_Name: IsDead
|
||||||
|
m_Type: 4
|
||||||
|
m_DefaultFloat: 0
|
||||||
|
m_DefaultInt: 0
|
||||||
|
m_DefaultBool: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
m_AnimatorLayers:
|
||||||
|
- serializedVersion: 5
|
||||||
|
m_Name: Base Layer
|
||||||
|
m_StateMachine: {fileID: 5460501127914818087}
|
||||||
|
m_Mask: {fileID: 0}
|
||||||
|
m_Motions: []
|
||||||
|
m_Behaviours: []
|
||||||
|
m_BlendingMode: 0
|
||||||
|
m_SyncedLayerIndex: -1
|
||||||
|
m_DefaultWeight: 0
|
||||||
|
m_IKPass: 0
|
||||||
|
m_SyncedLayerAffectsTiming: 0
|
||||||
|
m_Controller: {fileID: 9100000}
|
||||||
|
m_EvaluateTransitionsOnStart: 1
|
||||||
|
--- !u!1101 &785731946183937007
|
||||||
|
AnimatorStateTransition:
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name:
|
||||||
|
m_Conditions:
|
||||||
|
- m_ConditionMode: 4
|
||||||
|
m_ConditionEvent: Speed
|
||||||
|
m_EventTreshold: 0.1
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: 8302574255206078140}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 0
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.15
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.9
|
||||||
|
m_HasExitTime: 0
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 1
|
||||||
|
--- !u!1102 &1998880016702917871
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Death
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions: []
|
||||||
|
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: 1827226128182048838, guid: 52a2eb3158000934eb33267c807b8b15, type: 3}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
|
--- !u!1101 &5014557051428573475
|
||||||
|
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: IsDead
|
||||||
|
m_EventTreshold: 0
|
||||||
|
m_DstStateMachine: {fileID: 0}
|
||||||
|
m_DstState: {fileID: 1998880016702917871}
|
||||||
|
m_Solo: 0
|
||||||
|
m_Mute: 0
|
||||||
|
m_IsExit: 0
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TransitionDuration: 0.1
|
||||||
|
m_TransitionOffset: 0
|
||||||
|
m_ExitTime: 0.75
|
||||||
|
m_HasExitTime: 0
|
||||||
|
m_HasFixedDuration: 1
|
||||||
|
m_InterruptionSource: 0
|
||||||
|
m_OrderedInterruption: 1
|
||||||
|
m_CanTransitionToSelf: 0
|
||||||
|
--- !u!1107 &5460501127914818087
|
||||||
|
AnimatorStateMachine:
|
||||||
|
serializedVersion: 7
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Base Layer
|
||||||
|
m_ChildStates:
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: 8302574255206078140}
|
||||||
|
m_Position: {x: 200, y: 0, z: 0}
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: -8102868851794693864}
|
||||||
|
m_Position: {x: 235, y: 65, z: 0}
|
||||||
|
- serializedVersion: 1
|
||||||
|
m_State: {fileID: 1998880016702917871}
|
||||||
|
m_Position: {x: 270, y: 130, z: 0}
|
||||||
|
m_ChildStateMachines: []
|
||||||
|
m_AnyStateTransitions:
|
||||||
|
- {fileID: 5014557051428573475}
|
||||||
|
m_EntryTransitions: []
|
||||||
|
m_StateMachineTransitions: {}
|
||||||
|
m_StateMachineBehaviours: []
|
||||||
|
m_AnyStatePosition: {x: 50, y: 20, z: 0}
|
||||||
|
m_EntryPosition: {x: 50, y: 120, z: 0}
|
||||||
|
m_ExitPosition: {x: 800, y: 120, z: 0}
|
||||||
|
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
|
||||||
|
m_DefaultState: {fileID: 8302574255206078140}
|
||||||
|
--- !u!1102 &8302574255206078140
|
||||||
|
AnimatorState:
|
||||||
|
serializedVersion: 6
|
||||||
|
m_ObjectHideFlags: 1
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: Idle
|
||||||
|
m_Speed: 1
|
||||||
|
m_CycleOffset: 0
|
||||||
|
m_Transitions:
|
||||||
|
- {fileID: -1505565898738729052}
|
||||||
|
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: 1827226128182048838, guid: a4b8f2deec6a99945987cfc59b1b4e54, type: 3}
|
||||||
|
m_Tag:
|
||||||
|
m_SpeedParameter:
|
||||||
|
m_MirrorParameter:
|
||||||
|
m_CycleOffsetParameter:
|
||||||
|
m_TimeParameter:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: a5d5a67914ca2704aa7cb97b63d5ce28
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 9100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
%YAML 1.1
|
||||||
|
%TAG !u! tag:unity3d.com,2011:
|
||||||
|
--- !u!114 &-592490988042947487
|
||||||
|
MonoBehaviour:
|
||||||
|
m_ObjectHideFlags: 11
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_GameObject: {fileID: 0}
|
||||||
|
m_Enabled: 1
|
||||||
|
m_EditorHideFlags: 0
|
||||||
|
m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
|
||||||
|
m_Name:
|
||||||
|
m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
|
||||||
|
version: 10
|
||||||
|
--- !u!21 &2100000
|
||||||
|
Material:
|
||||||
|
serializedVersion: 8
|
||||||
|
m_ObjectHideFlags: 0
|
||||||
|
m_CorrespondingSourceObject: {fileID: 0}
|
||||||
|
m_PrefabInstance: {fileID: 0}
|
||||||
|
m_PrefabAsset: {fileID: 0}
|
||||||
|
m_Name: M_SpaceSoldier_Animated
|
||||||
|
m_Shader: {fileID: -6465566751694194690, guid: 32ea7846f763cb340950fafed798ecf6, type: 3}
|
||||||
|
m_Parent: {fileID: 0}
|
||||||
|
m_ModifiedSerializedProperties: 0
|
||||||
|
m_ValidKeywords:
|
||||||
|
- _USE_VERTEX_COLOR
|
||||||
|
m_InvalidKeywords: []
|
||||||
|
m_LightmapFlags: 2
|
||||||
|
m_EnableInstancingVariants: 0
|
||||||
|
m_DoubleSidedGI: 0
|
||||||
|
m_CustomRenderQueue: -1
|
||||||
|
stringTagMap: {}
|
||||||
|
disabledShaderPasses:
|
||||||
|
- MOTIONVECTORS
|
||||||
|
m_LockedProperties:
|
||||||
|
m_SavedProperties:
|
||||||
|
serializedVersion: 3
|
||||||
|
m_TexEnvs:
|
||||||
|
- _BaseColorMap:
|
||||||
|
m_Texture: {fileID: 2800000, guid: 936bffa65ef866a43969ea77c103adb3, type: 3}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _MaskMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- _NormalMap:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_Lightmaps:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_LightmapsInd:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
- unity_ShadowMasks:
|
||||||
|
m_Texture: {fileID: 0}
|
||||||
|
m_Scale: {x: 1, y: 1}
|
||||||
|
m_Offset: {x: 0, y: 0}
|
||||||
|
m_Ints: []
|
||||||
|
m_Floats:
|
||||||
|
- _DeformedMeshIndex: 0
|
||||||
|
- _Metallic: 0
|
||||||
|
- _QueueControl: 0
|
||||||
|
- _QueueOffset: 0
|
||||||
|
- _Smoothness: 0
|
||||||
|
- _USE_VERTEX_COLOR: 1
|
||||||
|
m_Colors:
|
||||||
|
- _BaseColor: {r: 1, g: 1, b: 1, a: 1}
|
||||||
|
- _DeformationParamsForMotionVectors: {r: 0, g: 0, b: 0, a: 0}
|
||||||
|
m_BuildTextureStacks: []
|
||||||
|
m_AllowLocking: 1
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 41a2f8334e8bf9741a3974e02657b107
|
||||||
|
NativeFormatImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
mainObjectFileID: 2100000
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,160 @@
|
|||||||
|
using ProjectM.Simulation;
|
||||||
|
using Rukhanka; // FastAnimatorParameter, AnimatorParametersAspect, ParameterValue,
|
||||||
|
// AnimatorControllerParameterComponent, AnimatorControllerParameterIndexTableComponent,
|
||||||
|
// RukhankaAnimationSystemGroup
|
||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode; // GhostOwnerIsLocal
|
||||||
|
using Unity.Transforms; // LocalTransform
|
||||||
|
using Unity.CharacterController; // KinematicCharacterBody
|
||||||
|
|
||||||
|
namespace ProjectM.Client
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Client-only animation driver. OBSERVES authoritative/replicated state and writes Rukhanka animator
|
||||||
|
/// blend params; never mutates the sim (presentation-only). Runs once/frame in the client/local world,
|
||||||
|
/// BEFORE Rukhanka evaluates the controller this frame (same-frame, no 1-tick lag). NOTE: this lands in
|
||||||
|
/// SimulationSystemGroup (via UpdateBefore the Rukhanka group, which itself has no UpdateInGroup), a
|
||||||
|
/// 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).
|
||||||
|
///
|
||||||
|
/// Two paths:
|
||||||
|
/// LOCAL (owner-predicted, GhostOwnerIsLocal ENABLED): realized CC RelativeVelocity (wall-aware).
|
||||||
|
/// REMOTE (interpolated, GhostOwnerIsLocal DISABLED): KinematicCharacterBody is NOT a [GhostField] and
|
||||||
|
/// 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
|
||||||
|
/// [GhostField] (valid on remotes); EffectiveCharacterStats.MoveSpeed is derived locally each
|
||||||
|
/// tick by StatRecomputeSystem (present on remotes). Cache prevPos per Entity, prune each frame.
|
||||||
|
/// </summary>
|
||||||
|
[WorldSystemFilter(WorldSystemFilterFlags.LocalSimulation | WorldSystemFilterFlags.ClientSimulation)]
|
||||||
|
[UpdateBefore(typeof(RukhankaAnimationSystemGroup))]
|
||||||
|
[RequireMatchingQueriesForUpdate]
|
||||||
|
public partial class PlayerAnimationDriveSystem : SystemBase
|
||||||
|
{
|
||||||
|
// Perfect-hash keys, built once (managed string ctor). Names MUST match AC_PlayerTopDown.controller
|
||||||
|
// parameter names exactly. Immutable readonly hashes -> domain-reload safe.
|
||||||
|
static readonly FastAnimatorParameter k_MoveX = new FastAnimatorParameter("MoveX");
|
||||||
|
static readonly FastAnimatorParameter k_MoveZ = new FastAnimatorParameter("MoveZ");
|
||||||
|
static readonly FastAnimatorParameter k_Speed = new FastAnimatorParameter("Speed");
|
||||||
|
static readonly FastAnimatorParameter k_IsDead = new FastAnimatorParameter("IsDead");
|
||||||
|
|
||||||
|
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||||
|
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||||
|
|
||||||
|
protected override void OnCreate()
|
||||||
|
{
|
||||||
|
_prevPos = new NativeParallelHashMap<Entity, float3>(16, Allocator.Persistent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDestroy()
|
||||||
|
{
|
||||||
|
if (_prevPos.IsCreated) _prevPos.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
float dt = SystemAPI.Time.DeltaTime; // wall-frame delta is correct for presentation
|
||||||
|
if (dt < 1e-5f) dt = 1e-5f;
|
||||||
|
|
||||||
|
// --- LOCAL owner (CC velocity) ---
|
||||||
|
var localJob = new LocalDriveJob
|
||||||
|
{
|
||||||
|
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||||
|
};
|
||||||
|
Dependency = localJob.ScheduleParallel(Dependency);
|
||||||
|
|
||||||
|
// --- REMOTE players (position-delta). Single-threaded write to the shared prevPos cache. ---
|
||||||
|
var seen = new NativeParallelHashSet<Entity>(16, Allocator.TempJob);
|
||||||
|
var remoteJob = new RemoteDriveJob
|
||||||
|
{
|
||||||
|
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||||
|
dt = dt,
|
||||||
|
prevPos = _prevPos,
|
||||||
|
seen = seen,
|
||||||
|
};
|
||||||
|
Dependency = remoteJob.Schedule(Dependency); // .Schedule (not parallel): mutates _prevPos
|
||||||
|
// Prune stale entries (despawned remotes) AFTER the job, on the main thread.
|
||||||
|
Dependency.Complete();
|
||||||
|
PruneCache(seen);
|
||||||
|
seen.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void PruneCache(NativeParallelHashSet<Entity> seen)
|
||||||
|
{
|
||||||
|
using var keys = _prevPos.GetKeyArray(Allocator.Temp);
|
||||||
|
for (int i = 0; i < keys.Length; i++)
|
||||||
|
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
||||||
|
// (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).
|
||||||
|
[BurstCompile]
|
||||||
|
[WithAll(typeof(GhostOwnerIsLocal))]
|
||||||
|
[WithPresent(typeof(Dead))]
|
||||||
|
partial struct LocalDriveJob : IJobEntity
|
||||||
|
{
|
||||||
|
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||||
|
|
||||||
|
void Execute(
|
||||||
|
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||||
|
DynamicBuffer<AnimatorControllerParameterComponent> parametersArr,
|
||||||
|
in PlayerFacing facing,
|
||||||
|
in EffectiveCharacterStats stats,
|
||||||
|
in KinematicCharacterBody body,
|
||||||
|
EnabledRefRO<Dead> dead)
|
||||||
|
{
|
||||||
|
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||||
|
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
|
||||||
|
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// REMOTE: GhostOwnerIsLocal DISABLED -> interpolated teammates. Velocity from LocalTransform.Position
|
||||||
|
// delta (KinematicCharacterBody.RelativeVelocity is baked-zero on remotes, non-GhostField, owner-only).
|
||||||
|
[BurstCompile]
|
||||||
|
[WithDisabled(typeof(GhostOwnerIsLocal))]
|
||||||
|
[WithPresent(typeof(Dead))]
|
||||||
|
partial struct RemoteDriveJob : IJobEntity
|
||||||
|
{
|
||||||
|
public FastAnimatorParameter moveX, moveZ, speed, isDead;
|
||||||
|
public float dt;
|
||||||
|
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||||
|
public NativeParallelHashSet<Entity> seen;
|
||||||
|
|
||||||
|
void Execute(
|
||||||
|
Entity e,
|
||||||
|
AnimatorControllerParameterIndexTableComponent indexTable,
|
||||||
|
DynamicBuffer<AnimatorControllerParameterComponent> parametersArr,
|
||||||
|
in LocalTransform xform,
|
||||||
|
in PlayerFacing facing,
|
||||||
|
in EffectiveCharacterStats stats,
|
||||||
|
EnabledRefRO<Dead> dead)
|
||||||
|
{
|
||||||
|
seen.Add(e);
|
||||||
|
float3 cur = xform.Position;
|
||||||
|
float3 vel = float3.zero;
|
||||||
|
if (prevPos.TryGetValue(e, out var prev))
|
||||||
|
vel = (cur - prev) / dt;
|
||||||
|
prevPos[e] = cur;
|
||||||
|
|
||||||
|
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||||
|
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
|
||||||
|
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||||
|
static void Write(ref AnimatorParametersAspect a, float3 p, bool isDeadVal,
|
||||||
|
FastAnimatorParameter moveX, FastAnimatorParameter moveZ,
|
||||||
|
FastAnimatorParameter speed, FastAnimatorParameter isDead)
|
||||||
|
{
|
||||||
|
if (a.HasParameter(moveX)) a.SetParameterValue(moveX, p.x);
|
||||||
|
if (a.HasParameter(moveZ)) a.SetParameterValue(moveZ, p.y);
|
||||||
|
if (a.HasParameter(speed)) a.SetParameterValue(speed, p.z);
|
||||||
|
if (a.HasParameter(isDead)) a.SetParameterValue(isDead, isDeadVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: ec2539089c1135e4cbe1f320a604119a
|
||||||
@@ -12,7 +12,11 @@
|
|||||||
"Unity.Entities.Graphics",
|
"Unity.Entities.Graphics",
|
||||||
"Unity.InputSystem",
|
"Unity.InputSystem",
|
||||||
"Unity.Networking.Transport",
|
"Unity.Networking.Transport",
|
||||||
"UnityEngine.UI"
|
"UnityEngine.UI",
|
||||||
|
"Unity.CharacterController",
|
||||||
|
"Unity.Physics",
|
||||||
|
"Rukhanka.Runtime",
|
||||||
|
"Rukhanka.Toolbox"
|
||||||
],
|
],
|
||||||
"includePlatforms": [],
|
"includePlatforms": [],
|
||||||
"excludePlatforms": [],
|
"excludePlatforms": [],
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace ProjectM.Server
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Animation is a CLIENT-ONLY presentation concern (Rukhanka netcode OFF — see DR-022). Rukhanka still
|
||||||
|
/// touches the SERVER world: its deformation systems use [WorldSystemFilter(Default)] (Default includes
|
||||||
|
/// ServerSimulation), so a headless server would run skinned-mesh prep + mesh deformation on the player's
|
||||||
|
/// baked bones/meshes that nothing renders. (Rukhanka's bootstrap creates RukhankaAnimationSystemGroup on
|
||||||
|
/// the server too, but leaves it EMPTY — it only fills the update list for client/local worlds.)
|
||||||
|
///
|
||||||
|
/// This server-only one-shot disables every Rukhanka.Runtime system in the server world. Disabling a
|
||||||
|
/// ComponentSystemGroup stops all its children (managed AND unmanaged ISystems), so this covers the whole
|
||||||
|
/// Rukhanka stack — the server then does ZERO animation/deformation work. Matched by assembly name (no
|
||||||
|
/// hard Rukhanka type ref → no asmdef change), so it survives Rukhanka updates.
|
||||||
|
/// </summary>
|
||||||
|
[WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)]
|
||||||
|
public partial class ServerStripAnimationSystem : SystemBase
|
||||||
|
{
|
||||||
|
protected override void OnUpdate()
|
||||||
|
{
|
||||||
|
int disabled = 0;
|
||||||
|
foreach (var sys in World.Systems)
|
||||||
|
{
|
||||||
|
if (sys == null || !sys.Enabled) continue;
|
||||||
|
if (sys.GetType().Assembly.GetName().Name == "Rukhanka.Runtime")
|
||||||
|
{
|
||||||
|
sys.Enabled = false;
|
||||||
|
disabled++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Debug.Log($"[ProjectM] ServerStripAnimationSystem: disabled {disabled} Rukhanka systems on the server world (animation is client-only).");
|
||||||
|
Enabled = false; // one-shot
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: eb468a67572121749b2ca104dbe8c476
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure mapping from authoritative planar velocity + facing to client animation blend params.
|
||||||
|
/// Forward (along facing) -> MoveZ+, right of facing -> MoveX+ (matches the slim controller's 2D
|
||||||
|
/// strafe tree: +X=right, +Z=forward). EditMode-tested, no World needed. Burst-safe (no enums/strings).
|
||||||
|
/// </summary>
|
||||||
|
public static class AnimParamMath
|
||||||
|
{
|
||||||
|
/// <param name="planarVelocity">World velocity (KinematicCharacterBody.RelativeVelocity for the owner,
|
||||||
|
/// or LocalTransform position-delta/dt for remotes). Y is ignored.</param>
|
||||||
|
/// <param name="facing">Normalized world planar facing (PlayerFacing.Direction, world XZ as x,y). May be zero.</param>
|
||||||
|
/// <param name="maxSpeed">EffectiveCharacterStats.MoveSpeed (normalizer; guarded > 0).</param>
|
||||||
|
/// <returns>x = MoveX (strafe, -1..1), y = MoveZ (fwd/back, -1..1), z = Speed (0..1).</returns>
|
||||||
|
public static float3 LocomotionParams(float3 planarVelocity, float2 facing, float maxSpeed)
|
||||||
|
{
|
||||||
|
float2 vWorld = planarVelocity.xz;
|
||||||
|
float safeMax = math.max(maxSpeed, 1e-4f);
|
||||||
|
float speed = math.saturate(math.length(vWorld) / safeMax);
|
||||||
|
|
||||||
|
// Degenerate facing -> default to world +Z so the basis is well-defined.
|
||||||
|
float2 fwd = math.lengthsq(facing) > 1e-6f ? math.normalize(facing) : new float2(0f, 1f);
|
||||||
|
float2 right = new float2(fwd.y, -fwd.x); // 90deg clockwise in XZ (right-hand of forward)
|
||||||
|
|
||||||
|
float2 local = new float2(math.dot(vWorld, right), math.dot(vWorld, fwd)) / safeMax;
|
||||||
|
local = math.clamp(local, -1f, 1f);
|
||||||
|
return new float3(local.x, local.y, speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 89e6fd18a1754b443afa2296d68033b2
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 28913e196c8f57448843676e6ea3ba26
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 5decd9bf356b14449a7845d74d676419
|
||||||
|
folderAsset: yes
|
||||||
|
DefaultImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 32ea7846f763cb340950fafed798ecf6
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||||
|
AssetOrigin:
|
||||||
|
serializedVersion: 1
|
||||||
|
productId: 298480
|
||||||
|
packageName: Rukhanka Animation System 2
|
||||||
|
packageVersion: 2.9.0
|
||||||
|
assetPath: Packages/com.rukhanka.animation/Samples~/Samples/Shaders/AnimatedLitShader.shadergraph
|
||||||
|
uploadId: 897522
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4e6c707d4d6a4d94e86a4c9cf4f80dd3
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||||
|
AssetOrigin:
|
||||||
|
serializedVersion: 1
|
||||||
|
productId: 298480
|
||||||
|
packageName: Rukhanka Animation System 2
|
||||||
|
packageVersion: 2.9.0
|
||||||
|
assetPath: Packages/com.rukhanka.animation/Samples~/Samples/Shaders/GPUAttachmentLitShader.shadergraph
|
||||||
|
uploadId: 897522
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 9a04d2f9faf6a1e46b1f185bc64f118f
|
||||||
|
ScriptedImporter:
|
||||||
|
internalIDToNameTable: []
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
||||||
|
script: {fileID: 11500000, guid: 625f186215c104763be7675aa2d941aa, type: 3}
|
||||||
|
AssetOrigin:
|
||||||
|
serializedVersion: 1
|
||||||
|
productId: 298480
|
||||||
|
packageName: Rukhanka Animation System 2
|
||||||
|
packageVersion: 2.9.0
|
||||||
|
assetPath: Packages/com.rukhanka.animation/Samples~/Samples/Shaders/Lit URP.shadergraph
|
||||||
|
uploadId: 897522
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
using NUnit.Framework;
|
||||||
|
using ProjectM.Simulation;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Tests
|
||||||
|
{
|
||||||
|
public class AnimParamMathTests
|
||||||
|
{
|
||||||
|
const float Eps = 1e-3f;
|
||||||
|
static readonly float2 Fwd = new float2(0f, 1f); // facing world +Z
|
||||||
|
const float Max = 5f;
|
||||||
|
|
||||||
|
[Test] public void ZeroVelocity_AllZero()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(float3.zero, Fwd, Max);
|
||||||
|
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(0f, r.y, Eps); Assert.AreEqual(0f, r.z, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void FullForward_MoveZ1_Speed1()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max), Fwd, Max);
|
||||||
|
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(1f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void FullBackward_MoveZNeg1()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, -Max), Fwd, Max);
|
||||||
|
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(-1f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void StrafeRight_MoveX1()
|
||||||
|
{
|
||||||
|
// facing +Z -> right = (+X). world velocity +X -> MoveX = +1.
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(Max, 0f, 0f), Fwd, Max);
|
||||||
|
Assert.AreEqual(1f, r.x, Eps); Assert.AreEqual(0f, r.y, Eps); Assert.AreEqual(1f, r.z, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void StrafeLeft_MoveXNeg1()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(-Max, 0f, 0f), Fwd, Max);
|
||||||
|
Assert.AreEqual(-1f, r.x, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void HalfForward_HalfSpeed()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max * 0.5f), Fwd, Max);
|
||||||
|
Assert.AreEqual(0.5f, r.y, Eps); Assert.AreEqual(0.5f, r.z, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void OverMax_SpeedClampsTo1()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max * 3f), Fwd, Max);
|
||||||
|
Assert.AreEqual(1f, r.z, Eps); Assert.AreEqual(1f, r.y, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void RotatedFacing_ProjectsIntoFrame()
|
||||||
|
{
|
||||||
|
// facing world +X; velocity world +X should read as forward (MoveZ+), not strafe.
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(Max, 0f, 0f), new float2(1f, 0f), Max);
|
||||||
|
Assert.AreEqual(0f, r.x, Eps); Assert.AreEqual(1f, r.y, Eps);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void DegenerateFacing_DefaultsToWorldForward()
|
||||||
|
{
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, Max), float2.zero, Max);
|
||||||
|
Assert.AreEqual(1f, r.y, Eps); // treated as facing +Z
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test] public void ZeroMaxSpeed_NoNaN_NoDivByZero()
|
||||||
|
{
|
||||||
|
// guard: safeMax = max(maxSpeed, 1e-4) -> finite output even with maxSpeed 0.
|
||||||
|
var r = AnimParamMath.LocomotionParams(new float3(0f, 0f, 1f), Fwd, 0f);
|
||||||
|
Assert.IsFalse(float.IsNaN(r.x) || float.IsNaN(r.y) || float.IsNaN(r.z));
|
||||||
|
Assert.AreEqual(1f, r.z, Eps); // clamps to 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: f861fc391a6fa9e4ab9293cd489f9df1
|
||||||
Reference in New Issue
Block a user