UnrealScript

维基教科书,自由的教学读本

导言[编辑]

文档目标[编辑]

这是一份讲述UnrealScript语言的技术性文档。这不是一个教程,也没有提供有用的UnrealScript代码的详细例子。读者可以参考虚幻引擎,它包了数万行的UnrealScript源代码,它解决了诸如人工智能,运动,inventory,和触发器等问题。一个好的入手点是看"Actor", "Object", "Controller", "Pawn", and "Weapon" 的脚本。

本文假设读者具备C/C++ 或Java的编程经验,熟悉面向对象编程,玩过虚幻游戏,用过虚幻关卡编辑器(UnrealEd)。

对于OOP(面向对象编程)新手,强烈建议去书店买本Java的书。Java和UnrealScript非常相似,它清晰、简洁、优秀。

UnrealScript的设计目标[编辑]

由于游戏编程的自然需要和细微差别UnrealScript被创建出来,为开发团队和第三方虚幻开发商提供了一个强大的内置编程语言。

UnrealScript的主要设计目标是:

  • 支持传统编程语言没有解决的时间,状态,属性的网络化。这大大简化了UnrealScript的代码。编写基于人工智能和游戏逻辑的程序,主要运用事件交互技术,这些事件采取一定的游戏时间完成,并且事件非常依赖对象状态,这让网络化问题极度复杂化。在c/c++中书写、理解、维护和调试这样的代码是非常困难的。UnrealScript天生支持时间、状态、网络复制大大简化了游戏编程。
  • 提供Java编程风格,简单、基于对象的、编译时错误检查的特性。就象Java为Web程序员带来了清晰、简洁的编程平台一样,UnrealScript为3D游戏程序员提供了一个同样清晰、简洁、可靠的编程语言。从Java继承的主要编程概念如下:
    • 具有自动垃圾收集的无指针环境;
    • 一个简单的类单一继承机制;
    • 强壮的编译时类型检查;
    • 安全客户端执行的“沙盒”;
    • 熟悉的c/c++/java代码外观和感受;
  • 提供游戏对象丰富的高层互动,而不是底层的位和像素。在设计权衡时,由于UnrealScript工作在对象互动层,而不是底层的位和像素,因此在UnrealScript中,牺牲了性能选择了简单性和功能。而在底层和性能的关键代码是用c/c++写的,在这些地方增加性能获得的好处超过了增加复杂性得到的坏处,选择了性能牺牲了简单性和功能。

在UnrealScript的早期发展期间,我们探索过几个主要的不同的编程模式。首先,我们研究把Sun和微软的Java虚拟机的Windows版本,作为虚幻脚本语言的基础。证明了在虚幻中Java由于缺乏必要的语言特性(如运算符重载),而拥有令人沮丧的限制,同时在巨型图形对象的情况中,虚拟机任务切换和Java垃圾收集器效率过低,慢得无法接受,Java没有提供不在c/c++中编程的优点。其次,我们尝试使用VB的一个早期修改版作为UnrealScript的实现,效果良好,但可惜的是它不符合c/c++程序员的习惯。最终,基于对速度和熟悉感的期望,我们决定UnrealScript采用c++/java的一个修改版来实现,同时融入游戏特有的概念到语言的定义中。

虚幻引擎3的新内容[编辑]

熟悉UnrealScript的读者们,可以在这里找到自虚幻引擎2后的重大改变。

  • 复制-复制语法在UE3中有以下变化:
    • 复制块现在只用于变量
    • 复制函数现在由函数说明符定义(Server, Client, Reliable)
  • 堆叠状态-你现在可以把状态从堆栈中推进和弹出
  • UnrealScript预处理-支持宏和条件编译
  • 调试函数-添加了新的调试函数
  • 默认属性-defaultproperties有所改变和增强
  • 默认结构-现在结构也有默认属性了
    • 不允许再给配置或局部变量设置缺省值
    • Defaultproperties在运行变为只读,不允许做class'MyClass'.default.variable = 1 操作
  • 动态数组-动态数组新增了一个find()方法,用于按索引查询元素
  • 动态数组迭代器-现在能用foreach操作动态数组的方法
  • 函数委托-现在UE3允许委托作为函数的参数
  • 接口-添加对接口的支持
  • 访问其他类的常量:class'SomeClass'.const.SOMECONST
  • 支持多定时器
  • 函数默认参数值-现在能指定函数的可选参数了,给可选参数设定默认值即可
  • 支持工具提示(Tooltip)-当您的鼠标悬浮在属性上时,如果那个属性的UnrealScript上面存在/** 工具提示文本 */的注释声明,那么编辑器属性窗口将会显示一个包含该注释信息的工具提示。
  • 支持元数据-通过与元数据相关联的各种属性扩充游戏和编辑器的功能

