init
This commit is contained in:
@@ -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:
|
||||
Reference in New Issue
Block a user