UDN
Search public documentation:

UnrealScriptFoundationsCH
English Translation
日本語訳
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

UE3 主页 > 虚幻脚本 > 虚幻脚本基本概念

UnrealScript基本概念


概述


UnrealScript语言是个自定义构建的脚本语言,仅由虚幻引擎使用,用于向使用虚幻引擎制作的游戏中添加新的游戏性对象,而不需要重新编译整个引擎。它为游戏程序员提供了在游戏中使用的专用功能,并使得创建基于事件的游戏性实现起来更加轻松更加高效。

这里讨论的主题可能提及一些语言功能,比如类、变量、函数等,这些功能本身的说明不在本文的范围之内。本页面讲解了一些和特定语言功能没有必要关系但是对于理解这些功能来说非常重要的概念,特别是对初学者来说。请参照 UnrealScript参考指南页面获得关于所有语言功能的详细信息。

什么是UnrealScript?


每个UnrealScript文件通常也被称为一个UnrealScript。这是个具有 .uc 文件扩展名的简单文本,它包含了一个单独的UnrealScript类的定义。请参照 UnrealScript类页面获得关于这个类是什么、引擎如何使用这些类、以及如何定义新类的更多信息。

从本质上讲,可以使用任何文本编辑器程序创建UnrealScripts,但是某些文本编辑器没有针对UnrealScirpt提供可以更加方便地使用UnrealScript的高亮显示功能及其他特定功能。某些集成开发环境 - nFringe和WOTGreal - 也提供了使用UnrealScript开发Unreal项目的完整集成解决方案。

脚本命名和位置


UnrealScript文件的名称必须总是精确地和它定义的类的名称相匹配。如果文件名称和类名称之间存在差异,那么在编译过程中将会产生错误。

UnrealScripts位于 Development\Src 目录中的各种文件夹内。这个目录默认包含了很多文件夹 - Core、Engine、UDKBase、UnrealEd等 - 每个文件夹代表不同的UnrealScript工程或包。在每个包文件夹中都有一个 Classes 文件夹,它包含了属于该包的所有UnrealScripts 。

当您想为您的游戏添加引擎所使用的自定义UnrealScirpts时,您必须在 Development\Src 目录中创建一个或多个新的包文件及后续的 Classes 文件夹。这些文件夹是您放置您的UnrealScripts的地方。

请参照自定义UnrealScirpt项目获得关于设置您自己的由引擎使用的自定义UnrealScript包的更多信息。

UnrealScript剖析


MyActor.uc
  
  /*********************
   * Class Declaration *
   *********************/
  
  
  class MyActor extends Actor;
  
  
  /************************************
   * Instance Variables/Structs/Enums *
   ************************************/
  
  
  enum MyEnum
  {
  	ME_None.
  	ME_Some,
  	ME_All
  }
  
  struct MyStruct
  {
  	var int IntVal;
  	var float FloatVal;
  }
  
  var int IntVar;
  
  var float FloatVar;
  
  var bool BoolVar;
  
  var Actor ActorVar;
  
  var MyEnum EnumVar;
  
  var MyStruct StructVar;
  
  
  /**********************
   * Functions & States *
   **********************/
  
  
  function MyFunction()
  {
  	local int TempInt;
  
  	if(ActorVar != none && BoolVar)
  	{
  		TempInt = IntVar;
  	}
  }
  
  state NewState
  {
  	function MyFunction()
  	{
  		local float TempFloat;
  
  		if(ActorVar != none && BoolVar)
  		{
  			TempFloat = FloatVar;
  		}
  	}
  }
  
  
  /**********************
   * Default Properties *
   **********************/
  
  
  defaultproperties
  {
  	IntVar=5
  	FloatVar=10.0
  	BoolVar=true
  }
  

类声明
每个UnrealScript都以类声明开始。这也就是命名和该脚本相关的类、确定该类所继承的其他类,并可以通过类修饰符来控制类的其他几个方面,这些修饰符选项添加在声明的结尾处。除了您想在开头处添加描述类的注释、指定版权等外,这些应该是脚本中最先出现的内容。
实例 变量/结构体/枚举值
类声明的后面是实例变量声明。这指定了类所包含的属性。
函数 & 状态
函数声明和状态声明构成了类的大部分内容。这些声明指出了类可以执行的动作。
默认属性
defaultproperties 是脚本中最后出现的内容。这是为类中声明的实例变量指定默认值的地方。

