脚本编译规则

  • 最终所有代码都会生成 dll,放在 Project/Library/ScriptAssembiles 下面。
  • 脚本分为运行时和编辑时两类,运行时脚本最终会编译进游戏包中,而编辑时脚本仅用于编辑器模式下,不会被打包进游戏包。
  • 脚本编译顺序:最先编译 Plugins 目录下的,然后是 Plugins 下的所有 Editor 子目录,然后编译其他目录,最后编译其他 Editor 目录。
  • 先编译的不可以访问后面的数据,所以 Plugins 下的代码不能访问其他代码,后编译的可以访问先编译的脚本。
  • 各目录脚本最终所在的 dll:
    • Plugins 下非 Editor 目录脚本编译进 Assembly-CSharp-firstpass.dll
    • Plugins 下的 Editor 目录脚本编译进 Assembly-CSharp-Editor-firstpass.dll
    • 其他非 Editor 目录脚本编译进 Assembly-CSharp.dll
    • 其他 Editor 目录脚本编译进 Assembly-CSharp-Editor.dll

优化编译

Tips:

  • 当游戏内有很多 C# 代码时,改动一点点代码就需要等很久,因为 Unity 在重新编译 dll,可以将代码分为框架类代码和逻辑性代码,框架性代码一般写好之后不会经常变动,常常需要更改的往往是逻辑性代码,将框架性代码放到 Plugins 目录下,那么在修改逻辑性代码时,就不会额外的编译 Plugins 目录下的代码了。
  • 如果代码量非常巨大,那么编译还是会很慢,可以将部分 CS 代码预先变异成 dll,这样编译速度就更快了。(暂时还不知道怎么做,先记录一下)

编译DLL (没懂)

.NET 可以把 C/C++ 语言编译进 DLL,但是游戏发布后,有的平台是识别不了的,例如移动平台,此时如果编译 DLL 时只能编译 C# 代码,则需要在 macOS 系统中打开终端,输入编译指令,DLL 编译完后直接拖到项目中即可。

1
2
mcs -r:/Applications/Unity/Unity.app/Contents/Managed/UnityEngine.dll
-target:library -out:test.dll *.cs
  • UnityEngine.dll:编译所依赖的 DLL 文件。
  • -target:library:生成 Library1 类型。
  • -out:test.dll:最终生成 DLL 的保存目录。
  • *.cs:表示当前目录下的所有 C# 代码。如果有多个目录多个文件,可以用空格分隔。

脚本跨平台

雨松大大夸了一波 Unity 的跨平台,没懂,应该是指 Unity 针对每个平台有一套底层的核心库,然后通过 C# 去调用,所以对于用户来说只需要使用 C# 就好了。

Unity 自己提供了两个核心的 DLL 库,Unity 编辑器只支持 Windows、macOS 和 Linux 这 3 个平台,所以代码大部分是由 C# 编写的并且编译在 UnityEditor.dll 中,然后通过 Mono 实现了跨平台。运行时由于它兼容的平台非常多(目前已经有 20 多个平台了),并不是所有平台都能运行 DLL 的。再说,底层渲染方法也不能使用 C# 来调用,所以 Unity 只把 C# 接口封装到了 UnityEngine.dll 中。至于更底层的内部实现,则是由这个 DLL 再去调用 C++ 来完成的。拿移动平台来说,编译 C++ 的方式是不同的,Android 需要编译成 .so,ios 则需要编译在 .a 中。所以,Unity 会针对每个平台编译出这份核心库,从而实现了跨平台。现在 Unity 还支持 IL2CPP,它可以把 DLL 代码转化成 C++ 来执行,这从效率上又能提高一个台阶,并且开发者依然使用 C#,只有在打包的时候才会转化成 IL2CPP,整个过程使开发者无感,超棒der。虽然拿不到 Unity 的源码,但是 UnityEngine.dll 和 UnityEditor.dll 这两个 DLL 文件是可以反编译的。

程序集定义

上面提过如果程序过大的话,那么每次改动一点都会等待很久重新编译,除了上面两种方法之外还可以使用程序集。
用户在一个文件夹下新建一个程序集定义,那么就意味着这个文件夹及其子文件夹都变成了一个程序集,单独编译成一个 DLL,如果需要依赖其他的程序集的话,在程序集定义的 Inspector 面板上设置依赖。如图是在 A 程序集中设置了对 B 程序集的依赖。

日志

在游戏发布时一定要将 Debug 给关掉,因为他会有额外的消耗,或者在编写需要输出日志的时候就加上,如果没有定义 UNITY_EDITOR 那么就关闭日志。

1
2
3
#if !UNITY_EDITOR
Debug.unityLogger.logEnabled = false;
#endif

除此之外,错误日志并不是主动打的,下面实现将错误信息打在屏幕上的功能,以方便 Debug。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// 文件名:Script_04_21.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// 捕获错误,并将其显示在屏幕上
public class Script_04_21 : MonoBehaviour
{
// 是否显示 UI
private bool m_bIsVisible = false;

// 存储所有错误信息和调用栈
private List<string> m_lErrorMsg = new List<string>();

// 显示的 UI 范围
private Rect m_rWindow = new Rect(0, 0, Screen.width, Screen.height);

// 设置 Scroll 的位置
private Vector2 m_v2ScrollPosition = Vector2.zero;

// Start is called before the first frame update
void Start()
{
// 监听 logMessageReceived ,参数:日志字符串(string),调用栈(string),消息类型(LogType)
Application.logMessageReceived += (logMsg, stackTrace, type) =>
{
if(type == LogType.Error || type == LogType.Exception)
{
m_bIsVisible = true;
m_lErrorMsg.Add(string.Format("{0} \n {1}", logMsg, stackTrace));
}
};

// 设置 Error
for(int i = 0; i < 10; i++)
{
Debug.LogError("出错了:" + i);
}

// 设置异常
int[] a = null;
a[1] = 1;
}

private void OnGUI()
{
if (m_bIsVisible)
{
m_rWindow = GUILayout.Window(0, m_rWindow, WindowFun, "console");
}
}

void WindowFun(int id)
{
GUILayout.BeginHorizontal();
if (GUILayout.Button("Clear", GUILayout.MaxWidth(200)))
{
m_lErrorMsg.Clear();
}

if (GUILayout.Button("Close", GUILayout.MaxWidth(200)))
{
m_bIsVisible = false;
}
GUILayout.EndHorizontal();

m_v2ScrollPosition = GUILayout.BeginScrollView(m_v2ScrollPosition);
foreach(var msg in m_lErrorMsg)
{
// 因为不需要再打印其他的字符串,所以这边的颜色要不要换回来都没关系
Color currentColor = GUI.contentColor;
GUI.contentColor = Color.red;
GUILayout.TextField(msg);
GUI.contentColor = currentColor;
}
GUILayout.EndScrollView();
}
}

在代码中我们估计打印了一个错误和故意搞了一个异常,运行效果如图所示,错误信息和调用栈都被打印下来了。

脚本调试

Unity 2018 彻底废除了 MonoDevelop,使用 VS,设置了断点之后,在点击 “附加到 Unity”,就会在断点处停住,但不知道为什么我无法像书上所说的,直接将鼠标放到变量身上就可以看到对应的值。
书中还写了一个小技巧,可以直接在监视中直接输入 gameObject.transfrom.position 或者 gameObject.name 来直接查看。