Hello everyone, it’s world streamer, it should create objects in the stream zone and delete them outside the stream zone, but assets doesn’t unload from RAM
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.ResourceManagement.ResourceLocations;
public class WorldStreamer : MonoBehaviour
{
[SerializeField] private Transform _target;
[SerializeField] public List<Chunk> _chunks = new();
[SerializeField] private int _chunkSize = 256;
[SerializeField] private bool _drawChunkGizmos;
[SerializeField] private AssetLabelReference _mapLabel;
private readonly ConcurrentBag<Chunk.ChunkObject> _objectsToInstance = new();
private readonly ConcurrentBag<InstancedObject> _objectsToDestroy = new();
private readonly Dictionary<string, InstancedObject> _instancedObjects = new();
private Vector3 _position;
private Transform _mapParent;
private int _loadDistance = 500;
private readonly System.Threading.CancellationTokenSource _cancelAsyncTaskWhenExitPlayMode = new();
private void Start()
{
_mapParent = new GameObject(_mapLabel.labelString).transform;
_ = Task.Run(()=>CheckVisibleObjects(_cancelAsyncTaskWhenExitPlayMode));
}
private void Update()
{
_position = _target == null ? transform.position : _target.position;
for (int i = 0; i < _objectsToInstance.Count; i++)
{
if (_objectsToInstance.TryTake(out var result))
{
_instancedObjects[result.AssetReference.AssetGUID].AsyncOperationHandle = Addressables.InstantiateAsync(result.AssetReference, _mapParent);
}
}
for (int i = 0; i < _objectsToDestroy.Count; i++)
{
if (_objectsToDestroy.TryTake(out var result))
{
Addressables.Release(result.AsyncOperationHandle);
}
}
}
private void CheckVisibleObjects(CancellationTokenSource cancellationTokenSource){
DateTime dateTime = DateTime.Now;
Vector3 position = Vector3.zero;
while (!cancellationTokenSource.IsCancellationRequested)
{
if((float)(DateTime.Now - dateTime).TotalMilliseconds < 1000f/60f) continue;
if(position == _position)
continue;
position = _position;
foreach (var item in _chunks)
{
if (Vector3.Distance(item.Bounds.ClosestPoint(_position), _position) < _loadDistance)
{
foreach (var mapObject in item.Objects)
{
bool isClose = Vector3.Distance(mapObject.Bounds.ClosestPoint(_position), _position) < _loadDistance;
string hashCode = mapObject.AssetReference.AssetGUID;
if (isClose && !_instancedObjects.ContainsKey(hashCode))
{
_instancedObjects.Add(hashCode, new(mapObject, default));
_objectsToInstance.Add(mapObject);
}
}
}
}
foreach (var instancedObject in new List<InstancedObject>(_instancedObjects.Values))
{
string hashCode = instancedObject.ChunkObject.AssetReference.AssetGUID;
if (Vector3.Distance(instancedObject.ChunkObject.Bounds.ClosestPoint(_position), _position) >= _loadDistance)
{
if (instancedObject.AsyncOperationHandle.IsDone && instancedObject.AsyncOperationHandle.IsValid())
{
_objectsToDestroy.Add(instancedObject);
_instancedObjects.Remove(hashCode);
}
}
}
dateTime = DateTime.Now;
}
}
private void OnEnable()
{
GameSettingsManager.Instance.OnChangeLoadDistance.AddListener(ChangeDistance);
EventManager.PlayerEvents.OnPlayerSpawned.AddListener(OnPlayerSpawned);
_loadDistance = GameSettingsManager.Instance.LoadDistance;
}
private void OnDisable()
{
EventManager.PlayerEvents.OnPlayerSpawned.RemoveListener(OnPlayerSpawned);
GameSettingsManager.Instance.OnChangeLoadDistance.RemoveListener(ChangeDistance);
}
private void OnDestroy() => _cancelAsyncTaskWhenExitPlayMode.Cancel();
private void ChangeDistance(int distance) => _loadDistance = distance;
private void OnPlayerSpawned(IPlayerController playerController) => _target = playerController.PlayerTransform;
private void OnDrawGizmos(){
if(!_drawChunkGizmos)
return;
foreach (var item in _chunks)
{
Gizmos.color = Color.yellow;
DrawBoundsGizmo(item.Bounds);
}
}
private void DrawBoundsGizmo(Bounds bounds)
{
Vector3 boundsCenter = bounds.center;
Vector3 boundsExtents = bounds.extents;
Vector3[] boundsVertices = new Vector3[]
{
boundsCenter + new Vector3(boundsExtents.x, 0, boundsExtents.z),
boundsCenter + new Vector3(boundsExtents.x, 0, -boundsExtents.z),
boundsCenter + new Vector3(-boundsExtents.x, 0, -boundsExtents.z),
boundsCenter + new Vector3(-boundsExtents.x, 0, boundsExtents.z)
};
Gizmos.DrawLine(boundsVertices[0], boundsVertices[1]);
Gizmos.DrawLine(boundsVertices[1], boundsVertices[2]);
Gizmos.DrawLine(boundsVertices[2], boundsVertices[3]);
Gizmos.DrawLine(boundsVertices[3], boundsVertices[0]);
}
[Serializable]
public class Chunk
{
public Bounds Bounds;
public List<ChunkObject> Objects;
[Serializable]
public class ChunkObject
{
public Bounds Bounds;
public AssetReference AssetReference;
public ChunkObject(Bounds bounds, AssetReference assetReference)
{
Bounds = bounds;
AssetReference = assetReference;
}
}
public Chunk(Bounds bounds)
{
Bounds = bounds;
Objects = new();
}
}
[Serializable]
public class InstancedObject{
public Chunk.ChunkObject ChunkObject;
public AsyncOperationHandle<GameObject> AsyncOperationHandle;
public InstancedObject(Chunk.ChunkObject chunkObject, AsyncOperationHandle<GameObject> asyncOperationHandle){
ChunkObject = chunkObject;
AsyncOperationHandle = asyncOperationHandle;
}
}
[ContextMenu("Update Map Assets")]
private void UpdateMapAssets()
{
List<Bounds> assetPositions = new();
List<AssetReference> assetsReferences = new();
_chunks.Clear();
if (string.IsNullOrEmpty(_mapLabel.labelString))
{
Debug.LogError("Map label is not assigned or empty.");
return;
}
Addressables.LoadAssetsAsync<GameObject>(_mapLabel, null).Completed += OnBoundsLoaded;
Addressables.LoadResourceLocationsAsync(_mapLabel).Completed += OnAssetsLoaded;
Debug.Log("Assets loading has started");
void OnBoundsLoaded(AsyncOperationHandle<IList<GameObject>> handle)
{
handle.Completed -= OnBoundsLoaded;
if (handle.Status == AsyncOperationStatus.Succeeded)
{
Bounds worldBounds = new();
foreach (GameObject assetRef in handle.Result)
{
if (assetRef.TryGetComponent<Renderer>(out var renderer))
{
assetPositions.Add(renderer.bounds);
worldBounds.Encapsulate(renderer.bounds);
}
else
Debug.Log($"Renderer component not found on: {assetRef.name}");
}
float originalSizeX = worldBounds.size.x;
float originalSizeZ = worldBounds.size.z;
int countX = Mathf.CeilToInt(originalSizeX / _chunkSize);
int countZ = Mathf.CeilToInt(originalSizeZ / _chunkSize);
float squareSizeX = originalSizeX / countX;
float squareSizeZ = originalSizeZ / countZ;
for (int i = 0; i < countX; i++)
{
for (int j = 0; j < countZ; j++)
{
float minX = worldBounds.min.x + i * squareSizeX;
float minZ = worldBounds.min.z + j * squareSizeZ;
float maxX = minX + squareSizeX;
float maxZ = minZ + squareSizeZ;
Bounds squareBounds = new(new((minX + maxX) / 2, worldBounds.center.y, (minZ + maxZ) / 2),
new(squareSizeX, worldBounds.size.y, squareSizeZ));
_chunks.Add(new(squareBounds));
}
}
for (int i = 0; i < assetPositions.Count; i++)
{
Bounds mapPrefab = assetPositions[i];
AssetReference assetReference = assetsReferences[i];
foreach (var item in _chunks)
{
if (item.Bounds.Contains(mapPrefab.center))
{
item.Objects.Add(new(mapPrefab, assetReference));
item.Bounds.Encapsulate(mapPrefab);
break;
}
}
}
for (int i = _chunks.Count - 1; i >= 0; i--)
{
if (_chunks[i].Objects.Count == 0)
_chunks.RemoveAt(i);
}
}
else
{
Debug.LogError($"AsyncOperation status: {handle.Status}");
}
Addressables.Release(handle);
Debug.Log($"Loading assets has finished. Chunk Size: {_chunkSize} Chunks count: {_chunks.Count} Assets Count: {assetPositions.Count}");
}
void OnAssetsLoaded(AsyncOperationHandle<IList<IResourceLocation>> handle)
{
handle.Completed -= OnAssetsLoaded;
foreach (IResourceLocation location in handle.Result)
assetsReferences.Add(new(location.PrimaryKey));
Addressables.Release(handle);
}
}
}