脚本和类 对 Objects和Actors


从本质上讲,一个类是游戏中引擎使用的一个物品的设计蓝本。它通过变量函数定义了该物品可以具有的属性和行为。这些设计蓝本用于创建这些物品的单独的、唯一的实例。一个类的唯一实例在一般的面向对象编程中称为一个对象。这个概念在UnrealScript中进行了强化,因为Object也是类层次结构中的基类。所以从某种程度来讲,每个类都继承Object类,任何类的实例从技术上讲都是一个对象。一个对象可以具有它自己的可以进行修改和访问的属性值及可以执行的行为或动作,这些行为和动作在执行时不会影响世界中的其他高射炮实例。

好了,一个类是个设计蓝本,而一个对象是个实例,但是它们的真正意义是什么哪? 以高射炮为例,它是虚幻竞技场中常见的武器之一。高射炮有弹药,发射几个小的射弹作为一级火力,发射一个大的炮弹作为二级火力。这就是高射炮的设计蓝本或Flak Cannon(高射炮)类(进行了极度简化)。现在,当玩家跑到世界中一个高射炮拾取物处时,将会给那个玩家一个高射炮实例。但,这样的真正意义是什么哪? 它意味着使用Flak Cannon(高射炮)类作为设计蓝本创建一个新的高射炮。这是一个具有其自己的属性值的特定高射炮,它可以独立于其他高射炮执行各种行为。所以,当玩家开火时,那个特定高射炮实例的弹药量降低,并从那个特定高射炮发射一个射弹,而不影响其他高射炮。

这是个面向对象编程的基本概念,也是您在进一步深入进行UnrealScript编程之前必须要理解的概念。也就是说,我们不会在这里对其进行深入研究,因为已经有大量的资源介绍了该主题,并且已经介绍的非常详细。

脚本间的通信


那么,如果您使用UnrealScript创建类,但世界中填充的是实例而不是类本身,那么脚本之间是如何通信的哪? 当您书写类时,类并不知道世界中存在哪些实例。这方面的一个示例是具有上述的高射炮的同一个玩家。玩家是如何通知发射该特定高射炮而不是同样的其他发射器炮哪? 答案是 引用 。UnrealScript中其中一种变量类型是对象引用数据类型。这是Object(或Actor)类型变量的特定名称。一个对象引用变量的值是到一个那种对象类型的实例的引用。比如,Controller类有一个变量,它用于引用它当前控制的 Pawn 。这个变量的类型是 Pawn ( 因此它的名称也是 Pawn )。这个变量的声明如下所示:

  var Pawn Pawn;
  

在上面的高射炮示例中,玩家类(实际上在这个示例中是Pawn)将具有一个存放到玩家当前正在使用的武器的引用的变量。这个变量指向当玩家跑向那个武器拾取物处时给与玩家的高射炮实例。这是允许直接和那个实例通信的一种方法。

