using UnityEngine;
using UnityEditor;
using System;
using System.Collections.Generic;
using UnityEngine.UIElements;
using System.Reflection;
using UnityEditor.UIElements;
namespace Unity.Cinemachine.Editor
{
///
/// Helpers for drawing CinemachineCamera inspectors.
///
static class CmCameraInspectorUtility
{
struct PipelineStageItem
{
public CinemachineCore.Stage Stage;
public DropdownField Dropdown;
public Label WarningIcon;
}
static bool IsPrefab(UnityEngine.Object target)
{
var t = target as CinemachineVirtualCameraBase;
return t != null && PrefabUtility.IsPartOfPrefabAsset(t);
}
static Color s_NormalColor = Color.black;
static Color s_NormalBkgColor = Color.black;
/// Add the camera status controls and indicators in the inspector
public static void AddCameraStatus(this UnityEditor.Editor editor, VisualElement ux)
{
// No status and Solo for prefabs or multi-select
if (Selection.objects.Length > 1 || IsPrefab(editor.target))
return;
var cameraParentingMessage = ux.AddChild(new HelpBox(
$"Setup error: {editor.target.GetType().Name} should not be a child "
+ "of CinemachineCamera or CinemachineBrain.\n\n"
+ "Best practice is to have CinemachineCamera, CinemachineBrain, and camera targets as "
+ "separate objects, not parented to each other.",
HelpBoxMessageType.Error));
var noBrainMessage = ux.AddChild(InspectorUtility.HelpBoxWithButton(
"A CinemachineBrain is required in the scene.",
HelpBoxMessageType.Warning, "Add Brain", () => CinemachineMenu.GetOrCreateBrain()));
var navelGazeMessage = ux.AddChild(new HelpBox(
"The camera is trying to look at itself.",
HelpBoxMessageType.Warning));
var row = ux.AddChild(new InspectorUtility.LabeledRow("Status"));
var statusText = row.Label;
var soloButton = row.Contents.AddChild(new Button()
{
text = "Solo",
style = { flexGrow = 1, paddingLeft = 0, paddingRight = 0,
marginLeft = 0, marginRight = 0, borderLeftWidth = 1, borderRightWidth = 1 }
});
var updateMode = row.Contents.AddChild(new Label("(Update Mode)") { style = { flexGrow = 0, alignSelf = Align.Center }});
updateMode.SetEnabled(false);
updateMode.style.display = DisplayStyle.None;
var target = editor.target as CinemachineVirtualCameraBase; // capture for lambda
soloButton.RegisterCallback(_ =>
{
var isSolo = CinemachineCore.SoloCamera != (ICinemachineCamera)target;
CinemachineCore.SoloCamera = isSolo ? target : null;
InspectorUtility.RepaintGameView();
});
ux.TrackAnyUserActivity(() =>
{
if (target == null)
return;
// Is the camera navel-gazing?
CameraState state = target.State;
bool isNavelGazing = target.PreviousStateIsValid && state.HasLookAt() &&
(state.ReferenceLookAt - state.GetCorrectedPosition()).AlmostZero();
if (isNavelGazing)
{
var aim = target.GetCinemachineComponent(CinemachineCore.Stage.Aim);
if (aim == null || !aim.CameraLooksAtTarget)
isNavelGazing = false;
}
navelGazeMessage.SetVisible(isNavelGazing);
// Is the camera parenting incorrect?
cameraParentingMessage.SetVisible(
target.GetComponentInParent() != null
|| (target.ParentCamera != null && target.ParentCamera is not CinemachineCameraManagerBase));
// Is there a Brain?
noBrainMessage.SetVisible(CinemachineBrain.ActiveBrainCount == 0 && !IsPrefab(target));
});
// Capture "normal" colors
if (s_NormalBkgColor == Color.black)
{
ux.OnInitialGeometry(() =>
{
s_NormalColor = statusText.resolvedStyle.color;
s_NormalBkgColor = soloButton.resolvedStyle.backgroundColor;
});
}
// Refresh camera state
ux.ContinuousUpdate(() =>
{
if (target == null)
return;
bool isSolo = CinemachineCore.SoloCamera == (ICinemachineCamera)target;
var color = isSolo ? Color.Lerp(s_NormalColor, CinemachineCore.SoloGUIColor(), 0.5f) : s_NormalColor;
bool isLive = CinemachineCore.IsLive(target);
statusText.text = isLive ? "Status: Live"
: target.isActiveAndEnabled ? "Status: Standby" : "Status: Disabled";
statusText.SetEnabled(isLive);
statusText.style.color = color;
if (!Application.isPlaying)
updateMode.SetVisible(false);
else
{
var mode = CameraUpdateManager.GetVcamUpdateStatus(target);
updateMode.text = mode == UpdateTracker.UpdateClock.Fixed ? " Fixed Update" : " Late Update";
updateMode.SetVisible(true);
}
soloButton.style.color = color;
soloButton.style.backgroundColor = isSolo
? Color.Lerp(s_NormalBkgColor, CinemachineCore.SoloGUIColor(), 0.2f) : s_NormalBkgColor;
// Refresh the game view if solo and not playing
if (isSolo && !Application.isPlaying)
InspectorUtility.RepaintGameView();
});
// Kill solo when inspector shuts down
ux.RegisterCallback(_ =>
{
if (target != null && CinemachineCore.SoloCamera == (ICinemachineCamera)target)
{
CinemachineCore.SoloCamera = null;
InspectorUtility.RepaintGameView();
}
});
}
public static void AddTransitionsSection(
this UnityEditor.Editor editor, VisualElement ux,
List otherProperties = null)
{
var serializedObject = editor.serializedObject;
var target = editor.target as CinemachineVirtualCameraBase;
ux.Add(new PropertyField(serializedObject.FindProperty(() => target.Priority)));
ux.Add(new PropertyField(serializedObject.FindProperty(() => target.OutputChannel)));
ux.Add(new PropertyField(serializedObject.FindProperty(() => target.StandbyUpdate)));
for (int i = 0; otherProperties != null && i < otherProperties.Count; ++i)
ux.Add(new PropertyField(otherProperties[i]));
}
/// Add the pipeline control dropdowns in the inspector
public static void AddPipelineDropdowns(this UnityEditor.Editor editor, VisualElement ux)
{
var target = editor.target as CinemachineCamera;
if (target == null)
return;
var targets = editor.targets; // capture for lambda
// Add a dropdown for each pipeline stage
var pipelineItems = new List();
for (int i = 0; i < PipelineStageMenu.s_StageData.Length; ++i)
{
// Skip empty categories
if (PipelineStageMenu.s_StageData[i].Types.Count < 2)
continue;
var stage = i; // capture for lambda
var row = ux.AddChild(new InspectorUtility.LeftRightRow());
row.Left.Add(new Label(PipelineStageMenu.s_StageData[stage].Name)
{
tooltip = "Will add a Behaviour to implement this stage in the procedural pipeline",
style = { flexGrow = 1, alignSelf = Align.Center }
});
var warningIcon = row.Left.AddChild(InspectorUtility.MiniHelpIcon("Component is disabled or has a problem"));
warningIcon.SetVisible(false);
int currentSelection = PipelineStageMenu.GetSelectedComponent(
i, target.GetCinemachineComponent((CinemachineCore.Stage)i));
var dropdown = row.Right.AddChild(new DropdownField
{
choices = PipelineStageMenu.s_StageData[stage].Choices,
index = currentSelection,
style = { flexGrow = 1 }
});
dropdown.RegisterValueChangedCallback(evt =>
{
var newType = PipelineStageMenu.s_StageData[stage].Types[GetTypeIndexFromSelection(evt.newValue, stage)];
for (int j = 0; j < targets.Length; j++)
{
var t = targets[j] as CinemachineCamera;
if (t == null)
continue;
var oldComponents = t.GetComponents();
CinemachineComponentBase existingComponent = null;
for (int k = 0; k < oldComponents.Length; ++k)
{
if (existingComponent == null && oldComponents[k].GetType() == newType)
existingComponent = oldComponents[k];
else if (oldComponents[k].Stage == (CinemachineCore.Stage)stage)
Undo.DestroyObjectImmediate(oldComponents[k]);
}
if (newType != null && existingComponent == null)
Undo.AddComponent(t.gameObject, newType);
t.InvalidatePipelineCache();
}
static int GetTypeIndexFromSelection(string selection, int stage)
{
for (var j = 0; j < PipelineStageMenu.s_StageData[stage].Choices.Count; ++j)
if (PipelineStageMenu.s_StageData[stage].Choices[j].Equals(selection))
return j;
return 0;
}
});
pipelineItems.Add(new PipelineStageItem
{
Stage = (CinemachineCore.Stage)i,
Dropdown = dropdown,
WarningIcon = warningIcon
});
}
ux.TrackAnyUserActivity(() =>
{
if (target == null)
return; // deleted
target.InvalidatePipelineCache();
for (int i = 0; i < pipelineItems.Count; ++i)
{
var item = pipelineItems[i];
var c = target.GetCinemachineComponent(item.Stage);
int selection = PipelineStageMenu.GetSelectedComponent((int)item.Stage, c);
item.Dropdown.value = PipelineStageMenu.s_StageData[(int)item.Stage].Choices[selection];
item.WarningIcon.SetVisible(c != null && !c.IsValid);
}
});
}
/// Draw the Extensions dropdown in the inspector
public static void AddExtensionsDropdown(this UnityEditor.Editor editor, VisualElement ux)
{
var row = new InspectorUtility.LabeledRow(
"Add Extension", "Extensions are behaviours that inject themselves into "
+ "the Cinemachine pipeline to alter the camera's behaviour. "
+ "This dropdown will add the selected extension behaviour.");
var menu = new ContextualMenuManipulator((evt) =>
{
for (int i = 0; i < PipelineStageMenu.s_ExtensionTypes.Count; ++i)
{
var type = PipelineStageMenu.s_ExtensionTypes[i];
if (type == null)
continue;
var name = PipelineStageMenu.s_ExtensionNames[i];
evt.menu.AppendAction(name,
(action) =>
{
var target = editor.target as CinemachineVirtualCameraBase;
Undo.AddComponent(target.gameObject, type);
},
(status) =>
{
var target = editor.target as CinemachineVirtualCameraBase;
var disable = target == null || target.GetComponent(type) != null;
return disable ? DropdownMenuAction.Status.Disabled : DropdownMenuAction.Status.Normal;
}
);
}
});
var button = row.Contents.AddChild(new Button
{
text = "(select)",
style =
{
flexGrow = 1, marginRight = 0, marginLeft = 3,
paddingTop = 0, paddingBottom = 0, paddingLeft = 1,
height = InspectorUtility.SingleLineHeight + 2,
unityTextAlign = TextAnchor.MiddleLeft
}
});
menu.activators.Clear();
menu.activators.Add(new ManipulatorActivationFilter { button = MouseButton.LeftMouse });
button.AddManipulator(menu);
ux.Add(row);
}
[InitializeOnLoad]
static class PipelineStageMenu
{
// Pipeline stages
public struct StageData
{
public CinemachineCore.Stage Stage;
public string Name;
public List Types; // first entry is null - this array is synched with PopupOptions
public List Choices;
}
public static StageData[] s_StageData = null;
// Extensions
public static List s_ExtensionTypes;
public static List s_ExtensionNames;
public static int GetSelectedComponent(int stage, CinemachineComponentBase component)
{
if (component != null)
for (int j = 0; j < s_StageData[stage].Choices.Count; ++j)
if (s_StageData[stage].Types[j] == component.GetType())
return j;
return 0;
}
// This code dynamically discovers eligible classes and builds the menu
// data for the various component pipeline stages.
static PipelineStageMenu()
{
s_StageData = new StageData[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
for (int i = 0; i < s_StageData.Length; ++i)
{
var stage = (CinemachineCore.Stage)i;
s_StageData[i] = new StageData
{
Stage = stage,
Name = stage == CinemachineCore.Stage.Body ? "Position Control"
: stage == CinemachineCore.Stage.Aim ? "Rotation Control"
: ObjectNames.NicifyVariableName(stage.ToString()),
Types = new List() { null }, // first item is "None"
Choices = new List() { "None" }
};
}
// Get all CinemachineComponentBase
var allTypes = ReflectionHelpers.GetTypesInAllDependentAssemblies((Type t) =>
typeof(CinemachineComponentBase).IsAssignableFrom(t) && !t.IsAbstract
&& t.GetCustomAttribute() != null
&& t.GetCustomAttribute() == null);
var iter = allTypes.GetEnumerator();
while (iter.MoveNext())
{
var t = iter.Current;
var stage = (int)t.GetCustomAttribute().Stage;
s_StageData[stage].Types.Add(t);
s_StageData[stage].Choices.Add(InspectorUtility.NicifyClassName(t));
}
// Populate the extension list
s_ExtensionTypes = new List();
s_ExtensionNames = new List();
s_ExtensionTypes.Add(null);
s_ExtensionNames.Add("(select)");
var allExtensions
= ReflectionHelpers.GetTypesInAllDependentAssemblies(
(Type t) => typeof(CinemachineExtension).IsAssignableFrom(t)
&& !t.IsAbstract && t.GetCustomAttribute() == null);
var iter2 = allExtensions.GetEnumerator();
while (iter2.MoveNext())
{
var t = iter2.Current;
s_ExtensionTypes.Add(t);
s_ExtensionNames.Add(t.Name);
}
}
}
/// Draw the global settings controls in the inspector
public static void AddGlobalControls(this UnityEditor.Editor editor, VisualElement ux)
{
var helpBox = ux.AddChild(new HelpBox("CinemachineCamera settings changes made during Play Mode will be "
+ "propagated back to the scene when Play Mode is exited.",
HelpBoxMessageType.Info));
helpBox.SetVisible(SaveDuringPlay.Enabled && Application.isPlaying);
var toggle = ux.AddChild(new Toggle(CinemachineCorePrefs.s_SaveDuringPlayLabel.text)
{
tooltip = CinemachineCorePrefs.s_SaveDuringPlayLabel.tooltip,
value = SaveDuringPlay.Enabled
});
toggle.AddToClassList(InspectorUtility.kAlignFieldClass);
toggle.RegisterValueChangedCallback((evt) =>
{
SaveDuringPlay.Enabled = evt.newValue;
helpBox.SetVisible(evt.newValue && Application.isPlaying);
});
var choices = new List() { "Disabled", "Passive", "Interactive" };
int index = CinemachineCorePrefs.ShowInGameGuides.Value
? (CinemachineCorePrefs.DraggableComposerGuides.Value ? 2 : 1) : 0;
var dropdown = ux.AddChild(new DropdownField("Game View Guides")
{
tooltip = CinemachineCorePrefs.s_ShowInGameGuidesLabel.tooltip,
choices = choices,
index = index,
style = { flexGrow = 1 }
});
dropdown.AddToClassList(InspectorUtility.kAlignFieldClass);
dropdown.RegisterValueChangedCallback((evt) =>
{
CinemachineCorePrefs.ShowInGameGuides.Value = evt.newValue != choices[0];
CinemachineCorePrefs.DraggableComposerGuides.Value = evt.newValue == choices[2];
InspectorUtility.RepaintGameView();
});
}
static List s_componentCache = new ();
enum SortOrder { None, Camera, Pipeline, Extensions = CinemachineCore.Stage.Finalize + 1, Other };
///
/// This is only for aesthetics, sort order does not affect camera logic.
/// Behaviours should be sorted like this:
/// CinemachineCamera, Body, Aim, Noise, Finalize, Extensions, everything else.
///
public static void SortComponents(CinemachineVirtualCameraBase target)
{
if (target == null || PrefabUtility.IsPartOfNonAssetPrefabInstance(target))
return; // target was deleted or is part of a prefab instance
SortOrder lastItem = SortOrder.None;
bool sortNeeded = false;
target.gameObject.GetComponents(s_componentCache);
for (int i = 0; i < s_componentCache.Count && !sortNeeded; ++i)
{
var current = GetSortOrderForComponent(s_componentCache[i]);
if (current < lastItem)
sortNeeded = true;
lastItem = current;
}
if (sortNeeded)
{
// This is painful, but it won't happen too often
var pos = 0;
if (MoveComponentToPosition(pos, SortOrder.Camera, s_componentCache)) ++pos;
if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Body, s_componentCache)) ++pos;
if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Aim, s_componentCache)) ++pos;
if (MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Noise, s_componentCache)) ++pos;
MoveComponentToPosition(pos, SortOrder.Pipeline + (int)CinemachineCore.Stage.Finalize, s_componentCache);
// leave everything else where it is
}
SortOrder GetSortOrderForComponent(MonoBehaviour component)
{
if (component is CinemachineVirtualCameraBase)
return SortOrder.Camera;
if (component is CinemachineExtension)
return SortOrder.Extensions;
if (component is CinemachineComponentBase)
return SortOrder.Pipeline + (int)(component as CinemachineComponentBase).Stage;
return SortOrder.Other;
}
// Returns true if item exists. Will re-sort components if something changed.
bool MoveComponentToPosition(int pos, SortOrder item, List components)
{
for (int i = pos; i < components.Count; ++i)
{
var component = components[i];
if (GetSortOrderForComponent(component) == item)
{
for (int j = i; j > pos; --j)
UnityEditorInternal.ComponentUtility.MoveComponentUp(component);
if (i > pos)
component.gameObject.GetComponents(components);
return true;
}
}
return false;
}
}
///
/// Use this delegate to control the display of warning icons next to the child cameras
///
public delegate string GetChildWarningMessageDelegate(object childObject);
/// If camera is a CinemachineCameraManagerBase, draw the Child camera list
public static void AddChildCameras(
this UnityEditor.Editor editor, VisualElement ux,
GetChildWarningMessageDelegate getChildWarning)
{
var vcam = editor.target as CinemachineCameraManagerBase;
if (vcam == null)
return;
var floatFieldWidth = EditorGUIUtility.singleLineHeight * 2.5f;
var helpBox = ux.AddChild(new HelpBox(
"Child Cameras cannot be displayed when multiple objects are selected.",
HelpBoxMessageType.Info));
var container = ux.AddChild(new VisualElement());
var header = container.AddChild(new VisualElement { style = { flexDirection = FlexDirection.Row, marginBottom = -2 } });
header.AddToClassList("unity-collection-view--with-border");
header.AddChild(new Label("Child Cameras") { style = { marginLeft = 3, flexGrow = 1, flexBasis = 10 }});
header.AddChild(new Label("Priority")
{ style = { marginRight = 4, flexGrow = 1, flexBasis = floatFieldWidth, unityTextAlign = TextAnchor.MiddleRight }});
var list = container.AddChild(new ListView()
{
reorderable = false,
showAddRemoveFooter = true,
showBorder = true,
showBoundCollectionSize = false,
showFoldoutHeader = false,
style = { borderTopWidth = 0 },
});
list.itemsSource = vcam.ChildCameras;
list.makeItem = () => new VisualElement { style = { flexDirection = FlexDirection.Row }};
list.bindItem = (row, index) =>
{
// Remove children - items seem to get recycled
for (int i = row.childCount - 1; i >= 0; --i)
row.RemoveAt(i);
var element = list.itemsSource[index] as CinemachineVirtualCameraBase;
row.AddChild(new ObjectField
{
value = element,
objectType = typeof(CinemachineVirtualCameraBase),
style = { flexBasis = 20, flexGrow = 1 }
}).SetEnabled(false);
if (element == null)
return;
var warningIcon = row.AddChild(InspectorUtility.MiniHelpIcon("Item is null"));
var warningText = getChildWarning == null ? string.Empty : getChildWarning(element);
warningIcon.tooltip = warningText;
warningIcon.SetVisible(!string.IsNullOrEmpty(warningText));
var dragger = row.AddChild(new Label(" "));
dragger.AddToClassList("unity-base-field__label--with-dragger");
var so = new SerializedObject(element);
var prop = so.FindProperty("Priority");
var enabledProp = prop.FindPropertyRelative("Enabled");
var priorityProp = prop.FindPropertyRelative("m_Value");
var priorityField = row.AddChild(new IntegerField
{
value = enabledProp.boolValue ? priorityProp.intValue : 0,
style = { flexBasis = floatFieldWidth, flexGrow = 0, marginRight = 4 }
});
new FieldMouseDragger(priorityField).SetDragZone(dragger);
priorityField.RegisterValueChangedCallback((evt) =>
{
if (evt.newValue != 0)
enabledProp.boolValue = true;
priorityProp.intValue = evt.newValue;
so.ApplyModifiedProperties();
});
priorityField.TrackPropertyValue(priorityProp, (p) => priorityField.value = p.intValue);
priorityField.TrackPropertyValue(enabledProp, (p) => priorityField.value = p.boolValue ? priorityProp.intValue : 0);
};
list.itemsAdded += (added) =>
{
var selected = list.selectedIndex;
var selectedCam = (selected >= 0 && selected < list.itemsSource.Count)
? list.itemsSource[selected] as CinemachineVirtualCameraBase : null;
var name = selectedCam != null ? selectedCam.Name : "Child";
var iter = added.GetEnumerator();
while (iter.MoveNext())
CinemachineMenu.CreatePassiveCmCamera(name, vcam.gameObject);
Selection.activeObject = vcam;
vcam.InvalidateCameraCache();
};
list.itemsRemoved += (removed) =>
{
var iter = removed.GetEnumerator();
while (iter.MoveNext())
{
var child = list.itemsSource[iter.Current] as CinemachineVirtualCameraBase;
if (child != null)
Undo.DestroyObjectImmediate(child.gameObject);
}
vcam.InvalidateCameraCache();
};
container.TrackAnyUserActivity(() =>
{
if (editor == null || editor.target == null)
return; // object deleted
var isMultiSelect = editor.targets.Length > 1;
helpBox.SetVisible(isMultiSelect);
container.SetVisible(!isMultiSelect);
// Update child list
if (!isMultiSelect)
{
list.itemsSource = vcam.ChildCameras;
list.Rebuild();
}
});
}
}
}