Sorry for dragging my feet here, but I finally got around to working on this.
Here’s where I landed, hopefully it’s useful to you and others. Thanks again for sharing the above, super helpful.
I pulled out all of the initialization from each method and put it in an init(), just to make the methods a bit easier to read/write. I call init() from the InspectorEditor code, which I show after this reflection stuff.
static UnityEngine.Object _window;
static BindingFlags _flags;
static FieldInfo _animEditor;
static Type _animEditorType;
static System.Object _animEditorObject;
static FieldInfo _animWindowState;
static Type _windowStateType;
public static void init ()
{
_window = GetOpenAnimationWindow();
_flags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;
_animEditor = GetAnimationWindowType().GetField("m_AnimEditor", _flags);
_animEditorType = _animEditor.FieldType;
_animEditorObject = _animEditor.GetValue(_window);
_animWindowState = _animEditorType.GetField("m_State", _flags);
_windowStateType = _animWindowState.FieldType;
}
Then I exposed the following methods by using the same reflection approach you suggested…
public static bool GetPlaying()
{
bool ret = false;
if (_window != null)
{
System.Object playing = _windowStateType.InvokeMember ("get_playing", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), null);
ret = (bool)playing;
}
return ret;
}
public static void Repaint()
{
if (_window != null)
{
_windowStateType.InvokeMember ("Repaint", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), null);
}
}
public static void StartPlayback()
{
if (_window != null)
{
_windowStateType.InvokeMember("StartPlayback", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), null);
}
}
public static void StopPlayback()
{
if (_window != null)
{
_windowStateType.InvokeMember("StopPlayback", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), null);
}
}
public static void SetCurrentFrame(int frame)
{
if (_window != null)
{
_windowStateType.InvokeMember ("set_currentFrame", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), new object[1] { frame });
}
}
public static int GetCurrentFrame()
{
int ret = 0;
if (_window != null)
{
System.Object frame = _windowStateType.InvokeMember("get_currentFrame", BindingFlags.InvokeMethod | BindingFlags.Public, null, _animWindowState.GetValue(_animEditorObject), null);
ret = (int)frame;
}
return ret;
}
Lastly, I created a custom Inspector Editor, AnimEditorController, with a simple class AnimController, which allows me to set/get Start / End -frame state from the Inspector.
I attach this to the GameObject I’m animating e.g. the game’s hero – a Shield Maiden perhaps
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AnimController : MonoBehaviour {
public int StartFrame;
public int EndFrame;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
}
}
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using System.Reflection;
[CustomEditor(typeof(AnimController))]
public class AnimEditorController : Editor
{
AnimController _animCtrl;
private bool _play = false;
private bool _looped = true;
private int _lastFrame = 0;
void OnEnable ()
{
EditorApplication.update += Update;
Debug.Log("OnEnable");
wAnimationWindowHelper.init();
}
void Update ()
{
if (_play && wAnimationWindowHelper.GetPlaying() == false)
{
StopPlayback();
}
else if (_play)
{
int frame = wAnimationWindowHelper.GetCurrentFrame();
float time = wAnimationWindowHelper.GetAnimationWindowCurrentTime();
if (_looped)
{
if (frame > _animCtrl.EndFrame - 1)
{
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.StartFrame);
}
}
else
{
if (frame > _animCtrl.EndFrame - 1 || frame < _lastFrame)
{
StopPlayback();
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.EndFrame);
}
_lastFrame = frame;
}
}
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
_animCtrl = target as AnimController;
EditorGUI.BeginDisabledGroup(_play);
_looped = EditorGUILayout.Toggle("Loop Playback", _looped);
EditorGUI.EndDisabledGroup();
if (_play == false)
{
if (GUILayout.Button("Play"))
{
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.StartFrame);
wAnimationWindowHelper.StartPlayback();
_lastFrame = _animCtrl.StartFrame;
_play = true;
}
}
else
{
if (GUILayout.Button("Stop"))
{
StopPlayback();
}
}
EditorGUI.BeginDisabledGroup(_play);
if (GUILayout.Button("Set Start"))
{
_animCtrl.StartFrame = wAnimationWindowHelper.GetCurrentFrame();
}
if (GUILayout.Button("Set End"))
{
_animCtrl.EndFrame = wAnimationWindowHelper.GetCurrentFrame();
}
if (GUILayout.Button("Goto Start"))
{
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.StartFrame);
// keyframe line doesn't seem to auto-repaint, had to force it
wAnimationWindowHelper.Repaint();
}
EditorGUI.EndDisabledGroup();
}
void StopPlayback ()
{
wAnimationWindowHelper.StopPlayback();
_play = false;
}
}
A few notes.
In order to “bracket” the animation looping I needed to check for “CurrentFrame” at regular intervals, more frequent the call rate of OnGUI(). I did this by hooking into the Update() event.
void OnEnable ()
{
EditorApplication.update += Update;
Debug.Log("OnEnable");
}
void Update ()
{
if (_play && wAnimationWindowHelper.GetPlaying() == false)
{
StopPlayback();
}
else if (_play)
{
int frame = wAnimationWindowHelper.GetCurrentFrame();
float time = wAnimationWindowHelper.GetAnimationWindowCurrentTime();
if (_looped)
{
if (frame > _animCtrl.EndFrame - 1)
{
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.StartFrame);
}
}
else
{
if (frame > _animCtrl.EndFrame - 1 || frame < _lastFrame)
{
StopPlayback();
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.EndFrame);
}
_lastFrame = frame;
}
}
}
You’ll notice that I added a ‘Loop Playback’ option. I often find it useful to see an animation once rather than looped, especially when animating attack animations or hit reactions.
It only sort of works :(. Perhaps the Unity devs could add something like this
If you set the Start / End -frames to something “inside” the Min / Max -frames of the animation, then it works fine – playing once and then stopping on the end frame. However, if you set your Start / End -frames to Min / Max then it doesn’t work as well.
I assume that when Start / End are equal to Min / Max we’re fighting with the AnimationEditor’s own “looped play” functionality. Perhaps an editor dev could confirm and or suggest a way around this?
The best I could do is a hack. Stopping playback and snapping to the end frame if we’ve gone beyond the end frame or started a new loop.
if (_looped)
{
... snipped ...
}
else
{
if (frame > _animCtrl.EndFrame - 1 || frame < _lastFrame)
{
StopPlayback();
wAnimationWindowHelper.SetCurrentFrame(_animCtrl.EndFrame);
}
_lastFrame = frame;
}
I also tried do this by tracking elapsed time, but found it was too unpredictable. If someone has a better suggestion let me know.
Anyways, that’s it. If I add more I’ll share it here. Cheers!