using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor; using UnityEditor.IMGUI.Controls; using UnityEngine; using System.IO; namespace FMODUnity { class EventBrowser : EditorWindow, ISerializationCallbackReceiver { [MenuItem("FMOD/Event Browser", priority = 2)] public static void ShowWindow() { EventBrowser eventBrowser = GetWindow("FMOD Events"); eventBrowser.minSize = new Vector2(380, 600); eventBrowser.BeginStandaloneWindow(); eventBrowser.Show(); } public static bool IsOpen { get; private set; } public void OnBeforeSerialize() { treeViewState = treeView.state; } public void OnAfterDeserialize() { } [NonSerialized] float nextRepaintTime; [NonSerialized] float[] cachedMetering; const float RepaintInterval = 1/30.0f; void Update() { bool forceRepaint = false; float[] currentMetering = EditorUtils.GetMetering(); if (cachedMetering == null || !cachedMetering.SequenceEqual(currentMetering)) { cachedMetering = currentMetering; forceRepaint = true; } if (LastKnownCacheTime != EventManager.CacheTime) { ReadEventCache(); forceRepaint = true; } if (forceRepaint || (previewArea != null && previewArea.forceRepaint && nextRepaintTime < Time.realtimeSinceStartup)) { Repaint(); nextRepaintTime = Time.time + RepaintInterval; } } void ReadEventCache() { LastKnownCacheTime = EventManager.CacheTime; treeView.Reload(); } class TreeView : UnityEditor.IMGUI.Controls.TreeView { public TreeView(State state) : base(state.baseState) { noSearchExpandState = state.noSearchExpandState; SelectedObject = state.selectedObject; TypeFilter = state.typeFilter; DragEnabled = state.dragEnabled; for (int i = 0; i < state.itemPaths.Count; ++i) { itemIDs.Add(state.itemPaths[i], state.itemIDs[i]); } } public void JumpToEvent(string path) { JumpToItem(path); } public void JumpToBank(string name) { JumpToItem(BankPrefix + name); } private void JumpToItem(string path) { nextFramedItemPath = path; Reload(); int itemID; if (itemIDs.TryGetValue(path, out itemID)) { SetSelection(new List { itemID }, TreeViewSelectionOptions.RevealAndFrame | TreeViewSelectionOptions.FireSelectionChanged); } else { SetSelection(new List()); } } private static readonly Texture2D folderOpenIcon = EditorGUIUtility.Load("FMOD/FolderIconOpen.png") as Texture2D; private static readonly Texture2D folderClosedIcon = EditorGUIUtility.Load("FMOD/FolderIconClosed.png") as Texture2D; private static readonly Texture2D eventIcon = EditorGUIUtility.Load("FMOD/EventIcon.png") as Texture2D; private static readonly Texture2D snapshotIcon = EditorGUIUtility.Load("FMOD/SnapshotIcon.png") as Texture2D; private static readonly Texture2D bankIcon = EditorGUIUtility.Load("FMOD/BankIcon.png") as Texture2D; private static readonly Texture2D parameterIcon = EditorGUIUtility.Load("FMOD/EventIcon.png") as Texture2D; private class LeafItem : TreeViewItem { public LeafItem(int id, int depth, ScriptableObject data) : base(id, depth) { Data = data; } public ScriptableObject Data; } class FolderItem : TreeViewItem { public FolderItem(int id, int depth, string displayName) : base(id, depth, displayName) { } } private FolderItem CreateFolderItem(string name, string path, bool hasChildren, bool forceExpanded, TreeViewItem parent) { FolderItem item = new FolderItem(AffirmItemID("folder:" + path), 0, name); bool expanded; if (!hasChildren) { expanded = false; } else if (forceExpanded || expandNextFolderSet || (nextFramedItemPath != null && nextFramedItemPath.StartsWith(path))) { SetExpanded(item.id, true); expanded = true; } else { expanded = IsExpanded(item.id); } if (expanded) { item.icon = folderOpenIcon; } else { item.icon = folderClosedIcon; if (hasChildren) { item.children = CreateChildListForCollapsedParent(); } } parent.AddChild(item); return item; } protected override TreeViewItem BuildRoot() { return new TreeViewItem(-1, -1); } private Dictionary itemIDs = new Dictionary(); private int AffirmItemID(string path) { int id; if (!itemIDs.TryGetValue(path, out id)) { id = itemIDs.Count; itemIDs.Add(path, id); } return id; } private const string EventPrefix = "event:/"; private const string SnapshotPrefix = "snapshot:/"; private const string BankPrefix = "bank:/"; private const string ParameterPrefix = "parameter:/"; bool expandNextFolderSet = false; string nextFramedItemPath; private string[] searchStringSplit; public TypeFilter TypeFilter { get; set; } public bool DragEnabled { get; set; } protected override IList BuildRows(TreeViewItem root) { if (hasSearch) { searchStringSplit = searchString.Split(' '); } if (rootItem.children != null) { rootItem.children.Clear(); } if ((TypeFilter & TypeFilter.Event) != 0) { CreateSubTree("Events", EventPrefix, EventManager.Events.Where(e => e.Path.StartsWith(EventPrefix)), e => e.Path, eventIcon); CreateSubTree("Snapshots", SnapshotPrefix, EventManager.Events.Where(e => e.Path.StartsWith(SnapshotPrefix)), s => s.Path, snapshotIcon); } if ((TypeFilter & TypeFilter.Bank) != 0) { CreateSubTree("Banks", BankPrefix, EventManager.Banks, b => b.StudioPath, bankIcon); } if ((TypeFilter & TypeFilter.Parameter) != 0) { CreateSubTree("Global Parameters", ParameterPrefix, EventManager.Parameters, p => ParameterPrefix + p.Name, parameterIcon, (path, p) => string.Format("{0}:{1:x}:{2:x}", path, p.ID.data1, p.ID.data2)); } List rows = new List(); AddChildrenInOrder(rows, rootItem); SetupDepthsFromParentsAndChildren(rootItem); expandNextFolderSet = false; nextFramedItemPath = null; return rows; } private class NaturalComparer : IComparer { public int Compare(string a, string b) { return EditorUtility.NaturalCompare(a, b); } } private static NaturalComparer naturalComparer = new NaturalComparer(); private void CreateSubTree(string rootName, string rootPath, IEnumerable sourceRecords, Func GetPath, Texture2D icon, Func MakeUniquePath = null) where T : ScriptableObject { var records = sourceRecords.Select(r => new { source = r, path = GetPath(r) }); if (hasSearch) { records = records.Where(r => { foreach (var word in searchStringSplit) { if (word.Length > 0 && r.path.IndexOf(word, StringComparison.OrdinalIgnoreCase) < 0) { return false; } } return true; }); } records = records.OrderBy(r => r.path, naturalComparer); TreeViewItem root = CreateFolderItem(rootName, rootPath, records.Any(), TypeFilter != TypeFilter.All, rootItem); List currentFolderItems = new List(); foreach (var record in records) { string leafName; TreeViewItem parent = CreateFolderItems(record.path, currentFolderItems, root, out leafName); if (parent != null) { string uniquePath; if (MakeUniquePath != null) { uniquePath = MakeUniquePath(record.path, record.source); } else { uniquePath = record.path; } TreeViewItem leafItem = new LeafItem(AffirmItemID(uniquePath), 0, record.source); leafItem.displayName = leafName; leafItem.icon = icon; parent.AddChild(leafItem); } } } private TreeViewItem CreateFolderItems(string path, List currentFolderItems, TreeViewItem root, out string leafName) { TreeViewItem parent = root; char separator = '/'; // Skip the type prefix at the start of the path int elementStart = path.IndexOf(separator) + 1; for (int i = 0; ; ++i) { if (!IsExpanded(parent.id)) { leafName = null; return null; } int elementEnd = path.IndexOf(separator, elementStart); if (elementEnd < 0) { // No more folders; elementStart points to the event name break; } string folderName = path.Substring(elementStart, elementEnd - elementStart); if (i < currentFolderItems.Count && folderName != currentFolderItems[i].displayName) { currentFolderItems.RemoveRange(i, currentFolderItems.Count - i); } if (i == currentFolderItems.Count) { FolderItem folderItem = CreateFolderItem(folderName, path.Substring(0, elementEnd), true, false, parent); currentFolderItems.Add(folderItem); } elementStart = elementEnd + 1; parent = currentFolderItems[i]; } leafName = path.Substring(elementStart); return parent; } private static void AddChildrenInOrder(List list, TreeViewItem item) { if (item.children != null) { foreach (TreeViewItem child in item.children.Where(child => child is FolderItem)) { list.Add(child); AddChildrenInOrder(list, child); } foreach (TreeViewItem child in item.children.Where(child => !(child == null || child is FolderItem))) { list.Add(child); } } } protected override bool CanMultiSelect(TreeViewItem item) { return false; } protected override bool CanChangeExpandedState(TreeViewItem item) { return item.hasChildren; } protected override bool CanStartDrag(CanStartDragArgs args) { if (DragEnabled && args.draggedItem is LeafItem) { return IsDraggable((args.draggedItem as LeafItem).Data); } else { return false; } } protected override void SetupDragAndDrop(SetupDragAndDropArgs args) { IList items = FindRows(args.draggedItemIDs); if (items[0] is LeafItem) { LeafItem item = items[0] as LeafItem; DragAndDrop.PrepareStartDrag(); DragAndDrop.objectReferences = new UnityEngine.Object[] { Instantiate(item.Data) }; string title = string.Empty; if (item.Data is EditorEventRef) { title = "New FMOD Studio Emitter"; } else if (item.Data is EditorBankRef) { title = "New FMOD Studio Bank Loader"; } else if (item.Data is EditorParamRef) { title = "New FMOD Studio Global Parameter Trigger"; } DragAndDrop.StartDrag(title); } } protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args) { return DragAndDropVisualMode.None; } IList noSearchExpandState; protected override void SearchChanged(string newSearch) { if (!string.IsNullOrEmpty(newSearch.Trim())) { expandNextFolderSet = true; if (noSearchExpandState == null) { // A new search is beginning noSearchExpandState = GetExpanded(); SetExpanded(new List()); } } else { if (noSearchExpandState != null) { // A search is ending SetExpanded(noSearchExpandState); noSearchExpandState = null; } } } public ScriptableObject SelectedObject { get; private set; } public ScriptableObject DoubleClickedObject { get; private set; } protected override void SelectionChanged(IList selectedIDs) { SelectedObject = null; if (selectedIDs.Count > 0) { TreeViewItem item = FindItem(selectedIDs[0], rootItem); if (item is LeafItem) { SelectedObject = (item as LeafItem).Data; } } } protected override void DoubleClickedItem(int id) { TreeViewItem item = FindItem(id, rootItem); if (item is LeafItem) { DoubleClickedObject = (item as LeafItem).Data; } } float oldBaseIndent; protected override void BeforeRowsGUI() { oldBaseIndent = baseIndent; DoubleClickedObject = null; } protected override void RowGUI(RowGUIArgs args) { if (hasSearch) { // Hack to undo TreeView flattening the hierarchy when searching baseIndent = oldBaseIndent + args.item.depth * depthIndentWidth; } base.RowGUI(args); TreeViewItem item = args.item; if (Event.current.type == EventType.MouseUp && item is FolderItem && item.hasChildren) { Rect rect = args.rowRect; rect.xMin = GetContentIndent(item); if (rect.Contains(Event.current.mousePosition)) { SetExpanded(item.id, !IsExpanded(item.id)); Event.current.Use(); } } } protected override void AfterRowsGUI() { baseIndent = oldBaseIndent; } [Serializable] public class State { public State() : this(new TreeViewState()) { } public State(TreeViewState baseState) { this.baseState = baseState; } public TreeViewState baseState; public List noSearchExpandState; public ScriptableObject selectedObject; public List itemPaths = new List(); public List itemIDs = new List(); public TypeFilter typeFilter = TypeFilter.All; public bool dragEnabled = true; } new public State state { get { State result = new State(base.state); if (noSearchExpandState != null) { result.noSearchExpandState = new List(noSearchExpandState); } result.selectedObject = SelectedObject; foreach (var entry in itemIDs) { result.itemPaths.Add(entry.Key); result.itemIDs.Add(entry.Value); } result.typeFilter = TypeFilter; result.dragEnabled = true; return result; } } } private Texture2D borderIcon; private GUIStyle borderStyle; private void AffirmResources() { if (borderIcon == null) { borderIcon = EditorGUIUtility.Load("FMOD/Border.png") as Texture2D; borderStyle = new GUIStyle(GUI.skin.box); borderStyle.normal.background = borderIcon; borderStyle.margin = new RectOffset(); } } [NonSerialized] TreeView treeView; [NonSerialized] SearchField searchField; [SerializeField] PreviewArea previewArea = new PreviewArea(); [SerializeField] TreeView.State treeViewState; [NonSerialized] DateTime LastKnownCacheTime; private SerializedProperty outputProperty; bool InChooserMode { get { return outputProperty != null; } } void OnGUI() { AffirmResources(); if (InChooserMode) { GUILayout.BeginVertical(borderStyle, GUILayout.ExpandWidth(true)); } treeView.searchString = searchField.OnGUI(treeView.searchString); Rect treeRect = GUILayoutUtility.GetRect(0, 0, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); treeRect.y += 2; treeRect.height -= 2; treeView.OnGUI(treeRect); if (InChooserMode) { GUILayout.EndVertical(); HandleChooserModeEvents(); } else { previewArea.treeView = treeView; previewArea.OnGUI(cachedMetering != null ? cachedMetering : EditorUtils.GetMetering()); } } void HandleChooserModeEvents() { if (Event.current.isKey) { KeyCode keyCode = Event.current.keyCode; if ((keyCode == KeyCode.Return || keyCode == KeyCode.KeypadEnter) && treeView.SelectedObject != null) { SetOutputProperty(treeView.SelectedObject); Event.current.Use(); Close(); } else if (keyCode == KeyCode.Escape) { Event.current.Use(); Close(); } } else if (treeView.DoubleClickedObject != null) { SetOutputProperty(treeView.DoubleClickedObject); Close(); } } private void SetOutputProperty(ScriptableObject data) { if (data is EditorEventRef) { string path = (data as EditorEventRef).Path; outputProperty.stringValue = path; EditorUtils.UpdateParamsOnEmitter(outputProperty.serializedObject, path); } else if (data is EditorBankRef) { outputProperty.stringValue = (data as EditorBankRef).Name; } else if (data is EditorParamRef) { outputProperty.stringValue = (data as EditorParamRef).Name; } outputProperty.serializedObject.ApplyModifiedProperties(); } [Serializable] class PreviewArea { [NonSerialized] public TreeView treeView; public bool forceRepaint { get { return transportControls.forceRepaint; } } [NonSerialized] private EditorEventRef currentEvent; void SetEvent(EditorEventRef eventRef) { if (eventRef != currentEvent) { currentEvent = eventRef; EditorUtils.PreviewStop(); transportControls.Reset(); event3DPreview.Reset(); parameterControls.Reset(); } } [SerializeField] DetailsView detailsView = new DetailsView(); [SerializeField] TransportControls transportControls = new TransportControls(); [SerializeField] Event3DPreview event3DPreview = new Event3DPreview(); [SerializeField] PreviewMeters meters = new PreviewMeters(); [SerializeField] EventParameterControls parameterControls = new EventParameterControls(); private GUIStyle mainStyle; private void AffirmResources() { if (mainStyle == null) { mainStyle = new GUIStyle(GUI.skin.box); mainStyle.margin = new RectOffset(); } } public void OnGUI(float[] metering) { AffirmResources(); ScriptableObject selectedObject = treeView.SelectedObject; if (selectedObject is EditorEventRef) { SetEvent(selectedObject as EditorEventRef); } else { SetEvent(null); } if (selectedObject != null) { GUILayout.BeginVertical(mainStyle, GUILayout.ExpandWidth(true)); if (selectedObject is EditorEventRef) { EditorEventRef eventRef = selectedObject as EditorEventRef; if (eventRef.Path.StartsWith("event:")) { DrawEventPreview(eventRef, metering); } else if (eventRef.Path.StartsWith("snapshot:")) { detailsView.DrawSnapshot(eventRef); } } else if (selectedObject is EditorBankRef) { detailsView.DrawBank(selectedObject as EditorBankRef); } else if (selectedObject is EditorParamRef) { detailsView.DrawParameter(selectedObject as EditorParamRef); } GUILayout.EndVertical(); if (Event.current.type == EventType.Repaint) { Rect rect = GUILayoutUtility.GetLastRect(); isNarrow = rect.width < 600; } } } private void DrawSeparatorLine() { GUILayout.Box(GUIContent.none, GUILayout.Height(1), GUILayout.ExpandWidth(true)); } private bool isNarrow; private void DrawEventPreview(EditorEventRef eventRef, float[] metering) { detailsView.DrawEvent(eventRef, isNarrow); DrawSeparatorLine(); // Playback controls, 3D Preview and meters EditorGUILayout.BeginHorizontal(GUILayout.Height(event3DPreview.Height)); GUILayout.FlexibleSpace(); EditorGUILayout.BeginVertical(); if (!isNarrow) { GUILayout.FlexibleSpace(); } transportControls.OnGUI(eventRef, parameterControls.ParameterValues); if (isNarrow) { EditorGUILayout.Separator(); meters.OnGUI(true, metering); } else { GUILayout.FlexibleSpace(); } EditorGUILayout.EndVertical(); event3DPreview.OnGUI(eventRef); if (!isNarrow) { meters.OnGUI(false, metering); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); DrawSeparatorLine(); parameterControls.OnGUI(eventRef); } } [Serializable] class DetailsView { private Texture copyIcon; private GUIStyle textFieldNameStyle; private void AffirmResources() { if (copyIcon == null) { copyIcon = EditorGUIUtility.Load("FMOD/CopyIcon.png") as Texture; textFieldNameStyle = new GUIStyle(EditorStyles.label); textFieldNameStyle.fontStyle = FontStyle.Bold; } } public void DrawEvent(EditorEventRef selectedEvent, bool isNarrow) { AffirmResources(); DrawCopyableTextField("Full Path", selectedEvent.Path); DrawTextField("Banks", string.Join(", ", selectedEvent.Banks.Select(x => x.Name).ToArray())); EditorGUILayout.BeginHorizontal(); DrawTextField("Panning", selectedEvent.Is3D ? "3D" : "2D"); DrawTextField("Oneshot", selectedEvent.IsOneShot.ToString()); TimeSpan t = TimeSpan.FromMilliseconds(selectedEvent.Length); DrawTextField("Length", selectedEvent.Length > 0 ? string.Format("{0:D2}:{1:D2}:{2:D3}", t.Minutes, t.Seconds, t.Milliseconds) : "N/A"); if (!isNarrow) DrawTextField("Streaming", selectedEvent.IsStream.ToString()); EditorGUILayout.EndHorizontal(); if (isNarrow) DrawTextField("Streaming", selectedEvent.IsStream.ToString()); } public void DrawSnapshot(EditorEventRef eventRef) { AffirmResources(); DrawCopyableTextField("Full Path", eventRef.Path); } public void DrawBank(EditorBankRef bank) { AffirmResources(); DrawCopyableTextField("Full Path", "bank:/" + bank.Name); string[] SizeSuffix = { "B", "KB", "MB", "GB" }; GUILayout.Label("Platform Bank Sizes", textFieldNameStyle); EditorGUI.indentLevel++; foreach (var sizeInfo in bank.FileSizes) { int order = 0; long size = sizeInfo.Value; while (size >= 1024 && order + 1 < SizeSuffix.Length) { order++; size /= 1024; } EditorGUILayout.LabelField(sizeInfo.Name, string.Format("{0} {1}", size, SizeSuffix[order])); } EditorGUI.indentLevel--; } public void DrawParameter(EditorParamRef parameter) { AffirmResources(); DrawCopyableTextField("Name", parameter.Name); DrawCopyableTextField("ID", string.Format("{{ data1 = 0x{0:x8}, data2 = 0x{1:x8} }}", parameter.ID.data1, parameter.ID.data2)); DrawTextField("Minimum", parameter.Min.ToString()); DrawTextField("Maximum", parameter.Max.ToString()); } private void DrawCopyableTextField(string name, string value) { EditorGUILayout.BeginHorizontal(); DrawTextField(name, value); if (GUILayout.Button(copyIcon, GUILayout.ExpandWidth(false))) { EditorGUIUtility.systemCopyBuffer = value; } EditorGUILayout.EndHorizontal(); } private void DrawTextField(string name, string content) { EditorGUILayout.BeginHorizontal(); GUILayout.Label(name, textFieldNameStyle, GUILayout.Width(75)); GUILayout.Label(content); EditorGUILayout.EndHorizontal(); } } [Serializable] class TransportControls { public bool forceRepaint { get; private set; } public void Reset() { forceRepaint = false; } private Texture playOff; private Texture playOn; private Texture stopOff; private Texture stopOn; private Texture openIcon; private GUIStyle buttonStyle; private void AffirmResources() { if (playOff == null) { playOff = EditorGUIUtility.Load("FMOD/TransportPlayButtonOff.png") as Texture; playOn = EditorGUIUtility.Load("FMOD/TransportPlayButtonOn.png") as Texture; stopOff = EditorGUIUtility.Load("FMOD/TransportStopButtonOff.png") as Texture; stopOn = EditorGUIUtility.Load("FMOD/TransportStopButtonOn.png") as Texture; openIcon = EditorGUIUtility.Load("FMOD/transportOpen.png") as Texture; buttonStyle = new GUIStyle(); buttonStyle.padding.left = 4; buttonStyle.padding.top = 10; } } public void OnGUI(EditorEventRef selectedEvent, Dictionary parameterValues) { AffirmResources(); var previewState = EditorUtils.PreviewState; bool playing = previewState == PreviewState.Playing; bool paused = previewState == PreviewState.Paused; bool stopped = previewState == PreviewState.Stopped; EditorGUILayout.BeginHorizontal(); if (GUILayout.Button(stopped || paused ? stopOn : stopOff, buttonStyle, GUILayout.ExpandWidth(false))) { forceRepaint = false; if (paused) { EditorUtils.PreviewStop(); } if (playing) { EditorUtils.PreviewPause(); } } if (GUILayout.Button(playing ? playOn : playOff, buttonStyle, GUILayout.ExpandWidth(false))) { if (playing || stopped) { EditorUtils.PreviewEvent(selectedEvent, parameterValues); } else { EditorUtils.PreviewPause(); } forceRepaint = true; } if (GUILayout.Button(new GUIContent(openIcon, "Show Event in FMOD Studio"), buttonStyle, GUILayout.ExpandWidth(false))) { string cmd = string.Format("studio.window.navigateTo(studio.project.lookup(\"{0}\"))", selectedEvent.Guid.ToString("b")); EditorUtils.SendScriptCommand(cmd); } EditorGUILayout.EndHorizontal(); } } [Serializable] class Event3DPreview { private bool isDragging; private Rect arenaRect; private Vector2 eventPosition; private float eventDistance = 0; private float eventOrientation = 0; public void Reset() { eventPosition = new Vector2(0, 0); eventDistance = 0; eventOrientation = 0; } private Texture arena; private Texture emitter; private void AffirmResources() { if (arena == null) { arena = EditorGUIUtility.Load("FMOD/preview.png") as Texture; emitter = EditorGUIUtility.Load("FMOD/previewemitter.png") as Texture; } } public float Height { get { AffirmResources(); return GUI.skin.label.CalcSize(new GUIContent(arena)).y; } } public void OnGUI(EditorEventRef selectedEvent) { AffirmResources(); var originalColour = GUI.color; if (!selectedEvent.Is3D) { GUI.color = new Color(1.0f, 1.0f, 1.0f, 0.1f); } GUILayout.Label(arena, GUILayout.ExpandWidth(false)); if (Event.current.type == EventType.Repaint) { arenaRect = GUILayoutUtility.GetLastRect(); } Vector2 center = arenaRect.center; Rect rect2 = new Rect(center.x + eventPosition.x - 6, center.y + eventPosition.y - 6, 12, 12); GUI.DrawTexture(rect2, emitter); GUI.color = originalColour; if (selectedEvent.Is3D) { bool useGUIEvent = false; switch (Event.current.type) { case EventType.MouseDown: if (arenaRect.Contains(Event.current.mousePosition)) { isDragging = true; useGUIEvent = true; } break; case EventType.MouseUp: if (isDragging) { isDragging = false; useGUIEvent = true; } break; case EventType.MouseDrag: if (isDragging) { useGUIEvent = true; } break; } if (useGUIEvent) { Vector2 newPosition = Event.current.mousePosition; Vector2 delta = newPosition - center; float maximumDistance = (arena.width - emitter.width) / 2; float distance = Math.Min(delta.magnitude, maximumDistance); delta.Normalize(); eventPosition = delta * distance; eventDistance = distance / maximumDistance * selectedEvent.MaxDistance; float angle = Mathf.Atan2(delta.y, delta.x); eventOrientation = angle + Mathf.PI * 0.5f; Event.current.Use(); } } EditorUtils.PreviewUpdatePosition(eventDistance, eventOrientation); } } [Serializable] class EventParameterControls { [NonSerialized] private Dictionary parameterValues = new Dictionary(); public Dictionary ParameterValues { get { return parameterValues; } } [NonSerialized] private Vector2 scrollPosition; public void Reset() { parameterValues.Clear(); } public void OnGUI(EditorEventRef selectedEvent) { scrollPosition = GUILayout.BeginScrollView(scrollPosition, GUILayout.Height(EditorGUIUtility.singleLineHeight * 3.5f)); foreach (EditorParamRef paramRef in selectedEvent.Parameters) { if (!parameterValues.ContainsKey(paramRef.Name)) { parameterValues[paramRef.Name] = paramRef.Default; } parameterValues[paramRef.Name] = EditorGUILayout.Slider(paramRef.Name, parameterValues[paramRef.Name], paramRef.Min, paramRef.Max); EditorUtils.PreviewUpdateParameter(paramRef.ID, parameterValues[paramRef.Name]); } GUILayout.EndScrollView(); } } [Serializable] class PreviewMeters { private Texture meterOn; private Texture meterOff; private void AffirmResources() { if (meterOn == null) { meterOn = EditorGUIUtility.Load("FMOD/LevelMeter.png") as Texture; meterOff = EditorGUIUtility.Load("FMOD/LevelMeterOff.png") as Texture; } } public void OnGUI(bool minimized, float[] metering) { AffirmResources(); int meterHeight = minimized ? 86 : 128; int meterWidth = (int)((128 / (float)meterOff.height) * meterOff.width); List meterPositions = meterPositionsForSpeakerMode(speakerModeForChannelCount(metering.Length), meterWidth, 2, 6); const int MeterCountMaximum = 16; int minimumWidth = meterWidth * MeterCountMaximum; Rect fullRect = GUILayoutUtility.GetRect(minimumWidth, meterHeight, GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true)); float baseX = fullRect.x + (fullRect.width - (meterWidth * metering.Length)) / 2; for(int i = 0; i < metering.Length; i++) { Rect meterRect = new Rect(baseX + meterPositions[i], fullRect.y, meterWidth, fullRect.height); GUI.DrawTexture(meterRect, meterOff); float db = 20.0f * Mathf.Log10(metering[i] * Mathf.Sqrt(2.0f)); db = Mathf.Clamp(db, -80.0f, 10.0f); float visible = 0; int[] segmentPixels = new int[] { 0, 18, 38, 60, 89, 130, 187, 244, 300 }; float[] segmentDB = new float[] { -80.0f, -60.0f, -50.0f, -40.0f, -30.0f, -20.0f, -10.0f, 0, 10.0f }; int segment = 1; while (segmentDB[segment] < db) { segment++; } visible = segmentPixels[segment - 1] + ((db - segmentDB[segment - 1]) / (segmentDB[segment] - segmentDB[segment - 1])) * (segmentPixels[segment] - segmentPixels[segment - 1]); visible *= fullRect.height / (float)meterOff.height; Rect levelPosRect = new Rect(meterRect.x, fullRect.height - visible + meterRect.y, meterWidth, visible); Rect levelUVRect = new Rect(0, 0, 1.0f, visible / fullRect.height); GUI.DrawTextureWithTexCoords(levelPosRect, meterOn, levelUVRect); } } private FMOD.SPEAKERMODE speakerModeForChannelCount(int channelCount) { switch(channelCount) { case 1: return FMOD.SPEAKERMODE.MONO; case 4: return FMOD.SPEAKERMODE.QUAD; case 5: return FMOD.SPEAKERMODE.SURROUND; case 6: return FMOD.SPEAKERMODE._5POINT1; case 8: return FMOD.SPEAKERMODE._7POINT1; case 12: return FMOD.SPEAKERMODE._7POINT1POINT4; default: return FMOD.SPEAKERMODE.STEREO; } } private List meterPositionsForSpeakerMode(FMOD.SPEAKERMODE mode, float meterWidth, float groupGap, float lfeGap) { List offsets = new List(); switch(mode) { case FMOD.SPEAKERMODE.MONO: // M offsets.Add(0); break; case FMOD.SPEAKERMODE.STEREO: // L R offsets.Add(0); offsets.Add(meterWidth); break; case FMOD.SPEAKERMODE.QUAD: switch(Settings.Instance.MeterChannelOrdering) { case MeterChannelOrderingType.Standard: case MeterChannelOrderingType.SeparateLFE: // L R | LS RS offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // LS offsets.Add(meterWidth*3 + groupGap); // RS break; case MeterChannelOrderingType.Positional: // LS | L R | RS offsets.Add(meterWidth*1 + groupGap); // L offsets.Add(meterWidth*2 + groupGap); // R offsets.Add(0); // LS offsets.Add(meterWidth*3 + groupGap*2); // RS break; } break; case FMOD.SPEAKERMODE.SURROUND: switch(Settings.Instance.MeterChannelOrdering) { case MeterChannelOrderingType.Standard: case MeterChannelOrderingType.SeparateLFE: // L R | C | LS RS offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*3 + groupGap*2); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS break; case MeterChannelOrderingType.Positional: // LS | L C R | RS offsets.Add(meterWidth*1 + groupGap); // L offsets.Add(meterWidth*3 + groupGap); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(0); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS break; } break; case FMOD.SPEAKERMODE._5POINT1: switch(Settings.Instance.MeterChannelOrdering) { case MeterChannelOrderingType.Standard: // L R | C | LFE | LS RS offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*3 + groupGap*2); // LFE offsets.Add(meterWidth*4 + groupGap*3); // LS offsets.Add(meterWidth*5 + groupGap*3); // RS break; case MeterChannelOrderingType.SeparateLFE: // L R | C | LS RS || LFE offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*5 + groupGap*2 + lfeGap); // LFE offsets.Add(meterWidth*3 + groupGap*2); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS break; case MeterChannelOrderingType.Positional: // LS | L C R | RS || LFE offsets.Add(meterWidth*1 + groupGap); // L offsets.Add(meterWidth*3 + groupGap); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*5 + groupGap*2 + lfeGap); // LFE offsets.Add(0); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS break; } break; case FMOD.SPEAKERMODE._7POINT1: switch(Settings.Instance.MeterChannelOrdering) { case MeterChannelOrderingType.Standard: // L R | C | LFE | LS RS | LSR RSR offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*3 + groupGap*2); // LFE offsets.Add(meterWidth*4 + groupGap*3); // LS offsets.Add(meterWidth*5 + groupGap*3); // RS offsets.Add(meterWidth*6 + groupGap*4); // LSR offsets.Add(meterWidth*7 + groupGap*4); // RSR break; case MeterChannelOrderingType.SeparateLFE: // L R | C | LS RS | LSR RSR || LFE offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*7 + groupGap*3 + lfeGap); // LFE offsets.Add(meterWidth*3 + groupGap*2); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS offsets.Add(meterWidth*5 + groupGap*3); // LSR offsets.Add(meterWidth*6 + groupGap*3); // RSR break; case MeterChannelOrderingType.Positional: // LSR LS | L C R | RS RSR || LFE offsets.Add(meterWidth*2 + groupGap); // L offsets.Add(meterWidth*4 + groupGap); // R offsets.Add(meterWidth*3 + groupGap); // C offsets.Add(meterWidth*7 + groupGap*2 + lfeGap); // LFE offsets.Add(meterWidth*1); // LS offsets.Add(meterWidth*5 + groupGap*2); // RS offsets.Add(0); // LSR offsets.Add(meterWidth*6 + groupGap*2); // RSR break; } break; case FMOD.SPEAKERMODE._7POINT1POINT4: switch(Settings.Instance.MeterChannelOrdering) { case MeterChannelOrderingType.Standard: // L R | C | LFE | LS RS | LSR RSR | TFL TFR TBL TBR offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*3 + groupGap*2); // LFE offsets.Add(meterWidth*4 + groupGap*3); // LS offsets.Add(meterWidth*5 + groupGap*3); // RS offsets.Add(meterWidth*6 + groupGap*4); // LSR offsets.Add(meterWidth*7 + groupGap*4); // RSR offsets.Add(meterWidth*8 + groupGap*5); // TFL offsets.Add(meterWidth*9 + groupGap*5); // TFR offsets.Add(meterWidth*10 + groupGap*5); // TBL offsets.Add(meterWidth*11 + groupGap*5); // TBR break; case MeterChannelOrderingType.SeparateLFE: // L R | C | LS RS | LSR RSR | TFL TFR TBL TBR || LFE offsets.Add(0); // L offsets.Add(meterWidth*1); // R offsets.Add(meterWidth*2 + groupGap); // C offsets.Add(meterWidth*11 + groupGap*4 + lfeGap); // LFE offsets.Add(meterWidth*3 + groupGap*2); // LS offsets.Add(meterWidth*4 + groupGap*2); // RS offsets.Add(meterWidth*5 + groupGap*3); // LSR offsets.Add(meterWidth*6 + groupGap*3); // RSR offsets.Add(meterWidth*7 + groupGap*4); // TFL offsets.Add(meterWidth*8 + groupGap*4); // TFR offsets.Add(meterWidth*9 + groupGap*4); // TBL offsets.Add(meterWidth*10 + groupGap*4); // TBR break; case MeterChannelOrderingType.Positional: // LSR LS | L C R | RS RSR | TBL TFL TFR TBR || LFE offsets.Add(meterWidth*2 + groupGap); // L offsets.Add(meterWidth*4 + groupGap); // R offsets.Add(meterWidth*3 + groupGap); // C offsets.Add(meterWidth*11 + groupGap*3 + lfeGap); // LFE offsets.Add(meterWidth*1); // LS offsets.Add(meterWidth*5 + groupGap*2); // RS offsets.Add(0); // LSR offsets.Add(meterWidth*6 + groupGap*2); // RSR offsets.Add(meterWidth*8 + groupGap*3); // TFL offsets.Add(meterWidth*9 + groupGap*3); // TFR offsets.Add(meterWidth*7 + groupGap*3); // TBL offsets.Add(meterWidth*10 + groupGap*3); // TBR break; } break; } return offsets; } } [Flags] enum TypeFilter { Event = 1, Bank = 2, Parameter = 4, All = Event | Bank | Parameter, } public void ChooseEvent(SerializedProperty property) { BeginInspectorPopup(property, TypeFilter.Event); if (!string.IsNullOrEmpty(property.stringValue)) { treeView.JumpToEvent(property.stringValue); } } public void ChooseBank(SerializedProperty property) { BeginInspectorPopup(property, TypeFilter.Bank); if (!string.IsNullOrEmpty(property.stringValue)) { treeView.JumpToBank(property.stringValue); } } public void ChooseParameter(SerializedProperty property) { BeginInspectorPopup(property, TypeFilter.Parameter); } public void FrameEvent(string path) { treeView.JumpToEvent(path); } private void BeginInspectorPopup(SerializedProperty property, TypeFilter typeFilter) { treeView.TypeFilter = typeFilter; outputProperty = property; searchField.SetFocus(); treeView.DragEnabled = false; } private void BeginStandaloneWindow() { treeView.TypeFilter = TypeFilter.All; outputProperty = null; searchField.SetFocus(); treeView.DragEnabled = true; } public void OnEnable() { if (treeViewState == null) { treeViewState = new TreeView.State(); } searchField = new SearchField(); treeView = new TreeView(treeViewState); ReadEventCache(); searchField.downOrUpArrowKeyPressed += treeView.SetFocus; #if UNITY_2019_1_OR_NEWER SceneView.duringSceneGui += SceneUpdate; #else SceneView.onSceneGUIDelegate += SceneUpdate; #endif EditorApplication.hierarchyWindowItemOnGUI += HierarchyUpdate; IsOpen = true; } public void OnDestroy() { EditorUtils.PreviewStop(); IsOpen = false; } private static bool IsDraggable(UnityEngine.Object data) { return data is EditorEventRef || data is EditorBankRef || data is EditorParamRef; } public static bool IsDroppable(UnityEngine.Object[] data) { return data.Length > 0 && IsDraggable(data[0]); } // This is an event handler on the hierachy view to handle dragging our objects from the browser void HierarchyUpdate(int instance, Rect rect) { if (Event.current.type == EventType.DragPerform && rect.Contains(Event.current.mousePosition)) { if (IsDroppable(DragAndDrop.objectReferences)) { UnityEngine.Object data = DragAndDrop.objectReferences[0]; GameObject target = EditorUtility.InstanceIDToObject(instance) as GameObject; if (data is EditorEventRef) { Undo.SetCurrentGroupName("Add Studio Event Emitter"); StudioEventEmitter emitter = Undo.AddComponent(target); emitter.Event = (data as EditorEventRef).Path; } else if (data is EditorBankRef) { Undo.SetCurrentGroupName("Add Studio Bank Loader"); StudioBankLoader loader = Undo.AddComponent(target); loader.Banks = new List(); loader.Banks.Add((data as EditorBankRef).Name); } else // data is EditorParamRef { Undo.SetCurrentGroupName("Add Studio Global Parameter Trigger"); StudioGlobalParameterTrigger trigger = Undo.AddComponent(target); trigger.parameter = (data as EditorParamRef).Name; } Selection.activeObject = target; Event.current.Use(); } } } // This is an event handler on the scene view to handle dragging our objects from the browser // and creating new gameobjects void SceneUpdate(SceneView sceneView) { if (Event.current.type == EventType.DragPerform && IsDroppable(DragAndDrop.objectReferences)) { UnityEngine.Object data = DragAndDrop.objectReferences[0]; GameObject newObject; if (data is EditorEventRef) { string path = (data as EditorEventRef).Path; string name = path.Substring(path.LastIndexOf("/") + 1); newObject = new GameObject(name + " Emitter"); StudioEventEmitter emitter = newObject.AddComponent(); emitter.Event = path; Undo.RegisterCreatedObjectUndo(newObject, "Create Studio Event Emitter"); } else if (data is EditorBankRef) { newObject = new GameObject("Studio Bank Loader"); StudioBankLoader loader = newObject.AddComponent(); loader.Banks = new List(); loader.Banks.Add((data as EditorBankRef).Name); Undo.RegisterCreatedObjectUndo(newObject, "Create Studio Bank Loader"); } else // data is EditorParamRef { string name = (data as EditorParamRef).Name; newObject = new GameObject(name + " Trigger"); StudioGlobalParameterTrigger trigger = newObject.AddComponent(); trigger.parameter = name; Undo.RegisterCreatedObjectUndo(newObject, "Create Studio Global Parameter Trigger"); } Ray ray = HandleUtility.GUIPointToWorldRay(Event.current.mousePosition); object hit = HandleUtility.RaySnap(ray); if (hit != null) { newObject.transform.position = ((RaycastHit)hit).point; } else { newObject.transform.position = ray.origin + ray.direction * 10.0f; } Selection.activeObject = newObject; Event.current.Use(); } else if (Event.current.type == EventType.DragUpdated && IsDroppable(DragAndDrop.objectReferences)) { DragAndDrop.visualMode = DragAndDropVisualMode.Move; DragAndDrop.AcceptDrag(); Event.current.Use(); } } } }