diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs new file mode 100644 index 0000000000..ce547f9376 --- /dev/null +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs @@ -0,0 +1,14 @@ +using UnityEngine; + +namespace DCLServices.EmotesService.Domain +{ + public struct AnimationSequence + { + public AnimationClip AvatarStart; + public AnimationClip AvatarLoop; + public AnimationClip AvatarEnd; + public AnimationClip PropStart; + public AnimationClip PropLoop; + public AnimationClip PropEnd; + } +} diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs.meta b/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs.meta new file mode 100644 index 0000000000..8c523d3c02 --- /dev/null +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/AnimationSequence.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: dd75e8ffbceb4184bcc265406c3f25dc +timeCreated: 1695136539 \ No newline at end of file diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmbedEmoteReference.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmbedEmoteReference.cs index dfb0d5addd..5086b16371 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmbedEmoteReference.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmbedEmoteReference.cs @@ -1,21 +1,24 @@ -namespace DCL.Emotes +using DCLServices.EmotesService.Domain; +using System; + +namespace DCL.Emotes { public class EmbedEmoteReference : IEmoteReference { private readonly WearableItem emoteItem; - private readonly EmoteClipData clipData; + private readonly EmoteAnimationData animationData; - public EmbedEmoteReference(WearableItem emoteItem, EmoteClipData clipData) + public EmbedEmoteReference(WearableItem emoteItem, EmoteAnimationData animationData) { this.emoteItem = emoteItem; - this.clipData = clipData; + this.animationData = animationData; } public WearableItem GetEntity() => emoteItem; - public EmoteClipData GetData() => - clipData; + public EmoteAnimationData GetData() => + animationData; public void Dispose() { diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmoteAnimationData.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmoteAnimationData.cs new file mode 100644 index 0000000000..1082153ea4 --- /dev/null +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmoteAnimationData.cs @@ -0,0 +1,303 @@ +using JetBrains.Annotations; +using System; +using UnityEngine; + +namespace DCLServices.EmotesService.Domain +{ + public enum EmoteState + { + PLAYING, + STOPPING, + STOPPED, + } + + public class EmoteAnimationData + { + private const float EXPRESSION_EXIT_TRANSITION_TIME = 0.2f; + private const float EXPRESSION_ENTER_TRANSITION_TIME = 0.1f; + + [CanBeNull] private readonly Animation extraContentAnimation; + [CanBeNull] private readonly GameObject extraContent; + [CanBeNull] private readonly Renderer[] renderers; + [CanBeNull] private readonly AudioSource audioSource; + + private readonly AnimationClip avatarClip; + private readonly bool loop; + private AnimationSequence animationSequence; + private Animation avatarAnimation; + private bool isSequential; + private AnimationState currentAvatarAnimation; + private AnimationState avatarClipState; + private AnimationState sequenceAvatarLoopState; + private EmoteState currentState = EmoteState.STOPPED; + + // Constructor used by Embed Emotes + public EmoteAnimationData(AnimationClip avatarClip, bool loop = false) + { + this.avatarClip = avatarClip; + this.loop = loop; + } + + // Constructor used by Remote Emotes + public EmoteAnimationData(AnimationClip mainClip, GameObject container, AudioSource audioSource, bool loop = false) + { + this.avatarClip = mainClip; + this.loop = loop; + this.extraContent = container; + this.audioSource = audioSource; + + if (extraContent == null) return; + + extraContentAnimation = extraContent.GetComponentInChildren(); + + if (extraContentAnimation == null) + Debug.LogError($"Animation {avatarClip.name} extra content does not have an animation"); + + renderers = extraContent.GetComponentsInChildren(); + } + + public bool IsLoop() => + loop; + + public bool HasAudio() => + audioSource != null; + + public bool IsSequential() => + isSequential; + + public void UnEquip() + { + if (extraContent != null) + extraContent.transform.SetParent(null, false); + + if (isSequential) + { + avatarAnimation.RemoveClip(animationSequence.AvatarStart); + avatarAnimation.RemoveClip(animationSequence.AvatarLoop); + avatarAnimation.RemoveClip(animationSequence.AvatarEnd); + } + else + { + avatarAnimation.RemoveClip(avatarClip); + Debug.Log("UnEquip " + avatarClip); + } + } + + public int GetLoopCount() => + Mathf.RoundToInt(currentAvatarAnimation.time / currentAvatarAnimation.length); + + public void Equip(Animation animation) + { + avatarAnimation = animation; + + if (isSequential) + { + avatarAnimation.AddClip(animationSequence.AvatarStart, animationSequence.AvatarStart.name); + avatarAnimation.AddClip(animationSequence.AvatarLoop, animationSequence.AvatarLoop.name); + avatarAnimation.AddClip(animationSequence.AvatarEnd, animationSequence.AvatarEnd.name); + sequenceAvatarLoopState = avatarAnimation[animationSequence.AvatarLoop.name]; + } + else + { + avatarAnimation.AddClip(avatarClip, avatarClip.name); + avatarClipState = animation[avatarClip.name]; + Debug.Log(avatarClip.name, avatarAnimation); + } + + // We set the extra content as a child of the avatar gameobject and use its local position to mimick its positioning and correction + if (extraContent != null) + { + Transform animationTransform = animation.transform; + extraContent.transform.SetParent(animationTransform.parent, false); + extraContent.transform.localRotation = animationTransform.localRotation; + extraContent.transform.localScale = animationTransform.localScale; + extraContent.transform.localPosition = animationTransform.localPosition; + } + } + + public void Play(int layer, bool spatial, float volume, bool occlude) + { + currentState = EmoteState.PLAYING; + + EnableRenderers(layer, occlude); + + if (isSequential) + PlaySequential(spatial, volume); + else + PlayNormal(spatial, volume); + } + + private void EnableRenderers(int gameObjectLayer, bool occlude) + { + if (renderers == null) return; + + foreach (Renderer renderer in renderers) + { + renderer.enabled = true; + renderer.gameObject.layer = gameObjectLayer; + renderer.allowOcclusionWhenDynamic = occlude; + } + } + + private void PlayNormal(bool spatial, float volume) + { + string avatarClipName = avatarClip.name; + + avatarAnimation.wrapMode = loop ? WrapMode.Loop : WrapMode.Once; + + if (avatarAnimation.IsPlaying(avatarClipName)) + avatarAnimation.Rewind(avatarClipName); + + avatarAnimation.CrossFade(avatarClipName, EXPRESSION_ENTER_TRANSITION_TIME, PlayMode.StopAll); + currentAvatarAnimation = avatarClipState; + + if (extraContentAnimation != null) + { + var layer = 0; + + extraContentAnimation.enabled = true; + + foreach (AnimationState state in extraContentAnimation) + { + if (state.clip == avatarClip) continue; + state.layer = layer++; + state.wrapMode = loop ? WrapMode.Loop : WrapMode.Once; + extraContentAnimation.Play(state.clip.name); + } + } + + if (audioSource != null) + { + audioSource.spatialBlend = spatial ? 1 : 0; + audioSource.volume = volume; + audioSource.loop = loop; + audioSource.Play(); + } + } + + private void PlaySequential(bool spatial, float volume) + { + avatarAnimation.wrapMode = WrapMode.Default; + avatarAnimation[animationSequence.AvatarStart.name].wrapMode = WrapMode.Once; + avatarAnimation[animationSequence.AvatarLoop.name].wrapMode = WrapMode.Loop; + avatarAnimation.Stop(); + avatarAnimation.CrossFadeQueued(animationSequence.AvatarStart.name, EXPRESSION_ENTER_TRANSITION_TIME, QueueMode.PlayNow); + avatarAnimation.CrossFadeQueued(animationSequence.AvatarLoop.name, 0, QueueMode.CompleteOthers); + currentAvatarAnimation = sequenceAvatarLoopState; + + if (extraContentAnimation != null) + { + extraContentAnimation.enabled = true; + extraContentAnimation.wrapMode = WrapMode.Default; + extraContentAnimation[animationSequence.PropStart.name].wrapMode = WrapMode.Once; + extraContentAnimation[animationSequence.PropLoop.name].wrapMode = WrapMode.Loop; + extraContentAnimation.Stop(); + extraContentAnimation.CrossFadeQueued(animationSequence.PropStart.name, EXPRESSION_ENTER_TRANSITION_TIME, QueueMode.PlayNow); + extraContentAnimation.CrossFadeQueued(animationSequence.PropLoop.name, 0, QueueMode.CompleteOthers); + } + + if (audioSource == null) return; + + audioSource.spatialBlend = spatial ? 1 : 0; + audioSource.volume = volume; + audioSource.loop = loop; + audioSource.Play(); + } + + public void Stop(bool immediate) + { + if (isSequential) + { + SequentialStop(immediate); + currentState = !immediate ? EmoteState.STOPPING : EmoteState.STOPPED; + } + else + { + NormalStop(immediate); + currentState = EmoteState.STOPPED; + } + + if (audioSource != null) + audioSource.Stop(); + } + + private void SequentialStop(bool immediate) + { + avatarAnimation[animationSequence.AvatarEnd.name].wrapMode = WrapMode.Once; + avatarAnimation.Stop(); + + if (immediate) + { + if (renderers != null) + foreach (Renderer renderer in renderers) + renderer.enabled = false; + } + else + avatarAnimation.CrossFade(animationSequence.AvatarEnd.name, EXPRESSION_EXIT_TRANSITION_TIME); + + currentAvatarAnimation = avatarAnimation[animationSequence.AvatarEnd.name]; + + if (extraContentAnimation == null) return; + + extraContentAnimation[animationSequence.PropEnd.name].wrapMode = WrapMode.Once; + extraContentAnimation.Stop(); + + if (immediate) + { + foreach (AnimationState state in extraContentAnimation) + { + if (state.clip == avatarClip) continue; + extraContentAnimation.Stop(state.clip.name); + } + + extraContentAnimation.enabled = false; + } + else + extraContentAnimation.CrossFade(animationSequence.PropEnd.name, EXPRESSION_EXIT_TRANSITION_TIME); + } + + private void NormalStop(bool immediate) + { + avatarAnimation.Blend(avatarClip.name, 0, !immediate ? EXPRESSION_EXIT_TRANSITION_TIME : 0); + + if (renderers != null) + foreach (Renderer renderer in renderers) + renderer.enabled = false; + + if (extraContentAnimation == null) return; + + foreach (AnimationState state in extraContentAnimation) + { + if (state.clip == avatarClip) continue; + extraContentAnimation.Stop(state.clip.name); + } + + extraContentAnimation.enabled = false; + } + + public void SetupSequentialAnimation(AnimationSequence sequence) + { + isSequential = true; + animationSequence = sequence; + + if (extraContentAnimation == null) return; + + extraContentAnimation.AddClip(animationSequence.PropStart, animationSequence.PropStart.name); + extraContentAnimation.AddClip(animationSequence.PropLoop, animationSequence.PropLoop.name); + extraContentAnimation.AddClip(animationSequence.PropEnd, animationSequence.PropEnd.name); + } + + public bool CanTransitionOut() => + !isSequential || IsFinished(); + + public bool IsFinished() + { + if (loop && !isSequential) return false; + float timeTillEnd = currentAvatarAnimation == null ? 0 : currentAvatarAnimation.length - currentAvatarAnimation.time; + return timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME; + } + + public EmoteState GetState() => + currentState; + } +} diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/EmoteClipData.cs.meta b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmoteAnimationData.cs.meta similarity index 100% rename from unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/EmoteClipData.cs.meta rename to unity-renderer/Assets/DCLServices/EmotesService/Domain/EmoteAnimationData.cs.meta diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmotesServiceDefinitions.asmdef b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmotesServiceDefinitions.asmdef index e54df0b60d..815138debb 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmotesServiceDefinitions.asmdef +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/EmotesServiceDefinitions.asmdef @@ -2,10 +2,10 @@ "name": "EmotesServiceDefinitions", "rootNamespace": "", "references": [ - "GUID:7ac9f9c835ec1084ab35e3f9b176cf1e", "GUID:3b80b0b562b1cbc489513f09fc1b8f69", "GUID:f51ebe6a0ceec4240a699833d6309b23", - "GUID:99cea83ca76dcd846abed7e61a8c90bd" + "GUID:99cea83ca76dcd846abed7e61a8c90bd", + "GUID:7ac9f9c835ec1084ab35e3f9b176cf1e" ], "includePlatforms": [], "excludePlatforms": [], diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteAnimationLoader.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteAnimationLoader.cs index 1e09668f26..7da53810c8 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteAnimationLoader.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteAnimationLoader.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using Cysharp.Threading.Tasks; +using DCLServices.EmotesService.Domain; using UnityEngine; namespace DCL.Emotes @@ -10,6 +11,8 @@ public interface IEmoteAnimationLoader : IDisposable AnimationClip mainClip { get; } GameObject container { get; } AudioSource audioSource { get; } + bool IsSequential { get; } + AnimationSequence GetSequence(); UniTask LoadRemoteEmote(GameObject targetContainer, WearableItem emote, string bodyShapeId, CancellationToken ct = default); UniTask LoadLocalEmote(GameObject targetContainer, ExtendedEmote embeddedEmote, CancellationToken ct = default); } diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteReference.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteReference.cs index dd1c46f28c..41b27b7e96 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteReference.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/IEmoteReference.cs @@ -1,10 +1,11 @@ -using System; +using DCLServices.EmotesService.Domain; +using System; namespace DCL.Emotes { public interface IEmoteReference : IDisposable { WearableItem GetEntity(); - EmoteClipData GetData(); + EmoteAnimationData GetData(); } } diff --git a/unity-renderer/Assets/DCLServices/EmotesService/Domain/NftEmoteReference.cs b/unity-renderer/Assets/DCLServices/EmotesService/Domain/NftEmoteReference.cs index 649abe6263..7451c15d1a 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/Domain/NftEmoteReference.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/Domain/NftEmoteReference.cs @@ -1,23 +1,30 @@ -namespace DCL.Emotes +using DCLServices.EmotesService.Domain; + +namespace DCL.Emotes { public class NftEmoteReference : IEmoteReference { private readonly WearableItem emoteItem; private readonly IEmoteAnimationLoader loader; - private readonly EmoteClipData emoteClipData; + private readonly EmoteAnimationData emoteAnimationData; public NftEmoteReference(WearableItem emoteItem, IEmoteAnimationLoader loader, bool loop) { this.emoteItem = emoteItem; this.loader = loader; - emoteClipData = new EmoteClipData(loader.mainClip, loader.container, loader.audioSource, loop); + emoteAnimationData = new EmoteAnimationData(loader.mainClip, loader.container, loader.audioSource, loop); + + if (loader.IsSequential) + { + emoteAnimationData.SetupSequentialAnimation(loader.GetSequence()); + } } public WearableItem GetEntity() => emoteItem; - public EmoteClipData GetData() => - emoteClipData; + public EmoteAnimationData GetData() => + emoteAnimationData; public void Dispose() { diff --git a/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs b/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs index 3b90efcbed..600b868c4e 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/EmoteAnimationLoader.cs @@ -4,6 +4,8 @@ using Cysharp.Threading.Tasks; using DCL.Helpers; using DCL.Providers; +using DCLServices.EmotesService.Domain; +using System.Collections.Generic; using UnityEngine; using Object = UnityEngine.Object; @@ -11,15 +13,25 @@ namespace DCL.Emotes { public class EmoteAnimationLoader : IEmoteAnimationLoader { + private const string AVATAR_START = "avatar_start"; + private const string AVATAR_LOOP = "avatar_loop"; + private const string AVATAR_END = "avatar_end"; + private const string PROP_START = "prop_start"; + private const string PROP_LOOP = "prop_loop"; + private const string PROP_END = "prop_end"; private const string EMOTE_AUDIO_SOURCE = "EmoteAudioSource"; + private readonly IWearableRetriever retriever; private readonly AddressableResourceProvider resourceProvider; + public AnimationClip mainClip { get; private set; } public GameObject container { get; private set; } public AudioSource audioSource { get; private set; } + public bool IsSequential { get; private set; } private AudioClip audioClip; private AssetPromise_AudioClip audioClipPromise; + private AnimationSequence animationSequence; public EmoteAnimationLoader(IWearableRetriever retriever, AddressableResourceProvider resourceProvider) { @@ -27,6 +39,9 @@ public EmoteAnimationLoader(IWearableRetriever retriever, AddressableResourcePro this.resourceProvider = resourceProvider; } + public AnimationSequence GetSequence() => + animationSequence; + public async UniTask LoadRemoteEmote(GameObject targetContainer, WearableItem emote, string bodyShapeId, CancellationToken ct = default) { if (targetContainer == null) @@ -56,14 +71,9 @@ public async UniTask LoadRemoteEmote(GameObject targetContainer, WearableItem em if (!IsValidAudioClip(contentMap.file)) continue; AudioClip audioClip = null; - try - { - audioClip = await AsyncLoadAudioClip(contentMap.file, contentProvider); - } - catch (Exception e) - { - Debug.LogError(e); - } + + try { audioClip = await AsyncLoadAudioClip(contentMap.file, contentProvider); } + catch (Exception e) { Debug.LogError(e); } if (audioClip != null) await SetupAudioClip(audioClip, emoteInstance, ct); @@ -71,13 +81,13 @@ public async UniTask LoadRemoteEmote(GameObject targetContainer, WearableItem em // we only support one audio clip break; } - } public async UniTask LoadLocalEmote(GameObject targetContainer, ExtendedEmote embeddedEmote, CancellationToken ct) { GameObject emoteInstance = Object.Instantiate(embeddedEmote.propPrefab, targetContainer.transform, false); var renderers = emoteInstance.GetComponentsInChildren(true); + foreach (Renderer renderer in renderers) renderer.enabled = false; @@ -95,6 +105,12 @@ private void SetupEmote(GameObject emoteInstance, string emoteId) return; } + var requiredAnimationSequence = new HashSet + { + AVATAR_END, AVATAR_LOOP, AVATAR_START, + PROP_END, PROP_LOOP, PROP_START, + }; + if (animation.GetClipCount() > 1) { this.container = emoteInstance; @@ -102,6 +118,8 @@ private void SetupEmote(GameObject emoteInstance, string emoteId) // we cant use the animation order so we use the naming convention at /creator/emotes/props-and-sounds/ foreach (AnimationState state in animation) { + FillAnimationSequence(requiredAnimationSequence, state.clip); + AnimationClip clip = state.clip; if (clip.name.Contains("_avatar", StringComparison.OrdinalIgnoreCase) || clip.name == emoteId) @@ -112,6 +130,9 @@ private void SetupEmote(GameObject emoteInstance, string emoteId) animation.Play(clip.name); } + if (requiredAnimationSequence.Count == 0) + IsSequential = true; + // in the case that the animation names are badly named, we just get the first animation that does not contain prop in its name if (mainClip == null) { @@ -132,21 +153,54 @@ private void SetupEmote(GameObject emoteInstance, string emoteId) return; } - // Clip names should be unique because of the Legacy Animation string based usage. - // In rare cases some animations might use the same GLB, thus causing this clip to be used by 2 different emotes - // so we avoid renaming the clip again witch can cause problems as we use the clip names internally - if (!mainClip.name.Contains("urn")) - mainClip.name = emoteId; + //Clip names should be unique because of the Legacy Animation string based usage + if (IsSequential) + { + animationSequence.AvatarStart.name = RenameIfValid(animationSequence.AvatarStart.name, $"{emoteId}:{animationSequence.AvatarStart.name}"); + animationSequence.AvatarLoop.name = RenameIfValid(animationSequence.AvatarLoop.name, $"{emoteId}:{animationSequence.AvatarLoop.name}"); + animationSequence.AvatarEnd.name = RenameIfValid(animationSequence.AvatarEnd.name, $"{emoteId}:{animationSequence.AvatarEnd.name}"); + } + else { mainClip.name = RenameIfValid(mainClip.name, emoteId); } animation.Stop(); animation.enabled = false; } + // Kinerius note: There was a weird case when testing where 2 emotes with different urn ids where using the same GLTF, + // when the second one loads the clip gets renamed and that causes the first user to not be able to use the emote. + // To avoid this, we rename only when the clip is not already renamed + private string RenameIfValid(string from, string to) => + from.StartsWith("urn") ? from : to; + + private void FillAnimationSequence(HashSet requiredAnimationSequence, AnimationClip clip) + { + string name = clip.name.ToLower(); + + if (!requiredAnimationSequence.Contains(name)) return; + + if (name.EndsWith(AVATAR_START, StringComparison.OrdinalIgnoreCase)) + animationSequence.AvatarStart = clip; + else if (name.EndsWith(AVATAR_LOOP, StringComparison.OrdinalIgnoreCase)) + animationSequence.AvatarLoop = clip; + else if (name.EndsWith(AVATAR_END, StringComparison.OrdinalIgnoreCase)) + animationSequence.AvatarEnd = clip; + else if (name.EndsWith(PROP_START, StringComparison.OrdinalIgnoreCase)) + animationSequence.PropStart = clip; + else if (name.EndsWith(PROP_LOOP, StringComparison.OrdinalIgnoreCase)) + animationSequence.PropLoop = clip; + else if (name.EndsWith(PROP_END, StringComparison.OrdinalIgnoreCase)) + animationSequence.PropEnd = clip; + + requiredAnimationSequence.Remove(name); + } + private async UniTask SetupAudioClip(AudioClip clip, GameObject audioSourceParent, CancellationToken ct) { audioClip = clip; + audioSource = await resourceProvider.Instantiate(EMOTE_AUDIO_SOURCE, "EmoteAudioSource", cancellationToken: ct); + audioSource.clip = audioClip; audioSource.transform.SetParent(audioSourceParent.transform, false); audioSource.transform.ResetLocalTRS(); diff --git a/unity-renderer/Assets/DCLServices/EmotesService/EmotesService.cs b/unity-renderer/Assets/DCLServices/EmotesService/EmotesService.cs index cd81120e26..bf4dcdbfc0 100644 --- a/unity-renderer/Assets/DCLServices/EmotesService/EmotesService.cs +++ b/unity-renderer/Assets/DCLServices/EmotesService/EmotesService.cs @@ -1,6 +1,7 @@ using AvatarAssets; using Cysharp.Threading.Tasks; using DCL.Configuration; +using DCLServices.EmotesService.Domain; using DCLServices.WearablesCatalogService; using System; using System.Collections.Generic; @@ -82,7 +83,7 @@ private void SetupEmbeddedExtendedEmote(ExtendedEmote embeddedEmote) private void SetupEmbeddedClip(EmbeddedEmote embeddedEmote, AnimationClip clip, string urnPrefix) { clip.name = embeddedEmote.id; - var clipData = new EmoteClipData(clip, embeddedEmote.emoteDataV0?.loop ?? false); + var clipData = new EmoteAnimationData(clip, embeddedEmote.emoteDataV0?.loop ?? false); embeddedEmotes.Add(new EmoteBodyId(urnPrefix, embeddedEmote.id), new EmbedEmoteReference(embeddedEmote, clipData)); } diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/AvatarEmotesController.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/AvatarEmotesController.cs index ab44386e80..aa2566a59c 100644 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/AvatarEmotesController.cs +++ b/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/AvatarEmotesController.cs @@ -54,6 +54,7 @@ private void LoadEmote(string bodyShapeId, WearableItem emote) private async UniTask AsyncEmoteLoad(string bodyShapeId, string emoteId) { + Debug.Log("Loading " + emoteId); var emoteKey = new EmoteBodyId(bodyShapeId, emoteId); try diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/Definitions/IAnimator.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/Definitions/IAnimator.cs index df5fc04f47..dfe1764bc5 100644 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/Definitions/IAnimator.cs +++ b/unity-renderer/Assets/Scripts/MainScripts/DCL/AvatarSystem/Definitions/IAnimator.cs @@ -1,4 +1,5 @@ using DCL.Emotes; +using DCLServices.EmotesService.Domain; using UnityEngine; namespace AvatarSystem @@ -9,7 +10,7 @@ public interface IAnimator void PlayEmote(string emoteId, long timestamps, bool spatial, float volume, bool occlude, bool ignoreTimestamp); void StopEmote(); - void EquipEmote(string emoteId, EmoteClipData emoteClipData); + void EquipEmote(string emoteId, EmoteAnimationData emoteAnimationData); void UnequipEmote(string emoteId); } diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs index 3ba9a85790..50c6da35bd 100644 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs +++ b/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs @@ -4,8 +4,7 @@ using Cysharp.Threading.Tasks; using DCL; using DCL.Components; -using DCL.Emotes; -using DCL.Helpers; +using DCLServices.EmotesService.Domain; using UnityEngine; using Environment = DCL.Environment; @@ -41,8 +40,6 @@ public class AvatarAnimatorLegacy : MonoBehaviour, IPoolLifecycleHandler, IAnima const float WALK_RUN_SWITCH_TIME = 1.5f; const float JUMP_TRANSITION_TIME = 0.01f; const float FALL_TRANSITION_TIME = 0.5f; - const float EXPRESSION_EXIT_TRANSITION_TIME = 0.2f; - const float EXPRESSION_ENTER_TRANSITION_TIME = 0.1f; const float OTHER_PLAYER_MOVE_THRESHOLD = 0.02f; const float AIR_EXIT_TRANSITION_TIME = 0.2f; @@ -96,7 +93,7 @@ public class BlackBoard private float lastOnAirTime = 0; - private Dictionary emoteClipDataMap = new (); + private Dictionary emoteClipDataMap = new (); private string runAnimationName; private string walkAnimationName; @@ -110,11 +107,12 @@ public class BlackBoard private Ray rayCache; private bool hasTarget; - private EmoteClipData lastExtendedEmoteData; + private EmoteAnimationData lastExtendedEmoteData; private string lastCrossFade; - private AnimationState currentEmote; private int lastEmoteLoopCount; + private readonly DataStore_Player dataStorePlayer = DataStore.i.player; + private void Awake() { hasTarget = target != null; @@ -129,8 +127,8 @@ public bool Prepare(string bodyshapeId, GameObject container) { StopEmote(); - animation = container.gameObject.GetOrCreateComponent(); - container.gameObject.GetOrCreateComponent(); + animation = GetOrCreateComponent(container.gameObject); + GetOrCreateComponent(container.gameObject); PrepareLocomotionAnims(bodyshapeId); SetIdleFrame(); @@ -156,6 +154,12 @@ public bool Prepare(string bodyshapeId, GameObject container) return true; } + private static T GetOrCreateComponent(GameObject gameObject) where T: Component + { + T component = gameObject.GetComponent(); + return !component ? gameObject.AddComponent() : component; + } + private void PrepareLocomotionAnims(string bodyshapeId) { if (bodyshapeId.Contains(WearableLiterals.BodyShapes.MALE)) @@ -357,7 +361,7 @@ void State_Air(BlackBoard bb) private void State_Expression(BlackBoard bb) { - var prevAnimation = latestAnimationState; + latestAnimationState = AvatarAnimation.EMOTE; var exitTransitionStarted = false; @@ -367,22 +371,39 @@ private void State_Expression(BlackBoard bb) exitTransitionStarted = true; } - if (ExpressionGroundTransitionCondition(animationState: animation[bb.expressionTriggerId])) + if (IsEmoteFinished()) { - currentState = State_Ground; - exitTransitionStarted = true; + bool canTransitionOut = lastExtendedEmoteData?.CanTransitionOut() ?? true; + bool isPlaying = lastExtendedEmoteData?.GetState() == EmoteState.PLAYING; + + if (isPlaying && !canTransitionOut) + exitTransitionStarted = true; + + if (canTransitionOut) + { + StopEmoteInternal(true); + + if (isOwnPlayer) + dataStorePlayer.canPlayerMove.Set(true); + + currentState = State_Ground; + } } if (exitTransitionStarted) + { StopEmoteInternal(false); - else if (prevAnimation != AvatarAnimation.EMOTE) // this condition makes Blend be called only in first frame of the state + } + + // TODO: check why we have this condition here + /*else if (prevAnimation != AvatarAnimation.EMOTE) // this condition makes Blend be called only in first frame of the state { animation.wrapMode = bb.shouldLoop ? WrapMode.Loop : WrapMode.Once; animation.Blend(bb.expressionTriggerId, 1, EXPRESSION_ENTER_TRANSITION_TIME); - } + }*/ // If we reach the emote loop, we send the RPC message again to refresh new users - if (bb.shouldLoop && isOwnPlayer) + if (bb.shouldLoop && isOwnPlayer && !IsEmoteFinished()) { int emoteLoop = GetCurrentEmoteLoopCount(); @@ -394,18 +415,21 @@ private void State_Expression(BlackBoard bb) return; - bool ExpressionGroundTransitionCondition(AnimationState animationState) + bool IsEmoteFinished() { - float timeTillEnd = animationState == null ? 0 : animationState.length - animationState.time; - bool isAnimationOver = timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME && !bb.shouldLoop; + //bool emoteIsFinished = lastExtendedEmoteData?.IsFinished() ?? true; + //bool isAnimationOver = emoteIsFinished && !bb.shouldLoop; bool isMoving = isOwnPlayer ? DCLCharacterController.i.isMovingByUserInput : Math.Abs(bb.movementSpeed) > OTHER_PLAYER_MOVE_THRESHOLD; - - return isAnimationOver || isMoving; + bool isJumping = isOwnPlayer && DCLCharacterController.i.isJumping; + bool emoteIsFinished = lastExtendedEmoteData?.IsFinished() ?? true; + bool isAnimationFinishing = lastExtendedEmoteData?.GetState() == EmoteState.STOPPING; + return isMoving || isAnimationFinishing || emoteIsFinished; } } private int GetCurrentEmoteLoopCount() => - Mathf.RoundToInt(currentEmote.time / currentEmote.length); + lastExtendedEmoteData.GetLoopCount(); + public void StopEmote() { @@ -414,33 +438,27 @@ public void StopEmote() private void StopEmoteInternal(bool immediate) { - if (string.IsNullOrEmpty(blackboard.expressionTriggerId)) return; - if (animation.GetClip(blackboard.expressionTriggerId) == null) return; - - animation.Blend(blackboard.expressionTriggerId, 0, !immediate ? EXPRESSION_EXIT_TRANSITION_TIME : 0); blackboard.expressionTriggerId = null; - blackboard.shouldLoop = false; - lastExtendedEmoteData?.Stop(); + lastExtendedEmoteData?.Stop(immediate); if (!immediate) OnUpdateWithDeltaTime(blackboard.deltaTime); } private void StartEmote(string emoteId, bool spatial, float volume, bool occlude) { - if (!string.IsNullOrEmpty(emoteId)) - { - lastExtendedEmoteData?.Stop(); + lastExtendedEmoteData?.Stop(false); - if (emoteClipDataMap.TryGetValue(emoteId, out var emoteClipData)) - { - lastExtendedEmoteData = emoteClipData; - emoteClipData.Play(gameObject.layer, spatial, volume, occlude); - } - } - else - { - lastExtendedEmoteData?.Stop(); - } + if (string.IsNullOrEmpty(emoteId)) return; + if (!emoteClipDataMap.TryGetValue(emoteId, out var emoteClipData)) return; + + blackboard.expressionTriggerId = emoteId; + blackboard.shouldLoop = emoteClipData.IsLoop(); + + lastExtendedEmoteData = emoteClipData; + emoteClipData.Play(gameObject.layer, spatial, volume, occlude); + + if (lastExtendedEmoteData.IsSequential() && isOwnPlayer) + dataStorePlayer.canPlayerMove.Set(false); } public void Reset() @@ -463,33 +481,19 @@ public void PlayEmote(string emoteId, long timestamps, bool spatial, float volum if (string.IsNullOrEmpty(emoteId)) return; - if (animation.GetClip(emoteId) == null) - return; - - bool loop = emoteClipDataMap.TryGetValue(emoteId, out var clipData) && clipData.Loop; - bool mustTriggerAnimation = !string.IsNullOrEmpty(emoteId) && (blackboard.expressionTriggerTimestamp != timestamps || ignoreTimestamp); + bool loop = emoteClipDataMap.TryGetValue(emoteId, out var clipData) && clipData.IsLoop(); + var mustTriggerAnimation = !string.IsNullOrEmpty(emoteId) && (blackboard.expressionTriggerTimestamp != timestamps || ignoreTimestamp); if (loop && blackboard.expressionTriggerId == emoteId) return; - blackboard.expressionTriggerId = emoteId; - blackboard.expressionTriggerTimestamp = timestamps; if (mustTriggerAnimation || loop) { + StopEmoteInternal(true); StartEmote(emoteId, spatial, volume, occlude); + blackboard.expressionTriggerTimestamp = timestamps; - if (!string.IsNullOrEmpty(emoteId)) - { - animation.Stop(emoteId); - latestAnimationState = AvatarAnimation.IDLE; - } - - blackboard.shouldLoop = loop; - - CrossFadeTo(AvatarAnimation.EMOTE, emoteId, EXPRESSION_EXIT_TRANSITION_TIME, PlayMode.StopAll); - - currentEmote = animation[emoteId]; lastEmoteLoopCount = GetCurrentEmoteLoopCount(); currentState = State_Expression; } @@ -507,24 +511,13 @@ public void EquipBaseClip(AnimationClip clip) animation.AddClip(clip, clipId); } - public void EquipEmote(string emoteId, EmoteClipData emoteClipData) + public void EquipEmote(string emoteId, EmoteAnimationData emoteAnimationData) { if (animation == null) return; - if (animation.GetClip(emoteId) != null) - animation.RemoveClip(emoteId); - - emoteClipDataMap[emoteId] = emoteClipData; - - animation.AddClip(emoteClipData.AvatarClip, emoteId); - - if (emoteClipData.ExtraContent != null) - { - emoteClipData.ExtraContent.transform.SetParent(animation.transform.parent, false); - emoteClipData.ExtraContent.transform.ResetLocalTRS(); - emoteClipData.ExtraContent.transform.localPosition = animation.transform.localPosition; - } + emoteClipDataMap[emoteId] = emoteAnimationData; + emoteAnimationData.Equip(animation); } public void UnequipEmote(string emoteId) @@ -535,19 +528,14 @@ public void UnequipEmote(string emoteId) if (animation.GetClip(emoteId) == null) return; - animation.RemoveClip(emoteId); - if (emoteClipDataMap.TryGetValue(emoteId, out var emoteClipData)) - { - if (emoteClipData.ExtraContent != null) - emoteClipData.ExtraContent.transform.SetParent(null, false); - } + emoteClipData.UnEquip(); } private void InitializeAvatarAudioAndParticleHandlers(Animation createdAnimation) { //NOTE(Mordi): Adds handler for animation events, and passes in the audioContainer for the avatar - AvatarAnimationEventHandler animationEventHandler = createdAnimation.gameObject.GetOrCreateComponent(); + AvatarAnimationEventHandler animationEventHandler = GetOrCreateComponent(createdAnimation.gameObject); AudioContainer audioContainer = transform.GetComponentInChildren(); if (audioContainer != null) diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/CharacterController/DCLCharacterController.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/CharacterController/DCLCharacterController.cs index a4d465dbf9..d4030a3f18 100644 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/CharacterController/DCLCharacterController.cs +++ b/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/CharacterController/DCLCharacterController.cs @@ -267,15 +267,14 @@ bool Moved(Vector3 previousPosition, bool useThreshold = false) internal void LateUpdate() { - if(!dataStorePlayer.canPlayerMove.Get()) - return; - if (transform.position.y < minimumYPosition) { SetPosition(characterPosition.worldPosition); return; } + bool canMove = dataStorePlayer.canPlayerMove.Get(); + if (freeMovementController.IsActive()) { velocity = freeMovementController.CalculateMovement(); @@ -306,7 +305,11 @@ internal void LateUpdate() // Horizontal movement var speed = movementSpeed * (isWalking ? runningSpeedMultiplier : 1f); - transform.forward = characterForward.Get().Value; + if (!canMove) + speed = 0; + + if(canMove) + transform.forward = characterForward.Get().Value; var xzPlaneForward = Vector3.Scale(cameraForward.Get(), new Vector3(1, 0, 1)); var xzPlaneRight = Vector3.Scale(cameraRight.Get(), new Vector3(1, 0, 1)); @@ -331,12 +334,17 @@ internal void LateUpdate() forwardTarget.Normalize(); velocity += forwardTarget * speed; - CommonScriptableObjects.playerUnityEulerAngles.Set(transform.eulerAngles); + + if(canMove) + CommonScriptableObjects.playerUnityEulerAngles.Set(transform.eulerAngles); } bool jumpButtonPressedWithGraceTime = jumpButtonPressed && (Time.time - lastJumpButtonPressedTime < 0.15f); - if (jumpButtonPressedWithGraceTime) // almost-grounded jump button press allowed time + if (jumpButtonPressedWithGraceTime) + isMovingByUserInput = true; + + if (jumpButtonPressedWithGraceTime && canMove) // almost-grounded jump button press allowed time { bool justLeftGround = (Time.time - lastUngroundedTime) < 0.1f; @@ -372,6 +380,7 @@ internal void LateUpdate() { SaveLateUpdateGroundTransforms(); } + OnUpdateFinish?.Invoke(Time.deltaTime); } diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/AvatarEditorHUD/Scripts/EmotesCustomization/EmotesCustomizationComponentController.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/AvatarEditorHUD/Scripts/EmotesCustomization/EmotesCustomizationComponentController.cs index 080170e61b..b76ec00851 100644 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/AvatarEditorHUD/Scripts/EmotesCustomization/EmotesCustomizationComponentController.cs +++ b/unity-renderer/Assets/Scripts/MainScripts/DCL/Controllers/HUD/AvatarEditorHUD/Scripts/EmotesCustomization/EmotesCustomizationComponentController.cs @@ -213,7 +213,7 @@ internal void RefreshEmoteLoadingState(string emoteId) if (emoteCard != null) { emoteCard.SetAsLoading(false); - emoteCard.SetSoundIcon(emoteReference.GetData().AudioSource != null); + emoteCard.SetSoundIcon(emoteReference.GetData().HasAudio()); emotesInLoadingState.Remove(emoteId); } } diff --git a/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/EmoteClipData.cs b/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/EmoteClipData.cs deleted file mode 100644 index 4a924bf464..0000000000 --- a/unity-renderer/Assets/Scripts/MainScripts/DCL/Models/AvatarAssets/EmoteClipData.cs +++ /dev/null @@ -1,111 +0,0 @@ -using JetBrains.Annotations; -using System; -using UnityEngine; - -namespace DCL.Emotes -{ - public class EmoteClipData - { - public AnimationClip AvatarClip { get; } - public bool Loop { get; } - - [CanBeNull] private Animation animation; - [CanBeNull] public GameObject ExtraContent { get; set; } - - [CanBeNull] private Renderer[] renderers; - - [CanBeNull] public AudioSource AudioSource { get; } - - public EmoteClipData(AnimationClip avatarClip, bool loop = false) - { - this.AvatarClip = avatarClip; - this.Loop = loop; - } - - public EmoteClipData(AnimationClip mainClip, GameObject container, AudioSource audioSource, bool loop = false) - { - this.AvatarClip = mainClip; - this.Loop = loop; - this.ExtraContent = container; - this.AudioSource = audioSource; - - if (ExtraContent == null) return; - - animation = ExtraContent.GetComponentInChildren(); - - if (animation == null) - Debug.LogError($"Animation {AvatarClip.name} extra content does not have an animation"); - else - { - animation.wrapMode = WrapMode.Default; - - foreach (AnimationState state in animation) - { - if (state.clip == AvatarClip) continue; - state.wrapMode = Loop ? WrapMode.Loop : WrapMode.Once; - } - } - - renderers = ExtraContent.GetComponentsInChildren(); - } - - public void Play(int gameObjectLayer, bool spatial, float volume, bool occlude) - { - if (renderers != null) - { - foreach (Renderer renderer in renderers) - { - renderer.enabled = true; - renderer.gameObject.layer = gameObjectLayer; - renderer.allowOcclusionWhenDynamic = occlude; - } - } - - if (animation != null) - { - animation.enabled = true; - - foreach (AnimationState state in animation) - { - if (state.clip == AvatarClip) continue; - - // this reduntant stop is intended, sometimes when animations are triggered their first frame is not 0 - animation.Stop(state.clip.name); - animation.Play(state.clip.name, PlayMode.StopAll); - } - } - - if (AudioSource == null) return; - - AudioSource.spatialBlend = spatial ? 1 : 0; - AudioSource.volume = volume; - AudioSource.loop = Loop; - AudioSource.Play(); - } - - public void Stop() - { - if (renderers != null) - { - foreach (Renderer renderer in renderers) - { - renderer.enabled = false; - } - } - - if (animation != null) - { - foreach (AnimationState state in animation) - { - if (state.clip == AvatarClip) continue; - animation.Stop(state.clip.name); - } - - animation.enabled = false; - } - - if (AudioSource != null) - AudioSource.Stop(); - } - } -}