/// <summary> /// Recreates the entire curve editor GUI depending on the currently selected scene object. /// </summary> private void RebuildGUI() { GUI.Clear(); guiCurveEditor = null; guiFieldDisplay = null; 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; } // 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 GUIToggle(playIcon, EditorStyles.Button); recordButton = new GUIToggle(recordIcon, EditorStyles.Button); prevFrameButton = new GUIButton(prevFrameIcon); frameInputField = new GUIIntField(); nextFrameButton = new GUIButton(nextFrameIcon); addKeyframeButton = new GUIButton(addKeyframeIcon); addEventButton = new GUIButton(addEventIcon); optionsButton = new GUIButton(optionsIcon); playButton.OnToggled += x => { if(x) SwitchState(State.Playback); else SwitchState(State.Normal); }; recordButton.OnToggled += x => { if (x) SwitchState(State.Recording); else SwitchState(State.Normal); }; prevFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx - 1); switch (state) { case State.Recording: case State.Normal: PreviewFrame(currentFrameIdx); break; default: SwitchState(State.Normal); break; } }; frameInputField.OnChanged += x => { SetCurrentFrame(x); switch (state) { case State.Recording: case State.Normal: PreviewFrame(currentFrameIdx); break; default: SwitchState(State.Normal); break; } }; nextFrameButton.OnClick += () => { SetCurrentFrame(currentFrameIdx + 1); switch (state) { case State.Recording: case State.Normal: PreviewFrame(currentFrameIdx); break; default: SwitchState(State.Normal); break; } }; addKeyframeButton.OnClick += () => { SwitchState(State.Normal); guiCurveEditor.AddKeyFrameAtMarker(); }; addEventButton.OnClick += () => { SwitchState(State.Normal); guiCurveEditor.AddEventAtMarker(); }; optionsButton.OnClick += () => { Vector2I openPosition = ScreenToWindowPos(Input.PointerPosition); AnimationOptions dropDown = DropDownWindow.Open<AnimationOptions>(this, openPosition); dropDown.Initialize(this); }; // Property buttons addPropertyBtn = new GUIButton(new LocEdString("Add property")); delPropertyBtn = new GUIButton(new LocEdString("Delete selected")); addPropertyBtn.OnClick += () => { Action openPropertyWindow = () => { Vector2I windowPos = ScreenToWindowPos(Input.PointerPosition); FieldSelectionWindow fieldSelection = DropDownWindow.Open<FieldSelectionWindow>(this, windowPos); fieldSelection.OnFieldSelected += OnFieldAdded; }; if (clipInfo.clip == null) { LocEdString title = new LocEdString("Warning"); LocEdString message = new LocEdString("Selected object doesn't have an animation clip assigned. Would you like to create" + " a new animation clip?"); DialogBox.Open(title, message, DialogBox.Type.YesNoCancel, type => { if (type == DialogBox.ResultType.Yes) { string clipSavePath; if (BrowseDialog.SaveFile(ProjectLibrary.ResourceFolder, "*.asset", out clipSavePath)) { SwitchState(State.Empty); clipSavePath = Path.ChangeExtension(clipSavePath, ".asset"); AnimationClip newClip = new AnimationClip(); ProjectLibrary.Create(newClip, clipSavePath); LoadAnimClip(newClip); Animation animation = selectedSO.GetComponent<Animation>(); if (animation == null) animation = selectedSO.AddComponent<Animation>(); animation.DefaultClip = newClip; EditorApplication.SetSceneDirty(); SwitchState(State.Normal); openPropertyWindow(); } } }); } else { if (clipInfo.isImported) { LocEdString title = new LocEdString("Warning"); LocEdString message = new LocEdString("You cannot add/edit/remove curves from animation clips that" + " are imported from an external file."); DialogBox.Open(title, message, DialogBox.Type.OK); } else { SwitchState(State.Normal); openPropertyWindow(); } } }; delPropertyBtn.OnClick += () => { if (clipInfo.clip == null) return; SwitchState(State.Normal); if (clipInfo.isImported) { LocEdString title = new LocEdString("Warning"); LocEdString message = new LocEdString("You cannot add/edit/remove curves from animation clips that" + " are imported from an external file."); DialogBox.Open(title, message, DialogBox.Type.OK); } else { 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(); ApplyClipChanges(); } }); } }; GUIPanel mainPanel = GUI.AddPanel(); GUIPanel backgroundPanel = GUI.AddPanel(1); GUILayout mainLayout = mainPanel.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; GUITexture buttonBackground = new GUITexture(null, EditorStyles.HeaderBackground); buttonBackground.Bounds = new Rect2I(0, 0, Width, buttonLayoutHeight); backgroundPanel.AddElement(buttonBackground); 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); horzScrollBar = new GUIResizeableScrollBarH(); horzScrollBar.OnScrollOrResize += OnHorzScrollOrResize; vertScrollBar = new GUIResizeableScrollBarV(); vertScrollBar.OnScrollOrResize += OnVertScrollOrResize; GUITexture separator = new GUITexture(null, EditorStyles.Separator, GUIOption.FixedWidth(3)); contentLayout.AddElement(separator); GUILayout curveLayout = contentLayout.AddLayoutY(); GUILayout curveLayoutHorz = curveLayout.AddLayoutX(); GUILayout horzScrollBarLayout = curveLayout.AddLayoutX(); horzScrollBarLayout.AddElement(horzScrollBar); horzScrollBarLayout.AddFlexibleSpace(); editorPanel = curveLayoutHorz.AddPanel(); curveLayoutHorz.AddElement(vertScrollBar); curveLayoutHorz.AddFlexibleSpace(); scrollBarHeight = horzScrollBar.Bounds.height; scrollBarWidth = vertScrollBar.Bounds.width; Vector2I curveEditorSize = GetCurveEditorSize(); guiCurveEditor = new GUICurveEditor(this, editorPanel, curveEditorSize.x, curveEditorSize.y); guiCurveEditor.OnFrameSelected += OnFrameSelected; guiCurveEditor.OnEventAdded += OnEventsChanged; guiCurveEditor.OnEventModified += EditorApplication.SetProjectDirty; guiCurveEditor.OnEventDeleted += OnEventsChanged; guiCurveEditor.OnCurveModified += () => { SwitchState(State.Normal); ApplyClipChanges(); PreviewFrame(currentFrameIdx); EditorApplication.SetProjectDirty(); }; guiCurveEditor.OnClicked += () => { if(state != State.Recording) SwitchState(State.Normal); }; guiCurveEditor.Redraw(); horzScrollBar.SetWidth(curveEditorSize.x); vertScrollBar.SetHeight(curveEditorSize.y); UpdateScrollBarSize(); }
private static extern void Internal_CreateInstance(AnimationClip instance);
/// <summary> /// Refreshes the contents of the curve and property display by loading animation curves from the provided /// animation clip. /// </summary> /// <param name="clip">Clip containing the animation to load.</param> private void LoadAnimClip(AnimationClip clip) { EditorPersistentData persistentData = EditorApplication.PersistentData; if (persistentData.dirtyAnimClips.TryGetValue(clip.UUID, out clipInfo)) { // If an animation clip is imported, we don't care about it's cached curve values as they could have changed // since last modification, so we re-load the clip. But we persist the events as those can only be set // within the editor. if (clipInfo.isImported) { EditorAnimClipInfo newClipInfo = EditorAnimClipInfo.Create(clip); newClipInfo.events = clipInfo.events; } } else clipInfo = EditorAnimClipInfo.Create(clip); persistentData.dirtyAnimClips[clip.UUID] = clipInfo; AnimFieldInfo[] fieldInfos = new AnimFieldInfo[clipInfo.curves.Count]; int idx = 0; foreach (var curve in clipInfo.curves) fieldInfos[idx++] = new AnimFieldInfo(curve.Key, curve.Value); guiFieldDisplay.SetFields(fieldInfos); guiCurveEditor.Events = clipInfo.events; guiCurveEditor.DisableCurveEdit = clipInfo.isImported; SetCurrentFrame(0); FPS = clipInfo.sampleRate; }
/// <summary> /// Rebuilds internal curve -> property mapping about the currently playing animation clip. This mapping allows the /// animation component to know which property to assign which values from an animation curve. This Should be called /// whenever playback for a new clip starts, or when clip curves change. /// </summary> internal void RefreshClipMappings() { primaryClip = _native.GetClip(0); RebuildFloatProperties(primaryClip); UpdateSceneObjectMapping(); }
/// <summary> /// Called whenever an animation event triggers. /// </summary> /// <param name="clip">Clip that the event originated from.</param> /// <param name="name">Name of the event.</param> private void EventTriggered(AnimationClip clip, string name) { // Event should be in format "ComponentType/MethodName" if (string.IsNullOrEmpty(name)) return; string[] nameEntries = name.Split('/'); if (nameEntries.Length != 2) return; string typeName = nameEntries[0]; string methodName = nameEntries[1]; Component[] components = SceneObject.GetComponents(); for (int i = 0; i < components.Length; i++) { if (components[i].GetType().Name == typeName) { components[i].Invoke(methodName); break; } } }
/// <summary> /// Changes the state of a playing animation clip. If animation clip is not currently playing the state change is /// ignored. /// </summary> /// <param name="clip">Clip to change the state for.</param> /// <param name="state">New state of the animation (e.g. changing the time for seeking).</param> public void SetState(AnimationClip clip, AnimationClipState state) { switch (this.state) { case State.Active: case State.EditorActive: _native.SetState(clip, state); break; } }
/// <summary> /// Allows the caller to play an animation clip during edit mode. This form of animation playback is limited as /// you have no control over clip properties, and features like blending, cross fade or animation events are not /// supported. /// /// Caller will need to manually call <see cref="UpdateFloatProperties"/> in order to apply evaluated animation data /// to relevant float properties (if required). /// /// Caller will also need to manually call <see cref="RefreshClipMappings"/> whenever the curves internal to the /// animation clip change. This should be called before the call to <see cref="UpdateFloatProperties"/>. /// </summary> /// <param name="clip">Animation clip to play.</param> /// <param name="startTime">Time to start playing at, in seconds.</param> /// <param name="freeze">If true, only the frame at the specified time will be shown, without advancing the /// animation.</param> internal void EditorPlay(AnimationClip clip, float startTime, bool freeze = false) { switch (state) { case State.Inactive: SwitchState(State.EditorActive); break; } switch (state) { case State.EditorActive: if (freeze) Sample(clip, startTime); else { AnimationClipState clipState = AnimationClipState.Create(); clipState.time = startTime; SetState(clip, clipState); } RefreshClipMappings(); break; } }
/// <summary> /// Plays the specified animation clip. /// </summary> /// <param name="clip">Clip to play.</param> public void Play(AnimationClip clip) { switch (state) { case State.Active: _native.Play(clip); break; } }
/// <summary> /// Samples an animation clip at the specified time, displaying only that particular frame without further playback. /// </summary> /// <param name="clip">Animation clip to sample.</param> /// <param name="time">Time to sample the clip at.</param> public void Sample(AnimationClip clip, float time) { switch (state) { case State.Active: case State.EditorActive: _native.Sample(clip, time); break; } }
/// <summary> /// Retrieves detailed information about a currently playing animation clip. /// </summary> /// <param name="clip">Clip to retrieve the information for.</param> /// <param name="state">Animation clip state containing the requested information. Only valid if the method returns /// true.</param> /// <returns>True if the state was found (animation clip is playing), false otherwise.</returns> public bool GetState(AnimationClip clip, out AnimationClipState state) { switch (this.state) { case State.Active: case State.EditorActive: return _native.GetState(clip, out state); default: state = new AnimationClipState(); return false; } }
/// <summary> /// Fades the specified animation clip in, while fading other playing animation out, over the specified time period. /// </summary> /// <param name="clip">Clip to fade in.</param> /// <param name="fadeLength">Determines the time period over which the fade occurs. In seconds.</param> public void CrossFade(AnimationClip clip, float fadeLength) { switch (state) { case State.Active: _native.CrossFade(clip, fadeLength); break; } }
/// <summary> /// Plays the specified animation clip on top of the animation currently playing in the main layer. Multiple such /// clips can be playing at once, as long as you ensure each is given its own layer. Each animation can also have a /// weight that determines how much it influences the main animation. /// </summary> /// <param name="clip">Clip to additively blend. Must contain additive animation curves.</param> /// <param name="weight">Determines how much of an effect will the blended animation have on the final output. /// In range [0, 1].</param> /// <param name="fadeLength">Applies the blend over a specified time period, increasing the weight as the time /// passes. Set to zero to blend immediately. In seconds.</param> /// <param name="layer">Layer to play the clip in. Multiple additive clips can be playing at once in separate layers /// and each layer has its own weight.</param> public void BlendAdditive(AnimationClip clip, float weight, float fadeLength, int layer) { switch (state) { case State.Active: _native.BlendAdditive(clip, weight, fadeLength, layer); break; } }
/// <summary> /// Changes the state of the animation state machine. Doesn't check for valid transitions. /// </summary> /// <param name="state">New state of the animation.</param> private void SwitchState(State state) { this.state = state; switch (state) { case State.Active: if (_native != null) _native.Destroy(); _native = new NativeAnimation(); _native.OnEventTriggered += EventTriggered; animatedRenderable = SceneObject.GetComponent<Renderable>(); // Restore saved values after reset _native.WrapMode = serializableData.wrapMode; _native.Speed = serializableData.speed; _native.Cull = serializableData.cull; UpdateBounds(); if (serializableData.defaultClip != null) _native.Play(serializableData.defaultClip); primaryClip = _native.GetClip(0); if (primaryClip != null) RebuildFloatProperties(primaryClip); SetBoneMappings(); UpdateSceneObjectMapping(); if (animatedRenderable != null) animatedRenderable.RegisterAnimation(this); break; case State.EditorActive: if (_native != null) _native.Destroy(); _native = new NativeAnimation(); animatedRenderable = SceneObject.GetComponent<Renderable>(); UpdateBounds(); SetBoneMappings(); if (animatedRenderable != null) animatedRenderable.RegisterAnimation(this); break; case State.Inactive: if (animatedRenderable != null) animatedRenderable.UnregisterAnimation(); if (_native != null) { _native.Destroy(); _native = null; } primaryClip = null; mappingInfo.Clear(); floatProperties = null; break; } }
/// <summary> /// Builds a list of properties that will be animated using float animation curves. /// </summary> /// <param name="clip">Clip to retrieve the float animation curves from.</param> private void RebuildFloatProperties(AnimationClip clip) { if (clip == null) { floatProperties = null; return; } AnimationCurves curves = clip.Curves; List<FloatCurvePropertyInfo> newFloatProperties = new List<FloatCurvePropertyInfo>(); for (int i = 0; i < curves.FloatCurves.Length; i++) { bool isMorphCurve = curves.FloatCurves[i].Flags.HasFlag(AnimationCurveFlags.MorphWeight) || curves.FloatCurves[i].Flags.HasFlag(AnimationCurveFlags.MorphFrame); if (isMorphCurve) continue; string suffix; SerializableProperty property = FindProperty(SceneObject, curves.FloatCurves[i].Name, out suffix); if (property == null) continue; int elementIdx = 0; if (!string.IsNullOrEmpty(suffix)) { PropertySuffixInfo suffixInfo; if (PropertySuffixInfos.TryGetValue(suffix, out suffixInfo)) elementIdx = suffixInfo.elementIdx; } Action<float> setter = null; Type internalType = property.InternalType; switch (property.Type) { case SerializableProperty.FieldType.Vector2: if (internalType == typeof(Vector2)) { setter = f => { Vector2 value = property.GetValue<Vector2>(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Vector3: if (internalType == typeof(Vector3)) { setter = f => { Vector3 value = property.GetValue<Vector3>(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Vector4: if (internalType == typeof(Vector4)) { setter = f => { Vector4 value = property.GetValue<Vector4>(); value[elementIdx] = f; property.SetValue(value); }; } else if (internalType == typeof(Quaternion)) { setter = f => { Quaternion value = property.GetValue<Quaternion>(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Color: if (internalType == typeof(Color)) { setter = f => { Color value = property.GetValue<Color>(); value[elementIdx] = f; property.SetValue(value); }; } break; case SerializableProperty.FieldType.Bool: setter = f => { bool value = f > 0.0f; property.SetValue(value); }; break; case SerializableProperty.FieldType.Int: setter = f => { int value = (int)f; property.SetValue(value); }; break; case SerializableProperty.FieldType.Float: setter = f => { property.SetValue(f); }; break; } if (setter == null) continue; FloatCurvePropertyInfo propertyInfo = new FloatCurvePropertyInfo(); propertyInfo.curveIdx = i; propertyInfo.setter = setter; newFloatProperties.Add(propertyInfo); } floatProperties = newFloatProperties.ToArray(); }