What's the mean of ExposedReference<T>?

hi, these days i'm learning about the timeline. i found 'ExposedReference ObjectToMove' at timeline example scripts, i google about it but only get few things. i want to know what it and the Resolve () means, and why we should use it, how it works ? thanks.

2 Likes

Playable Assets, including Timeline, are assets and cannot directly contain references to game objects and components in the scene.

An exposed reference is a placeholder for the scene object. Resolve() uses a 'Resolver' as a look up table that maps exposed references to scene objects.

In the case of Timeline, all (non-prefab) GameObjects and Component references are stored inside the PlayableDirector that is playing the Timeline. The resolver is actually the PlayableDirector, and Resolve() is asking it to retrieve the assigned game object.

The drawback is if you reuse a timeline asset, you need to reassign the ExposedReferences.

1 Like

https://www.youtube.com/watch?v=KFFCGABCH3c

4 Likes

@winxalex Thank you for the video. It was very helpful in understanding how to implement the IExposedPropertyTable interface. However, I think I am trying to do the opposite of your example and resolve the exposed reference from a method called on the ScriptableObject itself and not the MonoBehaviour.

I think I need to create a method like PlayableGraph.GetResolver() to do this because just passing the script that implements IExposedPropertyTable into the Resolve() function doesn't seem to work, but I don't know how to use the ExposedPropertyResolver stuct in order to do this. Any help with this would be greatly appreciated!


thanks for explain.


Unity3d 5.6.2's IExposedPropertyTable is no-public for us...
So we can't use ExposedReference in Unity5.6.2:(

Here is an example in Unity2017
https://gist.github.com/frideal/a55961c696acea49d54600c9f66c13ff

1 Like

@tinyant Thanks for the example code! Maybe now I can finally figure out how to use this thing . Unity really needs better documentation and tutorials on things like this.

Sorry to hear you're stuck on Unity 5.6.2. Is this maybe there under a different name/space and/or implementation? Believe the API was at first experimental and has changed a lot in the final release.

Yeah Unity has some undocument or unclear document for Us.
In Unity5.6.2

namespace UnityEngine
{
    internal interface IExposedPropertyTable
    {
        void ClearReferenceValue(PropertyName id);
        Object GetReferenceValue(PropertyName id, out bool idValid);
        void SetReferenceValue(PropertyName id, Object value);
    }
}

so we can't inherent from IExposedPropertyTable.

In Unity2017 IExposedPropertyTable marked public for Us, I think Unity must use ExposedReference for their TimeLine system.:)

namespace UnityEngine
{
    public interface IExposedPropertyTable
    {
        //
        // Summary:
        //     ///
        //     Remove a value for the given reference.
        //     ///
        //
        // Parameters:
        //   id:
        //     Identifier of the ExposedReference.
        void ClearReferenceValue(PropertyName id);
        Object GetReferenceValue(PropertyName id, out bool idValid);
        //
        // Summary:
        //     ///
        //     Assigns a value for an ExposedReference.
        //     ///
        //
        // Parameters:
        //   id:
        //     Identifier of the ExposedReference.
        //
        //   value:
        //     The value to assigned to the ExposedReference.
        void SetReferenceValue(PropertyName id, Object value);
    }
}

I created a MonoBehaviour that implements IExposedPropertyTable and I can get from any other component.

That way the playables can get a resolver from any GameObject, as long as they always get the resolver from the same GameObject they will always get right scene references.

I don't know why Unity API can't be simpler.

public class ExposedReferencesTable : MonoBehaviour, IExposedPropertyTable
{
    public List<PropertyName> properties = new();
    public List<Object> references = new();

    public T Get<T>(ExposedReference<T> reference) where T : Object
    {
        var result = GetReferenceValue(reference.exposedName, out var idValid);
        return idValid ? result as T : null;
    }

    public void Set<T>(ExposedReference<T> reference, T value)
        where T : Object
    {
        SetReferenceValue(reference.exposedName, value);
    }

    public Object GetReferenceValue(PropertyName id, out bool idValid)
    {
        var index = properties.IndexOf(id);

        if (index == -1) {
            idValid = false;
            return null;
        }

        idValid = true;
        return references[index];
    }

    public void SetReferenceValue(PropertyName id, Object value)
    {
        var index = properties.IndexOf(id);

        if (index == -1) {
            properties.Add(id);
            references.Add(value);
        } else {
            references[index] = value;
        }
    }

    public void ClearReferenceValue(PropertyName id)
    {
        var index = properties.IndexOf(id);

        if (index == -1)
            return;

        properties.RemoveAt(index);
        references.RemoveAt(index);
    }
}
public static class ComponentExtensions
{
    public static ExposedReferencesTable GetExposedReferencesResolver(this Component transform)
    {
        var found = gameObject.GetComponent<ExposedReferencesTable>();
        return found == null ? gameObject.AddComponent<ExposedReferencesTable>() : found;
    }
}

Usage:

public class MyPlayable : PlayableBehaviour
{
    private ExposedReferencesTable referenceResolver;

    [SerializeField]
    private ExposedReference<SomeType> _someType;

    public SomeType someType {
        get => referenceResolver.Get(_someType);
        set => referenceResolver.Set(_someType, value);
    }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        if (playerData is not Component component)
            return;

