[目录]
1. 脚本模板
(1)本地模板
使用"C# script"创建后,会发现,不是一个空的 C#文件,其实会是一个脚本模板,这些模板都保存在ScriptTemplates 目录下,比如“D:\ProgramFiles\Unity\Hub\Editor\2021.1.25f1c1\Editor\Data\Resources\ScriptTemplates”,“81-C# Script-NewBehaviourScript.cs.txt”。文件前的数字代表展示优先级,“_“以及”-” 会决定模板目录结构。更改模板内的内容,或者按照规则拓展一个新的模板。
(2)模板拓展
ScriptTemplates 目录的更改是存在于本地的,不方便进行版本管理,可以通过写编辑器脚本来添加模板如下
// 《unity3d游戏开发》,Script_04_01
using UnityEngine;
using UnityEditor;
using System;
using System.IO;
using System.Text;
using UnityEditor.ProjectWindowCallback;
using System.Text.RegularExpressions;
public class Script_04_01
{
//脚本模板所目录
private const string MY_SCRIPT_DEFAULT = "Assets/Editor/ScriptTemplates/C# Script-MyNewBehaviourScript.cs.txt";
[MenuItem("Assets/Create/C# MyScript", false, 80)]
public static void CreatMyScript()
{
string locationPath = GetSelectedPathOrFallback();
ProjectWindowUtil.StartNameEditingIfProjectWindowExists(0,
ScriptableObject.CreateInstance<MyDoCreateScriptAsset>(),
locationPath + "/MyNewBehaviourScript.cs",
null, MY_SCRIPT_DEFAULT);
}
public static string GetSelectedPathOrFallback()
{
string path = "Assets";
foreach (UnityEngine.Object obj in Selection.GetFiltered(typeof(UnityEngine.Object), SelectionMode.Assets))
{
path = AssetDatabase.GetAssetPath(obj);
if (!string.IsNullOrEmpty(path) && File.Exists(path))
{
path = Path.GetDirectoryName(path);
break;
}
}
return path;
}
}
class MyDoCreateScriptAsset : EndNameEditAction
{
public override void Action(int instanceId, string pathName, string resourceFile)
{
UnityEngine.Object o = CreateScriptAssetFromTemplate(pathName, resourceFile);
ProjectWindowUtil.ShowCreatedAsset(o);
}
internal static UnityEngine.Object CreateScriptAssetFromTemplate(string pathName, string resourceFile)
{
string fullPath = Path.GetFullPath(pathName);
StreamReader streamReader = new StreamReader(resourceFile);
string text = streamReader.ReadToEnd();
streamReader.Close();
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(pathName);
//替换文件名
text = Regex.Replace(text, "#NAME#", fileNameWithoutExtension);
bool encoderShouldEmitUTF8Identifier = true;
bool throwOnInvalidBytes = false;
UTF8Encoding encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier, throwOnInvalidBytes);
bool append = false;
StreamWriter streamWriter = new StreamWriter(fullPath, append, encoding);
streamWriter.Write(text);
streamWriter.Close();
AssetDatabase.ImportAsset(pathName);
return AssetDatabase.LoadAssetAtPath(pathName, typeof(UnityEngine.Object));
}
}
后续我们需要将模板放在Assets\Editor\ScriptTemplates中即可, [MenuItem(“Assets/Create/C# MyScript”, false, 80)] 决定了调用他的 菜单位置 。MyDoCreateScriptAsset使得脚本可以像其他脚本一样做 命名 的操作处理
2. 脚本序列化
(1)序列化
- 什么是序列化?
序列化可以理解为,把对象转化为字节序列,那对应的反序列化就是,把字节序列还原为对象。
注意的是,序列化从来都是针对对象的,脚本严格来说是不能进行序列化的(因为它是个一串代码),而是脚本对象才是可以序列化的。只不过我们通常也会用脚本序列化指代,脚本对象的序列化。 - 为什么要序列化?
很多情况下,我们需要将某个对象的数据保存下来下次使用,或者重复使用,或者发送到网络,那这个时候,只有字符序列(二进制序列之类)是可以做到存储以及网络传输的功能的。另外因为序列化,对象属性可以被解释成为字符串,改字符串再简单不过了,所以序列化也可以对属性修改起到便捷作用。
比如这个脚本是地图生成的脚本,包含地图大小、树生成位置等很多属性。那这个时候我们做了很多调整地图大小200*300,在(12,13)位置生成一棵树,希望将这个数据,下次生成地图的时候按照这个属性来加载。比起自己来逐个,按规则“200*300,(12,13)…”等方式自己去创建文件并记录内容,这个过程就很繁琐,而且容易出错,每次更改属性都要重新写,数据可读性也不佳。
Unity其实以及提供了很多基础属性的序列化处理,我们只需要调用相关的序列化处理即可做到将其转为字符串,并在需要的时候反序列化来得到我们想要的数据。方便又准确。另外,脚本属性是可序列化的,把文件中的(12,30)改成(30,30)就可以改变树的位置这个属性,这样我们又可以很方便的在Unity中去修改这些数据。 - 其他序列化
这个序列化的规则其实就有很多种,比如出名的JSON、XML。
(2)序列化格式&数据查看
ProjectSetting-Editor中设置对象被序列化成的内容,建议是 Force Text,这样可读性会高很多,方便查看,而且不会影响发布后的内容
这样之后我们就可以很方便的用文本编辑器,打开序列化的数据进行查看了
(3)序列化规则以及属性面板拓展
public字段默认支持序列化,private默认不序列化。
[HideInInspector] 和 [SerializeField] 可以标记属性是否可被序列化,以及是否被外部查看修改。这俩个装饰并不冲突。
对于自定义的数据类,我们也可以用 [System.Serialzable] 标记这个类是可序列化的,而 [System.NonSerialzied] 可以标记这个类是不参与序列化的。
下面的脚本可以方便我们了解序列化的规则,以及属性面板的拓展。
// 改自《unity3d游戏开发》,Script_04_08
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Script_04_08 : MonoBehaviour
{
public int Id_public;
private int Id_private;
[HideInInspector]
public int Id_public_Hide;
[HideInInspector]
[SerializeField]
public int Id_private_Serialize_Hide;
[SerializeField]
private int id;
[SerializeField]
private string m_name;
[SerializeField]
private GameObject prefab;
}
#if UNITY_EDITOR
[CustomEditor(typeof(Script_04_08))]
public class ScriptInsector : Editor
{
public override void OnInspectorGUI()
{
EditorGUILayout.LabelField("[常规显示的面板]");
// 渲染常规内容
base.OnInspectorGUI();
EditorGUILayout.LabelField("[自行处理的面板]");
//更新最新数据
serializedObject.Update();
//获取数据信息
SerializedProperty property = serializedObject.FindProperty("id");
//赋值数据
property.intValue = EditorGUILayout.IntField("主键", property.intValue);
property = serializedObject.FindProperty("m_name");
property.stringValue = EditorGUILayout.TextField("姓名", property.stringValue);
property = serializedObject.FindProperty("prefab");
property.objectReferenceValue = EditorGUILayout.ObjectField("游戏对象", property.objectReferenceValue, typeof(GameObject), true);
//全部保存数据
serializedObject.ApplyModifiedProperties();
}
}
#endif
serializedObject 只在编辑器中使用的,专门用来做序列化属性修改等处理,直接使用EditorGUILayout.PropertyField,让序列化属性按照Unity原生的方式进行渲染。
//以默认样式绘制数组数据
EditorGUILayout.PropertyField(serializedObject.FindProperty ("prefab"), true);
(4)面板属性修改事件
简单修改检查可以用来监听
void OnValidate(){
Debug.Log("change")
}
更多修改监听处理,可以参照如下
//《unity3d游戏开发》,Script_04_09
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Script_04_10 : MonoBehaviour
{
[SerializeField]
private GameObject[] targets;
}
#if UNITY_EDITOR
[CustomEditor(typeof(Script_04_10))]
public class ScriptInsector:Editor
{
public override void OnInspectorGUI ()
{
//更新最新数据
serializedObject.Update ();
//标记检查
EditorGUI.BeginChangeCheck ();
EditorGUILayout.PropertyField(serializedObject.FindProperty ("targets"), true);
//标记检查发生变化
if (EditorGUI.EndChangeCheck ()) {
Debug.Log ("元素发生变化");
}
//判断面板元素是否任意发生改变
if (GUI.changed) {
}
//全部保存数据
serializedObject.ApplyModifiedProperties ();
}
}
#endif
(5)序列化与分序列化监听
序列化与反序列化都是Unity自动处理,如果需要对数据提前做处理,则需要做监听处理。
//《unity3d游戏开发》,Script_04_11
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public class Script_04_11 : MonoBehaviour ,ISerializationCallbackReceiver
{
[SerializeField]
private List<Sprite> m_Values = new List<Sprite>();
[SerializeField]
private List<string> m_Keys = new List<string>();
public Dictionary<string,Sprite>spriteDic = new Dictionary<string, Sprite>();
#region ISerializationCallbackReceiver implementation
void ISerializationCallbackReceiver.OnBeforeSerialize ()
{
//序列化
m_Keys.Clear();
m_Values.Clear();
foreach(KeyValuePair<string, Sprite> pair in spriteDic)
{
m_Keys.Add(pair.Key);
m_Values.Add(pair.Value);
}
}
void ISerializationCallbackReceiver.OnAfterDeserialize ()
{
//反序列化
spriteDic.Clear();
if (m_Keys.Count != m_Values.Count) {
Debug.LogError ("m_Keys and m_Values 长度不匹配!!!");
} else {
for (int i = 0; i < m_Keys.Count; i++) {
spriteDic [m_Keys [i]] = m_Values [i];
}
}
}
#endregion
}
#if UNITY_EDITOR
[CustomEditor(typeof(Script_04_11))]
public class ScriptInsector:Editor
{
public override void OnInspectorGUI ()
{
//更新最新数据
serializedObject.Update ();
SerializedProperty propertyKey = serializedObject.FindProperty ("m_Keys");
SerializedProperty propertyValue = serializedObject.FindProperty ("m_Values");
int size = propertyKey.arraySize;
GUILayout.BeginVertical ();
for (int i = 0; i < size; i++) {
GUILayout.BeginHorizontal ();
SerializedProperty key = propertyKey.GetArrayElementAtIndex (i);
SerializedProperty value = propertyValue.GetArrayElementAtIndex (i);
key.stringValue = EditorGUILayout.TextField ("key", key.stringValue);
value.objectReferenceValue = EditorGUILayout.ObjectField ("value", value.objectReferenceValue, typeof(Sprite), false);
GUILayout.EndHorizontal ();
}
GUILayout.EndVertical ();
GUILayout.BeginHorizontal ();
if (GUILayout.Button ("+"))
{
(target as Script_04_11).spriteDic [size.ToString ()] = null;
}
GUILayout.EndHorizontal ();
//全部保存数据
serializedObject.ApplyModifiedProperties ();
}
}
#endif
(6)Attributes
刚刚像我们用的“[SerializeField]”,其实是C#的Attribute,也叫特性。特性(Attribute)是用于在运行时传递程序中各种元素(比如类、方法、结构、枚举、组件等)的行为信息的声明性标签。简单可以理解为是个标记,方便对应的东西做特殊处理。Unity已经预制了许多标签并且可以应用到编辑器中。比如
// 在 Inspector 中将此浮点数显示为 0 到 10 之间的滑动条
[Range(0f, 10f)]
float myFloat = 0f;
另外这些便签我们也是可以自行定义的,比如定义一个int限定特性,RangeInt。
//《unity3d游戏开发》,Script_04_13
using UnityEngine;
public class Script_04_13 : MonoBehaviour
{
[RangeInt(1,100)]
public int rangeInt;
}
//《unity3d游戏开发》,RangeIntAttribute
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
public sealed class RangeIntAttribute: PropertyAttribute
{
public readonly int min;
public readonly int max;
public RangeIntAttribute (int min, int max)
{
this.min = min;
this.max = max;
}
}
#if UNITY_EDITOR
[CustomPropertyDrawer( typeof( RangeIntAttribute ) )]
public sealed class RangeIntDrawer : PropertyDrawer
{
public override float GetPropertyHeight (SerializedProperty property, GUIContent label)
{
return 100; //设置面板的高度
}
public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
{
RangeIntAttribute attribute = this.attribute as RangeIntAttribute;
property.intValue = Mathf.Clamp (property.intValue, attribute.min, attribute.max);
EditorGUI.HelpBox(new Rect (position.x, position.y, position.width, 30),
string.Format("范围{0}~{1}",attribute.min, attribute.max),MessageType.Info);
EditorGUI.PropertyField( new Rect (position.x, position.y +35, position.width, 20), property, label );
}
}
#endif
更多的Unity预制的标签和使用可以参考官网链接:
https://docs.unity.cn/cn/current/Manual/editor-PropertyDrawers.html
C#的Attributes是复杂且强大的,如果需要了解更多可以参考如下链接:
https://learn.microsoft.com/zh-cn/dotnet/csharp/advanced-topics/reflection-and-attributes/
http://doc.yaojieyun.com/www.runoob.com/csharp/csharp-attribute.html
https://www.cnblogs.com/SFAN/articles/2004133.html
(7) ScriptableObject
对于只有数据的配置类等,不需要Awake等调用,那么只要继承ScriptableObject即可,即可将数据序列化并保存为文件,等待需要时调用。一个简单的调用样例如下,(当然这样的静态实例是并不符合常规的,毕竟所有对象都可通过Config .Inst 来调用他,所以仅供了解ScriptableObject作为参考)
namespace Exportor
{
internal class Config : ScriptableObject
{
/// <summary>
/// 表格文件加载路径
/// </summary>
public string LoadPath;
/// <summary>
/// 导出数据保存路径
/// </summary>
public string SaveDataPath;
private static Config inst;
public static Config Inst => inst;
// 加载数据
public static void LoadValue(string path)
{
inst = (Config)AssetDatabase.LoadAssetAtPath(path, typeof(Config));
}
// 创建数据
public static void CreateValue(string path)
{
Config config = ScriptableObject.CreateInstance<Config>();
config.LoadPath = @"Assets\TableExporter\Demo\Excel";
config.SaveDataPath = @"Assets\TableExporter\Demo\Data";
AssetDatabase.CreateAsset(config, path);
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
}
}
}
另外 AssetDatabase ,只用于Editor中,对于发布后还需要使用的数据,我们也可以放在Resources中,用 Rescources.Load<type>(path); 来做加载处理。如果有Ab包再另外做考量。
3. 脚本编译
脚本更改后会编译为4个文件,在Project/Library/ScriptAssemblies目录下
(1)编译规则
脚本目录决定脚本类型,也就决定了脚本编译在哪个文件中。假如文件结构如下
Plugins
----A.cs
----Editor
--------B.cs
C.cs
Editor
----D.cs
那么编译顺序以及Dll位置如下
A.cs-->Assembely-CSharp-firstpass.Dll
B.cs-->Assembely-CSharp-Editor-firstpass.Dll
C.cs-->Assembely-CSharp.Dll
D.cs-->Assembely-CSharp-Editor.Dll
其中晚编译的dll可以访问先编译好的dll,比如D.cs可以访问C.cs,但不能反过来。另外,D.cs以及B.cs属于编辑器内容,其对应编译的Dll在仅在编辑器内使用,不会发布。
(2)编译优化
我们可以将框架代码等很少更改的Plugins中,与经常更改的逻辑代码分离开,这样就不会每次只改一点都需要重复编译很多内容。另外也可以预先编译一些内容为Dll,以处理编译慢的问题。
4. 结束咯
所有内容就结束咯,主要参考为雨松大大的《unity3d游戏开发》第四章。