[游戏开发]Unity游戏脚本_模板、序列化与编译

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,这样可读性会高很多,方便查看,而且不会影响发布后的内容
Editor
这样之后我们就可以很方便的用文本编辑器,打开序列化的数据进行查看了

(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游戏开发》第四章。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值