游戏脚本
[TOC]
注意,在使用UnityEditor命名空间下的类,或者写UnityEditor相关代码时,代码块应该使用宏
#ifdef UNITY_EDITOR
和#endif
包裹起来。避免非Editor环境下导致脚本编译失败。
本章主要讲述C#脚本的创建、生命周期、执行顺序、序列化、编译、调试相关内容。
(本文忽略了自定义类的Inspector扩展、工作线程以及一些细节内容。)
创建脚本
创建脚本主要通过Asset/Create/C# Script菜单项来进行。
模板
通常创建脚本使用的是UnityEditor默认的模板,默认模板在目录/Applications/Unity/Unity.app/Contents/Resources/ScriptTemplates
下。如有需要,可以在该目录下添加自己常用的C#模板。
通过以上方式添加的模板,因为不在工程目录下,往往无法使用Git进行统一管理。所以,更常用的方式应该是在工程目录下新建一个Templates
目录,然后为Asset/Create
菜单添加自己的脚本创建按钮。
创建自定义脚本的工具类:
- AssetDatabase、Selection、Path。用于获取当前选中的路径、添加生成的新文件。
- StreamReader、StreamWriter。用于读取脚本模板的内容,生成新的脚本文件。
- Regex。用于替换新生成的脚本文件中的自定义内容。
创建自定义脚本功能的基本步骤(伪代码):
// 获取选中的路径
var path = AssetDatabase.GetAssetPath(Selection.activeObject);
path = Path.GetDirectoryName(path);
// 读取模板内容
var reader = new StreamReader(_templatePath_);
var text = reader.ReadToEnd();
reader.Close();
// 替换模板内容
text = Regex.Replace(text, "#SCRIPTNAME#", "NewScript");
// 生成新脚本
var encoding = new UTF8Encoding(true, false);
var writer = new StreamWriter(path, false, encoding);
writer.Write(text);
writer.Close();
// 导入新脚本
AssetDatabase.ImportAsset(path);
脚本的生命周期
生命周期事件
脚本的生命周期可以按功能划分为多块:Editor、Physics、SceneRender、Gizmos、GUI。
常用的生命周期函数:
// Editor
Reset // 编辑器模式下,当脚本被添加或重置时被调用
// Initialization
Awake // 脚本自己的初始化,只会被调用一次
OnEnable // 脚本每次进入Enable状态都会被调用
Start // 脚本被启动时调用,在此初始化与其他脚本相关内容,只会被调用一次
// Physics
FixedUpdate // (相对)固定间隔的Update
yield WaitForFixedUpdate
//[Internal physics update]
OnTriggerXXX // 物理系统处理Trigger相关事件
OnCollisionXXX // 屋里系统处理Collision相关事件
// Input
OnMouseXXX // 输入事件
// Game Logic
Update // 游戏逻辑刷新事件
yield WaitForSeconds
yield WWW
yield StartCoroutine // 协程的启动时机
//[Internal animation update]
LateUpdate // 延迟的Update,适用于处理摄像机和UI的刷新
// Scene
OnWillRenderObject
OnPreCull
OnBecameVisible
OnBecameInvisible
OnPreRender
OnRenderObject
OnPostRender
OnRenderImage
// Gizmo
OnDrawGizmos // 绘制Scene窗口可视化辅助内容
// GUI
OnGUI // 绘制GUI,一帧之内会被调用多次
// End of frame
yield WaitForEndOfFrame
// Pause
OnApplicationPause // 程序暂停,切到后台、收到电话都会被调用
// Enable/Disable
OnDisable // 脚本每次进入Disable的状态都会被调用
// Decommissioning
OnDestroy // 脚本被销毁时被调用
OnApplicationQuit // 程序直接退出时被调用
协程
Unity只支持单线程,但是可以使用C#的协程来完成需要延时进行的操作。
示例:
public class TestCoroutine : MonoBehaviour
{
Coroutine myCoroutine;
Start()
{
// 启动协程
myCoroutine = StartCouroutine(MyCoroutine());
// 停止协程
StopCoroutine(myCoroutine);
}
// 协程函数返回值必须是IEnumerator
private IEnumerator MyCoroutine()
{
for(int i = 0; i < 1000; i++)
{
// do something
}
yield return new WaitForSeconds(1f);
}
}
使用协程和单例实现一个简单的计时器:
public class MyTimer
{
class MyBehaviour:MonoBehaviour
{}
private static MyBehaviour mBehaviour;
// 在静态构造函数中创建不会被销毁的对象,并初始化一个继承MonoBehaviour的脚本用于开启/关闭协程
static MyTimer()
{
var obj = new GameObject("MyTimer");
GameObject.DontDestroyOnLoad(obj);
mBehaviour = obj.AddComponent();
}
public static Coroutine Wait(float time, Action action)
{
return mBehaviour.StartCoroutine(WaitCallback(time, action));
}
public static void CancelWait(ref Coroutine coroutine)
{
if(coroutine != null)
{
mBehaviour.StopCoroutine(coroutine);
coroutine = null;
}
}
private static IEnumerator WaitCallback(float time, Action action)
{
yield return new WaitForSeconds(time);
action?.Invoke();
}
}
如果需要更复杂的定时器:每隔一定时间回调一次。可以通过继承CustomYieldInstruction来实现。
脚本序列化
序列化标签
脚本的public
参数默认会被序列化,并显示在Inspector面板上。Unity还提供了标签用于灵活控制属性的序列化。
public class TestSerialize : MonoBehaviour
{
// public参数默认初始化
public string name;
// 标记参数不序列化
[NonSerialized]
public int age;
// 标记参数要序列化
[SerializeField]
private float weight;
}
序列化的配置文件
通常我们的脚本会挂在GameObject上,脚本会被序列化并显示在Inspector面板上。往往我们有一些用于游戏配置的脚本,它们不需要依赖GameObject,但是又要能被序列化并显示在Inspector面板上便于修改。这里我们可以使用ScriptableObject
对象来保存这些数据。
创建ScriptableObject
对象作为配置文件的过程:
- 通过
Scriptable.CreateInstance
来创建对象。 - 使用
AssetDatabase
将对象保存为.asset
文件。
Wiki上有一个用于创建ScriptableObject
的工具类:
// 用于创建ScriptableObject的工具类
public class ScriptableUtilty
{
public static void CreateAsset where T:ScriptableObject
{
// 生成ScriptableObject
var obj = ScriptableObject.CreateInstance();
// 获取当前path
var path = AssetDatabase.GetAssetPath( Selection.activeObject );
if( path == "" )
{
path = "Assets";
}
else if(Path.GetExtension( path ) != "")
{
path = path.Replace( Path.GetFileName( AssetDatabase.GetAssetPath( Selection.activeObject ) ), "" );
}
// 生成asset文件路径
path = AssetDatabase.GenerateUniqueAssetPath( $"{path}/New{typeof( T ).ToString()}.asset" );
// 保存为asset文件
AssetDatabase.CreateAsset( obj, path );
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 窗口聚焦到创建的asset文件
EditorUtility.FocusProjectWindow();
Selection.activeObject = obj;
}
}
// 测试类
public class MyConfig:ScriptableObject
{
// 配置参数
public int id;
public string name;
public float speed;
// 创建菜单
[MenuItem("Assets/Scriptable/MyConfig")]
public static void Create()
{
ScriptableUtilty.CreateAsset();
}
}
编译
Unity的跨平台是通过Mono实现的。Unity提供的接口都封装到了UnityEditor.dll和UnityEngine.dll中,更底层由DLL调用C++接口实现。开发过程中编写的C#文件也会被编译成多个dll文件。
程序集定义
在默认情况下,Unity游戏工程中的代码会被编译成4个dll文件,这4个dll文件有不同的编译顺序,后编译的代码能访问到先编译的代码。
编译顺序:
- Assembly-CSharp-firstpass.dll(Assets/Plugins下的C#文件)
- Assembly-CSharp-Editor-firstpass.dll(Assets/Plugins/Editor下的C#文件)
- Assembly-CSharp.dll(Assets下的C#文件)
- Assembly-CSharp-Editor.dll(Assets/Editor下的C#文件)
除了默认的程序集,我们还可以通过创建AssemblyDefinition来配置我们自己的程序集定义,程序集定义之间可以有依赖关系。
通常,项目中的底层框架代码是不需要经常修改的,此时可以将底层代码配置在一个Assembly中。如果其中代码不被修改,这个程序集是不需要被重新编译成dll的。通过将不常修改的代码分离出去避免重复编译,能加快项目的编译速度。
调试
在开发阶段,有如下常用的显示调试数据的方式:
-
Debug.Log();
打印自定义数据。 -
Debug.DrawLine();
在Scene窗口绘制线段。 -
Gizmos.Draw();
在Scene窗口中绘制各种内容。
但是有些问题只会出现在真机上,此时我们可以通过监听事件Application.logMessageReseived
来获取程序打印出的数据,并将这些数据记录在本地,以供调试bug。