Files

393 lines
17 KiB
C#
Raw Permalink Normal View History

2026-01-08 16:50:20 +00:00
#if CINEMACHINE_TIMELINE
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
using System.Collections.Generic;
using UnityEditor.Timeline;
using UnityEditor.SceneManagement;
namespace Unity.Cinemachine.Editor
{
[CustomEditor(typeof(CinemachineShot))]
class CinemachineShotEditor : UnityEditor.Editor
{
CinemachineShot Target => target as CinemachineShot;
bool m_IsPrefabOrInPrefabMode;
[InitializeOnLoad]
class SyncCacheEnabledSetting
{
static SyncCacheEnabledSetting() => TargetPositionCache.UseCache = CinemachineTimelinePrefs.UseScrubbingCache.Value;
}
public static CinemachineVirtualCameraBase CreatePassiveVcamFromSceneView()
{
var vcam = CinemachineMenu.CreatePassiveCmCamera("CinemachineCamera", null, false);
vcam.StandbyUpdate = CinemachineVirtualCameraBase.StandbyUpdateMode.Never;
#if false
// GML this is too bold. What if timeline is a child of something moving?
// also, SetActive(false) prevents the animator from being able to animate the object
vcam.gameObject.SetActive(false);
var d = TimelineEditor.inspectedDirector;
if (d != null)
Undo.SetTransformParent(vcam.transform, d.transform, "");
#endif
return vcam;
}
private void OnEnable()
{
var director = TimelineEditor.inspectedDirector;
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
m_IsPrefabOrInPrefabMode = director == null;
if (!m_IsPrefabOrInPrefabMode)
{
m_IsPrefabOrInPrefabMode = !PrefabUtility.IsPartOfPrefabInstance(director)
&& (PrefabUtility.IsPartOfPrefabAsset(director)
|| (prefabStage != null && prefabStage.IsPartOfPrefabContents(director.gameObject)));
}
}
#if CINEMACHINE_TIMELINE_1_8_2
VisualElement m_ParentElement;
VisualElement m_CreateButton;
CinemachineVirtualCameraBase m_CachedReferenceObject;
readonly List<MonoBehaviour> m_ComponentsCache = new ();
readonly List<Subeditor> m_Subeditors = new ();
class Subeditor
{
// Keep track of which component types are expanded
static Dictionary<System.Type, bool> s_EditorExpanded = new ();
UnityEditor.Editor m_Editor;
public Object Target { get; private set; }
public Foldout Foldout { get; private set; }
public Subeditor(Object target)
{
Target = target;
// Target can be null for behaviours with missing scripts
if (target == null)
return;
CreateCachedEditor(target, null, ref m_Editor);
// Wrap editor in a foldout
var type = target.GetType();
s_EditorExpanded.TryGetValue(type, out var expanded);
Foldout = new Foldout { text = type.Name, value = expanded, style = { marginTop = 4, marginLeft = 0 }};
Foldout.AddToClassList("clip-inspector-custom-properties__foldout"); // make it pretty
Foldout.Add(new InspectorElement(m_Editor) { style = { paddingLeft = 0, paddingRight = 0 }});
Foldout.RegisterValueChangedCallback((evt) =>
{
if (evt.target == Foldout)
s_EditorExpanded[type] = evt.newValue;
});
Foldout.contentContainer.style.marginLeft = 0; // kill the indent
}
public void Dispose()
{
Foldout?.parent?.Remove(Foldout);
if (m_Editor != null)
DestroyImmediate(m_Editor);
Foldout = null;
m_Editor = null;
Target = null;
}
}
void DestroySubeditors()
{
for (int i = 0; i < m_Subeditors.Count; ++i)
m_Subeditors[i].Dispose();
m_Subeditors.Clear();
}
void OnDisable() => DestroySubeditors();
public override VisualElement CreateInspectorGUI()
{
m_ParentElement = new VisualElement();
// Auto-create shots
var toggle = m_ParentElement.AddChild(new Toggle(CinemachineTimelinePrefs.s_AutoCreateLabel.text)
{
tooltip = CinemachineTimelinePrefs.s_AutoCreateLabel.tooltip,
value = CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value
});
toggle.AddToClassList(InspectorUtility.AlignFieldClassName);
toggle.RegisterValueChangedCallback((evt) => CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value = evt.newValue);
// Cached scrubbing
var row = m_ParentElement.AddChild(new InspectorUtility.LeftRightRow());
row.Left.AddChild(new Label(CinemachineTimelinePrefs.s_ScrubbingCacheLabel.text)
{
tooltip = CinemachineTimelinePrefs.s_ScrubbingCacheLabel.tooltip,
style = { alignSelf = Align.Center, flexGrow = 1 }
});
var cacheToggle = row.Right.AddChild(new Toggle
{
tooltip = CinemachineTimelinePrefs.s_ScrubbingCacheLabel.tooltip,
value = CinemachineTimelinePrefs.UseScrubbingCache.Value,
style = { flexGrow = 0, marginRight = 5 }
});
var clearCacheButton = row.Right.AddChild(new Button
{
text = "Clear",
style = { flexGrow = 0, alignSelf = Align.Center, marginLeft = 5, marginRight = 0 }
});
clearCacheButton.RegisterCallback<ClickEvent>((evt) => TargetPositionCache.ClearCache());
clearCacheButton.SetEnabled(CinemachineTimelinePrefs.UseScrubbingCache.Value);
cacheToggle.RegisterValueChangedCallback((evt) => CinemachineTimelinePrefs.UseScrubbingCache.Value = evt.newValue);
InspectorUtility.ContinuousUpdate(m_ParentElement, () =>
{
cacheToggle.SetEnabled(!Application.isPlaying);
clearCacheButton.SetEnabled(
CinemachineTimelinePrefs.UseScrubbingCache.Value
&& !TargetPositionCache.IsEmpty
&& !Application.isPlaying);
});
m_ParentElement.AddSpace();
if (m_IsPrefabOrInPrefabMode)
m_ParentElement.Add(new HelpBox(
"Only Cinemachine Cameras inside the prefab may be assigned.",
HelpBoxMessageType.Info));
// Camera Reference
var vcamProperty = serializedObject.FindProperty(() => Target.VirtualCamera);
#if true
// we do it in IMGUI until the ExposedReference UITK bugs are fixed
row = m_ParentElement.AddChild(new InspectorUtility.LeftRightRow());
row.Left.AddChild(new Label("Cinemachine Camera")
{
tooltip = "The Cinemachine camera to use for this shot",
style = { alignSelf = Align.Center, flexGrow = 1 }
});
row.Right.Add(new IMGUIContainer(() =>
{
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(vcamProperty, GUIContent.none);
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
}) { style = { flexGrow = 1, marginTop = 2, marginBottom = 3, marginLeft = 2 }} );
#else
row = m_ParentElement.AddChild(InspectorUtility.PropertyRow(vcamProperty, out _, "Cinemachine Camera"));
#endif
m_CreateButton = row.Right.AddChild(new Button(() =>
{
vcamProperty.exposedReferenceValue = CreatePassiveVcamFromSceneView();
vcamProperty.serializedObject.ApplyModifiedProperties();
})
{
text = "Create",
tooltip = "Create a passive Cinemachine camera matching the scene view",
style = { flexGrow = 0, alignSelf = Align.Center, marginLeft = 5, marginBottom = 2, marginRight = 0 }
});
// Display name
m_ParentElement.Add(new PropertyField(serializedObject.FindProperty(() => Target.DisplayName)));
m_ParentElement.AddSpace();
// Component editors
m_ParentElement.TrackAnyUserActivity(UpdateComponentEditors);
return m_ParentElement;
}
void UpdateComponentEditors()
{
if (m_ParentElement == null || serializedObject == null)
return;
var vcamProperty = serializedObject.FindProperty(() => Target.VirtualCamera);
if (vcamProperty == null)
return;
m_CreateButton.SetVisible(!m_IsPrefabOrInPrefabMode && vcamProperty.exposedReferenceValue as CinemachineVirtualCameraBase == null);
var vcam = vcamProperty.exposedReferenceValue as CinemachineVirtualCameraBase;
m_ComponentsCache.Clear();
if (vcam != null)
vcam.GetComponents(m_ComponentsCache);
bool dirty = m_CachedReferenceObject != vcam || m_Subeditors.Count != m_ComponentsCache.Count + 1;
for (int i = 0; !dirty && i < m_ComponentsCache.Count; ++i)
dirty = m_Subeditors[i + 1].Target != m_ComponentsCache[i];
if (dirty)
{
DestroySubeditors();
m_CachedReferenceObject = vcam;
if (vcam != null)
{
m_Subeditors.Add(new Subeditor(vcam.transform));
for (int i = 0; i < m_ComponentsCache.Count; ++i)
m_Subeditors.Add(new Subeditor(m_ComponentsCache[i]));
for (int i = 0; i < m_Subeditors.Count; ++i)
m_ParentElement.Add(m_Subeditors[i].Foldout);
}
}
}
#else // IMGUI VERSION - used for older Timeline versions
readonly GUIContent s_CmCameraLabel = new ("CinemachineCamera", "The Cinemachine camera to use for this shot");
readonly GUIContent m_ClearText = new ("Clear", "Clear the target position scrubbing cache");
void OnDisable() => DestroyComponentEditors();
void OnDestroy() => DestroyComponentEditors();
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUI.indentLevel = 0; // otherwise subeditor layouts get screwed up
CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value = EditorGUILayout.Toggle(
CinemachineTimelinePrefs.s_AutoCreateLabel, CinemachineTimelinePrefs.AutoCreateShotFromSceneView.Value);
Rect rect;
GUI.enabled = !Application.isPlaying;
rect = EditorGUILayout.GetControlRect();
var r = rect;
r.width = EditorGUIUtility.labelWidth + EditorGUIUtility.singleLineHeight;
if (Application.isPlaying)
EditorGUI.Toggle(r, CinemachineTimelinePrefs.s_ScrubbingCacheLabel, false);
else
CinemachineTimelinePrefs.UseScrubbingCache.Value = EditorGUI.Toggle(
r, CinemachineTimelinePrefs.s_ScrubbingCacheLabel, CinemachineTimelinePrefs.UseScrubbingCache.Value);
r.x += r.width + 6; r.width = GUI.skin.button.CalcSize(m_ClearText).x;
GUI.enabled &= !TargetPositionCache.IsEmpty;
if (GUI.Button(r, m_ClearText))
TargetPositionCache.ClearCache();
GUI.enabled = true;
EditorGUILayout.Space();
if (m_IsPrefabOrInPrefabMode)
EditorGUILayout.HelpBox(
"Only Cinemachine Cameras inside the prefab may be assigned, and the Property must be Exposed.",
MessageType.Info);
var vcamProperty = serializedObject.FindProperty(() => Target.VirtualCamera);
CinemachineVirtualCameraBase vcam = vcamProperty.exposedReferenceValue as CinemachineVirtualCameraBase;
if (m_IsPrefabOrInPrefabMode || vcam != null)
EditorGUILayout.PropertyField(vcamProperty, s_CmCameraLabel);
else
{
GUIContent createLabel = new GUIContent("Create");
Vector2 createSize = GUI.skin.button.CalcSize(createLabel);
rect = EditorGUILayout.GetControlRect(true);
rect.width -= createSize.x;
EditorGUI.PropertyField(rect, vcamProperty, s_CmCameraLabel);
rect.x += rect.width; rect.width = createSize.x;
if (GUI.Button(rect, createLabel))
{
vcam = CreatePassiveVcamFromSceneView();
vcamProperty.exposedReferenceValue = vcam;
}
serializedObject.ApplyModifiedProperties();
}
EditorGUI.BeginChangeCheck();
EditorGUILayout.PropertyField(serializedObject.FindProperty(() => Target.DisplayName));
if (EditorGUI.EndChangeCheck())
serializedObject.ApplyModifiedProperties();
EditorGUILayout.Space();
EditorGUILayout.HelpBox(
"For best inspector display, please upgrade Timeline to version 1.8.2 or later",
MessageType.Info);
EditorGUI.BeginChangeCheck();
if (vcam != null)
DrawSubeditors(vcam);
// by default timeline rebuilds the entire graph when something changes,
// but if a property of the virtual camera changes, we only need to re-evaluate the timeline.
// this prevents flicker on post processing updates
if (EditorGUI.EndChangeCheck())
{
UnityEditor.Timeline.TimelineEditor.Refresh(UnityEditor.Timeline.RefreshReason.SceneNeedsUpdate);
GUI.changed = false;
}
}
void DrawSubeditors(CinemachineVirtualCameraBase vcam)
{
// Create an editor for each of the cinemachine virtual cam and its components
GUIStyle foldoutStyle = new GUIStyle(EditorStyles.foldout) { fontStyle = FontStyle.Bold };
UpdateComponentEditors(vcam);
if (m_editors != null)
{
foreach (UnityEditor.Editor e in m_editors)
{
if (e == null || e.target == null || (e.target.hideFlags & HideFlags.HideInInspector) != 0)
continue;
// Separator line - how do you make a thinner one?
GUILayout.Box("", new GUILayoutOption[] { GUILayout.ExpandWidth(true), GUILayout.Height(1) } );
bool expanded = true;
if (!s_EditorExpanded.TryGetValue(e.target.GetType(), out expanded))
expanded = true;
expanded = EditorGUILayout.Foldout(
expanded, e.target.GetType().Name, true, foldoutStyle);
if (expanded)
e.OnInspectorGUI();
s_EditorExpanded[e.target.GetType()] = expanded;
}
}
}
CinemachineVirtualCameraBase m_cachedReferenceObject;
UnityEditor.Editor[] m_editors = null;
static Dictionary<System.Type, bool> s_EditorExpanded = new();
void UpdateComponentEditors(CinemachineVirtualCameraBase obj)
{
MonoBehaviour[] components = null;
if (obj != null)
components = obj.gameObject.GetComponents<MonoBehaviour>();
int numComponents = (components == null) ? 0 : components.Length;
int numEditors = (m_editors == null) ? 0 : m_editors.Length;
if (m_cachedReferenceObject != obj || (numComponents + 1) != numEditors)
{
DestroyComponentEditors();
m_cachedReferenceObject = obj;
if (obj != null)
{
m_editors = new UnityEditor.Editor[components.Length + 1];
CreateCachedEditor(obj.gameObject.GetComponent<Transform>(), null, ref m_editors[0]);
for (int i = 0; i < components.Length; ++i)
CreateCachedEditor(components[i], null, ref m_editors[i + 1]);
}
}
}
void DestroyComponentEditors()
{
m_cachedReferenceObject = null;
if (m_editors != null)
{
for (int i = 0; i < m_editors.Length; ++i)
{
if (m_editors[i] != null)
Object.DestroyImmediate(m_editors[i]);
m_editors[i] = null;
}
m_editors = null;
}
}
#endif
}
}
#endif