Make timeline playableasset define variables in runtime.

Now this is my first time in posting on Unity’s forum so forgive me if Im not being clear. I am trying to code a playableasset and playablebehaviour script pair to allow my game to enable and disable several gameobjects during a timeline. Here are the scripts in question.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[Serializable]
public class TimelineGameObjectOnOff : PlayableAsset, ITimelineClipAsset
{

    public GameObjectOnOffPlayableBehaviour template = new GameObjectOnOffPlayableBehaviour();

    public List<ExposedReferenceEnabledObject> exposedReferenceEnabledObjects;
    public List<ExposedReferenceDisabledObject> exposedReferenceDisabledObjects;
    [Tooltip("When set to true, it will match the gameobjects from the exposed enable and disable gameobjects lists to the lists in gameObjectsEnabled and gameObjectsDisabled in the GameObjectOnOffPlayableBehaviour script")]
    public bool matchItems;

    private bool matchEnabledSize;
    private bool matchDisabledSize;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        ScriptPlayable<GameObjectOnOffPlayableBehaviour> playable = ScriptPlayable<GameObjectOnOffPlayableBehaviour>.Create(graph, template);
        GameObjectOnOffPlayableBehaviour gameObjectOnOffPlayableBehaviour = playable.GetBehaviour();
       
        //This will make the size of the enable and disable lists be the same in the gameObjectOnOffPlayableBehaviour script
if (gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Count > exposedReferenceEnabledObjects.Count)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Clear();
            matchEnabledSize = true;
        }
        while (gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Count < exposedReferenceEnabledObjects.Count || matchEnabledSize)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Add(null);
            if (gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Count >= exposedReferenceEnabledObjects.Count)
            {
                matchEnabledSize = false;
            }
        }

        if (gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Count > exposedReferenceDisabledObjects.Count)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Clear();
            matchDisabledSize = true;
        }
        while (gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Count < exposedReferenceDisabledObjects.Count || matchEnabledSize)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Add(null);
            if (gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Count >= exposedReferenceDisabledObjects.Count)
            {
                matchDisabledSize = false;
            }
        }
        //When matchItems is set to true, it will go and match the enabled and disabled lists of the exposed gameobjects to the gameobject lists from the GameObjectsOnOffPlayableBehaviour script.
        while (matchItems)
        {
            //Go throught the whole list of enabling gameobjects on this script and transfer them to the GameObjectsOnOffPlayableBehaviour script.
            for (int a = 0; gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Count > a; a++)
            {
                gameObjectOnOffPlayableBehaviour.gameObjectsEnabled[a] = exposedReferenceEnabledObjects[a].exposedEnabledGameObject.Resolve(graph.GetResolver()) as GameObject;
            }
            //Go throught the whole list of disabling gameobjects on this script and transfer them to the GameObjectsOnOffPlayableBehaviour script.
            for (int a = 0; gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Count > a; a++)
            {
                gameObjectOnOffPlayableBehaviour.gameObjectsDisabled[a] = exposedReferenceDisabledObjects[a].exposedDisabledGameObject.Resolve(graph.GetResolver()) as GameObject;
            }

            //Once we've matched everything, we make sure to exit the statement.
            matchItems = false;
        }
        return playable;
    }

    public ClipCaps clipCaps
    {
        get { return ClipCaps.None; }
    }

}
//This is a workaround from not being able to expose a list of gameObjects.
[Serializable]
public class ExposedReferenceEnabledObject
{
    [SerializeField]
    public ExposedReference<GameObject> exposedEnabledGameObject;
}

[Serializable]
public class ExposedReferenceDisabledObject
{
    [SerializeField]
    public ExposedReference<GameObject> exposedDisabledGameObject;
}

Here is the behavior script.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[Serializable]
public class GameObjectOnOffPlayableBehaviour : PlayableBehaviour
{

    public List<GameObject> gameObjectsEnabled;
    public List<GameObject> gameObjectsDisabled;

    public bool clipPlayed = false;

    private PlayableDirector director;

    public override void OnPlayableCreate(Playable playable)
    {
        director = (playable.GetGraph().GetResolver() as PlayableDirector);
    }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {

        if (!clipPlayed
            && info.weight > 0f)
        {
            if (Application.isPlaying)
            {
               
                foreach (GameObject gameObject in gameObjectsEnabled)
                {
                    if (gameObject != null)
                    {
                        if (!gameObject.activeSelf)
                            gameObject.SetActive(true);
                    }
                }
                foreach (GameObject gameObject in gameObjectsDisabled)
                {
                    if(gameObject != null)
                        if(gameObject.activeSelf)
                            gameObject.SetActive(false);

                }
            }
            clipPlayed = true;
        }
    }

    public override void OnGraphStart(Playable playable)
    {
        base.OnGraphStart(playable);
    }
}

I’m able to define the variables by checking true to the matching items variable in the inspector, but the problem is that the gameobjects don’t stay defined.

If I open another scene and come back to this scene, the variables for the GameObjectOnOffPlayableBehaviour script is missing. (See inspector on right.)


So that means that during runtime, the script might as well not even be there. Is there a way to define these variables during runtime without setting matchItems to true in the inspector?

And again, I’m sorry if some parts aren’t clear. I’ve been working on this for days and don’t seem to be getting anywhere.

What you are seeing is expected behaviour. Because the behaviour template is serialized in the asset, it will lose it’s scene references. That is the reason for the exposed references, it stores the scene references inside the playable director instead.

