读书小结(《Unity3D游戏开发(第2版)》第四章 游戏脚本)


游戏脚本

[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菜单添加自己的脚本创建按钮。

创建自定义脚本的工具类:

  1. AssetDatabase、Selection、Path。用于获取当前选中的路径、添加生成的新文件。
  2. StreamReader、StreamWriter。用于读取脚本模板的内容,生成新的脚本文件。
  3. 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对象作为配置文件的过程:

  1. 通过Scriptable.CreateInstance来创建对象。
  2. 使用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文件有不同的编译顺序,后编译的代码能访问到先编译的代码。

编译顺序:

  1. Assembly-CSharp-firstpass.dll(Assets/Plugins下的C#文件)
  2. Assembly-CSharp-Editor-firstpass.dll(Assets/Plugins/Editor下的C#文件)
  3. Assembly-CSharp.dll(Assets下的C#文件)
  4. Assembly-CSharp-Editor.dll(Assets/Editor下的C#文件)

除了默认的程序集,我们还可以通过创建AssemblyDefinition来配置我们自己的程序集定义,程序集定义之间可以有依赖关系。

通常,项目中的底层框架代码是不需要经常修改的,此时可以将底层代码配置在一个Assembly中。如果其中代码不被修改,这个程序集是不需要被重新编译成dll的。通过将不常修改的代码分离出去避免重复编译,能加快项目的编译速度。

调试

在开发阶段,有如下常用的显示调试数据的方式:

  1. Debug.Log(); 打印自定义数据。
  2. Debug.DrawLine(); 在Scene窗口绘制线段。
  3. Gizmos.Draw(); 在Scene窗口中绘制各种内容。

但是有些问题只会出现在真机上,此时我们可以通过监听事件Application.logMessageReseived来获取程序打印出的数据,并将这些数据记录在本地,以供调试bug。

你可能感兴趣的:(读书小结(《Unity3D游戏开发(第2版)》第四章 游戏脚本))