Files
jelito/Assets/jelycho/Code/Player/PlayerAnimator.cs
2025-09-24 12:13:56 +02:00

358 lines
12 KiB
C#

using System;
using System.Runtime.CompilerServices;
using RebootKit.Engine.Animations;
using RebootReality.jelycho.Items;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Events;
using UnityEngine.Playables;
namespace RebootReality.jelycho.Player {
public struct PlayerLocomotionAnimatorParams {
public float VelocityForwardNormalized;
public float VelocityRightNormalized;
public float TurnVelocity;
public bool IsGrounded;
}
[Serializable]
public class BasicCharacterLocomotionReAnimatorNode : IReAnimatorNode {
[field: SerializeField] public string Name { get; private set; }
[SerializeField] AnimationClip m_IdleClip;
[SerializeField] AnimationClip m_RunForwardClip;
[SerializeField] AnimationClip m_RunBackwardsClip;
[SerializeField] AnimationClip m_StrafeRightClip;
[SerializeField] AnimationClip m_StrafeLeftClip;
[SerializeField] AnimationClip m_TurnRightClip;
[SerializeField] AnimationClip m_TurnLeftClip;
[SerializeField] float m_TransitionSpeed = 5.0f;
[SerializeField, Range(0.0f, 1.0f)] float m_ForceIdleMagnitudeThreshold = 0.2f;
AnimationMixerPlayable m_Mixer;
float2 m_TargetInput;
float2 m_CurrentInput;
float m_Turning;
public void Tick(float deltaTime) {
if (m_TransitionSpeed > 0.0f) {
m_CurrentInput = Vector2.MoveTowards(m_CurrentInput,
m_TargetInput,
m_TransitionSpeed * deltaTime);
} else {
m_CurrentInput = m_TargetInput;
}
if (math.length(m_CurrentInput) <= m_ForceIdleMagnitudeThreshold) {
for (int i = 0; i < 7; ++i) {
m_Mixer.SetInputWeight(i, 0.0f);
}
float turnWeight = math.clamp(math.abs(m_Turning), 0.0f, 1.0f);
if (m_Turning > 0.1f) {
m_Mixer.SetInputWeight(5, turnWeight);
} else if (m_Turning < -0.1f) {
m_Mixer.SetInputWeight(6, turnWeight);
}
m_Mixer.SetInputWeight(0, 1.0f - turnWeight);
return;
}
float inputMagnitude = math.length(m_CurrentInput);
float2 inputNormalized = math.normalizesafe(m_CurrentInput);
inputMagnitude = math.min(1.0f, inputMagnitude);
float forwardWeight = math.max(0.0f, math.dot(inputNormalized, new float2(0, 1)) * inputMagnitude);
float backwardsWeight = math.max(0.0f, math.dot(inputNormalized, new float2(0, -1)) * inputMagnitude);
float rightWeight = math.max(0.0f, math.dot(inputNormalized, new float2(1, 0)) * inputMagnitude);
float leftWeight = math.max(0.0f, math.dot(inputNormalized, new float2(-1, 0)) * inputMagnitude);
float totalWeight = forwardWeight + backwardsWeight + rightWeight + leftWeight;
if (totalWeight > 1.0f) {
forwardWeight /= totalWeight;
backwardsWeight /= totalWeight;
rightWeight /= totalWeight;
leftWeight /= totalWeight;
totalWeight = 1.0f;
}
float idleWeight = math.max(0.0f, 1.0f - totalWeight);
m_Mixer.SetInputWeight(0, idleWeight);
m_Mixer.SetInputWeight(1, forwardWeight);
m_Mixer.SetInputWeight(2, backwardsWeight);
m_Mixer.SetInputWeight(3, rightWeight);
m_Mixer.SetInputWeight(4, leftWeight);
m_Mixer.SetInputWeight(5, 0.0f);
m_Mixer.SetInputWeight(6, 0.0f);
}
public IPlayable Build(PlayableGraph graph) {
m_Mixer = AnimationMixerPlayable.Create(graph, 7);
var idlePlayable = AnimationClipPlayable.Create(graph, m_IdleClip);
var runForwardPlayable = AnimationClipPlayable.Create(graph, m_RunForwardClip);
var runBackwardsPlayable = AnimationClipPlayable.Create(graph, m_RunBackwardsClip);
var strafeRightPlayable = AnimationClipPlayable.Create(graph, m_StrafeRightClip);
var strafeLeftPlayable = AnimationClipPlayable.Create(graph, m_StrafeLeftClip);
var turnRightPlayable = AnimationClipPlayable.Create(graph, m_TurnRightClip);
var turnLeftPlayable = AnimationClipPlayable.Create(graph, m_TurnLeftClip);
m_Mixer.ConnectInput(0, idlePlayable, 0, 1.0f);
m_Mixer.ConnectInput(1, runForwardPlayable, 0, 0.0f);
m_Mixer.ConnectInput(2, runBackwardsPlayable, 0, 0.0f);
m_Mixer.ConnectInput(3, strafeRightPlayable, 0, 0.0f);
m_Mixer.ConnectInput(4, strafeLeftPlayable, 0, 0.0f);
m_Mixer.ConnectInput(5, turnRightPlayable, 0, 0.0f);
m_Mixer.ConnectInput(6, turnLeftPlayable, 0, 0.0f);
return m_Mixer;
}
public bool TryFindChild(string name, out IReAnimatorNode node) {
node = null;
return false;
}
public void SetInput(float2 input, float turning) {
m_TargetInput = input;
m_Turning = turning;
}
}
public class CharacterHandsReAnimatorNode : IReAnimatorNode {
enum State {
None,
Idle,
QuickAttack,
Charging,
ChargedIdle,
Charged,
ChargedUse
}
[field: SerializeField] public string Name { get; private set; }
PlayableGraph m_Graph;
AnimationMixerPlayable m_Mixer;
AnimationClipPlayable m_CurrentPlayable;
ItemHandsAnimationClipsSet m_ClipsSet;
State m_State;
public event Action OnQuickAttackAnimationFinished = delegate { };
public event Action OnCharged = delegate { };
public void Tick(float deltaTime) {
switch (m_State) {
case State.QuickAttack: {
if (IsCurrentClipFinished() && m_CurrentPlayable.GetPlayState() == PlayState.Playing) {
m_CurrentPlayable.Pause();
OnQuickAttackAnimationFinished?.Invoke();
}
break;
}
case State.Charging: {
if (IsCurrentClipFinished()) {
SetChargedIdle();
OnCharged?.Invoke();
}
break;
}
case State.ChargedUse: {
if (IsCurrentClipFinished()) {
SetIdle();
}
break;
}
}
}
public IPlayable Build(PlayableGraph graph) {
m_Graph = graph;
m_Mixer = AnimationMixerPlayable.Create(graph, 1);
return m_Mixer;
}
public bool TryFindChild(string name, out IReAnimatorNode node) {
node = null;
return false;
}
public void UpdateClips(ItemHandsAnimationClipsSet clipsSet) {
m_ClipsSet = clipsSet;
if (clipsSet == null) {
m_State = State.None;
return;
}
SetIdle();
}
public void PlayQuickAttack(int combo) {
if (m_ClipsSet == null) {
return;
}
AnimationClip clip = m_ClipsSet.quickAttacks[combo % m_ClipsSet.quickAttacks.Length];
m_CurrentPlayable = AnimationClipPlayable.Create(m_Graph, clip);
m_Mixer.DisconnectInput(0);
m_Mixer.ConnectInput(0, m_CurrentPlayable, 0, 1.0f);
m_State = State.QuickAttack;
m_CurrentPlayable.Play();
}
public void SetIdle() {
if (m_ClipsSet == null) {
return;
}
m_CurrentPlayable = AnimationClipPlayable.Create(m_Graph, m_ClipsSet.idle);
m_Mixer.DisconnectInput(0);
m_Mixer.ConnectInput(0, m_CurrentPlayable, 0, 1.0f);
m_CurrentPlayable.Play();
m_State = State.Idle;
}
public void SetCharging() {
if (m_ClipsSet == null) {
return;
}
m_CurrentPlayable = AnimationClipPlayable.Create(m_Graph, m_ClipsSet.charging);
m_Mixer.DisconnectInput(0);
m_Mixer.ConnectInput(0, m_CurrentPlayable, 0, 1.0f);
m_CurrentPlayable.Play();
m_State = State.Charging;
}
public void SetChargedIdle() {
if (m_ClipsSet == null) {
return;
}
m_CurrentPlayable = AnimationClipPlayable.Create(m_Graph, m_ClipsSet.chargedIdle);
m_Mixer.DisconnectInput(0);
m_Mixer.ConnectInput(0, m_CurrentPlayable, 0, 1.0f);
m_CurrentPlayable.Play();
m_State = State.ChargedIdle;
}
public void PlayChargedUse() {
if (m_ClipsSet == null) {
return;
}
m_CurrentPlayable = AnimationClipPlayable.Create(m_Graph, m_ClipsSet.chargedUse);
m_Mixer.DisconnectInput(0);
m_Mixer.ConnectInput(0, m_CurrentPlayable, 0, 1.0f);
m_CurrentPlayable.Play();
m_State = State.ChargedUse;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
bool IsCurrentClipFinished() {
return m_CurrentPlayable.GetTime() >= m_CurrentPlayable.GetAnimationClip().length;
}
}
public class PlayerAnimator : MonoBehaviour {
[SerializeField] ReAnimator m_ReAnimator;
[SerializeField] int m_HandsLayerIndex = 2;
LayerMixerNode m_LegsLayerMixer;
BasicCharacterLocomotionReAnimatorNode m_GroundBlendTree;
CharacterHandsReAnimatorNode m_Hands;
AnimationClipNode m_QuickKickNode;
// @TODO: for some reason `SetLocomotionParams` is called before awake
bool m_IsReady = false;
public UnityEvent onQuickAttackFinished = new UnityEvent();
public UnityEvent onChargeReady = new UnityEvent();
void Awake() {
m_LegsLayerMixer = m_ReAnimator.FindNode<LayerMixerNode>("legs_mixer");
m_LegsLayerMixer.SetLayerWeight(0, 1.0f);
m_LegsLayerMixer.SetLayerWeight(1, 0.0f);
m_LegsLayerMixer.SetLayerWeight(2, 0.0f);
m_GroundBlendTree = m_ReAnimator.FindNode<BasicCharacterLocomotionReAnimatorNode>("legs_locomotion_ground");
m_GroundBlendTree.SetInput(new float2(0, 0), 0.0f);
m_Hands = m_ReAnimator.FindNode<CharacterHandsReAnimatorNode>("hands");
m_Hands.OnQuickAttackAnimationFinished += () => { onQuickAttackFinished?.Invoke(); };
m_Hands.OnCharged += () => { onChargeReady?.Invoke(); };
m_QuickKickNode = m_ReAnimator.FindNode<AnimationClipNode>("legs_kick_quick");
m_IsReady = true;
}
public void SetLocomotionParams(PlayerLocomotionAnimatorParams locomotionParams) {
if (!m_IsReady) {
return;
}
m_LegsLayerMixer.SetLayerWeight(1, locomotionParams.IsGrounded ? 0.0f : 1.0f);
var groundBlendDirection = new float2(locomotionParams.VelocityRightNormalized,
locomotionParams.VelocityForwardNormalized);
m_GroundBlendTree.SetInput(groundBlendDirection, locomotionParams.TurnVelocity);
}
public void SetHandsAnimationSet(ItemHandsAnimationClipsSet clipsSet) {
if (clipsSet == null) {
m_ReAnimator.SetLayerWeight(m_HandsLayerIndex, 0.0f);
return;
}
m_ReAnimator.SetLayerWeight(m_HandsLayerIndex, 1.0f);
m_Hands.UpdateClips(clipsSet);
}
public void PlayQuickAttack(int combo) {
m_Hands.PlayQuickAttack(combo);
}
public void PlayHandsIdle() {
m_Hands.SetIdle();
}
public void PlayCharging() {
m_Hands.SetCharging();
}
public void PlayChargedUse() {
m_Hands.PlayChargedUse();
}
public void PlayKickAnimation() {
m_LegsLayerMixer.SetLayerWeight(2, 1.0f);
m_QuickKickNode.PlayOnceWithCallback(() => {
m_LegsLayerMixer.SetLayerWeight(2, 0.0f);
});
}
}
}