Unity教程之- Unity3d开发中的零碎小技巧

 

使用 Unity 这么久了,收集了许多零碎的小经验或小技巧,在这里分享出来。

初始化与执行顺序

脚本的 Awake 方法是在物体第一次激活时执行的,也就是说初始状态下没有激活的物体,它们的脚本上的  Awake 方法不会执行。同一个组件上的 OnEnable 在 Awake 执行后立即执行,所以其他组件的 Awake 在前一个组件的 OnEnable 之后才执行。如果你想初始化没有激活的物体上的脚本,你可以默认激活物体,在 Awake 的最后关闭物体。但是这可能导致子物体上的脚本无法初始化。

Unity 的物体层级关系与脚本执行顺序没有什么关系,同一个物体上的多个脚本执行顺序也没有什么规则。脚本执行顺序可以在 Unity中设置,也可以在同一个脚本里按顺序去调用,但是这样就不容易灵活的划分独立模块了。灵活快速是 Unity 的优点,但是处理不当就会变成缺点。有的人则选择完全抛弃 Unity 的这些优点,以避免可能带来的任何麻烦。我不知道怎样才是最好的做法,总之根据情况选择最合适的方法来组织代码吧,没有最好的,只有最合适的。

 

构造方法与序列化

Unity 通过序列化来保存你在 Inspector 上设置的数据。MonoBehaviour 和 ScriptableObject 对象只能由 Unity 构造或通过反序列化来初始化已有对象。Unity 仅调用无参构造方法来产生这些类的对象。一般情况下,为了保证 Unity 可以正常工作,我们不在脚本里写构造方法。但是理论上是可以写的,只要你提供一个公开的、无参构造就可以了。但是构造方法执行的时机、执行时的状态是很混乱的,当构造方法与反序列化在一起工作时我们几乎无法写好代码。所以,除非你很清楚会发生什么否则不要写构造方法。

关于【序列化】和【可序列化】还有必要说明一下。使用 C# 的 Serializable 可以标记一个自定义的类可序列化,但是 Unity 并不会对所有的可序列化字段都执行序列化,Unity 仅对 public 的、非 public 的且标记了 SerializeField 的可序列化字段执行序列化。默认 Inspector 仅显示出序列化的且可识别的字段,HideInInspector 仅隐藏而不会取消序列化字段,NonSerialized 可以让字段不可序列化。MonoBehaviour 和 ScriptableObject 无需标记 Serializable,它们默认都是可序列化的。

 

使用编辑器资源

编辑器中使用图标等资源时,最好使用静态引用保存加载的资源,避免每次显示 Inspector 时重新加载。你可能知道特殊文件夹 Editor Default Resources 用来存放编辑器资源,但是这个文件夹只能存在于根目录,对于开发可重用的插件并不实用。下面的例子展示如何仅加载一次资源、并且将资源存放在特定功能插件的 Editor 文件夹下。


static Texture _removeButton;
static Texture removeButton
{
get
{
return _removeButton != null ? _removeButton :
_removeButton = AssetDatabase.LoadAssetAtPath("Assets/MyPlugin/Editor/Images/remove.png", typeof(Texture)) as Texture;
}
}

脚本组件单例

就是单例嘛。


public class MonoBehaviourSingleton<T> : MonoBehaviourX where T : MonoBehaviourSingleton<T>
{
static T _instance;
public static T instance
{
get { return _instance ? _instance : _instance = GetInstance(); }
}
static T GetInstance()
{
T[] instances = FindObjectsOfType<T>();

if (instances.Length == 0)
{
Debug.Log("There is no instance of singleton type " + typeof(T) + ", a new instance will be created immediately.");

GameObject go = new GameObject(typeof(T).ToString());
DontDestroyOnLoad(go);
return go.AddComponent<T>();
}

if (instances.Length > 1)
{
Debug.LogError("There are more than one instance of singleton type " + typeof(T) + ", the first found one will be returned.");
}

return instances[0];
}
protected virtual void Awake()
{
if (_instance)  Debug.LogError("Already exist a instance of singleton type " + typeof(T) + ", you should not create it again.");
else  _instance = this as T;
}
}

