Files
2026-05-19 16:33:47 +07:00

585 lines
23 KiB
C#

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<AddressableObject> _candidatesForLoading = new List<AddressableObject>();
private List<AddressableObject> _objectsToUnload = new List<AddressableObject>();
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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="destroyToken"></param>
/// <returns></returns>
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;
//}
}
}
/// <summary>
/// process to call Load and Unload on each addressable object that we need to.
/// </summary>
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.
/// <summary>
/// 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.
/// </summary>
private void CallBackAssetLoadingDone()
{
_currentIdxAsset++;
if (_currentIdxAsset >= _maxIdxAsset)
{
if (_hostPlayer != null)
{
_hostPlayer.isTerrainToReady = true;
}
_currentIdxAsset = -1;
_maxIdxAsset = 0;
}
}
/// <summary>
/// Main-thread anchor for terrain streaming: host position, or navigate clone when force-navigate is active (same idea as LitModelHolder).
/// 地形流式锚点:普通用宿主;强制导航用导航克隆(与 LitModelHolder 一致)。
/// </summary>
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<AddressableObject>(_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<Renderer>().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<Mesh>(meshAssetPath);
var materialCopy = AssetDatabase.LoadAssetAtPath<Material>(materialAssetPath);
var maskTextureCopy = AssetDatabase.LoadAssetAtPath<Texture2D>(maskTextureAssetPath);
var maskTexture2Copy = AssetDatabase.LoadAssetAtPath<Texture2D>(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<MeshFilter>().sharedMesh = meshCopy;
currentTerrainObject.GetComponent<Renderer>().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>();
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<MeshFilter>(true);
for (int j = 0; j < meshFilters.Length; j++)
{
MeshFilter meshFilter = meshFilters[j];
if (meshFilter.sharedMesh == null)
{
continue;
}
MeshCollider meshCollider = meshFilter.GetComponent<MeshCollider>();
if (meshCollider == null)
{
meshCollider = meshFilter.gameObject.AddComponent<MeshCollider>();
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<MeshFilter>().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<MeshFilter>();
if (mf != null && mf.sharedMesh != null)
{
return mf.sharedMesh;
}
var smr = go.GetComponent<SkinnedMeshRenderer>();
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
}
}