< Summary

Class:AvatarAnimatorLegacy
Assembly:AvatarShape
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs
Covered lines:17
Uncovered lines:181
Coverable lines:198
Total lines:536
Line coverage:8.5% (17 of 198)
Covered branches:0
Total branches:0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
AvatarAnimatorLegacy()0%110100%
Start()0%110100%
Prepare(...)0%20400%
PrepareLocomotionAnims(...)0%12300%
OnEventHandlerUpdate()0%2100%
OnPoolGet()0%220100%
OnPoolRelease()0%20400%
OnUpdateWithDeltaTime(...)0%6200%
UpdateInterface()0%56700%
State_Init(...)0%6200%
State_Ground(...)0%56700%
CrossFadeTo(...)0%20400%
State_Air(...)0%12300%
ExpressionGroundTransitionCondition(...)0%12300%
ExpressionAirTransitionCondition(...)0%2100%
State_Expression(...)0%56700%
SetExpressionValues(...)0%64.659011.76%
Reset()0%6200%
SetIdleFrame()0%2100%
PlayEmote(...)0%110100%
EquipBaseClip(...)0%12300%
EquipEmote(...)0%20400%
UnequipEmote(...)0%12300%
InitializeAvatarAudioAndParticleHandlers(...)0%12300%
OnEnable()0%2.52050%
OnDisable()0%2.862040%
OnDestroy()0%2.152066.67%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using AvatarSystem;
 4using DCL;
 5using DCL.Components;
 6using DCL.Emotes;
 7using DCL.Helpers;
 8using UnityEngine;
 9using Environment = DCL.Environment;
 10
 11public enum AvatarAnimation
 12{
 13    IDLE,
 14    RUN,
 15    WALK,
 16    EMOTE,
 17    JUMP,
 18    FALL,
 19}
 20
 21public static class AvatarAnimationExtensions
 22{
 23    public static bool ShouldLoop(this AvatarAnimation avatarAnimation)
 24    {
 25        return avatarAnimation == AvatarAnimation.RUN ||
 26               avatarAnimation == AvatarAnimation.IDLE ||
 27               avatarAnimation == AvatarAnimation.FALL ||
 28               avatarAnimation == AvatarAnimation.WALK;
 29    }
 30}
 31
 32public class AvatarAnimatorLegacy : MonoBehaviour, IPoolLifecycleHandler, IAnimator
 33{
 34    const float IDLE_TRANSITION_TIME = 0.2f;
 35    const float RUN_TRANSITION_TIME = 0.15f;
 36    const float WALK_TRANSITION_TIME = 0.15f;
 37    const float WALK_MAX_SPEED = 6;
 38    const float RUN_MIN_SPEED = 4f;
 39    const float WALK_MIN_SPEED = 0.1f;
 40    const float WALK_RUN_SWITCH_TIME = 1.5f;
 41    const float JUMP_TRANSITION_TIME = 0.01f;
 42    const float FALL_TRANSITION_TIME = 0.5f;
 43    const float EXPRESSION_EXIT_TRANSITION_TIME = 0.2f;
 44    const float EXPRESSION_ENTER_TRANSITION_TIME = 0.1f;
 45    const float OTHER_PLAYER_MOVE_THRESHOLD = 0.02f;
 46
 47    const float AIR_EXIT_TRANSITION_TIME = 0.2f;
 48
 49    const float ELEVATION_OFFSET = 0.6f;
 50    const float RAY_OFFSET_LENGTH = 3.0f;
 51
 52    // Time it takes to determine if a character is grounded when vertical velocity is 0
 53    const float FORCE_GROUND_TIME = 0.05f;
 54
 55    // Minimum vertical speed used to consider whether an avatar is on air
 56    const float MIN_VERTICAL_SPEED_AIR = 0.025f;
 57
 58    [System.Serializable]
 59    public class AvatarLocomotion
 60    {
 61        public AnimationClip idle;
 62        public AnimationClip walk;
 63        public AnimationClip run;
 64        public AnimationClip jump;
 65        public AnimationClip fall;
 66    }
 67
 68    [System.Serializable]
 69    public class BlackBoard
 70    {
 71        public float walkSpeedFactor;
 72        public float runSpeedFactor;
 73        public float movementSpeed;
 74        public float verticalSpeed;
 75        public bool isGrounded;
 76        public string expressionTriggerId;
 77        public long expressionTriggerTimestamp;
 78        public float deltaTime;
 79        public bool shouldLoop;
 80    }
 81
 82    [SerializeField] internal AvatarLocomotion femaleLocomotions;
 83    [SerializeField] internal AvatarLocomotion maleLocomotions;
 84    AvatarLocomotion currentLocomotions;
 85
 86    public new Animation animation;
 87    public BlackBoard blackboard;
 88    public Transform target;
 89
 90    internal System.Action<BlackBoard> currentState;
 91
 92    Vector3 lastPosition;
 93    bool isOwnPlayer = false;
 94    private AvatarAnimationEventHandler animEventHandler;
 95
 96    private float lastOnAirTime = 0;
 97
 81198    private Dictionary<string, EmoteClipData> emoteClipDataMap =
 99        new Dictionary<string, EmoteClipData>();
 100
 101    private string runAnimationName;
 102    private string walkAnimationName;
 103    private string idleAnimationName;
 104    private string jumpAnimationName;
 105    private string fallAnimationName;
 106    private AvatarAnimation latestAnimation;
 107    private AnimationState runAnimationState;
 108    private AnimationState walkAnimationState;
 109    private bool isUpdateRegistered = false;
 110
 111    private Ray rayCache;
 112
 766113    public void Start() { OnPoolGet(); }
 114
 115    // AvatarSystem entry points
 116    public bool Prepare(string bodyshapeId, GameObject container)
 117    {
 0118        if (!container.transform.TryFindChildRecursively("Armature", out Transform armature))
 119        {
 0120            Debug.LogError($"Couldn't find Armature for AnimatorLegacy in path: {transform.GetHierarchyPath()}");
 121
 0122            return false;
 123        }
 124
 0125        Transform armatureParent = armature.parent;
 0126        animation = armatureParent.gameObject.GetOrCreateComponent<Animation>();
 0127        armatureParent.gameObject.GetOrCreateComponent<StickerAnimationListener>();
 128
 0129        PrepareLocomotionAnims(bodyshapeId);
 0130        SetIdleFrame();
 0131        animation.Sample();
 0132        InitializeAvatarAudioAndParticleHandlers(animation);
 133
 134        // since the avatar can be updated when changing a wearable we shouldn't register to the update event twice
 0135        if (!isUpdateRegistered)
 136        {
 0137            isUpdateRegistered = true;
 138
 0139            if (isOwnPlayer)
 140            {
 0141                DCLCharacterController.i.OnUpdateFinish += OnUpdateWithDeltaTime;
 142            }
 143            else
 144            {
 0145                Environment.i.platform.updateEventHandler.AddListener(IUpdateEventHandler.EventType.Update, OnEventHandl
 146            }
 147
 148        }
 149
 0150        return true;
 151    }
 152
 153    private void PrepareLocomotionAnims(string bodyshapeId)
 154    {
 0155        if (bodyshapeId.Contains(WearableLiterals.BodyShapes.MALE))
 156        {
 0157            currentLocomotions = maleLocomotions;
 158        }
 0159        else if (bodyshapeId.Contains(WearableLiterals.BodyShapes.FEMALE))
 160        {
 0161            currentLocomotions = femaleLocomotions;
 162        }
 163
 0164        EquipBaseClip(currentLocomotions.idle);
 0165        EquipBaseClip(currentLocomotions.walk);
 0166        EquipBaseClip(currentLocomotions.run);
 0167        EquipBaseClip(currentLocomotions.jump);
 0168        EquipBaseClip(currentLocomotions.fall);
 169
 0170        idleAnimationName = currentLocomotions.idle.name;
 0171        walkAnimationName = currentLocomotions.walk.name;
 0172        runAnimationName = currentLocomotions.run.name;
 0173        jumpAnimationName = currentLocomotions.jump.name;
 0174        fallAnimationName = currentLocomotions.fall.name;
 175
 0176        runAnimationState = animation[runAnimationName];
 0177        walkAnimationState = animation[walkAnimationName];
 0178    }
 0179    private void OnEventHandlerUpdate() { OnUpdateWithDeltaTime(Time.deltaTime); }
 180
 181    public void OnPoolGet()
 182    {
 383183        if (DCLCharacterController.i != null)
 184        {
 383185            isOwnPlayer = DCLCharacterController.i.transform == transform.parent;
 186
 187            // NOTE: disable MonoBehaviour's update to use DCLCharacterController event instead
 383188            this.enabled = !isOwnPlayer;
 189        }
 190
 383191        currentState = State_Init;
 383192    }
 193
 194    public void OnPoolRelease()
 195    {
 0196        if (isUpdateRegistered)
 197        {
 0198            isUpdateRegistered = false;
 199
 0200            if (isOwnPlayer && DCLCharacterController.i)
 201            {
 0202                DCLCharacterController.i.OnUpdateFinish -= OnUpdateWithDeltaTime;
 203            }
 204            else
 205            {
 0206                Environment.i.platform.updateEventHandler.RemoveListener(IUpdateEventHandler.EventType.Update, OnEventHa
 207            }
 208        }
 0209    }
 210
 211    void OnUpdateWithDeltaTime(float deltaTime)
 212    {
 0213        blackboard.deltaTime = deltaTime;
 0214        UpdateInterface();
 0215        currentState?.Invoke(blackboard);
 0216    }
 217
 218    void UpdateInterface()
 219    {
 0220        Vector3 velocityTargetPosition = target.position;
 0221        Vector3 flattenedVelocity = velocityTargetPosition - lastPosition;
 222
 223        //NOTE(Brian): Vertical speed
 0224        float verticalVelocity = flattenedVelocity.y;
 225
 226        //NOTE(Kinerius): if we have more or less than zero we consider that we are either jumping or falling
 0227        if (Mathf.Abs(verticalVelocity) > MIN_VERTICAL_SPEED_AIR)
 228        {
 0229            lastOnAirTime = Time.time;
 230        }
 231
 0232        blackboard.verticalSpeed = verticalVelocity;
 233
 0234        flattenedVelocity.y = 0;
 235
 0236        if (isOwnPlayer)
 0237            blackboard.movementSpeed = flattenedVelocity.magnitude - DCLCharacterController.i.movingPlatformSpeed;
 238        else
 0239            blackboard.movementSpeed = flattenedVelocity.magnitude;
 240
 0241        Vector3 rayOffset = Vector3.up * RAY_OFFSET_LENGTH;
 242
 243        //NOTE(Kinerius): This check is just for the playing character, it uses a combination of collision flags and ray
 0244        bool isGroundedByCharacterController = isOwnPlayer && DCLCharacterController.i.isGrounded;
 245
 246        //NOTE(Kinerius): This check is for interpolated avatars (the other players) as we dont have a Character Control
 247        //                this check is cheap and fast but not precise
 0248        bool isGroundedByVelocity = !isOwnPlayer && Time.time - lastOnAirTime > FORCE_GROUND_TIME;
 249
 250        //NOTE(Kinerius): This additional check is both for the player and interpolated avatars, we cast an additional r
 0251        bool isGroundedByRaycast = false;
 252
 0253        if (!isGroundedByCharacterController && !isGroundedByVelocity)
 254        {
 0255            rayCache.origin = velocityTargetPosition + rayOffset;
 0256            rayCache.direction = Vector3.down;
 257
 0258            isGroundedByRaycast = Physics.Raycast(rayCache,
 259                RAY_OFFSET_LENGTH - ELEVATION_OFFSET,
 260                DCLCharacterController.i.groundLayers);
 261
 262        }
 263
 0264        blackboard.isGrounded = isGroundedByCharacterController || isGroundedByVelocity || isGroundedByRaycast;
 265
 0266        lastPosition = velocityTargetPosition;
 0267    }
 268
 269    void State_Init(BlackBoard bb)
 270    {
 0271        if (bb.isGrounded)
 272        {
 0273            currentState = State_Ground;
 274        }
 275        else
 276        {
 0277            currentState = State_Air;
 278        }
 0279    }
 280
 281    void State_Ground(BlackBoard bb)
 282    {
 0283        if (bb.deltaTime <= 0)
 0284            return;
 285
 0286        float movementSpeed = bb.movementSpeed / bb.deltaTime;
 287
 0288        runAnimationState.normalizedSpeed = movementSpeed * bb.runSpeedFactor;
 0289        walkAnimationState.normalizedSpeed = movementSpeed * bb.walkSpeedFactor;
 290
 0291        if (movementSpeed >= WALK_MAX_SPEED)
 292        {
 0293            CrossFadeTo(AvatarAnimation.RUN, runAnimationName, RUN_TRANSITION_TIME);
 294        }
 0295        else if (movementSpeed >= RUN_MIN_SPEED && movementSpeed < WALK_MAX_SPEED)
 296        {
 297            // Keep current animation, leave empty
 298        }
 0299        else if (movementSpeed > WALK_MIN_SPEED)
 300        {
 0301            CrossFadeTo(AvatarAnimation.WALK, walkAnimationName, WALK_TRANSITION_TIME);
 302        }
 303        else
 304        {
 0305            CrossFadeTo(AvatarAnimation.IDLE, idleAnimationName, IDLE_TRANSITION_TIME);
 306        }
 307
 308
 0309        if (!bb.isGrounded)
 310        {
 0311            currentState = State_Air;
 0312            OnUpdateWithDeltaTime(bb.deltaTime);
 313        }
 0314    }
 315
 316    private void CrossFadeTo(AvatarAnimation avatarAnimation, string animationName,
 317        float runTransitionTime, PlayMode playMode = PlayMode.StopSameLayer)
 318    {
 0319        if (latestAnimation == avatarAnimation)
 0320            return;
 321
 0322        animation.wrapMode = avatarAnimation.ShouldLoop() ? WrapMode.Loop : WrapMode.Once;
 0323        animation.CrossFade(animationName, runTransitionTime, playMode);
 0324        latestAnimation = avatarAnimation;
 0325    }
 326
 327    void State_Air(BlackBoard bb)
 328    {
 0329        if (bb.verticalSpeed > 0)
 330        {
 0331            CrossFadeTo(AvatarAnimation.JUMP, jumpAnimationName, JUMP_TRANSITION_TIME, PlayMode.StopAll);
 332        }
 333        else
 334        {
 0335            CrossFadeTo(AvatarAnimation.FALL, fallAnimationName, FALL_TRANSITION_TIME, PlayMode.StopAll);
 336        }
 337
 0338        if (bb.isGrounded)
 339        {
 0340            animation.Blend(jumpAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 0341            animation.Blend(fallAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 0342            currentState = State_Ground;
 0343            OnUpdateWithDeltaTime(bb.deltaTime);
 344        }
 0345    }
 346
 347    private static bool ExpressionGroundTransitionCondition(AnimationState animationState,
 348        BlackBoard bb,
 349        DCLCharacterController dclCharacterController,
 350        bool ownPlayer)
 351    {
 0352        float timeTillEnd = animationState.length - animationState.time;
 0353        bool isAnimationOver = timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME && !bb.shouldLoop;
 0354        bool isMoving = ownPlayer ? dclCharacterController.isMovingByUserInput : Math.Abs(bb.movementSpeed) > OTHER_PLAY
 0355        return isAnimationOver || isMoving;
 356    }
 357
 358    private static bool ExpressionAirTransitionCondition(BlackBoard bb)
 359    {
 0360        return !bb.isGrounded;
 361    }
 362
 363    internal void State_Expression(BlackBoard bb)
 364    {
 0365        var animationState = animation[bb.expressionTriggerId];
 366
 0367        var prevAnimation = latestAnimation;
 0368        CrossFadeTo(AvatarAnimation.EMOTE, bb.expressionTriggerId, EXPRESSION_EXIT_TRANSITION_TIME, PlayMode.StopAll);
 369
 0370        bool exitTransitionStarted = false;
 0371        if (ExpressionAirTransitionCondition(bb))
 372        {
 0373            currentState = State_Air;
 0374            exitTransitionStarted = true;
 375        }
 376
 0377        if (ExpressionGroundTransitionCondition(animationState, bb, DCLCharacterController.i, isOwnPlayer))
 378        {
 0379            currentState = State_Ground;
 0380            exitTransitionStarted = true;
 381        }
 382
 0383        if (exitTransitionStarted)
 384        {
 0385            animation.Blend(bb.expressionTriggerId, 0, EXPRESSION_EXIT_TRANSITION_TIME);
 386
 0387            bb.expressionTriggerId = null;
 0388            bb.shouldLoop = false;
 0389            OnUpdateWithDeltaTime(bb.deltaTime);
 390        }
 391        else
 392        {
 393            //this condition makes Blend be called only in first frame of the state
 0394            if (prevAnimation != AvatarAnimation.EMOTE)
 395            {
 0396                animation.wrapMode = bb.shouldLoop ? WrapMode.Loop : WrapMode.Once;
 0397                animation.Blend(bb.expressionTriggerId, 1, EXPRESSION_ENTER_TRANSITION_TIME);
 398            }
 399        }
 0400    }
 401
 402    private void SetExpressionValues(string expressionTriggerId, long expressionTriggerTimestamp)
 403    {
 2404        if (animation == null)
 2405            return;
 406
 0407        if (string.IsNullOrEmpty(expressionTriggerId))
 0408            return;
 409
 0410        if (animation.GetClip(expressionTriggerId) == null)
 0411            return;
 412
 0413        var mustTriggerAnimation = !string.IsNullOrEmpty(expressionTriggerId)
 414                                   && blackboard.expressionTriggerTimestamp != expressionTriggerTimestamp;
 0415        blackboard.expressionTriggerId = expressionTriggerId;
 0416        blackboard.expressionTriggerTimestamp = expressionTriggerTimestamp;
 417
 0418        if (mustTriggerAnimation)
 419        {
 0420            if (!string.IsNullOrEmpty(expressionTriggerId))
 421            {
 0422                animation.Stop(expressionTriggerId);
 0423                latestAnimation = AvatarAnimation.IDLE;
 424            }
 425
 0426            blackboard.shouldLoop = emoteClipDataMap.TryGetValue(expressionTriggerId, out var clipData)
 427                                    && clipData.loop;
 428
 0429            currentState = State_Expression;
 0430            OnUpdateWithDeltaTime(Time.deltaTime);
 431        }
 0432    }
 433
 434    public void Reset()
 435    {
 0436        if (animation == null)
 0437            return;
 438
 439        //It will set the animation to the first frame, but due to the nature of the script and its Update. It wont stop
 0440        animation.Stop();
 0441    }
 442
 0443    public void SetIdleFrame() { animation.Play(currentLocomotions.idle.name); }
 444
 445    public void PlayEmote(string emoteId, long timestamps)
 446    {
 2447        SetExpressionValues(emoteId, timestamps);
 2448    }
 449
 450    public void EquipBaseClip(AnimationClip clip)
 451    {
 0452        var clipId = clip.name;
 0453        if (animation == null)
 0454            return;
 455
 0456        if (animation.GetClip(clipId) != null)
 0457            animation.RemoveClip(clipId);
 458
 0459        animation.AddClip(clip, clipId);
 0460    }
 461
 462    public void EquipEmote(string emoteId, EmoteClipData emoteClipData)
 463    {
 0464        if (animation == null)
 0465            return;
 466
 0467        if (emoteClipData.clip == null)
 468        {
 0469            Debug.LogError("Can't equip null animation clip for emote " + emoteId);
 0470            return;
 471        }
 472
 0473        if (animation.GetClip(emoteId) != null)
 0474            animation.RemoveClip(emoteId);
 475
 0476        emoteClipDataMap[emoteId] = emoteClipData;
 477
 0478        animation.AddClip(emoteClipData.clip, emoteId);
 0479    }
 480
 481    public void UnequipEmote(string emoteId)
 482    {
 0483        if (animation == null)
 0484            return;
 485
 0486        if (animation.GetClip(emoteId) == null)
 0487            return;
 488
 0489        animation.RemoveClip(emoteId);
 0490    }
 491
 492    private void InitializeAvatarAudioAndParticleHandlers(Animation createdAnimation)
 493    {
 494        //NOTE(Mordi): Adds handler for animation events, and passes in the audioContainer for the avatar
 0495        AvatarAnimationEventHandler animationEventHandler = createdAnimation.gameObject.GetOrCreateComponent<AvatarAnima
 0496        AudioContainer audioContainer = transform.GetComponentInChildren<AudioContainer>();
 497
 0498        if (audioContainer != null)
 499        {
 0500            animationEventHandler.Init(audioContainer);
 501
 502            //NOTE(Mordi): If this is a remote avatar, pass the animation component so we can keep track of whether it i
 0503            AvatarAudioHandlerRemote audioHandlerRemote = audioContainer.GetComponent<AvatarAudioHandlerRemote>();
 504
 0505            if (audioHandlerRemote != null)
 506            {
 0507                audioHandlerRemote.Init(createdAnimation.gameObject);
 508            }
 509        }
 510
 0511        animEventHandler = animationEventHandler;
 0512    }
 513
 514    private void OnEnable()
 515    {
 811516        if (animation == null)
 811517            return;
 518
 0519        animation.enabled = true;
 0520    }
 521
 522    private void OnDisable()
 523    {
 811524        if (animation == null)
 811525            return;
 526
 0527        animation.Stop();
 0528        animation.enabled = false;
 0529    }
 530
 531    private void OnDestroy()
 532    {
 786533        if (animEventHandler != null)
 0534            Destroy(animEventHandler);
 786535    }
 536}