Skip to main content
Missing serializable markup
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373
[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;
    }
}
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;
    }
}
[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;
    }
}
Adding bundled version
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

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

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();

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

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();
added 5 characters in body
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373

The problem is that as-written, your T data data typereference 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;
}

The problem is that as-written, your T data type 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;
}

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;
}
Better explanation
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373
Loading
Source Link
DMGregory
  • 136.3k
  • 22
  • 247
  • 373
Loading