< 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:156
Uncovered lines:22
Coverable lines:178
Total lines:395
Line coverage:87.6% (156 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.049092.31%
EvaluateMeshBounds(...)0%6.026091.67%
EvaluateEntityPosition(...)0%330100%
EvaluateAvatarMeshBounds(...)0%12300%
SetEntityInsideBoundariesState(...)0%330100%
HasMesh(...)0%440100%
IsEntityMeshInsideSceneBoundaries(...)0%550100%
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;
 100614        public bool enabled => entitiesCheckRoutine != null;
 71115        public float timeBetweenChecks { get; set; } = RECHECK_BUDGET;
 216        public int entitiesToCheckCount => entitiesToCheck.Count;
 17
 18        private const bool VERBOSE = false;
 67219        private Logger logger = new Logger("SceneBoundsChecker") {verboseEnabled = VERBOSE};
 67220        private HashSet<IDCLEntity> entitiesToCheck = new HashSet<IDCLEntity>();
 67221        private HashSet<IDCLEntity> checkedEntities = new HashSet<IDCLEntity>();
 67222        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        {
 67231            Start();
 67232        }
 33
 67234        public SceneBoundsChecker(ISceneBoundsFeedbackStyle feedbackStyle = null)
 35        {
 67236            this.feedbackStyle = feedbackStyle ?? new SceneBoundsFeedbackStyle_Simple();
 67237            messagingManager = Environment.i.messaging.manager as MessagingControllersManager;
 67238            isNerfed = DataStore.i.featureFlags.flags.Get().IsFeatureEnabled("NERF_SBC");
 67239        }
 40
 41        public void SetFeedbackStyle(ISceneBoundsFeedbackStyle feedbackStyle)
 42        {
 5543            this.feedbackStyle.CleanFeedback();
 5544            this.feedbackStyle = feedbackStyle;
 5545            Restart();
 5546        }
 47
 7848        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        {
 689056            while (true)
 57            {
 58                // Kinerius: Since the nerf can skip the process a lot faster than before, we need faster rechecks
 761759                var finalTimeBetweenChecks = isNerfed ? NERFED_TIME_BUDGET : timeBetweenChecks;
 60
 761761                float elapsedTime = Time.realtimeSinceStartup - lastCheckTime;
 761762                if ((entitiesToCheck.Count > 0) && (finalTimeBetweenChecks <= 0f || elapsedTime >= finalTimeBetweenCheck
 63                {
 7464                    var timeBudget = NERFED_TIME_BUDGET;
 65
 66                    void processEntitiesList(HashSet<IDCLEntity> entities)
 67                    {
 7468                        if (IsTimeBudgetDepleted(timeBudget))
 69                        {
 70                            if(VERBOSE)
 71                                logger.Verbose("Time budget reached, escaping entities processing until next iteration..
 072                            return;
 73                        }
 74
 7475                        using HashSet<IDCLEntity>.Enumerator iterator = entities.GetEnumerator();
 15276                        while (iterator.MoveNext())
 77                        {
 7878                            if (IsTimeBudgetDepleted(timeBudget))
 79                            {
 080                                if(VERBOSE)
 81                                    logger.Verbose("Time budget reached, escaping entities processing until next iterati
 82                                return;
 83                            }
 84
 7885                            float startTime = Time.realtimeSinceStartup;
 86
 7887                            RunEntityEvaluation(iterator.Current);
 7888                            checkedEntities.Add(iterator.Current);
 89
 7890                            float finishTime = Time.realtimeSinceStartup;
 7891                            float usedTimeBudget = finishTime - startTime;
 92
 7893                            if (!isNerfed)
 94                            {
 7895                                if ( messagingManager != null )
 096                                    messagingManager.timeBudgetCounter -= usedTimeBudget;
 97                            }
 98
 7899                            timeBudget -= usedTimeBudget;
 100                        }
 148101                    }
 102
 74103                    processEntitiesList(entitiesToCheck);
 104
 105                    // As we can't modify the hashset while traversing it, we keep track of the entities that should be 
 74106                    using (var iterator = checkedEntities.GetEnumerator())
 107                    {
 152108                        while (iterator.MoveNext())
 109                        {
 78110                            RemoveEntity(iterator.Current, removeIfPersistent: false, resetState: false);
 111                        }
 74112                    }
 113
 114                    if(VERBOSE)
 115                        logger.Verbose($"Finished checking entities: checked entities {checkedEntities.Count}; entitiesT
 116
 74117                    checkedEntities.Clear();
 118
 74119                    lastCheckTime = Time.realtimeSinceStartup;
 120                }
 121
 7617122                yield return null;
 123            }
 124        }
 152125        private bool IsTimeBudgetDepleted(float timeBudget) { return isNerfed ? timeBudget <= 0f : messagingManager != n
 126
 127        public void Restart()
 128        {
 55129            Stop();
 55130            Start();
 55131        }
 132
 133        public void Start()
 134        {
 727135            if (entitiesCheckRoutine != null)
 0136                return;
 137
 727138            lastCheckTime = Time.realtimeSinceStartup;
 727139            entitiesCheckRoutine = CoroutineStarter.Start(CheckEntities());
 727140        }
 141
 142        public void Stop()
 143        {
 748144            if (entitiesCheckRoutine == null)
 21145                return;
 146
 727147            CoroutineStarter.Stop(entitiesCheckRoutine);
 727148            entitiesCheckRoutine = null;
 727149        }
 150
 151        public void Dispose()
 152        {
 672153            Stop();
 672154        }
 155
 156        public void AddEntityToBeChecked(IDCLEntity entity, bool isPersistent = false, bool runPreliminaryEvaluation = f
 157        {
 1280158            if (!enabled || (entity.scene != null && entity.scene.isPersistent))
 969159                return;
 160
 311161            if (runPreliminaryEvaluation)
 162            {
 163                // The outer bounds check is cheaper than the regular check
 246164                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.
 246168                if (!isPersistent && !entity.isInsideSceneOuterBoundaries)
 89169                    return;
 170            }
 171
 222172            entitiesToCheck.Add(entity);
 173
 222174            if (isPersistent)
 6175                persistentEntities.Add(entity);
 222176        }
 177
 178        public void RemoveEntity(IDCLEntity entity, bool removeIfPersistent = false, bool resetState = false)
 179        {
 546180            if (!enabled || (!removeIfPersistent && persistentEntities.Contains(entity)))
 0181                return;
 182
 546183            entitiesToCheck.Remove(entity);
 546184            persistentEntities.Remove(entity);
 185
 546186            if(resetState)
 466187                SetMeshesAndComponentsInsideBoundariesState(entity, true);
 546188        }
 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        {
 92195            RunEntityEvaluation(entity, false);
 92196        }
 197
 198        public void RunEntityEvaluation(IDCLEntity entity, bool onlyOuterBoundsCheck)
 199        {
 349200            if (entity == null || entity.gameObject == null || entity.scene == null || entity.scene.isPersistent)
 7201                return;
 202
 203            // Recursively evaluate entity children as well, we need to check this up front because this entity may not 
 342204            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
 342215            if (HasMesh(entity)) // If it has a mesh we don't evaluate its position due to artists "pivot point sloppine
 163216                EvaluateMeshBounds(entity, onlyOuterBoundsCheck);
 179217            else if (entity.scene.componentsManagerLegacy.HasComponent(entity, CLASS_ID_COMPONENT.AVATAR_SHAPE)) // Avat
 0218                EvaluateAvatarMeshBounds(entity, onlyOuterBoundsCheck);
 219            else
 179220                EvaluateEntityPosition(entity, onlyOuterBoundsCheck);
 179221        }
 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
 163227            if (entity.meshRootGameObject.GetComponent<MaterialTransitionController>()) // the object's MaterialTransiti
 0228                return;
 229
 163230            var loadWrapper = Environment.i.world.state.GetLoaderForEntity(entity);
 163231            if (loadWrapper != null && !loadWrapper.alreadyLoaded)
 10232                return;
 233
 153234            entity.isInsideSceneOuterBoundaries = entity.scene.IsInsideSceneOuterBoundaries(entity.meshesInfo.mergedBoun
 235
 153236            if (!entity.isInsideSceneOuterBoundaries)
 99237                SetMeshesAndComponentsInsideBoundariesState(entity, false);
 238
 153239            if (onlyOuterBoundsCheck)
 104240                return;
 241
 49242            SetMeshesAndComponentsInsideBoundariesState(entity, IsEntityMeshInsideSceneBoundaries(entity));
 49243        }
 244
 245        private void EvaluateEntityPosition(IDCLEntity entity, bool onlyOuterBoundsCheck = false)
 246        {
 179247            Vector3 entityGOPosition = entity.gameObject.transform.position;
 179248            entity.isInsideSceneOuterBoundaries = entity.scene.IsInsideSceneOuterBoundaries(entityGOPosition);
 249
 179250            if (!entity.isInsideSceneOuterBoundaries)
 251            {
 35252                SetComponentsInsideBoundariesValidState(entity, false);
 35253                SetEntityInsideBoundariesState(entity, false);
 254            }
 255
 179256            if (onlyOuterBoundsCheck)
 140257                return;
 258
 39259            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entityGOPosition + CommonScriptableObjects.wo
 39260            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 39261            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 39262        }
 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        {
 688292            if (entity.isInsideSceneBoundaries == isInsideBoundaries)
 562293                return;
 294
 126295            entity.isInsideSceneBoundaries = isInsideBoundaries;
 126296            OnEntityBoundsCheckerStatusChanged?.Invoke(entity, isInsideBoundaries);
 3297        }
 298
 299        private bool HasMesh(IDCLEntity entity)
 300        {
 342301            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        {
 90309            if (entity.meshesInfo == null
 310                || entity.meshesInfo.meshRootGameObject == null
 311                || entity.meshesInfo.mergedBounds == null)
 25312                return false;
 313
 314            // 1st check (full mesh AABB)
 65315            bool isInsideBoundaries = entity.scene.IsInsideSceneBoundaries(entity.meshesInfo.mergedBounds);
 316
 317            // 2nd check (submeshes & colliders AABB)
 65318            if (!isInsideBoundaries)
 319            {
 30320                isInsideBoundaries = AreSubmeshesInsideBoundaries(entity) && AreCollidersInsideBoundaries(entity);
 321            }
 322
 65323            return isInsideBoundaries;
 324        }
 325
 326        private bool AreSubmeshesInsideBoundaries(IDCLEntity entity)
 327        {
 70328            for (int i = 0; i < entity.meshesInfo.renderers.Length; i++)
 329            {
 31330                Renderer renderer = entity.meshesInfo.renderers[i];
 31331                if (renderer == null)
 332                    continue;
 333
 31334                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(renderer.bounds, renderer.transf
 26335                    return false;
 336            }
 337
 4338            return true;
 339        }
 340
 341        private bool AreCollidersInsideBoundaries(IDCLEntity entity)
 342        {
 13343            foreach (Collider collider in entity.meshesInfo.colliders)
 344            {
 4345                if (collider == null)
 346                    continue;
 347
 4348                if (!entity.scene.IsInsideSceneBoundaries(MeshesInfoUtils.GetSafeBounds(collider.bounds, collider.transf
 3349                    return false;
 350            }
 351
 1352            return true;
 3353        }
 354
 355        private void SetMeshesAndComponentsInsideBoundariesState(IDCLEntity entity, bool isInsideBoundaries)
 356        {
 614357            SetEntityMeshesInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 614358            SetEntityCollidersInsideBoundariesState(entity.meshesInfo, isInsideBoundaries);
 614359            SetComponentsInsideBoundariesValidState(entity, isInsideBoundaries);
 360
 361            // Should always be set last as entity.isInsideSceneBoundaries is checked to avoid re-running code unnecessa
 614362            SetEntityInsideBoundariesState(entity, isInsideBoundaries);
 614363        }
 364
 365        private void SetEntityMeshesInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 366        {
 614367            feedbackStyle.ApplyFeedback(meshesInfo, isInsideBoundaries);
 614368        }
 369
 370        private void SetEntityCollidersInsideBoundariesState(MeshesInfo meshesInfo, bool isInsideBoundaries)
 371        {
 614372            if (meshesInfo == null || meshesInfo.colliders.Count == 0 || !meshesInfo.currentShape.HasCollisions())
 395373                return;
 374
 978375            foreach (Collider collider in meshesInfo.colliders)
 376            {
 270377                if (collider == null) continue;
 378
 270379                if (collider.enabled != isInsideBoundaries)
 140380                    collider.enabled = isInsideBoundaries;
 381            }
 219382        }
 383
 384        private void SetComponentsInsideBoundariesValidState(IDCLEntity entity, bool isInsideBoundaries)
 385        {
 688386            if(entity.isInsideSceneBoundaries == isInsideBoundaries || !DataStore.i.sceneBoundariesChecker.componentsChe
 685387                return;
 388
 12389            foreach (IOutOfSceneBoundariesHandler component in DataStore.i.sceneBoundariesChecker.componentsCheckSceneBo
 390            {
 3391                component.UpdateOutOfBoundariesState(isInsideBoundaries);
 392            }
 3393        }
 394    }
 395}