ScriptableObject 是Unity3D整個引擎的設計中,最為出彩的地方。通過他我們將數據保存,數據和編輯器的交互以及數據在runtime的使用三部分很方便的聯系在一起。這是一個容易被Unity3D的初學者們容易忽略的領域。
簡單的說,你可以把ScriptableObject當作Unity3D下的xml。但是其存儲格式為二進制。讓我們從一個實際例子出發,來理解ScriptableObject在實際項目中的整個運用。
假設我們有AbilityInfo需要設置和保存,我們可以將AbilityInfo制作成ScriptableObject,如下:
public class AbilityInfo : ScriptableObject {
public enum Type {
Unknown,
FireBall,
FireWall,
IceBall,
DarkForce,
Heal,
Poison,
}
public Type type = Type.Unknown;
public float point = 10.0f;
public float affectRange = 100.0f;
}
創建ScriptableObject
ScriptableObject 可以通過 ScriptableObject.CreateInstance<T>() 動態創建。然而將它保存為Unity3D Asset是ScriptableObject的設計本意。一般的,我們通過這個函數來完成整個創建過程:
public static class AbilityInfoUtility {
public static AbilityInfo Create ( string _path, string _name ) {
//
if ( new DirectoryInfo(_path).Exists == false ) {
Debug.LogError ( "can't create asset, path not found" );
return null;
}
if ( string.IsNullOrEmpty(_name) ) {
Debug.LogError ( "can't create asset, the name is empty" );
return null;
}
string assetPath = Path.Combine( _path, _name + ".asset" );
//
AbilityInfo newAbilityInfo = ScriptableObject.CreateInstance<AbilityInfo>();
AssetDatabase.CreateAsset(newAbilityInfo, assetPath);
Selection.activeObject = newAbilityInfo;
return newAbilityInfo;
}
}
-
注意1: 這是一個在Editor下使用的函數,所以這份代碼需要放在項目中名為 “Editor” 的文件夾中 (目錄層次無所謂),並且需要在代碼開頭處引入: using UnityEditor;
-
注意2: ScriptableObject的腳本文件名必須和他的類名一致,並且一個腳本文件只能對應一個ScriptableObject定義。
-
注意3: ScriptableObject只能保存為后綴名為”.asset”的文件,其他格式Unity3D將不能正確讀取。
將Asset的創建方式提供給用戶
對於編輯器的使用者,自然需要有UI或者方便的執行腳本方法來幫助創建數據。我們使用[MenuItem]來完成操作。以下代碼會將Asset的創建加入到Project Window的右鍵菜單中:
class AbilityInfoUtility {
...
[MenuItem ("Assets/Create/Ability Info")]
static void Create () {
// get current selected directory
string assetName = "New AbilityInfo";
string assetPath = "Assets";
if ( Selection.activeObject ) {
assetPath = AssetDatabase.GetAssetPath(Selection.activeObject);
if ( Path.GetExtension(assetPath) != "" ) {
assetPath = Path.GetDirectoryName(assetPath);
}
}
//
bool doCreate = true;
string path = Path.Combine( assetPath, assetName + ".asset" );
FileInfo fileInfo = new FileInfo(path);
if ( fileInfo.Exists ) {
doCreate = EditorUtility.DisplayDialog( assetName + " already exists.",
"Do you want to overwrite the old one?",
"Yes", "No" );
}
if ( doCreate ) {
AbilityInfo abilityInfo = AbilityInfoUtility.Create ( assetPath, assetName );
Selection.activeObject = abilityInfo;
// EditorGUIUtility.PingObject(border);
}
}
}

自定義編輯顯示
ScriptableObject 可以通過自定義編輯器的方式進行特殊編輯。編輯器方面有EditorWindow和Editor兩種方式。EditorWindow更多地用在一些復雜數 據的可視化編輯。對於一些小的行為改變,我們可以通過Editor來實現。本節我們也已介紹Editor的編輯為主。設我們需要為AbilityInfo 在Inspector中加入預覽圖標。我們先於提供預覽圖標的美術工作人員約定好圖標的存放位置和名稱,如Editor/Icons/Ability 目錄內。這里我們通過簡單的硬編碼來存取這些圖標 (更靈活的做法是提供一些輔助的設置gui等)。我們在AbilityInfoUtility中添加如下函數:
public static class AbilityInfoUtility {
...
public static Texture2D GetTextureByType ( AbilityInfo.Type _type ) {
switch ( _type ) {
case AbilityInfo.Type.Unknown:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/Unknown.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.FireBall:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/FireBall.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.FireWall:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/FireWall.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.IceBall:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/IceBall.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.DarkForce:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/DarkForce.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.Heal:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/Heal.png", typeof(Texture2D) ) as Texture2D;
case AbilityInfo.Type.Poison:
return AssetDatabase.LoadAssetAtPath ( "Assets/Editor/Icons/Ability/Poison.png", typeof(Texture2D) ) as Texture2D;
}
return null;
}
}
要自定義Inspector顯示,只需要通過在派生的Editor中標記上[CustomEditor(Type)]這個Attribute即可,這里給出一個簡單的示例:
[CustomEditor(typeof(AbilityInfo))]
public class AbilityInfoEditor : Editor {
public override void OnInspectorGUI () {
AbilityInfo editInfo = target as AbilityInfo;
GUI.DrawTexture ( new Rect( 20, 20, 40, 40 ), AbilityInfoUtility.GetTextureByType(editInfo.type) );
GUILayoutUtility.GetRect ( 40, 40 );
GUILayout.Space (5);
base.OnInspectorGUI();
}
}
注意: 這里的base.OnInspectorGUI()實際上是比較偷懶的做法。對於需要仔細定義每個屬性用途和顯示的數據,請參考Unity3D的EditorGUI, EditorGUILayout文檔認真編寫。

在MonoBehavior中引用數據
現在我們只需要在MonoBehavior中引用這份數據,在讀取場景的時候,就會自動幫助我們完成數據的序列化操作。
public class MyBehavior : MonoBehavior {
public AbilityInfo normalSkill1;
public AbilityInfo normalSkill2;
public AbilityInfo specialSkill;
}