        if (referenceResolver == null)
            referenceResolver = component.GetExposedReferencesResolver();

        // magic
        Debug.Log(someType);
    }
}
2 Likes

Thanks for sharing the code! I was just bumping into ExposedReference yesterday when trying to create a CinemachineShot clip for a Timeline. I ended up with the following code (avoiding needing to implementing anything - and its not the same use case as you were describing), but I don't know if its a "good" approach! I assume the references are local per Timeline, so the scoping of names is relative to that Timeline...?

// The virtual camera reference uses the ExposedReference<> type magic.
var cmShot = clip.asset as CinemachineShot;
cmShot.DisplayName = cm.name;
cmShot.VirtualCamera.exposedName = cm.name; // Unity normally uses a GUID, I think just has to be unique.
shot.GetComponent<PlayableDirector>().SetReferenceValue(cmShot.VirtualCamera.exposedName, cm.GetComponent<CinemachineVirtualCamera>());
1 Like

Thanks for this code. Looks awesome. I just tried it out in a little experiment and created a little ExposedReference in a dummy ScriptableObject and (probably too naively) expected to see a Transform field in the ScriptableObjects inspector view. But nothing shows up.

public class TestSO : ScriptableObject
{
    private ExposedReferencesTable referenceResolver;

    [SerializeField]
    private ExposedReference<Transform> _transform;

    public Transform transform {
        get => referenceResolver.Get(_transform);
        set => referenceResolver.Set(_transform, value);
    }
}

What am I missing?


Not sure what the "ScriptableObjects Inspector view is" so what I did was create a game object with a script that had a component with a property of type TestSO then clicked through.

using UnityEngine;
public class Foo : MonoBehaviour
{
    public TestSO test = new();
    public void OnValidate()
    {
        test = new TestSO();
    }
}

In the inspector I double clicked on the test property and it opened up the TestSO object - its showing 'transform' for me.

8876001--1212264--upload_2023-3-14_10-26-51.png

The other thing you can try is the top right corner of the inspector window (the three dots) there is "normal" and "debug" view modes. Try Debug if Normal does not show what you want.


Well, normally when I have a public Field in a ScriptableObject, it will appear in the Unity-Inspector when selecting the ScriptableObject in the Project-Panel. So in my above Example, if I just write
public Transform myTransform;
then myTransform shows up in the Inspector. If I make an ExposedRefenrence instead, I would expect that it also shows up in the Unity inspector. But it doesn't.

Ah, got it! So I think the problem is its not a field, its a C# property. The Unity inspector does not show properties by default I believe. This blog seems to explain it: https://dev.to/jayjeckel/unity-tips-properties-and-the-inspector-1goo

8879097--1212909--upload_2023-3-15_11-19-15.png

No, it's a field

...
[SerializeField]
private ExposedReference _transform
...

This should definitely show up in the ScriptableObjects inspector.

When I tried your code, I could see the private field "_transform" of type exposed reference in the inspector (in 2022.2) in both Normal and Debug mode. The property was not shown. I confirmed by renaming it, and the name in the inspector changed as well. I wonder if its a bug they fixed or something? E.g. "transform" and "_transform" are both shown as "Transform" in the inspector. I got a funky result with "transform" and "_transform" fields for example.

using UnityEngine;
[CreateAssetMenu]
public class TestSO : ScriptableObject
{
    [SerializeField]
    private ExposedReference<Transform> _transform;
    public Transform transform;
}

Normal mode for inspector, both are visible, but the indentation is weird:
8879610--1213044--upload_2023-3-15_15-31-42.png

Debug mode for inspector looks better:
8879610--1213047--upload_2023-3-15_15-32-25.png

But I am out of ideas. Good luck with it!

I can see the fields just fine on my ScriptableObject, but I can't actually drag any scene objects into the fields, any ideas?

I don’t know sorry. Since it has to create an exposed reference (it has to use a key it can resolve later to fetch the object) I would have guessed the object you are dragging in would have to be in the resolver table so you can do a reverse lookup to find the key for that object in the scene. But that is pure guesswork.

In normal way, you can only start assigning scene objects to ExposedReference when the inspector is a in-scene GameObject Inspector and with a IExposedPropertyTable script attached (like playable director, or your ExposedReferencesTable script).
If the ExposedReference is in a ScriptableObject, you'll need to write custom editor code to show the contents. (some editor code such as, when attaching the ScriptableObject to some component, the content of the ScriptableObject will be shown in the compnoent's editor)

example inspector, that you can assign scene objects to ExposedReference:
GameObject Inspector
├ ExposedReferencesTable component editor
└ Some component's editor, that has [SerializedField] TestSO in it
└ attached TestSO's contents, containing the ExposedReference field (←normally Unity won't show attached content for you, so you have to write this Editor yourself! just iterate through the SerializeProperties and use PropertyField to show them)

The reason why it is so complicated is that, ExposedReference Editor is using SerializedObject.context to resolve the reference, which will be set when it is "GameObject Inspector and with a IExposedPropertyTable script attached". (context == IExposedPropertyTable)

I was able to solve my issue using this repo: https://github.com/instance-id/SO-Persistent-Reference, the ExposedReferenceObject class and its editor class.

My only issue now is that the property gets stored as 'Type Mismatch' so I think I'm doing something wrong.

1 Like