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