< Summary

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

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
ShouldLoop(...)0%20400%

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    {
 025        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
 98    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
 113    public void Start() { OnPoolGet(); }
 114
 115    // AvatarSystem entry points
 116    public bool Prepare(string bodyshapeId, GameObject container)
 117    {
 118        if (!container.transform.TryFindChildRecursively("Armature", out Transform armature))
 119        {
 120            Debug.LogError($"Couldn't find Armature for AnimatorLegacy in path: {transform.GetHierarchyPath()}");
 121
 122            return false;
 123        }
 124
 125        Transform armatureParent = armature.parent;
 126        animation = armatureParent.gameObject.GetOrCreateComponent<Animation>();
 127        armatureParent.gameObject.GetOrCreateComponent<StickerAnimationListener>();
 128
 129        PrepareLocomotionAnims(bodyshapeId);
 130        SetIdleFrame();
 131        animation.Sample();
 132        InitializeAvatarAudioAndParticleHandlers(animation);
 133
 134        // since the avatar can be updated when changing a wearable we shouldn't register to the update event twice
 135        if (!isUpdateRegistered)
 136        {
 137            isUpdateRegistered = true;
 138
 139            if (isOwnPlayer)
 140            {
 141                DCLCharacterController.i.OnUpdateFinish += OnUpdateWithDeltaTime;
 142            }
 143            else
 144            {
 145                Environment.i.platform.updateEventHandler.AddListener(IUpdateEventHandler.EventType.Update, OnEventHandl
 146            }
 147
 148        }
 149
 150        return true;
 151    }
 152
 153    private void PrepareLocomotionAnims(string bodyshapeId)
 154    {
 155        if (bodyshapeId.Contains(WearableLiterals.BodyShapes.MALE))
 156        {
 157            currentLocomotions = maleLocomotions;
 158        }
 159        else if (bodyshapeId.Contains(WearableLiterals.BodyShapes.FEMALE))
 160        {
 161            currentLocomotions = femaleLocomotions;
 162        }
 163
 164        EquipBaseClip(currentLocomotions.idle);
 165        EquipBaseClip(currentLocomotions.walk);
 166        EquipBaseClip(currentLocomotions.run);
 167        EquipBaseClip(currentLocomotions.jump);
 168        EquipBaseClip(currentLocomotions.fall);
 169
 170        idleAnimationName = currentLocomotions.idle.name;
 171        walkAnimationName = currentLocomotions.walk.name;
 172        runAnimationName = currentLocomotions.run.name;
 173        jumpAnimationName = currentLocomotions.jump.name;
 174        fallAnimationName = currentLocomotions.fall.name;
 175
 176        runAnimationState = animation[runAnimationName];
 177        walkAnimationState = animation[walkAnimationName];
 178    }
 179    private void OnEventHandlerUpdate() { OnUpdateWithDeltaTime(Time.deltaTime); }
 180
 181    public void OnPoolGet()
 182    {
 183        if (DCLCharacterController.i != null)
 184        {
 185            isOwnPlayer = DCLCharacterController.i.transform == transform.parent;
 186
 187            // NOTE: disable MonoBehaviour's update to use DCLCharacterController event instead
 188            this.enabled = !isOwnPlayer;
 189        }
 190
 191        currentState = State_Init;
 192    }
 193
 194    public void OnPoolRelease()
 195    {
 196        if (isUpdateRegistered)
 197        {
 198            isUpdateRegistered = false;
 199
 200            if (isOwnPlayer && DCLCharacterController.i)
 201            {
 202                DCLCharacterController.i.OnUpdateFinish -= OnUpdateWithDeltaTime;
 203            }
 204            else
 205            {
 206                Environment.i.platform.updateEventHandler.RemoveListener(IUpdateEventHandler.EventType.Update, OnEventHa
 207            }
 208        }
 209    }
 210
 211    void OnUpdateWithDeltaTime(float deltaTime)
 212    {
 213        blackboard.deltaTime = deltaTime;
 214        UpdateInterface();
 215        currentState?.Invoke(blackboard);
 216    }
 217
 218    void UpdateInterface()
 219    {
 220        Vector3 velocityTargetPosition = target.position;
 221        Vector3 flattenedVelocity = velocityTargetPosition - lastPosition;
 222
 223        //NOTE(Brian): Vertical speed
 224        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
 227        if (Mathf.Abs(verticalVelocity) > MIN_VERTICAL_SPEED_AIR)
 228        {
 229            lastOnAirTime = Time.time;
 230        }
 231
 232        blackboard.verticalSpeed = verticalVelocity;
 233
 234        flattenedVelocity.y = 0;
 235
 236        if (isOwnPlayer)
 237            blackboard.movementSpeed = flattenedVelocity.magnitude - DCLCharacterController.i.movingPlatformSpeed;
 238        else
 239            blackboard.movementSpeed = flattenedVelocity.magnitude;
 240
 241        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
 244        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
 248        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
 251        bool isGroundedByRaycast = false;
 252
 253        if (!isGroundedByCharacterController && !isGroundedByVelocity)
 254        {
 255            rayCache.origin = velocityTargetPosition + rayOffset;
 256            rayCache.direction = Vector3.down;
 257
 258            isGroundedByRaycast = Physics.Raycast(rayCache,
 259                RAY_OFFSET_LENGTH - ELEVATION_OFFSET,
 260                DCLCharacterController.i.groundLayers);
 261
 262        }
 263
 264        blackboard.isGrounded = isGroundedByCharacterController || isGroundedByVelocity || isGroundedByRaycast;
 265
 266        lastPosition = velocityTargetPosition;
 267    }
 268
 269    void State_Init(BlackBoard bb)
 270    {
 271        if (bb.isGrounded)
 272        {
 273            currentState = State_Ground;
 274        }
 275        else
 276        {
 277            currentState = State_Air;
 278        }
 279    }
 280
 281    void State_Ground(BlackBoard bb)
 282    {
 283        if (bb.deltaTime <= 0)
 284            return;
 285
 286        float movementSpeed = bb.movementSpeed / bb.deltaTime;
 287
 288        runAnimationState.normalizedSpeed = movementSpeed * bb.runSpeedFactor;
 289        walkAnimationState.normalizedSpeed = movementSpeed * bb.walkSpeedFactor;
 290
 291        if (movementSpeed >= WALK_MAX_SPEED)
 292        {
 293            CrossFadeTo(AvatarAnimation.RUN, runAnimationName, RUN_TRANSITION_TIME);
 294        }
 295        else if (movementSpeed >= RUN_MIN_SPEED && movementSpeed < WALK_MAX_SPEED)
 296        {
 297            // Keep current animation, leave empty
 298        }
 299        else if (movementSpeed > WALK_MIN_SPEED)
 300        {
 301            CrossFadeTo(AvatarAnimation.WALK, walkAnimationName, WALK_TRANSITION_TIME);
 302        }
 303        else
 304        {
 305            CrossFadeTo(AvatarAnimation.IDLE, idleAnimationName, IDLE_TRANSITION_TIME);
 306        }
 307
 308
 309        if (!bb.isGrounded)
 310        {
 311            currentState = State_Air;
 312            OnUpdateWithDeltaTime(bb.deltaTime);
 313        }
 314    }
 315
 316    private void CrossFadeTo(AvatarAnimation avatarAnimation, string animationName,
 317        float runTransitionTime, PlayMode playMode = PlayMode.StopSameLayer)
 318    {
 319        if (latestAnimation == avatarAnimation)
 320            return;
 321
 322        animation.wrapMode = avatarAnimation.ShouldLoop() ? WrapMode.Loop : WrapMode.Once;
 323        animation.CrossFade(animationName, runTransitionTime, playMode);
 324        latestAnimation = avatarAnimation;
 325    }
 326
 327    void State_Air(BlackBoard bb)
 328    {
 329        if (bb.verticalSpeed > 0)
 330        {
 331            CrossFadeTo(AvatarAnimation.JUMP, jumpAnimationName, JUMP_TRANSITION_TIME, PlayMode.StopAll);
 332        }
 333        else
 334        {
 335            CrossFadeTo(AvatarAnimation.FALL, fallAnimationName, FALL_TRANSITION_TIME, PlayMode.StopAll);
 336        }
 337
 338        if (bb.isGrounded)
 339        {
 340            animation.Blend(jumpAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 341            animation.Blend(fallAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 342            currentState = State_Ground;
 343            OnUpdateWithDeltaTime(bb.deltaTime);
 344        }
 345    }
 346
 347    private static bool ExpressionGroundTransitionCondition(AnimationState animationState,
 348        BlackBoard bb,
 349        DCLCharacterController dclCharacterController,
 350        bool ownPlayer)
 351    {
 352        float timeTillEnd = animationState.length - animationState.time;
 353        bool isAnimationOver = timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME && !bb.shouldLoop;
 354        bool isMoving = ownPlayer ? dclCharacterController.isMovingByUserInput : Math.Abs(bb.movementSpeed) > OTHER_PLAY
 355        return isAnimationOver || isMoving;
 356    }
 357
 358    private static bool ExpressionAirTransitionCondition(BlackBoard bb)
 359    {
 360        return !bb.isGrounded;
 361    }
 362
 363    internal void State_Expression(BlackBoard bb)
 364    {
 365        var animationState = animation[bb.expressionTriggerId];
 366
 367        var prevAnimation = latestAnimation;
 368        CrossFadeTo(AvatarAnimation.EMOTE, bb.expressionTriggerId, EXPRESSION_EXIT_TRANSITION_TIME, PlayMode.StopAll);
 369
 370        bool exitTransitionStarted = false;
 371        if (ExpressionAirTransitionCondition(bb))
 372        {
 373            currentState = State_Air;
 374            exitTransitionStarted = true;
 375        }
 376
 377        if (ExpressionGroundTransitionCondition(animationState, bb, DCLCharacterController.i, isOwnPlayer))
 378        {
 379            currentState = State_Ground;
 380            exitTransitionStarted = true;
 381        }
 382
 383        if (exitTransitionStarted)
 384        {
 385            animation.Blend(bb.expressionTriggerId, 0, EXPRESSION_EXIT_TRANSITION_TIME);
 386
 387            bb.expressionTriggerId = null;
 388            bb.shouldLoop = false;
 389            OnUpdateWithDeltaTime(bb.deltaTime);
 390        }
 391        else
 392        {
 393            //this condition makes Blend be called only in first frame of the state
 394            if (prevAnimation != AvatarAnimation.EMOTE)
 395            {
 396                animation.wrapMode = bb.shouldLoop ? WrapMode.Loop : WrapMode.Once;
 397                animation.Blend(bb.expressionTriggerId, 1, EXPRESSION_ENTER_TRANSITION_TIME);
 398            }
 399        }
 400    }
 401
 402    private void SetExpressionValues(string expressionTriggerId, long expressionTriggerTimestamp)
 403    {
 404        if (animation == null)
 405            return;
 406
 407        if (string.IsNullOrEmpty(expressionTriggerId))
 408            return;
 409
 410        if (animation.GetClip(expressionTriggerId) == null)
 411            return;
 412
 413        var mustTriggerAnimation = !string.IsNullOrEmpty(expressionTriggerId)
 414                                   && blackboard.expressionTriggerTimestamp != expressionTriggerTimestamp;
 415        blackboard.expressionTriggerId = expressionTriggerId;
 416        blackboard.expressionTriggerTimestamp = expressionTriggerTimestamp;
 417
 418        if (mustTriggerAnimation)
 419        {
 420            if (!string.IsNullOrEmpty(expressionTriggerId))
 421            {
 422                animation.Stop(expressionTriggerId);
 423                latestAnimation = AvatarAnimation.IDLE;
 424            }
 425
 426            blackboard.shouldLoop = emoteClipDataMap.TryGetValue(expressionTriggerId, out var clipData)
 427                                    && clipData.loop;
 428
 429            currentState = State_Expression;
 430            OnUpdateWithDeltaTime(Time.deltaTime);
 431        }
 432    }
 433
 434    public void Reset()
 435    {
 436        if (animation == null)
 437            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
 440        animation.Stop();
 441    }
 442
 443    public void SetIdleFrame() { animation.Play(currentLocomotions.idle.name); }
 444
 445    public void PlayEmote(string emoteId, long timestamps)
 446    {
 447        SetExpressionValues(emoteId, timestamps);
 448    }
 449
 450    public void EquipBaseClip(AnimationClip clip)
 451    {
 452        var clipId = clip.name;
 453        if (animation == null)
 454            return;
 455
 456        if (animation.GetClip(clipId) != null)
 457            animation.RemoveClip(clipId);
 458
 459        animation.AddClip(clip, clipId);
 460    }
 461
 462    public void EquipEmote(string emoteId, EmoteClipData emoteClipData)
 463    {
 464        if (animation == null)
 465            return;
 466
 467        if (emoteClipData.clip == null)
 468        {
 469            Debug.LogError("Can't equip null animation clip for emote " + emoteId);
 470            return;
 471        }
 472
 473        if (animation.GetClip(emoteId) != null)
 474            animation.RemoveClip(emoteId);
 475
 476        emoteClipDataMap[emoteId] = emoteClipData;
 477
 478        animation.AddClip(emoteClipData.clip, emoteId);
 479    }
 480
 481    public void UnequipEmote(string emoteId)
 482    {
 483        if (animation == null)
 484            return;
 485
 486        if (animation.GetClip(emoteId) == null)
 487            return;
 488
 489        animation.RemoveClip(emoteId);
 490    }
 491
 492    private void InitializeAvatarAudioAndParticleHandlers(Animation createdAnimation)
 493    {
 494        //NOTE(Mordi): Adds handler for animation events, and passes in the audioContainer for the avatar
 495        AvatarAnimationEventHandler animationEventHandler = createdAnimation.gameObject.GetOrCreateComponent<AvatarAnima
 496        AudioContainer audioContainer = transform.GetComponentInChildren<AudioContainer>();
 497
 498        if (audioContainer != null)
 499        {
 500            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
 503            AvatarAudioHandlerRemote audioHandlerRemote = audioContainer.GetComponent<AvatarAudioHandlerRemote>();
 504
 505            if (audioHandlerRemote != null)
 506            {
 507                audioHandlerRemote.Init(createdAnimation.gameObject);
 508            }
 509        }
 510
 511        animEventHandler = animationEventHandler;
 512    }
 513
 514    private void OnEnable()
 515    {
 516        if (animation == null)
 517            return;
 518
 519        animation.enabled = true;
 520    }
 521
 522    private void OnDisable()
 523    {
 524        if (animation == null)
 525            return;
 526
 527        animation.Stop();
 528        animation.enabled = false;
 529    }
 530
 531    private void OnDestroy()
 532    {
 533        if (animEventHandler != null)
 534            Destroy(animEventHandler);
 535    }
 536}

Methods/Properties

ShouldLoop(AvatarAnimation)