UDN
Search public documentation:

UT3ModsCH
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 主页 > Mod 主页 > 虚幻竞技场 3 Mod 主页 > 虚幻竞技场 3 的 Mod 制作

虚幻竞技场 3 的 Mod 制作


简介

开发关于虚幻引擎的mod是回报非常丰厚的工作。在当前游戏产业中,近乎没有更好的方法允许技术精湛的程序员或者拥有抱负的美术工作人员展示它们可以创建的世界。本文档的创建目的在于对mod创建感兴趣人员提供他们成功创建虚幻引擎游戏修改所需要的信息。它将会包括技术性信息以及与 mod 开发相关的建议。如果您努力学习并进行创作,那么您便可以通过mod创建制作出一些有趣的东西。

开始前的一些忠告

当您开始开发mod时,您应该从小的功能开始。不要打算一开始就写出 Total Conversion(TC,完全转换)。如果您设计的目标太难以至于不能完成,那么您在朝向这个目标进行努力的过程中变得沮丧;并且您可能永远都不能把您的愿景想法变为现实。设置一系列的小目标并不断地攻克每一个是个很好的主意。首先从一个可以扩展为较大游戏的简单的想法开始。总是从较小的可管理的并且其本身可以发行的模块上进行开发是很好的。如果您正在承担一个大的项目,那么请把您的功能组织划分为发布模块并制定时间表。如果您的游戏将要有5个武器,那么制作完2个或3个后,在制作剩余武器的过程中您可以先发行已经制作好的2、3个武器。在开发中需要控制您的节奏并做出长远的规划。

每个人都有一些创意想法。每个人都认为它们有一个革命性的新游戏概念,并且认为这些概念别人从来没有想到过。具有酷的创意很少能把您带到游戏产业的任何地方。因为您必须可以实现你的想法或者提供一些有用的技术, 这个事实也适用于mod创建。如果您成为了一个技艺精湛或著名的mod创建者,那么您会找到那些需要您为其实现它们的创意的人。永远不要加入项目的创意者或领导没有明显的开发技术的项目; 永远不要加入一个仅有网站设计人员的项目。您有您自己的思想, 认真地从较小的模块开始致力于开发它们,最终您将可以开发一个很棒的项目。

请记住开发一个mod后,如果您不发行它,那么它没有太多意义。权衡您的任务列表,以便随着您的mod的不断成熟,您可以快速地发行一些东西,以及添加和改进一些功能。如果您在您的项目的所有东西都变得完美之前隐藏您的项目,那么您会发现最终您自己没有发布任何东西。

一旦您有了您的创意,您需要选择哪种mod类型比较适合您。决定之后,您可以设立所有东西,并开始把您的想法变为现实。

创建Mod项目

现在是开始的动手操作的时候了。首先,您应该知道虚幻引擎游戏一般是如何安装的。之后,您应该知道您的mod适合放在游戏的哪个地方,以及知道一些虚幻引擎处理内容的方式 – 包括最初的和新的方式。然后,根据您的方法,您可以开始建立所有的东西,并开始创建您的mod了。

目录结构

默认情况下,虚幻竞技场3将会安装在以下目录中:

  <InstallDrive>:\Program Files\Unreal Tournament 3
  

另外,将会为每个用户存储用户设置:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\
  

运行及测试

可以通过点从开始菜单中点击桌面图标或者通过以下命令来运行游戏:

  UTGame
  

可以通过从开始菜单或者以下命令来运行游戏编辑器:

  UTGame editor
  

有用的控制台命令

help - 提供与给定的命令相关的提示信息。有用的参数包括 list

  UTGame help [command]
  

run - 运行命令行开关。如果您有一个名称为TestCommandlet的命令行开关,您可以仅使用 Test ,命令行将会附加 Commandlet

  UTGame run [commandlet name]
  

现在,开始设置虚幻竞技场来创建您的项目。首先,您需要理解UnrealScript如何使用包。

包是游戏资源的集合。资源是任何类型的: 贴图、模型、动画声效、音乐、UI页面。对于所有的资源来说,包的格式是一样的,并且多个资源类型可以混合在一个包中。为了便于管理,虚幻竞技场会将包按照资源分类。textures 目录包含贴图包、sounds 目录包含声效包等以此类推。即使这些包可能包含了不同类型的内容,但是它们都具有相同的文件扩展名(.upk),并且它们是同种文件。

程序员将会使用UnrealScript进行工作,并处理编译后的游戏代码包(.u)文件。代码包主要包含了编译后的UnrealScript ,但是也可以包含代码所依赖的贴图和声效。

请参照虚幻包页面获得更多信息。

编程

当您进行入门学习时,您需要自己进行很多的搜索。如果您花时间使自己熟悉引擎的架构和提供给您的代码,那么您便会成为一个优秀的UrealScript程序员。学习一些解决和您的mod设计目标相关的问题的创新方法并最大化地利用提供的工具是有用的。

UnrealScript

UnrealScript是一种面向对象语言(OO)。如果您对面向对象不熟悉,现在最好顺便学习一下,阅读一些和面向对象编程相关的资料: http://www.orangesmoothie.org/tuts/GM-OOtutorial.html。这篇文档很旧,但是仍然是个非常好的资源。

因为UnrealScript是面向对象的,您将不能编辑任何原始的源代码。这或许和其它的游戏引擎是不同的。 在虚幻引擎中,您将需要继承随同虚幻竞技场一起发行的类来创建子类,并重载或扩展它们来满足您的需要。

有关UnrealScript的完整参考信息,请参照UnrealScript参考指南页面。

UnrealScript 源码文件

您可以在这里下载最新的UnrealScript源文件: http://udn.epicgames.com/Files/UT3/Mods/UT3ScriptSource_1.5.rar

