Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save georgejecook/71b57ebd1f78593831a48e0af4eb4172 to your computer and use it in GitHub Desktop.
Save georgejecook/71b57ebd1f78593831a48e0af4eb4172 to your computer and use it in GitHub Desktop.
/***********************************************
* Copyright ? Far-Flung Creations Ltd.
* Author: Marius George
* Date: 25 October 2017
* Email: [email protected]
* DISCLAIMER: THE SOURCE CODE IN THIS FILE IS PROVIDED ?AS IS? AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL FAR-FLUNG CREATIONS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD
* PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************/
using System;
using UnityEngine;
using UnityEngine.Playables;
namespace Util.Playables.TimelineEvents
{
[Serializable]
public class TimelineEventBehaviour : PlayableBehaviour
{
public string ClipEventHandler_ClipStart;
public string ClipEventHandler_ClipEnd;
public string TrackEventHandler_ClipStart;
public string TrackEventHandler_ClipEnd;
public bool EnableTrackEvents;
public bool StartSingleArgMethods;
public bool EndSingleArgMethods;
public bool EnableClipEvents;
public bool InvokeEventsInEditMode;
public GameObject TargetObject;
public GameObject TrackTargetObject;
public float ClipStartTime;
public float ClipEndTime;
private Action _clipAction_ClipStart;
private Action _clipAction_ClipEnd;
private Action<string> _trackAction_ClipStart_arg;
private Action<string> _trackAction_ClipEnd_arg;
private Action _trackAction_ClipStart;
private Action _trackAction_ClipEnd;
private string _clipEventHandler_ClipStart;
private string _clipEventHandler_ClipEnd;
private string _trackEventHandler_ClipStart;
private string _trackEventHandler_ClipEnd;
public string StartArgValue;
public string EndArgValue;
private bool isPlayheadInside;
public override void OnGraphStart(Playable playable)
{
}
public override void OnGraphStop(Playable playable)
{
}
public override void PrepareFrame(Playable playable, FrameData info)
{
UpdateDelegates();
base.PrepareFrame(playable, info);
}
private void UpdateDelegates()
{
bool enableByMode = Application.isPlaying || InvokeEventsInEditMode;
UpdateDelegate(EnableClipEvents && enableByMode, ref _clipEventHandler_ClipStart,
ClipEventHandler_ClipStart,
TargetObject, ref _clipAction_ClipStart);
UpdateDelegate(EnableClipEvents && enableByMode, ref _clipEventHandler_ClipEnd, ClipEventHandler_ClipEnd,
TargetObject, ref _clipAction_ClipEnd);
if (StartSingleArgMethods)
{
UpdateSingleArgDelegate(EnableTrackEvents && enableByMode, ref _trackEventHandler_ClipStart,
TrackEventHandler_ClipStart,
TrackTargetObject, ref _trackAction_ClipStart_arg);
}
else
{
UpdateDelegate(EnableTrackEvents && enableByMode, ref _trackEventHandler_ClipStart,
TrackEventHandler_ClipStart,
TrackTargetObject, ref _trackAction_ClipStart);
}
if (EndSingleArgMethods)
{
UpdateSingleArgDelegate(EnableTrackEvents && enableByMode, ref _trackEventHandler_ClipEnd,
TrackEventHandler_ClipEnd,
TrackTargetObject, ref _trackAction_ClipEnd_arg);
}
else
{
UpdateDelegate(EnableTrackEvents && enableByMode, ref _trackEventHandler_ClipEnd,
TrackEventHandler_ClipEnd,
TrackTargetObject, ref _trackAction_ClipEnd);
}
}
private void UpdateDelegate(bool enable, ref string currentCallbackMethodName, string newCallbackMethodName,
GameObject targetObject, ref Action targetDelegate)
{
Behaviour targetBehaviour = null;
string methodName = null;
GetBehaviourAndMethod(enable, ref currentCallbackMethodName, newCallbackMethodName, targetObject,
ref targetBehaviour, ref methodName);
if (targetBehaviour != null)
{
targetDelegate = (Action) Delegate.CreateDelegate(typeof(Action), targetBehaviour, methodName);
}
}
private void GetBehaviourAndMethod(bool enable, ref string currentCallbackMethodName,
string newCallbackMethodName,
GameObject targetObject, ref Behaviour targetBehaviour, ref string methodName)
{
if ((!enable) || string.IsNullOrEmpty(newCallbackMethodName) || (newCallbackMethodName.ToLower() == "none"))
{
return;
}
if ((currentCallbackMethodName != newCallbackMethodName) && (!string.IsNullOrEmpty(newCallbackMethodName)))
{
currentCallbackMethodName = newCallbackMethodName;
int splitIndex = newCallbackMethodName.LastIndexOf('.');
string typeName = newCallbackMethodName.Substring(0, splitIndex);
methodName =
newCallbackMethodName.Substring(splitIndex + 1, newCallbackMethodName.Length - (splitIndex + 1));
if (string.IsNullOrEmpty(typeName) || string.IsNullOrEmpty(methodName))
{
throw new Exception("Unable to parse callback method: " + newCallbackMethodName);
}
targetBehaviour = null;
foreach (var behaviour in targetObject.GetComponents<MonoBehaviour>())
{
if (typeName == behaviour.GetType().ToString())
{
targetBehaviour = behaviour;
break;
}
}
if (targetBehaviour == null)
{
throw new Exception("Unable to find target behaviour: " + typeName);
}
}
}
private void UpdateSingleArgDelegate(bool enable, ref string currentCallbackMethodName,
string newCallbackMethodName,
GameObject targetObject, ref Action<string> targetDelegate)
{
Behaviour targetBehaviour = null;
string methodName = null;
GetBehaviourAndMethod(enable, ref currentCallbackMethodName, newCallbackMethodName, targetObject,
ref targetBehaviour, ref methodName);
if (targetBehaviour != null)
{
targetDelegate =
(Action<string>) Delegate.CreateDelegate(typeof(Action<string>), targetBehaviour, methodName);
}
}
public void OnEnter()
{
if (!isPlayheadInside)
{
if (StartSingleArgMethods)
{
_trackAction_ClipStart_arg?.Invoke(StartArgValue);
}
else
{
_trackAction_ClipStart?.Invoke();
}
_clipAction_ClipStart?.Invoke();
}
isPlayheadInside = true;
}
public void OnExit()
{
if (isPlayheadInside)
{
if (StartSingleArgMethods)
{
_trackAction_ClipEnd_arg?.Invoke(EndArgValue);
}
else
{
_trackAction_ClipEnd?.Invoke();
}
_clipAction_ClipEnd?.Invoke();
}
isPlayheadInside = false;
}
}
}
/***********************************************
* Copyright ? Far-Flung Creations Ltd.
* Author: Marius George
* Date: 25 October 2017
* Email: [email protected]
* DISCLAIMER: THE SOURCE CODE IN THIS FILE IS PROVIDED ?AS IS? AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL FAR-FLUNG CREATIONS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD
* PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************/
using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace Util.Playables.TimelineEvents
{
[Serializable]
public class TimelineEventClip : PlayableAsset, ITimelineClipAsset
{
public TimelineEventBehaviour template = new TimelineEventBehaviour();
public ExposedReference<GameObject> TargetObject;
public GameObject TrackTargetObject { get; set; }
public float ClipStartTime { get; set; }
public float ClipEndTime { get; set; }
public string StartArgValue { get; set; }
public string EndArgValue { get; set; }
public ClipCaps clipCaps
{
get { return ClipCaps.None; }
}
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
{
var playable = ScriptPlayable<TimelineEventBehaviour>.Create(graph, template);
TimelineEventBehaviour clone = playable.GetBehaviour();
clone.TargetObject = TargetObject.Resolve(graph.GetResolver());
clone.ClipStartTime = ClipStartTime;
clone.ClipEndTime = ClipEndTime;
clone.TrackTargetObject = TrackTargetObject;
clone.StartArgValue = StartArgValue;
clone.EndArgValue = EndArgValue;
return playable;
}
}
}
#if UNITY_EDITOR
/***********************************************
* Copyright ? Far-Flung Creations Ltd.
* Author: Marius George
* Date: 25 October 2017
* Email: [email protected]
* DISCLAIMER: THE SOURCE CODE IN THIS FILE IS PROVIDED ?AS IS? AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL FAR-FLUNG CREATIONS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD
* PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Sirenix.Utilities;
using UnityEditor;
using UnityEngine;
namespace Util.Playables.TimelineEvents.Editor
{
[CustomPropertyDrawer(typeof(TimelineEventBehaviour))]
public class TimelineEventDrawer : PropertyDrawer
{
private List<string> _eventHandlerListStart = new List<string> {"None"};
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
int fieldCount = 1;
return fieldCount * EditorGUIUtility.singleLineHeight;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
SerializedProperty clipEventHandlerStartProperty =
property.FindPropertyRelative("ClipEventHandler_ClipStart");
SerializedProperty clipEventHandlerEndProperty = property.FindPropertyRelative("ClipEventHandler_ClipEnd");
SerializedProperty trackEventHandlerStartProperty =
property.FindPropertyRelative("TrackEventHandler_ClipStart");
SerializedProperty trackEventHandlerEndProperty =
property.FindPropertyRelative("TrackEventHandler_ClipEnd");
SerializedProperty enableTrackEventsProperty = property.FindPropertyRelative("EnableTrackEvents");
SerializedProperty enableClipEventsProperty = property.FindPropertyRelative("EnableClipEvents");
SerializedProperty startSingleArgsProperty = property.FindPropertyRelative("StartSingleArgMethods");
SerializedProperty endSingleArgsProperty = property.FindPropertyRelative("EndSingleArgMethods");
SerializedProperty invokeEventsInEditModeProperty = property.FindPropertyRelative("InvokeEventsInEditMode");
SerializedProperty startArgProperty = property.FindPropertyRelative("StartArgValue");
SerializedProperty endArgProperty = property.FindPropertyRelative("EndArgValue");
TimelineEventClip clip = property.serializedObject.targetObject as TimelineEventClip;
GameObject gameObject =
clip.TargetObject.Resolve(property.serializedObject.context as IExposedPropertyTable);
bool hasEvents;
EditorGUILayout.Space();
EditorGUILayout.HelpBox("Call event handlers on the GameObject bound to the track.", MessageType.Info);
enableTrackEventsProperty.boolValue =
EditorGUILayout.BeginToggleGroup("Track Events", enableTrackEventsProperty.boolValue);
if (enableTrackEventsProperty.boolValue)
{
if (clip.TrackTargetObject == null)
{
EditorGUILayout.HelpBox(
"There is currently no GameObject bound to the track. Drag a GameObject from the scene into the field to the left of the track.",
MessageType.Warning);
}
startSingleArgsProperty.boolValue =
EditorGUILayout.BeginToggleGroup("Use Start argument?", startSingleArgsProperty.boolValue);
if (startSingleArgsProperty.boolValue)
{
EditorGUILayout.PropertyField(startArgProperty);
}
EditorGUILayout.EndToggleGroup();
hasEvents = AddMethodsPopup("At Clip Start", trackEventHandlerStartProperty, clip.TrackTargetObject,
startSingleArgsProperty.boolValue);
endSingleArgsProperty.boolValue =
EditorGUILayout.BeginToggleGroup("Use End argument?", endSingleArgsProperty.boolValue);
EditorGUILayout.EndToggleGroup();
if (endSingleArgsProperty.boolValue)
{
EditorGUILayout.PropertyField(endArgProperty);
}
AddMethodsPopup("At Clip End", trackEventHandlerEndProperty, clip.TrackTargetObject,
endSingleArgsProperty.boolValue);
if (!hasEvents)
{
EditorGUILayout.HelpBox(
"Unable to find any event handlers. The target GameObject must have at least one MonoBehaviour with one or more public parameterless methods to serve as event handlers.",
MessageType.Warning);
}
}
EditorGUILayout.EndToggleGroup();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.HelpBox("Call event handlers on the GameObject bound to this clip.", MessageType.Info);
enableClipEventsProperty.boolValue =
EditorGUILayout.BeginToggleGroup("Clip Events", enableClipEventsProperty.boolValue);
if (enableClipEventsProperty.boolValue)
{
if (clip.TrackTargetObject == null)
{
EditorGUILayout.HelpBox(
"There is currently no GameObject bound to this clip. Drag a GameObject from the scene into the Target Object field.",
MessageType.Warning);
}
hasEvents = AddMethodsPopup("At Clip Start", clipEventHandlerStartProperty, gameObject);
AddMethodsPopup("At Clip End", clipEventHandlerEndProperty, gameObject);
if (!hasEvents)
{
EditorGUILayout.HelpBox(
"Unable to find any event handlers. The target GameObject must have at least one MonoBehaviour with one or more public parameterless methods to serve as event handlers.",
MessageType.Warning);
}
}
EditorGUILayout.EndToggleGroup();
EditorGUILayout.Space();
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"Call event handlers while in Edit Mode, by dragging the time marker in the Timeline window.",
MessageType.Info);
invokeEventsInEditModeProperty.boolValue = EditorGUILayout.BeginToggleGroup("Invoke Events in Edit Mode",
invokeEventsInEditModeProperty.boolValue);
if (invokeEventsInEditModeProperty.boolValue)
{
EditorGUILayout.HelpBox(
"PLEASE NOTE! CHANGES MADE TO THE SCENE FROM WITHIN YOUR EVENT HANDLERS WILL BE PERSISTED!",
MessageType.Warning);
}
EditorGUILayout.EndToggleGroup();
}
private bool AddMethodsPopup(string label, SerializedProperty property, GameObject gameObject,
bool listSingleArgMethods = false)
{
if (gameObject == null)
{
return false;
}
MonoBehaviour[] behaviours = gameObject.GetComponents<MonoBehaviour>();
var callbackMethodsEnumarable = behaviours.SelectMany(
x => x.GetType()
.GetMethods(BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance))
.Where(
x =>
{
if (listSingleArgMethods)
{
return (x.ReturnType == typeof(void)) && (x.GetParameters().Length == 1) &&
x.GetParameters()[0].ParameterType == typeof(string);
}
else
{
return (x.ReturnType == typeof(void)) && (x.GetParameters().Length == 0);
}
}).Select(
x => x.DeclaringType.ToString() + "." + x.Name);
if (callbackMethodsEnumarable.Count() == 0)
{
property.stringValue = string.Empty;
return false;
}
string[] callbackMethods = _eventHandlerListStart.Concat(callbackMethodsEnumarable).ToArray();
int index = Array.IndexOf(callbackMethods, property.stringValue);
index = EditorGUILayout.Popup(label, index, callbackMethods, GUILayoutOptions.ExpandWidth(true));
if (index >= 0)
{
property.stringValue = callbackMethods[index];
}
return true;
}
}
}
#endif
/***********************************************
* Copyright ? Far-Flung Creations Ltd.
* Author: Marius George
* Date: 25 October 2017
* Email: [email protected]
* DISCLAIMER: THE SOURCE CODE IN THIS FILE IS PROVIDED ?AS IS? AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL FAR-FLUNG CREATIONS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD
* PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************/
using UnityEngine;
using UnityEngine.Playables;
namespace Util.Playables.TimelineEvents
{
public class TimelineEventMixerBehaviour : PlayableBehaviour
{
private float _lastTime;
// NOTE: This function is called at runtime and edit time. Keep that in mind when setting the values of properties.
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
/*GameObject trackBinding = playerData as GameObject;
if (!trackBinding)
return;*/
var go = playerData as GameObject;
int inputCount = playable.GetInputCount();
float time = (float) playable.GetGraph().GetRootPlayable(0).GetTime();
for (int i = 0; i < inputCount; i++)
{
float inputWeight = playable.GetInputWeight(i);
ScriptPlayable<TimelineEventBehaviour> inputPlayable =
(ScriptPlayable<TimelineEventBehaviour>) playable.GetInput(i);
TimelineEventBehaviour input = inputPlayable.GetBehaviour();
if ((_lastTime <= input.ClipStartTime) && (time > input.ClipStartTime))
{
input.OnEnter();
}
if ((_lastTime <= input.ClipEndTime) && (time > input.ClipEndTime))
{
input.OnExit();
}
}
_lastTime = time;
}
}
}
/***********************************************
* Copyright ? Far-Flung Creations Ltd.
* Author: Marius George
* Date: 25 October 2017
* Email: [email protected]
* DISCLAIMER: THE SOURCE CODE IN THIS FILE IS PROVIDED ?AS IS? AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL FAR-FLUNG CREATIONS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) SUSTAINED BY YOU OR A THIRD
* PARTY, HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* ARISING IN ANY WAY OUT OF THE USE OF THIS SAMPLE CODE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**********************************************/
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;
namespace Util.Playables.TimelineEvents
{
[TrackColor(0.4448276f, 0f, 1f)]
[TrackClipType(typeof(TimelineEventClip))]
[TrackBindingType(typeof(GameObject))]
public class TimelineEventTrack : TrackAsset
{
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
{
var director = go.GetComponent<PlayableDirector>();
var trackTargetObject = director.GetGenericBinding(this) as GameObject;
foreach (var clip in GetClips())
{
var playableAsset = clip.asset as TimelineEventClip;
if (playableAsset)
{
playableAsset.TrackTargetObject = trackTargetObject;
playableAsset.ClipStartTime = (float) clip.start;
playableAsset.ClipEndTime = (float) clip.end;
}
}
var scriptPlayable = ScriptPlayable<TimelineEventMixerBehaviour>.Create(graph, inputCount);
return scriptPlayable;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment