前言
我正在尝试连接一种方法来同步动画和 sfx(以及可能后来的粒子 fx、相机移动等)调用,以便我们的艺术家在将艺术放置在我们的关卡中时使用。我想出的方法是有一个触发器,它有一个基类类型数组FXEvent
,子类类型为AnimEvent
和SFXEvent
。C# 通常可以很好地处理多态性,但是 Unity 的 Inspector 却不能。我担心这是我问题的根源。
我4年前解决了这个问题,所以我知道可以做到;但是,唉,我再也无法访问该代码了。这次我试图用ScriptableObject
and构建一个解决方案SerilizableObject
,而且我有一些几乎可以工作的东西。
我的问题如下:
我将FXTriggerTimer
MonoBehavior(继承自FXTriggerBase
)应用于测试对象,然后将 anAnimEvent
和 a添加SFXEvent
到触发器。然后我保存场景,并重新加载场景,我的测试对象有一个包含两个空元素的数组(显然)不会在检查器中绘制。但是,如果我编辑一个脚本(例如点击空格并保存),脚本刷新将神奇地重新填充正确的两个项目。所以我知道所有的部分都在引擎盖下,它们只需要在场景加载时恢复。
注意:我不喜欢使用 ScriptableObject 和 SerializableObject 的当前解决方案。这对我来说感觉有点过于复杂,但它让我最接近真正的解决方案。
代码如下。
此外,如果有人想以这种方式查看问题,这里还有一个示例场景可下载。
我试图开始工作的基本多态的东西:
//Scripts/FXTriggerBase.cs
using UnityEngine;
using System.Collections;
public enum FXEventType {kNone, kAnim, kSFX,}
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
[System.Serializable]
public class FXEvent: UnityEngine.ScriptableObject
{
[SerializeField]
public FXEventType type;
[SerializeField]
public GameObject go;
virtual public void Fire() { }
};
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
[System.Serializable]
public class AnimEvent : FXEvent
{
[SerializeField]
public string animName = "";
public void Fire() {go.animation.Play(animName); }
}
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
[System.Serializable]
public class SFXEvent: FXEvent
{
[SerializeField]
public AudioClip clip;
public void Fire() {go.audio.PlayOneShot(clip); }
}
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
[AddComponentMenu("")] // prevents users from using this directly
public class FXTriggerBase : MonoBehaviour
{
[SerializeField]
public FXEvent[] events = new FXEvent[]{};
public void Fire()
{
for( int e = 0; e < events.Length; e++)
{
events[e].go = gameObject;
events[e].Fire();
}
}
}
使用它的示例 MonoBehavior:
//Scripts/FXTriggerTimer.cs
using UnityEngine;
using System.Collections;
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
public class FXTriggerTimer: FXTriggerBase
{
public float timeTrigger;
float timeCurrent;
public void Start()
{
timeCurrent = 0;
}
public void Update()
{
float frame = 1.0f/60.0f;
if(timeCurrent >= 0)
timeCurrent += frame;
if(timeCurrent > timeTrigger)
{
Fire();
timeCurrent = -1.0f;
}
}
}
一个自定义检查器实用程序类来编辑它:
//Editor/FXTriggerBaseUtil.cs
using UnityEngine;
using System.Collections;
using UnityEditor;
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
public class FXEditorUtil
{
SerializedObject[] serial;
public bool dirtied = false;
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
public void OnSubGUI(this FXEvent fx)
{
//having trouble getting polymorphism to work right in the inspector,
// so this is what works.
if(fx.type == FXEventType.kAnim)
OnSubGUI((AnimEvent)fx);
else if(fx.type == FXEventType.kSFX)
OnSubGUI((SFXEvent)fx);
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
public void OnSubGUI(this AnimEvent anim)
{
string newAnimName = GUILayout.TextField(anim.animName);
if(newAnimName != anim.animName)
{
anim.animName = newAnimName;
dirtied = true;
}
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
public void OnSubGUI(this SFXEvent sfx)
{
AudioClip newSFX = (AudioClip)EditorGUILayout.ObjectField(sfx.clip, typeof(AudioClip));
if(newSFX != sfx.clip)
{
sfx.clip =newSFX;
dirtied = true;
}
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
public FXEvent OnGUI(FXEvent fx)
{
if(fx == null)
return fx;
GUILayout.BeginHorizontal();
FXEvent ret = null;
//if the enum changes, create new FXEvent sub type corresponding to the
// new enum, and return it
FXEventType newType = (FXEventType) EditorGUILayout.EnumPopup(fx.type, GUILayout.Width(60));
if(newType != fx.type)
{
switch(newType)
{
case FXEventType.kNone:
ret = ScriptableObject.CreateInstance<FXEvent>();
break;
case FXEventType.kSFX:
ret = ScriptableObject.CreateInstance<SFXEvent>();
break;
case FXEventType.kAnim:
ret = ScriptableObject.CreateInstance<AnimEvent>();
break;
default:
Debug.LogError("FXEvent.OnGUI(): Magic Error");
break;
}
ret.type = newType;
dirtied = true;
}
//handle element GUI, if that applies
if(fx.type != FXEventType.kNone)
{
OnSubGUI(fx);
}
else
GUILayout.Label("");
GUILayout.EndHorizontal();
return ret;
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
public void OnGUI(FXTriggerBase trigger)
{
FXTriggerBase triggerBase = trigger as FXTriggerBase;
if(triggerBase == null)
{
Debug.LogError("FXTriggerBaseInspector.OnGUI() : null item to inspect?");
return;
}
//build an array of serialized versions of our array elements, so that they
// save out properly
int numEvents = triggerBase.events.Length;
serial = new SerializedObject[numEvents];
for(int e = 0; e < numEvents; e++)
{
serial[e] = new SerializedObject(triggerBase.events[e]);
}
dirtied = false;
//Do inspector GUI of our array elements
int ret = ArrayGUI("events", trigger.events, trigger.gameObject);
//handle deletion or addition of elements
if(ret >= 0 && ret < trigger.events.Length)
ArrayHandleDelete(ref trigger.events, ret);
else if (ret == trigger.events.Length)
ArrayHandleAdd(ref trigger.events);
//if any changes have been registered, save our serialized versions out
if(dirtied)
{
numEvents = serial.Length;
for(int e = 0; e < numEvents; e++)
{
serial[e].ApplyModifiedProperties();
}
EditorUtility.SetDirty(trigger);
}
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
int ArrayGUI(string label, FXEvent[] arr, GameObject go)
{
int ret = -1;
Color defColor = GUI.color;
GUILayout.BeginVertical(GUILayout.Width(250));
{
GUILayout.Label(label);
int numObjects = arr.Length;
for (int o = 0; o < numObjects; o++)
{
GUILayout.BeginHorizontal();
{
//Draw a red '-' button that will remove this element
GUI.color = Color.red;
bool hit = GUILayout.Button("-", GUILayout.Width(20));
if (hit)
ret = o;
GUI.color = defColor;
//spacer
GUILayout.Label("", GUILayout.Width(10));
//draw custom GUI for each of the polymorphic fx event types
FXEvent fx = arr[o];
FXEvent newFX = OnGUI(fx);
if(newFX != null)
{
arr[o] = newFX;
dirtied = true;
serial[o] = new SerializedObject(arr[o]);
ScriptableObject.DestroyImmediate(fx);
}
}
GUILayout.EndHorizontal();
}
//finally, at the bottom draw a blue plus button that will
GUI.color = Color.cyan;
bool add = GUILayout.Button("+", GUILayout.Width(20));
if (add)
ret = arr.Length;
GUI.color = defColor;
}
GUILayout.EndVertical();
return ret;
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
void ArrayHandleDelete(ref FXEvent[] arr, int index)
{
FXEvent fx = arr[index];
ScriptableObject.DestroyImmediate(fx);
//copy over the element we are deleting, and continue compacting the array
for (int i = index; i < arr.Length - 1; i++)
{
arr[i] = arr[i + 1];
serial[i] = serial[i+1];
}
//remove last element to finalize deletion
System.Array.Resize(ref arr, arr.Length - 1);
System.Array.Resize(ref serial, serial.Length - 1);
dirtied = true;
}
/*-------------------------------------------------------------------------
*///-----------------------------------------------------------------------
void ArrayHandleAdd(ref FXEvent[] arr)
{
int size = arr.Length;
//add new element to the end
System.Array.Resize(ref arr, size + 1);
System.Array.Resize(ref serial, size + 1);
arr[size] = ScriptableObject.CreateInstance<FXEvent>();
serial[size] = new SerializedObject(arr[size]);
dirtied = true;
}
}
并使用以下示例自定义检查器:
//Editor/FXTriggerTimerInspector.cs
using UnityEngine;
using System.Collections;
using UnityEditor;
/*-----------------------------------------------------------------------------
*///---------------------------------------------------------------------------
[CustomEditor(typeof(FXTriggerTimer))]
public class FXTriggerTimerInspector : Editor
{
FXEditorUtil util;
public override void OnInspectorGUI()
{
FXTriggerTimer trigger = (FXTriggerTimer)target;
if(util == null)
{
util = new FXEditorUtil();
}
util.OnGUI(trigger);
GUILayout.BeginHorizontal();
GUILayout.Label("Timer");
trigger.timeTrigger = EditorGUILayout.FloatField(trigger.timeTrigger);
GUILayout.EndHorizontal();
if(util.dirtied)
{
EditorUtility.SetDirty(target);
//serializedObject.ApplyModifiedProperties();
}
}
}