From 124b62f13d19aa28616964b31d4bfe31d2ececa1 Mon Sep 17 00:00:00 2001 From: Vivian V Wing Date: Sat, 23 May 2026 16:21:24 -0700 Subject: [PATCH] Implemented CulledGameObjectManager to cull distant world objects * added relevant 'Object Culling' setting to Video settings (defaulted to enabled) * ActiveGameObjectDatabase now supports inactive object lookups (required for non-buggy culling) * billboards are now registered with the ActiveGameObjectDatabase (so they may also be culled) * added CulledGameObjectManager to the DaggerfallUnityGame.unity scene --- Assets/Resources/defaults.ini.txt | 1 + Assets/Scenes/DaggerfallUnityGame.unity | 75 ++++ Assets/Scripts/ActiveGameObjectDatabase.cs | 99 +++--- .../Scripts/Game/CulledGameObjectManager.cs | 336 ++++++++++++++++++ .../Game/CulledGameObjectManager.cs.meta | 11 + .../DaggerfallAdvancedSettingsWindow.cs | 3 + .../Scripts/Internal/DaggerfallBillboard.cs | 2 + Assets/Scripts/SettingsManager.cs | 3 + Assets/StreamingAssets/Text/GameSettings.txt | 2 + 9 files changed, 491 insertions(+), 41 deletions(-) create mode 100644 Assets/Scripts/Game/CulledGameObjectManager.cs create mode 100644 Assets/Scripts/Game/CulledGameObjectManager.cs.meta diff --git a/Assets/Resources/defaults.ini.txt b/Assets/Resources/defaults.ini.txt index 25fec25626..6485213d04 100644 --- a/Assets/Resources/defaults.ini.txt +++ b/Assets/Resources/defaults.ini.txt @@ -31,6 +31,7 @@ DungeonShadowDistance=20.0 InteriorShadowDistance=30.0 ExteriorShadowDistance=90.0 EnableTextureArrays=True +EnableObjectCulling=True RandomDungeonTextures=0 [Effects] diff --git a/Assets/Scenes/DaggerfallUnityGame.unity b/Assets/Scenes/DaggerfallUnityGame.unity index ab2bfbec9b..7e31e617a4 100644 --- a/Assets/Scenes/DaggerfallUnityGame.unity +++ b/Assets/Scenes/DaggerfallUnityGame.unity @@ -2894,3 +2894,78 @@ PrefabInstance: objectReference: {fileID: 1769263313} m_RemovedComponents: [] m_SourcePrefab: {fileID: 100100000, guid: bbe5e15c9f6b3dc47bb9485a437750a0, type: 3} +--- !u!114 &69466200359737372 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1834928754549345153} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 43fe9d3db5f70f24bb00284402a4d97e, type: 3} + m_Name: + m_EditorClassIdentifier: + culledObjectsParent: {fileID: 1269490794079082448} +--- !u!4 &357226078190250312 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1834928754549345153} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 14.787919, y: 27.745857, z: 29.176554} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: + - {fileID: 2109761705362116671} + m_Father: {fileID: 0} + m_RootOrder: 23 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1269490794079082448 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2109761705362116671} + m_Layer: 0 + m_Name: CulledObjects + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 0 +--- !u!1 &1834928754549345153 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 357226078190250312} + - component: {fileID: 69466200359737372} + m_Layer: 0 + m_Name: CulledGameObjectManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &2109761705362116671 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1269490794079082448} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_Children: [] + m_Father: {fileID: 357226078190250312} + m_RootOrder: 0 + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} diff --git a/Assets/Scripts/ActiveGameObjectDatabase.cs b/Assets/Scripts/ActiveGameObjectDatabase.cs index e84bb562d4..a2154ea56b 100644 --- a/Assets/Scripts/ActiveGameObjectDatabase.cs +++ b/Assets/Scripts/ActiveGameObjectDatabase.cs @@ -29,7 +29,7 @@ public GameObjectCache(string name) // Returns all the active GameObjects in the cache // If a system calls SetActive(false) on an object without destroying it, it will not be returned here - public IEnumerable GetActiveObjects() + public IEnumerable GetActiveObjects(bool includeInactive = false) { cacheLock.EnterReadLock(); try @@ -42,7 +42,7 @@ public IEnumerable GetActiveObjects() // A null check on a GameObject does more than C#'s reference check, // it also checks if the object has been destroyed // Like Object.FindObjectsOfType, we should only include active objects - if (activeObject != null && activeObject.activeInHierarchy) + if (activeObject != null && (includeInactive || activeObject.activeInHierarchy)) gameObjects.Add(activeObject); } } @@ -57,12 +57,12 @@ public IEnumerable GetActiveObjects() // Returns all the enabled components of active GameObjects in the cache // If a system calls SetActive(false) on an object without destroying it, it will not be returned here - public IEnumerable GetActiveComponents() where T : MonoBehaviour + public IEnumerable GetActiveComponents(bool includeInactive = false) where T : MonoBehaviour { - foreach (GameObject gameObject in GetActiveObjects()) + foreach (GameObject gameObject in GetActiveObjects(includeInactive)) { var t = gameObject.GetComponent(); - if (t != null && t.isActiveAndEnabled) + if (t != null && (includeInactive || t.isActiveAndEnabled)) yield return t; } } @@ -201,35 +201,51 @@ public static class ActiveGameObjectDatabase static GameObjectCache staticNpcCache = new GameObjectCache("Static NPC"); static GameObjectCache actionDoorCache = new GameObjectCache("Action Door"); static GameObjectCache rdbCache = new GameObjectCache("RDB"); + static GameObjectCache billboardCache = new GameObjectCache("Billboard"); + + public static IEnumerable GetActiveBillboardObjects(bool includeInactive = false) + { + return billboardCache.GetActiveObjects(includeInactive); + } + + public static IEnumerable GetActiveBillboards(bool includeInactive = false) + { + return billboardCache.GetActiveComponents(includeInactive); + } + + public static void RegisterBillboard(GameObject billboard) + { + billboardCache.AddObject(billboard); + } // Gets all the active enemy GameObjects. Must be registered as Enemy (see below) - public static IEnumerable GetActiveEnemyObjects() + public static IEnumerable GetActiveEnemyObjects(bool includeInactive = false) { - return enemyCache.GetActiveObjects(); + return enemyCache.GetActiveObjects(includeInactive); } // Gets all the enabled DaggerfallEntityBehaviour components from active registered enemies - public static IEnumerable GetActiveEnemyBehaviours() + public static IEnumerable GetActiveEnemyBehaviours(bool includeInactive = false) { - return enemyCache.GetActiveComponents(); + return enemyCache.GetActiveComponents(includeInactive); } // Gets all the enabled DaggerfallEnemy components from active registered enemies - public static IEnumerable GetActiveEnemyEntities() + public static IEnumerable GetActiveEnemyEntities(bool includeInactive = false) { - return enemyCache.GetActiveComponents(); + return enemyCache.GetActiveComponents(includeInactive); } // Gets all the enabled QuestResourceBehaviour components from active registered enemies - public static IEnumerable GetActiveEnemyQuestResourceBehaviours() + public static IEnumerable GetActiveEnemyQuestResourceBehaviours(bool includeInactive = false) { - return enemyCache.GetActiveComponents(); + return enemyCache.GetActiveComponents(includeInactive); } // Gets all the enabled EnemyMotor components from active registered enemies - public static IEnumerable GetActiveEnemyMotors() + public static IEnumerable GetActiveEnemyMotors(bool includeInactive = false) { - return enemyCache.GetActiveComponents(); + return enemyCache.GetActiveComponents(includeInactive); } // Registers an enemy (monster or class) to the enemy cache. Does not have to be active @@ -239,15 +255,15 @@ public static void RegisterEnemy(GameObject enemy) } // Gets all the active Civilian Mobile GameObjects. Must be registered as Civilian Mobile (see below) - public static IEnumerable GetActiveCivilianMobileObjects() + public static IEnumerable GetActiveCivilianMobileObjects(bool includeInactive = false) { - return civilianCache.GetActiveObjects(); + return civilianCache.GetActiveObjects(includeInactive); } // Gets all the enabled DaggerfallEntityBehaviour components from active registered Civilian Mobiles - public static IEnumerable GetActiveCivilianMobileBehaviours() + public static IEnumerable GetActiveCivilianMobileBehaviours(bool includeInactive = false) { - return civilianCache.GetActiveComponents(); + return civilianCache.GetActiveComponents(includeInactive); } // Registers a mobile civilian NPC to the civilian cache. Does not have to be active @@ -257,15 +273,15 @@ public static void RegisterCivilianMobile(GameObject civilian) } // Gets all the active loot GameObjects. Must be registered as Loot (see below) - public static IEnumerable GetActiveLootObjects() + public static IEnumerable GetActiveLootObjects(bool includeInactive = false) { - return lootCache.GetActiveObjects(); + return lootCache.GetActiveObjects(includeInactive); } // Gets all the enabled DaggerfallLoot components from active registered loot - public static IEnumerable GetActiveLoot() + public static IEnumerable GetActiveLoot(bool includeInactive = false) { - return lootCache.GetActiveComponents(); + return lootCache.GetActiveComponents(includeInactive); } // Registers a loot object to the loot cache. Does not have to be active @@ -275,15 +291,15 @@ public static void RegisterLoot(GameObject loot) } // Gets all the active Foe Spawner GameObjects. Must be registered as Foe Spawner (see below) - public static IEnumerable GetActiveFoeSpawnerObjects() + public static IEnumerable GetActiveFoeSpawnerObjects(bool includeInactive = false) { - return foeSpawnerCache.GetActiveObjects(); + return foeSpawnerCache.GetActiveObjects(includeInactive); } // Gets all the enabled FoeSpawner components from active registered foe spawners - public static IEnumerable GetActiveFoeSpawners() + public static IEnumerable GetActiveFoeSpawners(bool includeInactive = false) { - return foeSpawnerCache.GetActiveComponents(); + return foeSpawnerCache.GetActiveComponents(includeInactive); } // Registers a foe spawner object to the foe spawner cache. Does not have to be active @@ -293,21 +309,21 @@ public static void RegisterFoeSpawner(GameObject foeSpawner) } // Gets all the active Static NPC GameObjects. Must be registered as a Static NPC (see below) - public static IEnumerable GetActiveStaticNPCObjects() + public static IEnumerable GetActiveStaticNPCObjects(bool includeInactive = false) { - return staticNpcCache.GetActiveObjects(); + return staticNpcCache.GetActiveObjects(includeInactive); } // Gets all the enabled StaticNPC components from active registered static NPCs - public static IEnumerable GetActiveStaticNPCs() + public static IEnumerable GetActiveStaticNPCs(bool includeInactive = false) { - return staticNpcCache.GetActiveComponents(); + return staticNpcCache.GetActiveComponents(includeInactive); } // Gets all the enabled QuestResourceBehaviour components from active registered static NPCs - public static IEnumerable GetActiveStaticNPCQuestResourceBehaviours() + public static IEnumerable GetActiveStaticNPCQuestResourceBehaviours(bool includeInactive = false) { - return staticNpcCache.GetActiveComponents(); + return staticNpcCache.GetActiveComponents(includeInactive); } // Registers a static NPC object to the Static NPC cache. Does not have to be active @@ -317,15 +333,15 @@ public static void RegisterStaticNPC(GameObject staticNPC) } // Gets all the active Action Door GameObjects. Must be registered as Action Door (see below) - public static IEnumerable GetActiveActionDoorObjects() + public static IEnumerable GetActiveActionDoorObjects(bool includeInactive = false) { - return actionDoorCache.GetActiveObjects(); + return actionDoorCache.GetActiveObjects(includeInactive); } // Gets all the enabled DaggerfallActionDoor components from active registered doors - public static IEnumerable GetActiveActionDoors() + public static IEnumerable GetActiveActionDoors(bool includeInactive = false) { - return actionDoorCache.GetActiveComponents(); + return actionDoorCache.GetActiveComponents(includeInactive); } // Registers an Action Door object to the Action Door cache. Does not have to be active @@ -336,15 +352,15 @@ public static void RegisterActionDoor(GameObject door) } // Gets all the active RDB GameObjects. Must be registered as RDB (see below) - public static IEnumerable GetActiveRDBObjects() + public static IEnumerable GetActiveRDBObjects(bool includeInactive = false) { - return rdbCache.GetActiveObjects(); + return rdbCache.GetActiveObjects(includeInactive); } // Gets all the enabled DaggerfallStaticDoors components from active registered RDBs - public static IEnumerable GetActiveRDBStaticDoors() + public static IEnumerable GetActiveRDBStaticDoors(bool includeInactive = false) { - return rdbCache.GetActiveComponents(); + return rdbCache.GetActiveComponents(includeInactive); } // Registers a Daggerfall dungeon block "RDB" game object to the RDB cache. Does not have to be active @@ -363,6 +379,7 @@ public static IEnumerable GetCacheDebugLines() yield return staticNpcCache.GetDebugString(); yield return actionDoorCache.GetDebugString(); yield return rdbCache.GetDebugString(); + yield return billboardCache.GetDebugString(); } } } \ No newline at end of file diff --git a/Assets/Scripts/Game/CulledGameObjectManager.cs b/Assets/Scripts/Game/CulledGameObjectManager.cs new file mode 100644 index 0000000000..83e13ddd7a --- /dev/null +++ b/Assets/Scripts/Game/CulledGameObjectManager.cs @@ -0,0 +1,336 @@ +using DaggerfallWorkshop.Game.Entity; +using DaggerfallWorkshop.Game.Serialization; +using System.Collections.Generic; +using System.Linq; +using UnityEngine; + +namespace DaggerfallWorkshop.Game +{ + public struct CulledGameObject + { + public GameObject gameObject; + public Transform originalParent; + public Vector3 originalLocalPosition; + public Quaternion originalLocalRotation; + public bool wasOriginalParentNull; + public bool wasObjectInside; + public CulledGameObject(GameObject objectToCull) + { + gameObject = objectToCull; + originalParent = objectToCull.transform.parent; + originalLocalPosition = objectToCull.transform.localPosition; + originalLocalRotation = objectToCull.transform.localRotation; + wasOriginalParentNull = objectToCull.transform.parent == null; + wasObjectInside = GameManager.Instance.IsPlayerInside; + } + } + + public class CulledGameObjectManager : MonoBehaviour + { + public static CulledGameObjectManager Instance { get; private set; } + public const float UnscaledBlockRange = 2060; + public const float ScaledBlockRange = UnscaledBlockRange * MeshReader.GlobalScale; + public const float ScaledBlockRangeSquared = ScaledBlockRange * ScaledBlockRange; + + [SerializeField] + private GameObject culledObjectsParent; + + private Dictionary culledObjects = new Dictionary(); + private List keysToRemove = new List(); + private int cullIteration = 0; + + private int lastFrameCulledAndUnculledAllObjects = -1; + + private Vector3 lastPlayerPosition = new Vector3(0, 0, 0); + private bool wasPlayerInside = false; + + private void Awake() + { + if (Instance == null) + { + Instance = this; + } + else + { + Destroy(gameObject); + return; + } + + if (culledObjectsParent == null) + { + culledObjectsParent = new GameObject("CulledObjectsParent"); + culledObjectsParent.transform.parent = transform; + culledObjectsParent.SetActive(false); + } + } + private void Start() + { + lastPlayerPosition = GameManager.Instance.PlayerMotor.transform.position; + SaveLoadManager.OnLoad += OnLoadEvent; + } + private void OnDestroy() + { + SaveLoadManager.OnLoad -= OnLoadEvent; + } + private void Update() + { + if (!DaggerfallUnity.Settings.EnableObjectCulling) + { + UnCullAllObjects(); + return; + } + + Vector3 playerPosition = GameManager.Instance.PlayerMotor.transform.position; + // Remove any deleted gameObjects from the culled objects + RemoveAnyDeletedObjectsFromCulledDictionary(); + if (GameManager.Instance.IsPlayerInside != wasPlayerInside + || (playerPosition - lastPlayerPosition).sqrMagnitude > ScaledBlockRangeSquared / 4 + || culledObjects.Count == 0) // make sure everything is updated instantly upon teleport/transition + { + UpdateAllCullableObjects(playerPosition); + } + else + UpdateCullableObjectsBasedOnIteration(cullIteration, playerPosition); // otherwise do a new batch of objects every other frame. + cullIteration++; + cullIteration %= 12; + lastPlayerPosition = playerPosition; + wasPlayerInside = GameManager.Instance.IsPlayerInside; + } + + public bool IsObjectCulled(GameObject obj) + { + return obj && culledObjects.ContainsKey(obj.GetInstanceID()); + } + private void UpdateAllCullableObjects(Vector3 playerPosition, bool skipDoors = true) + { + // prevent multiple calls updating all cullable objects in the same frame + if (Time.frameCount == lastFrameCulledAndUnculledAllObjects) + return; + lastFrameCulledAndUnculledAllObjects = Time.frameCount; + + // now iterate through and cull/uncull all objects + for (int i = 0; i <= 10; i += 2) + { + if (skipDoors && i == 4) + continue; // skip doors if asked to do so + UpdateCullableObjectsBasedOnIteration(i, playerPosition); + } + cullIteration = 10; // jump to iteration 10 so it's a couple frames and then it starts over + } + private void UpdateCullableObjectsBasedOnIteration(int cullIteration, Vector3 playerPosition) + { + // Cull and un-cull objects based on range. All objects outside of the ScaledBlockRange distance from player should be culled. + switch (cullIteration) + { + case 0: + if (!GameManager.Instance.IsPlayerInside) + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveBillboardObjects(true), 150 * 150); + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveFoeSpawnerObjects(true)); + break; + case 2: + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveEnemyObjects(true)); + CullAndUncullDistantDungeonBlocks(playerPosition, ActiveGameObjectDatabase.GetActiveRDBObjects(true).Where(p => !p.transform.root || p.transform.root.gameObject.name != "Automap")); + break; + case 4: + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveActionDoorObjects(true)); + break; + case 6: + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveStaticNPCObjects(true)); + break; + case 8: + CullAndUncullDistantObjects(playerPosition, ActiveGameObjectDatabase.GetActiveLootObjects(true)); + break; + case 10: + break; + //disabled since civilian mobile objects already cull themselves. + //case 5: + // //allCullableObjects.AddRange(ActiveGameObjectDatabase.GetActiveCivilianMobileObjects(true)); + // break; + + } + } + + private void RemoveAnyDeletedObjectsFromCulledDictionary() + { + foreach (var kvp in culledObjects) + { + if (!kvp.Value.gameObject || !kvp.Value.wasOriginalParentNull && !kvp.Value.originalParent || kvp.Value.wasObjectInside != GameManager.Instance.IsPlayerInside) + keysToRemove.Add(kvp.Key); + } + foreach (int k in keysToRemove) + { + if (culledObjects[k].gameObject) + Destroy(culledObjects[k].gameObject); + culledObjects.Remove(k); + } + keysToRemove.Clear(); + } + private void CullAndUncullDistantObjects(Vector3 playerPosition, IEnumerable cullableObjects, float maxSquaredDistance = ScaledBlockRangeSquared) + { + foreach (GameObject obj in cullableObjects) + { + if (obj.transform.position.sqrMagnitude < 0.0001f) // Don't cull objects spawned at world origin. + continue; // These are usually things like quest spawners. + + float sqrDistance = (playerPosition - obj.transform.position).sqrMagnitude; + if (sqrDistance > maxSquaredDistance) + { + if (!IsObjectCulled(obj)) + { + CullObject(obj); + } + } + else + { + if (IsObjectCulled(obj)) + { + UnCullObject(obj); + } + } + } + } + private void CullAndUncullDistantDungeonBlocks(Vector3 playerPosition, IEnumerable cullableRDBObjects) + { + foreach (GameObject block in cullableRDBObjects) + { + // Constructing bounds manually based on the block's footprint and pivot information + Vector3 blockCenter = block.transform.position + Vector3.one * (1024 * MeshReader.GlobalScale); // Center of the block + Vector3 blockSize = Vector3.one * (2048 * MeshReader.GlobalScale); // Assuming infinite height for simplicity + Bounds blockBounds = new Bounds(blockCenter, blockSize); + + // Check if the player is within the ScaledBlockRange from the closest point on the bounds + float closestPointDistance = Vector3.Distance(playerPosition, blockBounds.ClosestPoint(playerPosition)); + if (closestPointDistance > ScaledBlockRange && !blockBounds.Contains(playerPosition)) // Check if outside range and player not inside block + { + if (!IsObjectCulled(block)) + { + CullDungeonBlock(block); + } + } + else + { + if (IsObjectCulled(block)) + { + UnCullDungeonBlock(block); + } + } + } + } + + private bool CullObject(GameObject objectToCull) + { + if (!objectToCull) + return false; + int objectToCullID = objectToCull.GetInstanceID(); + if (objectToCull == null || culledObjects.ContainsKey(objectToCullID)) + return false; + + CulledGameObject culledObject = new CulledGameObject(objectToCull); + + objectToCull.transform.SetParent(culledObjectsParent.transform, true); + culledObjects[objectToCullID] = culledObject; + return true; + } + + private bool UnCullObject(GameObject objectToUnCull) + { + if (!objectToUnCull) + return false; + int objectToUncullID = objectToUnCull.GetInstanceID(); + if (!culledObjects.ContainsKey(objectToUncullID)) + return false; + CulledGameObject culledObject = culledObjects[objectToUncullID]; + + if (culledObject.gameObject != null) + { + if (!culledObject.wasOriginalParentNull && !culledObject.originalParent) // If the original parent was destroyed + { + Destroy(culledObject.gameObject); // Destroy the object, since it would have been destroyed along with the parent + } + else // Parent exists or it didn't have one in the first place. Restore the original parent and transform + { + culledObject.gameObject.transform.SetParent(culledObject.originalParent, true); + culledObject.gameObject.transform.localPosition = culledObject.originalLocalPosition; + culledObject.gameObject.transform.localRotation = culledObject.originalLocalRotation; + } + } + culledObjects.Remove(objectToUncullID); + return true; + } + + private bool CullDungeonBlock(GameObject dungeonBlock) + { + if (!dungeonBlock) + return false; + int objectToCullID = dungeonBlock.GetInstanceID(); + if (dungeonBlock == null || culledObjects.ContainsKey(objectToCullID)) + return false; + + CulledGameObject culledObject = new CulledGameObject(dungeonBlock); + + SetDungeonBlockCulled(culledObject.gameObject, true); + culledObjects[objectToCullID] = culledObject; + return true; + } + + private bool UnCullDungeonBlock(GameObject dungeonBlock) + { + if (!dungeonBlock) + return false; + int objectToUncullID = dungeonBlock.GetInstanceID(); + if (!culledObjects.ContainsKey(objectToUncullID)) + return false; + CulledGameObject culledObject = culledObjects[objectToUncullID]; + SetDungeonBlockCulled(culledObject.gameObject, false); + culledObjects.Remove(objectToUncullID); + return true; + } + private void SetDungeonBlockCulled(GameObject block, bool culled) + { + if (!block) + return; + for (int i = 0; i < block.transform.childCount; ++i) + { + GameObject blockChild = block.transform.GetChild(i).gameObject; + blockChild.SetActive(!culled ? true : blockChild.name == "Models" || blockChild.name == "Action Models"); + } + block.transform.Find("Models").GetComponentsInChildren().ToList().ForEach(p => p.enabled = !culled); + block.transform.Find("Action Models").GetComponentsInChildren().ToList().ForEach(p => p.enabled = !culled); + } + + private void UnCullAllObjects() + { + if (culledObjects.Count == 0) + return; + + keysToRemove.Clear(); + foreach (var kvp in culledObjects) + keysToRemove.Add(kvp.Key); + + foreach (int key in keysToRemove) + { + CulledGameObject culledObject = culledObjects[key]; + if (!culledObject.gameObject) + { + culledObjects.Remove(key); + continue; + } + + if (culledObject.gameObject.transform.parent == culledObjectsParent.transform) + UnCullObject(culledObject.gameObject); + else + UnCullDungeonBlock(culledObject.gameObject); + } + + keysToRemove.Clear(); + } + + private void OnLoadEvent(SaveData_v1 saveData) + { + if (DaggerfallUnity.Settings.EnableObjectCulling) + UpdateAllCullableObjects(GameManager.Instance.PlayerMotor.transform.position); + else + UnCullAllObjects(); + } + } +} diff --git a/Assets/Scripts/Game/CulledGameObjectManager.cs.meta b/Assets/Scripts/Game/CulledGameObjectManager.cs.meta new file mode 100644 index 0000000000..f271139d10 --- /dev/null +++ b/Assets/Scripts/Game/CulledGameObjectManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 43fe9d3db5f70f24bb00284402a4d97e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/Game/UserInterfaceWindows/DaggerfallAdvancedSettingsWindow.cs b/Assets/Scripts/Game/UserInterfaceWindows/DaggerfallAdvancedSettingsWindow.cs index 0b6b6db336..6284f5f819 100644 --- a/Assets/Scripts/Game/UserInterfaceWindows/DaggerfallAdvancedSettingsWindow.cs +++ b/Assets/Scripts/Game/UserInterfaceWindows/DaggerfallAdvancedSettingsWindow.cs @@ -157,6 +157,7 @@ enum IconsPositioningSchemes { classic, medium, small, smalldeckleft, smalldeckr Checkbox interiorLightShadows; Checkbox exteriorLightShadows; Checkbox ambientLitInteriors; + Checkbox enableObjectCulling; // Accessibility Button automapTempleColor; @@ -378,6 +379,7 @@ private void Video(Panel leftPanel, Panel rightPanel) interiorLightShadows = AddCheckbox(rightPanel, "interiorLightShadows", DaggerfallUnity.Settings.InteriorLightShadows); exteriorLightShadows = AddCheckbox(rightPanel, "exteriorLightShadows", DaggerfallUnity.Settings.ExteriorLightShadows); ambientLitInteriors = AddCheckbox(rightPanel, "ambientLitInteriors", DaggerfallUnity.Settings.AmbientLitInteriors); + enableObjectCulling = AddCheckbox(rightPanel, "enableObjectCulling", DaggerfallUnity.Settings.EnableObjectCulling); string textureArrayLabel = TextManager.Instance.GetLocalizedText("textureArrayLabel", TextCollections.TextSettings); if (!SystemInfo.supports2DArrayTextures) textureArrayLabel += TextManager.Instance.GetLocalizedText("unsupported", TextCollections.TextSettings); @@ -531,6 +533,7 @@ private void SaveSettings() DaggerfallUnity.Settings.InteriorLightShadows = interiorLightShadows.IsChecked; DaggerfallUnity.Settings.ExteriorLightShadows = exteriorLightShadows.IsChecked; DaggerfallUnity.Settings.AmbientLitInteriors = ambientLitInteriors.IsChecked; + DaggerfallUnity.Settings.EnableObjectCulling = enableObjectCulling.IsChecked; /* Accessibility */ DaggerfallUnity.Settings.AutomapTempleColor = automapTempleColor.BackgroundColor; diff --git a/Assets/Scripts/Internal/DaggerfallBillboard.cs b/Assets/Scripts/Internal/DaggerfallBillboard.cs index b16028c6f3..1446b19a37 100644 --- a/Assets/Scripts/Internal/DaggerfallBillboard.cs +++ b/Assets/Scripts/Internal/DaggerfallBillboard.cs @@ -82,6 +82,8 @@ void Start() // Example is the treasury in Daggerfall castle, some action records flow through the quest item marker meshRenderer.enabled = false; } + + ActiveGameObjectDatabase.RegisterBillboard(gameObject); } } diff --git a/Assets/Scripts/SettingsManager.cs b/Assets/Scripts/SettingsManager.cs index bd0c24924d..9f846b443f 100644 --- a/Assets/Scripts/SettingsManager.cs +++ b/Assets/Scripts/SettingsManager.cs @@ -179,6 +179,7 @@ string ReadDistributionSuffix() public float InteriorShadowDistance { get; set; } public float ExteriorShadowDistance { get; set; } public bool EnableTextureArrays { get; set; } + public bool EnableObjectCulling { get; set; } public int RandomDungeonTextures { get; set; } public int CursorWidth { get; set; } public int CursorHeight { get; set; } @@ -425,6 +426,7 @@ public void LoadSettings() InteriorShadowDistance = GetFloat(sectionVideo, "InteriorShadowDistance", 0.1f, 50.0f); ExteriorShadowDistance = GetFloat(sectionVideo, "ExteriorShadowDistance", 0.1f, 150.0f); EnableTextureArrays = GetBool(sectionVideo, "EnableTextureArrays"); + EnableObjectCulling = GetBool(sectionVideo, "EnableObjectCulling"); RandomDungeonTextures = GetInt(sectionVideo, "RandomDungeonTextures", 0, 4); AntialiasingMethod = GetInt(sectionEffects, "AntialiasingMethod", 0, 3); @@ -622,6 +624,7 @@ public void SaveSettings() SetFloat(sectionVideo, "InteriorShadowDistance", InteriorShadowDistance); SetFloat(sectionVideo, "ExteriorShadowDistance", ExteriorShadowDistance); SetBool(sectionVideo, "EnableTextureArrays", EnableTextureArrays); + SetBool(sectionVideo, "EnableObjectCulling", EnableObjectCulling); SetInt(sectionVideo, "RandomDungeonTextures", RandomDungeonTextures); SetInt(sectionEffects, "AntialiasingMethod", AntialiasingMethod); diff --git a/Assets/StreamingAssets/Text/GameSettings.txt b/Assets/StreamingAssets/Text/GameSettings.txt index c33580d54e..73689477d2 100644 --- a/Assets/StreamingAssets/Text/GameSettings.txt +++ b/Assets/StreamingAssets/Text/GameSettings.txt @@ -172,6 +172,8 @@ terrainDistance, Terrain Distance terrainDistanceInfo, Maximum distance of active terrains from player position shadowResolutionMode, Shadow Resolution shadowResolutionModeInfo, Quality of shadows +enableObjectCulling, Object Culling +enableObjectCullingInfo, Cull distant objects to improve performance dungeonLightShadows, Dungeon Light Shadows dungeonLightShadowsInfo, Dungeon lights cast shadows interiorLightShadows, Interior Light Shadows