Load object base on distance v1 - without loading screen

This commit is contained in:
Le Duc Anh
2026-02-16 13:46:16 +07:00
parent e7a940ff8c
commit 21754a63e7
8 changed files with 343 additions and 29 deletions
@@ -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
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2af08acf9536b4837b519f05326c9080
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:0e5e50bba3ad53b2b056bf2c465db0207542dddbe7ec72c03b23d26d3e977b15
size 108376
oid sha256:486a1814b7c89098f245397c87a04b43c8b9d7b906d4a7de20930badea5292fb
size 107133
@@ -65,6 +65,10 @@ namespace PerfectWorld.UI.MiniMap
UpdateMiniMap();
}
/// <summary>
/// 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.
/// </summary>
private void UpdateMiniMap()
{
m_pHostPlayer = GetHostPlayer();
@@ -84,6 +88,10 @@ namespace PerfectWorld.UI.MiniMap
_hostPlayerIcon.localRotation = Quaternion.Euler(0, 0, -hostTransform.localRotation.eulerAngles.y);
}
/// <summary>
/// Get the Host Player instance.
/// </summary>
/// <returns></returns>
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()
{
@@ -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);
}
@@ -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<AddressableObject> _objectsToLoad = new List<AddressableObject>();
private List<AddressableObject> _objectsToUnload = new List<AddressableObject>();
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();
}
}
/// <summary>
/// Get the Host Player instance.
/// </summary>
/// <returns></returns>
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<string, int>();
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
}
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2b36ad83acfce998e47fd8d2a57accdbf89d0299c143f4d116522316e50700b5
size 196456189
oid sha256:5416c9cdfd853925345f6d2269d19c815d0b14967e9cd0a066ee993b0ff98953
size 196456367
+6 -6
View File
@@ -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