第6章 SWT概述



第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所示。

[pic]

图1-1

另一种方法则是右击Project面板中的空白区域,并在快捷菜单中选择Create1的C# Script命令,如图1-2所示。这将在当前开启的文件夹中创建数据资源。

[pic]

图1-2

当创建完毕后,新的脚本文件将位于Project文件夹内,且包含.cs扩展名(表示C#文件)。该文件名十分重要,并对脚本文件的有效性产生重要影响——Unity使用该文件名确定创建于该文件内的C#类的名称。本章稍后将对类加以深入讨论。简而言之,用户应确保文件包含唯一且具有实际意义的名称。

关于唯一性,其含义是指项目中的其他文件名不应与此相同,无论该文件是否位于不同的文件夹内。也就是说,全部脚本文件在项目中应具有唯一的名称。另外,文件名应具有实际意义,并表达脚本行将执行的任务。进一步讲,C#语言中存在多种有效规则可对文件名和类名予以限定。关于此类规则的正式定义,读者可访问. com/en-us/library/aa664670%28VS.71%29.aspx。简单地讲,文件名应始于字母或下划线字符(不允许采用数字作为首字符);同时,文件名不应包含空格。对应示例如图1-3所示。

[pic]

图1-3

Unity脚本文件可在任意文本编辑器或IDE中打开,包括Visual Studio和Notepad++,但Unity提供了免费的开源编辑器MonoDevelop。该软件为主Unity包中的部分内容,并包含于安装过程中,因而无须单独下载。当在Project面板中双击脚本文件时,Unity将在MonoDevelop内自动打开文件。如果在后续操作中决定更改脚本文件名,则还需要在文件内修改C#类的名称,以使其与文件名准确匹配,如图1-4所示。否则,这将生成无效代码以及编译错误,并在脚本文件与对象绑定时出现问题。

[pic]

图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所示。

[pic]

图1-5

1.4节将对变量进行讨论。

关于Unity中脚本的创建和应用,读者可访问 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。对应示例如下所示:

❑ int(整数)= –3,–2,–1,0,1,2,3,…。

❑ float(浮点数或小数)= –3.0,–2.5,0.0,1.7,3.9,…。

❑ bool(布尔值或true/false)= true或false(1或0)。

❑ string(字符串)="hello world","a","another word…"。

❑ 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#语言中变量及其应用的更多信息,读者可访问 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语句。

[pic]

图1-6

关于C#语言中if语句、if-else语句应用的更多信息,读者可访问. com/en-GB/library/5011f09h.aspx。

在图1-6中,Unity中的调试工具为控制台,并可通过Debug.Log语句(或Print函数)于此处输出消息,以供开发人员进行查看。这对于诊断运行期或编译期内的问题十分有效。针对编译期或运行期错误,相关信息显示于Console选项卡中。默认状态下,Console选项卡于Unity Editor中处于可见状态;另外,也可采用手动方式显示该选项卡,即在Unity应用菜单中,选择Window菜单中的Console选项。关于Debug.Log函数的更多信息,读者可访问 Debug.Log.html。

除了相等条件(==)之外,用户还可对其他条件进行检测。例如,可使用>或= 0 && PlayerHealth = 0; i--)

{

//Destroy object

Destroy (MyObjects[i]);

}

对应的解释内容如下所示。

❑ for循环反向遍历MyObjects数组,并删除场景中的GameObject对象。其中使用了局部变量i,该变量用于控制循环的进程,因而也称作Iterator变量。

❑ for循环由以下3部分构成,各部分通过分号隔开。

➢ i:该变量初始化为MyObjects.Length – 1(即数组中的最后一个元素)。回忆一下,由于数组采用了0索引机制,因而最后一个元素的位置表示为MyObjects.Length – 1。这也使得循环始于数组的结尾处。

➢ i>=0:该表达式表示为循环结束时的条件。这里,变量i表示为倒计数变量。此时,当i不再大于或等于0时,循环结束,此处0表示为数组的开始位置。

➢ i--:在每次循环过程中,该表达式控制变量i的变化方式,这里是从数组的开始位置至结束位置。期间,i将以此递减1,也就是说,每次循环时i将减去1。相比较而言,++语句将加1。

❑ 在循环期间,表达式MyObjects[i]用于访问数组与元素。

关于C#语言中的for循环,读者可访问 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中的相关解释如下所示。

❑ 第13行代码:通过重复条件启动while循环,直至整型变量NumberOfMessages大于或等于5。

❑ 第15~19行代码:作为while循环体重复执行。

❑ 第19行代码:在每次循环过程中递增变量NumberOfMessages。

如在游戏模式下执行示例代码1-7,当关卡启动时,最终结果将向Unity Console输出5条文本消息,如图1-9所示。

[pic]

图1-9

关于C#语言中while循环及其应用,读者可访问 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的相关解释如下所示。

❑ 第08行代码:声明了一个私有整型变量Score,并用于记录分值。该变量稍后将用于UpdateScore函数中。