这是个非常重要的概念,主要用于完成您将在UnrealScript中完成的任何处理。无论何时当一个对象需要访问另一个对象的属性的值或者告诉另一个对象执行某个动作时,该对象需要具有一个到另一个对象的引用。当然,现在您一定在想您该怎样获得到世界中您想和其进行通信的实例的引用。简单地为该引用声明一个对象引用变量还不足以完成该处理。一个对象引用变量仅是一个存放引用的容器。直到您给该对象引用变量赋予某值之前,它的值将是 none 。实际上,您必须将一个值(您想引用的实例)赋予那个变量。不幸的是,这里没有固定的规则可以遵循。有很多获得到世界中一个对象实例的引用的方法,您如何操作完全取决于您的特定情况及那两个对象之间的关系。一些示例:

  • 辅助函数- 在某些情况下,某些类中提供了一些辅助函数,它们可以返回到常用对象的引用。比如, GFxMoviePlayer 类具有 GetPC() 函数,它返回到具有那个视频的 PlayerController 的引用。
  • 事件- 一个actor在另一个actor中触发一个事件,比如 Touch 事件。很多情况下,引擎会自动地将触发事件的actor的引用传递给那个事件。这允许您在事件中使用那个引用或者将其保存到一个变量中以便稍后使用。
  • 生成 - Spawn() 函数返回到它创建的actor的引用。当一个actor生成另一个actor并需要立即或稍后同该生成的actor进行通信时,您可以使用 Spawn() 函数返回的引用或者将该引用保存到一个变量中。
  • 第三方 - 很多时候,您需要引用的对象正在被您所引用的另一个类所引用。这意味着您可以您可以借用那个引用为己用。作为示例,一般需要及使用这个处理的是当前游戏类型或者 GameInfo 类的实例。唯一一个具有到游戏类型直接引用的类是 WorldInfo ,它通过它的 Game 进行引用,但是您通常需要在其他位置访问该引用。幸运的是, 每个Actor都具有到当前 WorldInfo 实例的引用,通过 WorldInfo 变量实现。这意味着您可以使用您的 WorldInfo 引用来获得到 GameInfo 实例的引用。这种方法通常被初学者所忽略。要经常检查您已经引用的对象,因为它们可能具有您正在查找的引用。
  • 迭代- 使用迭代器函数类似于执行一种搜索。它们允许您指定一些标准并以对象引用的形式返回一个结果列表。您可以在迭代器中直接对这些对象引用进行操作或者将它们保存到变量中稍后使用。
  • 编辑器 - 在某些情况下,您可以让关卡设计人员在编辑器中设置一个到actor的引用。这所需要做的就是创建一个可编辑变量来存放该引用。

一旦您具有了一个对象引用,那么您便可以通过使用特殊的语法(通常称为 点操作符 )访问那个对象的变量和函数 。这就是说在对象引用后面跟着一个点 (或句号) - 然后跟随着您想访问或执行的变量或函数的名称。

以上面提到的Controller中的Pawn引用为例。如果您想访问 Controller的Pawn中的 Health 变量,您可以这么做:

  Pawn.Health
  

类似地,如果您想告诉Pawn开火他的主要武器,那么您可以使用 StartFire() 函数来实现:

  Pawn.StartFire(0);
  

对象引用变量存放了到对象的引用,但要想引用一个对象,您并不总是需要一个对象引用变量。您可以将辅助函数的结果赋予一个变量,并使用那个变量作为您的引用,也可以直接使用函数结果作为对象引用。

为了示范这个处理,让我们使用上面讨论的 GFxMoviePlayer 类中的 GetPC() 函数。该函数返回了具有Scaleform视频的 PlayerController 。现在,假设我们想访问当前玩家的生命值,该值存放在由PlayerController控制的Pawn所存放。和上面一样,我们可以使用controller中的 Pawn 变量,但是这次我们必须先引用controller,因为我们位于 GFxMoviePlayer 类中。但我们不需要将该引用保存到一个变量中。我们可以直接结合 GetPC() 函数调用使用点操作符。

  GetPC().Pawn.Health
  

现在,我们假设在UI上有一个按钮,该按钮用于开火玩家的武器。我们可以像先前一样完成这个处理。

  GetPC().Pawn.StartFire(0);
  

这是可以正常工作的,因为从本质上讲可以认为 GetPC() 是到 PlayerController 实例的引用,因为该实例使它返回的值,正如您在 GFxMoviePlayer 类的函数声明中所看到的一样:

  /**
   * Helper function to get the owning player controller for this movie
   *
   * @return The PlayerController corresponding to the LocalPlayerOwnerIndex that owns this movie
   */
  event PlayerController GetPC()
  {
  	local LocalPlayer LocalPlayerOwner;
  
  	LocalPlayerOwner = GetLP();
  	if (LocalPlayerOwner == none)
  	{
  		return none;
  	}
  	return LocalPlayerOwner.Actor;
  }
  

如何使用现有脚本?


