Files
test/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs
T
2026-02-17 20:33:54 +07:00

328 lines
10 KiB
C#

using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using BrewMonster;
using BrewMonster.Scripts;
using Cysharp.Threading.Tasks;
using UnityEngine;
public class LitModelHolder : MonoSingleton<LitModelHolder>
{
[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;
// this list is only available if we want to wait until all the objects are loaded.
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()
{
CancellationToken destroyToken = this.GetCancellationTokenOnDestroy();
// Wait until Addressables are initialized.
while (!AddressableManager.Instance.IsInitialized())
{
await UniTask.DelayFrame(1, cancellationToken: destroyToken);
}
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();
}
private async UniTask LoadObjectBaseOnDistance(CancellationToken destroyToken)
{
if (_paddingDistance < _loadImmediateDistance)
{
_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, true, 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, false, destroyToken);
await UniTask.Delay(intervalMs, cancellationToken: destroyToken);
}
}
private async UniTask TickStreaming(Vector3 hostPos, float immediateSqr, float paddingSqr, bool waitUntilFinished, 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;
}
float distSqr = (_currentObjectToCheck.transform.position - hostPos).sqrMagnitude;
if (distSqr <= immediateSqr)
{
if (!_currentObjectToCheck.IsLoaded && !_currentObjectToCheck.IsLoading)
{
_currentObjectToCheck.LoadAsset().Forget();
_objectsToLoad.Add(_currentObjectToCheck);
}
}
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();
}
while (!IsFinishedLoadingObjects() && waitUntilFinished)
{
await UniTask.DelayFrame(1, cancellationToken: destroyToken);
}
_objectsToLoad.Clear();
BMLogger.Log("[LitModelHolder] All objects are loaded.");
}
/// <summary>
/// Get the Host Player instance.
/// </summary>
/// <returns></returns>
private CECHostPlayer GetHostPlayer()
{
return CECGameRun.Instance.GetHostPlayer();
}
/// <summary>
/// Return True if all the objects that are needed to be loaded are loaded.
/// </summary>
/// <returns></returns>
private bool IsFinishedLoadingObjects()
{
AddressableObject obj = null;
for (int i = 0; i < _objectsToLoad.Count; i++)
{
obj = _objectsToLoad[i];
if (obj == null)
{
continue;
}
if (!obj.IsLoaded && !obj.IsLoading)
{
return false;
}
}
return true;
}
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;
[ContextMenu("Setup Addressable Objects")]
public void SetupAddressableObjects()
{
foreach (var originalObject in originalObjects)
{
var addressableObject = originalObject.GetComponent<AddressableObject>();
if (addressableObject == null)
{
addressableObject = originalObject.AddComponent<AddressableObject>();
}
if (addressableObject != null)
{
addressableObject.GetAssetPath();
}
}
}
[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
}