单例在 Unity 里变的稍微复杂了些。

 

在不自定义 Editor 的情况下快速定制 Inspector

你可能已经知道 AddComponentMenu、MenuItem 这些 Attribute 的用法了——多说一句,MenuItem 可以将静态方法添加到菜单栏或组件下拉菜单中,支持快捷键,详见Unity文档。下面是一些更多的Attribute,让你的组件更易于使用。

更新: Unity 5.1 添加了 CreateAssetMenu 来快捷的创建 ScriptableObject 子类对象资源.

TextArea 和 Multiline:放置在string字段上,Inspector上显示为多行文本框而不是默认的单行。这对编辑多行文本非常实用。TextArea 中文本到达边界自动换行,Multiline 直到换行符处才换行,TextArea可能更实用些。

Tooltip:鼠标停留在字段上时显示一个小提示。如果你写个组件担心没人看懂就用这个吧。

Range:添加到int或float字段上可以限制它们的取值范围,在数值前显示一个滑块。当然这只是限制编辑器对数值的改变,代码中你还是需要通过get、set来保证数值在安全范围。

ContextMenuItem:右键点击字段弹出一个菜单,里面是你添加的方法,你可以通过这种方式方便的执行某些方法。不要与 MenuItem 混淆了。

Space:添加到字段上会使字段上方多出指定高度的空白。如果你的组件内容太多,合理的分开他们让编辑更轻松。

Header:添加到字段上会使字段上方显示一个加粗的标题。如果你的组件内容太多,合理的将他们分组并添加标题让编辑更轻松。

不够过瘾。看来更强大的功能只能通过编程实现了。PropertyAttribute 就是可以添加到字段上的标记,比如上面的 Range、Space 都是的。你需要继承这个类然后添加自定义的参数,但是还不够——需要再再定义一个PropertyDrawer或DecoratorDrawer。PropertyDrawer可以定制字段的显示和编辑,比如Range;DecoratorDrawer 可以定制修饰性的东西,比如Space。下面通过一个 MinAttribute 的例子来说明如何定制PropertyDrawer。


[AttributeUsage(AttributeTargets.Field, Inherited = true, AllowMultiple = false)]
public sealed class MinAttribute : PropertyAttribute
{
public readonly float min;

public MinAttribute(float min)
{
this.min = min;
}
}

使用一个只读的min记录限制的最小值。然后是MinPropertyDrawer:


[CustomPropertyDrawer(typeof(MinAttribute), true)]
public class MinPropertyDrawer : PropertyDrawer
{
public override void OnGUI(Rect rect, SerializedProperty property, GUIContent label)
{
float min = (attribute as MinAttribute).min;

switch(property.propertyType)
{
case SerializedPropertyType.Integer:
property.intValue = Mathf.Max((int)min, EditorGUI.IntField(rect, label, property.intValue));
return;

case SerializedPropertyType.Float:
property.floatValue = Mathf.Max(min, EditorGUI.FloatField(rect, label, property.floatValue));
return;

default:
EditorGUI.LabelField(rect, label.text, "Attribute does not support this type.");
return;
}
}
}

是不是很简单呢?通过实现自己的OnGUI和GetPropertyHeight来定制显示。DecoratorDrawer 的实现更简单些,因为它只需要纯粹的绘制即可。
但是这还不是PropertyDrawer最有价值的地方。PropertyDrawer不仅可以定制Attribute的显示方式,还可以直接定制一种可序列化类型的显示方式。这样你就不必为了显示一种自定义类型的字段时而重写整个编辑器了。这样做的时候,[CustomPropertyDrawer(typeof(XXX), true)]中的XXX是自定义类型而不是某种Attribute。下图是重新定制后的LayerMask,点击不会直接消失了,比默认的弹出菜单是不是更方便了呢?关键看起来是不是更酷了呢?

 

如果您发现我的文章存在错误,或有其他更好的想法,欢迎留下评论!这篇文章将会持续更新。