Skip to content
Closed
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
Expand Up @@ -255,7 +255,7 @@ private void UpdateVisibilityState(ref AvatarShapeComponent avatarShape, IAvatar

avatarCachedVisibility.IsVisible = shouldBeHidden;

avatarView.AvatarAnimator.enabled = shouldPlayFootstepFX;
avatarView.AvatarAnimator.enabled = shouldPlayFootstepFX && !avatarView.IsLegacyAnimationPlaying;
avatarView.AvatarAnimator.fireEvents = shouldPlayFootstepFX;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
using DCL.AvatarRendering.AvatarShape.UnityInterface;
using NUnit.Framework;
using UnityEditor;
using UnityEngine;
using Utility.Animations;

namespace DCL.AvatarRendering.AvatarShape.Tests
{
public class AvatarBaseLegacyAnimationShould
{
private const string AVATAR_BASE_TEST_ASSET_PATH = "Assets/DCL/AvatarRendering/AvatarShape/Tests/Instantiate/TestAssets/AvatarBase_TestAsset.prefab";

private GameObject avatarGameObject = null!;
private AvatarBase avatarBase = null!;

[SetUp]
public void SetUp()
{
var avatarBasePrefab = AssetDatabase.LoadAssetAtPath<GameObject>(AVATAR_BASE_TEST_ASSET_PATH);
Assert.IsNotNull(avatarBasePrefab, $"Could not load AvatarBase test prefab from {AVATAR_BASE_TEST_ASSET_PATH}");

avatarGameObject = Object.Instantiate(avatarBasePrefab);
avatarBase = avatarGameObject.GetComponentInChildren<AvatarBase>();
Assert.IsNotNull(avatarBase, "AvatarBase component not found on test prefab");
Assert.IsNotNull(avatarBase.AvatarAnimator, "AvatarAnimator not configured on test prefab");
}

[TearDown]
public void TearDown()
{
if (avatarGameObject != null) Object.DestroyImmediate(avatarGameObject);
}

[Test]
public void AddOrGetLegacyAnimation_CreatesAnimationOnAvatarAnimatorGameObject()
{
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();

Assert.IsNotNull(legacyAnimation);
Assert.AreSame(legacyAnimation, avatarBase.LegacyAnimation);
Assert.AreSame(avatarBase.AvatarAnimator.gameObject, legacyAnimation.gameObject,
"PlayLegacyEmote expects the Animation component to live on the same GameObject as the Animator.");
}

[Test]
public void AddOrGetLegacyAnimation_ReturnsSameInstance_OnRepeatedCalls()
{
Animation first = avatarBase.AddOrGetLegacyAnimation();
Animation second = avatarBase.AddOrGetLegacyAnimation();

Assert.AreSame(first, second,
"Repeated calls must not create duplicate Animation components — AddClip relies on a single stable Animation instance.");
}

[Test]
public void IsLegacyAnimationPlaying_ReturnsFalse_WhenLegacyAnimationExistsButIsNotPlaying()
{
avatarBase.AddOrGetLegacyAnimation();

Assert.IsFalse(avatarBase.IsLegacyAnimationPlaying,
"An idle Animation component must not gate the Mecanim animator or its setters.");
}

[Test]
public void StopLegacyAnimation_DoesNotThrow_WhenLegacyAnimationIsIdle()
{
avatarBase.AddOrGetLegacyAnimation();

Assert.DoesNotThrow(() => avatarBase.StopLegacyAnimation());
}

[Test]
public void SetAnimatorTrigger_EnablesAnimator_WhenLegacyNotPlaying()
{
avatarBase.AvatarAnimator.enabled = false;

avatarBase.SetAnimatorTrigger(AnimationHashes.EMOTE);

Assert.IsTrue(avatarBase.AvatarAnimator.enabled,
"With no legacy animation playing, Mecanim setters must re-enable the Animator to process the new trigger.");
}

[Test]
public void ReplaceEmoteAnimation_EnablesAnimator_WhenLegacyNotPlaying()
{
avatarBase.AvatarAnimator.enabled = false;
var clip = new AnimationClip { legacy = false };

avatarBase.ReplaceEmoteAnimation(clip);

Assert.IsTrue(avatarBase.AvatarAnimator.enabled,
"ReplaceEmoteAnimation must re-enable the Mecanim animator when no legacy animation is blocking it.");

Object.DestroyImmediate(clip);
}

[Test]
public void StopLegacyAnimation_StopsAPlayingLegacyClip()
{
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();
AnimationClip clip = CreateLegacyClip();

legacyAnimation.AddClip(clip, clip.name);
legacyAnimation.Play(clip.name);

Assume.That(avatarBase.IsLegacyAnimationPlaying, Is.True,
"Sanity check: legacy clip should report as playing right after Play() in EditMode.");

avatarBase.StopLegacyAnimation();

Assert.IsFalse(avatarBase.IsLegacyAnimationPlaying,
"StopLegacyAnimation must clear the playing flag so the IsLegacyAnimationPlaying gate releases for the next emote.");

Object.DestroyImmediate(clip);
}

[Test]
public void SetAnimatorTrigger_Ignored_WhileLegacyAnimationIsPlaying()
{
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();
AnimationClip clip = CreateLegacyClip();
legacyAnimation.AddClip(clip, clip.name);
legacyAnimation.Play(clip.name);
Assume.That(avatarBase.IsLegacyAnimationPlaying, Is.True);

avatarBase.AvatarAnimator.enabled = false;

avatarBase.SetAnimatorTrigger(AnimationHashes.EMOTE);

Assert.IsFalse(avatarBase.AvatarAnimator.enabled,
"While a legacy Animation is playing the Mecanim animator must stay disabled — otherwise motion systems stomp the legacy pose every frame.");

Object.DestroyImmediate(clip);
}

[Test]
public void ReplaceEmoteAnimation_DoesNotEnableAnimator_WhileLegacyAnimationIsPlaying()
{
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();
AnimationClip clip = CreateLegacyClip();
legacyAnimation.AddClip(clip, clip.name);
legacyAnimation.Play(clip.name);
Assume.That(avatarBase.IsLegacyAnimationPlaying, Is.True);

avatarBase.AvatarAnimator.enabled = false;
var mecanimClip = new AnimationClip { legacy = false };

avatarBase.ReplaceEmoteAnimation(mecanimClip);

Assert.IsFalse(avatarBase.AvatarAnimator.enabled,
"Overriding the emote slot while legacy is playing must not re-enable the animator — the switch happens after StopLegacyAnimation releases the gate.");

Object.DestroyImmediate(clip);
Object.DestroyImmediate(mecanimClip);
}

private static AnimationClip CreateLegacyClip()
{
var clip = new AnimationClip { name = "TestLegacyClip", legacy = true };
clip.SetCurve(string.Empty, typeof(Transform), "localPosition.x", AnimationCurve.Linear(0, 0, 1, 1));
return clip;
}
}
}

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
Expand Up @@ -468,6 +468,41 @@ public void CombineMultipleHiddenReasons()
Assert.IsTrue(updatedHiddenComponent.Reason.HasFlag(HiddenPlayerComponent.HiddenReason.BANNED));
}