❑ 第11、23、28行代码:MyScriptFile类包含3个函数(有时也称作方法或成员函数),即Start、Update以及UpdateScore函数。其中,Start和Update为Unity提供的特殊函数,稍后将对此加以讨论。UpdateScore函数则是针对MyScriptFile类的自定义函数。

❑ 第28行代码:UpdateScore函数表示一个完整的代码块,位于第29~38行之间。当游戏积分榜每次发生变化时,将会调用该函数。通过这一方式,函数可提供代码的复用性。

❑ 第14~19行代码:UpdateScore函数在Start函数中被调用多次。在每次调用过程中,Start函数将暂停其执行过程,直至UpdateScore函数执行完毕。此时,执行过程将在下一行代码处恢复进行。

❑ 第28行代码:UpdateScore接收两个参数,即整型参数AmountToAdd和布尔参数PrintToConsole。参数类似于输入内容,置于函数中并影响其操作方式。变量AmountToAdd表示向当前Score变量所加入的量值。当函数被执行时,PrintToConsole用于确定Score变量是否显示于Console窗口中。从理论上讲,参数的数量并无限制;另外,函数也可不包含任何参数,例如Start和Update函数。

❑ 第31~34行代码:必要时,可更新分数值并输出至Console中。需要注意的是,PrintToConsole参数包含了默认值true,并在第28行声明中赋值完毕。当函数被调用时,参数将处于可选状态。通过传递false值,第14、15、16行代码显式地覆写了默认值。相比而言,第19行代码则忽略了第二个值,因而只接收默认值true。

❑ 第28、37行代码:UpdateScore函数包含一个返回值,在第28行中,该返回值表示为特定的数据类型,且位于函数名之前。此处,返回值定义为int。这意味着,当函数退出或执行完毕后,将输出一个整数值。在当前示例中,这一整数值表示为当前的Score。在第37行代码中,这实际上表示为基于return语句的输出值。另外,函数也可不包含任何返回值,对此,返回类型可定义为void,例如Start和Update函数。

关于C#语言中的函数及其应用,读者可访问 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所示。

[pic]

图1-10

事件类型种类繁多,Unity中某些较为常见的事件,例如Start和Update,位于MonoBehaviour类中。关于MonoBehaviour类的更多信息,读者可访问. 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的解释内容如下所示。

❑ 第04行代码:关键字class用于定义一个名为Orc的类,该类继承自MonoBehaviour,稍后将对继承机制以及继承类加以讨论。

❑ 第09~19行代码:多个变量和枚举值添加至Orc类中。对应变量包含了不同的类型,但均与“半兽人”这一概念相关。

❑ 第35~45行代码:当前类包含了两个方法,即AttackMelee和AttackRange方法。

关于C#语言中的类及其应用,读者可访问 x9afc042.aspx以获取更多信息。

1.11 类和继承机制

假设某一方案定义了Orc类,并在游戏中对半兽人对象进行编码。对此,需要定义两个升级类型,分别是具有较好武器装备的OrcWarlord,以及法师OrcMage,二者均可实现一般半兽人角色具有的技能,同时还具备各自的炫技。当对其加以实现时,可定义3个独立的类,即Orc、OrcWarlord以及OrcMage类,并复制、粘贴其中的共有代码。

这里的问题是,OrcWarlord和OrcMage之间具有许多与Orc类相同的公共内容,大量的时间花费在复制共有行为的代码的复制和粘贴操作上。进一步讲,如果某一个类中的公有代码出现问题,则需要将修复结果复制、粘贴至其他类中。该过程将十分枯燥且比较危险:这一过程可能会引入其他bug,导致不必要的混乱并浪费了大量的操作时间。相反,继承这一面向对象概念可有效地处理这一类问题。继承可创建新类且隐式地涵盖了另一个类中的各项功能。也就是说,可扩展现有类进而定义新类,且不会对原始类产生任何影响。当采用继承机制时,两个类彼此间具有一定的关系。这里,原始类(例如Orc类)称作样例类或者祖先类;而新类(例如OrcWarlord或OrcMage类)则对祖先类进行适当的扩展,称作超类或继承类。

关于C#语言中的继承机制,读者可访问 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的解释内容如下所示。

❑ 第04行代码:NewScript类继承自MonoBehaviour类。根据派生的内容,用户可采用任意的有效类名替换MonoBehaviour。

❑ 第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的部分解释内容如下所示。

❑ 第07行代码:跟踪全部NPC,且无须关注NPC的类型。此处声明了MyCharacter类型的独立数组(Characters)。

❑ 第16~20行代码:Characters数组中设置了多个不同类型的NPC。虽然NPC表示为不同的类型,但均继承自同一基类,因而当前方法可正常工作。

❑ 第27行代码:关卡启动时将调用EnterTavern函数。

❑ 第34行代码:foreach循环遍历Characters数组中的全部NPC,并调用SayGreeting函数。最终结果如图1-11所示,并输出了各个NPC中的不同问候语,而非定义于基类中的通用消息。

[pic]

图1-11

关于C#语言中的多态机制,读者可访问 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

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 //------------------------------------------------------

示例代码的部分解释内容如下所示。

❑ 第10行代码:声明了一个public整型属性,该属性并非是独立的变量。对于第34行声明的private变量iMyNumber,该属性简单地表示为一个封装器和访问器接口。