以前的版本:

请注意,随着补丁的发行,将会制作和每个补丁更新相匹配的源码更新。 这些更新在补丁发行几天后便可以使用。前一个版本将进行存档。

您的mod项目所使用的UnrealScript源代码将会被放置在以下位置:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\Src\YourModName\Classes
  

注意 : 可能需要创建Src\YourModName\Classes目录结构。

在编译UnrealScript 源码之前,您必须首先通过打开 UTEditor.ini并在 [ModPackages] 部分添加以下行文字来把您的脚本mod名称注册到引擎中:

  ModPackages=YourModName
  

游戏的UnrealScript编译器可以通过下面的命令运行:

  UTGame make
  

... 或者用以下命令进行完全的重新编译:

  UTGame make -full
  

注意 : 永远不要修改核心的UT UnrealScript类!

编译后的UnrealScript包将会被放置在以下位置:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\Unpublished\CookedPC\Script\YourModName.u
  

注意 : 可能需要创建Script目录。

将会为mod包自动创建配置文件,它位于以下位置:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\Config\UTYourModName.ini
  

注意:配置文件生成仅检测UT*类的子类,因为仅有这些类使用bExportMenuData 标志来进行控制。如果您的mod不是以 UT 前缀开始的,那么它将为您添加改前缀。如果您的mod没有任何 UT* 类的子类,那么您必须手动地创建一个配置文件。

当您打开编辑器时,将会在编辑器启动过程中自动地加载 UTEditor.ini文件的 ModPackages 列表中给出的任何mod包,所以任何 可放置的 Actor类都将会自动地出现在Actor 浏览器中。要想禁用这个行为,请在运行编辑器时使用 =-nomodautoload 标志。

请记住,您或许需要花费大量的时间来为您的游戏书写大量的UnrealScript源代码。但您应该跟踪执行路径并查看各种类的继承关系及它们是如何交互的。最初可能比较繁琐,但是随着时间的推移,您会变得更加熟悉各种东西分别位于哪里以及它们的作用。请不要害怕通过网络在社区邮件列表及论坛中提问问题。如果您花费了必要的时间去学习,那么您将会具备承担更大更加复杂的项目的能力。

内容创建

关卡

当使用游戏编辑器时,关卡保存在以下目录中:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\Unpublished\CookedPC\CustomMaps
  

为了是您的关卡在游戏菜单中可见,您需要把它保存到CustomMaps的子目录中。直接保存到CustomMaps目录本身的任何东西都将被忽略。所以,您将需要创建一个和您的mod名称相是对应的目录。

请记住当保存关卡时要使用适当的游戏类型前缀(比如 DM-和VCTF-等)。

默认情况下,游戏在 Published 目录中查找自定义的内容。您有两个选项:

  1. 通过使用 =UT3.exe –useunpublished 命令告诉游戏对 Unpublished 目录进行检测。 这对于在开发过程中获得最快的周期是最有利的。
  2. 使用编辑器中的 Publish 按钮;这将会复制Unpublished文件夹中的内容到Published目录中,以便游戏可以自动地检测它。

当您第一次运行游戏时,它将会在 CustomMaps 文件夹的子目录中检测新的关卡的存在性,然后自动生成基于每个关卡的配置文件(.ini),来把关卡注册到游戏中的UI上。您可以编辑这些文件来提供关卡描述文本等。

如果您通过Unreal Front End(Unreal前端)来烘焙您的mod,请确保选中了‘Cook Mod(烘焙Mod)’选项。

发布及烘焙

CookedPC 的命名规则看上去可能有点令人迷惑,但是一旦正确地设立任何东西后这便是一个相对简单的过程。 Cooking 从地图中烘焙出所有不必要的内容,创建用于正确地加载并运行您的地图所需要的着色器缓存信息,并改进性能和加载时间。 Publishing 设置所有的文件以便进行发布,并把所有的东西放到适当的目录中,以便其他人可以理解您创建的东西。

关于 PublishedUnpublished 的行为: 默认情况下,保存在游戏编辑器中的内会被写到 Unpublished 目录中。这是mod创建者的工作目录,您可以在这里找到:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\
  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\Script\
  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\CustomMaps\
  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\CustomChars\
  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\Localization\<LANGUAGE>\
  

Published 目录放在这里:

  …\Documents and Settings\<UserID>\My Documents\My Games\Unreal Tournament 3\UTGame\Published\CookedPC\
  

在编辑器中保存游戏应该创建 UnPublished 文件夹。发布文件应该创建 Published 文件夹。如果这些文件夹中的任何一个都不存在,那么您需要手动地创建它们。当您现在一个地图并想播放它时,把它放在 Published 文件夹中,然后游戏将会对其进行组织,并使它显示在菜单中。注意,游戏将不会组织没有处于正确的目录中的文件,并且将不会正确地加载它们使它们显示在菜单上。

如果没有在启动游戏时指定了 -useunpublished 标记,那么游戏一般从 Published 目录中运行内容。典型的mod工作流程是在脚本和地图上进行迭代开发,并使用 -useunpublished 运行游戏。当mod建立好后,最终的 Publish 将会把烘焙好的内容写到 Published 目录中,并且这些进行了加载优化的文件将会用于发布。对于脚本和本地化文件来说,则必须手动地复制它们。

可替换地,当内容发生改变时,mod创建者通过执行 Publish 命令便可以不使用 -useunpublished 标志来运行它们正在开发的内容。通过这种方法,未烘焙的mod内容将会被复制到 Unpublished 目录,在那里游戏将会自动地识别它。正如上面所说的,我们推荐在发布mod之前才使用最终的 Publish 命令。

总结,=-useunpublished= 是为mod开发人员提供的快捷方式,使它们在工作过程中不必不断地复制文件。