程序结构示例[编辑]

这一个典型、简单的UnrealScript类的例子,它演示了UnrealScript的语法特点。请注意,此代码可能和当前的虚幻源代码不一致,因为这个文档并不参与代码同步。

//=====================================================================
// TriggerLight.
// A lightsource which can be triggered on or off.
//=====================================================================

class TriggerLight extends Light;

//---------------------------------------------------------------------
// Variables.

var() float ChangeTime; // Time light takes to change from on to off.
var() bool bInitiallyOn; // Whether it's initially on.
var() bool bDelayFullOn; // Delay then go full-on.

var ELightType InitialType; // Initial type of light.
var float InitialBrightness; // Initial brightness.
var float Alpha, Direction;
var actor Trigger;
 
//---------------------------------------------------------------------
// Engine functions.
 
// Called at start of gameplay.
function BeginPlay()
{
   // Remember initial light type and set new one.
   Disable( 'Tick' );
   InitialType = LightType;
   InitialBrightness = LightBrightness;
   if( bInitiallyOn )
   {
      Alpha = 1.0;
      Direction = 1.0;
   }
   else
   {
      LightType = LT_None;
      Alpha = 0.0;
      Direction = -1.0;
   }
}
 
// Called whenever time passes.
function Tick( float DeltaTime )
{
   LightType = InitialType;
   Alpha += Direction * DeltaTime / ChangeTime;
   if( Alpha > 1.0 )
   {
      Alpha = 1.0;
      Disable( 'Tick' );
      if( Trigger != None )
         Trigger.ResetTrigger();
   }
   else if( Alpha < 0.0 )
   {
      Alpha = 0.0;
      Disable( 'Tick' );
      LightType = LT_None;
      if( Trigger != None )
         Trigger.ResetTrigger();
   }
   if( !bDelayFullOn )
      LightBrightness = Alpha * InitialBrightness;
   else if( (Direction>0 &amp;amp;amp;&amp;amp;amp; Alpha!=1) || Alpha==0 )
      LightBrightness = 0;
   else
      LightBrightness = InitialBrightness;
}
 
//---------------------------------------------------------------------
// Public states.
 
// Trigger turns the light on.
state() TriggerTurnsOn
{
   function Trigger( actor Other, pawn EventInstigator )
   {
      Trigger = None;
      Direction = 1.0;
      Enable( 'Tick' );
   }
}
 
// Trigger turns the light off.
state() TriggerTurnsOff

{
   function Trigger( actor Other, pawn EventInstigator )
   {
      Trigger = None;
      Direction = -1.0;
      Enable( 'Tick' );
   }
}
 
// Trigger toggles the light.
state() TriggerToggle
{
   function Trigger( actor Other, pawn EventInstigator )
   {
      log("Toggle");
      Trigger = Other;
      Direction *= -1;
      Enable( 'Tick' );
   }
}
 
// Trigger controls the light.
state() TriggerControl
{
   function Trigger( actor Other, pawn EventInstigator )
   {
      Trigger = Other;
      if( bInitiallyOn ) Direction = -1.0;
      else Direction = 1.0;
      Enable( 'Tick' );
   }
   function UnTrigger( actor Other, pawn EventInstigator )
   {
      Trigger = Other;
      if( bInitiallyOn ) Direction = 1.0;
      else Direction = -1.0;
      Enable( 'Tick' );
   }
}