[Test]
public void KeepAnimatorDisabledWhileLegacyAnimationIsPlaying()
{
// Arrange — a non-player avatar that starts hidden so the first update caches IsVisible=true.
var avatarShape = CreateAvatarShapeComponent();
avatarShape.HiddenByModifierArea = true;
Entity avatarEntity = world.Create(avatarShape, avatarBase, new CharacterEmoteComponent());

// Prime the cached state — marks the avatar as hidden so the next update's visible-transition is NOT early-returned.
system.Update(0);

// Start a legacy Animation on the avatar — LSD/Builder-preview FullBody emotes run through this component.
Animation legacyAnimation = avatarBase.AddOrGetLegacyAnimation();
var clip = new AnimationClip { name = "TestLegacyClip", legacy = true };
clip.SetCurve(string.Empty, typeof(Transform), "localPosition.x", AnimationCurve.Linear(0, 0, 1, 1));
legacyAnimation.AddClip(clip, clip.name);
legacyAnimation.Play(clip.name);

Assume.That(avatarBase.IsLegacyAnimationPlaying, Is.True,
"Sanity check: legacy clip should report as playing right after Play() in EditMode.");

// Unhide so the system must transition to "visible" — this is the code path that sets AvatarAnimator.enabled.
ref var shapeRef = ref world.Get<AvatarShapeComponent>(avatarEntity);
shapeRef.HiddenByModifierArea = false;

// Act
system.Update(0);

// Assert — legacy playback must keep the Mecanim animator disabled so motion systems don't stomp the legacy pose every frame.
Assert.IsFalse(avatarBase.AvatarAnimator.enabled,
"AvatarShapeVisibilitySystem must not re-enable the Animator while a legacy Animation is playing.");

Object.DestroyImmediate(clip);
}

[Test]
public void HidePlayerAvatarWhenTransitioningToFirstPersonAndCloseToCameraStart()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public class AvatarBase : MonoBehaviour, IAvatarView
[field: SerializeField] public Animator AvatarAnimator { get; private set; }
[field: SerializeField] public AvatarAudioPlaybackController AudioPlaybackController { get; private set; }

public Animation? LegacyAnimation { get; private set; }

[field: SerializeField] public RigBuilder RigBuilder { get; private set; }

[field: SerializeField] public SkinnedMeshRenderer AvatarSkinnedMeshRenderer { get; private set; }
Expand Down Expand Up @@ -157,6 +159,20 @@ private void Awake()
public Transform GetTransform() =>
transform;