I’d recommend adding [NonSerialized] (or use an auto property) to the Lists in the gameObject template, and recreate the lists explicitly each time CreatePlayable is called, instead of trying to match them up. More or less what the matchItems code path you have is already doing.

I’m sorry. I’m still a little confused. When you say add nonserialized to the lists in gameObject template, do you mean the “gameObjectsEnabled” and “gameObjectsDisabled” variables in the GameObjectOnOffPlayableBehaviour script? Also when you say “recreate the lists explicitly each time CreatePlayable is called” what do you mean. Unfortunately I’m not that smart with timelines and am still struggling to understand. Maybe an example might help out

In GameObjectOnOffPlayableBehaviour , don’t serialize gameObjectsEnabled and gameObjectsDisabled. Make them a property or add [NonSerialized] to them.

In TimelineGameObjectOnOff.CreatePlayable, after creating the playable rebuild GameObjectOnOffPlayableBehaviour .gameObjectEnabled GameObjectOnOffPlayableBehaviour.gameObjectsDisabled completely.

i.e.
gameObjectOnOffPlayableBehaviour.gameObjectEnabled = new List();
foreach (var e in exposedReferenceEnabledObjects)
gameObjectOnOffPlayableBehaviour.gameObjectEnabled.Add(exposedReferenceEnabledObjects.Resolve(…));

Trying to keep the lists in sync won’t really work because each time Compile is called you are creating a new playable. The template object is useful if you want to start with some values defined by the user (it makes a copy of the template), but the template cannot store GameObjects.

I hope that helps.

Thank you so much for all your help. I was going to make the eyes for my characters change based off an animation, but now I don’t have to do that. Also this script will come in handy when I need to enable or disable other gameobjects. For those who want to study off of what was done, here is what the code looks like now.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[Serializable]
public class TimelineGameObjectOnOff : PlayableAsset, ITimelineClipAsset
{

    public GameObjectOnOffPlayableBehaviour template = new GameObjectOnOffPlayableBehaviour();
    public List<ExposedReferenceEnabledObject> exposedReferenceEnabledObjects;
    public List<ExposedReferenceDisabledObject> exposedReferenceDisabledObjects;

    //This will allow us to iterate on each item that is in the gameObjectOnOffPlayable behaviour's gameObject lists.
    private int enableIndexCounter = 0;
    private int disableIndexCounter = 0;

    //This is called everytime a clip is added or if the scene is loaded.
    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        ScriptPlayable<GameObjectOnOffPlayableBehaviour> playable = ScriptPlayable<GameObjectOnOffPlayableBehaviour>.Create(graph, template);
        GameObjectOnOffPlayableBehaviour gameObjectOnOffPlayableBehaviour = playable.GetBehaviour();
        //We make sure that the index of the lists that we will be iterating through will start at the beginning.
        enableIndexCounter = 0;
        disableIndexCounter = 0;
        //We refresh the list to have nothing in it just before we add to the list.
        gameObjectOnOffPlayableBehaviour.gameObjectsEnabled = new List<GameObject>();
        //We will make the exposedobjects in this list to match up with the currently empty lists from the GameObjectOnOffPlayableBehaviour script's gameObject lists.
        foreach (var e in exposedReferenceEnabledObjects)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsEnabled.Add(exposedReferenceEnabledObjects[enableIndexCounter].exposedEnabledGameObject.Resolve(graph.GetResolver()));
            enableIndexCounter++;
        }
        //We refresh the list to have nothing in it just before we add to the list.
        gameObjectOnOffPlayableBehaviour.gameObjectsDisabled = new List<GameObject>();
        //We will make the exposedobjects in this list to match up with the currently empty lists from the GameObjectOnOffPlayableBehaviour script's gameObject lists.
        foreach (var e in exposedReferenceDisabledObjects)
        {
            gameObjectOnOffPlayableBehaviour.gameObjectsDisabled.Add(exposedReferenceDisabledObjects[disableIndexCounter].exposedDisabledGameObject.Resolve(graph.GetResolver()));
            disableIndexCounter++;
        }
        return playable;
    }

    public ClipCaps clipCaps
    {
        get { return ClipCaps.None; }
    }

}

[Serializable]
public class ExposedReferenceEnabledObject
{
    [SerializeField]
    public ExposedReference<GameObject> exposedEnabledGameObject;
}

[Serializable]
public class ExposedReferenceDisabledObject
{
    [SerializeField]
    public ExposedReference<GameObject> exposedDisabledGameObject;
}

This is the behaviour script.

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

[Serializable]
public class GameObjectOnOffPlayableBehaviour : PlayableBehaviour
{
    [NonSerialized]
    public List<GameObject> gameObjectsEnabled;
    [NonSerialized]
    public List<GameObject> gameObjectsDisabled;

    public bool clipPlayed = false;

    private PlayableDirector director;

    public override void OnPlayableCreate(Playable playable)
    {
        director = (playable.GetGraph().GetResolver() as PlayableDirector);
    }

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {

        if (!clipPlayed
            && info.weight > 0f)
        {
            if (Application.isPlaying)
            {
                foreach (GameObject gameObject in gameObjectsEnabled)
                {
                    if (gameObject != null)
                    {
                        if (!gameObject.activeSelf)
                            gameObject.SetActive(true);
                    }
                }
                foreach (GameObject gameObject in gameObjectsDisabled)
                {
                    if(gameObject != null)
                        if(gameObject.activeSelf)
                            gameObject.SetActive(false);

                }
            }
            clipPlayed = true;
        }
    }

    public override void OnGraphStart(Playable playable)
    {
        base.OnGraphStart(playable);
    }
}
1 Like