示例代码关键元素分析:

  • 类声明。每个类"extends"(继承自)一个父类。每个类属于一个"package"(包)。包是一个,把多个相关对象组织在一起的对象集合。所有函数和变量属于一个类,并且只能通过属于这个类的元件(actor)进行访问。没有所谓的全局函数和全局变量。详见
  • 变量声明。UnrealScript支持非常多的变量类型,包括c/java的基本类型、对象引用、结构和数组。此外,变量还可声明为让设计人员无需进行任何编程工作,就能在UnrealEd(虚幻关卡编辑器)访问的可编辑属性。这些属性使用var()语法进行声明,而不是用var。详见
  • 函数。函数有一个可选的参数列表和返回值。函数可以有局部变量。一些函数由虚幻引擎本身调用(比如BeginPlay)。一些函数由其他地方的脚本代码调用(比如Trigger)。详见
  • 代码。支持c和java的标准关键字,象for、while、break、switch、if等等。花括号和分号的用法和c、c++和java中的一样。
  • 引用元件(actor)和对象。在脚本中,你能看到一些函数,被另外一个对象通过对象引用调用的情况。详见
  • “state“(状态)关键字。这个脚本定义了一些“state“,它由函数、变量和代码构成,这些成分只在元件(actor)进入相应的状态时才被执行。详见
  • 请注意,在UnrealScript中所有关键字、变量名、函数和对象名是不区分大小写的。在UnrealScript、Demo、demON和demon是相同的东西。

虚幻虚拟机[编辑]

虚幻虚拟机由以下部分组成:服务器、客户端、渲染引擎和引擎支持代码。

虚幻服务器控制玩家和元件之间的所有互动。在单人游戏中,客户端和服务端运行在同一台机器上;网络游戏的服务器运行于一台专用机器中;所有玩家使用客户端链连接到这台机器上。

所有玩家都在关卡中进行游戏,关卡是个包含几何体和元件的独立环境。虚幻服务器有同时运行多个关卡的能力。每个关卡独立运作互相隔离:元件不能在关卡之间往来,并且一个关卡中的元件不能和另外一个关卡中的元件进行通信。

地图中的每一个元件可以被玩家(网络游戏中可以有很多玩家)和脚本控制。脚本可以定义元件如何移动以及如何和其他元件交互。游戏世界中的元件有序的运作,脚本的执行,事件的发生,等等的一切是如何在UnrealScript中实现的。你可以在下面找到答案:

在时间处理方面,虚幻将每秒的游戏逻辑划分为tick做单位的时间片。一个tick是活动对象(actor)被 update的最小时间间隔。在大部分情形下,一个tick 为百分一或者十分一秒tick的划分细度是依cpu的表现性能的。cpu越强大,计算能力越强,tick就可以划分得越细。

UnrealScript中的一些命令零tick执行(也就是说,执行它们,游戏时间没有任何流逝),而另一些则需要花费许多tick。需要花费游戏时间的函数称为“潜伏函数”。Sleep,FinishAnim,和MoveTo都是潜函数的例子。潜伏函数只能在状态代码(state code)中调用,不能从函数代码中调用(包括state函数定义)。

在活动对象的潜伏函数执行完成前,它的state不会继续执行。而它的非潜伏函数仍然可以被其他活动对象和VM调用。这样的结果是,可以在任何时间调用UnrealScript函数,即使潜伏函数还未完成。

从传统编程角度来看UnrealScript的行为,似乎关卡中的每个活动对象都执行在自己的线程中。实际上,在UnrealScript内部并没有使用Windows线程,因为那样效率过低(Window95和WindowNT无法高效的处理数以千计的线程并发)。而是采用UnrealScript模拟线程。这对UnrealScrit来说是透明得,当你用c++书写和UnrealScript进行交互的代码时显得非常明显。

所有得UnrealScript脚本是独立执行得。如果在一个关卡中有100个怪物在周围走动,所有这些怪物脚本对每一个tick来说是同时独立得执行得。

