Files
DTrierFlood_Linux/Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs
s0wlz (Matthias Puchstein) bd1e6f4f4d set up addressable geo tile streaming for quest
- add addressables settings/groups for tile prefabs with custom TileBuildPath/TileLoadPath profiles and link.xml preservation
- add editor tools for building tile addressables, configuring openxr quest loaders, removing missing scripts, and forcing android tool paths
- add runtime loader + manifest model to stream tile bundles from persistent data with radius-based load/unload
- add TestArea1 scene wired to GeoTileAddressablesLoader and update build settings to enable it
- update geo tile prefab importer output path to Assets/TilePrefabs
- update project/xr/android settings: min sdk 34, app id, openxr composition layers + quest devices, scripting define symbols, and renderer tweaks
- update packages (addressables 2.8, ar foundation 6.3.2, composition layers 2.3, collab proxy 2.11.2) and record scriptable build pipeline config
- remove temporary recovery scene files and add Notes plan/progress docs
2026-01-22 01:12:59 +01:00

262 lines
7.9 KiB
C#

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 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;
private TileManifest manifest;
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 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;
}
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<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)
{
if (!string.IsNullOrWhiteSpace(tile.tileId))
tiles[tile.tileId] = tile;
}
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 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;
foreach (var kvp in tiles)
{
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));
if (distance <= loadRadiusMeters)
{
EnqueueTileLoad(tile.tileId);
}
else if (distance >= unloadRadiusMeters)
{
UnloadTile(tile.tileId);
}
}
}
private void EnqueueTileLoad(string tileId)
{
if (loaded.ContainsKey(tileId) || loading.Contains(tileId) || queued.Contains(tileId))
return;
loadQueue.Enqueue(tileId);
queued.Add(tileId);
}
private void ProcessQueue()
{
while (loading.Count < maxConcurrentLoads && loadQueue.Count > 0)
{
var tileId = loadQueue.Dequeue();
queued.Remove(tileId);
StartLoad(tileId);
}
}
private void StartLoad(string tileId)
{
if (!tiles.TryGetValue(tileId, out var tile))
return;
loading.Add(tileId);
Log($"Loading tile {tileId}...");
var handle = Addressables.InstantiateAsync(tileId, tilesParent);
handle.Completed += op =>
{
loading.Remove(tileId);
if (op.Status != AsyncOperationStatus.Succeeded)
{
Debug.LogError($"[GeoTileAddressablesLoader] Load failed for {tileId}: {op.OperationException}");
return;
}
var instance = op.Result;
instance.name = tileId;
instance.transform.position = new Vector3(tile.offsetX, tile.baseY, tile.offsetZ);
loaded[tileId] = instance;
Log($"Loaded tile {tileId}. LoadedCount={loaded.Count}");
};
}
private void UnloadTile(string tileId)
{
if (!loaded.TryGetValue(tileId, out var instance))
return;
Addressables.ReleaseInstance(instance);
loaded.Remove(tileId);
Log($"Unloaded tile {tileId}. LoadedCount={loaded.Count}");
}
private static string GetBuildTargetFolderName()
{
switch (Application.platform)
{
case RuntimePlatform.Android:
return "Android";
case RuntimePlatform.WindowsPlayer:
case RuntimePlatform.WindowsEditor:
return "Windows";
case RuntimePlatform.LinuxPlayer:
case RuntimePlatform.LinuxEditor:
return "Linux";
case RuntimePlatform.OSXPlayer:
case RuntimePlatform.OSXEditor:
return "OSX";
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();
}
private void Log(string message)
{
if (verboseLogging)
Debug.Log($"[GeoTileAddressablesLoader] {message}");
}
}