using UnityEngine; using UnityEditor; using System.Collections.Generic; namespace Cinemachine.Editor { [CustomPropertyDrawer(typeof(CinemachineImpulseDefinition))] internal sealed class CinemachineImpulseDefinitionPropertyDrawer : PropertyDrawer { const int vSpace = 2; const int kGraphHeight = 8; // lines #pragma warning disable 649 // variable never used CinemachineImpulseDefinition m_MyClass; #pragma warning restore 649 GUIContent m_TimeText = null; float m_TimeTextWidth; SerializedProperty m_ShapeProperty; float m_ShapePropertyHeight; SerializedProperty m_ImpulseTypeProperty; SerializedProperty m_DissipationRateProperty; float m_SpreadPropertyHeight; public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { m_ImpulseTypeProperty = property.FindPropertyRelative(() => m_MyClass.m_ImpulseType); var mode = (CinemachineImpulseDefinition.ImpulseTypes)m_ImpulseTypeProperty.intValue; if (mode == CinemachineImpulseDefinition.ImpulseTypes.Legacy) return LegacyModeGetPropertyHeight(property, label); int lines = 3; m_ShapePropertyHeight = EditorGUIUtility.singleLineHeight + vSpace; m_ShapeProperty = property.FindPropertyRelative(() => m_MyClass.m_ImpulseShape); if (m_ShapeProperty.isExpanded) { lines += kGraphHeight; m_ShapePropertyHeight *= 1 + kGraphHeight; } m_ShapePropertyHeight -= vSpace; m_DissipationRateProperty = property.FindPropertyRelative(() => m_MyClass.m_DissipationRate); if (mode != CinemachineImpulseDefinition.ImpulseTypes.Uniform) { m_SpreadPropertyHeight = EditorGUIUtility.singleLineHeight + vSpace; if (m_DissipationRateProperty.isExpanded) { lines += kGraphHeight; m_SpreadPropertyHeight *= 1 + kGraphHeight; } m_SpreadPropertyHeight -= vSpace; lines += 2; if (mode == CinemachineImpulseDefinition.ImpulseTypes.Propagating) ++lines; } return lines * (EditorGUIUtility.singleLineHeight + vSpace); } public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label) { // Using BeginProperty / EndProperty on the parent property means that // prefab override logic works on the entire property. EditorGUI.BeginProperty(rect, label, property); var mode = (CinemachineImpulseDefinition.ImpulseTypes)m_ImpulseTypeProperty.intValue; if (mode == CinemachineImpulseDefinition.ImpulseTypes.Legacy) { LegacyModeOnGUI(rect, property, label); return; } rect.height = EditorGUIUtility.singleLineHeight; EditorGUI.PropertyField(rect, property.FindPropertyRelative(() => m_MyClass.m_ImpulseChannel)); rect.y += rect.height + vSpace; // Impulse type EditorGUI.PropertyField(rect, m_ImpulseTypeProperty); rect.y += rect.height + vSpace; if (mode != CinemachineImpulseDefinition.ImpulseTypes.Uniform) { // Propaation speed if (mode == CinemachineImpulseDefinition.ImpulseTypes.Propagating) { EditorGUI.PropertyField(rect, property.FindPropertyRelative(() => m_MyClass.m_PropagationSpeed)); rect.y += rect.height + vSpace; } // Dissipation EditorGUI.PropertyField(rect, property.FindPropertyRelative(() => m_MyClass.m_DissipationDistance)); rect.y += rect.height + vSpace; // Spread combo rect.height = m_SpreadPropertyHeight; DrawSpreadCombo(rect, property); rect.y += rect.height + vSpace; rect.height = EditorGUIUtility.singleLineHeight; } // Impulse Shape combo rect.height = m_ShapePropertyHeight; DrawImpulseShapeCombo(rect, property); rect.y += rect.height + vSpace; rect.height = EditorGUIUtility.singleLineHeight; EditorGUI.EndProperty(); } void DrawImpulseShapeCombo(Rect fullRect, SerializedProperty property) { float floatFieldWidth = EditorGUIUtility.fieldWidth + 2; SerializedProperty timeProp = property.FindPropertyRelative(() => m_MyClass.m_ImpulseDuration); if (m_TimeText == null) { m_TimeText = new GUIContent(" s", timeProp.tooltip); m_TimeTextWidth = GUI.skin.label.CalcSize(m_TimeText).x; } var graphRect = fullRect; graphRect.y += EditorGUIUtility.singleLineHeight + vSpace; graphRect.height -= EditorGUIUtility.singleLineHeight + vSpace; var indentLevel = EditorGUI.indentLevel; Rect r = fullRect; r.height = EditorGUIUtility.singleLineHeight; r = EditorGUI.PrefixLabel(r, EditorGUI.BeginProperty( r, new GUIContent(m_ShapeProperty.displayName, m_ShapeProperty.tooltip), property)); m_ShapeProperty.isExpanded = EditorGUI.Foldout(r, m_ShapeProperty.isExpanded, GUIContent.none); bool isCustom = m_ShapeProperty.intValue == (int)CinemachineImpulseDefinition.ImpulseShapes.Custom; r.width -= floatFieldWidth + m_TimeTextWidth; if (isCustom) r.width -= 2 * r.height; EditorGUI.BeginChangeCheck(); { EditorGUI.PropertyField(r, m_ShapeProperty, GUIContent.none); if (EditorGUI.EndChangeCheck()) InvalidateImpulseGraphSample(); if (!isCustom && Event.current.type == EventType.Repaint && m_ShapeProperty.isExpanded) DrawImpulseGraph(graphRect, CinemachineImpulseDefinition.GetStandardCurve( (CinemachineImpulseDefinition.ImpulseShapes)m_ShapeProperty.intValue)); } if (isCustom) { SerializedProperty curveProp = property.FindPropertyRelative(() => m_MyClass.m_CustomImpulseShape); r.x += r.width; r.width = 2 * r.height; EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField(r, curveProp, GUIContent.none); if (EditorGUI.EndChangeCheck()) { curveProp.animationCurveValue = RuntimeUtility.NormalizeCurve(curveProp.animationCurveValue, true, false); curveProp.serializedObject.ApplyModifiedProperties(); InvalidateImpulseGraphSample(); } if (Event.current.type == EventType.Repaint && m_ShapeProperty.isExpanded) DrawImpulseGraph(graphRect, curveProp.animationCurveValue); } // Time float oldWidth = EditorGUIUtility.labelWidth; EditorGUIUtility.labelWidth = m_TimeTextWidth; r.x += r.width; r.width = floatFieldWidth + EditorGUIUtility.labelWidth; EditorGUI.BeginChangeCheck(); EditorGUI.PropertyField(r, timeProp, m_TimeText); if (EditorGUI.EndChangeCheck()) timeProp.floatValue = Mathf.Max(timeProp.floatValue, 0); EditorGUIUtility.labelWidth = oldWidth; EditorGUI.indentLevel = indentLevel; } const int kNumSamples = 100; Vector3[] m_ImpulseGraphSnapshot = new Vector3[kNumSamples+1]; float m_ImpulseGraphZero; Vector2 m_ImpulseGraphSize; void InvalidateImpulseGraphSample() { m_ImpulseGraphSize = Vector2.zero; } void DrawImpulseGraph(Rect rect, AnimationCurve curve) { // Resample if necessary if (m_ImpulseGraphSize != rect.size) { m_ImpulseGraphSize = rect.size; float minY = 0; float maxY = 0.1f; for (int i = 0; i <= kNumSamples; ++i) { var x = (float)i / kNumSamples; var y = curve.Evaluate(x); minY = Mathf.Min(y, minY); maxY = Mathf.Max(y, maxY); m_ImpulseGraphSnapshot[i] = new Vector2(x * rect.width, y); } var range = maxY - minY; m_ImpulseGraphZero = -minY / range; m_ImpulseGraphZero = rect.height * (1f - m_ImpulseGraphZero); // Apply scale for (int i = 0; i <= kNumSamples; ++i) m_ImpulseGraphSnapshot[i].y = rect.height * (1f - (m_ImpulseGraphSnapshot[i].y - minY) / range); } EditorGUI.DrawRect(rect, new Color(0.2f, 0.2f, 0.2f, 1)); var oldMatrix = Handles.matrix; Handles.matrix = Handles.matrix * Matrix4x4.Translate(rect.position); Handles.color = new Color(0, 0, 0, 1); Handles.DrawLine(new Vector3(0, m_ImpulseGraphZero, 0), new Vector3(rect.width, m_ImpulseGraphZero, 0)); Handles.color = new Color(1, 0.8f, 0, 1); Handles.DrawPolyLine(m_ImpulseGraphSnapshot); Handles.matrix = oldMatrix; } void DrawSpreadCombo(Rect fullRect, SerializedProperty property) { var graphRect = fullRect; graphRect.y += EditorGUIUtility.singleLineHeight + vSpace; graphRect.height -= EditorGUIUtility.singleLineHeight + vSpace; var indentLevel = EditorGUI.indentLevel; Rect r = fullRect; r.height = EditorGUIUtility.singleLineHeight; r = EditorGUI.PrefixLabel(r, EditorGUI.BeginProperty( r, new GUIContent(m_DissipationRateProperty.displayName, m_DissipationRateProperty.tooltip), property)); m_DissipationRateProperty.isExpanded = EditorGUI.Foldout(r, m_DissipationRateProperty.isExpanded, GUIContent.none); EditorGUI.BeginChangeCheck(); EditorGUI.Slider(r, m_DissipationRateProperty, 0, 1, GUIContent.none); if (EditorGUI.EndChangeCheck()) InvalidateSpreadGraphSample(); if (Event.current.type == EventType.Repaint && m_DissipationRateProperty.isExpanded) DrawSpreadGraph(graphRect, m_DissipationRateProperty.floatValue); EditorGUI.indentLevel = indentLevel; } Vector3[] m_SpreadGraphSnapshot = new Vector3[kNumSamples+1]; Vector2 m_SpreadGraphSize; void InvalidateSpreadGraphSample() { m_SpreadGraphSize = Vector2.zero; } void DrawSpreadGraph(Rect rect, float spread) { // Resample if necessary if (m_SpreadGraphSize != rect.size) { m_SpreadGraphSize = rect.size; for (int i = 0; i <= kNumSamples >> 1; ++i) { var x = (float)i / kNumSamples; var y = CinemachineImpulseManager.EvaluateDissipationScale(spread, Mathf.Abs(1 - x * 2)); m_SpreadGraphSnapshot[i] = new Vector2(x * rect.width, rect.height * (1 - y)); m_SpreadGraphSnapshot[kNumSamples - i] = new Vector2((1 - x) * rect.width, rect.height * (1 - y)); } } EditorGUI.DrawRect(rect, new Color(0.2f, 0.2f, 0.2f, 1)); var oldMatrix = Handles.matrix; Handles.matrix = Handles.matrix * Matrix4x4.Translate(rect.position); Handles.color = new Color(0, 0, 0, 1); Handles.DrawLine(new Vector3(rect.width * 0.5f, 0, 0), new Vector3(rect.width * 0.5f, rect.height, 0)); Handles.color = new Color(0, 0.6f, 1, 1); Handles.DrawPolyLine(m_SpreadGraphSnapshot); Handles.matrix = oldMatrix; } // Legacy mode float HeaderHeight { get { return EditorGUIUtility.singleLineHeight * 1.5f; } } float DrawHeader(Rect rect, string text) { float delta = HeaderHeight - EditorGUIUtility.singleLineHeight; rect.y += delta; rect.height -= delta; EditorGUI.LabelField(rect, new GUIContent(text), EditorStyles.boldLabel); return HeaderHeight; } string HeaderText(SerializedProperty property) { var attrs = property.serializedObject.targetObject.GetType() .GetCustomAttributes(typeof(HeaderAttribute), false); if (attrs != null && attrs.Length > 0) return ((HeaderAttribute)attrs[0]).header; return null; } List mHideProperties = new List(); float LegacyModeGetPropertyHeight(SerializedProperty prop, GUIContent label) { SignalSourceAsset asset = null; float height = 0; mHideProperties.Clear(); string prefix = prop.name; prop.NextVisible(true); // Skip outer foldout do { if (!prop.propertyPath.Contains(prefix)) // if it is part of an array, then it won't StartWith prefix break; string header = HeaderText(prop); if (header != null) height += HeaderHeight + vSpace; // Do we hide this property? bool hide = false; if (prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_RawSignal)) asset = prop.objectReferenceValue as SignalSourceAsset; if (prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_RepeatMode)) hide = asset == null || asset.SignalDuration <= 0; else if (prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_Randomize)) hide = asset == null || asset.SignalDuration > 0; else { hide = prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_ImpulseShape) || prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_CustomImpulseShape) || prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_ImpulseDuration) || prop.name == SerializedPropertyHelper.PropertyName(() => m_MyClass.m_DissipationRate); } if (hide) mHideProperties.Add(prop.name); else height += EditorGUI.GetPropertyHeight(prop, false) + vSpace; } while (prop.NextVisible(prop.isExpanded)); return height; } void LegacyModeOnGUI(Rect rect, SerializedProperty prop, GUIContent label) { string prefix = prop.name; prop.NextVisible(true); // Skip outer foldout do { if (!prop.propertyPath.Contains(prefix)) // if it is part of an array, then it won't StartWith prefix break; string header = HeaderText(prop); if (header != null) { rect.height = HeaderHeight; DrawHeader(rect, header); rect.y += HeaderHeight + vSpace; } if (mHideProperties.Contains(prop.name)) continue; rect.height = EditorGUI.GetPropertyHeight(prop, false); EditorGUI.PropertyField(rect, prop); rect.y += rect.height + vSpace; } while (prop.NextVisible(prop.isExpanded)); } } }