using System; using System.Collections; using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.AsyncOperations; 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; [Header("Buildings (2km blocks)")] [SerializeField] private bool loadBuildingsFor2kmBlocks = true; [SerializeField] private int buildingBlockSizeInTiles = 2; private TileManifest manifest; private TileManifest buildingManifest; private readonly Dictionary tiles = new Dictionary(); private readonly Dictionary loaded = new Dictionary(); private readonly HashSet loading = new HashSet(); private readonly HashSet queued = new HashSet(); private readonly Queue loadQueue = new Queue(); private readonly Dictionary buildingTiles = new Dictionary(); private readonly Dictionary buildingLoaded = new Dictionary(); private readonly HashSet buildingLoading = new HashSet(); private readonly HashSet buildingQueued = new HashSet(); private readonly Queue buildingLoadQueue = new Queue(); private readonly Dictionary tileBlockKey = new Dictionary(); private readonly Dictionary blockBuildingKey = new Dictionary(); 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); Log($"Initializing loader. BasePath={basePath}"); Log($"ManifestPath={manifestPath}"); if (!File.Exists(manifestPath)) { Debug.LogError($"[GeoTileAddressablesLoader] Manifest not found: {manifestPath}"); yield break; } manifest = JsonUtility.FromJson(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(); 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; } Log($"Loading catalog: {catalogPath}"); var handle = Addressables.LoadContentCatalogAsync(ToFileUri(catalogPath), true); yield return handle; if (handle.Status != AsyncOperationStatus.Succeeded) { Debug.LogError($"[GeoTileAddressablesLoader] Catalog load failed: {handle.OperationException}"); yield break; } Log("Catalog loaded successfully."); initialized = true; nextUpdateTime = Time.time; } private void LoadBuildingManifest(string basePath) { buildingManifest = null; buildingTiles.Clear(); if (!loadBuildingsFor2kmBlocks || string.IsNullOrWhiteSpace(buildingManifestFileName)) return; var path = Path.Combine(basePath, buildingManifestFileName); if (!File.Exists(path)) { Log($"Building manifest not found: {path}"); return; } buildingManifest = JsonUtility.FromJson(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; } 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(); HashSet wantedBlocks = (loadBuildingsFor2kmBlocks && buildingTiles.Count > 0) ? new HashSet() : 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(); 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(); 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(); 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); 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 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(); 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}"); } }