< 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:602
Line coverage:0% (0 of 1)
Covered branches:0
Total branches:0
Covered methods:0
Total methods:1
Method coverage:0% (0 of 1)

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 AvatarSystem;
 2using Cysharp.Threading.Tasks;
 3using DCL;
 4using DCL.Components;
 5using DCL.Emotes;
 6using DCL.Helpers;
 7using System;
 8using System.Collections.Generic;
 9using UnityEngine;
 10using Environment = DCL.Environment;
 11
 12public enum AvatarAnimation
 13{
 14    IDLE,
 15    RUN,
 16    WALK,
 17    EMOTE,
 18    JUMP,
 19    FALL,
 20}
 21
 22public static class AvatarAnimationExtensions
 23{
 24    public static bool ShouldLoop(this AvatarAnimation avatarAnimation)
 25    {
 026        return avatarAnimation == AvatarAnimation.RUN ||
 27               avatarAnimation == AvatarAnimation.IDLE ||
 28               avatarAnimation == AvatarAnimation.FALL ||
 29               avatarAnimation == AvatarAnimation.WALK;
 30    }
 31}
 32
 33public class AvatarAnimatorLegacy : MonoBehaviour, IPoolLifecycleHandler, IAnimator
 34{
 35    private const float ZERO_POSE_LOOP_TIME_SECONDS = 2f;
 36    private const float IDLE_TRANSITION_TIME = 0.2f;
 37    private const float RUN_TRANSITION_TIME = 0.15f;
 38    private const float WALK_TRANSITION_TIME = 0.15f;
 39    private const float WALK_MAX_SPEED = 6;
 40    private const float RUN_MIN_SPEED = 4f;
 41    private const float WALK_MIN_SPEED = 0.1f;
 42    private const float WALK_RUN_SWITCH_TIME = 1.5f;
 43    private const float JUMP_TRANSITION_TIME = 0.01f;
 44    private const float FALL_TRANSITION_TIME = 0.5f;
 45    private const float EXPRESSION_EXIT_TRANSITION_TIME = 0.2f;
 46    private const float EXPRESSION_ENTER_TRANSITION_TIME = 0.1f;
 47    private const float OTHER_PLAYER_MOVE_THRESHOLD = 0.5f;
 48
 49    private const float AIR_EXIT_TRANSITION_TIME = 0.2f;
 50
 51    private const float ELEVATION_OFFSET = 0.6f;
 52    private const float RAY_OFFSET_LENGTH = 3.0f;
 53
 54    // Time it takes to determine if a character is grounded when vertical velocity is 0
 55    private const float FORCE_GROUND_TIME = 0.05f;
 56
 57    // Minimum vertical speed used to consider whether an avatar is on air
 58    private const float MIN_VERTICAL_SPEED_AIR = 0.025f;
 59
 60    [Serializable]
 61    public class AvatarLocomotion
 62    {
 63        public AnimationClip idle;
 64        public AnimationClip walk;
 65        public AnimationClip run;
 66        public AnimationClip jump;
 67        public AnimationClip fall;
 68    }
 69
 70    [Serializable]
 71    public class BlackBoard
 72    {
 73        public float walkSpeedFactor;
 74        public float runSpeedFactor;
 75        public float movementSpeed;
 76        public float verticalSpeed;
 77        public bool isGrounded;
 78        public string expressionTriggerId;
 79        public long expressionTriggerTimestamp;
 80        public float deltaTime;
 81        public bool shouldLoop;
 82    }
 83
 84    [SerializeField] internal AvatarLocomotion femaleLocomotions;
 85    [SerializeField] internal AvatarLocomotion maleLocomotions;
 86    [SerializeField] internal string renderingLayer = "Default";
 87    private AvatarLocomotion currentLocomotions;
 88
 89    public new Animation animation;
 90    public BlackBoard blackboard;
 91    public Transform target;
 92
 93    private Action<BlackBoard> currentState;
 94
 95    private Vector3 lastPosition;
 96    private bool isOwnPlayer;
 97    private AvatarAnimationEventHandler animEventHandler;
 98
 99    private float lastOnAirTime = 0;
 100
 101    private Dictionary<string, EmoteClipData> emoteClipDataMap = new ();
 102
 103    private string runAnimationName;
 104    private string walkAnimationName;
 105    private string idleAnimationName;
 106    private string jumpAnimationName;
 107    private string fallAnimationName;
 108    private AvatarAnimation latestAnimationState;
 109    private AnimationState runAnimationState;
 110    private AnimationState walkAnimationState;
 111    private bool isUpdateRegistered = false;
 112
 113    private Ray rayCache;
 114    private bool hasTarget;
 115    private EmoteClipData lastExtendedEmoteData;
 116    private string lastCrossFade;
 117    private AnimationState currentEmote;
 118    private int lastEmoteLoopCount;
 119
 120    private float lastZeroPoseTime;
 121    private int zeroPoseLoop;
 122
 123    private void Awake()
 124    {
 125        hasTarget = target != null;
 126
 127        if (!hasTarget)
 128            Debug.LogError(message: $"Target is not assigned. {nameof(UpdateInterface)} will not work correctly.", this)
 129    }
 130
 131    public void Start()
 132    {
 133        OnPoolGet();
 134    }
 135
 136    // AvatarSystem entry points
 137    public bool Prepare(string bodyshapeId, GameObject container)
 138    {
 139        StopEmote(true);
 140
 141        animation = container.gameObject.GetOrCreateComponent<Animation>();
 142        container.gameObject.GetOrCreateComponent<StickerAnimationListener>();
 143
 144        PrepareLocomotionAnims(bodyshapeId);
 145        SetIdleFrame();
 146        animation.Sample();
 147        InitializeAvatarAudioAndParticleHandlers(animation);
 148
 149        // since the avatar can be updated when changing a wearable we shouldn't register to the update event twice
 150        if (!isUpdateRegistered)
 151        {
 152            isUpdateRegistered = true;
 153
 154            if (isOwnPlayer) { DCLCharacterController.i.OnUpdateFinish += OnUpdateWithDeltaTime; }
 155            else { Environment.i.platform.updateEventHandler.AddListener(IUpdateEventHandler.EventType.Update, OnEventHa
 156        }
 157
 158        return true;
 159    }
 160
 161    private void PrepareLocomotionAnims(string bodyshapeId)
 162    {
 163        if (bodyshapeId.Contains(WearableLiterals.BodyShapes.MALE)) { currentLocomotions = maleLocomotions; }
 164        else if (bodyshapeId.Contains(WearableLiterals.BodyShapes.FEMALE)) { currentLocomotions = femaleLocomotions; }
 165
 166        EquipBaseClip(currentLocomotions.idle);
 167        EquipBaseClip(currentLocomotions.walk);
 168        EquipBaseClip(currentLocomotions.run);
 169        EquipBaseClip(currentLocomotions.jump);
 170        EquipBaseClip(currentLocomotions.fall);
 171
 172        idleAnimationName = currentLocomotions.idle.name;
 173        walkAnimationName = currentLocomotions.walk.name;
 174        runAnimationName = currentLocomotions.run.name;
 175        jumpAnimationName = currentLocomotions.jump.name;
 176        fallAnimationName = currentLocomotions.fall.name;
 177
 178        runAnimationState = animation[runAnimationName];
 179        walkAnimationState = animation[walkAnimationName];
 180    }
 181
 182    private void OnEventHandlerUpdate()
 183    {
 184        OnUpdateWithDeltaTime(Time.deltaTime);
 185    }
 186
 187    public void OnPoolGet()
 188    {
 189        if (DCLCharacterController.i != null)
 190        {
 191            isOwnPlayer = DCLCharacterController.i.transform == transform.parent;
 192
 193            // NOTE: disable MonoBehaviour's update to use DCLCharacterController event instead
 194            this.enabled = !isOwnPlayer;
 195        }
 196
 197        currentState = State_Init;
 198    }
 199
 200    public void OnPoolRelease()
 201    {
 202        if (isUpdateRegistered)
 203        {
 204            isUpdateRegistered = false;
 205
 206            if (isOwnPlayer && DCLCharacterController.i) { DCLCharacterController.i.OnUpdateFinish -= OnUpdateWithDeltaT
 207            else { Environment.i.platform.updateEventHandler.RemoveListener(IUpdateEventHandler.EventType.Update, OnEven
 208        }
 209    }
 210
 211    private void OnUpdateWithDeltaTime(float deltaTime)
 212    {
 213        blackboard.deltaTime = deltaTime;
 214        UpdateInterface();
 215        currentState?.Invoke(blackboard);
 216    }
 217
 218    private void UpdateInterface()
 219    {
 220        if (!target) return;
 221
 222        Vector3 velocityTargetPosition = target.position;
 223        Vector3 flattenedVelocity = velocityTargetPosition - lastPosition;
 224
 225        //NOTE(Brian): Vertical speed
 226        float verticalVelocity = flattenedVelocity.y;
 227
 228        //NOTE(Kinerius): if we have more or less than zero we consider that we are either jumping or falling
 229        if (Mathf.Abs(verticalVelocity) > MIN_VERTICAL_SPEED_AIR) { lastOnAirTime = Time.time; }
 230
 231        blackboard.verticalSpeed = verticalVelocity;
 232
 233        flattenedVelocity.y = 0;
 234
 235        if (isOwnPlayer)
 236            blackboard.movementSpeed = flattenedVelocity.magnitude - DCLCharacterController.i.movingPlatformSpeed;
 237        else
 238            blackboard.movementSpeed = flattenedVelocity.magnitude;
 239
 240        Vector3 rayOffset = Vector3.up * RAY_OFFSET_LENGTH;
 241
 242        //NOTE(Kinerius): This check is just for the playing character, it uses a combination of collision flags and ray
 243        bool isGroundedByCharacterController = isOwnPlayer && DCLCharacterController.i.isGrounded;
 244
 245        //NOTE(Kinerius): This check is for interpolated avatars (the other players) as we dont have a Character Control
 246        //                this check is cheap and fast but not precise
 247        bool isGroundedByVelocity = !isOwnPlayer && Time.time - lastOnAirTime > FORCE_GROUND_TIME;
 248
 249        //NOTE(Kinerius): This additional check is both for the player and interpolated avatars, we cast an additional r
 250        bool isGroundedByRaycast = false;
 251
 252        if (!isGroundedByCharacterController && !isGroundedByVelocity)
 253        {
 254            rayCache.origin = velocityTargetPosition + rayOffset;
 255            rayCache.direction = Vector3.down;
 256
 257            LayerMask iGroundLayers = DCLCharacterController.i?.groundLayers ?? new LayerMask();
 258
 259            isGroundedByRaycast = Physics.Raycast(rayCache,
 260                RAY_OFFSET_LENGTH - ELEVATION_OFFSET,
 261                iGroundLayers);
 262        }
 263
 264        blackboard.isGrounded = isGroundedByCharacterController || isGroundedByVelocity || isGroundedByRaycast;
 265
 266        lastPosition = velocityTargetPosition;
 267    }
 268
 269    private void State_Init(BlackBoard bb)
 270    {
 271        if (bb.isGrounded) { currentState = State_Ground; }
 272        else { currentState = State_Air; }
 273    }
 274
 275    private void State_Ground(BlackBoard bb)
 276    {
 277        if (bb.deltaTime <= 0)
 278            return;
 279
 280        float movementSpeed = bb.movementSpeed / bb.deltaTime;
 281
 282        runAnimationState.normalizedSpeed = movementSpeed * bb.runSpeedFactor;
 283        walkAnimationState.normalizedSpeed = movementSpeed * bb.walkSpeedFactor;
 284
 285        if (movementSpeed >= WALK_MAX_SPEED)
 286            CrossFadeTo(AvatarAnimation.RUN, runAnimationName, RUN_TRANSITION_TIME);
 287        else if (movementSpeed >= RUN_MIN_SPEED && movementSpeed < WALK_MAX_SPEED)
 288            CrossFadeTo(AvatarAnimation.WALK, walkAnimationName, WALK_TRANSITION_TIME);
 289        else if (movementSpeed > WALK_MIN_SPEED)
 290            CrossFadeTo(AvatarAnimation.WALK, walkAnimationName, WALK_TRANSITION_TIME);
 291        else
 292            CrossFadeTo(AvatarAnimation.IDLE, idleAnimationName, IDLE_TRANSITION_TIME);
 293
 294        if (!bb.isGrounded)
 295        {
 296            currentState = State_Air;
 297            OnUpdateWithDeltaTime(bb.deltaTime);
 298        }
 299    }
 300
 301    private void CrossFadeTo(AvatarAnimation animationState, string animationName,
 302        float runTransitionTime, PlayMode playMode = PlayMode.StopSameLayer)
 303    {
 304        if (latestAnimationState == animationState)
 305            return;
 306
 307        lastCrossFade = animationName;
 308        latestAnimationState = animationState;
 309        animation.wrapMode = latestAnimationState.ShouldLoop() || blackboard.shouldLoop ? WrapMode.Loop : WrapMode.Once;
 310        animation.CrossFade(lastCrossFade, runTransitionTime, playMode);
 311    }
 312
 313    private void State_Air(BlackBoard bb)
 314    {
 315        if (bb.verticalSpeed > 0) { CrossFadeTo(AvatarAnimation.JUMP, jumpAnimationName, JUMP_TRANSITION_TIME, PlayMode.
 316        else { CrossFadeTo(AvatarAnimation.FALL, fallAnimationName, FALL_TRANSITION_TIME, PlayMode.StopAll); }
 317
 318        if (bb.isGrounded)
 319        {
 320            animation.Blend(jumpAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 321            animation.Blend(fallAnimationName, 0, AIR_EXIT_TRANSITION_TIME);
 322            currentState = State_Ground;
 323            OnUpdateWithDeltaTime(bb.deltaTime);
 324        }
 325    }
 326
 327    private void State_Expression(BlackBoard bb)
 328    {
 329        var prevAnimation = latestAnimationState;
 330
 331        var exitTransitionStarted = false;
 332
 333        if (!bb.isGrounded)
 334        {
 335            currentState = State_Air;
 336            exitTransitionStarted = true;
 337        }
 338
 339        if (ExpressionGroundTransitionCondition(animationState: animation[bb.expressionTriggerId]))
 340        {
 341            currentState = State_Ground;
 342            exitTransitionStarted = true;
 343        }
 344
 345        if (exitTransitionStarted)
 346            StopEmoteInternal(false);
 347        else if (prevAnimation != AvatarAnimation.EMOTE) // this condition makes Blend be called only in first frame of 
 348        {
 349            animation.wrapMode = bb.shouldLoop ? WrapMode.Loop : WrapMode.Once;
 350            animation.Blend(bb.expressionTriggerId, 1, EXPRESSION_ENTER_TRANSITION_TIME);
 351        }
 352
 353        // If we reach the emote loop, we send the RPC message again to refresh new users
 354        if (bb.shouldLoop && isOwnPlayer)
 355        {
 356            int emoteLoop = GetCurrentEmoteLoopCount();
 357
 358            if (emoteLoop != lastEmoteLoopCount) { UserProfile.GetOwnUserProfile().SetAvatarExpression(bb.expressionTrig
 359
 360            lastEmoteLoopCount = emoteLoop;
 361        }
 362
 363        return;
 364
 365        bool ExpressionGroundTransitionCondition(AnimationState animationState)
 366        {
 367            float timeTillEnd = animationState == null ? 0 : animationState.length - animationState.time;
 368            bool isAnimationOver = timeTillEnd < EXPRESSION_EXIT_TRANSITION_TIME && !bb.shouldLoop;
 369            bool isMoving = isOwnPlayer && DCLCharacterController.i.isMovingByUserInput; //Math.Abs(bb.movementSpeed) > 
 370
 371            bool expressionGroundTransitionCondition = isAnimationOver || isMoving;
 372            return expressionGroundTransitionCondition;
 373        }
 374    }
 375
 376    private int GetCurrentEmoteLoopCount()
 377    {
 378        // Some scenes might use animations with 0 length as loop poses (not ideal)
 379        if (Mathf.Approximately(currentEmote.length, 0))
 380        {
 381            if (Time.time - lastZeroPoseTime > ZERO_POSE_LOOP_TIME_SECONDS)
 382            {
 383                zeroPoseLoop++;
 384                lastZeroPoseTime = Time.time;
 385                return zeroPoseLoop;
 386            }
 387        }
 388
 389        return Mathf.RoundToInt(currentEmote.time / currentEmote.length);
 390    }
 391
 392    public void StopEmote(bool immediate)
 393    {
 394        StopEmoteInternal(immediate);
 395    }
 396
 397    private void StopEmoteInternal(bool immediate)
 398    {
 399        if (animation == null) return;
 400
 401        if (!string.IsNullOrEmpty(blackboard.expressionTriggerId))
 402            animation.Blend(blackboard.expressionTriggerId, 0, !immediate ? EXPRESSION_EXIT_TRANSITION_TIME : 0);
 403
 404        // Instantly replicate our emote status and position
 405        if (isOwnPlayer && !string.IsNullOrEmpty(blackboard.expressionTriggerId))
 406        {
 407            DCLCharacterController.i.ReportMovement();
 408            UserProfile.GetOwnUserProfile().SetAvatarExpression("", UserProfile.EmoteSource.EmoteCancel, true);
 409        }
 410
 411        blackboard.expressionTriggerId = null;
 412        blackboard.shouldLoop = false;
 413        lastExtendedEmoteData?.Stop();
 414
 415        if (!immediate) OnUpdateWithDeltaTime(blackboard.deltaTime);
 416    }
 417
 418    private void StartEmote(string emoteId, bool spatial, bool occlude)
 419    {
 420        if (!string.IsNullOrEmpty(emoteId))
 421        {
 422            lastExtendedEmoteData?.Stop();
 423
 424            if (emoteClipDataMap.TryGetValue(emoteId, out var emoteClipData))
 425            {
 426                lastExtendedEmoteData = emoteClipData;
 427
 428                emoteClipData.Play(gameObject.layer, spatial, occlude);
 429            }
 430        }
 431        else { lastExtendedEmoteData?.Stop(); }
 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()
 444    {
 445        animation.Play(currentLocomotions.idle.name);
 446    }
 447
 448    public void PlayEmote(string emoteId, long timestamps, bool spatial, bool occlude,
 449        bool forcePlay)
 450    {
 451        if (animation == null)
 452            return;
 453
 454        if (string.IsNullOrEmpty(emoteId))
 455            return;
 456
 457        if (animation.GetClip(emoteId) == null)
 458            return;
 459
 460        bool loop = emoteClipDataMap.TryGetValue(emoteId, out var clipData) && clipData.Loop;
 461        bool mustTriggerAnimation = !string.IsNullOrEmpty(emoteId) && (blackboard.expressionTriggerTimestamp != timestam
 462
 463        bool isTheSameLoopingEmote = loop && blackboard.expressionTriggerId == emoteId;
 464
 465        // Triggering an emote manually updates the timestamp, the looping emote by itself sends a timestamp of -1
 466        // so if we are already using an emote that looped and we receive the play emote command with that timestamp, we
 467        if (isTheSameLoopingEmote && timestamps < 0)
 468            return;
 469
 470        blackboard.expressionTriggerId = emoteId;
 471        blackboard.expressionTriggerTimestamp = timestamps;
 472
 473        if (mustTriggerAnimation || loop)
 474        {
 475            StartEmote(emoteId, spatial, occlude);
 476
 477            if (!string.IsNullOrEmpty(emoteId))
 478            {
 479                animation.Stop(emoteId);
 480                latestAnimationState = AvatarAnimation.IDLE;
 481            }
 482
 483            blackboard.shouldLoop = loop;
 484
 485            CrossFadeTo(AvatarAnimation.EMOTE, emoteId, EXPRESSION_EXIT_TRANSITION_TIME, PlayMode.StopAll);
 486
 487            currentEmote = animation[emoteId];
 488            lastEmoteLoopCount = GetCurrentEmoteLoopCount();
 489            currentState = State_Expression;
 490        }
 491    }
 492
 493    public void EquipBaseClip(AnimationClip clip)
 494    {
 495        var clipId = clip.name;
 496
 497        if (animation == null)
 498            return;
 499
 500        if (animation.GetClip(clipId) != null)
 501            animation.RemoveClip(clipId);
 502
 503        animation.AddClip(clip, clipId);
 504    }
 505
 506    public void EquipEmote(string emoteId, EmoteClipData emoteClipData)
 507    {
 508        if (animation == null)
 509            return;
 510
 511        if (animation.GetClip(emoteId) != null)
 512            animation.RemoveClip(emoteId);
 513
 514        emoteClipDataMap[emoteId] = emoteClipData;
 515
 516        animation.AddClip(emoteClipData.AvatarClip, emoteId);
 517
 518        if (emoteClipData.ExtraContent != null)
 519        {
 520            emoteClipData.ExtraContent.transform.SetParent(animation.transform.parent, false);
 521            emoteClipData.ExtraContent.transform.ResetLocalTRS();
 522            emoteClipData.ExtraContent.transform.localPosition = animation.transform.localPosition;
 523        }
 524    }
 525
 526    public void UnequipEmote(string emoteId)
 527    {
 528        if (animation == null)
 529            return;
 530
 531        if (animation.GetClip(emoteId) == null)
 532            return;
 533
 534        animation.RemoveClip(emoteId);
 535
 536        if (emoteClipDataMap.TryGetValue(emoteId, out var emoteClipData))
 537        {
 538            if (emoteClipData.ExtraContent != null)
 539                emoteClipData.ExtraContent.transform.SetParent(null, false);
 540        }
 541    }
 542
 543    public string GetCurrentEmoteId() =>
 544        blackboard.expressionTriggerId;
 545
 546    private void InitializeAvatarAudioAndParticleHandlers(Animation createdAnimation)
 547    {
 548        //NOTE(Mordi): Adds handler for animation events, and passes in the audioContainer for the avatar
 549        AvatarAnimationEventHandler animationEventHandler = createdAnimation.gameObject.GetOrCreateComponent<AvatarAnima
 550        AudioContainer audioContainer = transform.GetComponentInChildren<AudioContainer>();
 551
 552        if (audioContainer != null)
 553        {
 554            animationEventHandler.Init(audioContainer, LayerMask.NameToLayer(renderingLayer));
 555
 556            //NOTE(Mordi): If this is a remote avatar, pass the animation component so we can keep track of whether it i
 557            AvatarAudioHandlerRemote audioHandlerRemote = audioContainer.GetComponent<AvatarAudioHandlerRemote>();
 558
 559            if (audioHandlerRemote != null) { audioHandlerRemote.Init(createdAnimation.gameObject); }
 560        }
 561
 562        animEventHandler = animationEventHandler;
 563    }
 564
 565    private void OnEnable()
 566    {
 567        if (animation == null)
 568            return;
 569
 570        animation.enabled = true;
 571        SetIdleFrame();
 572    }
 573
 574    private void OnDisable()
 575    {
 576        if (animation == null)
 577            return;
 578
 579        StopEmoteInternal(true);
 580        CrossFadeTo(AvatarAnimation.IDLE, idleAnimationName, 0);
 581        currentState = State_Ground;
 582
 583        AsyncAnimationStop().Forget();
 584    }
 585
 586    private async UniTaskVoid AsyncAnimationStop()
 587    {
 588        await UniTask.Yield(PlayerLoopTiming.LastPostLateUpdate);
 589
 590        if (animation == null)
 591            return;
 592
 593        animation.Stop();
 594        animation.enabled = false;
 595    }
 596
 597    private void OnDestroy()
 598    {
 599        if (animEventHandler != null)
 600            Destroy(animEventHandler);
 601    }
 602}

Methods/Properties

ShouldLoop(AvatarAnimation)