虚幻引擎中包含了大量的UnrealScript脚本类,它们的实现位于 Development\Src 目录下的几个不同的包中。理解这些类是什么及如何在游戏中使用它们是非常重要的。现有的类主要提供了基本的通用的功能。从本质上讲,它们是引擎本身的一部分而不是“游戏”的一部分,尽管有些时候这很难分辨。大部分主要的系统及构成它们的类都在UDN上的各种“技术指南”中进行了解释。要想获得关于所有这些可用类的完整理解,您可能需要通过跟踪实际的脚本本身并分析代码、阅读注释等来进行研究。关于这方面的一个非常有用的工具是 UnCodeX 。它根据脚本创建了Java帮助文档风格的文档,很容易导航阅读。

首先要理解的是在正常情况下您不能修改现有的脚本。修改native类或native包中的类且不重新编译引擎将会导致严重的后果,一般会导致游戏运行时引擎崩溃。

UnrealScript是个面向对象的语言,意味着每个类都以UnrealScript 用法继承或 扩展 另一个类,从而创建了一种父类-子类关系。子类继承其父类中存在的所有变量、函数、状态等。所以,您应该继承现有脚本,使用它们作为您自己的自定义类的起点,而不是修改现有脚本。这样做的好处是您不会受到父类传递给子类的内容的限制,因为您可以添加您需要的任何新变量和函数;您也不会受到父类所传递的函数的实现方式的限制,因为UnrealScript允许您重载任何继承的函数,使它执行您需要的任何特定动作。

某些情况下,仅继承类而不能修改类看上去是种限制,因为通常都会想到在现有基类上添加一个变量,以便该变量可以在所有其子类中可用。但通常都有一种不需要修改现有类而可以设计出您的系统的可替换方法。

基本规则: 继承现有类并重载函数,永远不要修改现有类。

继承哪些类


在UnrealScript中典型的设计类的方法是构造一个继承了具有您所需要的大部分功能的现有类(比如Pawn类,即所有怪物的基类)的新类(比如一个”人身牛头怪物”)。使用这个方法,你从来不需要重新发明新的东西---你可以简单地增加您想自定义的新功能,然而保持所有你不需要进行自定义的现有的功能。这个方法在Unreal中实现AI时显得尤其有用,因为在内置的AI系统中提供了大量的基础功能,您可以把它们作为您自定义的创造物的组成部分。

掌握所继承的类层次的深度完全是由不同的情况决定的。很多时候,最终选择都归结为重新实现某些功能来避免重载大量的不必要的功能或者直接继承功能。选择哪个方法完全由您决定。再次说明,最好的方法就是找到和您想实现的类具有大部分类似或共享大部分通用功能的类,然后以它为基础开始实现。

对象层次


在使用UnrealScript进行工作之前,理解Unreal中高层次地理解对象之间的关系是非常重要的。Unreal的架构和其它大多数的游戏架构是有较大的背离的: Unreal是完全地面向对象的(非常像 COM/ActiveX),它有一个定义明确的对象模型,可以支持高层次的面向对象的概念,比如对象图、序列化、对象生命周期和多态性。从游戏历史上来说,尽管很多游戏比如Doom和Quake,已经证明在内容方面有较大的可扩展性,但大多数游戏都被设计成为整体式的,它们的主要功能都是被硬编码的并且是在对象的层次上不能扩展的。Unreal的面向对象的形式有一个很大的好处: 主要的新功能和对象类型可以在运行时添加到Unreal中,并且这个扩展是以划分子类的方式进行的,而不是(比如)通过修改一串已有的代码。这种扩展的形式是非常强大的,因为它鼓励Unreal团队共同来创建所有人都可以操作的改善。

Object
Unreal中所有对象的父类。在 Object 类中的所有函数可以在任何地方访问,因为任何东西都继承于 Object 。==Object== 是一个抽象基类,它没有做任何有用事情。所有的功能都是由子类提供,比如 Texture (一个贴图)、 TextBuffer (文字块)和 Class (它描述了其它对象的类)。
Actor (extends Object)
Unreal中所有独立游戏对象的父类。Actor包含了一个物体所需要的用于四处运动、和其它物体进行交互、影响环境及做其它与游戏相关的有用的事情的所有功能。
Pawn (extends Actor)
Unreal中所有的可以使用高层次AI和玩家控制的生物和玩家的父类。
Class (extends Object)
一种特殊的对象类别,描述了一类对象。刚看到时可能是令人疑惑的: 一个类是一个对象,而其一个类描述某些对象。但是这概念是合理的,并且在很多情况下您将会用到Class的对象。例如,当你在UnrealScript产生一个新的actor,你可能会通过Class对象来指定actor的类。