笔记:有两种时间概念:1、现实世界得时间;2、游戏世界中得时间;现实中得时间一般是用秒来衡量的,也不存在最小时间单位这个概念,因为物理上得时间是很难把握得,我们能感觉时间得流逝,确无法准确得测量它定义它(除非理论物理得到突破,建立大统一理论)。。。。,而游戏中得时间是由程序定义得,我们从现实中得时间简化出了游戏世界时间得概念,在虚幻中游戏时间得最小单位是tick(相当于现实得10-100毫秒),为了保持游戏对象得时间同步,UnrealScript使用了模拟线程,以保证在定义中得游戏世界中游戏对象时间得绝对性质(游戏对象得状态变化绝对独立,每个游戏对象得时间绝对同时流逝),这在现实中是不可能得。现实中不存在绝对时间得概念,即使通常差别不大。这并不是说几亿个cpu同时执行就能解决同时性的问题,物理本质上就不存在所谓的绝对同时,时间的绝对性是个幻觉。

对象层次结构[编辑]

[编辑]

每个脚本文件对应一个类,以class声明为开头,后面是类的父类和类的相关信息。简单的例子如下:

class MyClass extends MyParentClass;

这里声明了一个名称为“MyClass”的新类,这个类继承了“MyParentClass“的所有功能。此外类驻留在名为”MyPackage“的包中。

每个类继承父类的所有变量和函数以及状态。每个类都可以申明新的变量和新的函数(或者重写已经存在的函数)以及添加新的状态(或为已有的状态添加功能)。

在UnrealScript中设计一个新类(例如一个牛头人怪物)的典型的做法是,从具备你需要的功能的已有类(例如,Pawn类,所有怪物的基类)继承。

通过这样的做法,你永远都不用重新发明轮子--可以简单的添加新功能,你可以在自定义的同时保持不需要定制的现有功能。这种做法特别适合从虚幻中继承AI,内置的AI系统提供了大量的基本功能,同时你还可以建立自己的生物。

类声明有以下可选的说明符:

Native(包名称)(本机类)

指示:“这个类使用幕后c++支持“。虚幻native类包含在一个c++写的exe中。只有native类可以申明native函数或实现native接口。native始终来自native类。与脚本变量和特定的函数协作,native类需要创建一个自动生成的C + +头文件。默认情况下,包名是脚本类所在的exe名称。例如,引擎包中的类,要求EngineClasses.h文件。

NativeReplication

表示这个类的变量复制处理在c++中实现。只能用于native类。

DependsOn(ClassName[,ClassName,....])(依赖于)

表示ClassName是在这个类前编译。ClassName必须和这个类处于同一个包或者前一个包。可以用一个DependsOn指定多个依赖类,他们用逗号隔开。也可以使用多个DependsOn指定。

Abstract

声明类是一个“基本的抽象类”。这可以防止这个类被无意的实例化。这个关键字会对其内部子类起作用,但是不会对其他脚本文件中的子类起作用。

Deprecated(过时的)

类的所有对象被加载但不保存。当关卡设计师在关卡设计器中载入包含过时活动对象被的地图时,他们会收到编辑器的警告信息。这个关键字会对其子类产生作用。

Transient(瞬态)

表示该类的对象不应该被保存在磁盘上,这和一些非持久性的本机类自然结合是有用的。比如播放器和窗口。这个关键字会对其子类起作用;子类能使用NotTransient关键字重写它。

NotTransient(非瞬态)

表示不从基类继承Transient关键字标注的瞬态特性。

Config(IniName)(配置)

表明这个类允许访问ini文件中的数据。如果在类中有可配置变量(使用config和globalconfig申明),这可以让类访问那些保存在指定的配置文件的变量。这个标志将被传播到所有子类并且不能被否定,但是子类可以通过config关键字指定不同的配置文件。通常IniName指定ini文件的名称去存储数据,但有些名字具有特殊意义:

  • Config(Engine):用于引擎配置文件,通过你的游戏名称追加"Engine.ini"。例如,ExampleGame游戏的引擎配置文件名称是"ExampleEngine.ini"。
  • Config(Editor):用于编辑器配置文件。通过你的游戏名称追加“Editor.ini"。例如,ExampleGame游戏的编辑器配置文件名称是"ExampleEditor.ini"。
  • Config(Game): 用于游戏配置文件。通过你的游戏名称追加"Game.ini"。例如,ExampleGame游戏的配置文件名称是 ExampleGame.ini。
  • Config(Input):用于输入配置文件。通过你的游戏名称追加“Input.ini“。例如,ExampleGame游戏的输入配置文件名称是ExampleInput.ini。
  • PerObjectConfig:类的每个对象的配置信息都会被保存在配置文件中。每个对象在配置文件中都有一个如下格式的配置节[ObjectName ClassName]。这个关键字会对其子类起作用。

