< Summary

Class:DCL.Controllers.SceneBoundsChecker
Assembly:DCL.Runtime
File(s):/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/WorldRuntime/SceneBoundariesController/SceneBoundsChecker.cs
Covered lines:153
Uncovered lines:25
Coverable lines:178
Total lines:395
Line coverage:85.9% (153 of 178)
Covered branches:0
Total branches:0

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity NPath complexity Sequence coverage
SceneBoundsChecker(...)0%220100%
Initialize()0%110100%
SetFeedbackStyle(...)0%110100%
GetFeedbackStyle()0%110100%
GetOriginalMaterials(...)0%2100%
CheckEntities()0%880100%
IsTimeBudgetDepleted(...)0%330100%
Restart()0%110100%
Start()0%2.032080%
Stop()0%220100%
Dispose()0%110100%
AddEntityToBeChecked(...)0%880100%
RemoveEntity(...)0%5.075085.71%
WasAddedAsPersistent(...)0%110100%
RunEntityEvaluation(...)0%110100%
RunEntityEvaluation(...)0%9.299084.62%
EvaluateMeshBounds(...)0%6.026091.67%
EvaluateEntityPosition(...)0%330100%
EvaluateAvatarMeshBounds(...)0%12300%
SetEntityInsideBoundariesState(...)0%3.073080%
HasMesh(...)0%440100%
IsEntityMeshInsideSceneBoundaries(...)0%5.125083.33%
AreSubmeshesInsideBoundaries(...)0%440100%
AreCollidersInsideBoundaries(...)0%440100%
SetMeshesAndComponentsInsideBoundariesState(...)0%110100%
SetEntityMeshesInsideBoundariesState(...)0%110100%
SetEntityCollidersInsideBoundariesState(...)0%770100%
SetComponentsInsideBoundariesValidState(...)0%440100%

File(s)

/tmp/workspace/unity-renderer/unity-renderer/Assets/Scripts/MainScripts/DCL/WorldRuntime/SceneBoundariesController/SceneBoundsChecker.cs

