Load object base on distance v1 - without loading screen
This commit is contained in:
@@ -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:
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:2b36ad83acfce998e47fd8d2a57accdbf89d0299c143f4d116522316e50700b5
|
||||
size 196456189
|
||||
oid sha256:5416c9cdfd853925345f6d2269d19c815d0b14967e9cd0a066ee993b0ff98953
|
||||
size 196456367
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user