本地化

您的mod的所有本地化文件都将会被创建并防止在以下目录中:

  <InstallDrive>:\Documents and Settings\<userID>\My Documents\My Games\Unreal Tournament 3\UTGame\UnPublished\CookedPC\Localization\<LANGUAGE>\YourModName.int
  

发布

先前,已经使用虚幻引擎的 master 命令行开关进行打包并创建mod安装文件(UT的安装文件扩展名是.umod、UT2003和UT2004的是.ut2mod)。现在mod将会通过使用类似于WinRAR的工具将其简单地打包到一个压缩存档中。

命名规则

为了保持一致性和可维护性,在项目开发的早期建立一些命名规则和类似于编码规则的最佳实践是非常明智的。这将会保持您和您的团队成员保持一致性。

对于编程来说,我们推荐以下命名规则:

  • UTMutator_ - UT3 的 Mutator 类
  • UTGame_ - UT3 的 GameType 类
  • UTGameRules_ - UT3 的 GameRules 类(被您的 Mutator 或 GameType 所使用)
  • UTMod_ - 其他的UT3 mods;这对于包及相关的目录也是有好处的。

Total Conversions(完全转换)的命名规则可能会和上面的规定相背离,因为它们的项目可能和UT没有任何的关系,尽管仍然认为它是UT mod。

当命名您的包和相关目录时,请考虑一个和您的创意想法相一致的命名规则;但是不限于类文件的名称。比如,如果您正在制作一个称为 Jailbreak 的新的GameType ,那么您可以把包及相关目录命名为UTMod_Jailbreak或者Jailbreak,而不是核心游戏类型类的名称(比如UTGame_Jailbreak)。如果您正在创建一组mutator(设置器),请考虑把包及相关目录命名为UTMod_MyMutators 或MyMutators。

控制台游戏平台

PlayStation 3 Mod 支持

请参照PS3 Mods页面获得关于为PlayStation 3控制台游戏平台制作用户自定义的内容的更多信息。

四种Mod类型

Mutators(设置器)

Mutators 是迷你-mods。它们具有Mutator类定义的有限的功能。Mutators应该遵循一些规则 – 即GameType定义的那些规则。如果不能遵守这些规则或这这些规则的限制太多,那么您或许应该制作GameType mod。

第一个规则是这个Mutator必须可以和其它任何Mutator协同工作 – 特别是游戏中自带的mutator。如果您书写了一个允许玩家耗光他所射击的敌人的生命的"Vampire(吸血鬼)" Mutator,那么如果把这个Mutator和Arena mutators及No PowerupsMutator结合使用时它也应该可以正常工作。这是Mutator系统的其中一个有用的功能。它们稍微低改变(或 变异 )了一下游戏性,允许进行有趣的组合。

第二个规则是,Mutator应该仅稍微地改变一下游戏性。尽管这样说是一个模糊的概念,但是您需要尝试限定您的Mutator的行为。认真地设计Mutator将会提高它和其它mods协同工作的几率,并且将会降低您的进行支持所花费的经历。

第三个规则是那个Mutator应该和其它的Mutator共享资源。如果您的Mutator实现了 ModifyPlayer 函数,那么您需要在您的函数版本的某处调用 NextMutator.ModifyPlayer 。这将确保Mutator列表中处于您的mod后面的任何Mutator都将可以处理那个函数调用。如果不能实现这个功能,那么这将是比较低劣的编程风格,同时它也不具有团体友好性。

GameTypes(游戏类型)

GameTypes 是mod的比较大的类。它可以做任何Mutator不能做的事情,并且允许您访问大量的功能。如果您的想法在Mutator中不能实现,那么您已使用GameType。

GameType的缺点是它不能和其它的GameTypes相混合。比如,Capture the Flag(占领旗帜)是虚幻竞技场中的一种游戏类型。它是一种和Deathmatch (另一种游戏类型)游戏性完全不同的一种游戏类型。

GameTypes是作为游戏的GameInfo 类的子类来实现的。对于GameTypes来说没有任何特殊的规则,除了需要注意的客户端-服务器问题(我们将在稍后进行讨论)。

Total Conversions(完全变换)

Total Conversions (TC) 是从引擎的核心开始,完全地不使用已经建立的游戏类来创建mod。TC是通过作为基础引擎类的子类来实现的,而不是使用针对特定游戏的子类,比如GameInfo 类。如果您的mod不在那个游戏世界的范围之内,或者如果您想尝试一些完全不同的东西,那么您可以选择TC方式。需要注意的是这样您将需要从头开始进行创建。

自定义内容

根据不同的GameType(游戏类型),某些mod可能需要很多新的元素,比如AI或者和游戏规则本身相分离的特殊Actors。通过GameInfo或Mutator类可以创建不改变的游戏性的mods。这些包括 Player Plugin Models (PPM)(玩家内置模型)、新的武器或新的车辆。

当然,关卡是创建新内容而又不需要书写任何代码的最好方式。您可以简单地塑造出一个新的关卡,使用游戏中自带的现有资源装饰它;或者您可以创建您的自己的贴图及模型(静态网格物体)!

关于设计多玩家关卡的更多信息,请参照 多玩家地图理论(UT3)页面获得更多信息。

如何制作Mutator

Mutators 是您获得关于UnrealScript经验的最好的地方,因为它为您暴露有限但强大的引擎的子集。正如我前面提到的,Mutator应该仅以相对较小的方式修改游戏代码。这增加了您的mutator和其它mutator混合使用时正常工作的几率。(比如,您可以玩FatBoy, InstaGib, No Powerups 的死亡竞技。这是3个mutator的混合物)。

让我们看一个非常简单的Mutator mod:

  class ExampleMutator extends Mutator;
  
  defaultproperties
  {
  }
  