PerObjectLocalized

类得本地化数据将被定义在每对象得基础上。每个对象在已命名得本地化文件中有个如下格式得节[ObjectName ClassName]。这个关键字会对其子类起作用。

EditInlineNew

编辑器,表示这个类的对象能从虚幻便捷器的属性窗口创建(默认行为是可以通过关联的属性窗口引用已经存在的对象)。这个关键字会对其子类起作用。子类可以使用NotEditInlineNew关键字重写它。

NotEditInlineNew

编辑器,不从基类继承EditInlineNew关键字。如果没有任何父类使用EditInlineNew关键字将不起效果。

Placeable(可放置)

编辑器。表示这个类可以通过虚幻编辑器放到关卡,UI场景,或者kismet窗口(由类的类型定)。这个标志将被传播到其所有子类中;子类能通过NotPlaceable关键字重写它。

NotPlaceable

编辑器。不从基类继承NotPlaceable关键字。表示这个类不能在虚幻编辑器中放入关卡。

HideDropDown

编辑器。防止这个类在虚幻编辑器的属性窗口的组合框中显示。

HideCategories(Category[,Gategory,......])

编辑器。表示这个类的对象在虚幻编辑器的属性窗口中隐藏一个或多个类别。使用无类别声明可以隐藏变量,使用类名称声明变量的名称。

ShowCategories(Category[,Category,......])

编辑器。不从基类继承ShowCategories关键字。

AutoExpandCategories(Category[,Category,......])

编辑器。指示一个或多个类别在编辑器的属性窗口中自动展开。使用无类别声明可以自动展开变量,使用类名称声明变量的名称。

Collapsecategories

编辑器。表示类的属性不应该在虚幻编辑器的属性窗口类别中被分组。这个关键字会对其子类起作用;子类可以用DontCollapsecategories关键字重写它。

DontCollapsecategories

编辑器。表示不从基类继承Collapsecategories。

Within ClassName

高级。表示类的对象离不开ClassName的实例。为了建立这个类的对象,您必须指定作为外部对象ClassName的实例。这个关键字必须作为class申明的后的第一关键字。

Inherits(ClassName[,ClassName,......])

高级,使用多继承。只是适用于native类。不支持从多个UObject类继承。

Implements(ClassName[,ClassName,......])(实现)

高级。指示类实现的多个接口。只有nataive类可以实现native接口。

NoExport

高级。表明这个类的c++声明不应该通过脚本编译器包含在自动生成c++头文件中。类的c++声明手动定义在一个单独的头文件中。只适用于native类。

变量[编辑]

变量类型[编辑]

内置类型 (Built-in types)[编辑]

这里有一些在 UnrealScript 中宣告变数的例子:


var int a; // 宣告一个名称为"A"的整数变数。

var byte Table[64]; // 宣告一个长度64的静态 1-byte 阵列。

var string PlayerName; // 宣告一个名称为"PlayerName"的字串变数。

var actor Other; // 实体化 Actor 类别,并命名为"Other"。

var() float MaxTargetDist; // 宣告一个名称为"MaxTargetDist"的浮点数变数,并且它的值可以从 UnrealEd 的属性窗口中修改。


变数在 UnrealScript 能出现在两种地方:实体变数,它可以在整个类别内使用,在宣告完类别或 struct 后立即出现。局部变数,出现在函数中,只有在函数执行时有效。实体变数使用关键字 var 宣告。局部变数用关键字 local 宣告,例如:


function int Foo()

{

  local int Count;
  Count = 1;
  return Count;

}


以下是 UnrealScript 中支持的内置变数类型:


byte: 1-byte 值,范围从 0 到 255。

int: 32-bit 整数值。

bool: 布林运算值: 不是 true 就是 false。

float: 32-bit 浮点数值。

string: 一个字串(see Unreal Strings)。

constant: 不能修改的变数。

