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: 3545
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.

Who is online

Users browsing this forum: No registered users and 0 guests