配置

默认情况下,将会为您的包制作一个配置文件,它指向您的mutator。以下是相关信息:

  FriendlyName=Example Mutator
  Description=An Example Mutator
  

为了使得新的mutator正确地显示在菜单中,需要对其配置进行一些修改。

在Instant Action(即时战斗)和Multiplayer (多玩家)菜单中查看所有配置文件来查找UT 类的子类。如果您把以这些文本添加到您的包的本地化文本文件中,您将会在mutator列表中获得一个称为"Example Mutator"的元素。您将需要在您的mod包的本地化文本文件中提供文本,如下所示:

  ;///////////////
  ; Mutator Names
  ;///////////////
  [ExampleMutator UIDataProvider_Mutator]
  FriendlyName=Example Mutator
  Description=An Example Mutator.
  
  

某些mods也可以在游戏菜单中进行配置。为了完成这个操作,您必须创建一个UI页面并把它保存在包中,然后通过 UIConfig 变量引用那个包。这个已通过Mutator类的 defaultproperties 来完成。

这里是个示例:

  defaultproperties
  {
     UIConfigScene=UIScenes_Example.Menus.ExampleMutatorConfig
  }
  

通过一个简单的Mutator,我们看一下Mutator中可用的几个方法。

Mutator剖析 – 第一步

现在您已经学习了书写一个简单的UT mod。但是很明显,这不足以震惊世界或者在游戏行业获得一份工作。所以进一步查看Mutator基类内部的方法是值得的。这将会是您更好地理解您可以使用它们完成哪些功能。因为您具有使用多个继承函数和同其它对象进行交互的能力,所以它仅提供了简单的功能。

Mutate 让您的mutator定义玩家可以将其绑定到键盘上的新命令。如果一个玩家把 mutate givehealth 绑定到一个按键上,那么当使用那个按键时,每个mutator都使用 givehealth 参数调用mutate函数。您的mutator可以查找这个字符串,并给予发送这个信息的玩家一些额外的生命值。

ModifyLogin

无论何时当重新生成一个Pawn时游戏将会调用 ModifyPlayer 。它允许您可以修改Pawn的变量或者在Pawn上执行一些游戏逻辑。如果您重载了这个函数,请记得调用 Super.ModifyPlayer() 函数。那将会调用这个函数的父类版本。

AlwaysKeep 允许您阻断游戏将向世界中添加的对象。当它们出现时,您可以使用其它对象代替运行中的对象。虚幻竞技场的Arena mutator便是这种实例。它们获取游戏中的所有武器,然后使用另一种武器来替换它们。如果您正在向游戏中添加新的武器,那么您应该为它添加 Arena mutator。

当游戏想向世界中添加对象时调用 IsRelevant 。您可以使用特殊的代码来重载它,如果返回真则保存对象,否则如果返回假则拒绝对象。如果您让返回值为假,那么那个对象将会被销毁。

CheckRelevance

CheckReplacement

第二步: 游戏规则

从底层来讲,Mutator类具有一些事件,并且可以通过修改它们来对mutator进行调整。当然,您可能会立即体会到一些限制,比如得分技术或影响伤害。这时便是Game Rules有用的地方。每个GameInfo 游戏子类有一系列的Games Rules(游戏规则),我们称它为GameRulesModifiers。您可以创建GameRules的子类,来修改某些游戏事件的处理方式。

FindPlayerStart

HandleRestartGame

CheckEndGame

OverridePickupQuery

PreventDeath

ScoreObjective

ScoreKill

NetDamage

进一步讲,您的mutator甚至可以向HUD描画信息。可能通过游戏类型来实现这个功能时则有更多的灵活性,但那些有限的功能对于Mutator来说已经足够了。简单地创建一个复制的Actor子类,并把该子类本身添加到 UTHud 类的 PostRenderedActors 数组中。

通过使用这个信息,您应该可以使用几个类来制作较大的改变了。学习如何最大化mutator的功能的最好方法是阅读随同UT一同发行的mutator中的代码。

GameTypes(游戏类型)简介

Mutators可以做很多超酷的东西。它们是非常容易理解的,并且它们通过和某些游戏事件及规则进行交互可以做很多事情。您可以把很多mutator混合到一起来做获得更酷的效果;但是它们并不是非常地强大。如果您想制作一种新类型的游戏(比如ailbreak 或 Rocket Arena类型的mod),您则不能使用mutator实现它。您需要完全地控制所有的游戏规则。 这便是类的GameInfo起作用的地方。

GameInfo是引擎中的一个类。它是由游戏引擎创建的,并且它是游戏性规则的核心。虚幻竞技场使用了一系列的GameInfo子类,它们位于UTGame包中。UTGame类包含了对所有虚幻竞技场游戏类型都通用的代码。UTDeathmatch 包含了运行正常的死亡竞技所需的代码。UTTeamGame 包含了团队死亡竞技的代码和一般团队管理代码。UTCTFGame是UTTeamGame的子类,实现了它的特殊的游戏类型。

书写您的新的游戏类型的第一步要决定的是继承哪个类。如果您正在书写一个团队游戏,您或许想继承UTTeamGame类。如果您正在书写一个不需要组队的游戏,那么可以继承UTDeathmatch类。如果您正在书写一个和前面所提到的游戏类型相差甚远的游戏(但是该游戏存在于虚幻竞技场游戏中),那么请使用UTGame。将您的游戏类型设置为这些已有游戏类型的子类是有好处的: 您可以立即继承父类的所有代码,然后您可以扩展或重载它们来达到您的设计目标。

当然如果您正在书写一个Total Conversion (TC)(完全转变)的游戏 – 及该游戏和UT游戏完全地背离 – 那么您应该继承GameInfo类。

