< Summary

Class:AvatarAnimatorLegacy
Assembly:AvatarShape
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/Components/Avatar/AvatarAnimatorLegacy.cs
Covered lines:15
Uncovered lines:179
Coverable lines:194
Total lines:506
Line coverage:7.7% (15 of 194)
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%30500%
CrossFadeTo(...)0%6200%
State_Air(...)0%12300%
ExpressionGroundTransitionCondition(...)0%12300%
ExpressionAirTransitionCondition(...)0%2100%
State_Expression(...)0%56700%
SetExpressionValues(...)0%63.269012.5%
Reset()0%6200%
SetIdleFrame()0%2100%
PlayEmote(...)0%110100%
EquipBaseClip(...)0%12300%
EquipEmote(...)0%12300%
UnequipEmote(...)0%12300%
InitializeAvatarAudioAndParticleHandlers(...)0%12300%
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 class AvatarAnimatorLegacy : MonoBehaviour, IPoolLifecycleHandler, IAnimator
 22{
 23    const float IDLE_TRANSITION_TIME = 0.2f;
 24    const float STRAFE_TRANSITION_TIME = 0.25f;
 25    const float RUN_TRANSITION_TIME = 0.15f;
 26    const float WALK_TRANSITION_TIME = 0.15f;
 27    const float JUMP_TRANSITION_TIME = 0.01f;
 28    const float FALL_TRANSITION_TIME = 0.5f;
 29    const float EXPRESSION_EXIT_TRANSITION_TIME = 0.2f;
 30    const float EXPRESSION_ENTER_TRANSITION_TIME = 0.1f;
 31    const float OTHER_PLAYER_MOVE_THRESHOLD = 0.07f;
 32
 33    const float AIR_EXIT_TRANSITION_TIME = 0.2f;
 34    const float GROUND_BLENDTREE_TRANSITION_TIME = 0.15f;
 35
 36    const float RUN_SPEED_THRESHOLD = 0.05f;
 37    const float WALK_SPEED_THRESHOLD = 0.03f;
 38
 39    const float ELEVATION_OFFSET = 0.6f;
 40    const float RAY_OFFSET_LENGTH = 3.0f;
 41
 42    const float MAX_VELOCITY = 6.25f;
 43
 44    // Time it takes to determine if a character is grounded when vertical velocity is 0
 45    const float FORCE_GROUND_TIME = 0.05f;
 46
 47    // Minimum vertical speed used to consider whether an avatar is on air
 48    const float MIN_VERTICAL_SPEED_AIR = 0.025f;
 49
 50    [System.Serializable]
 51    public class AvatarLocomotion
 52    {
 53        public AnimationClip idle;
 54        public AnimationClip walk;
 55        public AnimationClip run;
 56        public AnimationClip jump;
 57        public AnimationClip fall;
 58    }
 59
 60    [System.Serializable]
 61    public class BlackBoard
 62    {
 63        public float walkSpeedFactor;
 64        public float runSpeedFactor;
 65        public float movementSpeed;
 66        public float verticalSpeed;
 67        public bool isGrounded;
 68        public string expressionTriggerId;
 69        public long expressionTriggerTimestamp;
 70        public float deltaTime;
 71        public bool shouldLoop;
 72    }
 73
 74    [SerializeField] internal AvatarLocomotion femaleLocomotions;
 75    [SerializeField] internal AvatarLocomotion maleLocomotions;
 76    AvatarLocomotion currentLocomotions;
 77
 78    public new Animation animation;
 79    public BlackBoard blackboard;
 80    public Transform target;
 81
 107882    [SerializeField] float runMinSpeed = 6f;
 107883    [SerializeField] float walkMinSpeed = 0.1f;
 84
 85    internal System.Action<BlackBoard> currentState;
 86
 87    Vector3 lastPosition;
 88    bool isOwnPlayer = false;
 89    private AvatarAnimationEventHandler animEventHandler;
 90
 91    private float lastOnAirTime = 0;
 92
 107893    private Dictionary<string, EmoteClipData> emoteClipDataMap =
 94        new Dictionary<string, EmoteClipData>();
 95
 96    private string runAnimationName;
 97    private string walkAnimationName;
 98    private string idleAnimationName;
 99    private string jumpAnimationName;
 100    private string fallAnimationName;
 101    private AvatarAnimation latestAnimation;
 102    private AnimationState runAnimationState;
 103    private AnimationState walkAnimationState;
 104    private bool isUpdateRegistered = false;
 105
 106    private Ray rayCache;
 107
 1140108    public void Start() { OnPoolGet(); }
 109
 110    // AvatarSystem entry points
 111    public bool Prepare(string bodyshapeId, GameObject container)
 112    {
 0113        if (!container.transform.TryFindChildRecursively("Armature", out Transform armature))
 114        {
 0115            Debug.LogError($"Couldn't find Armature for AnimatorLegacy in path: {transform.GetHierarchyPath()}");
 116
 0117            return false;
 118        }
 119
 0120        Transform armatureParent = armature.parent;
 0121        animation = armatureParent.gameObject.GetOrCreateComponent<Animation>();
 0122        armatureParent.gameObject.GetOrCreateComponent<StickerAnimationListener>();
 123
 0124        PrepareLocomotionAnims(bodyshapeId);
 0125        SetIdleFrame();
 0126        animation.Sample();
 0127        InitializeAvatarAudioAndParticleHandlers(animation);
 128
 129        // since the avatar can be updated when changing a wearable we shouldn't register to the update event twice
 0130        if (!isUpdateRegistered)
 131        {
 0132            isUpdateRegistered = true;
 133
 0134            if (isOwnPlayer)
 135            {
 0136                DCLCharacterController.i.OnUpdateFinish += OnUpdateWithDeltaTime;
 0137            }
 138            else
 139            {
 0140                Environment.i.platform.updateEventHandler.AddListener(IUpdateEventHandler.EventType.Update, OnEventHandl
 141            }
 142
 143        }
 144
 0145        return true;
 146    }
 147
 148    private void PrepareLocomotionAnims(string bodyshapeId)
 149    {
 0150        if (bodyshapeId.Contains(WearableLiterals.BodyShapes.MALE))
 151        {
 0152            currentLocomotions = maleLocomotions;
 0153        }
 0154        else if (bodyshapeId.Contains(WearableLiterals.BodyShapes.FEMALE))
 155        {
 0156            currentLocomotions = femaleLocomotions;
 157        }
 158
 0159        EquipBaseClip(currentLocomotions.idle);
 0160        EquipBaseClip(currentLocomotions.walk);
 0161        EquipBaseClip(currentLocomotions.run);
 0162        EquipBaseClip(currentLocomotions.jump);
 0163        EquipBaseClip(currentLocomotions.fall);
 164
 0165        idleAnimationName = currentLocomotions.idle.name;
 0166        walkAnimationName = currentLocomotions.walk.name;
 0167        runAnimationName = currentLocomotions.run.name;
 0168        jumpAnimationName = currentLocomotions.jump.name;
 0169        fallAnimationName = currentLocomotions.fall.name;
 170
 0171        runAnimationState = animation[runAnimationName];
 0172        walkAnimationState = animation[walkAnimationName];
 0173    }
 0174    private void OnEventHandlerUpdate() { OnUpdateWithDeltaTime(Time.deltaTime); }
 175
 176    public void OnPoolGet()
 177    {
 570178        if (DCLCharacterController.i != null)
 179        {
 570180            isOwnPlayer = DCLCharacterController.i.transform == transform.parent;
 181
 182            // NOTE: disable MonoBehaviour's update to use DCLCharacterController event instead
 570183            this.enabled = !isOwnPlayer;
 184        }
 185
 570186        currentState = State_Init;
 570187    }
 188
 189    public void OnPoolRelease()
 190    {
 0191        if (isUpdateRegistered)
 192        {
 0193            isUpdateRegistered = false;
 194
 0195            if (isOwnPlayer && DCLCharacterController.i)
 196            {
 0197                DCLCharacterController.i.OnUpdateFinish -= OnUpdateWithDeltaTime;
 0198            }
 199            else
 200            {
 0201                Environment.i.platform.updateEventHandler.RemoveListener(IUpdateEventHandler.EventType.Update, OnEventHa
 202            }
 203        }
 0204    }
 205
 206    void OnUpdateWithDeltaTime(float deltaTime)
 207    {
 0208        blackboard.deltaTime = deltaTime;
 0209        UpdateInterface();
 0210        currentState?.Invoke(blackboard);
 0211    }
 212
 213    void UpdateInterface()
 214    {
 0215        Vector3 velocityTargetPosition = target.position;
 0216        Vector3 flattenedVelocity = velocityTargetPosition - lastPosition;
 217
 218        //NOTE(Brian): Vertical speed
 0219        float verticalVelocity = flattenedVelocity.y;
 220
 221        //NOTE(Kinerius): if we have more or less than zero we consider that we are either jumping or falling
 0222        if (Mathf.Abs(verticalVelocity) > MIN_VERTICAL_SPEED_AIR)
 223        {
 0224            lastOnAirTime = Time.time;
 225        }
 226
 0227        blackboard.verticalSpeed = verticalVelocity;
 228
 0229        flattenedVelocity.y = 0;
 230
 0231        if (isOwnPlayer)
 0232            blackboard.movementSpeed = flattenedVelocity.magnitude - DCLCharacterController.i.movingPlatformSpeed;
 233        else
 0234            blackboard.movementSpeed = flattenedVelocity.magnitude;
 235
 0236        Vector3 rayOffset = Vector3.up * RAY_OFFSET_LENGTH;
 237
 238        //NOTE(Kinerius): This check is just for the playing character, it uses a combination of collision flags and ray
 0239        bool isGroundedByCharacterController = isOwnPlayer && DCLCharacterController.i.isGrounded;
 240
 241        //NOTE(Kinerius): This check is for interpolated avatars (the other players) as we dont have a Character Control
 242        //                this check is cheap and fast but not precise
 0243        bool isGroundedByVelocity = !isOwnPlayer && Time.time - lastOnAirTime > FORCE_GROUND_TIME;
 244
 245        //NOTE(Kinerius): This additional check is both for the player and interpolated avatars, we cast an additional r
 0246        bool isGroundedByRaycast = false;
 247
 0248        if (!isGroundedByCharacterController && !isGroundedByVelocity)
 249        {
 0250            rayCache.origin = velocityTargetPosition + rayOffset;
 0251            rayCache.direction = Vector3.down;
 252
 0253            isGroundedByRaycast = Physics.Raycast(rayCache,
 254                RAY_OFFSET_LENGTH - ELEVATION_OFFSET,
 255                DCLCharacterController.i.groundLayers);
 256
 257        }
 258
 0259        blackboard.isGrounded = isGroundedByCharacterController || isGroundedByVelocity || isGroundedByRaycast;
 260
 0261        lastPosition = velocityTargetPosition;
 0262    }
 263
 264    void State_Init(BlackBoard bb)
 265    {
 0266        if (bb.isGrounded)
 267        {
 0268            currentState = State_Ground;
 0269        }
 270        else
 271        {
 0272            currentState = State_Air;
 273        }
 0274    }
 275
 276    void State_Ground(BlackBoard bb)
 277    {
 0278        if (bb.deltaTime <= 0)
 0279            return;
 280
 0281        float movementSpeed = bb.movementSpeed / bb.deltaTime;
 282
 0283        runAnimationState.normalizedSpeed = movementSpeed * bb.runSpeedFactor;
 0284        walkAnimationState.normalizedSpeed = movementSpeed * bb.walkSpeedFactor;
 285
 0286        if (movementSpeed > runMinSpeed)
 287        {
 0288            CrossFadeTo(AvatarAnimation.RUN, runAnimationName, RUN_TRANSITION_TIME);
 0289        }
 0290        else if (movementSpeed > walkMinSpeed)
 291        {
 0292            CrossFadeTo(AvatarAnimation.WALK, walkAnimationName, WALK_TRANSITION_TIME);
 0293        }
 294        else
 295        {
 0296            CrossFadeTo(AvatarAnimation.IDLE, idleAnimationName, IDLE_TRANSITION_TIME);
 297        }
 298
 0299        if (!bb.isGrounded)
 300        {
 0301            currentState = State_Air;
 0302            OnUpdateWithDeltaTime(bb.deltaTime);
 303        }
 0304    }
 305    private void CrossFadeTo(AvatarAnimation avatarAnimation, string animationName,
 306        float runTransitionTime, PlayMode playMode = PlayMode.StopSameLayer)
 307    {
 0308        if (latestAnimation == avatarAnimation)
 0309            return;
 310
 0311        animation.CrossFade(animationName, runTransitionTime, playMode);
 0312        latestAnimation = avatarAnimation;
 0313    }
 314
 315    void State_Air(BlackBoard bb)
 316    {
 0317        if (bb.verticalSpeed > 0)
 318        {
 0319            CrossFadeTo(AvatarAnimation.JUMP, jumpAnimationName, JUMP_TRANSITION_TIME, PlayMode.StopAll);
 0320        }
 321        else
 322        {
 0323            CrossFadeTo(AvatarAnimation.FALL, fallAnimationName, FALL_TRANSITION_TIME, PlayMode.StopAll);
 324        }
 325
 0326        if (bb.isGrounded)
 327        {
 0328            animation.Blend(jumpAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 0329            animation.Blend(fallAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 0330            currentState = State_Ground;
 0331            OnUpdateWithDeltaTime(bb.deltaTime);
 332        }
 0333    }
 334
 335    private static bool ExpressionGroundTransitionCondition(AnimationState animationState,
 336        BlackBoard bb,
 337        DCLCharacterController dclCharacterController,
 338        bool ownPlayer)
 339    {
 0340        float timeTillEnd = animationState.length - animationState.time;
 0341        if (timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME)
 342        {
 0343            return ownPlayer ? dclCharacterController.isMovingByUserInput :
 344                Math.Abs(bb.movementSpeed) > OTHER_PLAYER_MOVE_THRESHOLD;
 345        }
 346
 0347        return false;
 348    }
 349
 350    private static bool ExpressionAirTransitionCondition(BlackBoard bb)
 351    {
 0352        return !bb.isGrounded;
 353    }
 354
 355    internal void State_Expression(BlackBoard bb)
 356    {
 0357        var animationState = animation[bb.expressionTriggerId];
 358
 0359        var prevAnimation = latestAnimation;
 0360        CrossFadeTo(AvatarAnimation.EMOTE, bb.expressionTriggerId, EXPRESSION_EXIT_TRANSITION_TIME, PlayMode.StopAll);
 361
 0362        bool exitTransitionStarted = false;
 363
 0364        if (ExpressionAirTransitionCondition(bb))
 365        {
 0366            currentState = State_Air;
 0367            exitTransitionStarted = true;
 368        }
 369
 0370        if (ExpressionGroundTransitionCondition(animationState, bb, DCLCharacterController.i, isOwnPlayer))
 371        {
 0372            currentState = State_Ground;
 0373            exitTransitionStarted = true;
 374        }
 375
 0376        if (exitTransitionStarted)
 377        {
 0378            animation.wrapMode = WrapMode.Default;
 0379            animation.Blend(bb.expressionTriggerId, 0, EXPRESSION_EXIT_TRANSITION_TIME);
 380
 0381            bb.expressionTriggerId = null;
 0382            bb.shouldLoop = false;
 0383            OnUpdateWithDeltaTime(bb.deltaTime);
 0384        }
 385        else
 386        {
 387            //this condition makes Blend be called only in first frame of the state
 0388            if (prevAnimation != AvatarAnimation.EMOTE)
 389            {
 0390                animation.wrapMode = bb.shouldLoop ? WrapMode.Loop : WrapMode.Once;
 0391                animation.Blend(bb.expressionTriggerId, 1, EXPRESSION_ENTER_TRANSITION_TIME);
 392            }
 393        }
 0394    }
 395
 396    private void SetExpressionValues(string expressionTriggerId, long expressionTriggerTimestamp)
 397    {
 2398        if (animation == null)
 2399            return;
 400
 0401        if (string.IsNullOrEmpty(expressionTriggerId))
 0402            return;
 403
 0404        if (animation.GetClip(expressionTriggerId) == null)
 0405            return;
 406
 0407        var mustTriggerAnimation = !string.IsNullOrEmpty(expressionTriggerId)
 408                                   && blackboard.expressionTriggerTimestamp != expressionTriggerTimestamp;
 0409        blackboard.expressionTriggerId = expressionTriggerId;
 0410        blackboard.expressionTriggerTimestamp = expressionTriggerTimestamp;
 411
 0412        if (mustTriggerAnimation)
 413        {
 0414            if (!string.IsNullOrEmpty(expressionTriggerId))
 415            {
 0416                animation.Stop(expressionTriggerId);
 417            }
 418
 0419            blackboard.shouldLoop = emoteClipDataMap.TryGetValue(expressionTriggerId, out var clipData)
 420                                    && clipData.loop;
 421
 0422            currentState = State_Expression;
 0423            OnUpdateWithDeltaTime(Time.deltaTime);
 424        }
 0425    }
 426
 427    public void Reset()
 428    {
 0429        if (animation == null)
 0430            return;
 431
 432        //It will set the animation to the first frame, but due to the nature of the script and its Update. It wont stop
 0433        animation.Stop();
 0434    }
 435
 0436    public void SetIdleFrame() { animation.Play(currentLocomotions.idle.name); }
 437
 438    public void PlayEmote(string emoteId, long timestamps)
 439    {
 2440        SetExpressionValues(emoteId, timestamps);
 2441    }
 442
 443    public void EquipBaseClip(AnimationClip clip)
 444    {
 0445        var clipId = clip.name;
 0446        if (animation == null)
 0447            return;
 448
 0449        if (animation.GetClip(clipId) != null)
 0450            animation.RemoveClip(clipId);
 451
 0452        animation.AddClip(clip, clipId);
 0453    }
 454
 455    public void EquipEmote(string emoteId, EmoteClipData emoteClipData)
 456    {
 0457        if (animation == null)
 0458            return;
 459
 0460        if (animation.GetClip(emoteId) != null)
 0461            animation.RemoveClip(emoteId);
 462
 0463        emoteClipDataMap[emoteId] = emoteClipData;
 464
 0465        animation.AddClip(emoteClipData.clip, emoteId);
 0466    }
 467
 468    public void UnequipEmote(string emoteId)
 469    {
 0470        if (animation == null)
 0471            return;
 472
 0473        if (animation.GetClip(emoteId) == null)
 0474            return;
 475
 0476        animation.RemoveClip(emoteId);
 0477    }
 478
 479    private void InitializeAvatarAudioAndParticleHandlers(Animation createdAnimation)
 480    {
 481        //NOTE(Mordi): Adds handler for animation events, and passes in the audioContainer for the avatar
 0482        AvatarAnimationEventHandler animationEventHandler = createdAnimation.gameObject.GetOrCreateComponent<AvatarAnima
 0483        AudioContainer audioContainer = transform.GetComponentInChildren<AudioContainer>();
 484
 0485        if (audioContainer != null)
 486        {
 0487            animationEventHandler.Init(audioContainer);
 488
 489            //NOTE(Mordi): If this is a remote avatar, pass the animation component so we can keep track of whether it i
 0490            AvatarAudioHandlerRemote audioHandlerRemote = audioContainer.GetComponent<AvatarAudioHandlerRemote>();
 491
 0492            if (audioHandlerRemote != null)
 493            {
 0494                audioHandlerRemote.Init(createdAnimation.gameObject);
 495            }
 496        }
 497
 0498        animEventHandler = animationEventHandler;
 0499    }
 500
 501    private void OnDestroy()
 502    {
 1059503        if (animEventHandler != null)
 0504            Destroy(animEventHandler);
 1059505    }
 506}