enumeration: 一个能够代表多个预先设定的整数值中的其中一个值。例如:在 Actor 内定义为 enumeration 的变数 ELightType 描述了一个动态光源,它的值可以像 LT_None、LT_Pulse、LT_Strobe,等等...。

聚合类型(Aggregate data types)[编辑]

虚幻类型[编辑]

变量说明符(specifiers)[编辑]

变量可编辑机制(Editability)[编辑]

数组[编辑]

结构[编辑]

结构说明符(specifiers)[编辑]

枚举[编辑]

常量[编辑]

对象和元件(actor)引用变量[编辑]

类引用变量[编辑]

表达式[编辑]

赋值[编辑]

类之间转换对象引用[编辑]

函数[编辑]

函数声明[编辑]

函数参数说明符(specifiers)[编辑]

函数重载(Function Overriding)[编辑]

"函数重载"是指在子类别中写一个同名的新函数。例如:你正在为一个名叫 Demon 的新怪物写一个类别。这个 Demon 类别,是继承自 Pawn 类别。现在,当怪物重生第一次看到玩家时,会呼叫父类别 Pawn 的 SeePlayer 函数,所以这个怪物会开始攻击这个玩家。这是一个很好的概念,但是当你想在你的新的 Demon 类别中以不同的方式处理 SeePlayer 函数时,你该怎么实现?答案就是函数重载。

重载一个函数,只需要从父类别中将函数的定剪下并复制贴上到你的新类别中。例如:你可以加入SeePlayer 函数到你的新类别 Demo 中。


// New Demon class version of the Touch function.

function SeePlayer( actor SeenPlayer )

{

  log( "The demon saw a player" );
  // Add new custom functionality here...

}

函数重载是创造新 UnrealScript 类别的关键。你可以创造一个继承自现有类别的新类别。然后,你需要做的事是重载一个你需要进行不同做法的函数。这可以使你在不必写大量程式码的情况下就建造一个新的类别。

在 UnrealScript 中的某些函数以 final 关键字做定义。这个 final 关键字(在关键字 function 马上出现的修饰字)意思是"这个函数无法被子类别所重载"。这可以用在一个你知道没有人会去重载的函数中,因为它会有更快的代码执行速度。例如:假如你有一个称做 VectorSize 用来计算一个向量大小的函数。可以确定的是绝对没有任何原因来让任何人重载它,所以可以在 function 后马上加入关键字 final。另一方面,像 Touch 这类的函数是很注重情境的,所以不能宣告它为 final。

函数高级说明符(specifiers)[编辑]

控制结构[编辑]

循环结构[编辑]

For循环[编辑]

Do循环[编辑]

While循环[编辑]

Continue语句[编辑]

Break语句[编辑]

分支结构[编辑]

If-Then-Else语句[编辑]

Case语句[编辑]

Goto语句[编辑]

语言功能[编辑]

内置运算符及其优先级[编辑]

一般功能[编辑]

创建对象[编辑]

整数函数[编辑]

浮点功能[编辑]

字符串函数[编辑]

矢量函数[编辑]

定时器函数[编辑]

调试函数[编辑]

UnrealScript预处理[编辑]

UnrealScript工具和实用程序[编辑]

脚本探测[编辑]

脚本调试器[编辑]

语言高级特性[编辑]

定时器[编辑]

状态[编辑]

状态概述[编辑]

状态标签和潜伏函数[编辑]

状态继承和作用域法则[编辑]

高级状态规划[编辑]

堆叠状态[编辑]

复制[编辑]

迭代语句(Foreach)[编辑]

函数调用说明符[编辑]

在可变类中访问静态函数[编辑]

变量的默认值[编辑]

通过类引用访问变量的默认值[编辑]

使用defaultproperties块指定默认值[编辑]

语法[编辑]

默认结构[编辑]

动态数组[编辑]

长度变量[编辑]

迭代动态数组[编辑]

接口类[编辑]

函数委托[编辑]

Native类[编辑]

元数据支持[编辑]

元数据概述[编辑]

使用多个元数据规范[编辑]

可用的元数据规范[编辑]

高级技术问题[编辑]

UnrealScript实施[编辑]

UnrealScript二进制兼容性问题[编辑]

技术说明[编辑]

UnrealScript编程策略[编辑]

参考资料[编辑]