读书小结(《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<MyBehaviour>();
    }
    
    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<T> where T:ScriptableObject
    {
        // 生成ScriptableObject
        var obj = ScriptableObject.CreateInstance<T>();
        // 获取当前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<MyConfig>();
    }
}


编译

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。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,287评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,346评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,277评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,132评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,147评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,106评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,019评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,862评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,301评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,521评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,682评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,405评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,996评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,651评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,803评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,674评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,563评论 2 352

推荐阅读更多精彩内容