using System; using Unity.Animation; using Unity.Collections; using Unity.Entities; using Unity.Jobs; using Unity.DataFlowGraph; using Unity.Sample.Core; using Unity.Mathematics; using UnityEngine; public class AnimSourceStand { public struct Settings : IComponentData { public BlobAssetReference StandClip; public BlobAssetReference TurnLeftClip; public BlobAssetReference TurnRightClip; public BlobAssetReference StandAimLeftClip; public BlobAssetReference StandAimMidClip; public BlobAssetReference StandAimRightClip; public BlobAssetReference AdditiveRefPoseClip; public float animTurnAngle; public float aimTurnLocalThreshold; public float turnSpeed; public float turnThreshold; public float turnTransitionSpeed; public float aimDuringReloadPitch; public float aimDuringReloadYaw; } public struct SystemState : ISystemStateComponentData { public static SystemState Default => new SystemState(); public NodeHandle Idle; public NodeHandle DeltaTimeNode; public NodeHandle TimeCounterNode; public NodeHandle TurnL; public NodeHandle TurnR; public NodeHandle AimLeft; public NodeHandle AimMid; public NodeHandle AimRight; public NodeHandle AdditiveRefPose; public NodeHandle AimLeftDelta; public NodeHandle AimMidDelta; public NodeHandle AimRightDelta; public NodeHandle MixerLeft; public NodeHandle MixerRight; public NodeHandle AimMixer; public float AimDuration; public float TurnDuration; } [UpdateInGroup(typeof(AnimSourceInitializationGroup))] [DisableAutoCreation] [AlwaysSynchronizeSystem] class InitSystem : JobComponentSystem { AnimationGraphSystem m_AnimationGraphSystem; protected override void OnCreate() { base.OnCreate(); m_AnimationGraphSystem = World.GetExistingSystem(); m_AnimationGraphSystem.AddRef(); } protected override void OnDestroy() { base.OnDestroy(); var cmdBuffer = new EntityCommandBuffer(Allocator.Temp); var animationGraphSystem = m_AnimationGraphSystem; Entities .WithoutBurst() // Can be removed once NodeSets are Burst-friendly .ForEach((Entity entity, ref SystemState state) => { Deinitialize(World, cmdBuffer, entity, animationGraphSystem, state); }).Run(); cmdBuffer.Dispose(); m_AnimationGraphSystem.RemoveRef(); } protected override JobHandle OnUpdate(JobHandle inputDeps) { inputDeps.Complete(); var nodeSet = m_AnimationGraphSystem.Set; var commands = new EntityCommandBuffer(Allocator.TempJob); var animationGraphSystem = m_AnimationGraphSystem; // Handle created entities Entities .WithoutBurst() // Can be removed once NodeSets are Burst-friendly .WithNone() .ForEach((Entity entity, ref AnimSource.Data animSource, ref Settings settings) => { GameDebug.Log(World,AnimSource.ShowLifetime,"Init Stand entity:{0} state entity:{1}", entity, animSource.animStateEntity); var state = SystemState.Default; state.Idle = AnimationGraphHelper.CreateNode(animationGraphSystem, "Idle"); state.DeltaTimeNode = AnimationGraphHelper.CreateNode(animationGraphSystem, "DeltaTimeNode"); state.TimeCounterNode = AnimationGraphHelper.CreateNode(animationGraphSystem, "TimeCounterNode"); state.TurnL = AnimationGraphHelper.CreateNode(animationGraphSystem, "TurnL"); state.TurnR = AnimationGraphHelper.CreateNode(animationGraphSystem, "TurnR"); state.AimLeft = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimLeft"); state.AimMid = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimMid"); state.AimRight = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimRight"); state.AdditiveRefPose = AnimationGraphHelper.CreateNode(animationGraphSystem, "AdditiveRefPose"); state.AimLeftDelta = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimLeftDelta"); state.AimMidDelta = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimMidDelta"); state.AimRightDelta = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimRightDelta"); state.MixerLeft = AnimationGraphHelper.CreateNode(animationGraphSystem, "MixerLeft"); state.MixerRight = AnimationGraphHelper.CreateNode(animationGraphSystem, "MixerRight"); state.AimMixer = AnimationGraphHelper.CreateNode(animationGraphSystem, "AimMixer"); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput0, 1f); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.BlendModeInput1, BlendingMode.Additive); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.BlendModeInput2, BlendingMode.Additive); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.BlendModeInput3, BlendingMode.Additive); nodeSet.SendMessage(state.Idle, UberClipNode.SimulationPorts.Configuration, new ClipConfiguration { Mask = (int)ClipConfigurationMask.LoopTime }); // TODO: Convert to using cascade mixer or blend space? nodeSet.Connect(state.DeltaTimeNode, DeltaTimeNode.KernelPorts.DeltaTime, state.TimeCounterNode, TimeCounterNode.KernelPorts.DeltaTime); nodeSet.Connect(state.TimeCounterNode, TimeCounterNode.KernelPorts.Time, state.Idle, UberClipNode.KernelPorts.Time); nodeSet.Connect(state.TurnL, ClipNode.KernelPorts.Output, state.MixerLeft , MixerNode.KernelPorts.Input0); nodeSet.Connect(state.Idle, UberClipNode.KernelPorts.Output, state.MixerLeft , MixerNode.KernelPorts.Input1); nodeSet.Connect(state.MixerLeft , MixerNode.KernelPorts.Output, state.MixerRight, MixerNode.KernelPorts.Input0); nodeSet.Connect(state.TurnR, ClipNode.KernelPorts.Output, state.MixerRight, MixerNode.KernelPorts.Input1); nodeSet.Connect(state.MixerRight, MixerNode.KernelPorts.Output, state.AimMixer, LayerMixerNode.KernelPorts.Input0); nodeSet.Connect(state.AimLeft, ClipNode.KernelPorts.Output, state.AimLeftDelta, DeltaNode.KernelPorts.Input); nodeSet.Connect(state.AimMid, ClipNode.KernelPorts.Output, state.AimMidDelta, DeltaNode.KernelPorts.Input); nodeSet.Connect(state.AimRight, ClipNode.KernelPorts.Output, state.AimRightDelta, DeltaNode.KernelPorts.Input); nodeSet.Connect(state.AdditiveRefPose, ClipNode.KernelPorts.Output, state.AimLeftDelta, DeltaNode.KernelPorts.Subtract); nodeSet.Connect(state.AdditiveRefPose, ClipNode.KernelPorts.Output, state.AimMidDelta, DeltaNode.KernelPorts.Subtract); nodeSet.Connect(state.AdditiveRefPose, ClipNode.KernelPorts.Output, state.AimRightDelta, DeltaNode.KernelPorts.Subtract); nodeSet.Connect(state.AimLeftDelta, DeltaNode.KernelPorts.Output, state.AimMixer, LayerMixerNode.KernelPorts.Input1); nodeSet.Connect(state.AimMidDelta, DeltaNode.KernelPorts.Output, state.AimMixer, LayerMixerNode.KernelPorts.Input2); nodeSet.Connect(state.AimRightDelta, DeltaNode.KernelPorts.Output, state.AimMixer, LayerMixerNode.KernelPorts.Input3); // TODO: Use pr. clip values (turn direction)? state.TurnDuration = settings.TurnLeftClip.Value.Duration; state.AimDuration = settings.StandAimLeftClip.Value.Duration; // Setup transition data var numMixerPorts = 3; for (var i = 0; i < numMixerPorts; i++) { var transitionWeights = EntityManager.GetBuffer(entity); transitionWeights.Add(new SimpleTransition.PortWeights { Value = 1f }); } // Expose input and outputs animSource.outputNode = state.AimMixer; animSource.outputPortID = (OutputPortID)LayerMixerNode.KernelPorts.Output; commands.AddComponent(entity, state); }).Run(); Entities .WithoutBurst() // Can be removed once NodeSets are Burst-friendly .WithNone() .ForEach((Entity entity, ref SystemState state) => { Deinitialize(World, commands, entity, animationGraphSystem, state); }).Run(); commands.Playback(EntityManager); commands.Dispose(); return default; } static void Deinitialize(World world, EntityCommandBuffer cmdBuffer, Entity entity, AnimationGraphSystem animGraphSys, SystemState state) { GameDebug.Log(world,AnimSource.ShowLifetime,"Deinit Stand entity:{0}", entity); AnimationGraphHelper.DestroyNode(animGraphSys,state.Idle); AnimationGraphHelper.DestroyNode(animGraphSys,state.DeltaTimeNode); AnimationGraphHelper.DestroyNode(animGraphSys,state.TimeCounterNode); AnimationGraphHelper.DestroyNode(animGraphSys,state.TurnL); AnimationGraphHelper.DestroyNode(animGraphSys,state.TurnR); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimLeft); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimMid); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimRight); AnimationGraphHelper.DestroyNode(animGraphSys,state.AdditiveRefPose); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimLeftDelta); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimMidDelta); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimRightDelta); AnimationGraphHelper.DestroyNode(animGraphSys,state.MixerLeft); AnimationGraphHelper.DestroyNode(animGraphSys,state.MixerRight); AnimationGraphHelper.DestroyNode(animGraphSys,state.AimMixer); cmdBuffer.RemoveComponent(entity, typeof(SystemState)); } } [UpdateInGroup(typeof(AnimSourceUpdateBGroup))] [DisableAutoCreation] [AlwaysSynchronizeSystem] class UpdateSystem : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { inputDeps.Complete(); // TODO (mogensh) find cleaner way to get time var globalTime = GetEntityQuery(ComponentType.ReadOnly()).GetSingleton(); var deltaTime = globalTime.frameDuration; var characterInterpolatedDataFromEntity = GetComponentDataFromEntity(false); // Update state Entities .ForEach((Entity entity, ref AnimSource.Data animSource, ref Settings settings, ref SystemState state, ref AnimSource.AllowWrite allowWrite) => { if (!characterInterpolatedDataFromEntity.HasComponent(animSource.animStateEntity)) { //GameDebug.LogWarning(World,"AnimSource does not have Character.InterpolatedData components. Has it been deleted?"); return; } var charInterpolatedState = characterInterpolatedDataFromEntity[animSource.animStateEntity]; if (allowWrite.FirstUpdate) { charInterpolatedState.turnDirection = 0; charInterpolatedState.rotation = charInterpolatedState.aimYaw; charInterpolatedState.turnStartAngle = charInterpolatedState.aimYaw; allowWrite.FirstUpdate = false; } var aimYawLocal = MathHelper.DeltaAngle(charInterpolatedState.rotation, charInterpolatedState.aimYaw); var absAimYawLocal = math.abs(aimYawLocal); // Non turning update if (charInterpolatedState.turnDirection == 0) { // Test for local yaw angle exeding threshold so we need to turn if (absAimYawLocal > settings.aimTurnLocalThreshold) // TODO: Document why we need local vs non local turn tolerances? { charInterpolatedState.turnStartAngle = charInterpolatedState.rotation; charInterpolatedState.turnDirection = (short)(aimYawLocal >= 0F ? 1F : -1F); } } else { var rotateAngleRemaining = MathHelper.DeltaAngle(charInterpolatedState.rotation, charInterpolatedState.turnStartAngle) + settings.animTurnAngle * charInterpolatedState.turnDirection; if (rotateAngleRemaining * charInterpolatedState.turnDirection <= 0) { charInterpolatedState.turnDirection = 0; } else { var turnSpeed = settings.turnSpeed; if (absAimYawLocal > settings.turnThreshold) { var factor = 1.0f - (180 - absAimYawLocal) / settings.turnThreshold; turnSpeed = turnSpeed + factor * 300; } var deltaAngle = deltaTime * turnSpeed; var absAngleRemaining = math.abs(rotateAngleRemaining); if (deltaAngle > absAngleRemaining) { deltaAngle = absAngleRemaining; } // TODO: Is regular sign that returns 0 for value 0 ok here? var sign = (rotateAngleRemaining >= 0F ? 1F : -1F); charInterpolatedState.rotation += sign * deltaAngle; while (charInterpolatedState.rotation > 360.0f) charInterpolatedState.rotation -= 360.0f; while (charInterpolatedState.rotation < 0.0f) charInterpolatedState.rotation += 360.0f; } } characterInterpolatedDataFromEntity[animSource.animStateEntity] = charInterpolatedState; }).Run(); return default; } } [UpdateInGroup(typeof(AnimSourceApplyGroup))] [DisableAutoCreation] [AlwaysSynchronizeSystem] class PrepareGraph : JobComponentSystem { protected override JobHandle OnUpdate(JobHandle inputDeps) { inputDeps.Complete(); var nodeSet = World.GetExistingSystem().Set; var globalTime = GetEntityQuery(ComponentType.ReadOnly()).GetSingleton(); var deltaTime = globalTime.frameDuration; var cmdBuffer = new EntityCommandBuffer(Allocator.TempJob); // Handle rig changes Entities .WithNone() .WithoutBurst() // Can be removed once NodeSets are Burst-friendly .ForEach((Entity entity, ref AnimSource.Data animSource, ref Settings settings, ref SystemState state) => { if (!EntityManager.HasComponent(animSource.animStateEntity)) return; var sharedRigDef = EntityManager.GetSharedComponentData(animSource.animStateEntity); var rig = sharedRigDef.Value; var standClipInstance = ClipManager.Instance.GetClipFor(rig, settings.StandClip); var turnLeftClipInstance = ClipManager.Instance.GetClipFor(rig, settings.TurnLeftClip); var turnRightClipInstance = ClipManager.Instance.GetClipFor(rig, settings.TurnRightClip); var standAimLeftClipInstance = ClipManager.Instance.GetClipFor(rig, settings.StandAimLeftClip); var standAimMidClipInstance = ClipManager.Instance.GetClipFor(rig, settings.StandAimMidClip); var standAimRightClipInstance = ClipManager.Instance.GetClipFor(rig, settings.StandAimRightClip); var addRefPoseClipInstance = ClipManager.Instance.GetClipFor(rig, settings.AdditiveRefPoseClip); nodeSet.SendMessage(state.Idle, UberClipNode.SimulationPorts.ClipInstance, standClipInstance); nodeSet.SendMessage(state.TurnL, ClipNode.SimulationPorts.ClipInstance, turnLeftClipInstance); nodeSet.SendMessage(state.TurnR, ClipNode.SimulationPorts.ClipInstance, turnRightClipInstance); nodeSet.SendMessage(state.AimLeft, ClipNode.SimulationPorts.ClipInstance, standAimLeftClipInstance); nodeSet.SendMessage(state.AimMid, ClipNode.SimulationPorts.ClipInstance, standAimMidClipInstance); nodeSet.SendMessage(state.AimRight, ClipNode.SimulationPorts.ClipInstance, standAimRightClipInstance); nodeSet.SendMessage(state.AdditiveRefPose, ClipNode.SimulationPorts.ClipInstance, addRefPoseClipInstance); nodeSet.SendMessage(state.AimLeftDelta, DeltaNode.SimulationPorts.RigDefinition, rig); nodeSet.SendMessage(state.AimMidDelta, DeltaNode.SimulationPorts.RigDefinition, rig); nodeSet.SendMessage(state.AimRightDelta, DeltaNode.SimulationPorts.RigDefinition, rig); nodeSet.SendMessage(state.MixerLeft, MixerNode.SimulationPorts.RigDefinition, rig); nodeSet.SendMessage(state.MixerRight, MixerNode.SimulationPorts.RigDefinition, rig); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.RigDefinition, rig); cmdBuffer.AddComponent(entity); }).Run(); // Apply state Entities .WithoutBurst() // Can be removed once NodeSets are Burst-friendly .ForEach((Entity entity, ref AnimSource.Data animSource, ref Settings settings, ref SystemState state) => { if (!EntityManager.HasComponent(animSource.animStateEntity)) { return; } var charInterpolatedState = EntityManager.GetComponentData(animSource.animStateEntity); // Handle turning float rotateAngleRemaining = 0f; if (charInterpolatedState.turnDirection != 0) rotateAngleRemaining = MathHelper.DeltaAngle(charInterpolatedState.rotation, charInterpolatedState.turnStartAngle) + settings.animTurnAngle * charInterpolatedState.turnDirection; var portWeights = EntityManager.GetBuffer(entity).AsNativeArray(); if (charInterpolatedState.turnDirection == 0) { SimpleTransition.Update((int)LocoMixerPort.Idle, settings.turnTransitionSpeed, deltaTime, ref portWeights); } else { var fraction = 1f - math.abs(rotateAngleRemaining / settings.animTurnAngle); var anim = (charInterpolatedState.turnDirection == -1) ? state.TurnL : state.TurnR; var mixerPort = (charInterpolatedState.turnDirection == -1) ? (int)LocoMixerPort.TurnL : (int)LocoMixerPort.TurnR; SimpleTransition.Update(mixerPort, settings.turnTransitionSpeed, deltaTime, ref portWeights); nodeSet.SetData(anim, ClipNode.KernelPorts.Time, state.TurnDuration * fraction); // Reset the time of the idle, so it's reset when we transition back if (portWeights[(int)LocoMixerPort.Idle].Value < 0.01f) nodeSet.SendMessage(state.TimeCounterNode, TimeCounterNode.SimulationPorts.Time, 0f); } var idleWeight = portWeights[(int)LocoMixerPort.Idle].Value; nodeSet.SendMessage(state.MixerLeft, MixerNode.SimulationPorts.Blend, idleWeight); var turnRightWeight = portWeights[(int)LocoMixerPort.TurnR].Value; nodeSet.SendMessage(state.MixerRight, MixerNode.SimulationPorts.Blend, turnRightWeight); // Handle Aim var aimMultiplier = 1f - charInterpolatedState.blendOutAim; float aimPitchFraction = charInterpolatedState.aimPitch / 180.0f; // var aimPitchMult = Mathf.Lerp(m_template.blendOutAimOnReloadPitch, 1f, aimMultiplier); var aimPitchMult = math.lerp(settings.aimDuringReloadPitch, 1f, aimMultiplier); aimPitchFraction = math.lerp(0.5f, aimPitchFraction, aimPitchMult); float aimTime = aimPitchFraction * state.AimDuration; nodeSet.SetData(state.AimLeft, ClipNode.KernelPorts.Time, aimTime); nodeSet.SetData(state.AimMid, ClipNode.KernelPorts.Time, aimTime); nodeSet.SetData(state.AimRight, ClipNode.KernelPorts.Time, aimTime); float aimYawLocal = MathHelper.DeltaAngle(charInterpolatedState.rotation, charInterpolatedState.aimYaw); // float aimYawFraction = Mathf.Abs(aimYawLocal / settings.aimYawAngle); float aimYawFraction = math.abs(aimYawLocal / 90f); var aimYawMult = math.lerp(settings.aimDuringReloadYaw, 1f, aimMultiplier); aimYawFraction = math.lerp(0.0f, aimYawFraction, aimYawMult); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput2, 1.0f - aimYawFraction); if (aimYawLocal < 0) { nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput1, aimYawFraction); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput3, 0.0f); } else { nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput1, 0.0f); nodeSet.SendMessage(state.AimMixer, LayerMixerNode.SimulationPorts.WeightInput3, aimYawFraction); } }).Run(); cmdBuffer.Playback(EntityManager); cmdBuffer.Dispose(); return default; } } public struct SimpleTransition { public struct PortWeights : IBufferElementData { public float Value; } public static void Update(int activePort, float blendVelocity, float deltaTime, ref NativeArray portWeights) { // Update current state weight float weight = portWeights[activePort].Value; if (weight != 1.0f) { weight = math.clamp(weight + blendVelocity * deltaTime, 0, 1); portWeights[activePort] = new PortWeights { Value = weight }; } // Adjust weight of other states and ensure total weight is 1 float weightLeft = 1.0f - weight; float totalWeight = 0; for (var i = 0; i < portWeights.Length; i++) { if (i == activePort) continue; totalWeight += portWeights[i].Value; } if (totalWeight == 0) return; float fraction = weightLeft / totalWeight; for (int i = 0; i < portWeights.Length; i++) { if (i == activePort) continue; float w = portWeights[i].Value; w = w * fraction; portWeights[i] = new PortWeights { Value = w }; } } } enum LocoMixerPort { TurnL, Idle, TurnR, Count } }