看一个简单的游戏类型mod:

  class ExampleGameType extends UTDeathMatch;
  defaultproperties
  {
     Acronym="EX"
     MapPrefixes[0]="EX"
     Description="Example GameType"
  }
  

在将上面的代码保存到名称为 ExampleGameType.uc文件中时,它便是一个新的GameType的外壳。这里唯一的不同是我们把描述改为了 "Example GameType."。这个新的名称将会在很多地方反应出来: Practice Session(练习过程)选项窗口,Scoreboard (等分牌)的标头等等。如果您玩一下这个游戏,您会发现它和任何其它的Deathmatch游戏一样,因为没有实现任何新的行为。

配置

和Mutator类似,将会为您的包创建一个配置文件,它包含了到您的游戏类型的引用。以下是相关信息:

  FriendlyName=Example GameType
  Description=An Example GameType
  

和Mutator类似,为了使新的游戏类型正确地显示在菜单中,必须对某些配置进行修改。您将需要在您的mod包的本地化文本文件中提供文本,如下所示:

  ;///////////////
  ; Game Modes
  ;///////////////
  [ExampleGameType]
  Description=An Example GameType.
  GameName=ExampleGameType
  EndOfMatchRulesTemplateStr_Scoring=First one wins!
  EndOfMatchRulesTemplateStr_Time=Most wins!
  
  
  ;///////////////
  ; GameType Names
  ;///////////////
  [ExampleGameType UIDataProvider_GameModeInfo]
  FriendlyName=Example GameType
  Description=An Example GameType.
  
  

Instant Action(实时动作)和Multiplayer (多玩家)启动菜单查看所有的本地化文件来查找名称和具有本地化的字符串的类的相关部分名称一样包。如果您把这些行添加到您的包的本地化列表中,您将会在游戏列表中获得一个称为"Example GameType"的入口。这个名称是从您的GameInfo类的FriendlyName变量获得的。Description(描述)行为您的游戏类型在菜单上提供一行描述性的文本。现在您不用管它。

现在通过这个简单的GameType(游戏类型),我们可以看几个GameInfo可用的方法。

初步了解GameInfo

引擎的GameInfo类定义了基本的游戏逻辑。在文件的顶部,您将会看到一个很长的变量声明列表。 大部分变量都有注释来描述它们的作用。在变量声明的下面是完成处理工作的函数(或方法)。

第一个要看的东西是 Timer 函数。在GameInfo中,它是非常短的,但是在UTDeathmatch它却是非常长的。=Timer= 是一个特殊的UnrealScript事件。如果您调用函数 SetTimer(int Time, bool bLoop) ,您将可在您的Actor上设置反复的计时器。Time参数描述了什么时候调用Timer函数。bLoop参数描述是否应该在第一次调用后重复地调用Timer函数。*所有的UTGame 类都使用循环时间为1秒的计时器。* 这意味着将每隔一秒调用一次Timer函数。您可以为那些必须在某个时刻发生的事件使用Timer函数。通过声明watch变量来累计秒数,当达到1秒中时您可以执行任何事件。UTDeathmatch在游戏中使用这个功能来检测并查看是否已经达到了TimeLimit(时间限制)。

另一个需要知道的重要的时间函数是 Tick 。GameInfo 不会使用Tick函数,但是任何Actor都可以使用它。 Tick 函数的声明是: Tick(float DeltaTime) 。游戏的每帧中都会在每个Actor上调用 Tick 函数。DeltaTime包含了自从上一次tick(更新)所过去的时间量。通过使用 Tick 函数,您可以执行任何需要在小于1秒的时间内需要执行的行为。您必须注意不要在 Tick 函数中执行占用很多CPU的行为,因为将会经常地调用它。

接下来GameInfo中的另一个函数是 Login 函数。无论何时当玩家登录游戏时,引擎将会调用这个函数。GameInfo的login函数版本做很多重要的设置工作,比如为玩家分配名称、皮肤、网格物体等。它也产生初始的电传效果,并可以找到产生玩家的点,把玩家放在那里。在 Login 之后还有一个函数 Logout 。无论何时当玩家离开游戏时则调用这个函数。 当玩家退出后,您可以使用logout来清理数据。

GameInfo中另一个有趣的函数是 AddDefaultInventory 。这个函数为玩家分配初始武器和装备。在UTDeathmatch中,会给玩家一个ImpactHammer和一个Enforcer。您可以使用AddDefaultInventory来给加入到您的mod的玩家添加自定义的武器(例如,您可能希望给他们一个手榴弹和一些钱)。

FindPlayerStart 方法在关卡中搜索actors来查找适合产生NavigationPoints的地方。关卡设计人员添加到他们关卡中的PlayerStart Actor便是这样的一个位置。在UTTeamGame中, FindPlayerStart 根据它们的Team(团队)来产生玩家和机器人。它检查每个PlayerStart的Team(团队)和每个将要产生的Pawn的Team(团队)。您可以使用 FindPlayerStart 来书写自定义的生成代码(比如,您或许想在一个位置产生Opposing Forces(反对力量)而在另一个位置产生有好的盟友)。

无论何时当玩家重新产生时调用 RestartPlayer 这个函数。基本的GameInfo版本函数调用 FindPlayerStart 来查找起始位置,把玩家移动到那个位置处,并产生电传效果。它也会存储玩家的生命值,设置玩家的碰撞,并为玩家提供默认的武器。

Killed 方法是非常有用的。无论何时当一个玩家杀死另一个玩家时将会调用这个函数。它查看死亡的情形(玩家是自毁或者是被别人成功地杀害)、伤害类型并输出一个信息。最后,它调用 ScoreKill 函数。

