using UnityEngine; using Cysharp.Threading.Tasks; using System.Threading; using BrewMonster.Network; using BrewMonster.Scripts; using System; using System.Collections.Generic; using System.IO; #if UNITY_EDITOR using UnityEditor; #endif namespace BrewMonster { public class TerrainHolder : MonoBehaviour { [SerializeField] private AddressableObject[] _addressableObjects; [SerializeField] private float _loadImmediateDistance; //150f; [SerializeField] private float _unloadDistance; //300f; [SerializeField] private float _minHostMoveToUpdate = 5f; private List _candidatesForLoading = new List(); private List _objectsToUnload = new List(); private CECHostPlayer _hostPlayer; private Vector3 _lastHostPosOxz; private bool _hasLastHostPos = false; 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; #region Unity Lifecycle private void Awake() { _cts = new CancellationTokenSource(); StartStreamingProcess(_cts.Token); } private void Update() { UpdateGlobalDataForStreaming(); if (_needToProcessLoadAndUnload) { ProcessLoadAndUnload(); _needToProcessLoadAndUnload = false; } } private void OnDisable() { // unload all the addressable objects. // foreach (var addressableObject in _addressableObjects) // { // if (addressableObject == null) continue; // addressableObject.UnloadAsset(); // } } private void OnDestroy() { // cancel the streaming process. if (_cts != null) { _cts.Cancel(); _cts.Dispose(); } } #endregion #region private functions // This function is expected to be called once at the beginning of a world (scene) private void StartStreamingProcess(CancellationToken destroyToken) { if (destroyToken.IsCancellationRequested) return; // Get the host player position from the UnityGameSession. This when user choose a role and enter a world (scene). _currentHostPosOxz = new Vector3(UnityGameSession.Instance.GetRoleInfo().posx, 0f, UnityGameSession.Instance.GetRoleInfo().posz); UniTask.RunOnThreadPool(async () => { await StreamByDistanceLoop(destroyToken); }).Forget(); } /// /// This is the main loop that keep checking the objects that are in the immediate loading range or padding range. /// If the host moved too little, we will skip the streaming. /// /// /// private async UniTask StreamByDistanceLoop(CancellationToken destroyToken) { if (_addressableObjects == null || _addressableObjects.Length == 0) return; //if (_unloadDistance < _loadImmediateDistance) // _unloadDistance = _loadImmediateDistance; float immediateSqr = _loadImmediateDistance * _loadImmediateDistance; float paddingSqr = _unloadDistance * _unloadDistance; float minMoveSqr = _minHostMoveToUpdate * _minHostMoveToUpdate; while (!destroyToken.IsCancellationRequested) { if (!_hostPosReady) { await UniTask.Delay(1000, cancellationToken: destroyToken); continue; } if (_hasLastHostPos) { if ((_currentHostPosOxz - _lastHostPosOxz).sqrMagnitude < minMoveSqr) { await UniTask.Delay(1000, cancellationToken: destroyToken); continue; } } _hasLastHostPos = true; _lastHostPosOxz = _currentHostPosOxz; _loadImmediateDistance = EC_Game.GetSettingViewDistance().fShow; _unloadDistance = EC_Game.GetSettingViewDistance().fHide; immediateSqr = _loadImmediateDistance * _loadImmediateDistance; paddingSqr = _unloadDistance * _unloadDistance; TickStreaming(_currentHostPosOxz, immediateSqr, paddingSqr); } } private void TickStreaming(Vector3 targetPos, float immediateSqr, float paddingSqr) { if (_addressableObjects == null || _addressableObjects.Length == 0) return; int count = _addressableObjects.Length; lock (_objectsToUnload) { targetPos.y = 0f; // we only consider the Oxz plane. float distanceSqr = 0f; for (int i = 0; i < count; i++) { _currentObjectToCheck = _addressableObjects[i]; if (_currentObjectToCheck == null) continue; distanceSqr = (_currentObjectToCheck.ObjectPositionOxz - targetPos).sqrMagnitude; if (distanceSqr <= immediateSqr) { if (_currentObjectToCheck.IsLoaded || _currentObjectToCheck.IsLoading) { continue; } if (!_candidatesForLoading.Contains(_currentObjectToCheck)) { _candidatesForLoading.Add(_currentObjectToCheck); } } else if (distanceSqr > paddingSqr) { if (_currentObjectToCheck.IsLoaded || _currentObjectToCheck.IsLoading) { if (!_objectsToUnload.Contains(_currentObjectToCheck)) { _objectsToUnload.Add(_currentObjectToCheck); } } } } _needToProcessLoadAndUnload = true; //if (_hostPlayer != null) //{ // Debug.LogError("ProcessLoadAndUnload Terain _hostPlayer.isTerrainToReady = false"); // _hostPlayer.isTerrainToReady = false; //} } } /// /// process to call Load and Unload on each addressable object that we need to. /// private void ProcessLoadAndUnload() { _currentIdxAsset = 0; _maxIdxAsset = _candidatesForLoading.Count; for (int i = 0; i < _candidatesForLoading.Count; i++) { _candidatesForLoading[i].LoadAsset(CallBackAssetLoadingDone).Forget(); } _candidatesForLoading.Clear(); for (int i = 0; i < _objectsToUnload.Count; i++) { _objectsToUnload[i].UnloadAsset(); } _objectsToUnload.Clear(); } int _currentIdxAsset = 0; // The current counter for loaded assets. int _maxIdxAsset = 0; // Limit the number of assets that have finished loading. /// /// isLitToReady is a condition used by _hostPlayer to wait until the Terrain 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. /// private void CallBackAssetLoadingDone() { _currentIdxAsset++; if (_currentIdxAsset >= _maxIdxAsset) { if (_hostPlayer != null) { _hostPlayer.isTerrainToReady = true; } _currentIdxAsset = -1; _maxIdxAsset = 0; } } /// /// Main-thread anchor for terrain streaming: host position, or navigate clone when force-navigate is active (same idea as LitModelHolder). /// 地形流式锚点:普通用宿主;强制导航用导航克隆(与 LitModelHolder 一致)。 /// private void UpdateGlobalDataForStreaming() { _realTimeSinceStartUp = Time.realtimeSinceStartup; if (_hostPlayer == null) _hostPlayer = CECGameRun.Instance.GetHostPlayer(); if (_hostPlayer == null) { _hostPosReady = false; return; } //In c++ version. clone object position is used as the host object position when force navigate. if (_hostPlayer.IsInForceNavigateState()) { CECHostNavigatePlayer nav = _hostPlayer.GetNavigatePlayer(); if (nav != null && nav.TryGetNavigateModelPosition(out Vector3 clonePos)) { _currentHostPosOxz = clonePos; } } else { _currentHostPosOxz = _hostPlayer.GetPosVector3(false); } _currentHostPosOxz.y = 0f; _hostPosReady = true; } #endregion #if UNITY_EDITOR private int MASK_TEXTURE_HASH = Shader.PropertyToID("_MaskTexture"); private int MASK_TEXTURE2_HASH = Shader.PropertyToID("_MaskTexture2"); private const string _terrainPathPrefix = "Assets/ModelRenderer/Art/Terrain"; private const string _terrainPathPrefixNotAsset = "ModelRenderer/Art/Terrain"; [Space(10)] [Header("FOR EDITOR SETUP ONLY")] [SerializeField] private GameObject[] _originalObjects; [SerializeField] private string _worldName; [ContextMenu("Update Asset Paths")] private void UpdateAssetPaths() { for (int i = 0; i < _addressableObjects.Length; i++) { AddressableObject addressableObject = _addressableObjects[i]; if (addressableObject == null) { continue; } string assetPath = addressableObject.assetPath; assetPath = assetPath.Replace(".mesh", ".prefab"); assetPath = assetPath.Replace("Assets/ModelRenderer/Art/Terrain/", ""); addressableObject.assetPath = assetPath; } } [ContextMenu("Setup Addressable Objects")] private void SetupAddressableObjects() { if (_originalObjects == null || _originalObjects.Length == 0) { Debug.LogWarning("[TerrainHolder] _originalObjects is empty."); return; } if (string.IsNullOrWhiteSpace(_worldName)) { Debug.LogError("[TerrainHolder] _worldName is empty. Please set it before running setup."); return; } string worldFolder = $"{_terrainPathPrefix}/{_worldName}"; EnsureFolderExists(worldFolder); var createdAddressableObjects = new List(_originalObjects.Length); for (int i = 0; i < _originalObjects.Length; i++) { GameObject currentTerrainObject = _originalObjects[i]; if (currentTerrainObject == null) { continue; } Mesh mesh = GetFirstMesh(currentTerrainObject); Material material = currentTerrainObject.GetComponent().sharedMaterial; Texture2D maskTexture = material.GetTexture(MASK_TEXTURE_HASH) as Texture2D; Texture2D maskTexture2 = material.GetTexture(MASK_TEXTURE2_HASH) as Texture2D; if (mesh == null) { Debug.LogWarning($"[TerrainHolder] No Mesh found on '{currentTerrainObject.name}', skipping."); continue; } string safeName = MakeSafeFileName(currentTerrainObject.name); string meshAssetPath = AssetDatabase.GenerateUniqueAssetPath($"{worldFolder}/{safeName}.mesh"); string materialAssetPath = AssetDatabase.GenerateUniqueAssetPath($"{worldFolder}/{safeName}.mat"); string maskTextureAssetPath = AssetDatabase.GenerateUniqueAssetPath($"{worldFolder}/{safeName}_MaskTexture.png"); string maskTexture2AssetPath = AssetDatabase.GenerateUniqueAssetPath($"{worldFolder}/{safeName}_MaskTexture2.png"); string maskTextureAbsolutePath = Path.Combine(Application.dataPath, maskTextureAssetPath.Substring(7)); string maskTexture2AbsolutePath = Path.Combine(Application.dataPath, maskTexture2AssetPath.Substring(7)); AssetDatabase.CreateAsset(mesh, meshAssetPath); AssetDatabase.CreateAsset(material, materialAssetPath); AAssit.SaveTexture2DToPNG(maskTexture, maskTextureAbsolutePath); AAssit.SaveTexture2DToPNG(maskTexture2, maskTexture2AbsolutePath); AssetDatabase.Refresh(); // load mesh from asset var meshCopy = AssetDatabase.LoadAssetAtPath(meshAssetPath); var materialCopy = AssetDatabase.LoadAssetAtPath(materialAssetPath); var maskTextureCopy = AssetDatabase.LoadAssetAtPath(maskTextureAssetPath); var maskTexture2Copy = AssetDatabase.LoadAssetAtPath(maskTexture2AssetPath); if (maskTextureCopy != null) { materialCopy.SetTexture(MASK_TEXTURE_HASH, maskTextureCopy); } if (maskTexture2Copy != null) { materialCopy.SetTexture(MASK_TEXTURE2_HASH, maskTexture2Copy); } // set the mesh to the current terrain object currentTerrainObject.GetComponent().sharedMesh = meshCopy; currentTerrainObject.GetComponent().sharedMaterial = materialCopy; // Create the addressable anchor object with matching transform. var newGo = new GameObject(currentTerrainObject.name); Transform srcTr = currentTerrainObject.transform; Transform dstTr = newGo.transform; // dstTr.SetParent(srcTr.parent, true); dstTr.position = srcTr.position; dstTr.rotation = srcTr.rotation; dstTr.localScale = srcTr.localScale; var addressableObject = newGo.AddComponent(); addressableObject.assetPath = meshAssetPath; createdAddressableObjects.Add(addressableObject); } // AssetDatabase.SaveAssets(); // AssetDatabase.Refresh(); _addressableObjects = createdAddressableObjects.ToArray(); // EditorUtility.SetDirty(this); // if (gameObject.scene.IsValid()) // { // EditorSceneManager.MarkSceneDirty(gameObject.scene); // } } [MenuItem("Examples/Add BoxCollider to Prefab Asset")] static void AddBoxColliderToPrefab() { // Get the Prefab Asset root GameObject and its asset path. GameObject assetRoot = Selection.activeObject as GameObject; string assetPath = AssetDatabase.GetAssetPath(assetRoot); BMLogger.Log($"[TerrainHolder] AddBoxColliderToPrefab: {assetPath}"); // Load the contents of the Prefab Asset. GameObject contentsRoot = PrefabUtility.LoadPrefabContents(assetPath); PrefabUtility.UnloadPrefabContents(contentsRoot); } [ContextMenu("Load All Addressable Objects")] private async UniTask LoadAllAddressableObjects() { if (_addressableObjects == null || _addressableObjects.Length == 0) { Debug.LogWarning("[TerrainHolder] _addressableObjects is empty."); return; } int updatedPrefabCount = 0; for (int i = 0; i < _addressableObjects.Length; i++) { AddressableObject addressableObject = _addressableObjects[i]; if (addressableObject == null || string.IsNullOrWhiteSpace(addressableObject.assetPath)) { continue; } string prefabPath = $"{_terrainPathPrefix}/{addressableObject.assetPath}"; // prefabPath = Path.Combine(Application.dataPath, prefabPath); GameObject prefab = PrefabUtility.LoadPrefabContents(prefabPath); if (prefab == null) { Debug.LogWarning($"[TerrainHolder] Could not load prefab at '{prefabPath}'."); continue; } bool prefabUpdated = false; MeshFilter[] meshFilters = prefab.GetComponentsInChildren(true); for (int j = 0; j < meshFilters.Length; j++) { MeshFilter meshFilter = meshFilters[j]; if (meshFilter.sharedMesh == null) { continue; } MeshCollider meshCollider = meshFilter.GetComponent(); if (meshCollider == null) { meshCollider = meshFilter.gameObject.AddComponent(); prefabUpdated = true; } if (meshCollider.sharedMesh != meshFilter.sharedMesh) { meshCollider.sharedMesh = meshFilter.sharedMesh; prefabUpdated = true; } } if (prefabUpdated) { // EditorUtility.SetDirty(prefab); PrefabUtility.SaveAsPrefabAsset(prefab, prefabPath); PrefabUtility.UnloadPrefabContents(prefab); updatedPrefabCount++; } // show progress dialog EditorUtility.DisplayProgressBar("Loading Addressable Objects", $"Loading {i} / {_addressableObjects.Length}", (float)i / _addressableObjects.Length); await UniTask.Delay(1); } EditorUtility.ClearProgressBar(); // AssetDatabase.SaveAssets(); Debug.Log($"[TerrainHolder] Added/updated mesh colliders on {updatedPrefabCount} prefab(s)."); } [ContextMenu("Unload All Addressable Objects")] private void UnloadAllAddressableObjects() { for (int i = 0; i < _addressableObjects.Length; i++) { _addressableObjects[i].UnloadAsset(); } } [ContextMenu("Update Addressable Objects Position Mesh Base")] private void UpdateAddressableObjectsPositionMeshBase() { Mesh mesh; for (int i = 0; i < _addressableObjects.Length; i++) { mesh = _originalObjects[i].GetComponentInChildren().sharedMesh; if (mesh == null) continue; Vector3[] vertices = mesh.vertices; // calculate the center of the mesh Vector3 center = Vector3.zero; for (int j = 0; j < vertices.Length; j++) { center += vertices[j]; } center /= vertices.Length; _addressableObjects[i].ObjectPosition = center; _addressableObjects[i].ObjectPositionOxz = new Vector3(center.x, 0, center.z); _addressableObjects[i].NeedToUpdatePositionAtStartUp = false; } } [ContextMenu("Update Get all Original Object from children")] private void UpdateGetAllOriginalObjectFromChildren() { _originalObjects = new GameObject[transform.childCount]; for (int i = 0; i < transform.childCount; i++) { GameObject child = transform.GetChild(i).gameObject; _originalObjects[i] = child; } } private static Mesh GetFirstMesh(GameObject go) { if (go == null) return null; var mf = go.GetComponent(); if (mf != null && mf.sharedMesh != null) { return mf.sharedMesh; } var smr = go.GetComponent(); if (smr != null && smr.sharedMesh != null) { return smr.sharedMesh; } return null; } private static void EnsureFolderExists(string folderPath) { if (AssetDatabase.IsValidFolder(folderPath)) { return; } string[] parts = folderPath.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0 || parts[0] != "Assets") { throw new ArgumentException($"Folder path must start with 'Assets': {folderPath}"); } string current = "Assets"; for (int i = 1; i < parts.Length; i++) { string next = $"{current}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(next)) { AssetDatabase.CreateFolder(current, parts[i]); } current = next; } } private static string MakeSafeFileName(string name) { if (string.IsNullOrEmpty(name)) return "Unnamed"; char[] invalid = Path.GetInvalidFileNameChars(); foreach (char c in invalid) { name = name.Replace(c, '_'); } return name.Trim(); } #endif } }