Unity Addressable Image Control
Posted: 12 Jun 2022, 21:29
Hi all,
The Unity Addressables system simplifies the lifecycle management of unmanaged resources (textures, models, GameObjects, etc.) in Unity. It also has the benefit of allowing you to store these assets outside the built application, including hosting them remotely. While Addressables has it's issues/quirks, we've found it to be an invaluable addition to our game.
Addressable Image Control
I've created an AddressableImage control which allows you to use Addressable Texture2D assets within Noesis. This control will handle the lifecycle of the Texture2D asset, using the Addressable API to load and release it as necessary.
How It Works
This AddressableImage control inherits from the Image class, and adds a new string AssetPath dependency property. This AssetPath string can be set to the name, or the Unity GUID, of the Addressable Texture2D asset. When AssetPath is set it will release any previously loaded/loading asset, it will then try to load the new Texture2d using Addressables.LoadAssetAsync<Texture2D>(AssetPath). The AddressableImage control takes full control of the Source dependency property, setting Source directly is not supported.
Usage
Here is an example of using the AddressableImage:
"Image" in this binding is an AssetReferenceTexture2d:
Example Project
I've created an example Unity project which takes the Noesis QuestLog sample and makes it's quest image Addressable, you can find this project in a zip archive here:
For the Addressable sample to work you'll need to install Noesis 3.1.4 and set your license details (as normal).
Here is a screenshot of the sample, including the Addressables Event Viewer showing where one image has been released, and the new image loaded (I've added a red arrow pointing to this in the timeline).
AddressableImage Class
Here is the full AddressableImage class. If you don't want to download the example project, this is all you need for integration into your own project.
To allow the use of AddressableImage controls in Blend/WPF, and for use with design time resources, AddressableImage in WPF implements AssetPath as an ImageSource.
The Unity Addressables system simplifies the lifecycle management of unmanaged resources (textures, models, GameObjects, etc.) in Unity. It also has the benefit of allowing you to store these assets outside the built application, including hosting them remotely. While Addressables has it's issues/quirks, we've found it to be an invaluable addition to our game.
Addressable Image Control
I've created an AddressableImage control which allows you to use Addressable Texture2D assets within Noesis. This control will handle the lifecycle of the Texture2D asset, using the Addressable API to load and release it as necessary.
How It Works
This AddressableImage control inherits from the Image class, and adds a new string AssetPath dependency property. This AssetPath string can be set to the name, or the Unity GUID, of the Addressable Texture2D asset. When AssetPath is set it will release any previously loaded/loading asset, it will then try to load the new Texture2d using Addressables.LoadAssetAsync<Texture2D>(AssetPath). The AddressableImage control takes full control of the Source dependency property, setting Source directly is not supported.
Usage
Here is an example of using the AddressableImage:
Code: Select all
<local:AddressableImage AssetPath="{Binding SelectedQuest.Image.RuntimeKey}"/>
Code: Select all
public AssetReferenceTexture2D _image;
public AssetReferenceTexture2D Image { get => _image; }
I've created an example Unity project which takes the Noesis QuestLog sample and makes it's quest image Addressable, you can find this project in a zip archive here:
For the Addressable sample to work you'll need to install Noesis 3.1.4 and set your license details (as normal).
Here is a screenshot of the sample, including the Addressables Event Viewer showing where one image has been released, and the new image loaded (I've added a red arrow pointing to this in the timeline).
AddressableImage Class
Here is the full AddressableImage class. If you don't want to download the example project, this is all you need for integration into your own project.
Code: Select all
#if UNITY_5_3_OR_NEWER
#define NOESIS
using Noesis;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
#else
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
#endif
using System;
namespace Addressable
{
public class AddressableImage : Image
{
#if NOESIS
private AsyncOperationHandle<Texture2D> _asyncOperationHandle;
public string AssetPath
{
get { return (string)GetValue(AssetPathProperty); }
set { SetValue(AssetPathProperty, value); }
}
public static readonly DependencyProperty AssetPathProperty =
DependencyProperty.Register(nameof(AssetPath), typeof(string), typeof(AddressableImage),
new PropertyMetadata(null, OnAssetKeyChanged));
public AddressableImage()
{
WeakReference weak = new WeakReference(this);
this.Unloaded += (s, e) => { ((AddressableImage)weak.Target)?.OnUnloaded(s, e); };
this.Loaded += (s, e) => { ((AddressableImage)weak.Target)?.OnLoaded(s, e); };
}
public void LoadAsset()
{
UnloadAsset();
if (string.IsNullOrEmpty(AssetPath))
{
return;
}
_asyncOperationHandle = Addressables.LoadAssetAsync<Texture2D>(AssetPath);
_asyncOperationHandle.Completed += OnAssetLoadCompleted;
}
public void UnloadAsset()
{
if (_asyncOperationHandle.IsValid())
{
Addressables.Release(_asyncOperationHandle);
_asyncOperationHandle = new AsyncOperationHandle<Texture2D>();
Source = null;
}
}
private void OnAssetLoadCompleted(AsyncOperationHandle<Texture2D> handle)
{
if (handle.IsValid())
{
if (handle.IsDone
&& handle.Status == AsyncOperationStatus.Succeeded
&& handle.Result != null)
{
Source = new TextureSource(handle.Result);
}
else
{
Addressables.Release(handle);
}
}
}
private void OnLoaded(object sender, Noesis.EventArgs args)
{
LoadAsset();
}
private void OnUnloaded(object sender, Noesis.EventArgs args)
{
UnloadAsset();
}
private static void OnAssetKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AddressableImage assetImage)
{
if (!string.IsNullOrEmpty((string)e.NewValue))
{
assetImage.LoadAsset();
}
else
{
assetImage.UnloadAsset();
}
}
}
#else
public AddressableImage()
{
WeakReference weak = new WeakReference(this);
this.Loaded += (s, e) => { ((AddressableImage)weak.Target)?.OnLoaded(s, e); };
}
private void OnLoaded(object sender, EventArgs args)
{
if (Source != null && AssetPath == null)
{
throw new Exception("Source cannot be used directly, use AssetPath instead.");
}
Source = AssetPath;
}
public ImageSource AssetPath
{
get { return (ImageSource)GetValue(AssetPathProperty); }
set { SetValue(AssetPathProperty, value); }
}
public static readonly DependencyProperty AssetPathProperty =
DependencyProperty.Register(nameof(AssetPath), typeof(ImageSource), typeof(AddressableImage),
new PropertyMetadata(null, OnAssetKeyChanged));
private static void OnAssetKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AddressableImage assetImage)
{
assetImage.Source = (ImageSource)e.NewValue;
}
}
#endif
}
}