ScoreKill 提供杀掉其他玩家的奖励分数。UTDeathmatch为成功的杀害分配一个旗帜,从自毁的玩家那减少一个旗帜。UTTeamGame同时会给Killer(杀人者)团队的TeamInfo添加分数,或者从自毁者的团队中减少分数。

无论何时当玩家从游戏中死亡或被删除时都会调用这个函数 DiscardInventory 。=DiscardInventory= 检查Pawn的所有武器,然后根据需要丢弃武器或者销毁武器。如果您想丢弃一个背包,那么您可以重载这个函数。

最后无论何时由于任何原因导致游戏结束时调用 EndGame 函数。您可以在这里执行特殊的日志记录或清理数据操作。

到此为止,我们已经快速地熟悉了比较重要的GameInfo函数。高级的GameInfo类,比如UTDeathmatch,添加了重要的新的行为来控制机器人和单玩家游戏,同时它也精练了GameInfo的方法,使它可以提供一些特殊的规则。

再次说明,学习如何使用您的GameType的最好的方法是阅读随同UT一同发行的游戏类型中的代码。

游戏规则

Heads Up Display (HUD)

Scoreboard(记分牌)

AI

如何制作自定义的内容

角色

请参阅创建UT3的自定义角色页面了解更多信息。

关卡

再次说明,为了使得新的关卡正确地显示在菜单中,必须对配置文件进行一些修改。您将需要在您的关卡的本地化文件中提供文本,如下所示:

  [DM-Example UIDataProvider_MapInfo]
  FriendlyName=<Strings:UTGameUI.FriendlyMapNames.DM-Example>
  NumPlayers=4 to 8 players
  
  

拾取物和武器

即将面世。

载具

即将面世。

注意事项

这里是当mod开发人员书写虚幻竞技场的mod时所遇到的一些主要“问题”。如果您还没有很多的使用引擎的经验,那么这些问题可能和你没有什么关系。学写一个新的游戏技术是一种非常酷的体验,但也是非常艰难。这里的一些指导可以使您在学习过程中变得更加轻松。

通常,需要考虑您书写的代码的性能实现。

Accessed Nones(访问值为空)

迟早这些信息都会出现在您的日志文件中。UnrealScript把Accessed Nones (访问值为空)当做警告处理,但是您应该把它们作为错误处理。Accessed Nones (访问值为空)是个很容易修复的问题,并且它通常也是您的代码中某些地方出现错误的标志。如果您熟悉C++ 或Java,那么您很容易就理解Accessed Nones (访问值为空)是什么意思。对于那些不熟悉这个概念的人,这里是一个简单的介绍…

UnrealScript是一个面向对象的语言。当您使用UnrealScript书写代码时,您为这些对象定义了他们必须遵守的、控制它们如何在游戏中交互的行为。一个对象有一组属性: 成员变量和成员函数。为了访问对象的属性,您需要一个到那个对象的引用。

这里是一些样本代码:

  class ExampleInfo extends Info;
  
  var PlayerReplicationInfo PlayerInfo;
  
  function PlayerReplicationInfo GetPlayerInfo()
  {
     return PlayerInfo;
  }
  

这里我们有一个名称为ExampleInfo的简单对象,它是Info的子类。它有两个属性: 一个是称为PlayerInfo的变量和一个称为GetPlayerInfo()的函数。您或许想在您的mod中和这个对象进行交互。假设您的mod中有一个到ExampleInfo对象的引用,并且您想从PlayerInfo属性中获得一些信息。您应该这样书写代码: class MyMod expands TournamentGameInfo;

  function string GetPlayerName()
  {
     local ExampleInfo TheInfo;
     local string PlayerName;
  
     TheInfo = GetMyObject();
     PlayerName = TheInfo.PlayerInfo.PlayerName;
     Log("The player's name is" @PlayerName);
  }
  

在这个实例中,我们调用了GetMyObject()函数来获得到ExampleInfo的引用。然后我们访问那个引用来获得PlayerInfo (TheInfo.PlayerInfo),然后访问PlayerInfo的引用来获得PlayerName (PlayerInfo.PlayerName)。但是假设没有TheInfo值或者GetMyObject()中有一个问题导致它返回ExampleInfo失败, 那么此时函数将会返回 None 。None是一个空的引用 – 和C++中的空指针类似。

如果在我们的实例中GetMyObject()函数返回 None ,那么变量TheInfo的值将是None。在下一行代码中,我们尝试访问TheInfo来获得PlayerInfo的引用。但是TheInfo是None(空的),它没有指向任何东西。所以不能访问它,从而导致虚幻引擎会向日志中输出一个警告,通知代码中断:

  Accessed None in ExampleMod.GetPlayerName!
  

很容易便可以避免这样的代码。仅需要在您的代码中添加一些判断检测,并在错误的情况下定义一些特殊的行为即可:

  class ExampleGameType extends UTGame;
  
  function string GetPlayerName()
  {
     local ExampleInfo TheInfo;
     local string PlayerName;
  
     TheInfo = GetMyObject();
     if ((TheInfo != None) && (TheInfo.PlayerInfo != None))
     {
        PlayerName = TheInfo.PlayerInfo.PlayerName;
     }
     else
     {
        PlayerName = "Unknown";
     }
  
     Log("The player's name is" @PlayerName);
  }
  

现在我们检测TheInfo是否为 None ,然后判断PlayerInfo的引用是否为 None 。UnrealScript中的 if 语句使用短路逻辑。也就是, if 语句从左到右进行计算判断。只要代码遇到使得 if 语句为否定的判断,那么它将停止计算判断。那意味着如果TheInfo是 None ,那么代码将永远不会计算执行以下语句。

  (TheInfo.PlayerInfo != None)
  

我们知道剩下的语句所做的内容并不重要,只要第一部分为假,那么整个语句便为假。

当在至关重要的函数比如 Timer()Tick() 中发生Accessed Nones(访问为空)的警告时是尤其危险的。它将会花费很长的时间来向日志中书写错误信息,如果您的代码一秒钟转存3000个错误信息,那么它将会彻底破坏性能(更不用说磁盘空间了)。

Iterators(迭代器)

UnrealScript实现了一个非常简单的语言功能,称为Iterators(迭代器)。迭代器是一个封装了一个列表的数据类型。(UnrealScript仅支持列表迭代器,以后的语言版本可能支持用户自定义的迭代器)。您可以获得一个迭代器,并在它的上面进行循环,在迭代器的每个对象上执行操作。这里是个示例:

  local Ammo A;
  
  foreach AllActors(class'Ammo', A)
  {
     A.AmmoAmount = 999;
  }
  

在这个实例中,我们使用了 AllActors() 函数获得一个Actor List迭代器。然后我们使用 foreach 迭代器进行循环来在AllActors函数返回的每个对象上执行一些行为。 AllActors() 取入您想要的Actor类型的类,并传入一个变量来存放它。=AllActors()= 将会搜索当前游戏中的每个Actor来获得您想要的对象。 这里我们假设 设置游戏中每个Actor的AmmoAmount 为999

这听上去很好,但是我们考虑一下它的副作用: 我们正在搜索数百个Actor查找少量的一些Actors。这确实不是一个最快的操作方法。

如果您认真地使用Iterators(迭代器),那么它是非常有用的。因为它们的运行速度倾向于较慢,您或许应该尽量避免在一秒钟之内执行多于两次的迭代。并且几乎没有任何原因需要我们在 Tick() 函数或其它循环中执行一个 AllActors() 迭代过程。所以请自己做好判断。

您将使用 AllActors() 搜索时最常见的类型应该是搜索所有的PlayerReplicationInfo Actors。PlayerReplicationInfo包含了服务器发送给客户端的关于玩家的重要信息。它允许每个客户端在不需要发送太多信息的情况下便可以知道其他玩家的状态。它用于显示记分牌上的分数和其它常见信息。

通常,在全局Actor List(Actor列表)中仅有一小组PlayerReplicationInfo Actors。进行一次耗时的搜索却仅获得这么几个结果并没有太大的意义。为了简化这个常用的迭代,我们在GameReplicationInfo中定义了一个 PRI数组。每隔1/10秒,更新一次PRIArray来包含当前的一组PlayerReplicationInfos。然后您可以对PRIArray进行操作,而不必调用 AllActors()

还有一些其它的迭代器。请查看Actor类的定义获得信息。它们的作用正如它们的名称所说明的那样: TouchingActors() 返回接触的actors, RadiusActors() 返回在给定半径之内的所有actors等。合理应用这些迭代器将帮助您保持代码的具有较快的运行速度。

Tracing(跟踪)

因为虚幻引擎默认情况下不使用可见集合,如果您想以空间的感觉在世界中找到某个东西,那么您将需要执行跟踪。大多数时候,您都会清楚地知道您正在跟踪到哪里,但您仅想知道在跟踪线的另一端是什么。其他的时候,您将使用一系列的跟踪来了解存在问题的对象的周围环境。

第一个建议是在任何可能的情况下都要尽量避免使用跟踪。您应该仔细地想想您正在使用跟踪干什么,然后尝试想出一个可替换的方式来达到目的。跟踪是一个性能消耗很大的操作,它可以是您的mod的运行速度减慢。您或许有一个玩家正在每次tick(更新)中执行两次跟踪,并且在您测试过程中它们也可以工作的很好。但是您没有意识到的是一旦您在网上和其它15或者更多的人同时玩游戏时,这些跟踪将开始不断地累加。

如果您必须执行跟踪,那么请限制它们的长度大小。较短的跟踪要比较长的跟踪快。如果您正在为UT设计一个新的Shotgun(散弹猎枪)武器,比如当武器开火来计算枪弹的散落时,您或许想执行12个跟踪。12个跟踪是非常合理的;因为玩家不可能在一秒钟之内使它的Shotgun(散弹猎枪)开火30次。但是,如果您的mod使用了大的开放的关卡,12个跟踪的消耗比较大。您的枪不可能会成为一个很有用的长距离武器,所以您可能同时需要在一定的限度内缩短它的跟踪的范围。这样可以避免出现最糟糕的情况,即从地图的一端跟踪到地图的另一端。

使用跟踪从根本上讲是一个判断函数。当您在单独的一帧中执行很多跟踪时,便会出现问题。尽管如此,您仍然需要注意跟踪的使用情况。

Replication(复制) - 解密及解惑

书写成功的虚幻引擎游戏的mod时,replication(复制)是最难理解的方面之一,但如果您想获得多样的游戏性,那么理解它是必要的。关于它的多数文档从本质上讲都具有较高的技术性,往深层次的挖掘学习是很困难的。这里讲解了一些基本概念;随着时间的推移,我们将会对replication(复制)有更深的理解。

simulated 函数既可以在客户端上进行调用也可以在服务器上调用,但是仅当从一个simulated(仿真)函数中调用它时。一旦函数调用打断了仿真链,仿真将会中止。您需要知道您正在仿真的是什么以及您需要在simulated(仿真)函数中做哪些操作。永远不要只是因为您在Unreal的源代码的某处看到了一个函数修饰符比如 simulated ,便添加它。您必须理解为什么要添加它,并且需要知道它的作用。如果您不知道您的代码正在做的是什么,您是不可能书写出高质量的代码的。

因为simulated(仿真)函数既可以在客户端调用也可以在服务器调用,所以您必须特别地注意您正在访问的数据。某些对象引用可以在服务器端获得而在客户端却不可以获得。比如,每个Actor都有一个到它所属于的当前关卡和当前世界的引用。在世界引用中是一个到当前游戏的引用。您或许会书写类似于下面的代码:

  simulated function bool CheckTeamGame()
  {
     return World.Game.bTeamGame
  }
  

这个简单的simulated(仿真)函数根据当前的游戏是否是Team Game(团队游戏)而返回真或假。它通过判断当前关卡的GameInfo引用的bTeamGame属性来完成这个功能。但是这里有一些错误…

Level(关卡)引用的Game属性仅能在服务器有效。客户端不知道关于服务器游戏对象的任何信息,所以客户端将会想日志中写入一个Accessed None(访问的对象为空)错误。

如果您打开关于LevelInfo的脚本,您会找到这段代码:

  //-----------------------------------------------------------------------------
  // 网络复制。
  
  replication
  {
     reliable if( Role==ROLE_Authority )
        Pauser, TimeDilation, bNoCheating, bAllowFOV;
  }
  

Replication(复制)代码块是一个特殊的语句,它告诉虚幻引擎如何处理这个对象的属性。让我们仔细地分析一下。

首先,我们有一个replication(复制)条件: reliable if( Role = ROLE_Authority)= 。条件的第一部分或者是可靠的或者是不可靠的。 如果它是可靠的,那么意味着引擎将确保复制的信息安全地到达每个客户端。因为UDP协议的工作方式,有可能导致在传输过程中丢失包。不可靠复制将不会检测包是否安全到达。可靠复制比不可靠复制的网络性能消耗高。

条件的第二部分 (Role = ROLE_Authority)= 告诉引擎什么时候发送数据。在这个情形中,无论何时在当前的LevelInfo对象是Authority时,我们将发送数据。为了真正地理解这个条件的意义,您必须理解我们所讨论的对象的特定功能。服务器将维持LevelInfo对象的权威版本。服务器告诉客户端这个关卡当前的行为,但这个过程不能反过来。对于我们实例中的复制代码块,这意味着将会从服务器向每个客户端发送数据。

另一个条件的常见类型是 (Role < ROLE_Authority) 。这意味着当当前的对象不是一个权威时,引擎将发送数据。更确切地说,是客户端告诉服务器正确的信息。

最后,我们看一下类在条件下面的4个变量。这些变量是语句应用的变量。在这个实例中,我们有一个语句指出“如果我们是服务器并且客户端有一个这些变量的更新版本,那么将把Pauser, TimeDilation, bNoCheating, 和bAllowFOV的信息发送到客户端。并且总是确保这些信息可以安全地到达。”

Replication(复制)语句没有覆盖LevelInfo中的其它变量。这可能意味着两件事情。或者信息已经使用C++语言通过客户端进行填充(在TimeSeconds的情况下)或者客户端的信息将永远不更新,并且这个信息是完全不可靠的(在Game的情况下)。

您没有权利访问C++代码,但是您可以制作几个关于对象属性的接口来帮助您判断一个类是否具有已经在底层填充了的不能被复制的属性。 请查看LevelInfo的类声明:

  class LevelInfo extends ZoneInfo
    native;
  

native 意味 这个对象是以C++和UnrealScript进行声明的 。Native类或许具有您不能看到的使用C++书写的行为。但是仅有几个特殊的类是native的 – 主要是由于性能原因。

最后,请注意那些在类声明中标记为 nativereplication 的类。这意味着UnrealScript中的那个 replication 没有做任何事情,那个replication(复制)的实际动作实在C++中定义的。某些对网络性能影响比较严重的对象一般会使用native replication(复制)来帮助改进网络性能。

现在您已经知道了如何避免replicated(复制)变量和simulated(仿真函数)的相关问题。那么现在我们来看一下replicated(复制)函数。

Replicated(复制)函数是一个既可以从客户端调用也可以从服务器调用的函数,但是它仅能在另一段上执行。复制函数的一个实例是 Say 函数。当您按下T键来和游戏中的其它成员说话时,您实际上是在您说活的过程中执行了Say函数。客户端获得这个函数和它的参数,并把它发送到服务器执行。服务器然后把您的消息广播到所有的客户端。

如果您记住这件事情那么复制函数是非常容易使用的,它是: 它们不能返回一个值。一个复制函数被从网络的一端传送到另一个端…它是需要花费时间的(大约等于您ping的时间)。如果赋值函数正发生阻塞(也就是, 它们等待返回一个值),那么网络通信将会中断。

这种情形对于想到这个问题的任何人来说都是显而易见的,但是当您正在制作您的mod时,您或许不会考虑到它。Replicated(复制)函数将会立即返回。可以使用它们来处罚客户端上的行为(比如特效)或者发送信息(向服务器发送武器开火信息)。

最后,replicated(复制)函数仅限于供几个类使用。在一个Actor上的函数调用仅能被复制给拥有那个Actor的玩家。一个函数调用仅能复制给一个actor(拥有那个actor的玩家);不能对它们进行多点传送。您或许在您制作的武器会使用到复制函数(这里函数将会被复制给拥有那些武器设备的玩家)。

这些函数可以帮助您理解复制的基本知识。

示例

Mutator(设置器)

游戏规则

Overlay

GameType(游戏类型)

游戏规则

HUD

Scoreboard(计分牌)

AI(人工智能)

Deathmatch Level(死亡竞技关卡)

CTF Level(CTF 关卡)

自定义角色

为了帮助您开始学习为虚幻竞技场3创建自定义角色,提供了以下的角色骨架供您参考:

此外,提供以下第一人称骨架作为参考。

注意有些骨架中包含 Biped 骨架。该 Biped 骨架只是易于进行动画处理的额外骨架。动画完成后,我们会将动画烘焙到初始骨架上,删除这个两���动物骨架,导出动画。

请参阅自定义角色页面了解更多信息。