//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using BansheeEngine; namespace BansheeEditor { /** @addtogroup Windows * @{ */ /// /// Displays animation curve editor window. /// [DefaultSize(900, 500)] internal class AnimationWindow : EditorWindow { /// /// A set of animation curves for a field of a certain type. /// private struct FieldCurves { public SerializableProperty.FieldType type; public EdAnimationCurve[] curves; } private const int FIELD_DISPLAY_WIDTH = 200; private bool isInitialized; private GUIButton playButton; private GUIButton recordButton; private GUIButton prevFrameButton; private GUIIntField frameInputField; private GUIButton nextFrameButton; private GUIButton addKeyframeButton; private GUIButton addEventButton; private GUIButton optionsButton; private GUIButton addPropertyBtn; private GUIButton delPropertyBtn; private GUILayout buttonLayout; private int buttonLayoutHeight; private GUIPanel editorPanel; private GUIAnimFieldDisplay guiFieldDisplay; private GUICurveEditor guiCurveEditor; private SceneObject selectedSO; private int currentFrameIdx; private int fps = 1; private Dictionary curves = new Dictionary(); private List selectedFields = new List(); internal int FPS { get { return fps; } set { guiCurveEditor.SetFPS(value); fps = MathEx.Max(value, 1); } } /// /// Opens the animation window. /// [MenuItem("Windows/Animation", ButtonModifier.CtrlAlt, ButtonCode.A, 6000)] private static void OpenGameWindow() { OpenWindow(); } /// protected override LocString GetDisplayName() { return new LocEdString("Animation"); } private void OnInitialize() { Selection.OnSelectionChanged += OnSelectionChanged; EditorInput.OnPointerPressed += OnPointerPressed; EditorInput.OnPointerMoved += OnPointerMoved; EditorInput.OnPointerReleased += OnPointerReleased; EditorInput.OnButtonUp += OnButtonUp; Rebuild(); } private void OnEditorUpdate() { } private void OnDestroy() { Selection.OnSelectionChanged -= OnSelectionChanged; EditorInput.OnPointerPressed -= OnPointerPressed; EditorInput.OnPointerMoved -= OnPointerMoved; EditorInput.OnPointerReleased -= OnPointerReleased; EditorInput.OnButtonUp -= OnButtonUp; } protected override void WindowResized(int width, int height) { if (!isInitialized) return; guiFieldDisplay.SetSize(FIELD_DISPLAY_WIDTH, height - buttonLayoutHeight*2); int curveEditorWidth = Math.Max(0, width - FIELD_DISPLAY_WIDTH); guiCurveEditor.SetSize(curveEditorWidth, height - buttonLayoutHeight); guiCurveEditor.Redraw(); } private void Rebuild() { GUI.Clear(); selectedFields.Clear(); curves.Clear(); isInitialized = false; selectedSO = Selection.SceneObject; if (selectedSO == null) { GUILabel warningLbl = new GUILabel(new LocEdString("Select an object to animate in the Hierarchy or Scene windows.")); GUILayoutY vertLayout = GUI.AddLayoutY(); vertLayout.AddFlexibleSpace(); GUILayoutX horzLayout = vertLayout.AddLayoutX(); vertLayout.AddFlexibleSpace(); horzLayout.AddFlexibleSpace(); horzLayout.AddElement(warningLbl); horzLayout.AddFlexibleSpace(); return; } // TODO - Retrieve Animation & AnimationClip from the selected object, fill curves dictionary // - If not available, show a button to create new animation clip // Top button row GUIContent playIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Play), new LocEdString("Play")); GUIContent recordIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.Record), new LocEdString("Record")); GUIContent prevFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameBack), new LocEdString("Previous frame")); GUIContent nextFrameIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.FrameForward), new LocEdString("Next frame")); GUIContent addKeyframeIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddKeyframe), new LocEdString("Add keyframe")); GUIContent addEventIcon = new GUIContent(EditorBuiltin.GetAnimationWindowIcon(AnimationWindowIcon.AddEvent), new LocEdString("Add event")); GUIContent optionsIcon = new GUIContent(EditorBuiltin.GetLibraryWindowIcon(LibraryWindowIcon.Options), new LocEdString("Options")); playButton = new GUIButton(playIcon); recordButton = new GUIButton(recordIcon); prevFrameButton = new GUIButton(prevFrameIcon); frameInputField = new GUIIntField(); nextFrameButton = new GUIButton(nextFrameIcon); addKeyframeButton = new GUIButton(addKeyframeIcon); addEventButton = new GUIButton(addEventIcon); optionsButton = new GUIButton(optionsIcon); playButton.OnClick += () => { // TODO // - Record current state of the scene object hierarchy // - Evaluate all curves manually and update them // - On end, restore original values of the scene object hierarchy }; recordButton.OnClick += () => { // TODO // - Every frame read back current values of all the current curve's properties and assign it to the current frame }; prevFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx - 1); }; frameInputField.OnChanged += SetCurrentFrame; nextFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx + 1); }; addKeyframeButton.OnClick += () => { guiCurveEditor.AddKeyFrameAtMarker(); // TODO - Update local curves? }; addEventButton.OnClick += () => { // TODO - Add event }; optionsButton.OnClick += () => { Vector2I openPosition = ScreenToWindowPos(Input.PointerPosition); AnimationOptions dropDown = DropDownWindow.Open(this, openPosition); dropDown.Initialize(this); }; // Property buttons addPropertyBtn = new GUIButton(new LocEdString("Add property")); delPropertyBtn = new GUIButton(new LocEdString("Delete selected")); addPropertyBtn.OnClick += () => { Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition); FieldSelectionWindow fieldSelection = DropDownWindow.Open(this, windowPos); fieldSelection.OnFieldSelected += OnFieldAdded; }; delPropertyBtn.OnClick += () => { LocEdString title = new LocEdString("Warning"); LocEdString message = new LocEdString("Are you sure you want to remove all selected fields?"); DialogBox.Open(title, message, DialogBox.Type.YesNo, x => { if (x == DialogBox.ResultType.Yes) { RemoveSelectedFields(); } }); }; GUILayout mainLayout = GUI.AddLayoutY(); buttonLayout = mainLayout.AddLayoutX(); buttonLayout.AddSpace(5); buttonLayout.AddElement(playButton); buttonLayout.AddElement(recordButton); buttonLayout.AddSpace(5); buttonLayout.AddElement(prevFrameButton); buttonLayout.AddElement(frameInputField); buttonLayout.AddElement(nextFrameButton); buttonLayout.AddSpace(5); buttonLayout.AddElement(addKeyframeButton); buttonLayout.AddElement(addEventButton); buttonLayout.AddSpace(5); buttonLayout.AddElement(optionsButton); buttonLayout.AddFlexibleSpace(); buttonLayoutHeight = playButton.Bounds.height; GUILayout contentLayout = mainLayout.AddLayoutX(); GUILayout fieldDisplayLayout = contentLayout.AddLayoutY(GUIOption.FixedWidth(FIELD_DISPLAY_WIDTH)); guiFieldDisplay = new GUIAnimFieldDisplay(fieldDisplayLayout, FIELD_DISPLAY_WIDTH, Height - buttonLayoutHeight * 2, selectedSO); guiFieldDisplay.OnEntrySelected += OnFieldSelected; GUILayout bottomButtonLayout = fieldDisplayLayout.AddLayoutX(); bottomButtonLayout.AddElement(addPropertyBtn); bottomButtonLayout.AddElement(delPropertyBtn); GUILayout curveLayout = contentLayout.AddLayoutY(); editorPanel = curveLayout.AddPanel(); int curveEditorWidth = Math.Max(0, Width - FIELD_DISPLAY_WIDTH); guiCurveEditor = new GUICurveEditor(this, editorPanel, curveEditorWidth, Height - buttonLayoutHeight); guiCurveEditor.OnFrameSelected += OnFrameSelected; guiCurveEditor.Redraw(); SetCurrentFrame(currentFrameIdx); isInitialized = true; } private void SetCurrentFrame(int frameIdx) { currentFrameIdx = Math.Max(0, frameIdx); frameInputField.Value = currentFrameIdx; guiCurveEditor.SetMarkedFrame(currentFrameIdx); float time = guiCurveEditor.GetTimeForFrame(currentFrameIdx); List values = new List(); foreach (var kvp in curves) { SerializableProperty property = GUIAnimFieldDisplay.FindProperty(selectedSO, kvp.Key); if (property != null) { GUIAnimFieldPathValue fieldValue = new GUIAnimFieldPathValue(); fieldValue.path = kvp.Key; switch (kvp.Value.type) { case SerializableProperty.FieldType.Vector2: { Vector2 value = new Vector2(); for(int i = 0; i < 2; i++) value[i] = kvp.Value.curves[i].Evaluate(time, false); fieldValue.value = value; } break; case SerializableProperty.FieldType.Vector3: { Vector3 value = new Vector3(); for (int i = 0; i < 3; i++) value[i] = kvp.Value.curves[i].Evaluate(time, false); fieldValue.value = value; } break; case SerializableProperty.FieldType.Vector4: { Vector4 value = new Vector4(); for (int i = 0; i < 4; i++) value[i] = kvp.Value.curves[i].Evaluate(time, false); fieldValue.value = value; } break; case SerializableProperty.FieldType.Color: { Color value = new Color(); for (int i = 0; i < 4; i++) value[i] = kvp.Value.curves[i].Evaluate(time, false); fieldValue.value = value; } break; case SerializableProperty.FieldType.Bool: case SerializableProperty.FieldType.Int: case SerializableProperty.FieldType.Float: fieldValue.value = kvp.Value.curves[0].Evaluate(time, false); ; break; } values.Add(fieldValue); } } guiFieldDisplay.SetDisplayValues(values.ToArray()); } private void OnPointerPressed(PointerEvent ev) { if (!isInitialized) return; guiCurveEditor.OnPointerPressed(ev); } private void OnPointerMoved(PointerEvent ev) { if (!isInitialized) return; guiCurveEditor.OnPointerMoved(ev); } private void OnPointerReleased(PointerEvent ev) { if (!isInitialized) return; guiCurveEditor.OnPointerReleased(ev); } private void OnButtonUp(ButtonEvent ev) { if (!isInitialized) return; guiCurveEditor.OnButtonUp(ev); } private void UpdateDisplayedCurves() { List curvesToDisplay = new List(); for (int i = 0; i < selectedFields.Count; i++) { EdAnimationCurve curve; if(TryGetCurve(selectedFields[i], out curve)) curvesToDisplay.Add(curve); } guiCurveEditor.SetCurves(curvesToDisplay.ToArray()); float xRange; float yRange; CalculateRange(curvesToDisplay, out xRange, out yRange); // Don't allow zero range if (xRange == 0.0f) xRange = 60.0f; if (yRange == 0.0f) yRange = 10.0f; // Add padding to y range yRange *= 1.05f; // Don't reduce visible range xRange = Math.Max(xRange, guiCurveEditor.XRange); yRange = Math.Max(yRange, guiCurveEditor.YRange); guiCurveEditor.SetRange(xRange, yRange); guiCurveEditor.Redraw(); } private static void CalculateRange(List curves, out float xRange, out float yRange) { xRange = 0.0f; yRange = 0.0f; foreach (var curve in curves) { KeyFrame[] keyframes = curve.KeyFrames; foreach (var key in keyframes) { xRange = Math.Max(xRange, key.time); yRange = Math.Max(yRange, Math.Abs(key.value)); } } } private bool TryGetCurve(string path, out EdAnimationCurve curve) { int index = path.LastIndexOf("."); string parentPath; string subPathSuffix = null; if (index == -1) { parentPath = path; } else { parentPath = path.Substring(0, index); subPathSuffix = path.Substring(index, path.Length - index); } FieldCurves fieldCurves; if (curves.TryGetValue(parentPath, out fieldCurves)) { if (!string.IsNullOrEmpty(subPathSuffix)) { if (subPathSuffix == ".x" || subPathSuffix == ".r") { curve = fieldCurves.curves[0]; return true; } else if (subPathSuffix == ".y" || subPathSuffix == ".g") { curve = fieldCurves.curves[1]; return true; } else if (subPathSuffix == ".z" || subPathSuffix == ".b") { curve = fieldCurves.curves[2]; return true; } else if (subPathSuffix == ".w" || subPathSuffix == ".a") { curve = fieldCurves.curves[3]; return true; } } else { curve = fieldCurves.curves[0]; return true; } } curve = null; return false; } private void OnFieldAdded(string path, SerializableProperty.FieldType type) { guiFieldDisplay.AddField(path); switch (type) { case SerializableProperty.FieldType.Vector4: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[4]; string[] subPaths = { ".x", ".y", ".z", ".w" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Vector3: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[3]; string[] subPaths = { ".x", ".y", ".z" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Vector2: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[2]; string[] subPaths = { ".x", ".y" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; case SerializableProperty.FieldType.Color: { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[4]; string[] subPaths = { ".r", ".g", ".b", ".a" }; for (int i = 0; i < subPaths.Length; i++) { string subFieldPath = path + subPaths[i]; fieldCurves.curves[i] = new EdAnimationCurve(); selectedFields.Add(subFieldPath); } curves[path] = fieldCurves; } break; default: // Primitive type { FieldCurves fieldCurves = new FieldCurves(); fieldCurves.type = type; fieldCurves.curves = new EdAnimationCurve[1]; fieldCurves.curves[0] = new EdAnimationCurve(); selectedFields.Add(path); curves[path] = fieldCurves; } break; } UpdateDisplayedCurves(); } private bool IsPathParent(string child, string parent) { string[] childEntries = child.Split('/', '.'); string[] parentEntries = parent.Split('/', '.'); if (parentEntries.Length >= child.Length) return false; int compareLength = Math.Min(childEntries.Length, parentEntries.Length); for (int i = 0; i < compareLength; i++) { if (childEntries[i] != parentEntries[i]) return false; } return true; } private string GetSubPathParent(string path) { int index = path.LastIndexOf("."); if (index == -1) return path; return path.Substring(0, index); } private void OnFieldSelected(string path) { if (!Input.IsButtonHeld(ButtonCode.LeftShift) && !Input.IsButtonHeld(ButtonCode.RightShift)) selectedFields.Clear(); if (!string.IsNullOrEmpty(path)) { selectedFields.RemoveAll(x => { return x == path || IsPathParent(x, path); }); selectedFields.Add(path); } guiFieldDisplay.SetSelection(selectedFields.ToArray()); UpdateDisplayedCurves(); } private void RemoveSelectedFields() { for (int i = 0; i < selectedFields.Count; i++) { selectedFields.Remove(selectedFields[i]); curves.Remove(GetSubPathParent(selectedFields[i])); } List existingFields = new List(); foreach(var KVP in curves) existingFields.Add(KVP.Key); guiFieldDisplay.SetFields(existingFields.ToArray()); selectedFields.Clear(); UpdateDisplayedCurves(); } private void OnSelectionChanged(SceneObject[] sceneObjects, string[] resourcePaths) { Rebuild(); } private void OnFrameSelected(int frameIdx) { SetCurrentFrame(frameIdx); } } /// /// Drop down window that displays options used by the animation window. /// [DefaultSize(100, 50)] internal class AnimationOptions : DropDownWindow { private AnimationWindow parent; /// /// Initializes the drop down window by creating the necessary GUI. Must be called after construction and before /// use. /// /// Animation window that this drop down window is a part of. internal void Initialize(AnimationWindow parent) { this.parent = parent; GUIIntField fpsField = new GUIIntField(new LocEdString("FPS"), 40); fpsField.Value = parent.FPS; fpsField.OnChanged += x => { parent.FPS = x; }; GUILayoutY vertLayout = GUI.AddLayoutY(); vertLayout.AddFlexibleSpace(); GUILayoutX contentLayout = vertLayout.AddLayoutX(); contentLayout.AddFlexibleSpace(); contentLayout.AddElement(fpsField); contentLayout.AddFlexibleSpace(); vertLayout.AddFlexibleSpace(); } } /** @} */ }