This commit is contained in:
2025-08-18 09:22:24 +08:00
commit cef5623ab0
1333 changed files with 305844 additions and 0 deletions
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: 34b6cee54f21dde4a80098c1be41ee29
folderAsset: yes
timeCreated: 1552388739
licenseType: Store
DefaultImporter:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,76 @@
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
/// <summary>
/// First in, First out caching. Use it when it's very unlikely or impossible to scroll back to older items.
/// </summary>
/// <remarks>In most cases, this is not actually the best way of doing caching. Consider <see cref="LRUCachingPool"/> instead, as it's more versatile.</remarks>
public class FIFOCachingPool : IPool
{
public delegate void ObjectDestroyer(object key, object value);
public int Capacity { get; private set; }
public int CurrentCount { get { return _Keys.Count; } }
readonly Queue<object> _Keys;
readonly Dictionary<object, object> _Cache;
readonly ObjectDestroyer _ObjectDestroyer;
/// <summary>First in, First out caching</summary>
/// <param name="capacity"></param>
/// <param name="objectDestroyer">
/// When an object is kicked out of the cache, this will be used to process its destruction,
/// in case special code needs to be executed. This is also called for each value when the cache is cleared usinc <see cref="Clear"/>
/// </param>
public FIFOCachingPool(int capacity, ObjectDestroyer objectDestroyer = null)
{
Capacity = capacity;
_Keys = new Queue<object>(capacity);
_Cache = new Dictionary<object, object>(capacity);
_ObjectDestroyer = objectDestroyer;
}
public object Get(object key)
{
object value;
if (_Cache.TryGetValue(key, out value))
{
return value;
}
return null;
}
public void Put(object key, object value)
{
if (CurrentCount == Capacity)
{
object keyToDiscard = _Keys.Dequeue();
object oldValue = _Cache[keyToDiscard];
_Cache.Remove(keyToDiscard);
if (_ObjectDestroyer != null)
_ObjectDestroyer(keyToDiscard, oldValue);
}
_Keys.Enqueue(key);
_Cache[key] = value;
}
public void Clear()
{
if (_ObjectDestroyer != null)
{
foreach (var kv in _Cache)
{
if (kv.Value != null)
_ObjectDestroyer(kv.Key, kv.Value);
}
}
_Cache.Clear();
}
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 9e512af370b74dc43958a0abf0df1946
timeCreated: 1552388740
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,18 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using Com.ForbiddenByte.OSA.Util;
using System;
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
public interface IPool
{
int Capacity { get; }
object Get(object key);
void Put(object key, object value);
void Clear();
}
}
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 00dd717d999bf284e97208b360a592a3
timeCreated: 1552388739
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,89 @@
using System.Collections.Generic;
namespace Com.ForbiddenByte.OSA.Util.IO.Pools
{
/// <summary>
/// Clears the least accessed items in favor of the most recently accessed ones.
/// </summary>
/// <remarks>This is more versatile than <see cref="FIFOCachingPool"/>.</remarks>
public class LRUCachingPool : IPool
{
public delegate void ObjectDestroyer(object key, object value);
public int Capacity { get; private set; }
public int CurrentCount { get { return _Cache.Count; } }
readonly Dictionary<object, LinkedListNode<CacheItem>> _Cache;
readonly LinkedList<CacheItem> _LruOrderList;
readonly ObjectDestroyer _ObjectDestroyer;
public LRUCachingPool(int capacity, ObjectDestroyer objectDestroyer = null)
{
Capacity = capacity;
_Cache = new Dictionary<object, LinkedListNode<CacheItem>>(capacity);
_LruOrderList = new LinkedList<CacheItem>();
_ObjectDestroyer = objectDestroyer;
}
public object Get(object key)
{
if (_Cache.TryGetValue(key, out var node))
{
// Move accessed node to the end to show that it was recently used
_LruOrderList.Remove(node); // remove current node
_LruOrderList.AddLast(node); // add it back to the end
return node.Value.Value;
}
return null;
}
public void Put(object key, object value)
{
// If the key already exists, remove it first
if (_Cache.TryGetValue(key, out var existingNode))
{
_LruOrderList.Remove(existingNode);
_Cache.Remove(key);
}
if (_Cache.Count == Capacity)
{
// Remove the least recently used item
LinkedListNode<CacheItem> oldestNode = _LruOrderList.First;
_LruOrderList.RemoveFirst();
_Cache.Remove(oldestNode.Value.Key);
if (_ObjectDestroyer != null)
_ObjectDestroyer(oldestNode.Value.Key, oldestNode.Value.Value);
}
var newNode = new LinkedListNode<CacheItem>(new CacheItem { Key = key, Value = value });
_LruOrderList.AddLast(newNode);
_Cache[key] = newNode;
}
public void Clear()
{
if (_ObjectDestroyer != null)
{
foreach (var cached in _LruOrderList)
{
if (cached.Value != null)
_ObjectDestroyer(cached.Key, cached.Value);
}
}
_Cache.Clear();
_LruOrderList.Clear();
}
class CacheItem
{
public object Key { get; set; }
public object Value { get; set; }
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 63c0c36a14e9f3d40927c554363757d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,154 @@
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using Com.ForbiddenByte.OSA.Util;
using System;
using Com.ForbiddenByte.OSA.Util.IO.Pools;
namespace Com.ForbiddenByte.OSA.Util.IO
{
/// <summary>Utility behavior to be attached to a GameObject containing a RawImage for loading remote images using <see cref="SimpleImageDownloader"/>, optionally displaying a specific image during loading and/or on error</summary>
[RequireComponent(typeof(RawImage))]
public class RemoteImageBehaviour : MonoBehaviour
{
public delegate void LoadCompleteDelegate(bool fromCache, bool success);
[Tooltip("If not assigned, will try to find it in this game object")]
[SerializeField] RawImage _RawImage = null;
#pragma warning disable 0649
[SerializeField] Texture2D _LoadingTexture = null;
[SerializeField] Texture2D _ErrorTexture = null;
#pragma warning restore 0649
string _CurrentRequestedURL;
bool _DestroyPending;
Texture2D _Texture;
IPool _Pool;
public void InitializeWithPool(IPool pool)
{
_Pool = pool;
}
void Awake()
{
if (!_RawImage)
_RawImage = GetComponent<RawImage>();
}
/// <summary>Starts the loading, setting the current image to <see cref="_LoadingTexture"/>, if available. If the image is already in cache, and <paramref name="loadCachedIfAvailable"/>==true, will load that instead</summary>
public void Load(string imageURL, bool loadCachedIfAvailable = true, LoadCompleteDelegate onCompleted = null, Action onCanceled = null)
{
bool currentRequestedURLAlreadyLoaded = _CurrentRequestedURL == imageURL;
_CurrentRequestedURL = imageURL;
if (loadCachedIfAvailable)
{
bool foundCached = false;
// Don't re-request if the url is the same. This is useful if there's no pool provided
if (currentRequestedURLAlreadyLoaded)
foundCached = _Texture != null;
else if (_Pool != null)
{
Texture2D cachedInPool = _Pool.Get(imageURL) as Texture2D;
if (cachedInPool)
{
_Texture = cachedInPool;
foundCached = true;
_CurrentRequestedURL = imageURL;
}
}
if (foundCached)
{
_RawImage.texture = _Texture;
if (onCompleted != null)
onCompleted(true, true);
return;
}
}
_RawImage.texture = _LoadingTexture;
var request = new SimpleImageDownloader.Request()
{
url = imageURL,
onDone = result =>
{
if (!_DestroyPending && imageURL == _CurrentRequestedURL) // this will be false if a new request was done during downloading, case in which the result will be ignored
{
// Commented: not reusing textures to load data into them anymore, since in most cases we'll use a pool
//result.LoadTextureInto(_Texture);
if (_Pool == null)
{
// Non-pooled textures should be destroyed
if (_Texture)
DisposeTexture(_Texture);
_Texture = result.CreateTextureFromReceivedData();
}
else
{
var textureAlreadyStoredMeanwhile = _Pool.Get(imageURL);
bool someoneStoredTheImageSooner = textureAlreadyStoredMeanwhile != null;
if (someoneStoredTheImageSooner)
{
// Happens when the same URL is requested multiple times for the first time, and of course only the first
// downloaded image should be kept. In this case, someone else already have downloaded and cached the image, so we just discard the one we downloaded
_Texture = textureAlreadyStoredMeanwhile as Texture2D;
}
else
{
// First time downloaded => cache
_Texture = result.CreateTextureFromReceivedData();
_Pool.Put(imageURL, _Texture);
}
}
_RawImage.texture = _Texture;
if (onCompleted != null)
onCompleted(false, true);
}
else if (onCanceled != null)
onCanceled();
},
onError = () =>
{
if (!_DestroyPending && imageURL == _CurrentRequestedURL) // this will be false if a new request was done during downloading, case in which the result will be ignored
{
_RawImage.texture = _ErrorTexture;
if (onCompleted != null)
onCompleted(false, false);
}
else if (onCanceled != null)
onCanceled();
}
};
SimpleImageDownloader.Instance.Enqueue(request);
}
void OnDestroy()
{
_DestroyPending = true;
// Non-pooled textures should be destroyed
if (_Pool == null && _Texture)
{
DisposeTexture(_Texture);
}
}
void DisposeTexture(Texture2D texture)
{
try
{
Destroy(texture);
}
catch { }
}
}
}
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c3acd27727b08d44fb5740f21dedfd14
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
@@ -0,0 +1,121 @@
// WWW class usage
#pragma warning disable 0618
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
using UnityEngine.Networking;
namespace Com.ForbiddenByte.OSA.Util.IO
{
/// <summary>
/// <para>A utility singleton class for downloading images using a LIFO queue for the requests. <see cref="MaxConcurrentRequests"/> can be used to limit the number of concurrent requests. </para>
/// <para>Default is <see cref="DEFAULT_MAX_CONCURRENT_REQUESTS"/>. Each request is executed immediately if there's room for it. When the queue is full, the downloder starts checking each second if a slot is freed, after which re-enters the loop.</para>
/// </summary>
public class SimpleImageDownloader : MonoBehaviour
{
public static SimpleImageDownloader Instance
{
get
{
if (_Instance == null)
_Instance = new GameObject(typeof(SimpleImageDownloader).Name).AddComponent<SimpleImageDownloader>();
return _Instance;
}
}
static SimpleImageDownloader _Instance;
public int MaxConcurrentRequests { get; set; }
const int DEFAULT_MAX_CONCURRENT_REQUESTS = 20;
List<Request> _QueuedRequests = new List<Request>();
List<Request> _ExecutingRequests = new List<Request>();
WaitForSeconds _Wait1Sec = new WaitForSeconds(1f);
IEnumerator Start()
{
if (MaxConcurrentRequests == 0)
MaxConcurrentRequests = DEFAULT_MAX_CONCURRENT_REQUESTS;
while (true)
{
while (_ExecutingRequests.Count >= MaxConcurrentRequests)
{
yield return _Wait1Sec;
}
int lastIndex = _QueuedRequests.Count - 1;
if (lastIndex >= 0)
{
var lastRequest = _QueuedRequests[lastIndex];
_QueuedRequests.RemoveAt(lastIndex);
StartCoroutine(DownloadCoroutine(lastRequest));
}
yield return null;
}
}
void OnDestroy()
{
_Instance = null;
}
public void Enqueue(Request request)
{ _QueuedRequests.Add(request); }
IEnumerator DownloadCoroutine(Request request)
{
_ExecutingRequests.Add(request);
var www = UnityWebRequestTexture.GetTexture(request.url);
yield return www.SendWebRequest();
if (string.IsNullOrEmpty(www.error))
{
if (request.onDone != null)
{
var result = new Result(www);
request.onDone(result);
}
}
else
{
if (request.onError != null)
request.onError();
}
www.Dispose();
_ExecutingRequests.Remove(request);
}
public class Request
{
public string url;
public Action<Result> onDone;
public Action onError;
}
public class Result
{
UnityEngine.Networking.UnityWebRequest _UsedRequest;
public Result(UnityWebRequest www)
{ _UsedRequest = www; }
public Texture2D CreateTextureFromReceivedData()
{ return DownloadHandlerTexture.GetContent(_UsedRequest); }
}
}
}
#pragma warning restore 0618
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4864fb691f2f269449b08caea7dc1284
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData: