using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; namespace FMODUnity { [CustomEditor(typeof(StudioEventEmitter))] [CanEditMultipleObjects] public class StudioEventEmitterEditor : Editor { ParameterValueView parameterValueView; public void OnEnable() { parameterValueView = new ParameterValueView(serializedObject); } public void OnSceneGUI() { var emitter = target as StudioEventEmitter; EditorEventRef editorEvent = EventManager.EventFromPath(emitter.Event); if (editorEvent != null && editorEvent.Is3D) { EditorGUI.BeginChangeCheck(); float minDistance = emitter.OverrideAttenuation ? emitter.OverrideMinDistance : editorEvent.MinDistance; float maxDistance = emitter.OverrideAttenuation ? emitter.OverrideMaxDistance : editorEvent.MaxDistance; minDistance = Handles.RadiusHandle(Quaternion.identity, emitter.transform.position, minDistance); maxDistance = Handles.RadiusHandle(Quaternion.identity, emitter.transform.position, maxDistance); if (EditorGUI.EndChangeCheck() && emitter.OverrideAttenuation) { Undo.RecordObject(emitter, "Change Emitter Bounds"); emitter.OverrideMinDistance = Mathf.Clamp(minDistance, 0, emitter.OverrideMaxDistance); emitter.OverrideMaxDistance = Mathf.Max(emitter.OverrideMinDistance, maxDistance); } } } public override void OnInspectorGUI() { var begin = serializedObject.FindProperty("PlayEvent"); var end = serializedObject.FindProperty("StopEvent"); var tag = serializedObject.FindProperty("CollisionTag"); var ev = serializedObject.FindProperty("Event"); var fadeout = serializedObject.FindProperty("AllowFadeout"); var once = serializedObject.FindProperty("TriggerOnce"); var preload = serializedObject.FindProperty("Preload"); var overrideAtt = serializedObject.FindProperty("OverrideAttenuation"); var minDistance = serializedObject.FindProperty("OverrideMinDistance"); var maxDistance = serializedObject.FindProperty("OverrideMaxDistance"); EditorGUILayout.PropertyField(begin, new GUIContent("Play Event")); EditorGUILayout.PropertyField(end, new GUIContent("Stop Event")); if ((begin.enumValueIndex >= (int)EmitterGameEvent.TriggerEnter && begin.enumValueIndex <= (int)EmitterGameEvent.TriggerExit2D) || (end.enumValueIndex >= (int)EmitterGameEvent.TriggerEnter && end.enumValueIndex <= (int)EmitterGameEvent.TriggerExit2D)) { tag.stringValue = EditorGUILayout.TagField("Collision Tag", tag.stringValue); } EditorGUI.BeginChangeCheck(); EditorGUILayout.PropertyField(ev, new GUIContent("Event")); EditorEventRef editorEvent = EventManager.EventFromPath(ev.stringValue); if (EditorGUI.EndChangeCheck()) { EditorUtils.UpdateParamsOnEmitter(serializedObject, ev.stringValue); if (editorEvent != null) { overrideAtt.boolValue = false; minDistance.floatValue = editorEvent.MinDistance; maxDistance.floatValue = editorEvent.MaxDistance; } } // Attenuation if (editorEvent != null) { { EditorGUI.BeginDisabledGroup(editorEvent == null || !editorEvent.Is3D); EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel("Override Attenuation"); EditorGUI.BeginChangeCheck(); overrideAtt.boolValue = EditorGUILayout.Toggle(overrideAtt.boolValue, GUILayout.Width(20)); if (EditorGUI.EndChangeCheck() || (minDistance.floatValue == -1 && maxDistance.floatValue == -1) // never been initialiased ) { minDistance.floatValue = editorEvent.MinDistance; maxDistance.floatValue = editorEvent.MaxDistance; } EditorGUI.BeginDisabledGroup(!overrideAtt.boolValue); EditorGUIUtility.labelWidth = 30; minDistance.floatValue = EditorGUILayout.FloatField("Min", minDistance.floatValue); minDistance.floatValue = Mathf.Clamp(minDistance.floatValue, 0, maxDistance.floatValue); maxDistance.floatValue = EditorGUILayout.FloatField("Max", maxDistance.floatValue); maxDistance.floatValue = Mathf.Max(minDistance.floatValue, maxDistance.floatValue); EditorGUIUtility.labelWidth = 0; EditorGUI.EndDisabledGroup(); EditorGUILayout.EndHorizontal(); EditorGUI.EndDisabledGroup(); } parameterValueView.OnGUI(editorEvent, !ev.hasMultipleDifferentValues); fadeout.isExpanded = EditorGUILayout.Foldout(fadeout.isExpanded, "Advanced Controls"); if (fadeout.isExpanded) { EditorGUILayout.PropertyField(preload, new GUIContent("Preload Sample Data")); EditorGUILayout.PropertyField(fadeout, new GUIContent("Allow Fadeout When Stopping")); EditorGUILayout.PropertyField(once, new GUIContent("Trigger Once")); } } serializedObject.ApplyModifiedProperties(); } private class ParameterValueView { // The "Params" property from the SerializedObject we're editing in the inspector, // so we can expand/collapse it or revert to prefab. private SerializedProperty paramsProperty; // This holds one SerializedObject for each object in the current selection. private List serializedTargets = new List(); // A mapping from EditorParamRef to the initial parameter value properties in the // current selection that have the same name. // We need this because some objects may be missing some properties, and properties with // the same name may be at different array indices in different objects. private class PropertyRecord { public string name { get { return paramRef.Name; } } public EditorParamRef paramRef; public List valueProperties; } // Mappings from EditorParamRef to initial parameter value property for all properties // found in the current selection. private List propertyRecords = new List(); // Any parameters that are in the current event but are missing from some objects in // the current selection, so we can put them in the "Add" menu. private List missingParameters = new List(); public ParameterValueView(SerializedObject serializedObject) { paramsProperty = serializedObject.FindProperty("Params"); foreach (UnityEngine.Object target in serializedObject.targetObjects) { serializedTargets.Add(new SerializedObject(target)); } } // Rebuilds the propertyRecords and missingParameters collections. private void RefreshPropertyRecords(EditorEventRef eventRef) { propertyRecords.Clear(); foreach (SerializedObject serializedTarget in serializedTargets) { SerializedProperty paramsProperty = serializedTarget.FindProperty("Params"); foreach (SerializedProperty parameterProperty in paramsProperty) { string name = parameterProperty.FindPropertyRelative("Name").stringValue; SerializedProperty valueProperty = parameterProperty.FindPropertyRelative("Value"); PropertyRecord record = propertyRecords.Find(r => r.name == name); if (record != null) { record.valueProperties.Add(valueProperty); } else { EditorParamRef paramRef = eventRef.Parameters.Find(p => p.Name == name); if (paramRef != null) { propertyRecords.Add( new PropertyRecord() { paramRef = paramRef, valueProperties = new List() { valueProperty }, }); } } } } // Only sort if there is a multi-selection. If there is only one object selected, // the user can revert to prefab, and the behaviour depends on the array order, // so it's helpful to show the true order. if (serializedTargets.Count > 1) { propertyRecords.Sort((a, b) => EditorUtility.NaturalCompare(a.name, b.name)); } missingParameters.Clear(); missingParameters.AddRange(eventRef.Parameters.Where( p => { PropertyRecord record = propertyRecords.Find(r => r.name == p.Name); return record == null || record.valueProperties.Count < serializedTargets.Count; })); } public void OnGUI(EditorEventRef eventRef, bool matchingEvents) { foreach (SerializedObject serializedTarget in serializedTargets) { serializedTarget.Update(); } if (Event.current.type == EventType.Layout) { RefreshPropertyRecords(eventRef); } DrawHeader(matchingEvents); if (paramsProperty.isExpanded) { if (matchingEvents) { DrawValues(); } else { GUILayout.Box("Cannot change parameters when different events are selected", GUILayout.ExpandWidth(true)); } } foreach (SerializedObject serializedTarget in serializedTargets) { serializedTarget.ApplyModifiedProperties(); } } private void DrawHeader(bool enableAddButton) { Rect controlRect = EditorGUILayout.GetControlRect(); Rect titleRect = controlRect; titleRect.width = EditorGUIUtility.labelWidth; // Let the user revert the whole Params array to prefab by context-clicking the title. EditorGUI.BeginProperty(titleRect, GUIContent.none, paramsProperty); paramsProperty.isExpanded = EditorGUI.Foldout(titleRect, paramsProperty.isExpanded, "Initial Parameter Values"); EditorGUI.EndProperty(); Rect buttonRect = controlRect; buttonRect.xMin = titleRect.xMax; EditorGUI.BeginDisabledGroup(!enableAddButton); DrawAddButton(buttonRect); EditorGUI.EndDisabledGroup(); } private void DrawAddButton(Rect position) { EditorGUI.BeginDisabledGroup(missingParameters.Count == 0); if (EditorGUI.DropdownButton(position, new GUIContent("Add"), FocusType.Passive)) { GenericMenu menu = new GenericMenu(); menu.AddItem(new GUIContent("All"), false, () => { foreach (EditorParamRef parameter in missingParameters) { AddParameter(parameter); } }); menu.AddSeparator(string.Empty); foreach (EditorParamRef parameter in missingParameters) { menu.AddItem(new GUIContent(parameter.Name), false, (userData) => { AddParameter(userData as EditorParamRef); }, parameter); } menu.DropDown(position); } EditorGUI.EndDisabledGroup(); } private void DrawValues() { // We use this to defer deletion so we don't mess with arrays while using // SerializedProperties that refer to array elements, as this can throw exceptions. string parameterToDelete = null; foreach (PropertyRecord record in propertyRecords) { if (record.valueProperties.Count == serializedTargets.Count) { bool delete; DrawValue(record, out delete); if (delete) { parameterToDelete = record.name; } } } if (parameterToDelete != null) { DeleteParameter(parameterToDelete); } } private void DrawValue(PropertyRecord record, out bool delete) { delete = false; GUIContent removeLabel = new GUIContent("Remove"); Rect position = EditorGUILayout.GetControlRect(); Rect nameLabelRect = position; nameLabelRect.width = EditorGUIUtility.labelWidth; Rect removeButtonRect = position; removeButtonRect.width = EditorStyles.miniButton.CalcSize(removeLabel).x; removeButtonRect.x = position.xMax - removeButtonRect.width; Rect sliderRect = position; sliderRect.xMin = nameLabelRect.xMax; sliderRect.xMax = removeButtonRect.xMin - EditorStyles.miniButton.margin.left; GUIContent nameLabel = new GUIContent(record.name); float value = 0; bool mixedValues = false; // We use EditorGUI.BeginProperty when there is a single object selected, so // the user can revert the value to prefab by context-clicking the name. // We handle multi-selections ourselves, so that we can deal with // mismatched arrays nicely. if (record.valueProperties.Count == 1) { value = record.valueProperties[0].floatValue; EditorGUI.BeginProperty(position, nameLabel, record.valueProperties[0]); } else { bool first = true; foreach (SerializedProperty property in record.valueProperties) { if (first) { value = property.floatValue; first = false; } else if (property.floatValue != value) { mixedValues = true; break; } } } EditorGUI.LabelField(nameLabelRect, nameLabel); EditorGUI.BeginChangeCheck(); EditorGUI.showMixedValue = mixedValues; float newValue = EditorGUI.Slider(sliderRect, value, record.paramRef.Min, record.paramRef.Max); EditorGUI.showMixedValue = false; if (EditorGUI.EndChangeCheck()) { foreach (SerializedProperty property in record.valueProperties) { property.floatValue = newValue; } } delete = GUI.Button(removeButtonRect, removeLabel, EditorStyles.miniButton); if (record.valueProperties.Count == 1) { EditorGUI.EndProperty(); } else { // Context menu to set all values from one object in the multi-selection. if (mixedValues && Event.current.type == EventType.ContextClick && nameLabelRect.Contains(Event.current.mousePosition)) { GenericMenu menu = new GenericMenu(); foreach (SerializedProperty sourceProperty in record.valueProperties) { UnityEngine.Object targetObject = sourceProperty.serializedObject.targetObject; menu.AddItem(new GUIContent(string.Format("Set to Value of '{0}'", targetObject.name)), false, (userData) => CopyValueToAll(userData as SerializedProperty, record.valueProperties), sourceProperty); } menu.DropDown(position); } } } // Copy the value from the source property to all target properties. private void CopyValueToAll(SerializedProperty sourceProperty, List targetProperties) { foreach (SerializedProperty targetProperty in targetProperties) { if (targetProperty != sourceProperty) { targetProperty.floatValue = sourceProperty.floatValue; targetProperty.serializedObject.ApplyModifiedProperties(); } } } // Add an initial value for the given parameter to all selected objects that don't have one. private void AddParameter(EditorParamRef parameter) { foreach (SerializedObject serializedTarget in serializedTargets) { StudioEventEmitter emitter = serializedTarget.targetObject as StudioEventEmitter; if (Array.FindIndex(emitter.Params, p => p.Name == parameter.Name) < 0) { SerializedProperty paramsProperty = serializedTarget.FindProperty("Params"); int index = paramsProperty.arraySize; paramsProperty.InsertArrayElementAtIndex(index); SerializedProperty arrayElement = paramsProperty.GetArrayElementAtIndex(index); arrayElement.FindPropertyRelative("Name").stringValue = parameter.Name; arrayElement.FindPropertyRelative("Value").floatValue = parameter.Default; serializedTarget.ApplyModifiedProperties(); } } } // Delete initial parameter values for the given name from all selected objects. private void DeleteParameter(string name) { foreach (SerializedObject serializedTarget in serializedTargets) { SerializedProperty paramsProperty = serializedTarget.FindProperty("Params"); foreach (SerializedProperty child in paramsProperty) { if (child.FindPropertyRelative("Name").stringValue == name) { child.DeleteCommand(); break; } } } } } } }