diff --git a/Assets/AddressableAssetsData/AssetGroups/Schemas/a61_BundledAssetGroupSchema.asset b/Assets/AddressableAssetsData/AssetGroups/Schemas/a61_BundledAssetGroupSchema.asset index a8070d2725..230caf4788 100644 --- a/Assets/AddressableAssetsData/AssetGroups/Schemas/a61_BundledAssetGroupSchema.asset +++ b/Assets/AddressableAssetsData/AssetGroups/Schemas/a61_BundledAssetGroupSchema.asset @@ -26,7 +26,7 @@ MonoBehaviour: m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.BundledAssetProvider m_StripDownloadOptions: 0 m_ForceUniqueProvider: 0 - m_UseAssetBundleCache: 1 + m_UseAssetBundleCache: 0 m_UseAssetBundleCrc: 1 m_UseAssetBundleCrcForCachedBundles: 1 m_UseUWRForLocalBundles: 0 @@ -42,7 +42,7 @@ MonoBehaviour: m_AssetBundleProviderType: m_AssemblyName: Unity.ResourceManager, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null m_ClassName: UnityEngine.ResourceManagement.ResourceProviders.AssetBundleProvider - m_UseDefaultSchemaSettings: 0 + m_UseDefaultSchemaSettings: 1 m_SelectedPathPairIndex: 0 m_BundleNaming: 0 m_AssetLoadMode: 0 diff --git a/Assets/PerfectWorld/Resources/surfaces.meta b/Assets/PerfectWorld/Resources/surfaces.meta new file mode 100644 index 0000000000..09df3f922b --- /dev/null +++ b/Assets/PerfectWorld/Resources/surfaces.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 2af08acf9536b4837b519f05326c9080 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/PerfectWorld/Scene/LoginScene.unity b/Assets/PerfectWorld/Scene/LoginScene.unity index b9f404b506..4a4c9db013 100644 --- a/Assets/PerfectWorld/Scene/LoginScene.unity +++ b/Assets/PerfectWorld/Scene/LoginScene.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e5e50bba3ad53b2b056bf2c465db0207542dddbe7ec72c03b23d26d3e977b15 -size 108376 +oid sha256:486a1814b7c89098f245397c87a04b43c8b9d7b906d4a7de20930badea5292fb +size 107133 diff --git a/Assets/PerfectWorld/Scripts/UI/MiniMap/MiniMapUI.cs b/Assets/PerfectWorld/Scripts/UI/MiniMap/MiniMapUI.cs index c0ea0c31a9..5409c8fecb 100644 --- a/Assets/PerfectWorld/Scripts/UI/MiniMap/MiniMapUI.cs +++ b/Assets/PerfectWorld/Scripts/UI/MiniMap/MiniMapUI.cs @@ -65,6 +65,10 @@ namespace PerfectWorld.UI.MiniMap UpdateMiniMap(); } + /// + /// We keep the player icon at the center of the minimap. Then we update the position of the map itself. + /// TODO: We have to keep track of the NPC icons on the map also. + /// private void UpdateMiniMap() { m_pHostPlayer = GetHostPlayer(); @@ -84,6 +88,10 @@ namespace PerfectWorld.UI.MiniMap _hostPlayerIcon.localRotation = Quaternion.Euler(0, 0, -hostTransform.localRotation.eulerAngles.y); } + /// + /// Get the Host Player instance. + /// + /// private CECHostPlayer GetHostPlayer() { return CECGameRun.Instance.GetHostPlayer(); @@ -114,6 +122,8 @@ namespace PerfectWorld.UI.MiniMap } } + + // this is for debuging/testing while this feature was in development [ContextMenu("MoveHostPlayerIconToPos")] public void MoveHostPlayerIconToPos() { diff --git a/Assets/PerfectWorld/Scripts/World/AddressableObject.cs b/Assets/PerfectWorld/Scripts/World/AddressableObject.cs index afbc23898d..0f81429001 100644 --- a/Assets/PerfectWorld/Scripts/World/AddressableObject.cs +++ b/Assets/PerfectWorld/Scripts/World/AddressableObject.cs @@ -1,30 +1,90 @@ using BrewMonster.Scripts; using Cysharp.Threading.Tasks; -using UnityEditor; using UnityEngine; -using UnityEngine.AddressableAssets; + +#if UNITY_EDITOR +using UnityEditor; +#endif public class AddressableObject : MonoBehaviour { public string assetPath; + + private GameObject _instance; + private bool _isLoading; + private int _loadRequestId; + + public bool IsLoaded => _instance != null; + public bool IsLoading => _isLoading; public async UniTask LoadAsset() { - var model = await AddressableManager.Instance.LoadPrefabAsync(assetPath); - if (model != null) + if (string.IsNullOrEmpty(assetPath)) { - model = Instantiate(model); - var modelTransform = model.transform; + return; + } + + if (_instance != null || _isLoading) + { + return; + } + + _isLoading = true; + int requestId = ++_loadRequestId; + var model = await AddressableManager.Instance.LoadPrefabAsync(assetPath); + + // Object might have been destroyed while awaiting. + if (this == null || gameObject == null) + { + return; + } + + // If we got unloaded while loading, release the cached handle to avoid keeping RAM. + if (requestId != _loadRequestId) + { + _isLoading = false; + AddressableManager.Instance.ReleaseAsset(assetPath); + return; + } + + _isLoading = false; + + if (model != null && _instance == null) + { + _instance = Instantiate(model); + var modelTransform = _instance.transform; modelTransform.SetParent(transform); modelTransform.localPosition = Vector3.zero; modelTransform.localRotation = Quaternion.identity; modelTransform.localScale = Vector3.one; - model.SetActive(true); + _instance.SetActive(true); } } public void UnloadAsset() { + // make the loading request invalid. + _loadRequestId++; + _isLoading = false; + + if (_instance != null) + { + Destroy(_instance); + _instance = null; + } + else if (transform.childCount > 0) + { + // Backward-compatible cleanup if older versions instantiated children without tracking. + for (int i = transform.childCount - 1; i >= 0; i--) + { + Destroy(transform.GetChild(i).gameObject); + } + } + + if (string.IsNullOrEmpty(assetPath)) + { + return; + } AddressableManager.Instance.ReleaseAsset(assetPath); } diff --git a/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs b/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs index 04a98a80cc..305eb2e975 100644 --- a/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs +++ b/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs @@ -1,3 +1,7 @@ +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using BrewMonster; using BrewMonster.Scripts; using Cysharp.Threading.Tasks; using UnityEngine; @@ -5,34 +9,208 @@ using UnityEngine; public class LitModelHolder : MonoBehaviour { [SerializeField] private AddressableObject[] addressableObjects; + private CECHostPlayer _hostPlayer; + + [Header("Distance Streaming")] + [SerializeField] private float _loadImmediateDistance = 200f; + [SerializeField] private float _paddingDistance = 400f; + + [Header("Performance")] + [SerializeField] private float _checkIntervalSeconds = 0.25f; + [SerializeField] private float _minHostMoveToUpdate = 2.0f; + [SerializeField] private int _scanChunkSize = 64; + [SerializeField] private int _maxLoadsPerTick = 4; + [SerializeField] private int _maxUnloadsPerTick = 16; + + private int _scanIndex = 0; + private List _objectsToLoad = new List(); + private List _objectsToUnload = new List(); + + private Vector3 _lastHostPos; + private bool _hasLastHostPos = false; + private AddressableObject _currentObjectToCheck; // the object that we're currently checking for loading/unloading. private async void Awake() { - if (!AddressableManager.Instance.IsInitialized()) + CancellationToken destroyToken = this.GetCancellationTokenOnDestroy(); + + // Wait until Addressables are initialized. + while (!AddressableManager.Instance.IsInitialized()) { - await UniTask.DelayFrame(1); + await UniTask.DelayFrame(1, cancellationToken: destroyToken); } - await LoadAllAddressableObjects(); + while (_hostPlayer == null) + { + _hostPlayer = GetHostPlayer(); + await UniTask.Delay(100); + continue; + } + + while (!_hostPlayer.IsAllResReady()) + { + await UniTask.Delay(100); + continue; + } + + // run this once at the beginning of the game. So we can load all the objects that are close to the host player. + await LoadObjectBaseOnDistance(destroyToken); + + // Start distance-based streaming loop. This will be running continuously in the background. + StreamByDistanceLoop(destroyToken).Forget(); } - // for debug - private Vector3 centerPos = new Vector3(-771.7f, 47.5f, -261.0f); - private async UniTask LoadAllAddressableObjects() + private async UniTask LoadObjectBaseOnDistance(CancellationToken destroyToken) { - foreach (var addressableObject in addressableObjects) + if (_paddingDistance < _loadImmediateDistance) { - if ((addressableObject.transform.position - centerPos).magnitude > 100f) + _paddingDistance = _loadImmediateDistance; + } + + float immediateSqr = _loadImmediateDistance * _loadImmediateDistance; + float paddingSqr = _paddingDistance * _paddingDistance; + + while (_hostPlayer == null) + { + _hostPlayer = GetHostPlayer(); + if (_hostPlayer != null) break; // we found the host player. + await UniTask.Delay(10); + continue; + } + + await UniTask.DelayFrame(1); // wait for the host player to be initialized. + + await TickStreaming(_hostPlayer.transform.position, immediateSqr, paddingSqr, destroyToken); + } + + private async UniTask StreamByDistanceLoop(CancellationToken destroyToken) + { + if (_paddingDistance < _loadImmediateDistance) + { + _paddingDistance = _loadImmediateDistance; + } + + float immediateSqr = _loadImmediateDistance * _loadImmediateDistance; + float paddingSqr = _paddingDistance * _paddingDistance; + + int intervalMs = Mathf.Max(10, Mathf.RoundToInt(_checkIntervalSeconds * 1000f)); + float minMoveSqr = _minHostMoveToUpdate * _minHostMoveToUpdate; + + while (!destroyToken.IsCancellationRequested) + { + if (_hostPlayer == null) + { + _hostPlayer = GetHostPlayer(); + if (_hostPlayer != null) break; // we found the host player. + await UniTask.Delay(1000, cancellationToken: destroyToken); + continue; + } + + Vector3 hostPos = _hostPlayer.transform.position; + if (_hasLastHostPos) + { + if ((hostPos - _lastHostPos).sqrMagnitude < minMoveSqr) + { + await UniTask.Delay(intervalMs, cancellationToken: destroyToken); + continue; + } + } + + _hasLastHostPos = true; + _lastHostPos = hostPos; + + await TickStreaming(hostPos, immediateSqr, paddingSqr, destroyToken); + + await UniTask.Delay(intervalMs, cancellationToken: destroyToken); + } + } + + private async UniTask TickStreaming(Vector3 hostPos, float immediateSqr, float paddingSqr, CancellationToken destroyToken) + { + if (addressableObjects == null || addressableObjects.Length == 0) + { + return; + } + + int count = addressableObjects.Length; + int chunk = _scanChunkSize <= 0 ? count : Mathf.Min(_scanChunkSize, count); + + int loads = 0; + int unloads = 0; + + for (int i = 0; i < chunk; i++) + { + if (_scanIndex >= count) + { + _scanIndex = 0; + } + + _currentObjectToCheck = addressableObjects[_scanIndex]; + _scanIndex++; + + if (_currentObjectToCheck == null) { continue; } - await addressableObject.LoadAsset(); - await UniTask.DelayFrame(1); + + float distSqr = (_currentObjectToCheck.transform.position - hostPos).sqrMagnitude; + + if (distSqr <= immediateSqr) + { + if (!_currentObjectToCheck.IsLoaded && !_currentObjectToCheck.IsLoading) + { + _currentObjectToCheck.LoadAsset().Forget(); + } + } + else if (distSqr > paddingSqr) + { + if (_currentObjectToCheck.IsLoaded || _currentObjectToCheck.IsLoading) + { + _objectsToUnload.Add(_currentObjectToCheck); + } + } + } + + // unload the objects that are too far away. + foreach (var obj in _objectsToUnload) + { + obj.UnloadAsset(); } } + /// + /// Get the Host Player instance. + /// + /// + private CECHostPlayer GetHostPlayer() + { + return CECGameRun.Instance.GetHostPlayer(); + } + + private void OnDestroy() + { + if (addressableObjects == null) + { + return; + } + for (int i = 0; i < addressableObjects.Length; i++) + { + AddressableObject obj = addressableObjects[i]; + if (obj == null) + { + continue; + } + if (obj.IsLoaded || obj.IsLoading) + { + obj.UnloadAsset(); + } + } + } + + // Keep this part of the code for the initial setup of the addressable objects. + // We have to run this once on every scene. #if UNITY_EDITOR [SerializeField] private GameObject[] originalObjects; @@ -53,5 +231,63 @@ public class LitModelHolder : MonoBehaviour } } } + + [ContextMenu("Validate Addressable Objects")] + private void ValidateAddressableObjects() + { + if (addressableObjects == null || addressableObjects.Length == 0) + { + Debug.LogWarning("[LitModelHolder] addressableObjects is empty."); + return; + } + + int nullCount = 0; + int emptyPathCount = 0; + int multiChildCount = 0; + + var pathCounts = new System.Collections.Generic.Dictionary(); + + for (int i = 0; i < addressableObjects.Length; i++) + { + AddressableObject obj = addressableObjects[i]; + if (obj == null) + { + nullCount++; + continue; + } + + if (string.IsNullOrEmpty(obj.assetPath)) + { + emptyPathCount++; + } + else + { + pathCounts.TryGetValue(obj.assetPath, out int c); + pathCounts[obj.assetPath] = c + 1; + } + + // If this is > 1 it often indicates duplicate instantiation under the anchor. + if (obj.transform.childCount > 1) + { + multiChildCount++; + } + } + + int duplicatePathKeys = 0; + foreach (var kvp in pathCounts) + { + if (kvp.Value > 1) + { + duplicatePathKeys++; + } + } + + Debug.Log($"[LitModelHolder] ValidateAddressableObjects:\n" + + $"- total={addressableObjects.Length}\n" + + $"- nullEntries={nullCount}\n" + + $"- emptyAssetPath={emptyPathCount}\n" + + $"- duplicateAssetPathKeys={duplicatePathKeys}\n" + + $"- anchorsWithMoreThanOneChild={multiChildCount}"); + } #endif } \ No newline at end of file diff --git a/Assets/Scenes/a61.unity b/Assets/Scenes/a61.unity index eec0314af8..93efb306da 100644 --- a/Assets/Scenes/a61.unity +++ b/Assets/Scenes/a61.unity @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2b36ad83acfce998e47fd8d2a57accdbf89d0299c143f4d116522316e50700b5 -size 196456189 +oid sha256:5416c9cdfd853925345f6d2269d19c815d0b14967e9cd0a066ee993b0ff98953 +size 196456367 diff --git a/Assets/Settings/Mobile_RPAsset.asset b/Assets/Settings/Mobile_RPAsset.asset index 124f6d0229..51ef81c244 100644 --- a/Assets/Settings/Mobile_RPAsset.asset +++ b/Assets/Settings/Mobile_RPAsset.asset @@ -26,7 +26,7 @@ MonoBehaviour: m_SupportsHDR: 1 m_HDRColorBufferPrecision: 0 m_MSAA: 1 - m_RenderScale: 0.8 + m_RenderScale: 1 m_UpscalingFilter: 3 m_FsrOverrideSharpness: 0 m_FsrSharpness: 0.92 @@ -45,7 +45,7 @@ MonoBehaviour: m_MainLightShadowsSupported: 1 m_MainLightShadowmapResolution: 1024 m_AdditionalLightsRenderingMode: 1 - m_AdditionalLightsPerObjectLimit: 4 + m_AdditionalLightsPerObjectLimit: 0 m_AdditionalLightShadowsSupported: 0 m_AdditionalLightsShadowmapResolution: 2048 m_AdditionalLightsShadowResolutionTierLow: 256 @@ -54,9 +54,9 @@ MonoBehaviour: m_ReflectionProbeBlending: 1 m_ReflectionProbeBoxProjection: 1 m_ShadowDistance: 50 - m_ShadowCascadeCount: 1 + m_ShadowCascadeCount: 3 m_Cascade2Split: 0.25 - m_Cascade3Split: {x: 0.1, y: 0.3} + m_Cascade3Split: {x: 0.10250626, y: 0.28145128} m_Cascade4Split: {x: 0.067, y: 0.2, z: 0.467} m_CascadeBorder: 0.2 m_ShadowDepthBias: 1 @@ -100,10 +100,10 @@ MonoBehaviour: m_Keys: [] m_Values: m_PrefilteringModeMainLightShadows: 3 - m_PrefilteringModeAdditionalLight: 0 + m_PrefilteringModeAdditionalLight: 3 m_PrefilteringModeAdditionalLightShadows: 0 m_PrefilterXRKeywords: 1 - m_PrefilteringModeForwardPlus: 2 + m_PrefilteringModeForwardPlus: 0 m_PrefilteringModeDeferredRendering: 0 m_PrefilteringModeScreenSpaceOcclusion: 0 m_PrefilterDebugKeywords: 1