Files
DTrierFlood_New/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs

720 lines
24 KiB
C#

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.Rendering;
public class GeoTileAddressablesLoader : MonoBehaviour
{
[Header("References")]
[SerializeField] private Transform player;
[SerializeField] private Transform tilesParent;
[Header("Paths")]
[SerializeField] private string tileBundleFolderName = "TileBundles";
[SerializeField] private string manifestFileName = "TileManifest.json";
[SerializeField] private string buildingManifestFileName = "TileBuildingsManifest.json";
[SerializeField] private string buildTargetFolderOverride = "";
[Header("Streaming")]
[SerializeField] private float loadRadiusMeters = 1500f;
[SerializeField] private float unloadRadiusMeters = 2000f;
[SerializeField] private float updateInterval = 0.5f;
[SerializeField] private int maxConcurrentLoads = 2;
[SerializeField] private bool verboseLogging = false;
[SerializeField] private bool useEmbeddedBuildingsInTilePrefabs = true;
[Header("Buildings (2km blocks)")]
[SerializeField] private bool loadBuildingsFor2kmBlocks = true;
[SerializeField] private int buildingBlockSizeInTiles = 2;
private TileManifest manifest;
private TileManifest buildingManifest;
private readonly Dictionary<string, TileEntry> tiles = new Dictionary<string, TileEntry>();
private readonly Dictionary<string, GameObject> loaded = new Dictionary<string, GameObject>();
private readonly HashSet<string> loading = new HashSet<string>();
private readonly HashSet<string> queued = new HashSet<string>();
private readonly Queue<string> loadQueue = new Queue<string>();
private readonly Dictionary<string, TileEntry> buildingTiles = new Dictionary<string, TileEntry>();
private readonly Dictionary<string, GameObject> buildingLoaded = new Dictionary<string, GameObject>();
private readonly HashSet<string> buildingLoading = new HashSet<string>();
private readonly HashSet<string> buildingQueued = new HashSet<string>();
private readonly Queue<string> buildingLoadQueue = new Queue<string>();
private readonly Dictionary<string, string> tileBlockKey = new Dictionary<string, string>();
private readonly Dictionary<string, string> blockBuildingKey = new Dictionary<string, string>();
private bool initialized;
private float nextUpdateTime;
private void OnValidate()
{
if (unloadRadiusMeters < loadRadiusMeters)
unloadRadiusMeters = loadRadiusMeters;
if (maxConcurrentLoads < 1)
maxConcurrentLoads = 1;
if (updateInterval < 0.05f)
updateInterval = 0.05f;
if (buildingBlockSizeInTiles < 1)
buildingBlockSizeInTiles = 1;
}
private void Awake()
{
if (tilesParent == null)
{
var parent = new GameObject("Geo_Tile_Addressables");
tilesParent = parent.transform;
}
}
private void Start()
{
if (player == null && Camera.main != null)
player = Camera.main.transform;
StartCoroutine(Initialize());
}
private IEnumerator Initialize()
{
var buildTargetFolder = string.IsNullOrWhiteSpace(buildTargetFolderOverride)
? GetBuildTargetFolderName()
: buildTargetFolderOverride;
var basePath = Path.Combine(Application.persistentDataPath, tileBundleFolderName, buildTargetFolder);
var manifestPath = Path.Combine(basePath, manifestFileName);
Debug.Log(
$"[GeoTileAddressablesLoader] Startup: platform={Application.platform}, " +
$"persistentDataPath={Application.persistentDataPath}, basePath={basePath}, manifestPath={manifestPath}");
LogRuntimeRenderState();
Log($"Initializing loader. BasePath={basePath}");
Log($"ManifestPath={manifestPath}");
if (!File.Exists(manifestPath))
{
Debug.LogError($"[GeoTileAddressablesLoader] Manifest not found: {manifestPath}");
yield break;
}
manifest = JsonUtility.FromJson<TileManifest>(File.ReadAllText(manifestPath));
if (manifest == null || manifest.tiles == null || manifest.tiles.Length == 0)
{
Debug.LogError("[GeoTileAddressablesLoader] Manifest is empty or invalid.");
yield break;
}
tiles.Clear();
foreach (var tile in manifest.tiles)
{
var key = !string.IsNullOrWhiteSpace(tile.tileKey) ? tile.tileKey : tile.tileId;
if (!string.IsNullOrWhiteSpace(key))
tiles[key] = tile;
}
LoadBuildingManifest(basePath);
BuildBuildingBlocks();
Debug.Log(
$"[GeoTileAddressablesLoader] Manifest loaded: path={manifestPath}, buildTarget={manifest.buildTarget}, " +
$"catalogFile={manifest.catalogFile}, catalogHashFile={manifest.catalogHashFile}, tileCount={tiles.Count}");
Log($"Manifest loaded. Tiles={tiles.Count} CatalogFile={manifest.catalogFile}");
var catalogPath = Path.Combine(basePath, manifest.catalogFile);
if (!File.Exists(catalogPath))
{
Debug.LogError($"[GeoTileAddressablesLoader] Catalog not found: {catalogPath}");
yield break;
}
var catalogUri = ToFileUri(catalogPath);
Debug.Log($"[GeoTileAddressablesLoader] Loading catalog: path={catalogPath}, uri={catalogUri}");
Log($"Loading catalog: {catalogPath}");
var handle = Addressables.LoadContentCatalogAsync(catalogUri, true);
yield return handle;
if (handle.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[GeoTileAddressablesLoader] Catalog load failed: {handle.OperationException}");
yield break;
}
Debug.Log($"[GeoTileAddressablesLoader] Catalog loaded successfully: {manifest.catalogFile}");
Log("Catalog loaded successfully.");
// Sanity-check key resolution against the loaded catalog so logcat immediately
// shows whether the runtime catalog actually contains our tile keys.
yield return StartCoroutine(LogCatalogKeyProbe());
initialized = true;
nextUpdateTime = Time.time;
}
private IEnumerator LogCatalogKeyProbe()
{
int probeCount = 0;
foreach (var kvp in tiles)
{
var key = kvp.Key;
var locationsHandle = Addressables.LoadResourceLocationsAsync(key);
yield return locationsHandle;
int locationCount = 0;
if (locationsHandle.Status == AsyncOperationStatus.Succeeded && locationsHandle.Result != null)
locationCount = locationsHandle.Result.Count;
Debug.Log($"[GeoTileAddressablesLoader] KeyProbe tile key={key} locations={locationCount}");
Addressables.Release(locationsHandle);
probeCount++;
if (probeCount >= 5)
break;
}
if (loadBuildingsFor2kmBlocks && buildingTiles.Count > 0)
{
int buildingProbeCount = 0;
foreach (var kvp in buildingTiles)
{
var key = kvp.Key;
var locationsHandle = Addressables.LoadResourceLocationsAsync(key);
yield return locationsHandle;
int locationCount = 0;
if (locationsHandle.Status == AsyncOperationStatus.Succeeded && locationsHandle.Result != null)
locationCount = locationsHandle.Result.Count;
Debug.Log($"[GeoTileAddressablesLoader] KeyProbe building key={key} locations={locationCount}");
Addressables.Release(locationsHandle);
buildingProbeCount++;
if (buildingProbeCount >= 5)
break;
}
}
}
private void LoadBuildingManifest(string basePath)
{
buildingManifest = null;
buildingTiles.Clear();
if (!loadBuildingsFor2kmBlocks || string.IsNullOrWhiteSpace(buildingManifestFileName))
return;
var path = Path.Combine(basePath, buildingManifestFileName);
Debug.Log($"[GeoTileAddressablesLoader] Building manifest path: {path}");
if (!File.Exists(path))
{
Log($"Building manifest not found: {path}");
return;
}
buildingManifest = JsonUtility.FromJson<TileManifest>(File.ReadAllText(path));
if (buildingManifest == null || buildingManifest.tiles == null || buildingManifest.tiles.Length == 0)
{
Log("Building manifest is empty or invalid.");
return;
}
foreach (var tile in buildingManifest.tiles)
{
var key = !string.IsNullOrWhiteSpace(tile.tileKey) ? tile.tileKey : tile.tileId;
if (!string.IsNullOrWhiteSpace(key))
buildingTiles[key] = tile;
}
Debug.Log(
$"[GeoTileAddressablesLoader] Building manifest loaded: path={path}, buildTarget={buildingManifest.buildTarget}, " +
$"catalogFile={buildingManifest.catalogFile}, tileCount={buildingTiles.Count}");
Log($"Building manifest loaded. BuildingTiles={buildingTiles.Count}");
}
private void Update()
{
if (!initialized || player == null)
return;
if (Time.time < nextUpdateTime)
return;
nextUpdateTime = Time.time + updateInterval;
UpdateTileSet();
ProcessQueue();
}
private void UpdateTileSet()
{
var playerPos = player.position;
var tileSize = manifest.tileSizeMeters;
var wantedTiles = new HashSet<string>();
HashSet<string> wantedBlocks = (loadBuildingsFor2kmBlocks && buildingTiles.Count > 0)
? new HashSet<string>()
: null;
foreach (var kvp in tiles)
{
var tileKey = kvp.Key;
var tile = kvp.Value;
var tileCenter = new Vector3(
tile.offsetX + tileSize * 0.5f,
tile.baseY,
tile.offsetZ + tileSize * 0.5f);
var distance = Vector2.Distance(
new Vector2(playerPos.x, playerPos.z),
new Vector2(tileCenter.x, tileCenter.z));
bool withinLoad = distance <= loadRadiusMeters;
bool withinKeep = distance <= unloadRadiusMeters;
if (withinLoad || (withinKeep && loaded.ContainsKey(tileKey)))
wantedTiles.Add(tileKey);
if (wantedBlocks != null && tileBlockKey.TryGetValue(tileKey, out var blockKey))
{
if (withinLoad)
wantedBlocks.Add(blockKey);
else if (withinKeep && blockBuildingKey.TryGetValue(blockKey, out var buildingKey) && buildingLoaded.ContainsKey(buildingKey))
wantedBlocks.Add(blockKey);
}
}
foreach (var tileKey in wantedTiles)
EnqueueTileLoad(tileKey);
if (loaded.Count == 0)
{
if (wantedBlocks == null)
return;
}
var toUnload = new List<string>();
foreach (var kvp in loaded)
{
if (!wantedTiles.Contains(kvp.Key))
toUnload.Add(kvp.Key);
}
for (int i = 0; i < toUnload.Count; i++)
UnloadTile(toUnload[i]);
if (wantedBlocks == null)
return;
var wantedBuildingKeys = new HashSet<string>();
foreach (var blockKey in wantedBlocks)
{
if (blockBuildingKey.TryGetValue(blockKey, out var buildingKey))
wantedBuildingKeys.Add(buildingKey);
}
foreach (var buildingKey in wantedBuildingKeys)
EnqueueBuildingLoad(buildingKey);
if (buildingLoaded.Count == 0)
return;
var toUnloadBuildings = new List<string>();
foreach (var kvp in buildingLoaded)
{
if (!wantedBuildingKeys.Contains(kvp.Key))
toUnloadBuildings.Add(kvp.Key);
}
for (int i = 0; i < toUnloadBuildings.Count; i++)
UnloadBuilding(toUnloadBuildings[i]);
}
private void EnqueueTileLoad(string tileKey)
{
if (loaded.ContainsKey(tileKey) || loading.Contains(tileKey) || queued.Contains(tileKey))
return;
loadQueue.Enqueue(tileKey);
queued.Add(tileKey);
}
private void EnqueueBuildingLoad(string buildingKey)
{
if (buildingLoaded.ContainsKey(buildingKey) || buildingLoading.Contains(buildingKey) || buildingQueued.Contains(buildingKey))
return;
buildingLoadQueue.Enqueue(buildingKey);
buildingQueued.Add(buildingKey);
}
private void ProcessQueue()
{
bool preferTiles = true;
while (loadQueue.Count > 0 || buildingLoadQueue.Count > 0)
{
int inFlight = loading.Count + buildingLoading.Count;
if (inFlight >= maxConcurrentLoads)
return;
if ((preferTiles && loadQueue.Count > 0) || buildingLoadQueue.Count == 0)
{
var tileKey = loadQueue.Dequeue();
queued.Remove(tileKey);
StartLoad(tileKey);
}
else
{
var buildingKey = buildingLoadQueue.Dequeue();
buildingQueued.Remove(buildingKey);
StartBuildingLoad(buildingKey);
}
preferTiles = !preferTiles;
}
}
private void StartLoad(string tileKey)
{
if (!tiles.TryGetValue(tileKey, out var tile))
return;
loading.Add(tileKey);
Log($"Loading tile {tileKey}...");
var handle = Addressables.InstantiateAsync(tileKey, tilesParent);
handle.Completed += op =>
{
loading.Remove(tileKey);
if (op.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[GeoTileAddressablesLoader] Load failed for {tileKey}: {op.OperationException}");
return;
}
var instance = op.Result;
instance.name = tileKey;
instance.transform.position = new Vector3(tile.offsetX, tile.baseY, tile.offsetZ);
if (!useEmbeddedBuildingsInTilePrefabs)
RemoveEmbeddedBuildings(instance, tileKey);
LogTerrainState(instance, tileKey);
loaded[tileKey] = instance;
Log($"Loaded tile {tileKey}. LoadedCount={loaded.Count}");
};
}
private void StartBuildingLoad(string buildingKey)
{
if (!buildingTiles.TryGetValue(buildingKey, out var tile))
return;
buildingLoading.Add(buildingKey);
Log($"Loading buildings {buildingKey}...");
var handle = Addressables.InstantiateAsync(buildingKey, tilesParent);
handle.Completed += op =>
{
buildingLoading.Remove(buildingKey);
if (op.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[GeoTileAddressablesLoader] Building load failed for {buildingKey}: {op.OperationException}");
return;
}
var instance = op.Result;
instance.name = buildingKey;
instance.transform.position = new Vector3(tile.offsetX, tile.baseY, tile.offsetZ);
buildingLoaded[buildingKey] = instance;
Log($"Loaded buildings {buildingKey}. LoadedCount={buildingLoaded.Count}");
};
}
private void UnloadTile(string tileKey)
{
if (!loaded.TryGetValue(tileKey, out var instance))
return;
Addressables.ReleaseInstance(instance);
loaded.Remove(tileKey);
Log($"Unloaded tile {tileKey}. LoadedCount={loaded.Count}");
}
private void UnloadBuilding(string buildingKey)
{
if (!buildingLoaded.TryGetValue(buildingKey, out var instance))
return;
Addressables.ReleaseInstance(instance);
buildingLoaded.Remove(buildingKey);
Log($"Unloaded buildings {buildingKey}. LoadedCount={buildingLoaded.Count}");
}
private static void LogTerrainState(GameObject instance, string tileKey)
{
if (instance == null)
return;
var terrain = instance.GetComponent<Terrain>();
if (terrain == null)
{
int rendererCount = instance.GetComponentsInChildren<Renderer>(true).Length;
Debug.LogWarning(
$"[GeoTileAddressablesLoader] Tile {tileKey}: no Terrain component on instance, " +
$"rendererCount={rendererCount}, rootPos={FormatVec3(instance.transform.position)}");
return;
}
var td = terrain.terrainData;
if (td == null)
{
Debug.LogWarning($"[GeoTileAddressablesLoader] Tile {tileKey}: TerrainData is NULL.");
return;
}
var layers = td.terrainLayers;
int layerCount = layers != null ? layers.Length : 0;
bool hasDiffuse = false;
string diffuseName = "<none>";
if (layerCount > 0 && layers[0] != null && layers[0].diffuseTexture != null)
{
hasDiffuse = true;
diffuseName = layers[0].diffuseTexture.name;
}
string materialName = terrain.materialTemplate != null ? terrain.materialTemplate.name : "<none>";
string shaderName = terrain.materialTemplate != null && terrain.materialTemplate.shader != null
? terrain.materialTemplate.shader.name
: "<none>";
bool shaderSupported = terrain.materialTemplate != null &&
terrain.materialTemplate.shader != null &&
terrain.materialTemplate.shader.isSupported;
Bounds localBounds = td.bounds;
Vector3 worldCenter = terrain.GetPosition() + localBounds.center;
Vector3 extents = localBounds.extents;
Debug.Log(
$"[GeoTileAddressablesLoader] Tile {tileKey}: terrain ok, " +
$"enabled={terrain.enabled}, drawHeightmap={terrain.drawHeightmap}, " +
$"layers={layerCount}, hasDiffuse={hasDiffuse}, diffuse={diffuseName}, " +
$"material={materialName}, shader={shaderName}, shaderSupported={shaderSupported}, " +
$"worldCenter={FormatVec3(worldCenter)}, extents={FormatVec3(extents)}, " +
$"rootPos={FormatVec3(instance.transform.position)}");
}
private void RemoveEmbeddedBuildings(GameObject tileInstance, string tileKey)
{
if (tileInstance == null)
return;
int removed = 0;
var removedIds = new HashSet<int>();
// Current tile prefab convention: a direct child named "Buildings"
var direct = tileInstance.transform.Find("Buildings");
if (direct != null)
{
Destroy(direct.gameObject);
removed++;
removedIds.Add(direct.gameObject.GetInstanceID());
}
// Safety pass if any nested "Buildings" transforms exist
var all = tileInstance.GetComponentsInChildren<Transform>(true);
for (int i = 0; i < all.Length; i++)
{
var t = all[i];
if (t == null || t == tileInstance.transform)
continue;
if (!string.Equals(t.name, "Buildings", StringComparison.Ordinal))
continue;
if (removedIds.Contains(t.gameObject.GetInstanceID()))
continue;
Destroy(t.gameObject);
removed++;
removedIds.Add(t.gameObject.GetInstanceID());
}
if (removed > 0)
Log($"Removed {removed} embedded building root(s) from tile {tileKey}.");
}
private void BuildBuildingBlocks()
{
tileBlockKey.Clear();
blockBuildingKey.Clear();
if (!loadBuildingsFor2kmBlocks)
return;
int blockSize = Math.Max(1, buildingBlockSizeInTiles);
foreach (var kvp in tiles)
{
if (!TryGetTileXY(kvp.Value, out int x, out int y))
continue;
int blockX = x - (x % blockSize);
int blockY = y - (y % blockSize);
string blockKey = $"{blockX}_{blockY}";
tileBlockKey[kvp.Key] = blockKey;
}
if (buildingTiles.Count == 0)
return;
foreach (var kvp in buildingTiles)
{
if (!TryGetTileXY(kvp.Value, out int x, out int y))
continue;
int blockX = x - (x % blockSize);
int blockY = y - (y % blockSize);
string blockKey = $"{blockX}_{blockY}";
blockBuildingKey[blockKey] = kvp.Key;
}
}
private static bool TryGetTileXY(TileEntry tile, out int x, out int y)
{
if (!string.IsNullOrWhiteSpace(tile.tileKey) && TryParseTileKey(tile.tileKey, out x, out y))
return true;
return TryParseTileId(tile.tileId, out x, out y);
}
private static bool TryParseTileKey(string tileKey, out int x, out int y)
{
x = 0;
y = 0;
if (string.IsNullOrWhiteSpace(tileKey))
return false;
var parts = tileKey.Split('_');
if (parts.Length != 2)
return false;
return int.TryParse(parts[0], out x) && int.TryParse(parts[1], out y);
}
private static bool TryParseTileId(string tileId, out int x, out int y)
{
x = 0;
y = 0;
if (string.IsNullOrWhiteSpace(tileId))
return false;
var parts = tileId.Split('_');
var coords = new List<int>();
for (int i = 0; i < parts.Length; i++)
{
var part = parts[i];
if (part.Length < 3)
continue;
bool allDigits = true;
for (int j = 0; j < part.Length; j++)
{
if (!char.IsDigit(part[j]))
{
allDigits = false;
break;
}
}
if (!allDigits)
continue;
if (int.TryParse(part, out int value))
coords.Add(value);
}
if (coords.Count < 2)
return false;
x = coords[coords.Count - 2];
y = coords[coords.Count - 1];
return true;
}
private static string GetBuildTargetFolderName()
{
switch (Application.platform)
{
case RuntimePlatform.Android:
return "Android";
case RuntimePlatform.WindowsPlayer:
case RuntimePlatform.WindowsEditor:
return "StandaloneWindows64";
case RuntimePlatform.LinuxPlayer:
case RuntimePlatform.LinuxEditor:
return "StandaloneLinux64";
case RuntimePlatform.OSXPlayer:
case RuntimePlatform.OSXEditor:
return "StandaloneOSX";
default:
return "Android";
}
}
private static string ToFileUri(string path)
{
var normalized = path.Replace("\\", "/");
return normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase)
? normalized
: "file://" + normalized;
}
private void OnDestroy()
{
foreach (var instance in loaded.Values)
Addressables.ReleaseInstance(instance);
loaded.Clear();
loading.Clear();
queued.Clear();
loadQueue.Clear();
foreach (var instance in buildingLoaded.Values)
Addressables.ReleaseInstance(instance);
buildingLoaded.Clear();
buildingLoading.Clear();
buildingQueued.Clear();
buildingLoadQueue.Clear();
}
private void Log(string message)
{
if (verboseLogging)
Debug.Log($"[GeoTileAddressablesLoader] {message}");
}
private void LogRuntimeRenderState()
{
var rp = GraphicsSettings.currentRenderPipeline;
string rpName = rp != null ? $"{rp.name} ({rp.GetType().Name})" : "BuiltInRenderPipeline";
int qualityIndex = QualitySettings.GetQualityLevel();
string qualityName = qualityIndex >= 0 && qualityIndex < QualitySettings.names.Length
? QualitySettings.names[qualityIndex]
: "<unknown>";
string cameraName = Camera.main != null ? Camera.main.name : "<none>";
Debug.Log(
$"[GeoTileAddressablesLoader] RenderState: quality={qualityIndex}:{qualityName}, " +
$"renderPipeline={rpName}, mainCamera={cameraName}");
}
private static string FormatVec3(Vector3 v)
{
return $"({v.x:F2},{v.y:F2},{v.z:F2})";
}
}