使用UnrealScript,你可以为任何Object类书写代码,但是99%的时候您都是在为从Actor继承的类来写代码。大多数有用的UnrealScript功能是游戏相关的并且是处理actors的。

UnrealScript编程策略


这里是如何高效地书写UnrealScript代码的一些技巧,充分地利用UnrealScript的优势而避免缺陷。

  • UnrealScript和C/C++ 相比是一个较慢的语言 。
 一个典型的C++程序的运行速度会比UnrealScript快20倍。我们自己书写脚本的编程思想是: 书写的脚本在大部分时候是不运行的。换句话说,仅使用UnrealScript来处理你想进行自定义的感兴趣的事件,而不是反复执行的任务,比如基本的运动,这些由Unreal的物理代码为您处理。比如,当您书写一个射弹脚本,你一般书写HitWall()、Bounce()和Touch()函数来描述当主要的事件发生时需要做什么处理即可。因此95%的时间中,您的射弹脚本是不会被执行的,它仅仅是等待物理代码来通知它一个事件。这是非常的高效的。在一般的关卡中,尽管UnrealScript要比C++慢很多,但UnrealScript的执行时间平均只占CPU执行时间的5-10%。
  • 尽可能地使用latent 函数(比如FinishAnim和Sleep) 。通过基于这些latent函数控制你的脚本的执行流程,您正在创建动画驱动或时间驱动的代码,这在UnrealScript中是非常高效的,
  • 当您在测试脚本时,请关注您的Unreal 日志 。在运行时通常会在日志文件中生成一些有用的警告来通知您正在发生的非致命问题。
  • 提防会引起无限递归的代码 。比如,"Move"命令移动actor,当您在碰撞某物时调用您的Bump()函数。因此,如果您在一个Bump函数中使用一个Move命令,那么请小心,您有进入无限递归的危险。注意: 无限递归和无限循环是UnrealScript不能很好处理的两个错误情形。
  • 在服务器端产生和销毁actors 是相当昂贵的操作,在网络游戏中则更加的昂贵,因为产生和销毁actors 会占用网络带宽 。请您合理的使用它们,把actors当成“性能消耗非常大”的对象对待。比如,不要尝试通过产生100个独立的actors来创建一个粒子系统并使用物理代码让它们在不同的轨道进行发射。那将会导致运行非常非常的慢。
  • 尽可能地使用UnrealScript 的面向对象兼容性能 。通过重载现有的函数和状态来创建新的功能,会使干净的代码更容易进行修改及更容易和其他人的代码进行集成。避免使用传统的C技术,比如基于一个actor或state的类来使用一个switch()语句,因为这样的代码在您增加一个新类及修改一些东西时更容易被破坏。
  • UnrealScript的.u包文件是严格地按照.ini文件的EditPackages列表进行编译的 ,所以每个包仅能引用本包中及在它前面编译的包中的对象,而不能引用在它后面编译的包中的对象。如果您发现需要在包之间进行循环引用,有两个解决方案:
    1. 把类分成需要在第一个.u包中进行编译的一组基类包和一组需要在第二个.u包中进行编译的子类包,要保证基类永远不会引用子类。不管怎样,这都是很好的编程实践,一般都会有效。
      注意: 如果给定类 C 需要一个到滞后连编包中的一个类或对象 O 的引用,那么您可以把那个类分解为两部分: 在第一个包中定义一个抽象基类 C ,它定义了一个变量 MyO (但在它的默认属性中不要给 MyO 设定默认值);在第二个包中定义一个子类 D ,它为 MyO 指定了适当的默认值,而且仅能在第二个包中来进行默认值的指定操作。
    2. 如果两个.u包由于相互引用而彻底的纠缠在一起,那么可以把它们融合到一个包中。这是合理的,因为包是作为代码模块化的一个单元来设计的,并且把这些不能分开的一组类分别放到多个包中不能带来任何实质性的好处(比如节约内存)。