0
\$\begingroup\$

Unity 2019.3

I am writing a few extension methods to make my life a little bit easier, but this one is bogging me down.

I am loading stuff with Unity's Addressable system, and I would like use it by extension methods.

I am passing in my variables (reference type & value type, doesn't matter) to my extension methods, hoping to set them and and hide some complexity and source of bugs, and errors. It seems to me they are passed by value, because when I am trying to use them outside the extension methods, they are not set, null, etc. Not working.

There is a callback called Completed, and I am subscribing with a lambda expression...

I tried with the ref keyword, but obviously the compiler is yelling, that she doesn't want to pass my ref, out whatever parameters to local functions... :(

I tried it out with no local functions, and it did not work without the ref keywork, but it did with it!

You get the idea! Please see my code below!

This is the one that I can't get to work...

///Trying to set the generic variable data from the Addressable assetref
public static void TryLoadData<T>(this AssetReference assetref, T data)
    {
        var handle = assetref.LoadAssetAsync<T>();

        handle.Completed += loadedresult =>
            {
                T result = loadedresult.Result;
                if (result == null) return;
                data = result;
            };
    }

This one does, but only with ref...

///Setting the generic data parameter to the parameter g.
public static void SwapTest<T>(this T g,ref T data) where T : class=> data = g;

I am not sure if there is a workaround, I thought of pointers, or global variables, but not sure which one is the right way to go.

Thanks

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The problem is that as-written, your T data reference could conceivably be a local struct allocated on the stack, that's vanished from existence by the time handle.Completed is called. Or a reference type variable that belongs to such an ephemeral struct. That could leave us with effectively a dangling pointer when the callback is ready to call, so the C# language specification makes this illegal.

You'll need to give your function something that can guarantee some independent lifecycle management, so we have a strong guarantee that the variable we're trying to set still exists as long as our callback holds a reference to it.

Here's one way you can tackle that:

public enum LoadStatus {
    Loading,
    Invalid,
    DataReady
}

public class Loadable<T> {

    public T data { get; private set; }
    public LoadingStatus status { get; private set; }

    public void OnComplete(AsyncOperationHandle<T> handle) {
        T result = handle.Result;
        if(result == Default(T)) {
            status = LoadingStatus.Invalid;
            return;
        }
        data = result;
        status = LoadingStatus.DataReady;
    }
}

Then you can either pass your Loadable<T> by reference (which happens without the ref keyword since it's a class type), or return a new Loadable<T> from your extension method.

public static void TryLoadData<T>(this AssetReference assetref, Loadable<T> data)
{
    var handle = assetref.LoadAssetAsync<T>();
    handle.Completed += data.OnComplete;
}

public static Loadable<T> TryLoadData<T>(this AssetReference assetref)
{
    var handle = assetref.LoadAssetAsync<T>();

    var data = new Loadable<T>();
    handle.Completed += data.OnComplete;
    return data;
}

Heck, you could even skip the extension method and build this functionality right into the Loadable class:

[System.Serializable]
public class Loadable<T> {
    [SerializeField, AssetReferenceTypeRestriction(typeof(T))]
    AssetReference _reference;

    [SerializeField] // Allow peeking at the data in the Inspector.
    T _data;
    public T data { get { return _data; } private set { _data = value; } }
    public LoadingStatus status { get; private set; }

    public void TryLoadAsync() {
        var handle = _reference.LoadAssetAsync<T>();
        handle.Completed += OnComplete;
    }

    public void OnComplete(AsyncOperationHandle<T> handle) {
        T result = handle.Result;
        if(result == Default(T)) {
            status = LoadingStatus.Invalid;
            return;
        }
        data = result;
        status = LoadingStatus.DataReady;
    }
}

Now you can write something like....

// This line makes the generic concrete so it works in the Inspector,
// though apparently it won't be needed anymore in Unity 2020+ :D
public class LoadableTexture2D : Loadable<Texture2D> {}

public LoadableTexture2D myTexture;

And get an inspector that allows you to assign the asset reference and peek at the loaded data in one neat bundle. You could even add in a UnityEvent to invoke when the asset finishes loading, if that's useful for your case.

You can kick off the load with:

myTexture.TryLoadAsync();
\$\endgroup\$
1
  • \$\begingroup\$ Thanks, I'll test it later on! \$\endgroup\$ Commented Jun 1, 2020 at 18:40

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .