档概要: UnrealScript 介绍和简要参考。最初作者是Tim Sweeney (EpicGames)
内容
介绍
快速链接
请一定要查看UnrealScript 的快速参考表和掌握UnrealScriptTOC指南 。
本文档的目的
这是一篇描述UnrealScript编程语言的技术文档。它不是指南,也没有提供有用的UnrealScript代码的详细例子。要获得UnrealScript的例子,读者可以参考引擎的源代码,它提供了成千上万行的有效代码,用来解决许多例如AI、运动、武器装备和触发事件等很多问题。最好的入门方式是查看"Actor"、"Object(物体)"、 "Controller(控制器)"、 "Pawn(士兵)" "Weapon(武器)"的脚本。
本文档假设读者使用过C/C++或 Java编程语言、熟悉面向对象程序设计、已经玩过Unreal的游戏并使用过UnrealEd的编辑环境。
对于是面向对象程序设计的新手的程序员,我强烈推荐您去Amazon.com或者书店买一本介绍java编程方面的书。Java和UnrealScript很相似,并且由于它的干净和简单的方法使它成了一种值得学习的极好的语言。
UnrealScript的设计目标
UnrealScript是为开发团队和第三方Unreal开发人员创建的,是一种强大的内置的编程语言,它自然地满足了游戏编程的需要和细小差别。
UnrealScript的主要设计目标:
- UnrealScript支持传统的编程语言没有提到的时间、状态、属性、网络等主要的概念。这大大地简化了UnrealScript代码。基于C/C++的AI和游戏逻辑编程的主要复杂性是处理要花费一定量的游戏时间来完成的事件以及处理依赖物体状态的各个方面的事件。在C/C++中,这将会导致冗长的混乱的代码,使代码难于书写、理解、维护和调试的。UnrealScript包含了对时间、状态和网络复制的内部支持,这大大地简化了游戏编程。
- 提供一种像Java类型编程语言一样简单的、面向对象的并在编译时进行错误检查的语言。就像Java为Web开发人员提供了一个干净的开发平台,UnrealScript为3D游戏提供了一个同样干净的、简单的、强大的编程语言。UnrealScript从Java语言中衍生的主要编程观念有:
- 没有指针并自动进行垃圾回收的环境;
- 一个简单的单继承类图;
- 编译时进行强类型检查;
- 安全的客户端执行的"sandbox"
- 像C/C++/Java代码一样熟悉的外观和感觉。
- UnrealScript为了在游戏对象和交互方面而不是位和像素方面提供丰富的高层次的编程语言,所以在设计上必须有一些妥协,我们为了获得开发的简单性和强大性,从而牺牲了执行速度。毕竟, Unreal的底层的、对性能起重要作用的代码是由C/C++书写的,在那里所获得的性能提高的价值超出了所增加的复杂性。UnrealScript是在那个层次的基础上进行运作的,在物体和交互性的层次上而不是位和像素的底层上。-
在UnrealScript开发的早期,我们探索了几个主要的不同的编程语言范例,然后又放弃了,最终开发出了目前的UnrealScript。首先,我们研究使用Sun 和 Microsoft为Windows设计的JavaVM(虚拟机) 作为Unreal脚本语言的基础。但最终证明在Unreal的情境中Java并没有提供比C/C++更大的编程好处,反而由于缺乏所需要的语言功能(比如 运算符重载)而增加了一些令人沮丧的限制,另外也证明由于VM过多的任务转换开销和Java垃圾回收器在处理大的对象图形时的低效率导致运行速度特别的慢。其次,我们设计了基于Visual Basic变量的UnrealScript初期实现,它可以工作的很好,但是对于已经习惯于C/C++的程序员来说,它不是那么友好。最终基于我们想将游戏的特定概念映射到语言定义本身的愿望以及获得速度和熟悉度的需要,我们决定将UnrealScript设置为基于C++/Java变种的语言。结果证明这是一个好主意,因为它在很多方面大大地简化了Unreal代码。
虚幻引擎3中UnrealScript的新功能
对于已经熟悉UnrealScript的人来说,这里是一个自虚幻引擎2后在UnrealScript中所发生的主要改变的简短概要。
- Replication(复制)-在UE3中replication(复制)语句已经改变:
- 现在复制(replication)语句块只适用于变量。
- 函数复制(replication)现在通过以下方法定义:function specifiers (Server, Client, Reliable )
- 状态栈 - 你现在可以使状态压入栈或弹出栈。
- UnrealScript处理器 - 支持宏和条件编译。
- 调试功能 - 添加了新的和调试相关的功能。
- 默认属性 - defaultproperties块的处理已经被改变/改进。
- 默认值 - structs现在也可以有默认属性。
- 不再允许给配置或者局部变量设定默认值。
- 在运行时系统中是随机的,它不再允许执行
class‘MyClass‘.default.variable = 1
的操作。
- 动态数组 - 动态数组现在有一个新功能 find() ,用于查找一个元素的索引。
- 动态数组迭代器 - Foreach操作符现在可以应用于动态数组。
- Delegate 函数参数 - UE3现在允许 delegates作为函数的传递参数。
- 接口 - 添加了对接口的支持
- 从其它的类中访问常量:
class‘SomeClass‘.const.SOMECONST
- 支持多个定时器
- 默认的函数参数值 - 现在可以指定操作函数参数的默认值。
- 支持工具提示- 如果在UnrealScript中属性声明的上面有一个=/** tooltip text */=形式的注释,则在编辑器的属性窗口中,当您将鼠标放到一个属性上时可以显示工具提示文本。
- 支持元数据 - 通过将属性和各种类型的元数据相关联,扩展了in-game(游戏中)和in-editor(编辑器中)的功能。
代码结构示例
这个示例说明了一个典型的简单的UnrealScript类,并且它囊括了UnrealScript的语法和功能。注意由于该文档没有和代码同步,所以这个代码可能和现在的Unreal源代码的呈现不一样。
//===================================================================== // TriggerLight. // 一个可以通过触发进行开关的光源 //===================================================================== class TriggerLight extends Light; //--------------------------------------------------------------------- // 变量 var() float ChangeTime; //光源从开到关所需要的时间 var() bool bInitiallyOn; //它在最初是否是亮的 var() bool bDelayFullOn; // 在到完全的点亮之前是否有延迟 var ELightType InitialType; // 光源的初始类型 var float InitialBrightness; //光源的初始亮度 var float Alpha, Direction; var actor Trigger; //--------------------------------------------------------------------- //引擎函数 //在游戏开始播放时调用 function BeginPlay() { // 记忆光源的初始类型并为它设置一个新的类型 Disable( ‘Tick‘ ); InitialType = LightType; InitialBrightness = LightBrightness; if( bInitiallyOn ) { Alpha = 1.0; Direction = 1.0; } else { LightType = LT_None; Alpha = 0.0; Direction = -1.0; } } // 当时间经过时调用 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; Alpha!=1) || Alpha==0 ) LightBrightness = 0; else LightBrightness = InitialBrightness; } //--------------------------------------------------------------------- //公有状态 //触发打开光源 state() TriggerTurnsOn { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = 1.0; Enable( ‘Tick‘ ); } } //触发关闭光源 state() TriggerTurnsOff { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = -1.0; Enable( ‘Tick‘ ); } } //触发切换光源的开关 state() TriggerToggle { function Trigger( actor Other, pawn EventInstigator ) { log("Toggle"); Trigger = Other; Direction *= -1; Enable( ‘Tick‘ ); } } // 触发控制光源 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(物体)可以访问。没有整个系统范围可以使用的全局函数或变量。More Details更多详情
- 变量声明。UnrealScript支持多种变量类型,包括大多数基于C/Java的类型、对象引用、structs (结构体)和数组。另外,变量可以被设置成为可以编辑的属性,这样设计人员在UnrealEd中即使不用任何编码就可以访问这些变量了。这些属性通过使用
var()
语法来指定,而不是var
。More Details更多详情 - 函数。函数可以有一系列的参数,并且可以随意地返回一个值,也可以有局部变量。某些函数是由虚幻引擎本身调用的(例如 BeginPlay),有些是由其它的脚本代码进行调用的。(比如Trigger)。
- 代码。支持所有的标准的C和Java关键字,比如
for=、 =while=、 =break=、 =switch=、 =if
等等。
- Actor和object(对象)引用。这里你看到了几个在另一个对象中使用对象引用调用一个函数的情况。
- 关键词"state"。这个脚本定义了几个"states",它们由函数、变量和仅当这个物体在那个状态时所执行的代码组成。
- 注意:在UnrealScript中,所有的关键词、变量名、函数和对象名都是大小写不敏感的。对于UnrealScript来说,=Demon=,
demON
, 和demon
是同样的事情。
Unreal虚拟机
Unreal虚拟机有以下几部分组成:服务器、客户端、渲染引擎及引擎支持代码。
Unreal控制着所有的玩家和物体间的游戏性和交互。在一个单人游戏中,Unreal客户端和Unreal服务器在同一台机器上运行;在一个网络游戏中,有一个机器用于专用服务器;所有连接到这个机器上的玩家是客户端。
所有的游戏播放都发生在一个"关卡"中,它是一个包含着几何体和物体的独立环境。尽管UnrealServer可以同时运行多个关卡,每个关卡独立运作并且彼此屏蔽:物体(actors)不能在不同关卡间穿行,而且一个关卡中的物体不能和另一个关卡中的物体进行交流。
在地图中的每个物体都可以或者在玩家的控制下(在网络游戏中可以有很多玩家)或者在脚本的控制下。当一个物体在脚本的控制下时,那么该脚本完全地定义了物体如何移动及如何与其它物体进行交互。
对于世界中所有这些物体的跑动、执行脚本、事件发生,你也许会问它们是如何能理解UnrealScript的执行流程哪。答案如下所示:
为了管理时间,Unreal将游戏播放的每秒钟分隔为"Ticks"。一个Tick是关卡中所有物体更新所使用的最小时间单位。一个tick一般是一秒钟的1/100到 1/10。Tick时间仅受到CPU功率的限制,机器速度越快,tick持续时间越短。
在UnrealScript中的某些命令的执行只需要使用零tick(也就是:它们的执行没有占有任何游戏时间),也有些命令需要占用很多ticks。需要占用游戏时间的函数称为"latent functions(潜伏的函数)"。一些latent functions函数的例子包括 Sleep , FinishAnim , and MoveTo 。UnrealScript中的Latent functions仅可以从在一个状态的代码中被调用(所以也称作"state code(状态代码)"),而不能从一个函数的代码中(包括在一个状态中定义的函数)被调用。
当一个actor在执行一个latent函数,那个actor的状态执行不会继续直到latent函数执行完毕。然而,其它的actor或者VM可能会调用actor内部的函数。最终的结果是所有的UnrealScript的函数可以在任何时间被调用,甚至在latent 函数没有执行完毕的情况下。
按照传统的编程术语来说,UnrealScript就像在关卡中的每个物体有它们自己的执行"thread(线程)"一样工作。在内部,Unreal不使用Windows线程,因为那将是非常低效的(Windows 95和Windows NT不能高效地处理同时发生的成千上万的线程)。然而,UnrealScript 可以模拟线程。虽然这个事实对于UnrealScript代码是透明的,但当您书写和UnrealScript交互的C++代码时则变得非常显然的。
所有的UnrealScripts将彼此独立地执行,如果有100个鬼怪正在关卡中走动,那么所有的这100个鬼怪的脚本在每个"Tick"中都正在同时地且独立地执行着。
对象层次
在使用UnrealScript进行工作之前,理解Unreal中物体的高层次的关系是非常重要的。Unreal的架构和其它大多数的游戏架构是有较大的背离的:Unreal是完全地面向对象的(非常像 COM/ActiveX),它有一个定义明确的对象模型,可以支持高层次的面向对象的概念比如对象图、序列化、对象生命周期和多态性。从游戏历史上来说,尽管很多游戏比如Doom和Quake,已经证明在内容方面有较大的可扩展性,但大多数游戏都被设计成为集成化的,它们的主要功能都是被硬编码的并且是在对象的层次上不能扩展的。Unreal的面向对象的形式有一个很大的好处:主要的新功能和对象类型可以在运行时添加到Unreal中,并且这个扩展是以划分子类的方式进行的,而不是(比如)通过修改一串已有的代码。这种扩展的形式是非常强大的,因为它鼓励Unreal团队共同来创建可以所有人都可以操作的改善。
Object是Unreal中所有对象的父类。在Object类中的所有函数可以在任何地方访问,因为任何东西都继承于Object。Object是一个抽象的基类,它没有做任何有用事情。所有的功能都是由子类提供,比如Texture (一个贴图),TextBuffer(文字块)和Class (它描述了其它对象的类)。
Actor(继承Object)是Unreal中所有独立运行的游戏对象的父类。Actor包含了所有的一个物体所需要的用于四处运动、和其它物体进行交互、影响环境及做其它与游戏相关的有用的事情的功能。
Pawn(继承Actor)是Unreal中所有的可以使用高层次AI和玩家控制的创造物和玩家的父类。
Class (继承 Object)是一种描述对象的类的特殊类型的object。刚看到时可能是令人疑惑的:一个类是一个对象,而其一个类描述某些的对象。但是这概念是合理的,并且在很多情况下您将会用到Class的对象。例如,当你在UnrealScript产生一个新的actor,你可能会通过Class对象来指定actor的类。
使用UnrealScript,你可以为任何Object类书写代码,但是99%的时候您都是在为从Actor继承的类来写代码。大多数有用的UnrealScript功能是游戏相关的并且是处理actors的。
类
每个脚本明确地对应一个类,并且脚本是通过声明类、类的父类及任何和该类相关的其它附加信息开始的。最简单的形式是:
class MyClass extends MyParentClass;
这里我声明了一个名为"MyClass"的新类,它继承了"MyParentClass"类的功能。另外,这个类存储在名为"MyPackage"的包中。
每个类继承它父类的所有的变量、函数和状态。然后它可以添加新的变量声明、添加新的函数(或者重写已有函数)、添加新的状态(或者为已有的状态添加功能)。
在UnrealScript中典型的设计类的方法是构造一个继承了具有您所需要的大部分功能的现有类(比如Pawn类,即所有怪物的基类)的新类(比如一个”人身牛头怪物”)。使用这个方法,你从来不需要重新发明新的东西---你可以简单地增加您想自定义的新功能,然而保持所有你不需要进行自定义的现有的功能。这个方法在Unreal中实现AI时显得尤其有用,因为在内置的AI系统中提供了大量的基础功能,您可以把它们作为您自定义的创造物的组成部分。
类的声明可以带有几个可选的影响类的修饰符:
- Native(包名称)
- 意味着“这个类使用C++作为幕后支持”。Unreal期望native类在.EXE文件中包含着一个C++实现。只有native类可以声明native函数或者实现native接口。Native类必须继承于另一个native类。Native类创建一个自动生成的具有必要的 _ glue_ 的C++头文件来和脚本变量及指定函数进行交互。
- NativeReplication
- 意味着对于该类的变量值的复制是在C++实现中处理的。仅对native类有效。
- DependsOn(ClassName[,ClassName,...])
- 意味着ClassName是在这个类之前进行编译。ClassName必须在同一个(或前一个)包中指定一个类。多个依赖的类可以通过使用一个单独的
DependsOn
代码行并且类之间通过逗号分界来指定,或者通过为每个类使用单独的DependsOn
来指定。
- Abstract
- 声明类为一个"abstract base class(抽象的基类)"。这可以防止用户在UnrealEd的世界中添加这个类中的actor或者在游戏中创建这个类的实例,因为这个类本身没有任何意义。例如,"Actor(物体)"基类是抽象的,然而它的子类"Ladder(梯子)"不是抽象的—那么你可以在世界中放置一个Ladder,但你不可以在世界中放置一个Actor。这个关键词可以传播到内在的子类,但是不能传播到脚本的子类。
- Deprecated
- 导致该类的所有对象可以被加载但不能被保存。当关卡设计者在编辑器中加载地图时,地图中任何已放置的deprecated actors的实例将会给他们产生一个警告。
- Transient
- 说明”属于这个类的对象将永远不会被保存到磁盘上” 。 仅在和某些类型的netive类结合时才有作用,这些netive类天生不需要持久存在的比如玩家或者窗口。这个关键字可以传播到子类,子类可以使用
NotTransient
关键字覆盖这个标志。 - NonTransient
- 取消从基类继承的
Transient
关键字。 - Config(IniName)
- 意味着允许这个类在.ini文件中存储数据。如果在类(以"config"或 "globalconfig"声明)中有任何可配置的变量,则可以把这些变量被保存到指定的配置文件中。这个标志可以传播到所有的子类且不能取消传播,但子类可以通过重新声明
Config
关键字 并指定一个不同的IniName来改变.ini文件。正常情况下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
- 为每个对象存储这个类的配置信息,每个对象在.ini文件中有一个在该对象后以[ObjectName ClassName]的形式命名的部分。这个关键字可以传播到子类。
- PerObjectLocalized
- 以每个对象为基础为这个类定义本地化数据,每个对象在本地化文件中都有一个在该对象后以[ObjectName ClassName]形式命名的部分。这个关键字可以传播到子类。
- EditInlineNew
- 编辑器。意味着这个类的对象可以从UnrealEd的属性窗口中创建(默认行为是仅已存在的对象的引用可以通过属性窗口进行分配)。这标志可以传播到所有子类,子类可以使用
NotEditInlineNew
关键字覆盖这个标志。 - NotEditInlineNew
- 编辑器。取消从基类继承
EditInlineNew
关键字。如果父类没有使用EditInlineNew
关键字,则该关键字没有影响。 - Placeable
- 编辑器。意味着这个类可以在UnrealEd中创建并可以被放置到关卡、UI scene或者kismet窗口中(依赖于类的类型)。这个标志可以传播到所有子类,子类可以通过使用
NotPlaceable
关键字覆盖这个标志。 - NotPlaceable
- 编辑器。取消从基类继承
Placeable
关键字,意味着这个类不能放置UnrealEd中的关卡中等。 - HideDropDown
- 编辑器。阻止这个类在UnrealEd的属性窗口的复选框中显示。
- HideCategories(Category[,Category,...])
- 编辑器。为这个类的对象指定一个或多个应该在UnrealEd属性窗口中隐藏的类别。为了隐藏声明时的没有类别的变量,可以使用声明变量时所使用的类的名称。
- ShowCategories(Category[,Category,...])
- 编辑器。取消从基类继承
HideCategories
关键字。 - AutoExpandCategories(Category[,Category,...])
- 编辑器。为这个类的对象指定一个或多个应该在UnrealEd属性窗口中自动展开的类别。为了自动展开声明时的没有类别的变量,可以使用声明变量时所使用的类的名称。
- Collapsecategories
- 编辑器。意味着这个类的属性不应该在UnrealEd的属性窗口中以类别分组。这个关键字可以传播到子类,子类可以使用
DontCollapseCategories
关键字来覆盖这个标志。 - DontCollapseCategories
- 编辑器。 取消从基类继承
CollapseCatogories
关键字。 - Within ClassName
- 高级。 意味着这个类的对象在没有ClassName的实例的情况下不能存在。为了创建这个类的对象,你必须指定一个ClassName的实例作为
Outer
对象。这个关键字必须紧跟在类的声明本身后面。Inherits(ClassName[,ClassName,...]):高级。用于多重继承-指定额外的基类。多个被继承的基类可以通过使用一个Inherits
并在多个基类之间用逗号分开来指定,或者通过为每个基类分别使用一个=Inherits= 来指定。仅对native类有效。不支持来自两个UObject-derived类的多重继承。 - Implements(ClassName[,ClassName,...])
- 高级。指定一个或多个这个类要实现的接口类。多个接口可以通过使用一个单独的=Implements=且多个接口使用逗号分隔来指定,或者通过为每个接口类使用一个=Implements=来指定。
- NoExport
- 高级。意味着这个类的C++声明不应该被脚本编译器包含到自动生成的C++头文件中。该C++类的声明必须在一个单独的头文件中手动地定义。
变量
变量类型
内置类型
这里是在UnrealScript中的一些实例变量声明的例子:
var int a; //声明了一个整型变量, 名称为"A"的 var byte Table[64]; //声明了一个64个字节的静态数组,名为"Table"的。 var string PlayerName; // 声明了一个字符串变量,名称为"PlayerName"。 var actor Other; //声明了一个可以被赋值为一个Actor实例的引用的变量。 var() float MaxTargetDist; // 声明了名称为"MaxTargetDist"的一个浮点变量,并且它的值可以从UnrealEd的属性窗口中进行修改。
变量在UnrealScript能出现在两种地方:实例变量,它应用于整个对象,在类声明后或者在struct声明内立即出现。局部变量,出现在函数中,仅在函数执行时有效。实例变量使用关键字 var
进行定义。局部变量使用关键字 local
进行定义,比如:
function int Foo() { local int Count; Count = 1; return Count; }
这里是UnrealScript中支持的内置的变量类型:
- byte(字节型): 一个字节值的范围是
0
到255
- int(整型): 32位的整型值
- bool(布尔型): 布尔值:=真= 或
假
- float(浮点型): 32位的浮点数。
- string(字符串型): 一个字符串(请查看Unreal 字符串)
- constant(常量): 不能修改的变量。
- enumeration(枚举型): 一个能够代表多个预先设定的整数值中其中一个值的变量。比如,在Actor脚本中定义的枚举类型ELightType描述了一个动态光源,它可以具有像
LT_None=、 =LT_Pulse=、 =LT_Strobe
等值。
集合数据类型
array<Type>:一个 Type
类型的数组。
- struct
- 和C中的结构体类似,UnrealScript的structs可以让您创建包含子变量的新的变量类型。比如,两个常用的Unreal structs是由X坐标、Y坐标及Z坐标组成的
vector(向量)
;及由倾斜度、偏转度、旋转度组成的rotator
。
Unreal类型
- Name
- 在Unreal中一个术语的名称(比如函数名、状态名、类名等)。Names(名称)会作为一个索引存储到全局名称表中。Names(名称)和简单的字符串相对应,最多可以包含64个字符。Names(名称)不像字符串那样一旦创建不能改变(请查看Unreal 字符串获得更多的信息)。
- Object and Actor references
- 一个指向世界中另一个object 或 actor的变量。比如,Pawn类有一个 "Enemy" 物体引用,它指定了玩家应该攻击的物体。Object 及actor引用是非常强大的工具,因为它们使您可以访问另一个actor的变量及函数。比如,在Pawn的脚本中,你可以用 Enemy.Damage(123) 来调用您的敌人的Damage函数—从而导致敌人受到伤害。Object 引用也可能包含着一个特殊的叫
None
的值,它和C 语言中的NULL
指针是等同的:它的意思是这个变量不能指向任何物体。 - Delegate
- 保存着到一个Unrealscript函数的引用。
变量修饰符
变量也可以具有其它的额外的修饰符来对其进一步的描述,比如 const
。事实上,将会有很多你在一般的编程语言中没有见过的修饰符,这主要是因为我们想使UnrealScript支持许多游戏-和特定环境中-的专用概念:
- config
- 这个变量是可以配置的。当前的值将被保存到一个ini文件中并在创建时加载。这个变量不能在默认属性中赋值。
- globalconfig
- 和config的使用基本相同,除了不可以在子类中覆盖它所定义的变量并且它不能在默认属性中进行赋值。意味着它是常量。[这里的理解可能有歧义,解释下:config的数据可以在派生类中重写,globalconfig不可以,mark by dongbo]。
- localized
- 这个变量的值将会定义一个局部变量,主要用于字符串。意味着是常量。在Localization Reference(局部化参考) 及 Unreal 字符串获得关于此的更多信息。
- const
- 把变量的内容当成一个常量。在UnrealScript,你可以读取常量的值,但是你不能对其写入值。"Const"仅用于引擎来负责更新的变量及不能从UnrealScript中安全地进行更新的变量,比如一个物体的位置(仅能通过调用MoveActor函数来进行设置)。
- private
- 这个变量是私有的,仅能通过类的脚本来进行访问;其它的类(包括子类)不能访问该变量。
- protected
- 这个变量仅能由此变量的类及其子类进行访问,其它类不能访问。
- repnotify
- 当这个属性通过拷贝被赋值时,Actors应该被通知(通过 ReplicatedEvent 函数)。
- deprecated
- 意味着这个变量在不久的将来将会被删除,并且将不能在编辑器中访问。Deprecated属性可以被加载,但不能被保存。
- instanced
- 仅对对象属性。当创建了一个这个类的实例,将会默认地为这个变量赋予一个这个对象的唯一拷贝。用于实例化在类的默认属性中定义的子对象。
- databinding
- 这个属性可以通过数据仓库系统进行操作。
- editoronly
- 属性值仅在运行UnrealEd或者一个命令开关是被加载。在游戏中,这个属性值将被丢弃。
- notforconsole
- 这个属性值将仅在运行PC时加载。在游戏控制台中,该属性值被丢弃。
- editconst
- 编辑器。这个变量可以在UnrealEd中被看见,但不能被编辑。一个editconst变量并 不 暗示着它是"const"变量。
- editfixedsize
- 编辑器。仅用于动态数组。这将防止用户通过UnrealEd属性窗口来改变数组的长度。
- editinline
- 编辑器。允许用户编辑在UnrealEd属性窗口中编辑这个变量所引用的物体属性。(仅用于对象引用,包括数组对象的引用)。
- editinlineuse
- 编辑器。除了执行 editinline 的相关行为外,在编辑器中这个对象引用的旁边添加一个"Use"按钮。
- noclear
- 编辑器。允许从编辑器中把这个对象引用设为None。
- interp
- 编辑器。意味着这个值可以受到Matinee中的浮点或向量属性轨迹的驱动从而随时间改变。
- input
- 高级。使变量在Unreal的输入系统中是可以访问的,从而使输入(比如按钮的押下和操作杆的运动)能够和变量直接地进行映射。
- transient
- 高级。说明这个变量是临时使用的,且不是对象永久状态的一部分。 Transient变量不会被保存到磁盘。当加载一个对象时,Transient变量被初始化为类的默认值。
- duplicatetransient
- 高级。意味着当创建对象的一个二进制副本时,变量的值将被重置为类的默认值(通过StaticDuplicateObject).
- noimport
- 高级。意味着当导入T3D文本时,将跳过这个变量。换句话说,当导入或者复制/粘帖对象时,这个变量的值将不会被传递到新的对象实例中。
- native
- 高级。声明了这个变量通过C++代码进行加载和保存,而不是通过UnrealScript。
- export
- 高级。仅对对象属性(或对象数组)有用,意味着当一个对象被复制(复制/粘帖)或导出到T3D中而不是仅仅输出对象引用本身时,赋予给这个属性的对象应该作为一个子对象块整体地导出。
- noexport
- 高级。仅用于native类。这个变量不能包含在自动生成的类声明中。
- nontransactional
- 高级。意味着这个变量值的改变将不会被包含到编辑器的取消/重做历史中。
- pointer{type}
- 高级。这个变量是一个指向 type(数据类型) 的指针( type(数据类型) 是随意的)。注意语法是:pointer varname(指针变量名){type(数据类型)}
- init
- 高级。这个属性将被以一个FString或TArray而不是一个FStringNoInit or TArrayNoInit导出到头文件中。仅适用于在native类中声明的字符串和动态数组。‘Init‘属性不能被赋予默认值,因为当创建对象时,默认值将会被清除。(请查看 Unreal 字符串 和 Native 字符串)
out:这个修饰符仅对函数参数有效。请查看函数来获取详细信息。 coerce:这个修饰符仅对函数参数有效。请查看函数来获取详细信息。 optional:这个修饰符仅对函数参数有效。请查看函数来获取详细信息。
- skip
- 这个修饰符仅对运算符函数参数有效。仅用于逻辑运算符比如&& 和 ||。如果一个表达式的输出已经确定则防止赋值。比如:FALSE && ++b==10 (更多详情).
可编辑性
在UnrealScript中,你可以使一个实例变量变得可编辑,以便用户可以在UnrealEd中编辑变量的值。这个机制负责了UnrealEd的"Actor Properties"对话框的全部内容,您在那里所看到的全部内容仅是UnrealScript中的一个声明为可以编辑的变量。
声明一个可以编辑的变量的语法如下所示:
var() int MyInteger; //在默认目录中声明了一个可以编辑的整型 var(MyCategory) bool MyBool; //中声明一个可以编辑的整型"MyCategory"
Y您也可以声明一个变量为 editconst
,这意味着这个变量将可以在UnrealEd中看见但 不 可以编辑。注意这仅仅是为了防止变量在编辑器中被改变而不是在脚本中。如果您想使一个变量为真正的 const
但是仍然可以在编辑器中可见,您必须声明它为 const editconst
:
// MyBool在UnrealEd中是可以看见的但却不可编辑 var(MyCategory) editconst bool MyBool; // MyBool变量在UnrealEd中是可见的但不能编辑而且不能通过脚本来改变它的值 var(MyCategory) const editconst bool MyBool; // MyBool变量在UnrealEd中是可见的且可以设置,但不能在脚本中改变他的值 var(MyCategory) const bool MyBool;
数组
数组使用以下语法声明:
var int MyArray[20]; //声明了一个长度为20的整形数组
UnrealScript仅支持一维数组,但你可以通过自己来设置行/类的元素从而来模拟多维数组。要获得关于动态数组的信息,请查看以下部分Advanced Language Features高级的语言功能
Structs
UnrealScript struct是把一串变量一起塞入到一个新类型称为struct的超变量的一种方法。UnrealScript structs和C语言中的结构体很像,它可以包含变量、数组及其它的结构体,但UnrealScript structs中不可以包含函数。
你可以按照以下方法来声明一个struct:
//在3D空间的一个点或方向向量 struct Vector { var float X; var float Y; var float Z; };
一旦您定义了一个struct,你便可以开始定义那个struct类型的特定变量了:
//声明了一组Vector类型的变量 var Vector Position; var Vector Destination;
要想访问struct中的分量 ,可以按照以下方式使用代码:
function MyFunction() { Local Vector A, B, C; //向量相加 C = A + B; //仅向量的X成员相加 C.X = A.X + B.X; // 将向量C传递到一个函数中 SomeFunction( C ); //将向量的某个成员传递到函数中 OtherFunction( A.X, C.Z ); }
你可以像操作其它变量一样来操作Struct变量:你可以赋值变量给它们、也可以传递它们给函数、而且您也可以访问它们的成员。
在Object类中定义了几个Structs,这些在整个的Unreal将一直会使用。你必须熟悉它们的操作,因为它们是脚本的基础构建模块。
- Vector
- 在空间中的一个唯一的三维点或者向量,具有X、 Y、Z分量。
- Plane
- 在三维空间内定义了一个的唯一的平面。平面是通过它的X、Y、Z分量(假设它们都是归一化的)加上它的W分量来定义的,W代表了沿着平面的法线平面到原点的距离 (它是从平面到原点的最短的线)。
- Rotator
- 一个rotation(旋转定)义了一个唯一的正交坐标系统。一个rotator包括Pitch(倾斜度)、Yaw(偏转度)及Roll(旋转度)分量。
- Coords
- 在三维空间中的任意一个坐标系统。
- Color
- 一个RGB颜色值。
- Region
- 定义了关卡中的一个凸起区域。
修饰符
Structs也有少许修饰符来影响struct的所有实例。
- atomic
- 意味着这个struct要一直作为一个单独的单元进行序列化;如果在struct中的任何属性和它的默认值不同,那么struct中的所有元素都将被序列化。
- atomicwhencooked
- applies the ‘atomic‘ flag only when working with cooked package data.仅在使用已烘焙的包的数据时才应用‘atomic‘标志。
- immutable
- 意味着这个结构体使用二进制序列化(减少磁盘空间占用并提高序列化性能);在没有增加包的版本的情况下从这个struct中增加/移除成员是不安全的。 immutablewhencooked:仅当使用已烘焙的包数据进行工作时才能应用‘immutable‘标志。
- strictconfig
- 意味着当一个struct属性有‘config/globalconfig‘修饰符时,仅在这个结构体中标记为config/globalconfig的属性才能被从.ini文件中读取。(如果没有此标志,在struct中的所有属性都是可配置的)
枚举
在UnrealScript中枚举类型的存在,可以方便地声明能够包含一串关键字中的任意一个值的变量。比如,actor类包含着枚举变量=EPhysics=,它描述了Unreal要应用到actor上的物理。这个物理可以设置为已经预先定义的值中的一个,比如 PHYS_None=、 =PHYS_Walking=、 =PHYS_Falling
等。
在内部,枚举变量是作为字符变量存储的。在设计UnrealScript时,并没有认为枚举类型是必需的,但是当看到一个actor得物理模型被设置为 PHYS_Swimming
而不是(比如) =3=,发现它使代码变得如此的易读。
这里是定义枚举类型的示例代码。
//定义了一个具有三个值的枚举类型EColor。 enum EColor { CO_Red, CO_Green, CO_Blue }; //现在定义EColor类型的两个变量 var EColor ShirtColor, HatColor; //可替换地,你可以像这样一起声明变量和枚举类型: var enum EFruit { FRUIT_Apple, FRUIT_Orange, FRUIT_Bannana } FirstFruit, SecondFruit;
在Unreal代码中,我们经常要声明枚举值,像 LT_Steady=、 =PHYS_Falling
等,而不是简单地声明为"Steady" 或 "Falling"。这只是编码风格的问题而不是语言的要求。
UnrealScript仅在枚举类型定义的类及其子类中可以识别没有明确枚举变量名的枚举标签(比如 FRUIT_Apple
)。如果你需要引用一个在类的层次的其它地方定义的枚举标签,你必须使它符合要求:
FRUIT_Apple // 如果Unreal不能找到这个枚举标签… EFruit.FRUIT_Apple //那么像这样来使它符合要求
常量
在UnrealScript,你几乎可以为所有的数据类型来指定常量值:
- 整型和字节型常量可以使用简单的数字来指定,比如
123
. 如果你必须以16进制的方式来指定一个整型或字节型常量,使用:0x123
- 使用十进制指定浮点型常量:
456.789
- 字符串常量必须加上双引号,比如:="MyString"=
- Name常量必须加上单引号,比如:
‘MyName‘
- Vector常量要像这样来了包含X, Y, 和 Z的值:
vect(1.0,2.0,4.0)
- Rotator常量要像这样来包含Pitch, Yaw, 和 Roll的值:
Rot(0x8000,0x4000,0)
- =None=常量指向"no object(没有对象)"(或者等同地"no actor")
- =Self=常量指向"this object"(或等同地,"this actor"),也就是正在执行的脚本所属的对象。
- 一般对象的常量通过用对象的类型并跟随带单引号的对象的名称来指定,比如:
texture‘Default‘
EnumCount=指定了一个枚举类型的元素个数,比如: =ELightType.EnumCount
ArrayCount=指定了一个静态数组中的元素的个数,比如:=ArrayCount(Touching)
你可以使用"const"关键字来声明一个常量,以后便可以通过常量的名称来引用了。比如:
const LargeNumber=123456; const PI=3.14159; const MyName="Tim"; const Northeast=Vect(1.0,1.0,0.0);
常量可以在类或structs中定义。
如果要访问在另一个类中定义的常量,请使用"class‘classname‘.const.constname"语法,比如:
class‘Pawn‘.const.LargeNumber
Object和actor引用变量
你可以声明一个指向一个actor或object的变量,像这样:
var actor A; //一个actor引用 var pawn P; //一个到Pawn类中的一个actor的引用 var texture T; //一个到贴图对像的引用
上面的变量"P"是一个到Pawn类的一个actor的引用。这个变量可以指向属于Pawn的子类的任何actor。比如,P可以指向一个Brute,或者一个Skaarj或Manta。它可以是任何类型的Pawn。然而,P永远不能指向一个Trigger actor(因为Trigger不是Pawn的子类)。
使变量指向一个actor从而获得便利的例子是在Pawn类中的Enemy变量,它指向Pawn正在试图攻击的actor。
当您有一个指向actor的变量时,你可以访问那个actor的变量并且调用它的函数。例如:
//声明指向一个pawn的两个变量 var pawn P, Q; //这里是一个使用P对象的函数,它显示了关于P对象的信息。 function MyFunction() { // 设置P的敌人为Q P.Enemy = Q; //告诉P播放这个跑动的动画 P.PlayRunning(); }
指向actors的变量通常或者指向一个有效的actor(任何在关卡中实际存在的actor)或者指向 None
值。None等同于C/C++中的 NULL
指针。然而,在UnrealScript中访问具有 None
引用的变量和函数是安全的;结果通常为0。
注意一个object 或 actor引用是“指向”另一个actor 或 object,它并不“包含”一个真正的actor 或 object对象。在C语言中,一个actor引用等同于指向一个AActor类的对象的指针(在C语言中,你会称它为AActor*)。例如,你可以在世界中有两个鬼怪(monsters)Bob和Fred,它们相互打斗。Bob 的"Enemy"变量将“指向”Fred,并且Fred的"Enemy"变量将“指向”Bob。
和C语言中的指针不同,UnrealScript的对象引用总是安全的可靠的。一个对象引用指向一个不存在的对象或者无效的对象是不可能的(除了特殊情况: None
值)。在UnrealScript中,当一个actor被销毁,那么它的所有引用将被设置为 None
。
类引用变量
在Unreal中,类就像actors、textures(贴图)及 sounds(声音)一样是对象。Class 对象属于名称为"class"的类。在很多情况下,您将需要存储一个到类对象的引用,以便您可以产生一个属于那个类的actor(在编译时不知道那个类是什么)。比如:
var() class C; var actor A; A = Spawn( C ); // 产生一个属于某个任意类C的actor.
现在,一定不要搞乱类C和属于类C的对象O(作为类C的一个“实例”)的角色作用。打一个不太靠谱的比喻,一个类就像一个胡椒粉的研磨工,而那个类的实例就像是胡椒粉。你可以使用胡椒粉的研磨工(类)来转动磨制机器的曲柄(调用Spawn函数)来创建胡椒粉(类的对象)…但是,一个胡椒粉研磨工(一个类)不是胡椒粉(属于这个类的对象),所以你一定不要尝试去吃它!
当定义一个引用这个类对象的变量时,你可以有选择性地使用语法 class<metaclass> 来限制这个类不能被metaclass 类型(及它的子类)的变量所引用。比如在以下声明中:
var class<actor> ActorClass;
变量ActorClass仅能引用继承类"actor"的类。这对改善编译时的类型检查是有帮助的。比如,Spawn函数采用一个类作为参数,但是仅当这个类是Actor的子类时才有效, class<classlimitor>语法使编译器来执行那个要求。
关于使用动态对象类型转换,你可以这样来动态地转换类的类型:
// 转换SomeFunctionCall()的结果作为Actor类型的一个类(或Actor类的子类) class<actor>( SomeFunctionCall() )
表达式
赋值
要给一个变量赋值,请使用 "=":
function Test() { local int i; local string s; local vector v, q; i = 10; //给一个整型变量i赋值 s = "Hello!"; //给字符型变型变量s赋值 v = q; //赋值向量q的值到v }
在UnrealScript中,当一个函数或其它表达式需要一个某种类型的数据(比如:"float")时,而您指定了一个不同的数据类型(比如:”int”),编译器将会自动转换你给的值为适当的类型。所有数字数据类型(byte、 int 和 float)之间的转换是自动进行的,不需要您做任何操作。
UnrealScript也可以转换许多内置的数据类型为其它的类型,如果在代码中明确地转换它们,用于转换的语法是:
function Test() { local int i; local string s; local vector v, q; local rotator r; s = string(i); //转换字符型i为一个字符串,并赋值给s s = string(v); //转换向量v为一个字符串,不把它赋值给s v = q + vector(r); //转换rotator类型的r为向量类型,不把它和q进行相加 }
这里是在UnrealScript您可以使用的不能自动地进行类型转换的完整集合:
- String转换为Byte 、Int、 Float:尝试转换一像
"123"
的字符串为123
。如果字符串不代表一个值,则它的值为0
。 - 转换Byte, Int, Float, Vector, Rotator为String:转换数字为文本格式。
- 转换String为Vector, Rotator:尝试解析vector 或 rotator的文本格式。
- 转换String 为 Bool:转换大小写不敏感的单词
"True"
或"False"
为True
和False=;将会使任何非零数值转换为 =True
;其它任何值转换为False
。 - 转换Bool为String:结果不是
"True"
就是"False"
。 - 转换Byte, Int, Float, Vector, Rotator类型为Bool类型:转换非零值为
True
;零值为False
。 - 转换Bool类型为Byte, Int, Float类型:转换
True
为1
;=False= 为0
。 - 转换Name为String:转换名称为等同的文本。
- 转换Rotator为Vector:根据rotator返回一个向前的向量。
- 转换Vector 为Rotator:返回一个以向量的方向进行倾斜、偏转的rotator;旋转度为0.
- 转换Object (或 Actor)为Int:返回一个针对于那个object的唯一的整型数值。
- 转换Object (或 Actor) 为 Bool类型:如果object是
None
则返回False=;否则为 =True
。 - 转换Object (或 Actor) 为String:返回那个object的文本格式。
在类之间转换对象引用
就像上面的简单数据类型的转换函数一样,在UnrealScript中你可以在各种类型之间转换actor和object引用。比如,所有的actors有一个叫"Target"的变量,它是到另一个actor的引用。假如您正在写一个脚本,在脚本中您需要检查并查看Target是否属于"Pawn" actor类的,并且你需要对您的target做一些特殊的处理,但仅当它是一个pawn类时才有效--比如,您需要调用Pawn函数的其中一个。actor的类型转换操作符可以让您完成这个目的。这里是一个例子:
var actor Target; //... function TestActorConversions() { local Pawn P; //转换Target的类型为Pawn并把它的值赋予P。如果Target不是一个Pawn(或者Pawn的子类),那么赋给P的值将会是None。 P = Pawn(Target); if( P != None ) { // Target是一个pawn,所以让它的Enemy为Self。 P.Enemy = Self; } else { // Target不是一个pawn。 } }
为了执行一个actor转换,输入类的名称并在后面加上你希望转换的actor表达式,并使用圆括号括起。根据这个转换是否合理,转换将或者成功或者失败。在上面的例子中,如果您的Target指向的是一个Trigger对象而不是一个Pawn,表达式Pawn(Target)将会返回"None"值,因为不能将一个Trigger转换为Pawn。然而,如果您的Target指向一个Brute对象,转换将会成功并返回那个Brute,因为Brute是Pawn的子类。
因此,actor类型转换有两个目的:第一,你可以使用它们来判断某个actor引用是否属于某个类。第二,你可以使用它们来将一个actor引用从一个类转换为一个更加明确的类。注意这些转换不会影响您正在转换的actor—它们只是使UnrealScript将对待那个actor引用就像它是一个更加明确的类型一样并且允许您访问这个更加子类中所声明的属性和方法。
另一个类型转换的列子是在Inventory脚本中。每个Inventory actor由一个Pawn所有,尽管它的Owner变量可以指向任何Actor(因为Actor.Owner是一个Actor类型的变量)。所以在Inventory代码中我们会将常看到将一个Owner转换为Pawn类型的现象,比如:
//当销毁时,由engine调用 function Destroyed() { //从拥有者的武器装备中移除 if( Pawn(Owner)!=None ) Pawn(Owner).DeleteInventory( Self ); }
函数
声明函数
在UnrealScript中,你可以声明新的函数及为已存在的函数书写一个新的版本(重写这些函数)。函数可以带有一个或多个参数(可以是UnrealScript支持的任何变量类型),并且可以可选择地返回一个值。尽管大多数函数是直接在UnrealScript中书写的,您也可以声明可以从UnrealScript中调用的函数,但是在C++中实现并存在于一个DLL文件中。Unreal支持所有可能函数调用组合:C++引擎可以调用脚本函数;脚本可以调用C++函数;而且脚本之间也可以相互调用。
这里是一个简单的函数声明。这个函数有一个vector(向量)作为参数并返回一个浮点型值:
// 用于计算一个vector的大小的函数 function float VectorSize( vector V ) { return sqrt( V.X * V.X + V.Y * V.Y + V.Z * V.Z ); }
关键词 function
总是在函数声明的前面,后面可以跟随任意的函数返回类型(在这里是 float
),然后是函数名及在括号里所包含的函数参数。
当调用一个函数时,在括弧内的代码将会被执行。在函数内部,您可以声明局部变量(使用关键字 local
),并且可以执行任何UnrealScript代码。可选择的 return
关键字可以使函数立刻地返回一个值。
你可以传递任何UnrealScript类型到一个函数(包括数组)中,并且这个函数可以返回任何类型。
默认情况下,在函数中声明的任何局部变量都将被初始化为0.
函数可以进行递归调用。比如,下面的函数计算了一个数的阶乘
//计算数的阶乘的函数 function int Factorial( int Number ) { if( Number <= 0 ) return 1; else return Number * Factorial( Number - 1 ); }
某些UnrealScript函数是在某些事件发生时由引擎来调用的。比如,当一个actor和另一个actor接触,引擎会调用它的 Touch 函数来告诉它是谁正在接触它。通过书写自定义的 Touch 函数,你可以在接触事件发生时采取特殊的行动:
//当某些东西接触这个actor时则调用这个函数 function Touch( actor Other ) { Log( "I was touched!") Other.Message( "You touched me!" ); }
上面的函数解释了几件事情。首先,函数使用 Log 命令向日志文件中写入了一个消息(除了格式规则以外,这和Basic语言的"print"及C语言的"printf"是类似的)。第二,它调用了在Other actor中的"Message"函数。调用在其它actors中的函数在UnrealScript是一个常用的操作,大体上和面向对象的语言比如Java是一致的,因为它为actors间彼此的交流提供了一个简单的方法。
函数参数修饰符
通常当您的调用一个函数时,UnrealScript会制作一个您传入函数的参数的一个本地副本。如果函数修改了某些参数,这对您传入的参数没有任何影响。比如,以下的代码:
function int DoSomething( int x ) { x = x * 2; return x; } function int DoSomethingElse() { local int a, b; a = 2; log( "The value of a is " $ a ); b = DoSomething( a ); log( "The value of a is " $ a ); log( "The value of b is " $ b ); }
当调用了DoSomethingElse函数后,产生了以下输出结果:
The value of a is 2 The value of a is 2 The value of b is 4
换句话说,函数DoSomething处理的是我们传递给它的变量a的本地副本,并且它不会影响真正的变量"a"。
out
修饰符告诉函数它应该修改传递给它的变量,而不是修改一个本地副本。这是有用的,比如,如果您有一个需要返回给调用者几个值的函数,你可以让调用者传递几个变量给带有 out
值的函数。
//计算一个向量中的最大值和最小值 function VectorRange( vector V, out float Min, out float Max ) { //计算最小值 if ( V.X<V.Y && V.X<V.Z ) Min = V.X; else if( V.Y<V.Z ) Min = V.Y; else Min = V.Z; //计算最大值 if ( V.X>V.Y && V.X>V.Z ) Max = V.X; else if( V.Y>V.Z ) Max = V.Y; else Max = V.Z; }
如果没有关键字 out
,那么尝试写出必须返回多个函数值的函数是个令人痛苦的事情。Out参数是通过引用传入的,所以在函数中修改参数的值将会立即影响原始值。这也可以用于多个值的优化,类似于C++中通过指定"const out"类型来实现。
使用 optional
关键字,你可以使某些函数的参数是可选的,这为调用者提供了便利。在UnrealScript函数中,调用者没有指定的可选参数的值时,将被设置为在函数声明是指定的默认值或如果在函数声明时没有指定值时则设置它为0(也就是 0、false、""、none)。对于native函数,可选参数的默认值依赖于函数。比如,Spawn函数有一个可选的参数location 和 rotation,它们默认为actor的位置和旋转度。可选参数的默认值可以通过添加 = value
来指定。比如,function myFunc(optional int x = -1)
。
关键字 coerce
强制使调用函数的参数转换为指定的类型(即使通常情况下UnrealScript将不会执行自动转换)。这对处理字符串的函数是有用的,函数将为您自动地将参数转换为字符串。
函数重载
"函数重写"是指在子类中写一个函数的新的版本。比如,您正在为一个新类型的叫Demon的鬼怪书写脚本。您所创建的Demon类继承了Pawn类。现在,当一个pawn第一次看到一个玩家时,将会调用pawn的"SeePlayer"函数,从而pawn可以开始袭击玩家。这是一个很好的概念,但是当您想在您的新的Demon类中以不同的方式处理"SeePlayer"函数时,你该怎么实现哪?答案便是函数重载。
要重写一个函数,只需要从它的父类中将函数的定义剪切并复制到您的新类中。比如,你可以添加SeePlayer函数到您的新类Demon中。
//类Demon 中的新的Touch函数版本 function SeePlayer( actor SeenPlayer ) { log( "The demon saw a player" ); //在这里添加中定义的功能… }
函数重载是有效地创建新的UnrealScript类的关键。您可以创建一个继承已有类的新类。然后,你所需做的便是重载那些你需要进行不同处理的函数。这可以使您在不必写大量的代码的情况下创建一种新的对象。
在UnrealScript中的某些函数是声明为 final
。=final=关键字(在关键字 function
后出现)是说:”这个函数不能被子类重载”。这可以在没有人重载的函数中使用,因为它可以提供更快的脚本代码执行速度。比如,你有一个VectorSize 函数用于计算一个向量的大小。因为绝对没有原因导致任何人来重载它,所以声明它为 final
。另一方面,像 Touch 这样的函数是非常依赖于情境的,所以不能声明它为final。
高级函数修饰符
- Static
- 静态函数就像C语言中的全局函数一样,在那里你可以在没有类的对象的引用的情况下来调用它。静态函数可以调用其它的静态函数,并且可以访问变量的默认值。静态函数不能调用非静态函数并且他们不能访问实例变量(因为它们的执行不是针对于对象的实例的)。和C++语言不一样,静态函数是虚函数并且可以在子类中重载。这在当您想在一个变量类(在编译时不能确定的但是通过变量或者表达式来引用的类)中调用一个静态函数时是有用的.
- Singular
singular
关键字,紧接在函数声明的前面出现,可以阻止函数对其本身进行递归调用。规则是:如果某个actor已经在一个singular函数中,任何对这个singular的后续调用将会被跳过。这在某些情况下来避免无限的递归循环bug是有利的。例如,如果你尝试着在您的 Bump 函数中移动一个actor, 那个actor将会在移动中撞击另一个actor是有很大的可能的,从而导致了另一个对 Bump 函数的调用等等。你必须细心地避免这种行为,但是如果您没有足够的信心来通过书写代码避免这种潜在的递归情况,那么请使用singular
关键字。
Native:你可以声明UnrealScript函数为 native
,这意味着该函数是可以从UnrealScript中调用的,但是事实上是在C++中实现的。比如,Actor类包含着许多native函数定义,比如:
native(266) final function bool Move( vector Delta );
在关键字 native
后的圆括号内的数字和在C++(使用 AUTOREGISTER_NATIVE
macro)中定义的函数的数字是相对应的,并且这仅在运算符函数中有要求。Native函数将存在于DLL文件中,该DLL文件的名称和包含了UnrealScript定义的类的包的名称是一致的。
- NoExport
- 仅用于native函数。说明了将不导出这个native函数的C++函数定义,仅导出该函数的在脚本版本的定义。
- Exec
- 意味着这个函数可以通过在控制台输入函数的名称来执行。仅在某些类中有效。
- Latent
- 说明了一个native函数是latent,意味着它仅能从状态代码中被调用,并且可能在游戏过去一段时间后返回。
- Iterator
- 说明了一个native函数是一个iterator,它可以通过使用
foreach
命令来在一系列的actors中循环。
- Simulated
- 不管一个actor是simulated代理或一个autonomous代理时,函数可以在客户端执行。所有不管是native还是final函数都可以自动地被simulated。
- Server
- 说明这个函数必须被发送到服务器上执行,而不是在本地客户端运行。
- Client
- 说明这个函数必须被发送到客户端执行,而不是在服务器上运行。这个标志也隐含着为该函数设置了simulated 标记。
- Reliable
- 说明了一个复制(replicated)函数(标记为 server(服务器) 或 client(客户端))是可靠发送,意味着相对于那个Actor上的其它复制来说,它将最终地按照顺序到达另一端。
- Unreliable
- 说明了一个复制函数(标记为 服务器 或 客户端)是不可靠发送,意味着它将不能保证会以任何特定的顺序到达另一端并且如果没有足够的带宽它可能会被完全地跳过。
- Private, Protected
- 这些关键字作为相应的变量关键字时具有同样的意思。
- Operator, PreOperator, PostOperator
- 这些关键字用于声明一个称为运算符重载函数(operator)的特殊函数(和C++中的运算符函数类似)。这是UnrealScript如何知道所有的内置运算符像"+", "-", "==", 和 "||"的方式。我将不会在这篇文档中详细说明运算符函数是如何工作的,但是运算符函数的概念和C++中是类似的,所以你可以像声明UnrealScript函数或native函数一样来声明一个新的运算符重载函数。
- Event
event=关键字和UnrealScript中的 =function
具有相同的意思。然而,当您可以使用unreal -make -h
来从Unreal中导出一个C++头文件时,UnrealEd为每个”event”自动生成一个C++ -> UnrealScript调用存根。这可以自动地保持C++代码和UnrealScript函数同步并消除了向一个UnrealScript函数中传递无效参数的可能性。比如,这些UnrealScript代码:
event Touch( Actor Other ) { ... }
生成类似于以下的在EngineClasses.h中的代码:
void eventTouch(class AActor* Other) { FName N("Touch",FNAME_Intrinsic); struct {class AActor* Other; } Parms; Parms.Other=Other; ProcessEvent(N, &Parms); }
因此您可以从C++中像这样来调用UnrealScript函数了:
AActor *SomeActor, *OtherActor; SomeActor->eventTouch(OtherActor);
- Const
- 仅用在声明为native的函数中,并且这个修饰符加在函数声明的 后面 。这个修饰符决定了这个函数是否应该在生成的头文件中作为‘const‘导出。举例应用:
native function int doSomething(string myData) const;
控制结构
UreanScript支持C/C++/Java中所有的标准的流程控制语句。
循环结构
循环
"For"循环指在没有满足某个条件之前一直执行循环。例如:
// "for"循环举例 function ForExample() { local int i; log( "Demonstrating the for loop" ); for( i=0; i<4; i++ ) { log( "The value of i is " $ i ); } log( "Completed with i=" $ i); }
这个循环的输出是:
Demonstrating the for loop The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 Completed with i=4
在for循环中,你必须指定3个表达式且它们之间使用分号分隔。第一个表达式用于初始化一个值作为它的初值;第二个表达式给出一个条件用于在每次循环执行前进行条件判断;如果这个表达式为真,则执行循环。如果它为假,则终止循环。第三个条件给出了一个增加循环计数器的表达式。
尽管大多数”for”循环仅仅是更新一个计数器,你也可以通过使用合适的初始化值、终止条件和增量表达式,从而使用”for”循环来做像遍历链表这样的高级的事情。
在所有的流程控制语句中,你可以或者执行一个没有括弧的单独的语句,如下所示:
for( i=0; i<4; i++ ) log( "The value of i is " $ i );
或者你可以执行由括弧包围的多个语句,像这样:
for( i=0; i<4; i++ ) { log( "The value of i is" ); log( i ); }
Do循环
"Do"循环是指在某个结束表达式为真的情况下,您可以不断的进行循环。注意Unreal使用 do-until
语法,这和C/Java (它们使用 do-while
)是不同的。
// "do"循环举例 function DoExample() { local int i; log( "Demonstrating the do loop" ); do { log( "The value of i is " $ i ); i = i + 1; } until( i == 4 ); log( "Completed with i=" $ i); }
这个循环的输出结果:
Demonstrating the do loop The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 Completed with i=4
While 循环
"While"是指当某个起始表达式为真时,您可以不断地进行循环。
// "while"循环举例 function WhileExample() { local int i; log( "Demonstrating the while loop" ); while( i < 4 ) { log( "The value of i is " $ i ); i = i + 1; } log( "Completed with i=" $ i); }
这个循环的输出是:
Demonstrating the do loop The value of i is 0 The value of i is 1 The value of i is 2 The value of i is 3 Completed with i=4
Continue
"continue"命令将会跳回到循环的开始位置,所以任何continue之后的命令将不会被执行。这可以用于在某些情况下跳过循环代码。
function ContinueExample() { local int i; log( "Demonstrating continue" ); for( i=0; i<4; i++ ) { if( i == 2 ) continue; log( "The value of i is " $ i ); } log( "Completed with i=" $ i ); }
这个循环的输出是:
Demonstrating break The value of i is 0 The value of i is 1 The value of i is 3 Completed with i=4
Break
"break"跳出最近一层循环("For", "Do", 或 "While")。
function BreakExample() { local int i; log( "Demonstrating break" ); for( i=0; i<10; i++ ) { if( i == 3 ) break; log( "The value of i is " $ i ); } log( "Completed with i=" $ i ); }
这个循环的输出是:
Demonstrating break The value of i is 0 The value of i is 1 The value of i is 2 Completed with i=3
注意"break"命令也可以用于跳过执行一个条件语句("switch")的剩余代码。
选择结构
If-Then-Else语句
"If", "Else If", and "Else"使您在满足某些条件是执行相应代码。
// "if"举例 if( LightBrightness < 20 ) log( "My light is dim" ); // "if-else"举例 if( LightBrightness < 20 ) log( "My light is dim" ); else log( "My light is bright" ); // if-else if-else"举例 if( LightBrightness < 20 ) log( "My light is dim" ); else if( LightBrightness < 40 ) log( "My light is medium" ); else if( LightBrightness < 60 ) log( "My light is kinda bright" ); else log( "My light is very bright" ); //执行语句带有括弧的"if"判断举例 if( LightType == LT_Steady ) { log( "Light is steady" ); } else { log( "Light is not steady" ); }
Case语句
"Case", "Default", 和 "Break"可以使您的轻易地处理一系列的条件判定。
// switch-case举例 function TestSwitch() { // 基于LightType的值,执行以下case语句的任意一条, switch( LightType ) { case LT_None: log( "There is no lighting" ); break; case LT_Steady: log( "There is steady lighting" ); break; case LT_Backdrop: log( "There is backdrop lighting" ); break; default: log( "There is dynamic" ); break; } }
"switch"由一个或多个"case"语句及一个可选择的"default"语句组成。在一个switch语句后,如果和"case"语句的某一个相匹配则执行该Case语句;如果没有相匹配的case则执行"default"语句;如果既没有相匹配的case语句也没有写default语句,则执行到"select"语句的末尾。
当您在代码中写好了以下的"case"标签,你必须是有一个"break"语句来使执行继续执行到"switch"语句的末尾。如果您不使用"break",将会继续执行接下来的"case"处理语句。
// Example of switch-case. function TestSwitch2() { switch( LightType ) { case LT_None: log( "There is no lighting" ); break; case LT_Steady: // 将一直执行到case: LT_Backdrop case LT_Backdrop: log( "There is lighting" ); break; default: log( "Something else" ); break; } }
Goto
"Goto"命令可以跳转到当前函数或状态的某处的标签。
// "goto"举例 function GotoExample() { log( "Starting GotoExample" ); goto Hither; Yon: log( "At Yon" ); goto Elsewhere; Hither: log( "At Hither" ); goto Yon; Elsewhere: log( "At Elsewhere" ); }
The output is输出结果是:
Starting GotoExample At Hither At Yon At Elsewhere
语言功能
内置的操作符和它们的优先级
UnrealScript提供了为各种操作提供了很多种类似于C/C++/Java类型的操作符,比如数字相加的操作、比较值的操作以及自增操作等。操作符的完整的集合在Object.u中定义,但是这里有一个简明的概括。这里使一些标准操作符,以优先级为顺序。注意所有的C类型的操作符的优先级和它们在C语言中的优先级是一致的。
操作符 | 应用的类型 | 意思 |
---|---|---|
@ | string(字符串) | 字符串连接,在两个字符串间有一个附加的空格"string1"@"string2" = "string1 string2" |
@= | string(字符串) | 字符串连接,在两个字符串间有一个附加的空格,连接并赋值(版本3323 及其后续版本) |
$ | string(字符串) | 字符串连接 |
$= | string (字符串) | 字符串连接,连接并赋值(版本3323 及其后续版本) |
*= | byte, int, float, vector, rotator | 相乘并赋值 |
/= | byte, int, float, vector, rotator | 相除并赋值 |
+= | byte, int, float, vector | 相加并赋值 |
-= | byte, int, float, vector | 相减并赋值 |
|| | bool | 逻辑“或“ |
&& | bool | 逻辑“与“ |
^^ | bool | 异或 |
& | int | 按位与 |
| | int | 按位或 |
^ | int | 按位异或 |
!= | All (所有类型) | 不等于 |
== | All(所有类型) | 等于 |
< | byte, int, float, string | 小于 |
> | byte, int, float, string | 大于 |
<= | byte, int, float, string | 小于等于 |
>= | byte, int, float, string | 大于等于 |
~= | float, string | 约等于(精确到0.0001),大小写不敏感等式 |
<< | int, vector | 左移(int),正向向量转换(vector) |
>> | int, vector | 左移(int), 反向向量转换(vector) |
>>> | 和>>相同 | |
+ | byte, int, float, vector | 加 |
- | byte, int, float, vector | 减 |
% | float, int, byte | 求模(除法的余数) |
* | byte, int, float, vector, rotator | 乘 |
/ | byte, int, float, vector, rotator | 除 |
Dot | vector | 矢量点积 |
Cross | vector | 矢量叉积 |
** |
float | 求幂 |
ClockwiseFrom | int (rotator elements) | 当从第一个参数可以通过第二个参数顺时针旋转得到,则返回true |
上面的表格按照优先顺序列出了操作符(具有相同优先级的分为一组)。当您输入一个复杂的表达式,比如"1*2+3*4",UnrealScript会自动地按照优先级为操作符分组。因为乘法的优先级比加法高,这个表达式等同于"(1*2)+(3*4)"。
"&&" (逻辑与) 和 "||" (逻辑或)操作符是短路的:如果表达式的结果能够单独地由第一个表达式决定(比如,如果 && 的第一个参数是false),第二个表达式则不需要进行计算。
关于向量的正向和反向转换,反向变换>>是从本地空间转换为世界空间而正向变换<<是从世界空间转换为本地空间。
比如,如果您有一个向量指向玩家前面64个单位的地方,vect(64,0,0)是在本地玩家坐标的值。如果您想让它在世界空间中,您必须使用玩家的旋转来把它转换为世界坐标,所以您必须通过以下方式来进行进算:
myWorldSpaceVect = my64Vect >> playerPawn.rotation;
当您有一个世界空间的向量并且你想让它呈现在本地玩家空间时,万一您想使用一个向前的rotation。比如,您可能想转换一个汽车actor的世界空间速度到本地空间,从而您可以获得X(前进速度)来把它输出到一个HUD中。
除了标准的操作符,UnrealScript支持一下一元操作符:
- ! (bool)逻辑非
- - (int, float) 负数
- ~ (int)按位取反
- ++, --自增,自减(或者在一个变量前或者在一个变量后使用)
新的操作符会不断地增加到引擎中。要获得操作符的完整列表,请查看最新的UnrealScript源代码-尤其是Object类。
通用函数
创建对象
为了在UnrealScript中创建一个新的对象,您将会根据这个对象是否是一个Actor来使用这两个函数其中的一个。对于Actors,您将会使用Spawn函数,声明在Actor.uc文件中。对于不是从Actor类派生的类,您必须使用 new
操作符。New操作符的语法和其它的函数不同。除了一个可选择的参数列表,你必须指定新的对象的类及可选择的模板对象。new操作符没有UnrealScript声明,但是这里是函数声明的样子:
native final operator function coerce Object new ( object InOuter, name InName, int InFlags, class InClass, object InTemplate );
- InOuter
- (可选的) 用于作为新创建的对象的Outer的对象 。如果没有指定,则此对象的Outer将被设置为一个仅当游戏运行时存在的被称为"transient package"的特殊包。
- InName
- (可选的)新对象的名称。如果没有指定,此对象将以ClassName_##的形式给定一个唯一的名称,每次创建一个这个类的实例,这里的##会不断增加。
- InFlags
- (可选的,目前还不能使用,因为对象标志是64位的)创建对象时所要使用的对象标志。
- 0x0000000100000000: 支持编辑器撤销/重复(RF_Transactional)。
- 0x0000000400000000: 可以被外部文件所引用(RF_Public)。
- 0x0000400000000000: 不能被保存到磁盘(RF_Transient)。
- 0x0010000000000000: 请不要再游戏客户端加载对象(RF_NotForClient)。
- 0x0020000000000000: 请不要再游戏服务器加载对象(RF_NotForServer)。
- 0x0040000000000000: 不要再编辑器内加载对象。(RF_NotForEdit)
- x0008000000000000:即使对象没有被引用,也要保持对象以便编辑。(RF_Standalone)
- InClass
- 用于创建一个实例的类。
- InTemplate
- 用于初始化新的对象的属性值的object。
新的操作符的实际的语法如下所示:
ObjectVar = new[(InOuter, InName, InFlags)] <class‘InClass‘>[(InTemplate)];
创建类LightFunction的一个对象
function CreateALight() { local LightFunction NewObj; NewObj = new class‘Engine.LightFunction‘; }
创建一个叫"NewLight"的LightFunction对象,分配这个对象作为它的Outer。
function CreateALight() { local LightFunction NewObj; NewObj = new(Self,‘NewLight‘) class‘Engine.LightFunction‘; }
在transient包中创建一个叫"NewLight"的新的LightFunction对象,使这个对象作为LightFunctionTemplate变量的值来初始化新的对象的属性:
var LightFunction LightFunctionTemplate; function CreateALight() { local LightFunction NewObj; NewObj = new(None,‘NewLight‘) class‘Engine.LightFunction‘ (LightFunctionTemplate); } defaultproperties { Begin Object class="LightFunction" Name=MyLightFunctionArchetype End Object LightFunctionTemplate=MyLightFunctionArchetype }
整型函数
- int Rand( int Max ); 返回一个0到Max-1的随机数
- int Min( int A, int B );返回两个数中最小的数
- int Max( int A, int B );返回两个数中最大的数
- int Clamp( int V, int A, int B );返回A到B间隔内所包含的第一个数值。
警告-Min和Max在整数上的应用和C或C++不同。当针对浮点型数据使用这两个函数时不会产生警告 – 您的数字会默认地进行四舍五入处理!对于浮点型,你需要使用函数FMin 和 FMax来进行操作。
浮点型数据函数
- float Abs( float A );返回数值的绝对值
- float Sin( float A ); 返回在半径中所表示的数值的正弦函数值。
- float Cos( float A );返回弧度数值的余弦值
- float Tan( float A ); 返回弧度数值的正切值
- float ASin( float A );返回半径所表示的数值的反正弦值。
- float ACos( float A );返回半径所表示的数值的反余弦值
- float Atan( float A );返回弧度数值的反正切值
- float Exp( float A ); 返回A的幂数的常量指数“e“
- float Loge( float A ); 返回A的对数
- float Sqrt( float A );返回A的平方根
- float Square( float A ); 返回A的平方的值
- float FRand();返回从0.0 到 1.0的一个随机数
- float FMin( float A, float B );返回两个数最小的值
- float FMax( float A, float B ); 返回两个数最大的值
- float FClamp( float V, float A, float B ); 返回A到B的间隔区间内的第一个数的值。
- float Lerp( float A, float B, float Alpha );返回A和B之间的线性插值。
- float Smerp( float Alpha, float A, float B );返回A和B之间的一个Alpha-smooth非线性插值。
- float Ceil ( float A ); 向上舍入
- float Round ( float A ); 正常舍入
字符串函数
- int Len( coerce string S ); 返回字符串的长度
- int InStr( coerce string S, coerce string t); 如果第一个字符串包含第二个字符串,则返回第二个字符串在第一个字符串中的偏移量;如果不存在,则返回值为-1.
- string Mid ( coerce string S, int i, optional int j );返回字符串S的中间部分,即从字符i开始后连续j个字符的字符串(或者如果没有指定j,则返回字符i后面的所有字符)。
- string Left ( coerce string S, int i );返回字符串S的最左边的字符i。
- string Right ( coerce string] S, int i ); 返回字符串S的最右边的字符i。
- string Caps ( coerce string S );返回到转换为大写字母的字符串S。
- string Locs ( coerce string S);返回转换为小写字母的字符串S (v3323 and up)。
- string Chr ( int i );从ASCII表格中返回一个字符
- int Asc ( string S );返回一个字符的ASCII码值(仅使用字符串的第一个字符)
- string Repl ( coerce string Src, coerce string Match, coerce string With, optional bool bCaseSensitive );在源代码中使用
With
代替Match
。 - string Split(coerce string Text, coerce string SplitStr, optional bool bOmitSplitStr); 当第一个
SplitStr
发生时分隔Text
并返回Text
的剩余部分。如果=bOmitSplitStr=为真,=SplitStr=将会从返回的字符串中被忽略。 - array SplitString( string Source, optional string Delimiter=",", optional bool bCullEmpty );使用一个单独的表达式来分隔一个字符串为一个字符串数组的封装函数。
- JoinArray(array StringArray, out string out_Result, optional string delim = ",", optional bool bIgnoreBlanks = true);从一个字符串数组中创建一个单独的字符串,使用指定的分界符,可以选择是否忽略空格。
- ParseStringIntoArray(string BaseString, out array Pieces, string Delim, bool bCullEmpty);分解一个具有分解符的字符串为一个字符串数组的多个元素。
- A == B; 比较,如果两个字符串相同则返回true(区分大小写)。
- A ~= B; 比较,如果两个字符串相同则返回true(不区分大小写)
- A != B; 比较,如果字符串不相同返回true.
请查看Unreal 字符串来获取更多的信息。
Vector函数
- vector vect( float X, float Y, float Z );使用给定的组成元素创建一个新的向量。
- float VSize( vector A );返回向量的欧几里得大小(元素开平方后值的总和的平方根)
- vector Normal( vector A );返回一个大小为1.0的向量,面向指定向量的方向。
- Invert ( out vector X, out vector Y, out vector Z ); I通过3个坐标轴向量来反转坐标系统。
- vector VRand ( );返回一个均匀分布的随机向量。
- vector MirrorVectorByNormal( vector Vect, vector Normal );根据指定的法线向量镜像一个向量。
计时器函数
计时器函数仅在Actor的子类中存在。
您可以创建具有不同触发速率的多个计时器。每个计时器有一个唯一的目标函数(默认为 Timer() )。
- function SetTimer(float inRate, optional bool inbLoop, optional Name inTimerFunc);在被触发 inRate 后启动一个计时器。如果 inbLoop 为真则计时器将会循环。_inTimerFunc_ 定义了要调用的函数,默认情况下这个函数是 Timer() ,这个值也用于识别多个计时器。
- ClearTimer(optional Name inTimerFunc);停止一个正在运行的计时器
- bool IsTimerActive(optional Name inTimerFunc);如果给定的计时器是出于启动状态,则返回true.
- float GetTimerCount(optional Name inTimerFunc);返回计时器的计数器的值。也就是自从上一次执行计时器开始所过的秒数。如果计时器没有启动,则返回-1。
- float GetTimerRate(optional name TimerFuncName = ‘Timer‘);返回计时器的速率,
GetTimerRate(‘SomeTimer‘) - GetTimerCount(‘SomeTimer‘)
将返回计时器剩余的时间。
调试函数
以下函数可以帮助您调试代码
- LogEx( ELoggingSeverity Severity, name Category, coerce string Msg );使用给定的安全级别和类别来记录一个消息。这个函数比标准的 log() 函数提供了更多的控制,它允许您在运行时根据安全级别和目录来过滤日志信息。
- LogFatal( name Category, coerce string Msg );用于调用 LogEx(LOG_FATAL, Category, Msg) 函数的缩写。
- LogError( name Category, coerce string Msg );
- function LogWarn( name Category, coerce string Msg );
- LogInfo( name Category, coerce string Msg );
- LogDebug( name Category, coerce string Msg );
- LogTrace( name Category, coerce string Msg );
注意,正如变更列表134102所示,以上的日志功能已经不能获得。它们已经被由UnrealScript 处理器处理的一个关于日志的宏所取代。请查看版本更新信息来获取详情:Upgrading to changelist 134102
- ScriptTrace();存储当前的脚本调用堆栈到一个日志文件中
- Name GetFuncName();返回当前正在调用的函数的名称
- DumpStateStack();记录当前的状态栈
UnrealScript处理器
要获得更多信息,请查看Unrealscript 处理器页面。
UnrealScript工具和使用
Script Profiler(脚本分析器)
Script Profiler(脚本分析器)能够帮助您了解脚本执行的那些部分占有了大部分的时间。
脚本调试器
请查看Unreal的调试工具页面获得更多信息。
虚幻开发环境(UDE)
如果想获得更多的使用UDE来进行UnrealScript开发和调试的信息,请查看虚幻开发环境页面。
高级的语言功能
计时器
计时器是一种用于安排一个事件随时间变化发生、重复发生的一种机制。以这种方式,当一段固定时间过后,一个Actor能使一个计时器来注册它本身到一个游戏引擎上,从而可以使一个 Timer()
函数被调用或者一次、或者重新发生。
UnrealScript计时器仅仅是在每个Actor内部作为一个structs数组来实现的(一个Actor可以有多个还没有被触发的计时器) 。这个struct包含了在计时器过期前所剩余的时间的数量、计时器到期时所要调用的函数等。
一般游戏主循环每帧ticks每个Actor一次,并且每个Actor的 Tick()
函数包含一个调用函数 UpdateTimers()
,它会检查任何将要过期的计时器并调用它们适当的UnrealScript代码。
时间间隔大小限于每个帧之间的时间间隔长度,但是对硬件和OS资源没有要求。所有的这些是在C++代码中实现的,所以您可以在不必担心可能引起的任何后果的情况下安全地更新成百个UnrealScript计时器。当然,您不会想使所有的计时器同时地过期或者在每一个帧过期,因为当这是计时器被激活时要以较慢的速度执行脚本代码。
状态
状态概述
从游戏历史来说,游戏开发人员自从游戏发展到"pong"阶段后便开始使用状态这个概念。状态(也被称为“状态机编程”)是使复杂的物体行为更加容易管理的一种常用方式。然而在UnrealScript出现之前,状态在编程语言的层次上还没有得到支持,需要开发人员基于物体的状态来创建C/C++"switch"语句,这样的代码是难于书写和更新的。
UnrealScript在编程语言的层次上支持状态。
在UnrealScript中,世界中的每个actor都处于并仅处于一个状态。它的状态反映了它要执行的动作。比如,移动画刷有几个状态像"StandOpenTimed" 和 "BumpOpenTimed"。Pawns有一些像"Dying"、 "Attacking"、 和 "Wandering"等状态。
在UnrealScript中,你可以针对一个特定的状态来书写函数和代码。这些函数和代码仅当actor在那个状态时被调用。比如,你正在写一个关于怪物的脚本,并正在思考如何处理"SeePlayer"函数,当您在到处巡逻的时候,您想攻击您看到的玩家,当您正在攻击玩家时,您想不停地进行连续攻击。
实现这个目的最容易的方式便是通过定义几个状态(Wandering(到处走动) 和 Attacking(攻击)),并且在每个状态写一个不同版本的"Touch"函数。UnrealScript支持这样做。
在对状态进行深入研究之前,你需要知道状态的两个主要好处和一个复杂性:
- 好处:状态提供了一个用于来书写针对状态的函数的简单方法,所以你可以根据actor的行为来不同的方式来处理同一个函数。
- 好处: 使用状态,您可以使用完整的规范的UnrealScript命令和几个以"latent functions"著称的专用函数来书写专用的"state code(状态代码)"。一个latent函数的执行是比较慢的(也就是非阻塞的),并且可能要在一定的游戏时间过后才能返回。这可以使您进行基于时间的编程 – 这个主要的好处是无论是C, C++, 还是Java都没有提供的。也就是,你可以按照您想的方式来书写代码;比如,你书写一个脚本来表达"打开这个门; 暂停2s中; 播放这个音效;打开那个门;发放一个怪物来攻击玩家"。你可以通过使用简单的线性的代码来完成它,并且虚幻引擎会负责管理基于时间的代码执行的细节。
- 复杂性:现在您可以多个状态及子类中重载函数(像 Touch ),这对您明确地分辨出在哪个特定的状态来调用哪个"Touch"函数增加了负担。UnrealScript提供了一个规则来清楚地描述这个过程,它是在您创建类及状态的复杂层次时必须考虑的。
这里是一个TriggerLight脚本中的一个状态的例子:
// Trigger打开光源 state() TriggerTurnsOn { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = 1.0; Enable( ‘Tick‘ ); } } // Trigger关闭光源 state() TriggerTurnsOff { function Trigger( actor Other, pawn EventInstigator ) { Trigger = None; Direction = -1.0; Enable( ‘Tick‘ ); } }
这里声明了两个不同的状态(TriggerTurnsOn和TriggerTurnsOff),并且在每个状态中书写了一个不同版本的Trigger函数。尽管您可以在不使用状态的情况下努力地完成这个实现,但是状态使代码更加的具有模块性和可扩展性:在UnrealScript中,你可以很容易地创建一个现有类的子类、增加新的状态及增加新的函数。如果过你尝试不使用状态来完成这个功能,最终的代码在以后将更加的难于扩展。
一个状态可以声明为可编辑的,意味着用户可以在UnrealEd中设置actor的状态,或者不能设置其状态。如果想定义一个可以编辑的状态,请按照以下方式:
state() MyState { //... }
如果要声明一个不可编辑的状态,请按照:
state MyState { //... }
您也可以通过"auto"关键字来指定一个actor的自动的或者初始的状态。这会导致当新的actor初次被激活时都会被置于那个状态。
auto state MyState { //... }
状态标签和Latent函数
除了函数,一个状态可以包含一个或多个标签,在其后面可以书写UnrealScript代码,比如:
auto state MyState { Begin: Log( "MyState has just begun!" ); Sleep( 2.0 ); Log( "MyState has finished sleeping" ); goto(‘Begin‘); }
上面的状态代码输出了信息 "MyState has just begun!"
,接着它停止了2秒钟,然后它输出了信息 "MyState has finished sleeping"
。这个例子中的一个有趣的事情便是调用latent函数"Sleep":这个函数的调用并不是立即返回,而是在一定量的游戏时间过去后才返回。Latent函数仅能在状态代码中并且不能从函数中调用。Latent函数可以帮助您管理包括一段时间的经过在内的一系列复杂的事件。
所有的状态代码都是以标签定义开始;在上面的例子中标签命名为"Begin"。标签提供了一个进入状态代码的方便的入口点。你可以在状态代码中使用任何标签名称,但是"Begin"标签式专用的:它是那个状态的默认入口点。
所有的actor都有三个主要的latent函数可以使用:
- Sleep( float Seconds )使状态的执行暂停一段时间,然后再继续执行。
- FinishAnim()等待指导当前你正在播放的动画序列播放完成,然后继续执行。这个函数使编写由动画驱动的脚本也就是执行是由网格物体动画控制的脚本变得容易。比如,大多数AI脚本都是由动画驱动的(相对于有时间驱动的脚本),因为平滑过度的动画是AI系统的主要目标。
- FinishInterpolation()等待当前的InterpolationPoint移动完成然后继续。
Pawn类为动作定义了几个重要的latent函数比如在世界中导航及短期运动。请查看单独的AI文档来获得它们用途的描述。
3个native UnrealScript函数在书写状态代码时尤其地有用:
- "Goto(‘标签名‘)"函数(和C/C++/Basic中的goto类似),该函数使状态代码在指定的标签处继续执行。
- 特殊的Goto(‘‘)命令,它会导致状态代码停止执行。状态代码在您到达一个新的状态或者到达当前状态中的一个新的标签之前将不会继续执行。
- "GotoState"函数会使actor跳转到一个新的状态,然后在指定的标签处继续执行(如果您没有指定一个标签,默认则是"Begin"标签)。你可以在状态代码中调用GotoState函数,它将会立刻跳转到目标状态。你也可以在actor中的任何函数中调用GotoState函数,但是它不能立即生效:它只有在函数执行返回到状态代码时才会生效。
这里是到目前为止所讨论的状态概念的示例:
//这是一个自动的状态 auto state Idle { //当被另一个actor触摸时… function Touch( actor Other ) { log( "I was touched, so I‘m going to Attacking" ); GotoState( ‘Attacking‘ ); Log( "I have gone to the Attacking state" ); } Begin: log( "I am idle..." ); sleep( 10 ); goto ‘Begin‘; } //攻击状态 state Attacking { Begin: Log( "I am executing the attacking state code" ); //... }
当您运行这个程序然后触摸那个actor,你会看到:
I am idle... I am idle... I am idle... I was touched, so I‘m going to Attacking I have gone to the Attacking state I am executing the attacking state code
请确保您理解了GotoState的这个重要的方面:当你从一个函数中调用GotoState函数,它不会立刻跳转到目标状态,而是当给函数的执行返回到状态代码时才会跳转到目标状态。
状态继承和范围规则
在UnrealScript中,当您创建一个现有类的子类时,您的新类继承了父类的所有变量、函数及状态。这很好理解。
然而,对UnrealScript编程模型的状态抽象给继承及其范围规则增加了额外的复杂性。
- 一个新的子类继承它的父类的所有变量。
- 一个新子类继承它的父类的所有的非状态函数。你可以冲在任何这些继承过来的非状态函数。你可以增加全新的非状态函数。
- 一个新的子类继承它的父类的所有状态,包括在这些状态中的函数和标签。你可以重载任何继承的状态函数及状态标签,你可以增加新的状态函数及新的状态标签。
这里是所有重载规则的一个例子:
// 这里是一个示例父类 class MyParentClass extends Actor; // 一个非状态函数 function MyInstanceFunction() { log( "Executing MyInstanceFunction" ); } // 一个状态 state MyState { //状态函数 function MyStateFunction() { Log( "Executing MyStateFunction" ); } // "Begin"标签 Begin: Log("Beginning MyState"); } // 这里是一个示例子类 class MyChildClass extends MyParentClass; // 这里我重载了一个非状态函数 function MyInstanceFunction() { Log( "Executing MyInstanceFunction in child class" ); } //这里我重新声明了MyState状态,以便我可以重载MyStateFunction函数。 state MyState { //这里我重载了MyStateFunction函数 function MyStateFunction() { Log( "Executing MyStateFunction" ); } //这里我重载了"Begin"标签 Begin: Log( "Beginning MyState in MyChildClass" ); }
当您在一个或多个状态中、及在一个或多个父类中有一个全局执行的函数,你需要理解在给定的环境中应该调用哪个版本的函数。范围规则解决了这些复杂的情况:
- 如果对象在一个状态中,并且函数的实现存在于那个状态的某处(不管是在actor类中还是在某个父类中),则调用最子类的状态函数版本。
- 否则,调用最底层派生函数的非状态版本。
高级的状态编程
如果一个状态没有重载父类中具有相同名称的状态,那么你可以随意地使用"extends"关键字来使该状态在当前类的所存在的状态上进行扩展。这是有用的,比如,在您有一组类似的状态的情况时(比如MeleeAttacking 和 RangeAttacking),它们有很多共有的功能。在这种情况下你可以像这样声明一个作为基类的Attacking状态:
// 基类的Attacking状态 state Attacking { //在这里粘贴基类函数... } //近处袭击 state MeleeAttacking extends Attacking { // 在这里粘贴序列化的函数... } //远处袭击 state RangeAttacking extends Attacking { //在这里粘贴序列化的函数... }
状态可以选择使用 ignores
修饰符来忽略函数,语法如下所示:
//声明一个状态 state Retreating { //忽略以下信息... ignores Touch, UnTouch, MyFunction; // 在这里粘贴函数... }
您可以从它的"state"变量、类型"name"变量来说明一个actor在哪个特定的状态中。
通过使用 GotoState(‘‘) 函数使一个actor处在"no state(没有状态)"中是可以实现的。当一个actor在"no state(没有状态)"时,仅可以调用它的全觉(非状态)函数。
无论您什么时候使用 GotoState 命令来设置一个actor的状态,引擎都可以调用两个专用的通知函数,如果您已经定义了它们:_EndState()_ 和 BeginState() 。 EndState 在新的状态开始之前将会在当前的状态中立刻被调用,BeginState 在新的状态开始后将立刻被调用。这些函数为您的状态可能需要进行的任何特定状态的初始化和清除提供了方便。
状态栈
使用正常的状态变换时,当你从一个状态到另一个状态后,便不能再回到先前的状态。但是通过使用状态栈使这个功能可以实现。调用 PushState 函数将会转换到放在栈的顶部的新状态,当前的状态将会被冻结。当调用PopState 时,将会恢复先前的状态并继续从 PushState 被调用的点开始继续执行。 当在状态代码内部使用PushState 时,它是作为latent函数的,所以和您在函数中调用 PushState 的代码执行行为是不同的。从函数中调用 PushState 将不会中断代码的执行(就像在一个函数中使用GotoState一样),然而当从状态代码中调用它时将会暂停代码的执行,直到子状态被弹出为止(同样也和在状态代码中调用GotoState类似)。
一个状态只能被放到栈中一次,尝试将同一个状态压入堆栈两次将会失败。 PushState 就像 GotoState 那样进行使用,它需要一个状态名称和一个可选的作为状态入口的标签。新的状态将会接受到一个 PushedState 时间,但前的状态受到一个 PausedState 的事件。当调用 PopState 后,当前状态将收到一个 PoppedState 事件并且新的状态(在栈中这个状态的下一个状态)将会收到 ContinuedState 。
state FirstState { function Myfunction() { doSomething(); PushState(‘SecondState‘); //只要我们在一个函数(不是latent功能)中,它将立即被执行。 JustPushedSecondState(); } Begin: doSomething(); PushState(‘SecondState‘); // 只要我们在一个状态代码快中,一旦SecondState被弹出,这将立即执行。(latent 功能) JustPoppedSecondState(); } state SecondState { event PushState() { //我们被压入,回到第一个状态 PopState(); } }
使用函数 IsInState ,你将可以检验一个某个状态是否在堆栈中。这个函数仅检查状态的名称,因此不能用于检查父类的状态。
state BaseState { ... } state ExtendedState extends BaseState { ... }
如果活动的状态是 ExtendedState
,那么 IsInState(‘BaseState‘) 将会返回false。当然,如果 BaseState 在栈上,调用 IsInState(‘BaseState‘, true) 将会返回true。
Replication(复制)
关于在UnrealScript中的复制的更多信息,请查看[https://udn.epicgames.com/Three/NetworkingOverview#UnrealScript:%20the%20Replication%20Statement][Networking Overview 网络概述]]页面。
Iteration(迭代)(ForEach)
UnrealScript 的 foreach
命令使处理大组的actor变得容易,比如在一个关卡或者在一个actor附近特定距离内的所有actor。"foreach"和一个叫"iterator"的用于在一系列的actor中全部地循环处理的特殊类型函数协同工作。
这里是一个 foreach
示例:
//显示关卡中的所有的光源 function Something() { local actor A; // 循环全部处理关卡中的所有的actor log( "Lights:" ); foreach AllActors( class ‘Actor‘, A ) { if( A.LightType != LT_None ) log( A ); } }
在所有的 foreach
中的第一个参数都是一个常量类,它指定了要搜索的actor的类型。你可以用这个来限定搜索,比如,仅搜索所有的Pawns。
在 foreach
中的第二个参数是一个变量,在 foreach
循环过程中的每个迭代期间中这个变量都会被赋予一个actor值。
这里是和"foreach"进行协同工作的所有的迭代函数。
AllActors ( class<actor> BaseClass, out actor Actor, optional name MatchTag )
循环迭代处理关卡中所有的actors。如果你指定一个可选的MatchTag(匹配标签),则仅包括actor的"Tag"变量和您所指定的标签相匹配的actors。
DynamicActors( class<actor> BaseClass, out actor Actor )
循环迭代所有的从关卡启动后产生的actors,忽略关卡启动时放置到关卡中的actors。
ChildActors( class<actor> BaseClass, out actor Actor )
循环迭代这个actor所拥有的所有的actors。
BasedActors( class<actor> BaseClass, out actor Actor )
循环迭代使用这个actor作为基类的所有actors。
TouchingActors( class<actor> BaseClass, out actor Actor )
循环迭代接触这个actor的所有actors。
TraceActors( class<actor> BaseClass, out actor Actor, out vector HitLoc, out vector HitNorm, vector End, optional vector Start, optional vector Extent )
循环迭代所有接触从开始点(Start)到结束点(End)之间并且碰撞范围盒为 Extent的轨迹线的actors。在每次迭代中,HitLoc设置为碰撞位置,HitNorm设置为一个向外指的碰撞法线。
OverlappingActors( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc, optional bool bIgnoreHidden )
循环迭代在指定位置的指定半径内的所有的actors(或如果没有指定,则是这个actor的位置)。
VisibleActors( class<actor> BaseClass, out actor Actor, optional float Radius, optional vector Loc )
循环迭代在指定位置可以看到的所有actors(或者如果没有指定位置,则是指这个actor的位置)。
VisibleCollidingActors ( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc, optional bool bIgnoreHidden );
返回在某个半径内的并且这些actors从一个位置(默认为调用者的位置)到那个actor的位置的轨迹不会碰撞世界中的所有碰撞(bCollideActors==true) actors。由于这个函数使用了碰撞散列函数,所以它要比 AllActors()快很多。
CollidingActors ( class<actor> BaseClass, out actor Actor, float Radius, optional vector Loc );
返回在某个半径内的碰撞(bCollideActors==true) actors。由于它使用了碰撞散列函数,所以在处理小半径时将比 AllActors() 函数快很多。
_注意_:迭代函数是特定类的所有成员,所以如果您想在一个非Actor类的函数中使用一个迭代器,你则必须有一个actor类的变量,使用以下语法:
foreach ActorVar.DynamicActors(class‘Pawn‘, P)
所以在一个交互作用的类中,你可以这样做:
foreach ViewportOwner.Actor.DynamicActors(class‘Pawn‘, P)
注意: 迭代器现在也支持动态数组,你可以在 Advanced Language Features 高级语言功能部分获得相关信息。
函数调用限制符
在复杂的编程情况下,你经常需要调用一个函数的指定版本,而不是在当前范围内的一个函数版本。为了处理这些情况,UnrealScript提供了以下关键字:
Global:调用最子类的全局(非状态) 函数版本 Super:调用函数在父类中的相应的版本,根据特定的情境被调用的函数可以是一个状态或者非状态函数。 Super(classname):调用指定类中的相应的函数,根据特定的情境被调用的函数可以是一个状态或者非状态函数。
结合使用多个调用修饰符(也就是 Super(Actor).Global.Touch )是无效的。
这里是一些调用修饰符的示例:
class MyClass extends Pawn; function MyExample( actor Other ) { Super(Pawn).Touch( Other ); Global.Touch( Other ); Super.Touch( Other ); }
作为一个附加例子,BeginPlay()在一个actor将要进入一个要播放的游戏时被调用。BeginPlay()函数在Actor类中实现而且它包含着需要执行的某些重要的功能。现在,假如您想在新类MyClass中重载BeginPlay()函数,来增加某些新的功能。为了安全地实现上述功能,您需要调用父类的BeginPlay()函数。
class MyClass extends Pawn; function BeginPlay() { //调用父类的BeginPlay函数(很重要) Super.BeginPlay(); //现在您可以自定义BeginPlay的内容 //... }
在一个变量类中访问静态函数
在一个变量类中的静态函数可以通过以下语法调用:
var class C; var class<Pawn> PC; class‘SkaarjTrooper‘.static.SomeFunction(); //在一个特定的调用一个静态函数 PC.static.SomeFunction(); //在一个变量类中调用一个静态函数 class<Pawn>(C).static.SomeFunction(); //在一个进行类型转换的类中调用一个静态函数
变量的默认值
访问变量的默认值
UnrealEd允许关卡设计人员修改一个物体的类的变量的默认值。当属于这个类的一个新actor产生,它的所有的变量都会被初始化为这些默认值。有时,手动地将一个变量重置为默认值是有帮助的。比如,当一个玩家投射掉一个武器装备,那么武器装备代码需要将actor的某些值重置为默认值。在UnrealScript中,您可以通过使用"Default"关键字来访问一个类的默认变量,比如:
var() float Health, Stamina; //... //重置所有变量为它们的默认值 function ResetToDefaults() { //重置健康状况和精力 Health = Default.Health; Stamina = Default.Stamina; }
通过一个类的引用来访问变量的默认值
如果您有一个类的引用(一个 class
或 class<classlimitor>
类型的变量),您便可以访问它所引用的类的默认属性,而不需要有那个类的一个对象。这个语法适用于和class类型等同的其它任何表达式。
var class C; var class<Pawn> PC; Health = class‘Spotlight‘.default.LightBrightness; // 访问在Spotlight中的LightBrightness的默认值 Health = PC.default.Health; //访问变量类PC的Health的默认值 Health = class<Pawn>(C).default.Health; //访问类型转换表达式中Health的默认值
使用defaultproperties(默认属性)语句块指定默认值
除了在UnrealEd中通过一个属性窗口来设置一个Actor的属性的默认值,你也可以通过在类中的defaultproperties(默认属性)语句块中放置特殊的赋值表达式来设置成员变量的默认值。
- 在defaultproperties语句块中不允处动态数组之外的代码语句。
- 分号可以放在每行的末尾,但不是必须的。
- 子类可以继承默认值。在子类的defaultproperties(默认属性)中指定的的值将会覆盖父类的值。
语法
defaultproperties(默认属性)语句块的语法和标准的unrealscript语法略有不同:
- Simple Types (Ints, Floats, Bools, Bytes):
- VarName=Value
- Static Array:
ArrayProp(0)=Value1
ArrayProp(1)=Value2
ORArrayProp[0]=Value1
ArrayProp[1]=Value2
- Dynamic Arrays:
ArrayProp=(Value1,Value2,Value3)
ORArrayProp(0)=Value1
ArrayProp(1)=Value2
ArrayProp(2)=Value3
ORArrayProp.Add(Value1)
ArrayProp.Add(Value2)
ArrayProp.Add(Value3)
- Names
NameProp=‘Value‘
ORNameProp=Value
- Objects
ObjectProp=ObjectClass‘ObjectName‘
- Subobjects
Begin Object class="ObjectClass" Name=ObjectName
VarName=Value
...
End Object
ObjectProperty=ObjectName
- Structs (including Vectors):
StructProperty=(InnerStructPropertyA=Value1,InnerStructPropertyB=Value2)
ORStructProperty={( InnerStructPropertyA=Value1, InnerStructPropertyB=Value2 )}
注意: 某些类型当在一个struct默认值中使用时需要不同的语法。
- 内联静态数组必须像这样来声明(注意这里是使用括弧"[]"来作为数组的分解符,而不是使用圆括号"()")
StructProperty=(StaticArray[0]=Value,StaticArrayProp[1]=Value)
- 内联动态函数必须使用单独的一行语法来声明:
StructProperty=(DynamicArray=(Value,Value))
- 内敛名称变量必须有双引号括起:
StructProperty=(NameProperty="Value")
- 动态数组操作。这些操作可以用于修改动态数组的内容,并且该修改可以被它所属的函数或类所继承。
Array.Empty
- 清除整个数组Array.Add(element)
-在数组尾端添加元素Array.Remove(element)
-从数组中删除元素,这将移除所有出现该元素的地方。Array.RemoveIndex(index)
-删除指定索引的元素Array.Replace(elm1, elm2)
- 用elm2代替elm1。所有出现的地方都会被替换。如果没有找到elm1将会弹出一个警告。
Consider the following example (based on Actor.uc):
defaultproperties { //对象 MessageClass=class‘LocalMessage‘ //声明了一个SpriteComponent类的内联子对象,命名为"Sprite" Begin Object class="SpriteComponent" Name=Sprite //这里指定的值将覆盖SpriteComponent类的默认值。 Sprite=Texture2D‘EngineResources.S_Actor‘ HiddenGame=true End Object //进行操作 Components.Add(Sprite) // 声明一个CylinderComponent类名称为"CollisionCylinder"的内联子对象。 Begin Object class="CylinderComponent" Name=CollisionCylinder // 这里指定的值将覆盖CylinderComponent类自己的值 CollisionRadius=10 CollisionHeight=10 AlwaysLoadOnClient=True AlwaysLoadOnServer=True End Object //进行操作 Components.Add(CollisionCylinder) CollisionComponent=CollisionCylinder //行首的‘+‘和行尾的‘f‘字符将会被忽略) DrawScale=00001.000000 Mass=+00100.000000 NetPriority=00001.f //整型 NetUpdateFrequency=100 //枚举 Role=ROLE_Authority RemoteRole=ROLE_None // structs DrawScale3D=(X=1,Y=1,Z=1) //布尔型 bJustTeleported=true bMovable=true bHiddenEdGroup=false bReplicateMovement=true // names InitialState=None //动态数组(在这里是一个类的动态数组) SupportedEvents(0)=class‘SeqEvent_Touch‘ SupportedEvents(1)=class‘SeqEvent_UnTouch‘ SupportedEvents(2)=class‘SeqEvent_Destroyed‘ SupportedEvents(3)=class‘SeqEvent_TakeDamage‘ }
Struct默认值
当您在UnrealScript中声明一个struct时,您可以为struct的属性随意地指定默认值。任何时候在UnrealScript中使用这个struct时,它的成员函数将会使用这些值进行初始化。语法和一个类的defaultproperties(默认属性)块的语法相同 – 唯一的不同是这个语句块名称为 structdefaultproperties 。比如:
struct LinearColor { var() config float R, G, B, A; structdefaultproperties { A=1.f } };
任何时候当您在UnrealScript中声明一个LinearColor变量,它的 A 属性的值将被设置为1.f。当使用structdefaultproperties时需要理解的另一个重要事情是类的默认值会覆盖struct的默认值。如果您有一个LinearColor类型的类成员变量,那么您在类defaultproperties中分配给成员变量的任何值将会覆盖struct中的默认值。
var LinearColor NormalColor, DarkColor; defaultproperties { NormalColor=(R=1.f,B=1.f,G=1.f) // 这个属性的A的值将是1.0f DarkColor=(R=1.f,B=1.f,G=1.f,A=0.2f) //这个属性的A的值将是0.2f }
动态数组
先前我们所讲的数组是指静态数组。它意味着数组的大小(数组中有多少个元素)是在编译时设置的并且是不能改变的。动态数组和静态数组都有以下共同的特征:
- 不变的搜索时间 – 时间代码在访问任何数组中给定的元素所花费的时间是相同的,无论在一个数组中有多少个元素。
- 没有限制的元素类型 – 您可以创建任何类型的数组 - ints, vectors, Actors等(booleans除外,它仅对动态数组有效。)
- 访问方式 – 您可以使用数组中的索引来访问任何元素,反过来说,尝试访问一个在数组范围之外的一个索引的元素将会抛出一个accessed none警告信息。
为了适应数组元素变化的需要,动态数组提供了一种使静态数组的功能具有在运行时改变元素数量的能力的方法。如果想使用动态数组,我们需要指导几件事情。
首先是变量的声明。声明一个动态数组和声明其它的unrealscript变量是非常相似的(也就是var/local 类型 变量名)。对于动态数组,类型可以使用关键字 array 来指定,后面跟着由尖括号括起的数组类型。如果数组类型也包含尖括号(比如 class<Actor> ),你必须在类型的结束括号和数组的结束括号之间加入空格,否则编译器会把这两个结束括号作为 >>
操作符来处理。比如 声明一个叫IntList的动态整型数组: var array<int> IntList;
声明一个类型为class<PlayerController> 名称为 Players的动态数组: var array<class< PlayerController> > Players;。
当脚本开始运行时,IntList将从0元素开始。动态数组所支持的方法允许我们向数组中添加元素、取出元素及任意地增加或降低数组的长度。调用这些方法的语法是(使用我们的IntList为例子): IntList.MethodName()
。以下是动态数组提供的方法:
- Add(int Count):通过使用 Count 来扩展数组的长度,和FArray::AddZeroed()一样。
- Insert(int Index, int Count):_Index_是插入元素的索引位置, Count 是插入的元素数量。任何在那个位置存在的元素将会被上移,将会创建新的元素并把它们插入到指定的位置。在索引为3的位置插入5个元素将会使数组中从索引3开始的所有元素都会被上移5位。现在放在索引3出的元素将会被放置到索引8,元素4将是元素9等等。新增加的元素都被初始化为默认值(除了structs包含structdefaultproperties外,其它所有元素将被初始化为 0/空)。
- Remove(int Index, int Count):_Index_ 是指从何处开始移除元素的索引, Count 是要移除的元素的数量。这允许我们从数组中任何有效的索引开始来移除数组中的一组元素。注意任何高于将被删除的范围的索引将会改变它们的索引值,如果您存储索引值到一个动态数组中时请记住这一点。
- AddItem(Item):在数组的末尾增加一个 Item ,从而是数组的长度加1。
- RemoveItem(Item):使用线性搜索来删除任何 Item 的实例 。
- InsertItem(int Index, Item):将元素 Item 插入到数组中的索引 Index 处,从而是数组的长度加1。
- Find(...) - 查找一个元素在数组中的索引。有两个Find函数版本:标准的查找函数是匹配所有的元素值,另一个专用版本的查找函数是基于struct的一个单独属性值来匹配一个struct。
- Find(Value): Value 是要搜索的值。返回在数组中第一个匹配指定值的元素的索引,如果没有找到返回-1。_Value_ 可以是任何有效的表达式。
- Find(PropertyName, Value): PropertyName 是用于搜索的在struct中的属性名称(必须是‘Name‘类型), Value 是要搜索的值。返回在数组中第一个匹配指定的属性名 PropertyName 的值的struct的索引,如果没有找到,返回-1. Value 可以是任何有效的表达式。
- Sort(SortDelegate) - 使用 SortDelegate 来适当地对数组的内容排序,_SortDelegate_ 应该有和以下相匹配的声明:
- delegate int ExampleSort(ArrayType A, ArrayType B) { return A < B ? -1 : 0; } // 一个负数返回值预示着应该交换项。
长度变量
动态数组也有一个变量叫 Length
,它是指动态数组的当前长度(元素的个数)。 我们不仅能读取这个Length变量,并且我们可以直接设置它,从而允许我们修改数组中元素的数量。当您直接修改Length变量时,在数组长度中的所有改变都在数组的末端发生。比如,如果我们设置 IntList.Length = 5,然后我们在设置IntList.Length = 10,那么我们刚刚添加的额外的5个元素会添加到数组的末端,同时保持我们原始的5个元素及其它们的值。如果我们降低Length,元素也会被从数组的末端拿掉。注意当您想一个数组增加元素时,不管是通过Insert()函数还是通过增加长度,元素都会被初始化为它的变量类型的默认值(ints为0,类的引用为None等)。 值得注意的是您也可以通过设置元素的索引比当前的Length值大来增加动态数组的长度。这将和您设置Length的值为较大的值来扩展数组是一样的。
注意 – 动态数组的成员Length是不能通过‘++‘, ‘--‘, ‘+=‘, or ‘-=‘来进行增量/减量运算的,也不能将Length作为一个输出参数传递给一个函数(那个函数能够改变它的值)。由于这样做使Length不再是精确的,从而会导致内存泄露和崩溃;仅能通过使用 ‘=‘ 操作符来设置Length(并设置设置一个元素所在的索引大于Length)来适当地修改动态数组的实际长度。
注意:不支持array<bool>
类型!
最后的一个注意点 – 不能 复制动态数组。你可以通过使用一个函数来避开这个问题。这个函数是可以复制的函数并且有两个参数,一个是动态数组的一个索引及那个索引处所存储的值。然而,你也必须要考虑在客户端和服务器的一个时间间隔内元素不一样的后果。
迭代动态数组
动态数组现在支持 ‘foreach‘ 操作符来进行简单的迭代操作。基本语法是 ‘foreach ArrayVariable(out ArrayItem,optional out ItemIndex) {}‘,在那里每次迭代将会增加索引的值并输出那项,如果可选项的属性提供了,也输出它的索引。
function IterateThroughArray(array<string> SomeArray) { local string ArrayItem; local int Index; foreach SomeArray(ArrayItem) { `log("Array iterator test #1:"@ArrayItem); } foreach SomeArray(ArrayItem,Index) { `log("Array iterator test #2:"@[email protected]); } }
接口类
要获得关于接口的更多信息,请查看UnrealScript 接口页面。
函数Delegates
要获得关于如何使用Delegates的信息,请查看UnrealScript Delegates页面。
Native类
要获得关于native类的更多信息,请查看编译 Native类 和 创建 Native 类页面。
元数据支持
在游戏中(In-game)和在编辑器(in-editor)中的功能可以通过属性元数据来进行扩展。
元数据概要
在UnrealScript中任何一个元数据都可以按照以下方式连接到一个属性上:
变量:
var float MyVar<TAG=VALUE>
枚举型:
enum EMyEnum { EME_ValA<TAG=VALUE>, EME_ValB<TAG=VALUE>, };
For a class类:
请查看以下的UnProg3邮件列表链接:https://udn.epicgames.com/lists/showpost.PHP?id=24834&list=unprog3
使用多个元数据说明
您可以为同样的属性使用多个元数据说明,在多个说明间使用字符 | 作为分隔。
比如:
var() LinearColor DrawColor<DisplayName=Draw Color|EditCondition=bOverrideDrawColor>;
提供的元数据说明
这里是当前支持的元数据标签及它们的功能:
<ToolTip=TEXT_STRING>
在编辑器属性窗口中,当鼠标放到相应的属性上时,使 TEXT_STRING
作为工具提示出现。
注意: 最新添加了支持,脚本编辑器可以将 /** VALUE */
注释自动地翻译为ToolTip的元数据。
<DisplayName=TEXT_STRING>
使属性的名称在编辑器的属性窗口中显示为 TEXT_STRING
而不是它的真实名称。
举例:
Var() bool bEnableSpawning<DisplayName=Spawning Enabled>;
警告: 如果您在编辑器的复选框中修改UPropertyInputCombo 为排序枚举值,那么在枚举类型中使用DisplayName 将会引起问题
请查看一下的UnProg3 邮件列表链接:https://udn.epicgames.com/lists/showpost.php?list=unprog3&id=24302
<EditCondition=ConditionalPropertyName>
这允许您基于另一个属性(Boolean)的值来设置编辑器的属性的可编辑状态为启用的还是禁用的。
比如,您有在UnrealScript类MyPackage.MyClass中有一下设置:
/**启用或禁用 spawning*/ Var() bool bEnableSpawning; /**设置 AIs 的产生速率。只有设置bEnableSpawning = TRUE的时候才有效。 */ Var() float RespawnsPerSecond<EditCondition=bEnableSpawning>;
当bEnableSpawning为false时,在编辑器中的RespawnsPerSecond将被灰掉。这使事情对于设计者来说不再那么令人迷惑。
重要:这个原数据设置需要可控变量(RespawnsPerSecond)使用一个绑定(WxCustomPropertyItem_ConditionalItem)的自定义属性项。
为了启用它,你需要在Editor.ini加入它,如下所示:
[UnrealEd.CustomPropertyItemBindings] CustomPropertyClasses=(PropertyPathName=" MyPackage.MyClass: RespawnsPerSecond ",PropertyItemClassName="WxCustomPropertyItem_ConditionalItem")
要获得关于这个更多的信息,请查看https://udn.epicgames.com/lists/showpost.php?list=unprog3&id=30824
<FriendlyName=TEXT_STRING>
不确定这个是否仅在内部使用,并且并不希望您在 UNREALSCRIPT使用它,或许Epic可以澄清这个问题?
<AllowAbstract>
如果显示在一个类的属性上,则用于编辑那个属性的编辑器下拉框将会包含抽象类。如果不显示,它们将仅包含具体的类。在这个元数据说明中没有必要指定一个值比如True 或 False。
<AutoComment=BOOLEAN_VALUE>
当给Kismet序列动作(Kismet Sequence Action)添加一个属性时,属性及它的当前的值将会自动地作为注释出现动作的上方。如果想看到效果,可以在脚本中放置一个新的"Gate"序列动作,在这个类中的bOpen和AutoCloseCount都使用了这个元数据选项。
高级技术问题
UnrealScript的实现
要想获得更多的关于UnrealScript在底层是如何实现的信息 – 从Compile Process 编译过程到Execution执行到最终的Byte Code字节代码的展示 – 请查看UnrealScript Implementation UnrealScript的实现页面。
UnrealScript的二进制兼容问题
UnrealScript的设计可以在不破坏二进制兼容性的情况下使包文件中的类随着时间不断地扩展。这里所说的二进制数据兼容性是指“所依赖的二进制文件可以无误地进行加载并连接”;而您所修改的代码是否向您设计的那样工作是一个单独的问题。需要特别说明的是,可以安全地进行修改的种类如下所示:
- 在包中的.uc脚本文件可以在不破坏二进制兼容性的情况下重新编译。
- 增加新的类到包中。
- 增加新的函数到类中。
- 增加新的状态到类中。
- 增加新的变量到类中。
- 从类中删除私有变量。
其它的改变一般都是不安全的,包括(但不限于):
- 添加新的成员到struct中。
- 从一个包中移除一个类。
- 改变任何变量、函数参数或者返回值的类型。
- 改变函数中参数的个数。
技术注意事项
垃圾 回收. Unreal中的所有objects和actors都是用一个类似于Java VM的树形结构遍历的垃圾回收器进行垃圾回收。Unreal垃圾回收器使用UObject类的序列化函数来递归地确定每个活动的对象在引用哪些其它的对象。因此,对象不必明确地进行删除,因为当它们不被引用时,垃圾回收器最终会对它们进行回收。尽管这个方法对删除laten没有引用的对象有副面影响,但是它的效率远远地高于把那种很少发生删除的情况在内的引用算在内。
UnrealScript 是 基于 字节代码的. UnrealScript代码编译成为一系列类似于p-code(移植码)或Java字节代码。这使UnrealScript具有平台独立性,这可以使Unreal的客户端和服务器端组件直接地移植到其它的平台上,也就是Macintosh或Unix,并且所有的版本通过执行相同的脚本都可以很容易地进行交互操作。
Unreal as a 虚拟 机. 虚幻引擎可以被看成一个3D游戏方面的虚拟机,就像Java语言和它的内置 Java类层次结构为网页开发脚本定义了一个虚拟机一样。Unreal虚拟机天生具有可移植性(由于在不同的模块中分离出了所有的平台独立代码)和可扩展性(由于可扩展的类的层次结构)。然而,目前我们没有想过要把Unreal VM文档化到那种足以让其它人可以用来创建独立的但兼容的实现的程度。
The UnrealScript 编译器 是 需要编译三遍.不像C++, UnrealScript编译要进行独立的三遍,第一遍,对变量、struct、枚举型、常量、状态及函数声明进行分析和记忆;构建了每个类的概要。在第二遍,将脚本代码编译为字节代码。这使得在这两遍中对带有循环依赖的复杂的脚本层次进行完全地编译和连接,没有独立的连接阶段。第三个阶段使用在.uc文件的 defaultproperties
语句块指定的值来分析并导入类的默认属性
持久的 actor 状态. 在Unreal中值得重点注意的是,因为用户可以在任何时间内保存游戏,所有actors的状态包括它们的脚本执行状态仅能在当所有的actors都处在UnrealScript栈的最低的层次时才能进行保存。这个持久的要求是latent函数仅能从状态代码中进行调用的背后的原因:状态代码在栈中尽可能低的层次上执行,因此可以非常容易被序列化。函数代码可以存在于任何栈级别,并在可以使(比如)C++ 的native函数在栈中处于它的下面,那显然不能保存到硬盘并在稍后进行恢复。
*Unrealfiles(Unreal文件)是Unreal的本地二进制文件 *. Unrealfiles包括一个索引、特定Unreal包内objects的序列化存储。Unrealfiles类似于DLL文件,它们可以包含到其它Unrealfiles存储的其它objects的引用。这个方法使在因特网上通过预先确定的包来发布Unreal内容成为可能,从而节约了下载时间(通过永远只能对一个特定的包进行一次下载)。
为什么 UnrealScript 不 支持 静态 变量. C++支持静态变量的很好的原因是忠实于低层次语言的根源,Java支持静态变量的原因似乎并没有彻底地想好,在UnrealScript中不支持静态变量是由于它们的作用范围在序列化、出处及多个关卡方面的含糊性:静态变量是否需要具有全局性,意味着所有的静态变量的值在所有活动的Unreal关卡中是一样的?它们是否在每一个包中?它们是否应该在每个关卡中?如果这样,那么它们如何进行序列化哪 – 使用在它的.u文件中的类还是使用它.unr文件中的关卡?它们在每个基类中都是唯一的或者子类是否有他们自己的静态变量的值?在UnrealScript中,我们通过不定义静态变量作为一个语言功能来回避了这个问题,把它留给了程序员来管理,程序员可以通过创建包含它们的类或在一个真实的对象中暴露它们来管理它们像静态变量还是像全局变量。
UnrealScript编程策略
这里我们描述了关于如何更高效地书写UnrealScript代码的几个主题,使我们可以充分地利用UnrealScript优势而避免它的缺陷。
- 一个典型的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列表进行编译的,所以每个包仅能引用本包中及在它前面编译的包中的物体,而不能引用在它后面编译的包中的物体。如果您发现需要在包之间进行循环引用,有两个解决方案:
-
- 把类分成需要在第一个.u包中进行编译的一组基类包和一组需要在第二个.u包中进行编译的子类包,要保证基类永远不会引用子类。这是一个很好的编程实践,并且它通常是有效的。 注意如果一个给定的类C需要参考在后面编译的包中的一个类或对象O,您通常可以把那个类分成两部分:在第一个包中定义一个抽象基类C,它定义了一个变量MyO(但在它的默认属性中不要给MyO设定默认值);在第二个包中定义一个子类D,它为MyO指定了适当的默认值,而且仅能在第二个包中来进行默认值的指定操作。
- 如果两个.u包由于相互引用而彻底的纠缠在一起,那么可以把它们融合到一个包中。这是合理的,因为包是作为代码模块化的一个单元来设计的并且把这些不能分开的一组类分别放到多个包中不能带来任何实质性的好处(比如节约内存)。