474 lines
16 KiB
C#
474 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using BrewMonster;
|
|
using BrewMonster.Network;
|
|
using BrewMonster.Scripts;
|
|
using Cysharp.Threading.Tasks;
|
|
using UnityEngine;
|
|
|
|
public class LitModelHolder : MonoSingleton<LitModelHolder>
|
|
{
|
|
#region Fields
|
|
[SerializeField] private AddressableObject[] addressableObjects;
|
|
private CECHostPlayer _hostPlayer;
|
|
|
|
[Header("Distance Streaming")]
|
|
[Tooltip("The distance from the host player to the object to be loaded immediately.")]
|
|
[SerializeField] private float _loadImmediateDistance => EC_Game.GetSettingViewDistance().fShow; // 200f
|
|
[Tooltip("Objects need to go outside of this distance to be unloaded.")]
|
|
[SerializeField] private float _unloadDistance => EC_Game.GetSettingViewDistance().fHide; // 400f
|
|
|
|
[Header("Performance")]
|
|
[SerializeField] private float _checkIntervalSeconds = 0.25f;
|
|
|
|
[Tooltip("How long a candidate object need to be in the immediate loading range to be loaded.")]
|
|
[SerializeField] private float _candidateWaitTimeSeconds = 1f;
|
|
|
|
[Tooltip("The minimum distance the host player needs to move to update the streaming.")]
|
|
[SerializeField] private float _minHostMoveToUpdate = 2.0f;
|
|
#endregion
|
|
|
|
#region Private Fields
|
|
// when an object enters the immediate loading range, we add it to this dictionary.
|
|
// The key is the object, the value is the enter timestamp.
|
|
private Dictionary<AddressableObject, float> _candidatesForLoading = new Dictionary<AddressableObject, float>();
|
|
private HashSet<AddressableObject> _loadedObjects = new HashSet<AddressableObject>();
|
|
private List<AddressableObject> _objectsToUnload = new List<AddressableObject>();
|
|
|
|
private Vector3 _lastHostPosOxz;
|
|
private bool _hasLastHostPos = false;
|
|
private Vector3 _currentHostPos;
|
|
private Vector3 _currentHostPosOxz;
|
|
private bool _hostPosReady = false;
|
|
private AddressableObject _currentObjectToCheck; // the object that we're currently checking for loading/unloading.
|
|
private bool _needToProcessLoadAndUnload = false;
|
|
|
|
private float _realTimeSinceStartUp;
|
|
|
|
private CancellationTokenSource _cts;
|
|
#endregion
|
|
|
|
|
|
#region Unity Lifecycle
|
|
protected override async void Initialize()
|
|
{
|
|
_currentHostPos = Vector3.zero;
|
|
_currentHostPosOxz = Vector3.zero;
|
|
await StartStreamingProcess();
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
// run every frame.
|
|
// We have to update the MainThread data here. Since other processes run on a different thread.
|
|
_realTimeSinceStartUp = Time.realtimeSinceStartup;
|
|
UpdateHostPlayerPosition();
|
|
|
|
// if load flag is set, process the load and unload objects.
|
|
if (_needToProcessLoadAndUnload)
|
|
{
|
|
ProcessLoadAndUnloadObjects();
|
|
_needToProcessLoadAndUnload = false;
|
|
}
|
|
}
|
|
|
|
private void OnDestroy()
|
|
{
|
|
if (_cts != null)
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region Private Functions
|
|
private async UniTask StartStreamingProcess()
|
|
{
|
|
if (_cts != null)
|
|
{
|
|
_cts.Cancel();
|
|
_cts.Dispose();
|
|
}
|
|
|
|
_cts = new CancellationTokenSource();
|
|
CancellationToken destroyToken = _cts.Token;
|
|
|
|
// Wait until Addressables are initialized.
|
|
while (!AddressableManager.Instance.IsInitialized())
|
|
{
|
|
await Task.Delay(100, destroyToken);
|
|
}
|
|
|
|
while (_hostPlayer == null)
|
|
{
|
|
_hostPlayer = GetHostPlayer();
|
|
await Task.Delay(100, destroyToken);
|
|
continue;
|
|
}
|
|
|
|
while (!_hostPlayer.IsAllResReady())
|
|
{
|
|
await Task.Delay(100, destroyToken);
|
|
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.
|
|
UniTask.RunOnThreadPool(async () =>
|
|
{
|
|
await StreamByDistanceLoop(destroyToken : destroyToken);
|
|
}
|
|
).Forget();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Immediately load the objects that are close to the host player. <br/>
|
|
/// Run this once at the beginning of the game. So we can load all the objects that are close to the host player.
|
|
/// </summary>
|
|
/// <param name="destroyToken"></param>
|
|
/// <returns></returns>
|
|
private async UniTask LoadObjectBaseOnDistance(CancellationToken destroyToken)
|
|
{
|
|
//if (_unloadDistance < _loadImmediateDistance)
|
|
//{
|
|
// _unloadDistance = _loadImmediateDistance;
|
|
//}
|
|
|
|
while (_hostPlayer == null)
|
|
{
|
|
_hostPlayer = GetHostPlayer();
|
|
if (_hostPlayer != null) break; // we found the host player.
|
|
await UniTask.Delay(10);
|
|
continue;
|
|
}
|
|
|
|
await Task.Delay(100, destroyToken); // wait for the host player to be initialized.
|
|
|
|
//In c++ version. clone object position is used as the host object position when force navigate.
|
|
if(_hostPlayer.IsInForceNavigateState())
|
|
{
|
|
_currentHostPosOxz = _hostPlayer.GetNavigatePlayer().GetNavigateModelPosition();
|
|
}
|
|
else
|
|
{
|
|
_currentHostPosOxz = _hostPlayer.GetPosVector3();
|
|
}
|
|
_currentHostPosOxz.y = 0;
|
|
LoadAllObjectsNearPosition(_currentHostPosOxz, destroyToken);
|
|
}
|
|
|
|
/// <summary>
|
|
/// This function will load all the objects that are close to the host player immediately. <br/> <br/>
|
|
/// <b>Use this once at the beginning of the game. Or when player is revived/GOTO/Teleported.</b>
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private void LoadAllObjectsNearPosition(Vector3 targetPosition, CancellationToken destroyToken)
|
|
{
|
|
if (addressableObjects == null || addressableObjects.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
//if (_unloadDistance < _loadImmediateDistance)
|
|
//{
|
|
// _unloadDistance = _loadImmediateDistance;
|
|
//}
|
|
|
|
float immediateSqr = _loadImmediateDistance * _loadImmediateDistance;
|
|
float paddingSqr = _unloadDistance * _unloadDistance;
|
|
|
|
List<AddressableObject> objectsToLoad = new List<AddressableObject>();
|
|
targetPosition.y = 0; // only consider Oxz plane
|
|
// go through all the objects, check for the immediateSqr and load the objects that are in the range.
|
|
TickStreaming(targetPosition, immediateSqr, paddingSqr, destroyToken);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// This function will be running continuously in the background.
|
|
/// </summary>
|
|
/// <param name="destroyToken"></param>
|
|
/// <returns></returns>
|
|
private async UniTask StreamByDistanceLoop(CancellationToken destroyToken)
|
|
{
|
|
//if (_unloadDistance < _loadImmediateDistance)
|
|
//{
|
|
// _unloadDistance = _loadImmediateDistance;
|
|
//}
|
|
|
|
float immediateSqr = _loadImmediateDistance * _loadImmediateDistance;
|
|
float paddingSqr = _unloadDistance * _unloadDistance;
|
|
|
|
int intervalMs = Mathf.Max(10, Mathf.RoundToInt(_checkIntervalSeconds * 1000f));
|
|
float minMoveSqr = _minHostMoveToUpdate * _minHostMoveToUpdate;
|
|
|
|
while (!destroyToken.IsCancellationRequested)
|
|
{
|
|
if (!_hostPosReady)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (_hasLastHostPos)
|
|
{
|
|
if ((_currentHostPosOxz - _lastHostPosOxz).sqrMagnitude < minMoveSqr)
|
|
{
|
|
await Task.Delay(intervalMs, destroyToken);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
_hasLastHostPos = true;
|
|
_lastHostPosOxz = _currentHostPosOxz;
|
|
|
|
TickStreaming(_currentHostPosOxz, immediateSqr, paddingSqr, destroyToken);
|
|
|
|
await Task.Delay(intervalMs, destroyToken);
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// The main loop of the distance streaming.
|
|
/// Go through all the objects and check if they are in the immediate loading range or padding range.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private void TickStreaming(Vector3 hostPos, float immediateSqr, float paddingSqr, CancellationToken destroyToken)
|
|
{
|
|
if (addressableObjects == null || addressableObjects.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int count = addressableObjects.Length;
|
|
lock (_objectsToUnload)
|
|
{
|
|
for (int i = 0; i < count; i++)
|
|
{
|
|
_currentObjectToCheck = addressableObjects[i];
|
|
if (_currentObjectToCheck == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float distSqr = (_currentObjectToCheck.ObjectPositionOxz - hostPos).sqrMagnitude;
|
|
|
|
if (distSqr <= immediateSqr)
|
|
{
|
|
if (_currentObjectToCheck.IsLoaded || _currentObjectToCheck.IsLoading)
|
|
{
|
|
continue;
|
|
}
|
|
if (!_candidatesForLoading.ContainsKey(_currentObjectToCheck))
|
|
{
|
|
//BMLogger.Log($"LitModelHolder: Added object to candidates for loading: {_currentObjectToCheck.assetPath}");
|
|
_candidatesForLoading[_currentObjectToCheck] = _realTimeSinceStartUp;
|
|
}
|
|
}
|
|
else if (distSqr > paddingSqr)
|
|
{
|
|
if (_currentObjectToCheck.IsLoaded || _currentObjectToCheck.IsLoading)
|
|
{
|
|
if (!_objectsToUnload.Contains(_currentObjectToCheck))
|
|
{
|
|
//BMLogger.Log($"LitModelHolder: Added object to unload: {_currentObjectToCheck.assetPath}");
|
|
_objectsToUnload.Add(_currentObjectToCheck);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
_needToProcessLoadAndUnload = true;
|
|
}
|
|
|
|
private void ProcessLoadAndUnloadObjects()
|
|
{
|
|
_currentIdxAsset = 0;
|
|
_maxIdxAsset = _candidatesForLoading.Count;
|
|
// load the objects that are in the loading range long enough. (_candidateWaitTimeSeconds)
|
|
foreach (var kvp in _candidatesForLoading)
|
|
{
|
|
kvp.Key.LoadAsset(CallBackAssetLoadingDone).Forget();
|
|
_loadedObjects.Add(kvp.Key);
|
|
}
|
|
_candidatesForLoading.Clear();
|
|
|
|
// unload the objects that are too far away.
|
|
for (int i = 0; i < _objectsToUnload.Count; i++)
|
|
{
|
|
_objectsToUnload[i].UnloadAsset();
|
|
// remove the object from the candidates for loading and loaded objects lists.
|
|
_loadedObjects.Remove(_objectsToUnload[i]);
|
|
_candidatesForLoading.Remove(_objectsToUnload[i]);
|
|
}
|
|
_objectsToUnload.Clear();
|
|
}
|
|
|
|
int _currentIdxAsset = 0; // The current counter for loaded assets.
|
|
int _maxIdxAsset = 0; // Limit the number of assets that have finished loading.
|
|
|
|
/// <summary>
|
|
/// isLitToReady is a condition used by _hostPlayer to wait until the Lit assets have finished loading.
|
|
/// This function counts the number of assets successfully loaded from the Addressable system,
|
|
/// and once the required number is reached, it sets _hostPlayer.isLitToReady = true.
|
|
/// </summary>
|
|
private void CallBackAssetLoadingDone()
|
|
{
|
|
_currentIdxAsset++;
|
|
if (_currentIdxAsset >= _maxIdxAsset)
|
|
{
|
|
if (_hostPlayer != null)
|
|
{
|
|
_hostPlayer.isLitToReady = true;
|
|
}
|
|
_currentIdxAsset = -1;
|
|
_maxIdxAsset = 0;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the Host Player instance.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
private CECHostPlayer GetHostPlayer()
|
|
{
|
|
return CECGameRun.Instance.GetHostPlayer();
|
|
}
|
|
|
|
private void UpdateHostPlayerPosition()
|
|
{
|
|
_hostPlayer = GetHostPlayer();
|
|
|
|
|
|
if (_hostPlayer != null)
|
|
{
|
|
if(_hostPlayer.IsInForceNavigateState())
|
|
{
|
|
_currentHostPosOxz = _hostPlayer.GetNavigatePlayer().GetNavigateModelPosition();
|
|
}
|
|
else
|
|
{
|
|
_currentHostPosOxz = _hostPlayer.GetPosVector3(false);
|
|
}
|
|
_currentHostPosOxz.y = 0;
|
|
_hostPosReady = true;
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
|
|
#region Public Functions
|
|
public async UniTask LoadAllObjectsNearTargetPosition(Vector3 targetPosition = default, bool useHostPlayerPosition = false)
|
|
{
|
|
if (!useHostPlayerPosition)
|
|
{
|
|
_currentHostPosOxz = targetPosition;
|
|
_currentHostPosOxz.y = 0;
|
|
}
|
|
else
|
|
{
|
|
while (_hostPlayer == null)
|
|
{
|
|
UpdateHostPlayerPosition();
|
|
await Task.Delay(10);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
|
|
UniTask.RunOnThreadPool(() => {LoadAllObjectsNearPosition(_currentHostPosOxz, _cts.Token);}).Forget();
|
|
}
|
|
#endregion
|
|
|
|
// 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
|
|
[Space(10)]
|
|
[Header("FOR EDITOR SETUP ONLY")]
|
|
[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
|
|
}
|