Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Sequential emotes #5771

Draft
wants to merge 3 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -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()
{
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Animation>();

if (extraContentAnimation == null)
Debug.LogError($"Animation {avatarClip.name} extra content does not have an animation");

renderers = extraContent.GetComponentsInChildren<Renderer>();
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
"name": "EmotesServiceDefinitions",
"rootNamespace": "",
"references": [
"GUID:7ac9f9c835ec1084ab35e3f9b176cf1e",
"GUID:3b80b0b562b1cbc489513f09fc1b8f69",
"GUID:f51ebe6a0ceec4240a699833d6309b23",
"GUID:99cea83ca76dcd846abed7e61a8c90bd"
"GUID:99cea83ca76dcd846abed7e61a8c90bd",
"GUID:7ac9f9c835ec1084ab35e3f9b176cf1e"
],
"includePlatforms": [],
"excludePlatforms": [],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Threading;
using Cysharp.Threading.Tasks;
using DCLServices.EmotesService.Domain;
using UnityEngine;

namespace DCL.Emotes
Expand All @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Loading