描述
开 本: 32开纸 张: 胶版纸包 装: 平装-胶订是否套装: 否国际标准书号ISBN: 9787302453987
本书适合作为高等院校计算机及相关专业的教材和教学参考书,也可作为相关开发人员的自学教材和参考手册。
目 录
第1章 Unity中的C#语言…………………………………………………………………………….. 1
1.1 为何选择C#语言………………………………………………………………………… 1
1.2 创建脚本文件……………………………………………………………………………. 2
1.3 脚本的实例化操作……………………………………………………………………… 4
1.4 变量………………………………………………………………………………………… 6
1.5 条件语句………………………………………………………………………………….. 7
1.5.1 if语句……………………………………………………………………………… 8
1.5.2 switch语句……………………………………………………………………… 10
1.6 数组……………………………………………………………………………………….. 13
1.7 循环……………………………………………………………………………………….. 16
1.7.1 foreach循环…………………………………………………………………….. 16
1.7.2 for循环………………………………………………………………………….. 17
1.7.3 while循环………………………………………………………………………. 18
1.7.4 无限循环………………………………………………………………………… 20
1.8 函数……………………………………………………………………………………….. 20
1.9 事件……………………………………………………………………………………….. 23
1.10 类和面向对象程序设计…………………………………………………………….. 24
1.11 类和继承机制…………………………………………………………………………. 26
1.12 类和多态……………………………………………………………………………….. 28
1.13 C#属性………………………………………………………………………………….. 32
1.14 注释……………………………………………………………………………………… 34
1.15 变量的可见性…………………………………………………………………………. 37
1.16 ?操作符…………………………………………………………………………………. 38
1.17 SendMessage和BroadcastMessage………………………………………………… 38
1.18 本章小结……………………………………………………………………………….. 40
第2章 调试机制……………………………………………………………………………………….. 41
2.1 编译错误和控制台…………………………………………………………………….. 41
2.2 利用Debug.Log进行调制——定制消息…………………………………………. 44
2.3 覆写ToString方法…………………………………………………………………….. 46
2.4 可视化调试……………………………………………………………………………… 50
2.5 错误日志…………………………………………………………………………………. 52
2.6 编辑器调试……………………………………………………………………………… 56
2.7 使用分析工具…………………………………………………………………………… 59
2.8 基于MonoDevelop的调试…………………………………………………………… 62
2.9 Watch窗口………………………………………………………………………………. 66
2.10 恢复执行程序和步进操作…………………………………………………………. 70
2.11 调用栈………………………………………………………………………………….. 71
2.12 Immediate窗口……………………………………………………………………….. 73
2.13 设置条件断点…………………………………………………………………………. 74
2.14 跟踪点………………………………………………………………………………….. 76
2.15 本章小结……………………………………………………………………………….. 78
第3章 单例模式、静态模式、GameObject 以及场景世界……………………………….. 79
3.1 GameObject对象………………………………………………………………………. 79
3.2 组件间的交互方式…………………………………………………………………….. 81
3.2.1 GetComponent函数…………………………………………………………… 82
3.2.2 获取多个组件………………………………………………………………….. 83
3.2.3 组件和消息…………………………………………………………………….. 84
3.3 GameObject和场景世界……………………………………………………………… 85
3.3.1 获取GameObject………………………………………………………………. 86
3.3.2 对象比较………………………………………………………………………… 88
3.3.3 获取近对象………………………………………………………………….. 88
3.3.4 获取特定类型的对象………………………………………………………… 89
3.3.5 GameObject之间的路径…………………………………………………….. 90
3.3.6 访问对象的层次结构………………………………………………………… 92
3.4 场景、时间和更新操作………………………………………………………………. 93
3.4.1 规则1——帧的重要性………………………………………………………. 95
3.4.2 规则2——相对于时间的运动…………………………………………….. 95
3.5 永久对象…………………………………………………………………………………. 96
3.6 理解单例模式和静态模式…………………………………………………………… 98
3.7 本章小结……………………………………………………………………………….. 101
第4章 事件驱动程序设计…………………………………………………………………………. 102
4.1 事件……………………………………………………………………………………… 102
4.2 事件管理……………………………………………………………………………….. 106
4.2.1 基于接口的事件管理……………………………………………………….. 107
4.2.2 定义EventManager………………………………………………………….. 109
4.3 MonoDevelop中的代码折叠——#region和#endregion……………………… 114
4.3.1 使用EventManager………………………………………………………….. 115
4.3.2 基于委托机制的替代方案…………………………………………………. 116
4.3.3 MonoBehaviour事件………………………………………………………… 121
4.3.4 鼠标事件………………………………………………………………………. 122
4.3.5 应用程序焦点和暂停……………………………………………………….. 125
4.4 本章小结……………………………………………………………………………….. 127
第5章 相机、渲染和场景…………………………………………………………………………. 128
5.1 相机Gizmo…………………………………………………………………………….. 128
5.2 可见性………………………………………………………………………………….. 131
5.2.1 检测对象的可见性………………………………………………………….. 132
5.2.2 关于对象可见性的其他问题……………………………………………… 133
5.2.3 视锥体测试——渲染器……………………………………………………. 134
5.2.4 视锥体测试——点………………………………………………………….. 135
5.2.5 视锥体测试——遮挡……………………………………………………….. 136
5.2.6 相机前、后视觉……………………………………………………………… 137
5.3 正交相机……………………………………………………………………………….. 138
5.4 相机渲染和后处理…………………………………………………………………… 142
5.5 相机震动……………………………………………………………………………….. 148
5.6 相机和动画……………………………………………………………………………. 150
5.7 相机和曲线……………………………………………………………………………. 152
5.8 本章小结……………………………………………………………………………….. 158
第6章 与Mono协同工作…………………………………………………………………………. 159
6.1 表和集合……………………………………………………………………………….. 160
6.1.1 List类…………………………………………………………………………… 160
6.1.2
Dictionary类………………………………………………………………….. 163
6.1.3 Stack类………………………………………………………………………… 164
6.2 IEnumerable和IEnumerator接口………………………………………………… 166
6.3 字符串和正则表达式……………………………………………………………….. 172
6.3.1 null、空字符串和空格……………………………………………………… 172
6.3.2 字符串比较……………………………………………………………………. 173
6.3.3 字符串的格式化……………………………………………………………… 174
6.3.4 字符串循环……………………………………………………………………. 175
6.3.5 创建字符串……………………………………………………………………. 176
6.3.6 搜索字符串……………………………………………………………………. 176
6.3.7 正则表达式……………………………………………………………………. 176
6.4 无穷参数……………………………………………………………………………….. 178
6.5 语言集成查询…………………………………………………………………………. 178
6.6 Linq和正则表达式…………………………………………………………………… 181
6.7 与文本数据资源协同工作…………………………………………………………. 182
6.8 从本地文件中加载文本数据………………………………………………………. 184
6.8.1 从INI文件中加载文本数据………………………………………………. 185
6.8.2 从CVS文件中加载文本数据…………………………………………….. 187
6.8.3 从Web中加载文本数据…………………………………………………… 187
6.9 本章小结……………………………………………………………………………….. 188
第7章 人工智能……………………………………………………………………………………… 189
7.1 游戏中的人工智能…………………………………………………………………… 189
7.2 开始项目……………………………………………………………………………….. 191
7.3 烘焙导航网格…………………………………………………………………………. 192
7.4 NPC主体对象………………………………………………………………………… 195
7.5 Mecanim中的有限状态机………………………………………………………….. 198
7.6 C#语言中的有限状态机……………………………………………………………. 202
7.7 构建Idle状态…………………………………………………………………………. 204
7.8 构建Patrol状态………………………………………………………………………. 207
7.9 构建Chase状态………………………………………………………………………. 211
7.10 构建Attack状态……………………………………………………………………. 213
7.11 构建Seek-Health(或逃跑)状态………………………………………………. 214
7.12 本章小结……………………………………………………………………………… 217
第8章 定制Unity编辑器………………………………………………………………………….. 219
8.1 批量重命名……………………………………………………………………………. 219
8.2 C#属性和反射………………………………………………………………………… 224
8.3 颜色混合……………………………………………………………………………….. 227
8.4 显示属性……………………………………………………………………………….. 232
8.5 本地化………………………………………………………………………………….. 238
8.6 本章小结……………………………………………………………………………….. 246
第9章 与纹理、模型和2D元素协同工作…………………………………………………….. 247
9.1 天空盒………………………………………………………………………………….. 247
9.2 过程式网格……………………………………………………………………………. 252
9.3 UV动画——纹理滚动……………………………………………………………… 259
9.4 纹理绘制……………………………………………………………………………….. 261
9.4.1 创建纹理混合着色器……………………………………………………….. 262
9.4.2 创建纹理绘制脚本………………………………………………………….. 265
9.4.3 设置纹理绘制………………………………………………………………… 272
9.5 本章小结……………………………………………………………………………….. 275
第10章 资源控制和其他…………………………………………………………………………… 276
10.1 Git——资源控制……………………………………………………………………. 276
10.1.1 下载…………………………………………………………………………… 277
10.1.2 构建Unity项目…………………………………………………………….. 278
10.1.3 基于源控制配置Unity……………………………………………………. 279
10.1.4 构建Git存储库…………………………………………………………….. 280
10.1.5 忽略文件…………………………………………………………………….. 281
10.1.6 创建首次提交………………………………………………………………. 282
10.1.7 修改文件…………………………………………………………………….. 284
10.1.8 从存储库中获取文件……………………………………………………… 285
10.1.9 浏览存储库………………………………………………………………….. 288
10.2 资源文件夹和外部文件…………………………………………………………… 289
10.3 AssetBundles和外部文件…………………………………………………………. 290
10.4 持久数据和游戏保存………………………………………………………………. 294
10.5 本章小结……………………………………………………………………………… 298
第1章 Unity中的C#语言
本书阐述Unity的脚本设计,因而读者需要了解Unity游戏开发环境下的C#语言。在进一步阅读之前,读者有必要明晰相关概念,进而可在理论基础上掌握脚本设计这一高级内容,此类内容多具有衔接性和实践性特征。关于衔接性,任何一种程序设计语言均会强调语法及其编程规则,这也是一种语言的正式内容之一,其中涉及变量、循环以及函数。随着程序员经验的不断增加,其关注点逐渐从语言本身转向对实际问题的处理,即由语言自身内容转向特定环境下的语言应用。因此,本书并非是一本C#语法书籍。
在结束本章的学习后,相信读者已经掌握了C#语言的基本内容,后续章节将运用C#语言处理相关案例以及实际问题,这也是本书的特点之一,并覆盖了C#语言的全部功能项,以使读者更好地理解相关操作结果。无论经验如何,这里建议读者逐章阅读,对于期望解决复杂问题的C#语言新手而言尤其如此。对于经验丰富的开发人员,本书则可强化其现有的知识,并在学习过程中提供新的建议和理念。本章将采用循序渐进的方式,从头开始阐述C#语言的基础内容。另外,如果读者熟悉另一门语言的编程知识,且尚未接触过C#语言,现在则是学习该语言的良好时机。
1.1 为何选择C#语言
当提及Unity脚本设计时,面临的一个问题则是选取哪一种语言,Unity对此提供了解决方案。相应地,官方选取方案则是C#和JavaScript语言。然而,考虑到基于Unity的特定应用,JavaScript应称作JavaScript或是UnityScript尚存争论,但其中原因并非是本书讨论的重点。当前问题是项目所选取的设计语言。作为一种方案,可在项目中选择两种语言,同时在其中分别编写脚本文件,并对这两种语言进行混合。当然,这在技术上是可行的,Unity对此并未加以限制,但这会导致混淆以及编译冲突,就像尝试同时以英里和千米为单位计算距离。
因此,这里建议采用一种语言,并在项目中作为主语言加以使用。本书则选用了C#语言,其原因在于:首先C#语言并非优于其他语言,根据个人观点,此处并不存在意义上的优劣性,每种语言均包含各自的优点和应用场合;同时,所有Unity语言均可用于游戏制作。这里选择C#语言的主要因素在于其应用的广泛性,以及对Unity的支持。针对Unity,C#语言可限度地与开发人员现有的知识体系结构相结合。大多数Unity教程均采用C#语言编写,同时也常见于其他应用开发领域中。C#语言的历史可追溯至.NET框架,后者也可用于Unity中(称作Mono)。另外,C#语言也借鉴了C 语言的内容。在游戏开发中,C 则是一类主要的开发语言。通过学习C#程序设计语言,读者可向当今游戏界的Unity程序开发人员看齐。因此,本书选用了C#语言,进而扩大其应用范围,在现有教程以及资源的基础上,限度地发挥读者的知识水平。
1.2 创建脚本文件
当定义游戏的逻辑或行为时,用户需要编辑相应的脚本文件。Unity中的脚本机制始于新文件的创建,即添加至项目中的标准文本文件。该文件定义了一个程序,并列出了Unity需要理解的全部指令。如前所述,对应指令可通过C#、JavaScript或Boo语言编写,而本书则选用了C#语言。在Unity中,存在多种方式可创建脚本文件。
其中,一种方法是从应用菜单中选择Assets | Create | C# Script命令,如图1-1所示。
图1-1
另一种方法则是右击Project面板中的空白区域,并在快捷菜单中选择Create1的C# Script命令,如图1-2所示。这将在当前开启的文件夹中创建数据资源。
图1-2
当创建完毕后,新的脚本文件将位于Project文件夹内,且包含.cs扩展名(表示C#文件)。该文件名十分重要,并对脚本文件的有效性产生重要影响——Unity使用该文件名确定创建于该文件内的C#类的名称。本章稍后将对类加以深入讨论。简而言之,用户应确保文件包含且具有实际意义的名称。
关于性,其含义是指项目中的其他文件名不应与此相同,无论该文件是否位于不同的文件夹内。也就是说,全部脚本文件在项目中应具有的名称。另外,文件名应具有实际意义,并表达脚本行将执行的任务。进一步讲,C#语言中存在多种有效规则可对文件名和类名予以限定。关于此类规则的正式定义,读者可访问http://msdn.microsoft. com/en-us/library/aa664670%28VS.71%29.aspx。简单地讲,文件名应始于字母或下划线字符(不允许采用数字作为首字符);同时,文件名不应包含空格。对应示例如图1-3所示。
图1-3
Unity脚本文件可在任意文本编辑器或IDE中打开,包括Visual Studio和Notepad ,但Unity提供了免费的开源编辑器MonoDevelop。该软件为主Unity包中的部分内容,并包含于安装过程中,因而无须单独下载。当在Project面板中双击脚本文件时,Unity将在MonoDevelop内自动打开文件。如果在后续操作中决定更改脚本文件名,则还需要在文件内修改C#类的名称,以使其与文件名准确匹配,如图1-4所示。否则,这将生成无效代码以及编译错误,并在脚本文件与对象绑定时出现问题。
图1-4
当在Unity中编译代码时,需要在MonoDevelop中保存脚本文件,即选择应用菜单中File命令中的Save选项(或者按Ctrl S快捷键),并于随后返回至Unity Editor中。当Unity窗口再次处于焦点状态时,Unity可自动检测到文件中的变化,进而对代码进行编译。如果存在错误,则游戏将无法正常运行,对应的错误信息将显示于Console窗口中。若编译成功,单击Editor工具栏上的Paly按钮即可。需要注意的是,如果在修改代码后未保存文件,Unity将使用之前的编译版本运行程序。针对这一原因以及备份要求,建议用户定期保存文件(按Ctrl S快捷键,将结果保存至MonoDevelop中)。
1.3 脚本的实例化操作
Unity中的各个脚本文件定义了一个主类,这类似于设计蓝图,并可对其进行实例化操作。该类可视为相关变量、函数以及事件的集合(稍后将对此进行分析)。默认状态下,脚本文件类似于其他任意一种Unity数据资源,例如网格和音频文件。特别地,脚本文件通常处于静止状态,且不执行任何操作,直至添加至某一特定的场景中(作为组件添加至某一对象中),并在运行期内处于活动状态。当前,作为与网格类似的独立对象,包含逻辑和数学内容的脚本尚未添加至场景中,鉴于此类对象不具备视觉和音频特征,因而用户尚无法对其直接感受。当作为组件加入至现有游戏对象中时,脚本定义了此类对象的相应行为。针对特定对象上的组件,脚本的激活过程称作实例化。当然,独立脚本可在多个对象上进行实例化操作,并针对全部对象复制某一行为,且无须针对各个对象制订多个脚本。例如,多个敌方角色可采用相同的人工智能逻辑。理想状态下,脚本核心内容可视为对象的抽象规则或行为模式,并可在可行方案中的多个相似对象间重复使用。当向某一对象中添加脚本文件时,可从场景目标对象上的Project面板中简单地拖曳脚本。该脚本将作为一个组件进行实例化,当该对象被选取时,其公有变量在Object Inspector中处于可见状态,如图1-5所示。
图1-5
1.4节将对变量进行讨论。
关于Unity中脚本的创建和应用,读者可访问http://docs.unity3d.com/412/ Documentation/Manual/Scripting.html以获取更多信息。
1.4 变 量
变量可视为C#以及其他程序设计语言中的核心概念。变量通常对应于多个字母,并代表了某一数值量,例如X,Y,Z 以及a,b,c。如果用户需要跟踪某种信息,例如玩家的名字、得分、位置、方向、弹药量、健康值,以及其他多种可量化的数据(通过名词表示),则可通过变量体现这一类信息。变量代表单一的信息单位,这也意味着,多个变量需要包含多个单位且一一对应。进一步讲,各个单位表示为某一特定类型。具体而言,玩家的名字表示为字母序列,例如”John”、”Tom”或”David”。相比较而言,玩家的健康值则采用数值数据,例如100%(1)或50%(0.5),这取决于玩家所受到的伤害程度。因此,各个变量均需要包含一种数据类型。在C#语言中,变量通过特定的语法加以定义,如示例代码1-1所示。
示例代码1-1
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyNewScript:MonoBehaviour
05 {
06 public string PlayerName = “”;
07 public int PlayerHealth = 100;
08 public Vector3 Position = Vector3.zero;
09
10 //Use this for initialization
11 void Start () {
12
13 }
14
15 //Update is called once per frame
16 void Update () {
17
18 }
19 }
各个变量包含某一数据类型,较为常见的类型包括int、float、bool、string以及Vector3。对应示例如下所示:
q
int(整数)= –3,–2,–1,0,1,2,3,…。
q
float(浮点数或小数)= –3.0,–2.5,0.0,1.7,3.9,…。
q
bool(布尔值或true/false)= true或false(1或0)。
q
string(字符串)=”hello world”,”a”,”another word…”。
q
Vector3(位置值)=(0, 0, 0),(10, 5, 0)…。
在示例代码1-1的06~08行中,各个变量赋予了一个初始值,其数据类型显式地标记为string、int(整型)和Vector3(表示为3D空间内的一点或者方向)。此处并未列出完整的数据类型列表,其内容一般处于变化中,并取决于具体项目(用户也可定义自己的数据类型)。本书将通过大量的示例展示常见的类型。另外,各个变量声明始于关键字public。通常情况下,变量可声明为public或private(以及protected,示例代码中未予显示)。其中,public变量可在Unity的Object Inspector中进行访问和编辑(稍后将对此加以解释,读者也可参考图1-5),同时还可通过其他类进行访问。
变量值可在一段时间内发生变化,当然,这种变化也应符合相应的规则。一类显式的调整方法是,可在Object Inspector中通过代码对其直接赋值;或者通过方法或函数调用。除此之外,变量还可采用直接或间接方式赋值。变量的直接赋值方式如下所示:
PlayerName=”NewName”;
另外,还可采取表达式实现间接赋值,也就是说,在执行赋值操作前需要对结果值进行计算,如下所示:
//Variable will result
to 50, because: 100 x 0.5=50
PlayerHealth=100 *
0.5;
各个变量的声明均包含了隐式范围,该范围用于确定变量的生命周期——在当前文件中,变量可引用和访问的位置。作用域通过变量的声明位置予以确定。示例代码1-1中声明的变量包含了类作用域,对应变量声明于类上方且位于函数之外。也就是说,变量可在类中任意位置进行访问;同时,作为public变量,还可被其他类访问。除此之外,变量还可声明于特定函数中,即局部变量,其作用域限定于该函数中。相应地,局部变量无法在函数外部进行访问。本章后续内容还将对类和函数进行讨论。
关于C#语言中变量及其应用的更多信息,读者可访问http://msdn.microsoft.com/ en-us/library/ aa691160%28v=vs.71%29.aspx。
1.5 条 件 语 句
变量可在多种不同的环境下进行修改:当玩家改变其位置时、当敌方角色被摧毁时、当关卡被调整后等。因此,用户需要经常检测变量值,并依据该值控制脚本的执行流程,进而实现不同的行为操作集。例如,如果PlayerHealth到达0%,则需要执行死亡操作序列;如果PlayerHealth为20%,则可显示一条警告消息。在该示例中,变量PlayerHealth负责按照某一方向驱动脚本。对此,C#语言提供了两种主要的条件语句,进而实现程序的分支操作,即if语句和switch语句。
1.5.1 if语句
if语句包含了多种形式,其为基本的形式负责检测某一条件,当且仅当该条件为true时,将执行某一代码块,如示例代码1-2所示。
示例代码1-2
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 public string PlayerName=””;
07 public int PlayerHealth=100;
08 public Vector3 Position=Vector3.zero;
09
10 //Use this for initialization
11 void Start () {
12 }
13
14 //Update is called once per frame
15 void
Update ()
16 {
17 //Check player health-the braces symbol {}
are option
for one-line if-statements
18 if(PlayerHealth == 100)
19 {
20 Debug.Log (“Player has full
health”);
21 }
22 }
23 }
上述代码的执行过程与Unity中的其他代码类型并无两样——针对活动场景中的某一对象,当脚本文件实例化后,可单击工具栏中的Play按钮。其中,第18行的if语句针对当前值检测PlayerHealth类变量。如果变量PlayerHealth等于(==)100,则执行{}中的代码(第19~21行)。对应的工作流程可描述为:全部条件结果表示为布尔值true或false,经检测后可查看对应结果是否为true(PlayerHealth == 100)。实际上,花括号中可包含大量的内容,而此处仅涉及一项功能,即Unity中的Debug.Log函数向控制台输出“Player has full health”信息(第20行代码),如图1-6所示。当然,if语句还可包含其他分支,例如,如果PlayerHealth值不等于100(99或101),则代码将不会输出任何信息。对应的执行流程通常取决于计算结果为true的上一条if语句。
图1-6
关于C#语言中if语句、if-else语句应用的更多信息,读者可访问http://msdn.microsoft.
com/en-GB/library/5011f09h.aspx。
在图1-6中,Unity中的调试工具为控制台,并可通过Debug.Log语句(或Print函数)于此处输出消息,以供开发人员进行查看。这对于诊断运行期或编译期内的问题十分有效。针对编译期或运行期错误,相关信息显示于Console选项卡中。默认状态下,Console选项卡于Unity Editor中处于可见状态;另外,也可采用手动方式显示该选项卡,即在Unity应用菜单中,选择Window菜单中的Console选项。关于Debug.Log函数的更多信息,读者可访问http://docs.unity3d.com/ScriptReference/ Debug.Log.html。
除了相等条件(==)之外,用户还可对其他条件进行检测。例如,可使用>或
if(PlayerHealth >=
0 && PlayerHealth <= 100 && PlayerHealth !=50)
{
Debug.Log (“Player has full health”);
}
if-else语句可视为if语句的变化版本。如果对应条件计算为true,则执行if语句,而if-else语句则对此进行了扩展。如果条件为true,则执行X代码块;若条件为false,则会执行Y代码块,如下所示:
if(MyCondition)
{
//X – perform my code
if MyCondition is true
}
else
{
//Y – perform my code
if MyCondition is false
}
1.5.2 switch语句
如前所述,if语句可用于确定某一条件为true或false,并在此基础上执行特定的代码块。相比较之下,switch语句将对多种可能的条件或状态检测变量,并依照某一个方向选取程序分支。例如,如果创建处于某一行为状态下的角色(CHASE、FLEE、FIGHT、HIDE等),则需要根据代码分支处理相应的状态。其中,关键字break用于从某一状态中退出,并返回至switch语句的结束处。示例代码1-3采用枚举值对敌方角色进行处理。
示例代码1-3
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Define possible states for enemy using an
enum
07 public enum EnemyState {CHASE, FLEE, FIGHT,
HIDE};
08
09 //The current state of enemy
10 public
EnemyState ActiveState=EnemyState.CHASE;
11
12 //Use this for initialization
13 void Start () {
14 }
15
16 //Update is called once per frame
17 void Update ()
18 {
19 //Check the ActiveState variable
20 switch(ActiveState)
21 {
22 case EnemyState.FIGHT:
23 {
24 //Perform fight code here
25 Debug.Log (“Entered fight
state”);
26 }
27 break;
28
29
30 case EnemyState.FLEE:
31 case EnemyState.HIDE:
32 {
33 //Flee
and hide performs the same behaviour
34 Debug.Log
(“Entered flee or hide state”);
35 }
36 break;
37
38 default:
39 {
40 //Default
case when all other states fail
41 //This
is used for the chase state
42 Debug.Log
(“Entered chase state”);
43 }
44 break;
45 }
46 }
47 }
示例代码1-3中的第07行声明了一个名为EnemyState的结构。枚举表示为一类特殊结构,用于存储一个或多个变量的多种可能值,其自身并非是变量,但可用于确定变量值的限定范围。在示例代码1-3中,声明于第10行的ActiveState变量使用了EnemyState,其值可以是源自ActiveState枚举的任意值。枚举机制可在特定范围内或一系列选项中对数据值进行限制。
枚举结构的另一个优点是,基于该结构的变量,可令其值显示于Object Inspector内下拉列表的可选项中,如图1-7所示。
图1-7
关于C#语言中的枚举和应用,读者可访问http://msdn.microsoft.com/en-us/library/ sbbt4032.aspx获取更多信息。
下列内容显示了示例代码中的某些解释信息。
q
第20行代码:开始执行switch语句。()中的内容用于选择变量,其值或状态将被检测。在当前示例中,变量ActiveState将被检测。
q
第22行代码:首个case语句位于switch语句内,如果变量ActiveState设置为EnemyState.Fight,则执行后续代码块(第24、25行代码);否则代码将被忽略。
q
第30、31行代码:此处包含了两个case语句。当且仅当ActiveState表示为EnemyState.Flee或EnemyState.Hide时,将执行第33、34行中的代码块。
q
第38行代码:default表示为switch语句的可选项。如果不存在为true的case语句,则执行该部分内容。在当前示例中,如果ActiveState为EnemyState.Chase,则执行default部分中的代码。
q
第27、36和44行代码:break语句应位于case语句的结尾处。当到达break语句时,将退出其所属的switch语句,并继续执行switch语句之后的内容,在当前示例中为第45行代码。
关于C#语言中switch语句及其应用,读者可访问http://msdn.microsoft.com/en-GB/
library/06tc147t.aspx以获取更多信息。
1.6 数 组
表和序列在游戏中随处可见。对此,用户可能需要经常跟踪同一类型的数据表,例如关卡中的全部敌方角色、收集的全部武器装备、采集的全部能量棒以及存储的全部法术和装备等。数组可用于表示同一类型的表,其中的各项内容表示为一个信息单位,并可在游戏体验过程中发生变化。数组可将全部相关变量(所有的敌方角色、全部武器装备等)收集于某一独立、线性、可遍历的表结构中,这也是数组的用武之地。在C#语言中,存在两种数组结构,即静态数组和动态数组。其中,静态数组可在内存中加载固定数量的数据项,对应尺寸需要事先予以确定,即使实际数据项的数量小于数组的尺寸。这也意味着,数组中的某些位置将被浪费。动态数组可根据具体要求增加或减少其大小,并与所需的数据项数量实现准确的匹配。相比较而言,静态数组执行速度较快,而动态数组则可避免内存空间的浪费。本章仅考察静态数组(后续内容将对动态数组予以分析),如示例代码1-4所示。
示例代码1-4
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Array of game objects in the scene
07 public GameObject[] MyObjects;
08
09 //Use this for initialization
10 void Start ()
11 {
12 }
13
14 //Update is called once per frame
15 void Update ()
16 {
17 }
18 }
在示例代码1-4中,第07行代码声明了一个名为MyObjects的空GameObject数组。当对此进行创建时,代码在GameObject数据类型后使用了[]语法,进而指定了一个数组,并表明声明了一个GameObject表,而非单一的GameObject。这里,声明后的数组表示为场景中所有对象的列表。初始状态下,数组为空,但用户可通过Unity Editor中的Object Inspector,并采用手动方式构建数组,即设置尺寸并将所需对象置于其中。对此,可选取场景中脚本所绑定的对象,针对My Objects字段输入Size值,进而确定该数组的尺寸,即需要加载的全部对象数量。随后,可将对象从场景层次结构面板中简单地拖曳至Object Inspector中的数组中,进而通过数据项设置列表,如图1-8所示。
图1-8
除此之外,还可通过Start函数在代码中以手动方式构建数组,而非使用Object Inspector。这可确保数组在关卡启动时构建数组。两种方法均可正常工作,如示例代码1-5所示。
示例代码1-5
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Array of game objects in the scene
07 public GameObject[] MyObjects;
08
09 //Use this for initialization
10 void Start ()
11 {
12 //Build
the array manually in code
13 MyObjects
= new GameObject[3];
14 //Scene
must have a camera tagged as MainCamera
15 MyObjects[0]
= Camera.main.gameObject;
16 //Use
GameObject.Find function to
17 //find objects in scene by name
18 MyObjects[1]
= GameObject.Find(“Cube”);
19 MyObjects[2]
= GameObject.Find(“Cylinder”);
20 }
21
22 //Update is called once per frame
23 void Update ()
24 {
25 }
26 }
示例代码1-5中的部分内容解释如下。
q
第10行代码:Start函数在关卡启动时予以执行。本章后续内容将对函数加以深入讨论。
q
第13行代码:关键字new用于创建新数组,其尺寸定义为3。这也意味着,数组一次多可存储3个数据元素。默认状态下,全部数据元素均被设置为初始值null,并表示为空。
q
第15行代码:数组中的首个元素设置为场景中的主相机对象。此处需要注意两点内容:首先,数组中的元素可通过数组下标[]访问。因此,MyObjects的个元素可采用MyObjects[0]进行访问。其次,C#语言中的数组采用“0索引”机制,也就是说,首个元素位于位置0处,下一个元素位于位置1处,随后的元素位于位置3处等。对于MyObjects3元素数组,每个元素可通过MyObjects[0],MyObjects[1]以及MyObjects[2]进行访问。需要注意的是,后一个元素的位置为2,而非3。
q
第18、19行代码:MyObjects数组的元素1和元素2通过GameObject.Find函数设置对象,这将利用特定的名称(大小写敏感)针对游戏对象搜索活动场景,并在MyObjects数组中的特定位置处插入其引用。如果未发现与当前名称匹配的对象,则插入null。
关于C#语言中的数组及其应用,读者可访问http://msdn.microsoft.com/en-GB/ library/9b9dty7d. aspx以获取更多信息。
1.7 循 环
循环在程序设计中可视为一项重要的工具。假设游戏中的关卡被核武器摧毁,程序需要销毁其中的全部内容。在讨论循环之前,用户只能在代码中逐一删除各个对象。对于包含少量对象的场景,只需几行代码即可解决这类问题;而对于包含数百个对象的场景,则需要编写大量的代码。另外,如果场景内容发生变化,还需对代码进行相应调整。这一过程将十分枯燥。对此,循环可通过少量代码简化这一处理过程,且无须考虑场景的复杂度和对象的数量。C#语言中存在多种循环方式,下面对其进行逐一讲解。
1.7.1 foreach循环
或许,C#语言中简单的类型应属foreach循环。当使用foreach循环时,可遍历数组中的各个元素,并从头至尾根据相关要求处理各个数据项。示例代码1-6将在GameObject数组中删除全部GameObjects。
示例代码1-6
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Array of game objects in the scene
07 public GameObject[] MyObjects;
08
09 //Use this for initialization
10 void Start ()
11 {
12 //Repeat code for all objects in array, one by
one
13 foreach(GameObject Obj in MyObjects)
14 {
15 //Destroy object
16 Destroy (Obj);
17 }
18 }
19
20 //Update is called once per frame
21 void Update ()
22 {
23 }
24 }
foreach循环重复执行第14~17行代码,一次处理MyObjects数组中的一个元素。循环操作与数组的尺寸紧密相关,这也意味着,较大的数组需要执行更多次循环,需要更多处理时间。另外,循环还定义了一个局部变量obj,该变量声明于第13行代码中的foreach语句中。在每次循环过程中,该变量表示为数组中的所选元素,或者活动元素。因此,obj分别表示为首次循环中的个元素,第二次循环中的第二个元素等。
关于C#语言中的foreach循环及其应用,读者可访问http://msdn.microsoft.com/
en-GB/library/ttw7t8t6.aspx以获取更多信息。
1.7.2 for循环
当需要从头至尾按照顺序遍历某一数组时,foreach使用起来十分方便,并逐一处理各个元素。某些时候,用户需要对循环过程加以控制,例如从后向前循环;同时处理两个等尺寸数组;间隔处理数组元素等。下列代码展示了for循环的操作过程:
//Repeat code
backwards for all objects in array, one by one
for(int
i=MyObjects.Length-1; i>= 0; i–)
{
//Destroy object
Destroy (MyObjects[i]);
}
对应的解释内容如下所示。
q
for循环反向遍历MyObjects数组,并删除场景中的GameObject对象。其中使用了局部变量i,该变量用于控制循环的进程,因而也称作Iterator变量。
q
for循环由以下3部分构成,各部分通过分号隔开。
- i:该变量初始化为MyObjects.Length – 1(即数组中的后一个元素)。回忆一下,由于数组采用了0索引机制,因而后一个元素的位置表示为MyObjects.Length – 1。这也使得循环始于数组的结尾处。
- i>=0:该表达式表示为循环结束时的条件。这里,变量i表示为倒计数变量。此时,当i不再大于或等于0时,循环结束,此处0表示为数组的开始位置。
- i–:在每次循环过程中,该表达式控制变量i的变化方式,这里是从数组的开始位置至结束位置。期间,i将以此递减1,也就是说,每次循环时i将减去1。相比较而言, 语句将加1。
q
在循环期间,表达式MyObjects[i]用于访问数组与元素。
关于C#语言中的for循环,读者可访问http://msdn.microsoft.com/en-gb/library/
ch45axte.aspx以获取更多信息。
1.7.3 while循环
当循环处理某一数组时,for和foreach循环均十分有用,并在每次循环过程中执行特定的操作。相比之下,while循环则可持续地重复处理特定的行为,直至相关条件计算为false。例如,如果需要处理遭受岩浆袭击的角色,或者车辆在刹车之前的运动行为,则需要使用到while循环,如示例代码1-7所示。
示例代码1-7
01 using UnityEngine;
02 using System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Use this for initialization
07 void Start ()
08 {
09 //Will count how many messages have been
printed
10 int NumberOfMessages = 0;
11
12 //Loop until 5 messages have been printed to
the console
13 while(NumberOfMessages < 5)
14 {
15 //Print message
16 Debug.Log (“This is
Message:”
NumberOfMessages.ToString());
17
18 //Increment
counter
19 NumberOfMessages;
20 }
21 }
22
23 //Update is called once per frame
24 void Update ()
25 {
26 }
27 }
Unity中的多个类和对象均包含ToString函数(位于示例代码1-7中的第16行)。该函数将对象(例如整数)转换为可读的单词或句子,并输出至Console或Debugging窗口中。当进行调试时,可将对象或数据输出至控制台中。需要注意的是,数值对象与字符串之间的转换需要使用到隐式转换。
示例代码1-7中的相关解释如下所示。
q
第13行代码:通过重复条件启动while循环,直至整型变量NumberOfMessages大于或等于5。
q
第15~19行代码:作为while循环体重复执行。
q
第19行代码:在每次循环过程中递增变量NumberOfMessages。
如在游戏模式下执行示例代码1-7,当关卡启动时,终结果将向Unity
Console输出5条文本消息,如图1-9所示。
图1-9
关于C#语言中while循环及其应用,读者可访问http://msdn.microsoft.com/en-gb/
library/2aeyhxcd.aspx以获取更多信息。
1.7.4 无限循环
循环操作的危险之处在于可出现无限循环这一类状况,特别是while循环。也就是说,循环无法终止。如果游戏进入无限循环状态,可能会永久性地处于“冻结”状态,需要终止程序并强行退出;或者更为糟糕的是,将导致系统的崩溃。通常情况下,Unity并不会采取该方式对问题予以捕捉进而退出程序。例如,如果移除示例代码1-7中的第19行代码,由于NumberOfMessages不会增加,进而满足while循环条件,因而将产生无限循环。因此,在编写并规划循环操作时,应尽量避免无限循环。另一个可导致游戏产生问题的无限循环如下所示:
//Loop forever
while(true)
{
}
然而,在某些场合下,循环也可在正确的条件下满足游戏需求。例如,如果角色在处于运动状态下的平台上反复跳跃前行,光球持续处于旋转状态,或者日夜反复交替,则需要使用到无限循环,且应通过正确方式对其加以实现。在后续章节中,读者将会看到无限循环的正确应用方式。循环可视为一类功能强大且有趣的结构,如果出现编码错误,无论是否为无限循环,这一类结构很可能是诸多问题的根源之一,并会导致性能问题,因而应对其予以谨慎设计。本书将对循环的创建提供良好的实践方案。
1.8 函 数
本章前述内容曾涉及函数问题,例如Start和Update函数,本节将对此予以正式讨论。实际上,函数可视为语句集合,并可作为独立、可识别的代码块。函数具有特定的名称,并可根据相关要求予以执行。其中,函数的每行代码将按顺序依次被执行。当考察游戏逻辑时,玩家需要针对相关对象重复执行某些操作,例如射击行为、跳跃动作、射杀敌方角色、更新积分榜或者播放音频。对此,用户可在源代码中复制和粘贴代码,以实现某种复用操作。当然,这并非是一种良好的设计习惯。相应地,可将重复代码置于某一函数中,并在必要时根据其名称予以执行,如示例代码1-8所示。
示例代码1-8
01
using UnityEngine;
02
using System.Collections;
03
04
public class MyScriptFile:MonoBehaviour
05
{
06 //Private variable for score
07 //Accessible only within this class
08 private int Score = 0;
09
10 //Use this for initialization
11 void Start ()
12 {
13 //Call update score
14 UpdateScore(5, false); //Add five points
15 UpdateScore(10, false); //Add ten points
16 int
CurrentScore = UpdateScore (15, false); //Add fifteen
points and store result
17
18 //Now double score
19 UpdateScore(CurrentScore);
20 }
21
22 //Update is called once per frame
23 void Update ()
24 {
25 }
26
27 //Update game score
28 public int UpdateScore (int AmountToAdd,
bool
PrintToConsole = true)
29 {
30 //Add points to score
31 Score = AmountToAdd;
32
33 //Should we print to console?
34 if(PrintToConsole){Debug.Log (“Score is:
”
Score.ToString());}
35
36 //Output current score and exit
function
37 return Score;
38 }
39 }
示例代码1-8的相关解释如下所示。
q
第08行代码:声明了一个私有整型变量Score,并用于记录分值。该变量稍后将用于UpdateScore函数中。
q
第11、23、28行代码:MyScriptFile类包含3个函数(有时也称作方法或成员函数),即Start、Update以及UpdateScore函数。其中,Start和Update为Unity提供的特殊函数,稍后将对此加以讨论。UpdateScore函数则是针对MyScriptFile类的自定义函数。
q
第28行代码:UpdateScore函数表示一个完整的代码块,位于第29~38行之间。当游戏积分榜每次发生变化时,将会调用该函数。通过这一方式,函数可提供代码的复用性。
q
第14~19行代码:UpdateScore函数在Start函数中被调用多次。在每次调用过程中,Start函数将暂停其执行过程,直至UpdateScore函数执行完毕。此时,执行过程将在下一行代码处恢复进行。
q
第28行代码:UpdateScore接收两个参数,即整型参数AmountToAdd和布尔参数PrintToConsole。参数类似于输入内容,置于函数中并影响其操作方式。变量AmountToAdd表示向当前Score变量所加入的量值。当函数被执行时,PrintToConsole用于确定Score变量是否显示于Console窗口中。从理论上讲,参数的数量并无限制;另外,函数也可不包含任何参数,例如Start和Update函数。
q
第31~34行代码:必要时,可更新分数值并输出至Console中。需要注意的是,PrintToConsole参数包含了默认值true,并在第28行声明中赋值完毕。当函数被调用时,参数将处于可选状态。通过传递false值,第14、15、16行代码显式地覆写了默认值。相比而言,第19行代码则忽略了第二个值,因而只接收默认值true。
q
第28、37行代码:UpdateScore函数包含一个返回值,在第28行中,该返回值表示为特定的数据类型,且位于函数名之前。此处,返回值定义为int。这意味着,当函数退出或执行完毕后,将输出一个整数值。在当前示例中,这一整数值表示为当前的Score。在第37行代码中,这实际上表示为基于return语句的输出值。另外,函数也可不包含任何返回值,对此,返回类型可定义为void,例如Start和Update函数。
关于C#语言中的函数及其应用,读者可访问http://csharp.net-tutorials.com/basics/ functions/以获取更多内容。
1.9 事 件
实际上,事件表示为以一种独特方式使用的函数。在前述内容中,更为准确地讲,Start和Update可描述为Unity事件。相应地,事件可定义为函数,经调用后可向某一对象通知所发生的事件,例如启动关卡、启动了新的一帧、敌方角色死亡、玩家处于跳跃状态等。当在对应时刻进行调用时,必要时可向对象提供响应操作。例如,当对象首次被创建时,Start函数将自动被Unity所调用,通常是在关卡启动时。另外,Update函数也将在各帧中被自动调用一次。因此,当启动关卡时,Start函数可执行特定的操作行为;而Update函数将在每秒内且于各帧中被调用多次。Update函数十分有用,并可在游戏中实现运动和动画行为。示例代码1-9将对某一对象执行旋转操作,如下所示:
示例代码1-9
01 using UnityEngine;
02 using
System.Collections;
03
04 public class
MyScriptFile:MonoBehaviour
05 {
06 //Use this for initialization
07 void Start ()
08 {
09 }
10
11 //Update is called once per frame
12 void Update ()
13 {
14 //Rotate object by 2 degrees per frame
around the Y axis
15 transform.Rotate(new Vector3(0.0f, 2.0f,
0.0f));
16 }
17 }
在示例代码1-9中,第15行代码将在各帧中被调用一次,进而围绕y轴持续旋转对象2°。该代码与帧速率相关,也就是说,在具有较高帧速率的机器上运行时,对象将具有较快的旋转速度——Update函数将更加频繁地被调用。相关技术可处理这一类帧速率问题,以确保全部游戏可在各种设备上一致地运行,且不会受到帧速率的影响。第2章将对此加以讨论。用户可在Unity
Editor Game选项卡中方便地查看游戏的帧速率,即选取Game选项卡,并单击工具栏右上方处的Stats按钮。随后将显示Stats面板,其中包含了与游戏性能相关的整体统计数据。该面板显示了游戏每秒的帧数(FPS),进而体现了Update函数的调用频率,以及系统中游戏的整体性能。总体来讲,若FPS低于15,则系统中存在较为明显的性能问题;FPS应尽量大于30。当访问Stats面板时,相关数据如图1-10所示。
图1-10
事件类型种类繁多,Unity中某些较为常见的事件,例如Start和Update,位于MonoBehaviour类中。关于MonoBehaviour类的更多信息,读者可访问http://docs. unity3d. com/
ScriptReference/MonoBehaviour.html。
1.10 类和面向对象程序设计
类表示为多个相关变量和函数的混合产物,全部结果形成一个自包含的单元。从另一个角度来看,当考察一款游戏时(例如RPG游戏),其中往往包含了大量的独立“事物”,例如法师、半兽人、树木、房屋、玩家、任务道具、库存物品、武器装备、法术技能、门廊、桥梁、力场、入口、士兵等。大多数对象也可在现实世界中见到。严格地讲,此类事物均为独立的对象,例如,法师与力场截然不同且彼此独立;士兵不同于树木且二者间并不相关。对此,各项事物可视为具有自定义类型的对象。当关注于某一特定对象时,例如敌方的半兽人角色,则可在该对象中确定其属性和行为。相应地,半兽人对象包含了位置、旋转以及缩放状态等内容,并与多个变量对应。
除此之外,半兽人对象还可包含多帧攻击行为,其中包括手持斧子的近战型攻击,以及采用弓弩的远程攻击,各种攻击行为可通过函数予以施展。通过这一方式,变量和函数集合构成了一种具有一定意义的关系,这一整合方式称作封装。在当前示例中,半兽人封装至一个类中,该类定义了一个通用、抽象的半兽人模板(即“半兽人”这一概念)。相比之下,对象则表示为关卡中特定的Orc类实例。在Unity中,脚本文件负责定义类。当作为关卡中的一个对象实例化该类时,需要将其添加至GameObject中。如前所述,类将作为组件绑定至游戏对象上。相应地,组件定义为对象,且多个组件整体形成了GameObject。示例代码1-10显示了Orc类。
示例代码1-10
01 using UnityEngine;
02 using System.Collections;
03
04 public class Orc:MonoBehaviour
05 {
06 //Reference
to the transform component of orc (position,
rotation, scale)
07 private
Transform ThisTransform = null;
08
09 //Enum for
states of orc
10 public enum
OrcStates {NEUTRAL, ATTACK_MELEE, ATTACK_RANGE};
11
12 //Current
state of orc
13 public
OrcStates CurrentState = OrcStates.NEUTRAL;
14
15 //Movement
speed of orc in meters per second
16 public float
OrcSpeed = 10.0f;
17
18 //Is orc
friendly to player
19 public bool
isFriendly = false;
20
21
//————————————————–
22 //Use this
for initialization
23 void Start ()
24 {
25 //Get transform of orc
26 ThisTransform = transform;
27 }
28
//————————————————–
29 //Update is
called once per frame
30 void Update
()
31 {
32 }
33
//————————————————–
34 //State
actions for orc
35 public void
AttackMelee()
36 {
37 //Do melee attack here
38 }
39
//————————————————–
40 public void
AttackRange()
41 {
42 //Do range attack here
43 }
44
//————————————————–
45 }
示例代码1-10的解释内容如下所示。
q
第04行代码:关键字class用于定义一个名为Orc的类,该类继承自MonoBehaviour,稍后将对继承机制以及继承类加以讨论。
q
第09~19行代码:多个变量和枚举值添加至Orc类中。对应变量包含了不同的类型,但均与“半兽人”这一概念相关。
q
第35~45行代码:当前类包含了两个方法,即AttackMelee和AttackRange方法。
关于C#语言中的类及其应用,读者可访问http://msdn.microsoft.com/en-gb/library/ x9afc042.aspx以获取更多信息。
1.11 类和继承机制
假设某一方案定义了Orc类,并在游戏中对半兽人对象进行编码。对此,需要定义两个升级类型,分别是具有较好武器装备的OrcWarlord,以及法师OrcMage,二者均可实现一般半兽人角色具有的技能,同时还具备各自的炫技。当对其加以实现时,可定义3个独立的类,即Orc、OrcWarlord以及OrcMage类,并复制、粘贴其中的共有代码。
这里的问题是,OrcWarlord和OrcMage之间具有许多与Orc类相同的公共内容,大量的时间花费在复制共有行为的代码的复制和粘贴操作上。进一步讲,如果某一个类中的公有代码出现问题,则需要将修复结果复制、粘贴至其他类中。该过程将十分枯燥且比较危险:这一过程可能会引入其他bug,导致不必要的混乱并浪费了大量的操作时间。相反,继承这一面向对象概念可有效地处理这一类问题。继承可创建新类且隐式地涵盖了另一个类中的各项功能。也就是说,可扩展现有类进而定义新类,且不会对原始类产生任何影响。当采用继承机制时,两个类彼此间具有一定的关系。这里,原始类(例如Orc类)称作样例类或者祖先类;而新类(例如OrcWarlord或OrcMage类)则对祖先类进行适当的扩展,称作超类或继承类。
关于C#语言中的继承机制,读者可访问http://msdn.microsoft.com/en-gb/library/ ms173149%28v=vs.80%29.aspx以获取更多信息。
默认条件下,每个新的Unity脚本文件将创建一个继承自MonoBehaviour的新类。这也意味着,新脚本包含了全部MonoBehaviour所涉及的功能项,并根据相关代码添加了新内容,如示例代码1-11所示。
示例代码1-11
01 using UnityEngine;
02 using System.Collections;
03
04 public class NewScript:MonoBehaviour
05 {
06 //————————————————–
07 //Use this for initialization
08 void Start ()
09 {
10 name = “NewObject”;
11 }
12 //————————————————–
13 //Update is called once per
frame
14 void Update ()
15 {
16 }
17 }
示例代码1-11的解释内容如下所示。
q
第04行代码:NewScript类继承自MonoBehaviour类。根据派生的内容,用户可采用任意的有效类名替换MonoBehaviour。
q
第10行代码:在Start事件中,变量名赋予至某一字符串中。需要注意的是,在NewScript源文件中,该名称并非显式地作为一个变量加以声明。如果NewScript定义为一个完全的新类,且不包含第04行所定义的祖先类,则第10行代码无效。考虑到NewScript继承自MonoBehaviour,因而将自动继承全部变量,并可从NewScript处对其进行访问和编辑。
需要注意的是,应在适宜处采用继承机制;否则,类将变得异常臃肿、庞大,且容易产生混淆。如果构建某一个类,并与另一个类共享多个功能项,则可在二者间建立某种联系,并于随后使用继承机制。继承机制的另一个应用则是覆写相关函数。
1.12 类 和 多 态
示例代码1-12展示了C#语言中的多态机制。其中,示例代码并未直接展示多态的行为,而是显示了其有效性。此处,基本的框架类针对RPG游戏中非玩家控制角色(NPC)加以定义。该类并不完善,仅定义了某些基本变量,以体现角色的初始状态。其中的核心内容为SayGreeting函数,当玩家与NPC交谈时将调用该函数,并通过如下方式向控制台输出欢迎消息:
示例代码1-12
01 using UnityEngine;
02 using System.Collections;
03
04 public class MyCharacter
05 {
06 public string CharName = “”;
07 public int Health = 100;
08 public int Strength = 100;
09 public float Speed = 10.0f;
10 public bool isAwake = true;
11
12 //Offer greeting to the
player when entering conversation
13 public virtual void SayGreeting()
14 {
15 Debug.Log (“Hello,
my friend”);
16 }
17 }
对于角色在游戏中的工作方式,个问题则与MyCharacter类多样性和可信度相关。特别地,当SayGreeting函数被调用时,实例化自MyCharacter的各个角色具有相同的问候语“Hello, my friend”,其中包括男人、女人、半兽人等。该结果缺乏可信度,也并非是期望中的结果。一种可能的做法是向类中添加public字符串变量,并定制所输出的消息。然而,为了清晰地表述多态机制,下面尝试采用一种不同的解决方案。相反,这里可创建多个附加类,且均继承自MyCharacter类。其中,各个类表示为一个新的NPC类型,并借助于SayGreeting函数提供不同的问候语。这对于MyCharacter类是可行的——SayGreeting函数采用关键字virtual予以声明(参见第13行代码)。这使得继承类可覆写MyCharacter类中的SayGreeting行为。因此,继承类中的SayGreeting函数将替换基类中原始函数的操作行为,如示例代码1-13所示。
示例代码1-13
01 using UnityEngine;
02 using System.Collections;
03 //——————————————-
04 public class MyCharacter
05 {
06 public string CharName = “”;
07 public int Health = 100;
08 public int Strength = 100;
09 public float Speed = 10.0f;
10 public bool isAwake = true;
11
12 //Offer greeting to the player when entering conversation
13 public virtual void SayGreeting()
14 {
15 Debug.Log
(“Hello, my friend”);
16 }
17 }
18 //——————————————-
19 public class ManCharacter: MyCharacter
20 {
21 public override
void SayGreeting()
22 {
23 Debug.Log (“Hello, I’m
a man”);
24 }
25 }
26 //——————————————-
27 public class WomanCharacter: MyCharacter
28 {
29 public override
void SayGreeting()
30 {
31 Debug.Log (“Hello,
I’m a woman”);
32 }
33 }
34 //——————————————-
35 public class OrcCharacter: MyCharacter
36 {
37 public override
void SayGreeting()
38 {
39 Debug.Log
(“Hello, I’m an Orc”);
40 }
41 }
42 //——————————————-
当采用上述代码时,其中的某些内容将进行调整,即针对各个NPC类型ManCharacter、WomanCharacter以及OrcCharacter创建的不同类。各个类均在SayGreeting函数中提供了不同的问候语。进一步而言,NPC继承了源自共享基类MyCharacter中的全部公共行为。然而,此处会产生一个与类型相关的问题。当前,假设游戏中的一家客栈中聚集了大量的不同类型的NPC并在开怀畅饮,当玩家进入客栈后,全部NPC应显示不同的问候语。为了实现这一功能,可定义一个包含全部NPC的数组,并在循环中调用其SayGreeting函数,进而令其提供相应的问候语。初看之下,该任务似乎不可能完成——数组中的全部元素应具有相同的数据类型,例如MyCharacter[]或OrcCharacter[],同一数组中无法实现类型的混用。当然,这里可针对各个NPC类型声明多个数组,但该方法显得十分笨拙,且难以实现更多NPC类型的无缝创建,因而需要特定的方案加以处理,这也是多态机制产生的原因之一。示例代码1-14在独立的脚本文件中定义了新的Tavern类。
示例代码1-14
01 using UnityEngine;
02 using System.Collections;
03
04 public class Tavern:MonoBehaviour
05 {
06 //Array of NPCs in tavern
07 public MyCharacter[] Characters = null;
08 //——————————————————-
09 //Use this for initialization
10 void Start () {
11
12 //New array – 5 NPCs in
tavern
13 Characters = new
MyCharacter[5];
14
15 //Add characters of
different types to array MyCharacter
16 Characters[0] = new
ManCharacter();
17 Characters[1] = new
WomanCharacter();
18 Characters[2] = new
OrcCharacter();
19 Characters[3] = new
ManCharacter();
20 Characters[4] = new
WomanCharacter();
21
22 //Now run enter tavern functionality
23 EnterTavern();
24 }
25 //——————————————————-
26 //Function when player enters Tavern
27 public void EnterTavern()
28 {
29 //Everybody say greeting
30 foreach(MyCharacter C in
Characters)
31 {
32 //call SayGreeting in
derived class
33 //Derived class is
accessible via base class
34 C.SayGreeting();
35 }
36 }
37 //——————————————————-
38 }
示例代码1-14的部分解释内容如下所示。
q
第07行代码:跟踪全部NPC,且无须关注NPC的类型。此处声明了MyCharacter类型的独立数组(Characters)。
q
第16~20行代码:Characters数组中设置了多个不同类型的NPC。虽然NPC表示为不同的类型,但均继承自同一基类,因而当前方法可正常工作。
q
第27行代码:关卡启动时将调用EnterTavern函数。
q
第34行代码:foreach循环遍历Characters数组中的全部NPC,并调用SayGreeting函数。终结果如图1-11所示,并输出了各个NPC中的不同问候语,而非定义于基类中的通用消息。
图1-11
关于C#语言中的多态机制,读者可访问http://msdn.microsoft.com/en-GB/library/ ms173152.aspx以获取更多信息。
1.13 C#属性
当向类变量中赋值时,例如“MyClass.x = 10;”,需要注意某些较为重要的问题。首先,用户一般需要验证赋值结果,以确保该变量有效。通常包括值、小值范围内的整数剪裁操作;或者针对某一字符串变量的检测限定字符串集。其次,当变量被修改时,用户需要对此进行检测,并初始化其他相关函数和操作。C#属性可实现上述各项操作。在示例代码1-15中,整数限定在1~10范围内,当对其进行调整时,将向控制台输出一条消息。
示例代码1-15
01 using UnityEngine;
02 using System.Collections;
03 //——————————————————
04 //Sample class – can be attached to object as a component
05 public class Database:MonoBehaviour
06 {
07 //——————————————————
08 //Public property for private variable iMyNumber
09 //This is a public property to the variable iMyNumber
10 public int MyNumber
11 {
12 //Called when retrieving
value
13 get
14 {
15 return iMyNumber;
//Output iMyNumber
16 }
17
18 //Called when setting value
19 set
20 {
21 //If value is within
1-10, set number else ignore
22 if(value >= 1
&& value <= 10)
23 {
24 //Update private
variable
25 iMyNumber = value;
26
27 //Call event
28 NumberChanged();
29 }
30 }
31 }
32 //——————————————————
33 //Internal reference a number between 1-10
34 private int iMyNumber = 0;
35 //——————————————————
36 //Use this for initialization
37 void Start ()
38 {
39 //Set MyNumber
40 MyNumber = 11; //Will fail
because number is > 10
41
42 //Set MyNumber
43 MyNumber = 7; //Will
succeed because number is between 1-10
44 }
45 //——————————————————
46 //Event called when iMyNumber is changed
47 void NumberChanged()
48 {
49 Debug.Log(“Variable
iMyNumber changed to : ”
iMyNumber.ToString());
50 }
51 //——————————————————
52 }
53 //——————————————————
示例代码的部分解释内容如下所示。
q
第10行代码:声明了一个public整型属性,该属性并非是独立的变量。对于第34行声明的private变量iMyNumber,该属性简单地表示为一个封装器和访问器接口。
q
第13行代码:当MyNumber被使用或引用时,将调用内部的get函数。
q
第14行代码:当MyNumber被赋值时,将调用内部的set函数。
q
第25行代码:set函数包含了一个隐式参数,表示为赋值结果。
q
第28行代码:当iMyNumber遍历被赋值时,将调用NumberChanged事件。
属性对于变量的验证和数值的赋值操作十分有用。在Unity中,其主要问题集中于Object Inspector中的可见性。特别地,C#属性并不会显示于Object Inspector中。用户可在编辑器中访问或设置数值。然而,他人提供的脚本和解决方案有可能改变此类默认行为,例如暴露C#属性。读者可访问http://wiki.unity3d.com/index. php?title=Expose_properties_in_inspector以获取相应的脚本的处理方案。
关于C#属性的更多信息,读者可访问http://msdn.microsoft.com/en-GB/library/
x9fsa0sw.aspx。
1.14 注 释
注释则是向代码中插入的可供阅读的信息,通常用于说明、描述,以方便读者阅读。在C#语言中,单行注释可采用//符号表示;而多行注释则始于/*且终止于*/。注释内容在本书的示例代码中十分有用且较为重要,因而建议读者养成这一良好的操作习惯。除了开发者本人之外,团队中的其他人员也可通过注释了解编码内容。这不仅可方便地回顾代码的功能,还有助于明晰代码的内容。当然,这一切取决于简单、扼要且具有实际含义的注释内容。相应地,MonoDevelop提供了XML注释分割,并以此描述函数和参数,并与代码实现予以整合。当与团队进行合作时,这将显著地提升开发进度。下面首先考察注释的应用方式,相关函数如图1-12所示。
图1-12
随后可在函数名上方插入3个斜杠符号(///),如图1-13所示。
图1-13
MonoDevelop将自动插入模板化的XML注释,以帮助用户完成相关描述。对应部分用于描述函数的整体功能,以及函数中针对各个参数的param项,如图1-14所示。
图1-14
随后可针对当前函数,并利用注释内容填充XML模板。需要注意的是,应对各个参数添加相关注释内容,如图1-15所示。
图1-15
当在代码中调用AddNumbers函数时,将显示提示框,进而针对该函数显示注释内容,以及参数注释内容,如图1-16所示。
图1-16
1.15 变量的可见性
Unity中一项十分有用的特性是可在Unity Editor中的Object Inspector内显示public类变量,进而可编辑和预览变量,甚至在运行期内也可实现,这对于调试操作特别有用。默认状态下,Object Inspector并不显示private变量,此类变量通常隐藏于查看器中,这难以满足调试要求;或者用户至少需要监视查看器中的private变量(不会将其作用域调整为public)。相应地,存在两种方法可处理这一类问题。
种方法主要用于在类中查看全部public和private变量,并将Object Inspector切换至Debug模式。对此,可单击Inspector窗口右上方的环境菜单图标,并于其中选取Debug,如图1-17所示。当选择了Debug模式后,将会显示当前类的全部public和private变量。
图1-17
第二种方法则是显示特定的private变量,以及希望在Object Inspector中显示的并予以显式标记的变量。随后,这一类变量将在Normal和Debug模式中加以显示。对此,可利用[SerializeField]属性声明private变量,如下所示。
01 using UnityEngine;
02 using System.Collections;
03
04 public class MyClass:MonoBehaviour
05 {
06 //Will always show
07 public int PublicVar1;
08
09 //Will always show
10 [SerializeField]
11 private int
PrivateVar1;
12
13 //Will show
only in Debug Mode
14 private int
PrivateVar2;
15
16 //Will show
only in Debug Mode
17 private int
PrivateVar3;
18 }
用户还可使用[HideInInspector],进而在查看器中隐藏某一全局变量。
1.16 ?操作符
if-else语句广泛地应用于C#语言中,对此,存在一种更为简短的记述方式,且无须使用多行的if-else语句,即?操作符。该语句的基本形式如下所示。
//If condition is true then do expression 1, else do expression 2
(condition) ? expression_1 : expression_2;
在实际操作过程中,?操作符的使用方式如下所示。
//We should hide this object if its Y position is above 100 units
bool
ShouldHideObject = (transform.position.y > 100) ? true :false;
//Update object visibility
gameObject.SetActive(!ShouldHideObject);
?操作符对于较短的语句十分有效,而对于较长或者相对复杂的语句,?操作符会提升代码的阅读难度。
1.17 SendMessage和BroadcastMessage
MonoBehaviour类位于Unity API中,定义为大多数新脚本的基类,并提供了SendMessage和BroadcastMessage方法。针对绑定于某一对象上的全部组件,用户可据此方便地通过名称执行相关函数。当调用类方法时,通常需要使用到一个指向该类的局部引用,进而可访问、运行其函数,并访问其中的变量。SendMessage和BroadcastMessage函数可通过字符串值运行函数(简单地定义函数名即可),进而对代码进行简化,如示例代码1-16所示。
示例代码1-16
01 using UnityEngine;
02 using System.Collections;
03
04 public class MyClass:MonoBehaviour
05 {
06 void start()
07 {
08 //Will invoke MyFunction on ALL
components/scripts
attached to this object (where the function is present)
09 SendMessage(“MyFunction”,
SendMessageOptions.DontRequireReceiver);
10 }
11
12 //Runs when SendMessage is called
13 void MyFunction()
14 {
15 Debug.Log (“hello”);
16 }
17 }
示例代码1-16的部分解释内容如下所示。
q 第09行代码:调用SendMessage函数时,MyFunction函数将随之被调用。MyFunction函数不仅在当前类中被调用,还将在与GameObject绑定的其他组件上进行调用(如对应组件定义了MyFunction成员函数),例如Transform组件等。
q
第09行代码:如果MyFunction不存在于某一组件中,参数SendMessageOptions. DontRequireReceiver负责确定随后的行为。此处,Unity将忽略该组件,并移至下一个组件的MyFunction函数调用。
当函数隶属于某个类中,术语函数和成员函数具有相同的含义。特别地,隶属于某一个类的函数称作成员函数。
在前述内容中曾讨论到,SendMessage方法在与独立GameObject绑定的全部组件中调用特定的函数。BroadcastMessage方法进一步整合了SendMessage方法,并针对GameObject上的全部组件调用特定的函数,并于随后针对场景层次结构中的全部子对象通过递归方式重复这一过程,直至全部子对象。
关于 SendMessage和BroadcastMessage方法的更多信息,读者可访问http://docs. unity3d.com/ScriptReference/GameObject.SendMessage.html和http://docs.unity3d. com/ScriptReference/Component.BroadcastMessage.html。
SendMessage和BroadcastMessage方法简化了对象间的通信,以及组件间的通信机制。也就是说,必要时,可使组件间彼此通信,同步操作行为并复用各项功能。SendMessage和BroadcastMessage方法依赖于C#语言中的反射(reflection)机制。当采用字符串调用某一函数时,应用程序需要在运行期内对自身加以考察,针对相关功能搜索其代码。与常规方式的函数运行过程相比,这将显著地增加计算开销。对此,可搜索并小化SendMessage和BroadcastMessage方法应用,特别是在Update事件,或者与帧相关的解决方案中。当然,这并不意味着拒绝使用反射机制,少量、适宜的操作不会对终结果产生任何影响。后续章节将讨论其替代方案,即委托类和接口机制。
读者可参考下列书籍,以获取与C#语言相关的更多信息:
q
Learning C# by Developing Games with Unity 3D Beginner‘s Guide, Terry Norton, Packt Publishing。
q
Intro to C# Programming and Scripting for Games in Unity, Alan Thorn(其视频教程位于https://www.udemy.com/3dmotiveintro-o-c-programming-and-scripting-for-
games-in-unity/)。
q
Pro Unity Game Development with C#, Alan
Thorn, Apress。
相关的在线资源如下所示:
q
http://msdn.microsoft.com/en-gb/library/aa288436%28v=vs.71%29.
aspx。
q
http://www.csharp-station.com/tutorial.aspx。
q
http://docs.unity3d.com/ScriptReference/。
1.18 本 章 小 结
本章整体讨论了Unity中的C#语言部分,针对游戏开发阐述了较为常用的语言特性。后续章节将通过更为高级的方式回顾此类话题,有助于理解并编写代码。本章内容应引起读者足够的重视。
评论
还没有评论。