❑ 第13行代码:当MyNumber被使用或引用时,将调用内部的get函数。

❑ 第14行代码:当MyNumber被赋值时,将调用内部的set函数。

❑ 第25行代码:set函数包含了一个隐式参数,表示为赋值结果。

❑ 第28行代码:当iMyNumber遍历被赋值时,将调用NumberChanged事件。

属性对于变量的验证和数值的赋值操作十分有用。在Unity中,其主要问题集中于Object Inspector中的可见性。特别地,C#属性并不会显示于Object Inspector中。用户可在编辑器中访问或设置数值。然而,他人提供的脚本和解决方案有可能改变此类默认行为,例如暴露C#属性。读者可访问. php?title=Expose_properties_in_inspector以获取相应的脚本的处理方案。

关于C#属性的更多信息,读者可访问 x9fsa0sw.aspx。

1.14 注 释

注释则是向代码中插入的可供阅读的信息,通常用于说明、描述,以方便读者阅读。在C#语言中,单行注释可采用//符号表示;而多行注释则始于/*且终止于*/。注释内容在本书的示例代码中十分有用且较为重要,因而建议读者养成这一良好的操作习惯。除了开发者本人之外,团队中的其他人员也可通过注释了解编码内容。这不仅可方便地回顾代码的功能,还有助于明晰代码的内容。当然,这一切取决于简单、扼要且具有实际含义的注释内容。相应地,MonoDevelop提供了XML注释分割,并以此描述函数和参数,并与代码实现予以整合。当与团队进行合作时,这将显著地提升开发进度。下面首先考察注释的应用方式,相关函数如图1-12所示。

[pic]

图1-12

随后可在函数名上方插入3个斜杠符号(///),如图1-13所示。

[pic]

图1-13

MonoDevelop将自动插入模板化的XML注释,以帮助用户完成相关描述。对应部分用于描述函数的整体功能,以及函数中针对各个参数的param项,如图1-14所示。

[pic]

图1-14

随后可针对当前函数,并利用注释内容填充XML模板。需要注意的是,应对各个参数添加相关注释内容,如图1-15所示。

[pic]

图1-15

当在代码中调用AddNumbers函数时,将显示提示框,进而针对该函数显示注释内容,以及参数注释内容,如图1-16所示。

[pic]

图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变量。

[pic]

图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的部分解释内容如下所示。

❑ 第09行代码:调用SendMessage函数时,MyFunction函数将随之被调用。MyFunction函数不仅在当前类中被调用,还将在与GameObject绑定的其他组件上进行调用(如对应组件定义了MyFunction成员函数),例如Transform组件等。

❑ 第09行代码:如果MyFunction不存在于某一组件中,参数SendMessageOptions. DontRequireReceiver负责确定随后的行为。此处,Unity将忽略该组件,并移至下一个组件的MyFunction函数调用。

当函数隶属于某个类中,术语函数和成员函数具有相同的含义。特别地,隶属于某一个类的函数称作成员函数。

在前述内容中曾讨论到,SendMessage方法在与独立GameObject绑定的全部组件中调用特定的函数。BroadcastMessage方法进一步整合了SendMessage方法,并针对GameObject上的全部组件调用特定的函数,并于随后针对场景层次结构中的全部子对象通过递归方式重复这一过程,直至全部子对象。

关于 SendMessage和BroadcastMessage方法的更多信息,读者可访问. ScriptReference/GameObject.SendMessage.html和. com/ScriptReference/Component.BroadcastMessage.html。

SendMessage和BroadcastMessage方法简化了对象间的通信,以及组件间的通信机制。也就是说,必要时,可使组件间彼此通信,同步操作行为并复用各项功能。SendMessage和BroadcastMessage方法依赖于C#语言中的反射(reflection)机制。当采用字符串调用某一函数时,应用程序需要在运行期内对自身加以考察,针对相关功能搜索其代码。与常规方式的函数运行过程相比,这将显著地增加计算开销。对此,可搜索并最小化SendMessage和BroadcastMessage方法应用,特别是在Update事件,或者与帧相关的解决方案中。当然,这并不意味着拒绝使用反射机制,少量、适宜的操作不会对最终结果产生任何影响。后续章节将讨论其替代方案,即委托类和接口机制。

读者可参考下列书籍,以获取与C#语言相关的更多信息:

❑ Learning C# by Developing Games with Unity 3D Beginner's Guide, Terry Norton, Packt Publishing。

❑ Intro to C# Programming and Scripting for Games in Unity, Alan Thorn(其视频教程位于 games-in-unity/)。

❑ Pro Unity Game Development with C#, Alan Thorn, Apress。

相关的在线资源如下所示:

❑ . aspx。

❑ 。

❑ 。

1.18 本 章 小 结

本章整体讨论了Unity中的C#语言部分,针对游戏开发阐述了较为常用的语言特性。后续章节将通过更为高级的方式回顾此类话题,有助于理解并编写代码。本章内容应引起读者足够的重视。

................
................

In order to avoid copyright disputes, this page is only a partial summary.

Google Online Preview   Download