public Animation AddOrGetLegacyAnimation()
{
if (LegacyAnimation != null) return LegacyAnimation;
LegacyAnimation = AvatarAnimator.gameObject.AddComponent<Animation>();
return LegacyAnimation;
}

public bool IsLegacyAnimationPlaying => LegacyAnimation != null && LegacyAnimation.isPlaying;

public void StopLegacyAnimation()
{
if (LegacyAnimation != null) LegacyAnimation.Stop();
}

public void SetPointAtLayerWeight(float weight)
{
AvatarAnimator.SetLayerWeight(rightPointAtLayerIndex, weight);
Expand All @@ -169,6 +185,7 @@ public void SetRotationLayerWeight(float weight)

public void SetAnimatorFloat(int hash, float value)
{
if (IsLegacyAnimationPlaying) return;
if (AvatarAnimator.GetFloat(hash).Equals(value)) return;

AvatarAnimator.enabled = true;
Expand All @@ -177,6 +194,7 @@ public void SetAnimatorFloat(int hash, float value)

public void SetAnimatorInt(int hash, int value)
{
if (IsLegacyAnimationPlaying) return;
if (AvatarAnimator.GetInteger(hash).Equals(value)) return;

AvatarAnimator.enabled = true;
Expand All @@ -185,6 +203,8 @@ public void SetAnimatorInt(int hash, int value)

public void SetAnimatorTrigger(int hash)
{
if (IsLegacyAnimationPlaying) return;

AvatarAnimator.enabled = true;
AvatarAnimator.SetTrigger(hash);
}
Expand Down Expand Up @@ -217,6 +237,7 @@ public void ResetState()
{
ResetArmatureInclination();
transform.localPosition = Vector3.zero;
LegacyAnimation?.Stop();
AvatarAnimator.Rebind();
HipsConstraint.data.offset = Vector3.zero;
HipsConstraint.weight = 0;
Expand All @@ -234,6 +255,7 @@ public bool GetAnimatorBool(int hash) =>

public void SetAnimatorBool(int hash, bool value)
{
if (IsLegacyAnimationPlaying) return;
if (AvatarAnimator.GetBool(hash) == value) return;

AvatarAnimator.enabled = true;
Expand All @@ -251,6 +273,8 @@ public void ReplaceEmoteAnimation(AnimationClip animationClip)
AvatarAnimator.runtimeAnimatorController = overrideController;

lastEmote = animationClip;

if (IsLegacyAnimationPlaying) return;
AvatarAnimator.enabled = true;
}

Expand All @@ -262,6 +286,8 @@ public void ReplaceMaskedEmoteAnimation(AnimationClip animationClip)
AvatarAnimator.runtimeAnimatorController = overrideController;

lastMaskedEmote = animationClip;

if (IsLegacyAnimationPlaying) return;
AvatarAnimator.enabled = true;
}

Expand Down Expand Up @@ -341,6 +367,12 @@ public interface IAvatarView

void ClearMaskedEmoteAnimationCache();

Animation AddOrGetLegacyAnimation();

bool IsLegacyAnimationPlaying { get; }

void StopLegacyAnimation();

float GetAnimatorFloat(int hash);

bool IsAnimatorInTag(int hashTag);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,13 @@ public struct CharacterEmoteComponent
private int currentAnimationTag;

public float PlayingEmoteDuration => CurrentEmoteReference?.avatarClip
? CurrentEmoteReference.avatarClip.length * CurrentEmoteReference.animatorComp!.speed
? CurrentEmoteReference.avatarClip.length * (CurrentEmoteReference.animatorComp != null ? CurrentEmoteReference.animatorComp.speed : 1f)
: 0f;

public readonly bool IsPlayingEmote =>
currentAnimationTag == AnimationHashes.EMOTE || currentAnimationTag == AnimationHashes.EMOTE_LOOP;
(CurrentEmoteReference != null && CurrentEmoteReference.legacy)
|| currentAnimationTag == AnimationHashes.EMOTE
|| currentAnimationTag == AnimationHashes.EMOTE_LOOP;
Comment thread
pravusjif marked this conversation as resolved.

public readonly int CurrentAnimationTag => currentAnimationTag;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,19 @@ public class EmoteReferences : MonoBehaviour
public AnimationClip? avatarClip { get; private set; }
public AnimationClip? propClip { get; private set; }
public Animator? animatorComp { get; private set; }
public Animation? animationComp { get; private set; }
public bool legacy { get; private set; }

public AudioSource? audioSource;

public void Initialize(AnimationClip? animationClip, AnimationClip? propClip, Animator? animatorComp, int propClipHash)
public void Initialize(AnimationClip? animationClip, AnimationClip? propClip, Animator? animatorComp, Animation? animationComp, int propClipHash, bool legacy)
{
this.avatarClip = animationClip;
this.propClip = propClip;
this.animatorComp = animatorComp;
this.animationComp = animationComp;
this.propClipHash = propClipHash;
this.legacy = legacy;
}
}
}
Loading
Loading