#LineLine coverage
 1using DCL.Models;
 2using UnityEngine;
 3using System.Collections.Generic;
 4using System.Collections;
 5using System;
 6
 7namespace DCL.Controllers
 8{
 9    public class SceneBoundsChecker : ISceneBoundsChecker
 10    {
 11        private const float NERFED_TIME_BUDGET = 0.5f / 1000f;
 12        private const float RECHECK_BUDGET = 0.5f;
 13        public event Action<IDCLEntity, bool> OnEntityBoundsCheckerStatusChanged;
 155714        public bool enabled => entitiesCheckRoutine != null;
 2735215        public float timeBetweenChecks { get; set; } = RECHECK_BUDGET;
 216        public int entitiesToCheckCount => entitiesToCheck.Count;
 17
 18        private const bool VERBOSE = false;
 49419        private Logger logger = new Logger("SceneBoundsChecker") {verboseEnabled = VERBOSE};
 49420        private HashSet<IDCLEntity> entitiesToCheck = new HashSet<IDCLEntity>();
 49421        private HashSet<IDCLEntity> checkedEntities = new HashSet<IDCLEntity>();
 49422        private HashSet<IDCLEntity> persistentEntities = new HashSet<IDCLEntity>();
 23        private ISceneBoundsFeedbackStyle feedbackStyle;
 24        private Coroutine entitiesCheckRoutine = null;
 25        private float lastCheckTime;
 26        private MessagingControllersManager messagingManager;
 27        private bool isNerfed;
 28
 29        public void Initialize()
 30        {
 49431            Start();
 49432        }
 33
 49434        public SceneBoundsChecker(ISceneBoundsFeedbackStyle feedbackStyle = null)
 35        {
 49436            this.feedbackStyle = feedbackStyle ?? new SceneBoundsFeedbackStyle_Simple();
 49437            messagingManager = Environment.i.messaging.manager as MessagingControllersManager;
 49438            isNerfed = DataStore.i.featureFlags.flags.Get().IsFeatureEnabled("NERF_SBC");
 49439        }
 40
 41        public void SetFeedbackStyle(ISceneBoundsFeedbackStyle feedbackStyle)
 42        {
 4743            this.feedbackStyle.CleanFeedback();
 4744            this.feedbackStyle = feedbackStyle;
 4745            Restart();
 4746        }
 47
 7748        public ISceneBoundsFeedbackStyle GetFeedbackStyle() { return feedbackStyle; }
 49
 050        public List<Material> GetOriginalMaterials(MeshesInfo meshesInfo) { return feedbackStyle.GetOriginalMaterials(me
 51
 52
 53        // TODO: Improve MessagingControllersManager.i.timeBudgetCounter usage once we have the centralized budget contr
 54        private IEnumerator CheckEntities()
 55        {
 2627856            while (true)
 57            {
 58                // Kinerius: Since the nerf can skip the process a lot faster than before, we need faster rechecks
 2681959                var finalTimeBetweenChecks = isNerfed ? NERFED_TIME_BUDGET : timeBetweenChecks;
 60
 2681961                float elapsedTime = Time.realtimeSinceStartup - lastCheckTime;
 2681962                if ((entitiesToCheck.Count > 0) && (finalTimeBetweenChecks <= 0f || elapsedTime >= finalTimeBetweenCheck
 63                {
 7664                    var timeBudget = NERFED_TIME_BUDGET;
 65
 66                    void processEntitiesList(HashSet<IDCLEntity> entities)
 67                    {
 7668                        if (IsTimeBudgetDepleted(timeBudget))
 69                        {
 70                            if(VERBOSE)
 71                                logger.Verbose("Time budget reached, escaping entities processing until next iteration..
 072                            return;
 73                        }
 74
 7675                        using HashSet<IDCLEntity>.Enumerator iterator = entities.GetEnumerator();
 15776                        while (iterator.MoveNext())
 77                        {
 8178                            if (IsTimeBudgetDepleted(timeBudget))
 79                            {
 080                                if(VERBOSE)
 81                                    logger.Verbose("Time budget reached, escaping entities processing until next iterati
 82                                return;
 83                            }
 84
 8185                            float startTime = Time.realtimeSinceStartup;
 86
 8187                            RunEntityEvaluation(iterator.Current);
 8188                            checkedEntities.Add(iterator.Current);
 89
 8190                            float finishTime = Time.realtimeSinceStartup;
 8191                            float usedTimeBudget = finishTime - startTime;
 92
 8193                            if (!isNerfed)
 94                            {
 8195                                if ( messagingManager != null )
 096                                    messagingManager.timeBudgetCounter -= usedTimeBudget;
 97                            }
 98
 8199                            timeBudget -= usedTimeBudget;
 100                        }
 152101                    }
 102
 76103                    processEntitiesList(entitiesToCheck);
 104
 105                    // As we can't modify the hashset while traversing it, we keep track of the entities that should be 
 76106                    using (var iterator = checkedEntities.GetEnumerator())
 107                    {
 157108                        while (iterator.MoveNext())
 109                        {
 81110                            RemoveEntity(iterator.Current, removeIfPersistent: false, resetState: false);
 111                        }
 76112                    }
 113
 114                    if(VERBOSE)
 115                        logger.Verbose($"Finished checking entities: checked entities {checkedEntities.Count}; entitiesT
 116
 76117                    checkedEntities.Clear();
 118
 76119                    lastCheckTime = Time.realtimeSinceStartup;
 120                }
 121
 26819122                yield return null;
 123            }
 124        }
 157125        private bool IsTimeBudgetDepleted(float timeBudget) { return isNerfed ? timeBudget <= 0f : messagingManager != n
 126
 127        public void Restart()
 128        {
 47129            Stop();
 47130            Start();
 47131        }
 132
 133        public void Start()
 134        {
 541135            if (entitiesCheckRoutine != null)
 0136                return;
 137
 541138            lastCheckTime = Time.realtimeSinceStartup;
 541139            entitiesCheckRoutine = CoroutineStarter.Start(CheckEntities());
 541140        }
 141
 142        public void Stop()
 143        {
 562144            if (entitiesCheckRoutine == null)
 21145                return;
 146
 541147            CoroutineStarter.Stop(entitiesCheckRoutine);
 541148            entitiesCheckRoutine = null;
 541149        }
 150
 151        public void Dispose()
 152        {
 494153            Stop();
 494154        }
 155
 156        public void AddEntityToBeChecked(IDCLEntity entity, bool isPersistent = false, bool runPreliminaryEvaluation = f
 157        {
 895158            if (!enabled || (entity.scene != null && entity.scene.isPersistent))
 635159                return;
 160
 260161            if (runPreliminaryEvaluation)
 162            {
 163                // The outer bounds check is cheaper than the regular check
 195164                RunEntityEvaluation(entity, onlyOuterBoundsCheck: true);
 165
 166                // No need to add the entity to be checked later if we already found it outside scene outer boundaries.
 167                // When the correct events are triggered again, the entity will be checked again.
 195168                if (!isPersistent && !entity.isInsideSceneOuterBoundaries)
 86169                    return;
 170            }
 171
 174172            entitiesToCheck.Add(entity);
 173
 174174            if (isPersistent)
 2175                persistentEntities.Add(entity);
 174176        }
 177
 178        public void RemoveEntity(IDCLEntity entity, bool removeIfPersistent = false, bool resetState = false)
 179        {
 243180            if (!enabled || (!removeIfPersistent && persistentEntities.Contains(entity)))
 0181                return;
 182
 243183            entitiesToCheck.Remove(entity);
 243184            persistentEntities.Remove(entity);
 185
 243186            if(resetState)
 160187                SetMeshesAndComponentsInsideBoundariesState(entity, true);
 243188        }
 189
 7190        public bool WasAddedAsPersistent(IDCLEntity entity) { return persistentEntities.Contains(entity); }
 191
 192        // TODO: When we remove the DCLBuilderEntity class we'll be able to remove this overload
 193        public void RunEntityEvaluation(IDCLEntity entity)
 194        {
 81195            RunEntityEvaluation(entity, false);
 81196        }
 197
 198        public void RunEntityEvaluation(IDCLEntity entity, bool onlyOuterBoundsCheck)
 199        {
 287200            if (entity == null || entity.gameObject == null || entity.scene == null || entity.scene.isPersistent)
 0201                return;
 202
 203            // Recursively evaluate entity children as well, we need to check this up front because this entity may not 
 287204            if (entity.children.Count > 0)
 205            {
 9206                using (var iterator = entity.children.GetEnumerator())
 207                {
 18208                    while (iterator.MoveNext())
 209                    {
 9210                        RunEntityEvaluation(iterator.Current.Value, onlyOuterBoundsCheck);
 211                    }
 9212                }
 213            }
 214
 287215            if (HasMesh(entity)) // If it has a mesh we don't evaluate its position due to artists "pivot point sloppine
 156216                EvaluateMeshBounds(entity, onlyOuterBoundsCheck);
 131217            else if (entity.scene.componentsManagerLegacy.HasComponent(entity, CLASS_ID_COMPONENT.AVATAR_SHAPE)) // Avat
 0218                EvaluateAvatarMeshBounds(entity, onlyOuterBoundsCheck);
 219            else
 131220                EvaluateEntityPosition(entity, onlyOuterBoundsCheck);
 131221        }
 222
 223        private void EvaluateMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 224        {
 225            // TODO: Can we cache the MaterialTransitionController somewhere to avoid this GetComponent() call?
 226            // If the mesh is being loaded we should skip the evaluation (it will be triggered again later when the load
 156227            if (entity.meshRootGameObject.GetComponent<MaterialTransitionController>()) // the object's MaterialTransiti
 0228                return;
 229
 156230            var loadWrapper = Environment.i.world.state.GetLoaderForEntity(entity);
 156231            if (loadWrapper != null && !loadWrapper.alreadyLoaded)
 8232                return;
 233
 148234            entity.isInsideSceneOuterBoundaries = entity.scene.IsInsideSceneOuterBoundaries(entity.meshesInfo.mergedBoun
 235
 148236            if (!entity.isInsideSceneOuterBoundaries)
 96237                SetMeshesAndComponentsInsideBoundariesState(entity, false);
 238
 148239            if (onlyOuterBoundsCheck)
 100240                return;
 241
 48242            SetMeshesAndComponentsInsideBoundariesState(entity, IsEntityMeshInsideSceneBoundaries(entity));
 48243        }
 244
 245        private void EvaluateEntityPosition(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 246        {
 131247            Vector3 entityGOPosition = entity.gameObject.transform.position;
 131248            entity.isInsideSceneOuterBoundaries = entity.scene.IsInsideSceneOuterBoundaries(entityGOPosition);
 249
 131250            if (!entity.isInsideSceneOuterBoundaries)
 251            {
 33252                SetComponentsInsideBoundariesValidState(entity, false);
 33253                SetEntityInsideBoundariesState(entity, false);
 254            }
 255
 131256            if (onlyOuterBoundsCheck)
 95257                return;
 258
 36259            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entityGOPosition + CommonScriptableObjects.wo
 36260            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 36261            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 36262        }
 263
 264        private void EvaluateAvatarMeshBounds(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 265        {
 0266            Vector3 entityGOPosition = entity.gameObject.transform.position;
 267
 268            // Heuristic using the entity scale for the size of the avatar bounds, otherwise we should configure the
 269            // entity's meshRootGameObject, etc. after its GPU skinning runs and use the regular entity mesh evaluation
 0270            Bounds avatarBounds = new Bounds();
 0271            avatarBounds.center = entityGOPosition;
 0272            avatarBounds.size = entity.gameObject.transform.lossyScale;
 273
 0274            entity.isInsideSceneOuterBoundaries = entity.scene.IsInsideSceneOuterBoundaries(avatarBounds);
 275
 0276            if (!entity.isInsideSceneOuterBoundaries)
 277            {
 0278                SetComponentsInsideBoundariesValidState(entity, false);
 0279                SetEntityInsideBoundariesState(entity, false);
 280            }
 281
 0282            if (onlyOuterBoundsCheck)
 0283                return;
 284
 0285            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(avatarBounds);
 0286            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 0287            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 0288        }
 289
 290        private void SetEntityInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries)
 291        {
 373292            if (entity.isInsideSceneBoundaries == isInsideBoundaries)
 251293                return;
 294
 122295            entity.isInsideSceneBoundaries = isInsideBoundaries;
 122296            OnEntityBoundsCheckerStatusChanged?.Invoke(entity, isInsideBoundaries);
 0297        }
 298
 299        private bool HasMesh(IDCLEntity entity)
 300        {
 287301            return entity.meshRootGameObject != null
 302                    && (entity.meshesInfo.colliders.Count > 0
 303                    || (entity.meshesInfo.renderers != null
 304                    && entity.meshesInfo.renderers.Length > 0));
 305        }
 306
 307        public bool IsEntityMeshInsideSceneBoundaries(IDCLEntity entity)
 308        {
 48309            if (entity.meshesInfo == null
 310                || entity.meshesInfo.meshRootGameObject == null
 311                || entity.meshesInfo.mergedBounds == null)
 0312                return false;
 313
 314            // 1st check (full mesh AABB)
 48315            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entity.meshesInfo.mergedBounds);
 316
 317            // 2nd check (submeshes & colliders AABB)
 48318            if (!isInsideBoundaries)
 319            {
 26320                isInsideBoundaries = AreSubmeshesInsideBoundaries(entity) && AreCollidersInsideBoundaries(entity);
 321            }
 322
 48323            return isInsideBoundaries;
 324        }
 325
 326        private bool AreSubmeshesInsideBoundaries(IDCLEntity entity)
 327        {
 54328            for (int i = 0; i < entity.meshesInfo.renderers.Length; i++)
 329            {
 24330                Renderer renderer = entity.meshesInfo.renderers[i];
 24331                if (renderer == null)
 332                    continue;
 333
 24334                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(renderer.bounds, renderer.transf
 23335                    return false;
 336            }
 337
 3338            return true;
 339        }
 340
 341        private bool AreCollidersInsideBoundaries(IDCLEntity entity)
 342        {
 10343            foreach (Collider collider in entity.meshesInfo.colliders)
 344            {
 3345                if (collider == null)
 346                    continue;
 347
 3348                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(collider.bounds, collider.transf
 2349                    return false;
 350            }
 351
 1352            return true;
 2353        }
 354
 355        private void SetMeshesAndComponentsInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries)
 356        {
 304357            SetEntityMeshesInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 304358            SetEntityCollidersInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 304359            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 360
 361            // Should always be set last as entity.isInsideSceneBoundaries is checked to avoid re-running code unnecessa
 304362            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 304363        }
 364
 365        private void SetEntityMeshesInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 366        {
 304367            feedbackStyle.ApplyFeedback(meshesInfo, isInsideBoundaries);
 304368        }
 369
 370        private void SetEntityCollidersInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 371        {
 304372            if (meshesInfo == null || meshesInfo.colliders.Count == 0 || !meshesInfo.currentShape.HasCollisions())
 88373                return;
 374
 960375            foreach (Collider collider in meshesInfo.colliders)
 376            {
 264377                if (collider == null) continue;
 378
 264379                if (collider.enabled != isInsideBoundaries)
 140380                    collider.enabled = isInsideBoundaries;
 381            }
 216382        }
 383
 384        private void SetComponentsInsideBoundariesValidState(IDCLEntity entity, bool isInsideBoundaries)
 385        {
 373386            if(entity.isInsideSceneBoundaries == isInsideBoundaries || !DataStore.i.sceneBoundariesChecker.componentsChe
 370387                return;
 388
 12389            foreach (IOutOfSceneBoundariesHandler component in DataStore.i.sceneBoundariesChecker.componentsCheckSceneBo
 390            {
 3391                component.UpdateOutOfBoundariesState(isInsideBoundaries);
 392            }
 3393        }
 394    }
 395}