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
This commit is contained in:
261
Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs
Normal file
261
Assets/Scripts/GeoDataUtils/GeoTileAddressablesLoader.cs
Normal file
@@ -0,0 +1,261 @@
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 14a2150bfb293bd30a7128844a9755da
|
||||
20
Assets/Scripts/GeoDataUtils/TileManifest.cs
Normal file
20
Assets/Scripts/GeoDataUtils/TileManifest.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
[Serializable]
|
||||
public class TileManifest
|
||||
{
|
||||
public string buildTarget;
|
||||
public string catalogFile;
|
||||
public string catalogHashFile;
|
||||
public float tileSizeMeters = 1000f;
|
||||
public TileEntry[] tiles;
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class TileEntry
|
||||
{
|
||||
public string tileId;
|
||||
public float offsetX;
|
||||
public float offsetZ;
|
||||
public float baseY;
|
||||
}
|
||||
2
Assets/Scripts/GeoDataUtils/TileManifest.cs.meta
Normal file
2
Assets/Scripts/GeoDataUtils/TileManifest.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d6d4f64953318e53fbf3d8d6333f31b6
|
||||
Reference in New Issue
Block a user