Faerdan
Topic Author
Posts: 73
Joined: 02 Oct 2015, 09:11
Location: Galway, Ireland
Contact:

Unity Addressable Image Control

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:
<local:AddressableImage AssetPath="{Binding SelectedQuest.Image.RuntimeKey}"/>
"Image" in this binding is an AssetReferenceTexture2d:
public AssetReferenceTexture2D _image;
public AssetReferenceTexture2D Image { get => _image; }
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).
Image

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.
#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
    }
}

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.
 
User avatar
jsantos
Site Admin
Posts: 3905
Joined: 20 Jan 2012, 17:18
Contact:

Re: Unity Addressable Image Control

15 Jun 2022, 16:50

This is an awesome contribution and we should work to have this example officially available on GitHub.

Thank you!
 
Faerdan
Topic Author
Posts: 73
Joined: 02 Oct 2015, 09:11
Location: Galway, Ireland
Contact:

Re: Unity Addressable Image Control

15 Jun 2022, 17:11

Great! I want to do a cleaner implementation which doesn't inherit from Image, but instead inherits from FrameworkElement and uses an Image in it's ControlTemplate. This will remove the weirdness of the AddressableImage control having a publicly writable Source dependency property, which should never be set directly.
 
SingingBard
Posts: 5
Joined: 22 Oct 2022, 16:57

Re: Unity Addressable Image Control

05 Dec 2022, 17:06

I really like this solution, as I wanted to use Addressables for my icons. However, I am having some issues I was hoping I could recruit your aid in resolving. I have an InventoryView which contains a bunch of item Icons. When the user receives an inventory packet from the server, the items in the Inventory refresh the binding. With your solution, all the icons flash. It isn't a huge delay, but it is very noticeable. When using the default Image control, I don't have this issue. I would prefer to use Addressables, but is there any way to eliminate the flashing while the icon is loaded? Is there an easy way to cache the loaded addressable icon so that it doesn't need to asynchronously load it each time? Thanks for your time
 
User avatar
maherne
Site Admin
Posts: 41
Joined: 01 Jul 2022, 10:10

Re: Unity Addressable Image Control

05 Dec 2022, 17:18

Addressables loads the asset on the next frame, which causes this flashing issue. Unless Unity improve this aspect of Addressables, the best thing to do is to load and keep a reference handle for any images/assets before using them in the UI. You don't need to do anything with this handle, it will keep the Addressable asset reference count greater than zero, keeping it in memory, until it and all other references to that asset are released.

One way to achieve this is to store a list of Addressable AssetReferences used for each scene or state in your game, and then load and release these when the scene/state is loaded and unloaded.

Who is online

Users browsing this forum: No registered users and 13 guests