diff --git a/Runtime/Engine/Code/Animations.meta b/Runtime/Engine/Code/Animations.meta new file mode 100644 index 0000000..5949b3c --- /dev/null +++ b/Runtime/Engine/Code/Animations.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3cf39a45a5774becbca8108fd7378be4 +timeCreated: 1754272712 \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/AnimationClipNode.cs b/Runtime/Engine/Code/Animations/AnimationClipNode.cs new file mode 100644 index 0000000..d5bcdc2 --- /dev/null +++ b/Runtime/Engine/Code/Animations/AnimationClipNode.cs @@ -0,0 +1,24 @@ +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Playables; + +namespace RebootKit.Engine.Animations { + public class AnimationClipNode : IReAnimatorNode { + [field: SerializeField] public string Name { get; private set; } + + public AnimationClip Clip; + + public void Tick(float deltaTime) { + } + + public IPlayable Build(PlayableGraph graph) { + AnimationClipPlayable playable = AnimationClipPlayable.Create(graph, Clip); + return playable; + } + + public bool TryFindChild(string name, out IReAnimatorNode node) { + node = null; + return false; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/AnimationClipNode.cs.meta b/Runtime/Engine/Code/Animations/AnimationClipNode.cs.meta new file mode 100644 index 0000000..0862966 --- /dev/null +++ b/Runtime/Engine/Code/Animations/AnimationClipNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 784b3fdd05a24633992c1bb93b59e4d6 +timeCreated: 1754275137 \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/MixerNode.cs b/Runtime/Engine/Code/Animations/MixerNode.cs new file mode 100644 index 0000000..0272191 --- /dev/null +++ b/Runtime/Engine/Code/Animations/MixerNode.cs @@ -0,0 +1,77 @@ +using System; +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Playables; + +namespace RebootKit.Engine.Animations { + [Serializable] + public class MixerNode : IReAnimatorNode { + [Serializable] + struct InputState { + [SerializeReference] public IReAnimatorNode node; + + [Range(0.0f, 1.0f)] public float targetWeight; + [NonSerialized] public float CurrentWeight; + } + + [field: SerializeField] public string Name { get; private set; } + + AnimationMixerPlayable m_Mixer; + + [SerializeField] float m_TransitionSpeed = 5.0f; + [SerializeField] InputState[] m_Inputs; + + public void Tick(float deltaTime) { + for (int i = 0; i < m_Inputs.Length; ++i){ + if (m_TransitionSpeed > 0.0f) { + m_Inputs[i].CurrentWeight = Mathf.MoveTowards(m_Inputs[i].CurrentWeight, + m_Inputs[i].targetWeight, + m_TransitionSpeed * deltaTime); + } else { + m_Inputs[i].CurrentWeight = m_Inputs[i].targetWeight; + } + + m_Mixer.SetInputWeight(i, m_Inputs[i].CurrentWeight); + + m_Inputs[i].node.Tick(deltaTime); + } + } + + public IPlayable Build(PlayableGraph graph) { + m_Mixer = AnimationMixerPlayable.Create(graph, m_Inputs.Length); + + for (int i = 0; i < m_Inputs.Length; ++i) { + IPlayable playable = m_Inputs[i].node.Build(graph); + + if (playable is AnimationMixerPlayable mixerPlayable) { + m_Mixer.ConnectInput(i, mixerPlayable, 0, m_Inputs[i].targetWeight); + } else if (playable is AnimationClipPlayable clipPlayable) { + m_Mixer.ConnectInput(i, clipPlayable, 0, m_Inputs[i].targetWeight); + } + } + + return m_Mixer; + } + + public bool TryFindChild(string name, out IReAnimatorNode node) { + for (int i = 0; i < m_Inputs.Length; i++) { + if (m_Inputs[i].node.Name.Equals(name, StringComparison.Ordinal)) { + node = m_Inputs[i].node; + return true; + } + + if (m_Inputs[i].node.TryFindChild(name, out IReAnimatorNode childNode)) { + node = childNode; + return true; + } + } + + node = null; + return false; + } + + public void SetInputWeight(int index, float weight) { + m_Inputs[index].targetWeight = weight; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/MixerNode.cs.meta b/Runtime/Engine/Code/Animations/MixerNode.cs.meta new file mode 100644 index 0000000..067aa11 --- /dev/null +++ b/Runtime/Engine/Code/Animations/MixerNode.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a76aeb9f1d9542f6ba98ffec129beb0c +timeCreated: 1754275958 \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/ReAnimator.cs b/Runtime/Engine/Code/Animations/ReAnimator.cs new file mode 100644 index 0000000..e2e0d16 --- /dev/null +++ b/Runtime/Engine/Code/Animations/ReAnimator.cs @@ -0,0 +1,116 @@ +using System; +using UnityEngine; +using UnityEngine.Animations; +using UnityEngine.Assertions; +using UnityEngine.Playables; +using Logger = RebootKit.Engine.Foundation.Logger; + +// @TODO: +// - avoid boxing values +// - think about refactoring in order to reduce gc allocs +namespace RebootKit.Engine.Animations { + public interface IReAnimatorNode { + string Name { get; } + + void Tick(float deltaTime); + IPlayable Build(PlayableGraph graph); + + bool TryFindChild(string name, out IReAnimatorNode node); + } + + [Serializable] + class ReAnimatorLayer { + public string name; + public AvatarMask mask; + public bool isAdditive; + [Range(0.0f, 1.0f)] public float weight = 1.0f; + + [SerializeReference] public IReAnimatorNode root; + } + + [DefaultExecutionOrder(-100)] + public class ReAnimator : MonoBehaviour { + static readonly Logger s_Logger = new Logger(nameof(ReAnimator)); + + [SerializeField] Animator m_Animator; + [SerializeField] ReAnimatorLayer[] m_Layers; + + PlayableGraph m_Graph; + AnimationPlayableOutput m_Output; + + AnimationLayerMixerPlayable m_LayerMixer; + + void Awake() { + Assert.IsNotNull(m_Animator, "Animator is not set"); + Assert.IsTrue(m_Layers.Length > 0, "Atleast one layer must be defined!"); + + m_Animator.runtimeAnimatorController = null; + + m_Graph = PlayableGraph.Create(name); + m_Output = AnimationPlayableOutput.Create(m_Graph, "Animation Output", m_Animator); + + m_LayerMixer = AnimationLayerMixerPlayable.Create(m_Graph, m_Layers.Length); + m_Output.SetSourcePlayable(m_LayerMixer, 0); + + for (int i = 0; i < m_Layers.Length; ++i) { + IPlayable playable = m_Layers[i].root.Build(m_Graph); + + if (playable is AnimationMixerPlayable mixerPlayable) { + m_LayerMixer.ConnectInput(i, mixerPlayable, 0, m_Layers[i].weight); + } else if (playable is AnimationClipPlayable clipPlayable) { + m_LayerMixer.ConnectInput(i, clipPlayable, 0, m_Layers[i].weight); + } + + m_LayerMixer.SetLayerAdditive((uint)i, m_Layers[i].isAdditive); + + if (m_Layers[i].mask != null) { + m_LayerMixer.SetLayerMaskFromAvatarMask((uint)i, m_Layers[i].mask); + } + } + + m_Graph.Play(); + } + + void OnDestroy() { + if (m_Graph.IsValid()) { + m_Graph.Destroy(); + } + } + + void Update() { + for (int i = 0; i < m_Layers.Length; i++) { + m_Layers[i].root.Tick(Time.deltaTime); + } + } + + public void SetLayerWeight(int layer, float weight) { + m_Layers[layer].weight = weight; + m_LayerMixer.SetInputWeight(layer, weight); + } + + public IReAnimatorNode FindNode(string nodeName) { + foreach (ReAnimatorLayer layer in m_Layers) { + if (layer.root.Name.Equals(nodeName, StringComparison.Ordinal)) { + return layer.root; + } + + if (layer.root.TryFindChild(nodeName, out IReAnimatorNode childNode)) { + return childNode; + } + } + + s_Logger.Error($"Couldn't find node with name: {nodeName}"); + return null; + } + + public TNode FindNode(string nodeName) where TNode : class, IReAnimatorNode { + IReAnimatorNode node = FindNode(nodeName); + if (node is TNode tnode) { + return tnode; + } + + s_Logger.Error($"Couldn't find node with name: {nodeName} of type {typeof(TNode).Name}"); + return null; + } + } +} \ No newline at end of file diff --git a/Runtime/Engine/Code/Animations/ReAnimator.cs.meta b/Runtime/Engine/Code/Animations/ReAnimator.cs.meta new file mode 100644 index 0000000..fff5e42 --- /dev/null +++ b/Runtime/Engine/Code/Animations/ReAnimator.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 960522ea44ce4513aea34826f00bc19c +timeCreated: 1754272717 \ No newline at end of file diff --git a/Runtime/Engine/Code/Main/RR.cs b/Runtime/Engine/Code/Main/RR.cs index e787179..f39d7ed 100755 --- a/Runtime/Engine/Code/Main/RR.cs +++ b/Runtime/Engine/Code/Main/RR.cs @@ -196,6 +196,12 @@ namespace RebootKit.Engine.Main { Network.SetCurrentWorld(worldID); } + public static Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, + Vector3 position, + Quaternion rotation) { + return Network.Actors.SpawnLocalOnlyActor(assetReference, position, rotation); + } + public static Actor SpawnActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { diff --git a/Runtime/Engine/Code/Simulation/Actor.cs b/Runtime/Engine/Code/Simulation/Actor.cs index 4bb8b21..fa65b41 100644 --- a/Runtime/Engine/Code/Simulation/Actor.cs +++ b/Runtime/Engine/Code/Simulation/Actor.cs @@ -250,6 +250,8 @@ namespace RebootKit.Engine.Simulation { } } + internal bool IsLocalOnly; + [SerializeField] internal Rigidbody actorRigidbody; [InfoBox("If empty, will use GetComponentsInChildren() to find colliders.")] [SerializeField] Collider[] m_OverrideActorColliders; @@ -346,7 +348,7 @@ namespace RebootKit.Engine.Simulation { } public void MountTo(Actor actor, string slotName) { - if (!RR.IsServer()) { + if (!RR.IsServer() && !IsLocalOnly) { s_ActorLogger.Error($"Only the server can mount actors. Actor: {name} (ID: {ActorID})"); return; } @@ -370,7 +372,7 @@ namespace RebootKit.Engine.Simulation { } public void UnMount() { - if (!RR.IsServer()) { + if (!RR.IsServer() && !IsLocalOnly) { s_ActorLogger.Error($"Only the server can unmount actors. Actor: {name} (ID: {ActorID})"); return; } diff --git a/Runtime/Engine/Code/Simulation/ActorsManager.cs b/Runtime/Engine/Code/Simulation/ActorsManager.cs index f6a9b73..a0d20fd 100644 --- a/Runtime/Engine/Code/Simulation/ActorsManager.cs +++ b/Runtime/Engine/Code/Simulation/ActorsManager.cs @@ -188,6 +188,35 @@ namespace RebootKit.Engine.Simulation { actor.ActorID = actorID; } } + + public Actor SpawnLocalOnlyActor(AssetReferenceGameObject assetReference, Vector3 position, Quaternion rotation) { + if (!assetReference.RuntimeKeyIsValid()) { + s_Logger.Error("Trying to spawn an actor with an invalid asset reference."); + return null; + } + + if (!TryGenerateNextActorID(out ushort actorID)) { + s_Logger.Error("Cannot spawn actor: Failed to generate next actor ID."); + return null; + } + + GameObject actorObject = assetReference.InstantiateAsync(position, rotation).WaitForCompletion(); + Actor actor = actorObject.GetComponent(); + if (actor is null) { + s_Logger.Error($"GameObject {actorObject.name} does not have an Actor component."); + Object.Destroy(actorObject); + return null; + } + + actor.Manager = this; + actor.IsLocalOnly = true; + actor.SourceActorPath = assetReference.AssetGUID; + actor.ActorID = actorID; + actor.Data = actor.InternalCreateActorData(); + // m_SpawnedActors.Add(actor); + + return actor; + } // // @MARK: Common API