575 lines
24 KiB
C#
575 lines
24 KiB
C#
using UnityEngine;
|
|
using UnityEditor;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using Cinemachine.Utility;
|
|
using System.Reflection;
|
|
using System.Linq;
|
|
|
|
namespace Cinemachine.Editor
|
|
{
|
|
[CustomEditor(typeof(CinemachineVirtualCamera))]
|
|
[CanEditMultipleObjects]
|
|
internal class CinemachineVirtualCameraEditor
|
|
: CinemachineVirtualCameraBaseEditor<CinemachineVirtualCamera>
|
|
{
|
|
// Static state and caches - Call UpdateStaticData() to refresh this
|
|
struct StageData
|
|
{
|
|
string ExpandedKey { get { return "CNMCN_Core_Vcam_Expanded_" + Name; } }
|
|
public bool IsExpanded
|
|
{
|
|
get { return EditorPrefs.GetBool(ExpandedKey, false); }
|
|
set { EditorPrefs.SetBool(ExpandedKey, value); }
|
|
}
|
|
public string Name;
|
|
public Type[] types; // first entry is null
|
|
public GUIContent[] PopupOptions;
|
|
}
|
|
static StageData[] sStageData = null;
|
|
bool[] m_hasSameStageDataTypes = new bool[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
|
|
// Instance data - call UpdateInstanceData() to refresh this
|
|
int[] m_stageState = null;
|
|
bool[] m_stageError = null;
|
|
CinemachineComponentBase[] m_components;
|
|
UnityEditor.Editor[] m_componentEditors = new UnityEditor.Editor[0];
|
|
bool IsPrefab { get; set; }
|
|
|
|
protected override void OnEnable()
|
|
{
|
|
// Build static menu arrays via reflection
|
|
base.OnEnable();
|
|
IsPrefab = Target.gameObject.scene.name == null; // causes a small GC alloc
|
|
|
|
UpdateStaticData();
|
|
UpdateStageDataTypeMatchesForMultiSelection();
|
|
Undo.undoRedoPerformed += ResetTargetOnUndo;
|
|
}
|
|
|
|
void ResetTargetOnUndo()
|
|
{
|
|
UpdateInstanceData();
|
|
ResetTarget();
|
|
}
|
|
|
|
protected override void OnDisable()
|
|
{
|
|
base.OnDisable();
|
|
Undo.undoRedoPerformed -= ResetTargetOnUndo;
|
|
// Must destroy editors or we get exceptions
|
|
if (m_componentEditors != null)
|
|
foreach (UnityEditor.Editor e in m_componentEditors)
|
|
if (e != null)
|
|
UnityEngine.Object.DestroyImmediate(e);
|
|
}
|
|
|
|
Vector3 mPreviousPosition;
|
|
private void OnSceneGUI()
|
|
{
|
|
if (!Target.UserIsDragging)
|
|
mPreviousPosition = Target.transform.position;
|
|
if (Selection.Contains(Target.gameObject) && Tools.current == Tool.Move
|
|
&& Event.current.type == EventType.MouseDrag)
|
|
{
|
|
// User might be dragging our position handle
|
|
Target.UserIsDragging = true;
|
|
Vector3 delta = Target.transform.position - mPreviousPosition;
|
|
if (!delta.AlmostZero())
|
|
{
|
|
OnPositionDragged(delta);
|
|
mPreviousPosition = Target.transform.position;
|
|
}
|
|
}
|
|
else if (GUIUtility.hotControl == 0 && Target.UserIsDragging)
|
|
{
|
|
// We're not dragging anything now, but we were
|
|
InspectorUtility.RepaintGameView();
|
|
Target.UserIsDragging = false;
|
|
}
|
|
}
|
|
|
|
[MenuItem("CONTEXT/CinemachineVirtualCamera/Adopt Current Camera Settings")]
|
|
static void AdoptCurrentCameraSettings(MenuCommand command)
|
|
{
|
|
var vcam = command.context as CinemachineVirtualCamera;
|
|
var brain = CinemachineCore.Instance.FindPotentialTargetBrain(vcam);
|
|
if (brain != null)
|
|
{
|
|
vcam.m_Lens = brain.CurrentCameraState.Lens;
|
|
vcam.transform.position = brain.transform.position;
|
|
vcam.transform.rotation = brain.transform.rotation;
|
|
}
|
|
}
|
|
|
|
[MenuItem("CONTEXT/CinemachineVirtualCamera/Adopt Scene View Camera Settings")]
|
|
static void AdoptSceneViewCameraSettings(MenuCommand command)
|
|
{
|
|
var vcam = command.context as CinemachineVirtualCamera;
|
|
CinemachineMenu.SetVcamFromSceneView(vcam);
|
|
}
|
|
|
|
void OnPositionDragged(Vector3 delta)
|
|
{
|
|
if (m_componentEditors != null)
|
|
{
|
|
foreach (UnityEditor.Editor e in m_componentEditors)
|
|
{
|
|
if (e != null)
|
|
{
|
|
MethodInfo mi = e.GetType().GetMethod("OnVcamPositionDragged"
|
|
, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
|
|
if (mi != null && e.target != null)
|
|
{
|
|
mi.Invoke(e, new object[] { delta } );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
BeginInspector();
|
|
DrawHeaderInInspector();
|
|
DrawPropertyInInspector(FindProperty(x => x.m_Priority));
|
|
DrawTargetsInInspector(FindProperty(x => x.m_Follow), FindProperty(x => x.m_LookAt));
|
|
DrawPropertyInInspector(FindProperty(x => x.m_StandbyUpdate));
|
|
DrawLensSettingsInInspector(FindProperty(x => x.m_Lens));
|
|
DrawRemainingPropertiesInInspector();
|
|
DrawPipelineInInspector();
|
|
DrawExtensionsWidgetInInspector();
|
|
}
|
|
|
|
protected void DrawPipelineInInspector()
|
|
{
|
|
UpdateInstanceData();
|
|
foreach (CinemachineCore.Stage stage in Enum.GetValues(typeof(CinemachineCore.Stage)))
|
|
{
|
|
int index = (int)stage;
|
|
|
|
// Skip pipeline stages that have no implementations
|
|
if (index < 0 || sStageData[index].PopupOptions.Length <= 1)
|
|
continue;
|
|
|
|
const float indentOffset = 3;
|
|
|
|
GUIStyle stageBoxStyle = GUI.skin.box;
|
|
EditorGUILayout.BeginVertical(stageBoxStyle);
|
|
Rect rect = EditorGUILayout.GetControlRect(true);
|
|
|
|
// Don't use PrefixLabel() because it will link the enabled status of field and label
|
|
GUIContent label = new GUIContent(InspectorUtility.NicifyClassName(stage.ToString()));
|
|
if (m_stageError[index])
|
|
label.image = EditorGUIUtility.IconContent("console.warnicon.sml").image;
|
|
float labelWidth = EditorGUIUtility.labelWidth - (indentOffset + EditorGUI.indentLevel * 15);
|
|
Rect r = rect; r.width = labelWidth;
|
|
EditorGUI.LabelField(r, label);
|
|
r = rect; r.width -= labelWidth; r.x += labelWidth;
|
|
|
|
EditorGUI.BeginChangeCheck();
|
|
GUI.enabled = !StageIsLocked(stage);
|
|
EditorGUI.showMixedValue = !m_hasSameStageDataTypes[index];
|
|
int newSelection = EditorGUI.Popup(r, m_stageState[index], sStageData[index].PopupOptions);
|
|
EditorGUI.showMixedValue = false;
|
|
GUI.enabled = true;
|
|
Type type = sStageData[index].types[newSelection];
|
|
if (EditorGUI.EndChangeCheck())
|
|
{
|
|
SetPipelineStage(stage, type);
|
|
if (newSelection != 0)
|
|
sStageData[index].IsExpanded = true;
|
|
UpdateInstanceData(); // because we changed it
|
|
ResetTarget(); // to allow multi-selection correctly adjust every target
|
|
|
|
return;
|
|
}
|
|
if (type != null)
|
|
{
|
|
Rect stageRect = new Rect(
|
|
rect.x - indentOffset, rect.y, rect.width + indentOffset, rect.height);
|
|
sStageData[index].IsExpanded = EditorGUI.Foldout(
|
|
stageRect, sStageData[index].IsExpanded, GUIContent.none, true);
|
|
if (sStageData[index].IsExpanded)
|
|
{
|
|
// Make the editor for that stage
|
|
UnityEditor.Editor e = GetEditorForPipelineStage(stage);
|
|
if (e != null)
|
|
{
|
|
++EditorGUI.indentLevel;
|
|
EditorGUILayout.Separator();
|
|
e.OnInspectorGUI();
|
|
|
|
EditorGUILayout.Separator();
|
|
--EditorGUI.indentLevel;
|
|
}
|
|
}
|
|
}
|
|
EditorGUILayout.EndVertical();
|
|
}
|
|
}
|
|
|
|
bool StageIsLocked(CinemachineCore.Stage stage)
|
|
{
|
|
if (IsPrefab)
|
|
return true;
|
|
CinemachineCore.Stage[] locked = Target.m_LockStageInInspector;
|
|
if (locked != null)
|
|
for (int i = 0; i < locked.Length; ++i)
|
|
if (locked[i] == stage)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
UnityEditor.Editor GetEditorForPipelineStage(CinemachineCore.Stage stage)
|
|
{
|
|
if (m_componentEditors != null)
|
|
{
|
|
foreach (UnityEditor.Editor e in m_componentEditors)
|
|
{
|
|
if (e != null)
|
|
{
|
|
CinemachineComponentBase c = e.target as CinemachineComponentBase;
|
|
if (c != null && c.Stage == stage)
|
|
return e;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Register with CinemachineVirtualCamera to create the pipeline in an undo-friendly manner
|
|
/// </summary>
|
|
[InitializeOnLoad]
|
|
class CreatePipelineWithUndo
|
|
{
|
|
static CreatePipelineWithUndo()
|
|
{
|
|
CinemachineVirtualCamera.CreatePipelineOverride =
|
|
(CinemachineVirtualCamera vcam, string name, CinemachineComponentBase[] copyFrom) =>
|
|
{
|
|
// Create a new pipeline
|
|
GameObject go = InspectorUtility.CreateGameObject(name);
|
|
Undo.RegisterCreatedObjectUndo(go, "created pipeline");
|
|
bool partOfPrefab = PrefabUtility.IsPartOfAnyPrefab(vcam.gameObject);
|
|
if (!partOfPrefab)
|
|
Undo.SetTransformParent(go.transform, vcam.transform, "parenting pipeline");
|
|
Undo.AddComponent<CinemachinePipeline>(go);
|
|
|
|
// If copying, transfer the components
|
|
if (copyFrom != null)
|
|
{
|
|
foreach (Component c in copyFrom)
|
|
{
|
|
Component copy = Undo.AddComponent(go, c.GetType());
|
|
Undo.RecordObject(copy, "copying pipeline");
|
|
ReflectionHelpers.CopyFields(c, copy);
|
|
}
|
|
}
|
|
return go.transform;
|
|
};
|
|
CinemachineVirtualCamera.DestroyPipelineOverride = (GameObject pipeline) =>
|
|
{
|
|
Undo.DestroyObjectImmediate(pipeline);
|
|
};
|
|
}
|
|
}
|
|
|
|
void SetPipelineStage(CinemachineCore.Stage stage, Type type)
|
|
{
|
|
Undo.SetCurrentGroupName("Cinemachine pipeline change");
|
|
|
|
// Get the existing components
|
|
for(int j = 0; j < targets.Length; j++)
|
|
{
|
|
var vCam = targets[j] as CinemachineVirtualCamera;
|
|
Transform owner = vCam.GetComponentOwner();
|
|
if (owner == null)
|
|
continue; // maybe it's a prefab
|
|
|
|
CinemachineComponentBase[] components = owner.GetComponents<CinemachineComponentBase>();
|
|
if (components == null)
|
|
components = new CinemachineComponentBase[0];
|
|
|
|
// Find an appropriate insertion point
|
|
int numComponents = components.Length;
|
|
int insertPoint = 0;
|
|
for (insertPoint = 0; insertPoint < numComponents; ++insertPoint)
|
|
if (components[insertPoint].Stage >= stage)
|
|
break;
|
|
|
|
// Remove the existing components at that stage
|
|
for (int i = numComponents - 1; i >= 0; --i)
|
|
{
|
|
if (components[i].Stage == stage)
|
|
{
|
|
Undo.DestroyObjectImmediate(components[i]);
|
|
components[i] = null;
|
|
--numComponents;
|
|
if (i < insertPoint)
|
|
--insertPoint;
|
|
}
|
|
}
|
|
|
|
// Add the new stage
|
|
if (type != null)
|
|
{
|
|
MonoBehaviour b = Undo.AddComponent(owner.gameObject, type) as MonoBehaviour;
|
|
|
|
while (numComponents-- > insertPoint)
|
|
UnityEditorInternal.ComponentUtility.MoveComponentDown(b);
|
|
}
|
|
}
|
|
}
|
|
|
|
// This code dynamically discovers eligible classes and builds the menu
|
|
// data for the various component pipeline stages.
|
|
static void UpdateStaticData()
|
|
{
|
|
if (sStageData != null)
|
|
return;
|
|
sStageData = new StageData[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
|
|
var stageTypes = new List<Type>[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
for (int i = 0; i < stageTypes.Length; ++i)
|
|
{
|
|
sStageData[i].Name = ((CinemachineCore.Stage)i).ToString();
|
|
stageTypes[i] = new List<Type>();
|
|
}
|
|
|
|
// Get all ICinemachineComponents
|
|
var allTypes
|
|
= ReflectionHelpers.GetTypesInAllDependentAssemblies(
|
|
(Type t) => typeof(CinemachineComponentBase).IsAssignableFrom(t) && !t.IsAbstract);
|
|
|
|
// Create a temp game object so we can instance behaviours
|
|
GameObject go = new GameObject("Cinemachine Temp Object");
|
|
go.hideFlags = HideFlags.DontSaveInBuild | HideFlags.DontSaveInEditor;
|
|
foreach (Type t in allTypes)
|
|
{
|
|
MonoBehaviour b = go.AddComponent(t) as MonoBehaviour;
|
|
CinemachineComponentBase c = b != null ? (CinemachineComponentBase)b : null;
|
|
if (c != null)
|
|
{
|
|
CinemachineCore.Stage stage = c.Stage;
|
|
stageTypes[(int)stage].Add(t);
|
|
}
|
|
}
|
|
GameObject.DestroyImmediate(go);
|
|
|
|
// Create the static lists
|
|
for (int i = 0; i < stageTypes.Length; ++i)
|
|
{
|
|
stageTypes[i].Insert(0, null); // first item is "none"
|
|
sStageData[i].types = stageTypes[i].ToArray();
|
|
GUIContent[] names = new GUIContent[sStageData[i].types.Length];
|
|
for (int n = 0; n < names.Length; ++n)
|
|
{
|
|
if (n == 0)
|
|
{
|
|
bool useSimple
|
|
= (i == (int)CinemachineCore.Stage.Aim)
|
|
|| (i == (int)CinemachineCore.Stage.Body);
|
|
names[n] = new GUIContent((useSimple) ? "Do nothing" : "none");
|
|
}
|
|
else
|
|
names[n] = new GUIContent(InspectorUtility.NicifyClassName(sStageData[i].types[n].Name));
|
|
}
|
|
sStageData[i].PopupOptions = names;
|
|
}
|
|
}
|
|
|
|
void GetPipelineTypes(CinemachineVirtualCamera vcam, ref Type[] types)
|
|
{
|
|
for (int i = 0; i < types.Length; ++i)
|
|
types[i] = null;
|
|
if (vcam != null)
|
|
{
|
|
var components = vcam.GetComponentPipeline();
|
|
for (int j = 0; j < components.Length; ++j)
|
|
types[(int)components[j].Stage] = components[j].GetType();
|
|
}
|
|
}
|
|
|
|
// scratch buffers for pipeline types
|
|
Type[] m_PipelineTypeCache0 = new Type[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
Type[] m_PipelineTypeCacheN = new Type[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
|
|
void UpdateStageDataTypeMatchesForMultiSelection()
|
|
{
|
|
for (int i = 0; i < m_hasSameStageDataTypes.Length; ++i)
|
|
m_hasSameStageDataTypes[i] = true;
|
|
|
|
if (targets.Length > 1)
|
|
{
|
|
GetPipelineTypes(serializedObject.targetObjects[0] as CinemachineVirtualCamera, ref m_PipelineTypeCache0);
|
|
for (int i = 1; i < targets.Length; ++i)
|
|
{
|
|
GetPipelineTypes(serializedObject.targetObjects[i] as CinemachineVirtualCamera, ref m_PipelineTypeCacheN);
|
|
for (int j = 0; j < m_PipelineTypeCache0.Length; ++j)
|
|
if (m_PipelineTypeCache0[j] != m_PipelineTypeCacheN[j])
|
|
m_hasSameStageDataTypes[j] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateInstanceData()
|
|
{
|
|
// Invalidate the target's cache - this is to support Undo
|
|
for (int i = 0; i < targets.Length; i++)
|
|
{
|
|
var cam = targets[i] as CinemachineVirtualCamera;
|
|
if(cam != null)
|
|
cam.InvalidateComponentPipeline();
|
|
}
|
|
UpdateStageDataTypeMatchesForMultiSelection();
|
|
UpdateComponentEditors();
|
|
UpdateStageState(m_components);
|
|
}
|
|
|
|
// This code dynamically builds editors for the pipeline components.
|
|
// Expansion state is cached statically to preserve foldout state.
|
|
void UpdateComponentEditors()
|
|
{
|
|
if (Target == null)
|
|
{
|
|
m_components = new CinemachineComponentBase[0];
|
|
return;
|
|
}
|
|
CinemachineComponentBase[] components = Target.GetComponentPipeline();
|
|
int numComponents = components != null ? components.Length : 0;
|
|
if (m_components == null || m_components.Length != numComponents)
|
|
m_components = new CinemachineComponentBase[numComponents];
|
|
bool dirty = (numComponents == 0);
|
|
for (int i = 0; i < numComponents; ++i)
|
|
{
|
|
if (m_components[i] == null || components[i] != m_components[i])
|
|
{
|
|
dirty = true;
|
|
m_components[i] = components[i];
|
|
}
|
|
}
|
|
if (dirty)
|
|
{
|
|
// Destroy the subeditors
|
|
if (m_componentEditors != null)
|
|
foreach (UnityEditor.Editor e in m_componentEditors)
|
|
if (e != null)
|
|
UnityEngine.Object.DestroyImmediate(e);
|
|
|
|
// Create new editors
|
|
m_componentEditors = new UnityEditor.Editor[numComponents];
|
|
for (int i = 0; i < numComponents; ++i)
|
|
{
|
|
List<MonoBehaviour> behaviours = new List<MonoBehaviour>();
|
|
for (int j = 0; j < targets.Length; j++)
|
|
{
|
|
var cinemachineVirtualCamera = targets[j] as CinemachineVirtualCamera;
|
|
if (cinemachineVirtualCamera == null)
|
|
continue;
|
|
|
|
var behaviour = cinemachineVirtualCamera.GetCinemachineComponent(components[i].Stage) as MonoBehaviour;
|
|
if (behaviour != null)
|
|
behaviours.Add(behaviour);
|
|
}
|
|
|
|
var behaviourArray = behaviours.ToArray();
|
|
if (behaviourArray.Length > 0 && m_hasSameStageDataTypes[(int)components[i].Stage])
|
|
CreateCachedEditor(behaviourArray, null, ref m_componentEditors[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateStageState(CinemachineComponentBase[] components)
|
|
{
|
|
m_stageState = new int[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
m_stageError = new bool[Enum.GetValues(typeof(CinemachineCore.Stage)).Length];
|
|
foreach (var c in components)
|
|
{
|
|
CinemachineCore.Stage stage = c.Stage;
|
|
int index = 0;
|
|
for (index = sStageData[(int)stage].types.Length - 1; index > 0; --index)
|
|
if (sStageData[(int)stage].types[index] == c.GetType())
|
|
break;
|
|
m_stageState[(int)stage] = index;
|
|
m_stageError[(int)stage] = c == null || !c.IsValid;
|
|
}
|
|
}
|
|
|
|
// Because the cinemachine components are attached to hidden objects, their
|
|
// gizmos don't get drawn by default. We have to do it explicitly.
|
|
[InitializeOnLoad]
|
|
static class CollectGizmoDrawers
|
|
{
|
|
static CollectGizmoDrawers()
|
|
{
|
|
m_GizmoDrawers = new Dictionary<Type, MethodInfo>();
|
|
string definedIn = typeof(CinemachineComponentBase).Assembly.GetName().Name;
|
|
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
|
foreach (Assembly assembly in assemblies)
|
|
{
|
|
// Note that we have to call GetName().Name. Just GetName() will not work.
|
|
if ((!assembly.GlobalAssemblyCache)
|
|
&& ((assembly.GetName().Name == definedIn)
|
|
|| assembly.GetReferencedAssemblies().Any(a => a.Name == definedIn)))
|
|
{
|
|
try
|
|
{
|
|
foreach (var type in assembly.GetTypes())
|
|
{
|
|
try
|
|
{
|
|
bool added = false;
|
|
foreach (var method in type.GetMethods(
|
|
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static))
|
|
{
|
|
if (added)
|
|
break;
|
|
if (!method.IsStatic)
|
|
continue;
|
|
var attributes = method.GetCustomAttributes(typeof(DrawGizmo), true) as DrawGizmo[];
|
|
foreach (var a in attributes)
|
|
{
|
|
if (typeof(CinemachineComponentBase).IsAssignableFrom(a.drawnType) && !a.drawnType.IsAbstract)
|
|
{
|
|
m_GizmoDrawers.Add(a.drawnType, method);
|
|
added = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch (System.Exception) {} // Just skip uncooperative types
|
|
}
|
|
}
|
|
catch (System.Exception) {} // Just skip uncooperative assemblies
|
|
}
|
|
}
|
|
}
|
|
public static Dictionary<Type, MethodInfo> m_GizmoDrawers;
|
|
}
|
|
|
|
[DrawGizmo(GizmoType.Active | GizmoType.InSelectionHierarchy, typeof(CinemachineVirtualCamera))]
|
|
internal static void DrawVirtualCameraGizmos(CinemachineVirtualCamera vcam, GizmoType selectionType)
|
|
{
|
|
var pipeline = vcam.GetComponentPipeline();
|
|
if (pipeline != null)
|
|
{
|
|
foreach (var c in pipeline)
|
|
{
|
|
if (c == null)
|
|
continue;
|
|
|
|
MethodInfo method;
|
|
if (CollectGizmoDrawers.m_GizmoDrawers.TryGetValue(c.GetType(), out method))
|
|
{
|
|
if (method != null)
|
|
method.Invoke(null, new object[] {c, selectionType});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|