diff --git a/Assets/PerfectWorld/Scripts/Common/Logger.cs b/Assets/PerfectWorld/Scripts/Common/Logger.cs index 11e092e00f..79cc5af3fa 100644 --- a/Assets/PerfectWorld/Scripts/Common/Logger.cs +++ b/Assets/PerfectWorld/Scripts/Common/Logger.cs @@ -1,4 +1,4 @@ -#define ENALBE_LOGGING +// #define ENALBE_LOGGING using UnityEngine; namespace BrewMonster diff --git a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs index 9346d3bce3..6b46d6afdf 100644 --- a/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs +++ b/Assets/PerfectWorld/Scripts/Move/CECIntelligentRoute.cs @@ -245,7 +245,7 @@ namespace BrewMonster.Scripts { // Fallback: assume maps/{mapName}/movemap/ structure // 回退:假设 maps/{mapName}/movemap/ 结构 - Debug.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: cannot resolve relative key '{key}' with basePath '{basePath}'"); + BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: cannot resolve relative key '{key}' with basePath '{basePath}'"); return null; } } @@ -279,18 +279,18 @@ namespace BrewMonster.Scripts // it means the files need to be properly configured in Addressables. // 如果资产存在但注册为 DefaultAsset(文件夹)而不是 TextAsset, // 这意味着文件需要在 Addressables 中正确配置。 - Debug.LogError($"[CECIntelligentRoute] ResolveAddressableBytes: Asset '{address}' is registered as DefaultAsset (folder) instead of TextAsset. " + + BMLogger.LogError($"[CECIntelligentRoute] ResolveAddressableBytes: Asset '{address}' is registered as DefaultAsset (folder) instead of TextAsset. " + $"Please ensure the file is properly imported as a TextAsset in Unity and marked as Addressable. " + $"You may need to: 1) Select the file in Unity, 2) Set Import Type to 'Text' in Inspector, 3) Mark as Addressable. Error: {ex.Message}"); return null; } - Debug.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: failed to load '{address}' (asset is null or has no bytes)"); + BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes: failed to load '{address}' (asset is null or has no bytes)"); return null; } catch (Exception ex) { - Debug.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes failed for '{key}': {ex.Message}"); + BMLogger.LogWarning($"[CECIntelligentRoute] ResolveAddressableBytes failed for '{key}': {ex.Message}"); return null; } } @@ -309,7 +309,7 @@ namespace BrewMonster.Scripts ResetSearch(); if (m_moveAgents.Count == 0) { - if (DEBUG_AUTOPF) Debug.LogWarning("[CECIntelligentRoute] Search: no moveAgents (uninitialized)"); + if (DEBUG_AUTOPF) BMLogger.LogWarning("[CECIntelligentRoute] Search: no moveAgents (uninitialized)"); return SearchResult.enumSearchUnInitialized; } @@ -364,7 +364,7 @@ namespace BrewMonster.Scripts { if (DEBUG_AUTOPF) { - Debug.LogWarning($"[CECIntelligentRoute] Search: no path. start=({start.x:F2},{start.y:F2},{start.z:F2}) end=({end.x:F2},{end.y:F2},{end.z:F2}) agentIndex={idx}"); + BMLogger.LogWarning($"[CECIntelligentRoute] Search: no path. start=({start.x:F2},{start.y:F2},{start.z:F2}) end=({end.x:F2},{end.y:F2},{end.z:F2}) agentIndex={idx}"); } ResetSearch(); return SearchResult.enumSearchNoPath; @@ -374,7 +374,7 @@ namespace BrewMonster.Scripts m_iCurDest = 0; if (DEBUG_AUTOPF) { - Debug.Log($"[CECIntelligentRoute] Search: success pathCount={agent.GetPathCount()} start=({start.x:F2},{start.z:F2}) end=({end.x:F2},{end.z:F2})"); + BMLogger.Log($"[CECIntelligentRoute] Search: success pathCount={agent.GetPathCount()} start=({start.x:F2},{start.z:F2}) end=({end.x:F2},{end.z:F2})"); } return SearchResult.enumSearchSuccess; } diff --git a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs index 5db820e60d..59da4b195c 100644 --- a/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/CSNetwork/GameSession.cs @@ -512,14 +512,14 @@ namespace CSNetwork { _logger.Log(LogType.Debug, $"Sending protocol: {protocol.GetType().Name} (Detail: {protocol.ToString})"); - Debug.Log($"[GameSession] Sending protocol: {protocol.GetType().Name} (Type: {protocol.GetPType()})"); + BMLogger.Log($"[GameSession] Sending protocol: {protocol.GetType().Name} (Type: {protocol.GetPType()})"); _networkManager.Send(protocol); complete?.Invoke(); } else { _logger.Log(LogType.Warning, $"Cannot send protocol ({protocol.GetType().Name}), not connected."); - Debug.LogError($"[GameSession] Cannot send protocol ({protocol.GetType().Name}), not connected."); + BMLogger.LogError($"[GameSession] Cannot send protocol ({protocol.GetType().Name}), not connected."); } } diff --git a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs index a05855d5bc..7435c6fa2b 100644 --- a/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs +++ b/Assets/PerfectWorld/Scripts/Network/UnityGameSession.cs @@ -150,7 +150,7 @@ namespace BrewMonster.Network public void c2s_SendCmdStopMove(in Vector3 vDest, float fSpeed, int iMoveMode, byte byDir, ushort wStamp, int iTime) { - Debug.LogWarning("HoangDev : c2s_SendCmdStopMove"); + BMLogger.LogWarning("HoangDev : c2s_SendCmdStopMove"); Instance._gameSession.c2s_SendCmdStopMove(vDest, fSpeed, iMoveMode, byDir, wStamp, iTime); } public void c2s_CmdPlayerMove(in Vector3 vCurPos, in Vector3 vDest, @@ -376,7 +376,7 @@ namespace BrewMonster.Network public static void EnterWorldAsync(RoleInfo roleInfo, Action callback = null) { - Debug.Log("EnterWorldAsync !!!!! nay "); + BMLogger.Log("EnterWorldAsync !!!!! nay "); Instance._gameSession.EnterWorldAsync(roleInfo, callback); } public static void SendChatData(byte cChannel, in string szMsg, int iPack, int iSlot) diff --git a/Assets/PerfectWorld/Scripts/World/AddressableObject.cs b/Assets/PerfectWorld/Scripts/World/AddressableObject.cs index 0f81429001..065876d8c5 100644 --- a/Assets/PerfectWorld/Scripts/World/AddressableObject.cs +++ b/Assets/PerfectWorld/Scripts/World/AddressableObject.cs @@ -12,10 +12,17 @@ public class AddressableObject : MonoBehaviour private GameObject _instance; private bool _isLoading; + private bool _isLoaded; private int _loadRequestId; - public bool IsLoaded => _instance != null; + public bool IsLoaded => _isLoaded; public bool IsLoading => _isLoading; + public Vector3 ObjectPosition; + + void Awake() + { + ObjectPosition = transform.position; + } public async UniTask LoadAsset() { @@ -58,6 +65,7 @@ public class AddressableObject : MonoBehaviour modelTransform.localRotation = Quaternion.identity; modelTransform.localScale = Vector3.one; _instance.SetActive(true); + _isLoaded = true; } } @@ -71,6 +79,7 @@ public class AddressableObject : MonoBehaviour { Destroy(_instance); _instance = null; + _isLoaded = false; } else if (transform.childCount > 0) { diff --git a/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs b/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs index 2c502bc4c2..1df775ec4e 100644 --- a/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs +++ b/Assets/PerfectWorld/Scripts/World/LitModelHolder.cs @@ -21,20 +21,21 @@ public class LitModelHolder : MonoSingleton // how long a candidate object need to be in the immediate loading range to be loaded. [SerializeField] private float _candidateWaitTimeSeconds = 1f; [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; // 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 _candidatesForLoading = new Dictionary(); + private HashSet _loadedObjects = new HashSet(); private List _objectsToUnload = new List(); private Vector3 _lastHostPos; private bool _hasLastHostPos = false; + private Vector3 _currentHostPos; + 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 async void Awake() { @@ -63,10 +64,25 @@ public class LitModelHolder : MonoSingleton await LoadObjectBaseOnDistance(destroyToken); // Start distance-based streaming loop. This will be running continuously in the background. - StreamByDistanceLoop(destroyToken).Forget(); + UniTask.RunOnThreadPool(() => + { + StreamByDistanceLoop(destroyToken : destroyToken); + } + ).Forget(); } + private void Update() + { + _realTimeSinceStartUp = Time.realtimeSinceStartup; + UpdateHostPlayerPosition(); + if (_needToProcessLoadAndUnload) + { + ProcessLoadAndUnloadObjects(); + _needToProcessLoadAndUnload = false; + } + } + /// /// Immediately load the objects that are close to the host player.
/// Run this once at the beginning of the game. So we can load all the objects that are close to the host player. @@ -131,6 +147,12 @@ public class LitModelHolder : MonoSingleton } } + + /// + /// This function will be running continuously in the background. + /// + /// + /// private async UniTask StreamByDistanceLoop(CancellationToken destroyToken) { if (_paddingDistance < _loadImmediateDistance) @@ -146,18 +168,14 @@ public class LitModelHolder : MonoSingleton while (!destroyToken.IsCancellationRequested) { - if (_hostPlayer == null) + if (!_hostPosReady) { - _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) + if ((_currentHostPos - _lastHostPos).sqrMagnitude < minMoveSqr) { await UniTask.Delay(intervalMs, cancellationToken: destroyToken); continue; @@ -165,14 +183,20 @@ public class LitModelHolder : MonoSingleton } _hasLastHostPos = true; - _lastHostPos = hostPos; + _lastHostPos = _currentHostPos; - await TickStreaming(hostPos, immediateSqr, paddingSqr, destroyToken); + await TickStreaming(_currentHostPos, immediateSqr, paddingSqr, destroyToken); await UniTask.Delay(intervalMs, cancellationToken: destroyToken); } } + + /// + /// 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. + /// + /// private async UniTask TickStreaming(Vector3 hostPos, float immediateSqr, float paddingSqr, CancellationToken destroyToken) { if (addressableObjects == null || addressableObjects.Length == 0) @@ -181,42 +205,27 @@ public class LitModelHolder : MonoSingleton } 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++) + for (int i = 0; i < count; i++) { - if (_scanIndex >= count) - { - _scanIndex = 0; - } - - _currentObjectToCheck = addressableObjects[_scanIndex]; - _scanIndex++; - + _currentObjectToCheck = addressableObjects[i]; if (_currentObjectToCheck == null) { continue; } - float distSqr = (_currentObjectToCheck.transform.position - hostPos).sqrMagnitude; + if (_loadedObjects.Contains(_currentObjectToCheck)) + { + continue; + } + + float distSqr = (_currentObjectToCheck.ObjectPosition - hostPos).sqrMagnitude; if (distSqr <= immediateSqr) { - // if the object is in the immediate loading range. - // we only load it if it has been in the range for at least _candidateWaitTimeSeconds. - if (_candidatesForLoading.TryGetValue(_currentObjectToCheck, out float enterTimestamp)) + if (!_candidatesForLoading.ContainsKey(_currentObjectToCheck)) { - if (!_currentObjectToCheck.IsLoaded && !_currentObjectToCheck.IsLoading && Time.realtimeSinceStartup - enterTimestamp > _candidateWaitTimeSeconds) - { - _currentObjectToCheck.LoadAsset().Forget(); - } - } - else - { - _candidatesForLoading[_currentObjectToCheck] = Time.realtimeSinceStartup; + _candidatesForLoading[_currentObjectToCheck] = _realTimeSinceStartUp; } } else if (distSqr > paddingSqr) @@ -225,18 +234,36 @@ public class LitModelHolder : MonoSingleton { _objectsToUnload.Add(_currentObjectToCheck); } - - if (_candidatesForLoading.TryGetValue(_currentObjectToCheck, out float enterTimestamp)) - { - _candidatesForLoading.Remove(_currentObjectToCheck); - } } } - // unload the objects that are too far away. - foreach (var obj in _objectsToUnload) + _needToProcessLoadAndUnload = true; + } + + private void ProcessLoadAndUnloadObjects() + { + // load the objects that are in the loading range long enough. (_candidateWaitTimeSeconds) + foreach (var kvp in _candidatesForLoading) { - obj.UnloadAsset(); + if (kvp.Value < _realTimeSinceStartUp - _candidateWaitTimeSeconds) + { + kvp.Key.LoadAsset().Forget(); + _loadedObjects.Add(kvp.Key); + } + } + // remove the loaded objects from the candidates for loading list. + foreach (var obj in _loadedObjects) + { + _candidatesForLoading.Remove(obj); + } + + // 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]); } } @@ -248,6 +275,16 @@ public class LitModelHolder : MonoSingleton { return CECGameRun.Instance.GetHostPlayer(); } + + private void UpdateHostPlayerPosition() + { + _hostPlayer = GetHostPlayer(); + if (_hostPlayer != null) + { + _currentHostPos = _hostPlayer.transform.position; + _hostPosReady = true; + } + } private void OnDestroy() { diff --git a/Assets/Scenes/a61.unity b/Assets/Scenes/a61.unity index 51bea4afc9..4d59fe0f74 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:c0f5a4d24a6f92366b7e5407e1e5dbdc0b52152071706efcfdcc36c6bd27ed9e -size 196457299 +oid sha256:4b0c11cfa72955c70b2047f94ab8c8b65419e3e268629154ead0c35d3f5b979c +size 196550502