Jump to content
模组网

Search the Community

Showing results for tags 'Creation Kit 学习指南'.



More search options

  • Search By Tags

    Type tags separated by commas.
  • Search By Author

Content Type


Forums

  • General Community
    • TES Lore
    • The Pub
    • Site Bugs & Suggestions
  • The Elder Scrolls IV - Oblivion
    • Oblivion Mods
    • Construction Set
    • Oblivion Support
  • The Elder Scrolls V - Skyrim
    • Skyrim Mods
    • Creation Kit
    • Skyrim Support
  • Hosted Projects
    • Fallout
    • TES Online
    • Unofficial Oblivion/Skyrim Tools Collection
    • 2D Imaging
    • 3D Modelling
  • Sinomod
    • Announcements and Updates
    • Site Questions

Find results in...

Find results that contain...


Date Created

  • Start

    End


Last Updated

  • Start

    End


Filter by number of...

Joined

  • Start

    End


Group


Found 37 results

  1. 本章将详细讲解Papyrus脚本的游戏保存系统。此页上的信息将会随目标改变!已知问题和目标的修复和改变将会被标记。 何时可以存档? 游戏可以在任意时刻存档。这既适用于脚本中的多线任务,也适用于单线任务。在大多数情况下,你不必担心它,在你未改变任何东西的情况下,加载完毕的游戏将可以成功返回脚本并运行。但是如果你改变某些东西,那么本章将会予以具体讲解。 脚本 添加目标 如果你在存档建立后对一个目标添加脚本并保存游戏,那么当存档加载后此脚本将存在于目标之上,但是会原封不动(所有这些属性将设定它们的主文件值,只要存档完成加载,它们的初始化模块将会被运行)。 删除目标 如果在存档建立后从目标删除一个脚本,那么当存档加载后此脚本将持续存在于目标上。如果脚本被完全删除,那么可能会产生异常。 属性和脚本变量 添加 有默认值的变量:当存档加载时,变量有默认值。(例如句法"int myVar = 5")无默认值的变量:当存档加载时,变量无默认值。(0,空字符串,空,假)自动属性:属性将收到由主文件给的值。(结尾为关键字"auto"的属性,例如:"ObjectReference Property MyObject Auto")非自动属性:属性将不会收到由主文件给的值,它将为空。注意:由于显而易见的原因,如果当存档建立后目标初始化事件已运行,那么它不会再次运行。因此,如果对初始化事件设定一个新的变量,你不能肯定变量将会有一个适当值,除非你能确定目标从未在之前保存的游戏中存在过。删除 如果你在存档建立后删除一个属性或变量,那么游戏存档中的属性或变量值将被丢弃,然后一个包含详细信息的警告将写入脚本日志。 改变 如果你改变游戏中的属性或变量,那么它将会先删除旧的再添加新的。如果你改变类别(并非名称),然后游戏存档中的任意值将被丢弃,然后包含详细信息的警告将被写入脚本日志。 改变主文件值 如果在主文件中有一个存在的属性并改变它的值,那么脚本将会收到一个存档中不存在的值。换句话说,改变值将不会写入游戏存档中已有的值。 属性范例 ; Version 1 Scriptname MyScript Extends ObjectReference int Property MyAutoValue Auto int internalValue int Property MyNonAutoValue Function Set(int value) internalValue = value EndFunction int Function Get() return internalValue EndFunction EndProperty int MyVariableWithInit = 1 int MyVariableWithoutInit Event OnInit() MyVariableWithoutInit = 1 EndEvent Event OnActivate(ObjectReference akActivator) Debug.Trace("MyAutoValue = " + MyAutoValue) Debug.Trace("MyNonAutoValue = " + MyNonAutoValue) Debug.Trace("MyVariableWithInit = " + MyVariableWithInit) Debug.Trace("MyVariableWithoutInit = " + MyVariableWithoutInit) EndEvent在主文件中:MyAutoValue = 1MyNonAutoValue = 1在日志中,activation之后: MyAutoValue = 1 MyNonAutoValue = 1 MyVariableWithInit = 1 MyVariableWithoutInit = 1 <save is made here> ; Version 2 Scriptname MyScript Extends ObjectReference int Property MyAutoValue Auto int Property MyAutoValue2 Auto int internalValue int Property MyNonAutoValue Function Set(int value) internalValue = value EndFunction int Function Get() return internalValue EndFunction EndProperty int internalValue2 int Property MyNonAutoValue2 Function Set(int value) internalValue2 = value EndFunction int Function Get() return internalValue2 EndFunction EndProperty int MyVariableWithInit = 2 int MyVariableWithoutInit int MyVariableWithInit2 = 2 int MyVariableWithoutInit2 Event OnInit() MyVariableWithoutInit = 2 MyVariableWithoutInit2 = 2 EndEvent Event OnActivate(ObjectReference akActivator) Debug.Trace("MyAutoValue = " + MyAutoValue) Debug.Trace("MyNonAutoValue = " + MyNonAutoValue) Debug.Trace("MyVariableWithInit = " + MyVariableWithInit) Debug.Trace("MyVariableWithoutInit = " + MyVariableWithoutInit) Debug.Trace("MyAutoValue2 = " + MyAutoValue2) Debug.Trace("MyNonAutoValue2 = " + MyNonAutoValue2) Debug.Trace("MyVariableWithInit2 = " + MyVariableWithInit2) Debug.Trace("MyVariableWithoutInit2 = " + MyVariableWithoutInit2) EndEvent在主文件中:MyAutoValue = 2MyNonAutoValue = 2MyAutoValue2 = 2MyNonAutoValue2 = 2在日志中,activation之后且加载先前存档: <save is loaded here> MyAutoValue = 1 MyNonAutoValue = 1 MyVariableWithInit = 1 MyVariableWithoutInit = 1 MyAutoValue2 = 2 MyNonAutoValue2 = 0 MyVariableWithInit2 = 2 MyVariableWithoutInit2 = 0函数注意 函数改变只会影响函数运行中建立的存档。然而,由于存档可以在任意时刻建立,因此这些接下来的点将会被写入。同样,假设在2010年11月29日之后建立并保存一个信息,那么一旦线程中的任意函数改变,早期建立和保存的存档(即使已加载和稍晚建立)将拒绝任何运行的堆栈。 添加 添加的新函数中没有任何问题。 删除 当存档建立后,如果在运行中删除一个函数,那么旧函数将从存档中加载并允许结束。警告将被写入脚本日志,大量对已删除函数的调用(经常由于一些其它改变或删除的函数调用它)将会失败。 改变 改变参数或返回类型 函数的差异将被注意,旧版本的函数将会从游戏存档中被加载并允许结束运行。大量对函数的调用将使用档案和零散文件中的版本,最有可能的问题错误是参数或返回类别的不匹配(经常由于一些其它改变或删除的函数调用它)。 添加/删除/改变函数变量 函数的差异将会被注意,旧版本的函数将从游戏存档中加载并允许结束运行。大量对函数的调用将使用存档/零散文件中的版本。 改变代码 函数的差异将会被注意,旧版本的函数将从游戏存档中加载并允许结束运行。大量对函数的调用将使用存档/零散文件中的版本。例外:如果一个本地函数被脚本化,堆栈将会被拒绝而非恢复。 函数范例 ; Version 1 Scriptname MyScript extends ObjectReference Event OnActivate(ObjectReference akActivator) MyFunction(1) MyFunction(2) EndEvent Function MyFunction(int aiMyValue) Debug.Trace("Entered my function version 1: aiMyValue = " + aiMyValue) ; 存档*在此*建立 Debug.Trace("Left my function version 1: aiMyValue = " + aiMyValue) EndFunction ; Version 2 - 和version 1相同, 除非状态追踪 Scriptname MyScript extends ObjectReference Event OnActivate(ObjectReference akActivator) MyFunction(1) MyFunction(2) EndEvent Function MyFunction(int aiMyValue) Debug.Trace("Entered my function version 2: aiMyValue = " + aiMyValue) ; 存档*在此*建立 Debug.Trace("Left my function version 2: aiMyValue = " + aiMyValue) EndFunction ; Version 3 Scriptname MyScript extends ObjectReference Event OnActivate(ObjectReference akActivator) Debug.Trace("OnActivate received!") EndEvent存档在调用version 1的OnActivate的MyFunction(1)运行期间建立。保存之前的脚本日志: Entered my function version 1: aiMyValue = 1加载version 2后的脚本日志: warning: Function MyScript..MyFunction in stack frame 2 in stack 457 differs from the in-game resource files - using version from save ... Left my function version 1: aiMyValue = 1 Entered my function version 2: aiMyValue = 2 Left my function version 2: aiMyValue = 2加载version 3后的脚本日志: warning: Function Myscript..MyFunction in stack frame 2 in stack 457 doesn't exist in the in-game resource files - using version from save warning: Function MyScript..OnActivate in stack frame 1 in stack 457 differs from the in-game resource files - using version from save ... Left my function version 1: aiMyValue = 1 error: Method MyFunction not found on MyScript. Aborting call and returning None ... <later, after another activation> OnActivate received!
  2. 概述 本教程系列的第一章将介绍开始并运行Creation Kit的基础: 读者将学会: 如何安装Creation Kit如何创造并保存一个插件(主要Mod数据文件)如何将一个插件载入游戏安装不管你是有制作Mod的计划,或想做一个简单的调整,或只是想看看,首先你需要安装Creation Kit。 Steam官方下载 你可从Steam直接下载Creation Kit,这需要一个Steam账户以及天际游戏。 打开Steam并登陆你的账户前往你的Game Library在下拉菜单里选择"TOOLS"找到"Creation Kit"条目右键单击并选择"Install Game..."免Steam版下载 http://sinomod.com/files/file-32/ 汉化版下载 http://sinomod.com/files/file-1088/ 完成安装后,将会在你的系统桌面上出现一个Creation Kit的快捷方式(如果你在安装过程中选择创建桌面快捷方式),你可双击快捷方式来打开Creation Kit,或在Steam中打开。 了解Creation Engine数据格式 Creation Engine使用和之前的Bethesda游戏制作工作室命名的同样的数据格式。使用“.esm”后缀名的Master Files是数据的大型集合。Skyrim.esm正是一个包含了基本游戏所使用的全部数据的主文件(Master File)。 Plugins,或者“.esp”文件,是较小的数据集合,可在Master Files“之上”载入。这些Plugin可能修改或参考一个主文件内所包含的数据,它们也可能会采用全新的数据。游戏或编辑器可载入多个Plugin。当使用Creation Kit时,只有Plugin能被列为"active file",表示当使用者保存时,任何变化将被存至Plugin。 需要注意的是,Plugin会是你的Mod的主要存储文件。现在我们将试着建立一个。 载入多个Plugin可能会导致冲突。例如,如果你载入两个自定义EncTrollFrost的Mod,只有其中之一会被允许“胜利”。一般来说,这会是最后载入的Plugin,从而使得对于大型Mod项目或同时使用几个Mod来说,载入顺序相当关键。创立你的第一个插件 第一次使用Creation Kit时,不会载入任何数据。首先在主要工具栏内选择文件>数据。你应该会看见一个下图的对话框。双击选择Skyrim.esm以便我们后面载入——会在左侧选框标上一个“X”表示已选择——然后选择“OK”。载入Skyrim.esm需要一些时间。请耐心等候,这项进程可能需要一到两分钟时间,实际这取决于你的硬件设备。 选择并载入数据文件。注意testquest.esp 是我们所激活的插件,也是保存所有改变的文件。 加载 Skyrim.esm 过程中可能会有许多警告,这些都是正常并且可以忽略的。你可以选择 Yes to All 或者敲 Esc 键跳过。 载入完成后,首先要做的事就是创立一个“插件”——保存你的工作的Mod文件。只需在主要工具栏内选择'"文件>保存"。因为你还未指定active file,Creation Kit将提示你创立一个新的文件。在本教程中吗,我们暂时取名为testquest.esp。 如果你没有看到创立新的插件的提示,那么你可能已经激活一个文件了。重新打开文件>数据以确认在载入Skyrim.esm时,你没有选取任何激活文件。Creation Kit没有“save as”(另存为),而所有的保存将重写激活的插件。如果您想要备份您的文件或者从其他源安装,您可以在下面的路径中找到您的插件。 64-bit 系统正常是: C:\Program Files (x86)\Steam\SteamApps\common\Skyrim\Data\.. 大部分 32-bit 系统则是: C:\Program Files\Steam\SteamApps\common\Skyrim\Data\.. 载入你的插件 尽管目前你的插件还是空挡,我们还是可以看看如何将其载入游戏。 首先,你需要在游戏开始时告诉天际(游戏)载入你的插件。有两种方法可达成目的: 通过游戏主要Launcher,选择“数据”并双击你的插件。高阶使用者可能会想直接打开SkyrimCustom.ini文件并手工指定要载入的插件。现在就先使用Launcher来选择你刚创立的插件。 如常地运行游戏。进入游戏后,按下"~"打开控制台。(你可以再次按下~键来关闭控制台)这将允许我们使用许多同于测试Plugin的关键调试命令。在游戏运行过程中的任何时候(出了载入界面)都可进入控制台。 我们暂时还没有任何数据在插件里,不过我们还是可以使用一些控制台命令。试着输入下面几行,在每行后按下Enter(回车键)。 TGM TWF COC RiverwoodSleepingGiantInn我们刚刚开启了无敌模式(TGM: ToggleGodMode),打开了一个结构线视角(TWF: ToggleWireframe),并传送到位于河木镇的旅馆(COC: CenterOnCell)。还有着更多的控制台命令,这仅仅只是少量的例子,旨在给你一个它如何运作的概念;在这里可以找到更多的控制台命令。 当你弄混了控制台时,游戏很容易被打断——因此,要特别注意载入那些你觉得重要的存档,尤其是如果你依赖于自动保存的话! 接下来呢? 当你安装并运行Creation Kit后,你已经准备好开始Mod编辑了。如果你曾经使用过之前的Bethesda Game Studios的游戏(如晨风,湮灭虚空或辐射3)的Mod开发工具,你可能会想看看相同点和新特性。你也可能会想看看我们的新特性页面获得一个概览。 如果你并非一个有经验的Mod开发者,我们建议你浏览一下教程页面,那里包括了许多的教程系列,可引领一名初学者创造一个新的地牢和简单的任务。而经验丰富的Mod开发者或对某一方面感兴趣的编辑者也可跳至那些他们感兴趣的教程页面。在每个阶段都提供了Plugin的例子,因此,如果你并不想阅读整个教程系列,你可以直接浏览你有兴趣的页面。 如果你有一个本wiki上尚未收录的问题,或者你只是想找个地方讨论一下你的mod想法,可到官方论坛上去看看,或是其他你所喜欢的Mod开发团队!
  3. 概述 本章将指导你使用"Ancient Nord Ruin"艺术为一个内部空间创建一个布局。 读者将会习得: "Kits"的定义和用法 如何在对象窗口找到Kit块 Kit块命名转换 如何放置和整合Kit块 如何在游戏中测试一个自定义的地牢 Kits 天际的大部分空间都使用"Kits"创造。这些Kits由许多设计成被一起使用来创造各种广阔空间的环境艺术部分构成,并且对我们在游戏中创造大量空间有所帮助,保持了事物的灵活性,以及在游戏中实验,因而将所需创造的自定义“艺术”数量减少到最小。 在天际中,一个用得非常多的Kit就是"Ancient Nord Ruin" Kit。因为大多数玩家会对此KIt非常熟悉,我们将在布局教程的第一阶段中使用它。还有其他的可用的Kit,比方说,Cave Kit,Imperial Fort Kit,等等。 在对象窗口找到Kit块 要使标准的Kit可用,首先点击Creation Kit窗口顶端工具栏中的载入按钮,双击在"Skyrim.esm"左侧的复选框将其选中,并点击"OK"。对于在过程中跳出的任何错误,回答"Yes"。 在对象窗口的左侧展开“世界对象 (World Objects)”分类,可在对象窗口中查看Kit的分类。从那儿开始,展开"STATIC > Dungeons"分类。有一些Kit类型可用。在本教程中,我们将使用"Nordic"分类。你可浏览分类,看看在这个Kit分类中的不同的子Kit和块。现在,选择"SmRooms"这个子Kit。 你可使用预览窗口来预览对象。在对象窗口中右键单击一个对象并选择“预览 (Preview)”。将会跳出一个窗口显示该对象。你可在这使用同样的摄像头操控(如在渲染窗口中所使用),并会自动更新为显示你在对象窗口中所选的任何对象。这使得用预览窗口浏览这些“艺术”非常便利。可惜,预览窗口的摄像头操控跟渲染窗口的不一样,是个简化版,而两者的区别需要你花些时间才能适应。   Kit非常耐用,而你可操控的Kit块数量可说是排山倒海之势。坚持下去!只要知道学习在Kit上有效地工作是个相当大的工程,并且你需要花点时间来体验和实践。在前往下一个教程之前,在这里花些时间好好看看。不要在这被打败了! Kit命名规则 图1:预览窗口 现在让我们来看看NorRmSmWallSideExSm01这个子Kit中的一块。名称像是某种密码——首次看到时,可能并不会知道它所代表的涵义,不过当你了解了命名规则之后,随便看一眼你就能知道它们是什么了。 让我们一一解析这些名字和检查每个部件的意思: Nor "Nord"的略称 —— 让我们知道这是Nord Ruins kit的部分。 Rm 代表"room",表示这块属于这个子Kit。例如,以NorHall开头的某块是Nord Hall子Kit中的一部分。 Sm 代表"small"。Nordic Room kit用"Sm"代表小房间,用"Bg"表示大房间。 Wall 不言自明:这是一道墙。在这个Kit中的块是墙壁,mid(中间)或cor(角落)的块。 Cor 拐角。包括 CorIn (内拐角) 和 CorOut (外拐角) Side Nordic Small room kit中的块组合在一起呈"Front (正面)"或"Side(侧面)"。之后会详细介绍。 Ex 表示一个离开 / 门口的块。这将很好地关上门,并与同Kit中的"Ex"门道相符。 Sm 表示离开 / 门口的块的大小。Nordic Room kit用"Sm"代表小型出口,用"Bg"表示大型出口。只有大小相符才可组合在一起。 01 这个数字后缀通常表示这个块有多个版本。这个数据会增长来确认该块的每个变体。   你可在对象窗口的筛选框中输入你所需要的块来查找搜索。例如,如果你想查看所有的Nordic Small Room块,在筛选中输入"NorRmSm"。搜索将基于在对象窗口左侧中所选定的分类来给予结果。 当你已经了解了命名规则,当在对象窗口中搜索块时,你可使用星号"(*)"当做通配符。例如,如果你想搜索从一个Nordic Small Room所有可能的出口,就在对象窗口筛选中输入"NorRmSm*Ex"。Nordic Small Room kit的所有可能的出口将会显示给你。Wikipedia上的更多关于通配符的信息 建立一个房间 你现在已经有了在创立一个实际的,可游戏的空间的全部信息。我们将逐步介绍这个过程。当你了解了基本原理后,你将有能力自己使用Nordic Ruins kit创造一个完整的布局。 步骤1:创造一个单元 在我们可做其他事之前,我们需要创造一个可工作的空间。最常用的是复制一个现有的单元。你所选择的单元可能会有一些你需要修改的数据,这个我们稍后再谈。现在,找到并右键单击内部空间aaaMarkers并选择Duplicate Cell(复制单元)。你应该会看见在单元列表中出现一个新的单元。选择它按下F2将其重命名。我们将叫这个地牢LokirsTomb。 现在你已经有了可供你工作的内部空间,确定你选择了LokirsTomb单元(而不是 aaaMarkers),并删除列于对象窗口中的LokirsTomb对象(单元视图右侧),因为我们希望新的开始。现在暂时不用担心删除导航网格。稍后我们后谈及此。 右键单击LokirsTomb单元并选择"View",将其在渲染窗口中显示。 在你做任何编辑之前,一定要百分之百地确定你所载入的单元。有两个地方可以检查:渲染窗口的标题栏,以及在单元视图筛选框上方区域,两者都会显示当前载入的单元的名称 步骤2:放置你的首个块 现在让我们开始放置我们的第一个块。看看你的对象窗口并展开:"World Objects > STATIC> Dungeons > Nordic"。你将注意到一些子Kit,不过现在我们只需关注小型房间,或"SmRooms"子Kit。展开该子Kit,在对象窗口右侧的列表中将出现一些以"NorRmSm"开头的块。 现在我们已经载入了子Kit并准备好下一步,选择"NorRmSmWallSide01"并将其拖到渲染窗口来放置你的单元。你可能会,也可能不会看见这个对象出现。不管哪种情况,在你的单元视图窗口右键单击这个新创造的reference ID。选择"Edit"。如图2所示,将XYZ位置坐标改为0,0,0,并单击OK。 在单元视图中右键单击ref ID,手动将渲染窗口的摄像头(视角)集中于它,你应当会看见在空间内浮动着一个地面的块。右键单击对象还会使该对象为当前所选项,因此你可以使用你的摄像头控制来找个好的角度。 网格 在开始下一步之前,首先我们需要了解"the Grid (网格)",以及"对齐"它。 当我们在渲染窗口中工作时,实际上我们是处于一个不可见的,三维笛卡尔网格中。网格的中心,位于XYZ坐标(0,0,0),是"原点"——就是不久前我们手动放置NorRmSmWallSide01的地方。见图2 你不用总是注意网格。比方说,当放置一个敌人,旋转一个宝箱,或调整一束灯光的位置时,精度并不是很重要。不过在使用Kit时,网格非常关键。Kit块被设计成必须完全“对齐”对方的形式,而网格是可获得如此精度的唯一方法。“目测”的话,在Kit块的使用中,往往会留下缝隙。 Creation Kit的所有东西都以"Units"(单位)度量。这些单位与任何真实世界的测量单位相应,图3应该可以给你一个它们如何衡量我们的Kit的概念,以及一些特点。 Nordic Ruin在一个128-单位的网格中效果很好。有两种方法:在渲染窗口的任一位置单击右键并选择"Render Window Properties",或点击在主工具栏中的此按钮:。将会显示图4所示的对话框。如果并不可见,选择"Movement Tab"。这儿有着一些选项,不过对于我们目前的情况拉说,我们只需要确保"Snap To Grid"设置为128,而"Snap To Angle"设为45。点击应用 (Apply)保存你的设置并解除对话框。 返回渲染窗口,确认对齐网格 (snap-to-grid)已经开启。这可通过快捷键"Q"或此按钮控制。同样,对其角度 (snap-to-angle)也应当开启。这通过"Ctrl+Q"或开关。 图2:手动将我们的第一个块放至原点。 图3:制作衡量单位的某些参考 图4:参数选择:Movement Tab 步骤3:组合Kit块 选择我们之前所放置的NorRmSmWallSide01。使用"ctrl+D"来创造一个复制的块。这会将一个新的NorRmSmWallSide01参照物放在之前已有的那个的上面。注意,系统会自动选择这个复制的对象。 记住,在做任何操作(像是复制)之前,你必须“点击”渲染窗口。同时,如果渲染窗口里面的物件没有被选择,按Ctrl+P或者点击主菜单栏的Enter Portal Mode按钮 在渲染窗口内点击并拖曳来移动这个新创建的墙壁块。由于我们打开了网格对齐,锁定位置会相对容易一些。 两堵墙可不能构成一个房间,我们需要角落!将你的注意力放回对象窗口并找到"NorRmSmCorIn01",然后将其拖曳至渲染窗口。将块旋转至正确的方位,使其与其中一堵墙对齐,并将它放进去。见 图5记住,要开启你的角度对齐功能并将角度锁定为45°。 当将新的参照物拖进渲染窗口时,在松开鼠标左键后,它们会被放在你的光标所处的平面。这使得在正确的地面高度创造新的块变得简单快捷。如果你在空白处释放,该对象将被放置于摄像头所处位置。 一些Kit有“流动性”,鉴于美学,这些块必须以某种特定的方式设定。例如,在当前Kit中,墙壁要么是"侧面",要么是"正面"。你已放置的两堵墙是侧面墙,如块的名称所述。任何与它们垂直的墙壁必须是正面类型。在这种情况时,由于上限根据房间的长度,而不当地使用块会导致在天花板上出现大裂缝。 在对象窗口中,找到"NorRmSmWallFront01"并将其拖至渲染窗口。如果需要的话,可旋转该块使其与其中一个角落块对齐。用另外两个"NorRmSmWallFront01"块完成外墙。你可在每个侧面用角落和同数量的"NorRmSmWallFront01"和"NorRmSmWallSide01"来完成这个房间的墙壁。 房间几乎要完成了——除了位于中心的那个大洞。放置一个NorRmSmMid01块,它可对其那个缺口。复制mid的那个块并不断地对其直到洞被填满。在这里有一些东西你需要检查:这个块同业有着特定的流动性,因此旋转很关键,尽管最初时它可能看起来不是这样。 要想查看上限,旋转摄像头来看着天花板。注意到块的线性模式。在需要连接墙壁的地方,单独旋转mid块。见图7所述的正确的设定。 旋转摄像头来向上看可能会让人失去方向感,特别是对于那些Creation Kit和3D的初学者。耐心,如果你发现最后跑到了一片空白的地方,使用Shift+F来重新定位视角。 图5:NorRmSmCorIn01对齐NorRmSmWallSide01 图6:你的房间的外边界 图7:NorRmSmMid01块没有正确摆放,注意缺口。在右侧,块被正确设定了。 步骤4:连接房间,并完成布局 我们已经用墙壁制造了一个房间,一个天花板以及一个地面——但没有出去的路。现在是时候加扇门了。在渲染窗口选择其中一个NorRmSmWallFront01块并选择"delete"(删除)。在对象窗口中,找到NorRmSmWallFrontExSm01并将其拖至渲染窗口。记住保持正面 / 侧面一致。因为这个出口位于一个“正面的”墙壁上,使用"front"出口块。 对你所选择的对象不确定?主工具栏会在它的左下角显示。如果你选择了多个对象,则会显示最后选择的对象名称和"+X more"来告诉你你总共选择了多少。你的当前选择同样也会在单元视图窗口高亮显示。 试着创造一个连接新的通道的玄关。在对象窗口找到NorHallSm1WayEndExSm01。确保你的对象窗口正对WorldObjects>Static>Dungeons>Nordic>SmHalls打开。注意ExSm后缀,代表"Exit Small"(小型出口)。这让我们知道,它将捕捉任何在 Nordic kit中的ExSm。确保你的网格对齐仍然保持开启状态,并将块对齐组合。 现在你知道你需要用来完成这个地牢的首个布局的全部信息。现在试着使用下面的图像这么做。如果你在某处卡住了,试着下载Plugin范例来查看已完成的布局。 图1:完整的布局 图2:完整的带有标签的布局 当下载Plugin范例时,记住不要载入你的Plugin,从而避免拥有两个同样名称的单元的冲突。 步骤5:在游戏中查看 在你工作的过程中,经常到游戏中去检查一下是个不错的主意。想重温一下如何载入你的Plugin,到这儿看看。在你深处该空间内走动时,注意一下缺口和缝隙,你可能在编辑器中漏掉了一些。 如果你选择将之前的Plugin载入你的游戏,或者你想命名你新创建的单元为LokirsTomb,你可在控制台输入COC LokirsTomb来传送至该单元。 当测试时,使用COCMarkerHeading可能会有帮助。这些标记只用于测试——当使用COC控制台命令时,这会是你将出现的地点。每个单元只能放置一个标记。如果一个单元有一个之前已经存在的标记,只需在保存Plugin之前移除它。和所有的标记相同,如果它被隐藏了,按"M"可使其可见。如果你在对象窗口中找不到它,只需在筛选框中输入"COC"并选择*ALL分类。 问题 'Creation-kit' 在将物件渲染到游戏中存在点问题,虽然这个问题部分人都会遇到。如果你创建了一个mod含有多个静态物体,然后你保存并上传,但是测试后你会发现这些物体看不到,问题出现了,当前还没找到修复的方法。希望 Bethesda Game Studios 能找到避免这个问题的方法或者修复这个问题。
  4. 概述 本章教程将讲解有关陷阱及其预设的基本知识, 你将学到以下内容: 触发条件 使用预置的伏击点 陷阱布置基本知识 使用其它预置内容 链接引用 先前的教程论述过链接引用。例如,在创建NPC巡逻路径时将使用链接引用。这些行为将由AI包中设置的逻辑决定。这是Creation Kit中通过目标引用及其它目标-目标关联来控制复杂活动的典型应用。 基本激活条件 当玩家“使用”一个目标,它们就会被激活。激活对于活动来说是一项很重要的概念。当目标被激活时将发生3件事。 先天行为 大多数目标被激活时都有默认的行为。门将开启/关闭,容器将展现它的目录,NPC将会谈话或被扒窃]]等等。这些行为将自动发生除非被脚本禁用。 脚本事件 激活行为也能用Papyrus控制,允许我们用新方法影响激活。虽然本教程将不包括有关脚本的内容,但是我们将利用预存的活动事件脚本。 触发条件 我们将引用指定的一个或几个触发条件。这标志着父对象被激活,所有子结果将发生。 Fig. 6.1: 注意拉链的摆放位置-放在醒目的地方。 现在利用触发条件关联,如图Fig 6.1所示的铁闸门使用触发条件链接,链接被蓝色和白色的线标明,白色的是父对象,蓝色的是子结果.在你的关卡里模拟这项设置: 从案例插件中加载LokirsTomb01。 用norPortcullisLarge01替换NorDoorMedium01。 确定新的铁闸门已被布置到空间里。 然后,在附近的墙上布置一个"NorPullChain01"。 双击拉链并点击活动引用标签。(Fig 6.2) 双击空白区域,然后"选择引用"对话框将出现。(Fig 6.3) 点击在渲染窗口选择引用按钮。(Fig 6.4)注意十字准线光标: 在渲染窗口中双击第一个PatrolIdleMarker。(Fig 6.5) 点击OK关闭并同意所有窗口。(Figs 6.6/6.7) 注意铁闸门和开关之间的蓝/白线。这表示触发条件的关系。 Fig. 6.2:铁闸门上的触发条件 Fig. 6.3:双击触发条件引用区域 Fig. 6.4:触发引用选择 Fig. 6.5:选择拉链 Fig. 6.6:确定触发引用 Fig. 6.7:确定引用 一个目标可以有多于一个触发条件,例如在铁闸门两面都有拉杆,从而避免玩家在错误一面触发陷阱而被卡死。你或许也注意到产生忽略所有无条件激活的引用的选项框。 伏击点 找到并布置一个预置的伏击点 突发事件是地下城的特征,但是并不明显。现在我们将添加一个;当玩家进入最后的房间时一个尸鬼大君将爬出坟墓,制作一个这样的事件能使游戏复杂。正因为如此,你能以"预设"为条件-它能简单地创造一个包括脚本及关联的目标的集合。 加载内部房间"warehouseAmbushes" 确定标记可见(热键M) 将你的注意力转移到顺着房间的最南端的预设尸鬼伏击点 这里有2个平放的石棺。如图Fig 6.9所示。 使用每个拖选或ctrl+click,将会有以下5个引用: LvlDraugrAmbushMelee1HMale defaultActivateSelfTRIG DragonPriestCoffinMarker NorSarcophagusTopAnim01 NorSarcophagusBottom 全选这些目标,按ctrl+C来复制全部,注意分次复制这些引用将没有作用;你将丢失它们的关联数据。 加载 LokirsTomb01. 在洞穴区域锁定视角,按"ctrl+V"粘贴这些预设目标 将复杂、重叠的目标标记选择,使用"1"键循环观察。通过这个按键能使选择的目标暗淡、隐藏、出现,这样能使已选和未选的一目了然。 不要担心将预设的目标粘贴在你不想布置的地方。很容易就将它们移动到附近空地方并摆放在各自的位置,如图Fig 6.10所示。最重要的事情是对于想链接的预设目标有思绪。让我们继续学习并完成以下内容: 选择“石棺、盖子”和橙色的“家具标记”。 关闭捕捉-方格。(热键Q) 移动这些使其成为一个组,放在地下城洞穴区域凸起的平台上。(Fig 6.11) (可选的) 确定石棺附近的LvlDraugrAmbushMelee1HMale位置。它将在石棺内开始;这是其更为醒目 (可选的) 如果你想用传统的方式遭遇"boss",按照先前的教程将尸鬼的难度调为非常困难 修改并删掉石棺底下的导航网格 将蓝色的触发盒移入你的房间。(Fig 6.12),目前它暂时不起作用-我们将在以后用到它。 Fig. 6.9:水平石棺的预设伏击点 Fig. 6.10:在Lokir的坟墓空白处创建并移动伏击点 Fig. 6.11:石棺创建并移入空间 Fig. 6.12:触发盒移入空间 触发伏击点 蓝色的触发盒在预置过程中起到关键性作用。当玩家与其碰撞时,尸鬼将爬出棺材。然而触发盒的默认尺寸并不理想。我们需要更大的触发盒从而使事件触发更为出色。我们需要通过使用非典型缩放使它适合这个房间。 选择原版的蓝色触发盒 按"2"打开工具缩放 注意非典型缩放工具(Fig 6.13)将给每个坐标系2个坐标轴 将你的光标移至红色的坐标轴。点击并按住鼠标左键,坐标轴将变为黄色。 "注意:如果红色和绿色坐标轴的位置颠倒了,不要担心。这是因为你的触发盒旋转,不会产生任何错误。" 通过拖动鼠标指针放缩触发盒使其达到合适宽度。如图Fig 6.14所示。 对绿色Y坐标轴重复步骤,使触发盒长度横跨房间,如图Fig 6.15所示。 通过蓝色Z坐标轴调整厚度,使玩家不可能从上跃过或从下钻过触发盒。 移动触发盒,覆盖洞穴和大厅之间的区域,并再次放缩至合适的大小(Fig 6.16)。用你的判断使玩家能在合适的时刻触发伏击点。 再次按"2",关闭工具缩放并回到正常编辑状态。 这里有一些好的触发盒布置规则: 通常太大的触发盒会降低游戏帧数。因此不宜布置过于巨大的触发盒。 避免过”瘦“的触发盒,否则很难发现玩家。每个轴的最厚处应最小128 触发盒的布置要有创造性。有时有效的触发盒往往布置到奇怪的地方。 如果要手动设定初始物品的尺寸。双击打开条件引用,点击“初始化”标签,XYZ坐标值将会把初始物品放入合适的坐标位置。 初始物品照样能被涂上你喜欢的颜色,仍然点击“初始化”标签。这也能将触发盒涂上合适的颜色。 如果想让触发盒成为玩家的必经之地,阻塞点将会非常有用。 保存插件,并在游戏中多次测试伏击点。试着用不同的角色,并用潜行、奔跑、后退等不同的行为。使得你的伏击点在不同的条件下都能被触发。 如果你想在你的关卡里布置很多尸鬼伏击点,那么你可以布置多重伏击点。 Fig. 6.13:初始触发盒的放缩工具 Fig. 6.14:在一个方向放缩触发盒 Fig. 6.15:在另一个方向放缩触发盒 Fig. 6.16:放缩触发盒从而覆盖走廊 陷阱 伏击点并不是惟一有用的预设装置。很多陷阱也组成相互关联的目标。陷阱是一个有用的装置,尽管有时它们很容易被发现并避开。玩家将找寻陷阱并将敌人引向它们。 其中一个可见陷阱是活动锤。我们将把它布置进Lokir的坟墓并以此为例。 使用预设的锤陷阱 设置锤陷阱与设置伏击点没有多大区别。步骤如下所示: 加载内部房间"warehouseTraps" 选择"TrapMace01" (Fig 6.33) 注意蓝/白线,在锤被选择后出现 选择"TrapMace01"并关联"TrapTripwire01"目标。(Fig 6.34) Ctrl+C复制 加载"Lokir的坟墓" Ctrl+V粘贴进房间 将锤放入合适的位置。记住在触发陷阱后它能自由地摆动/悬挂。 将绊绳放入合适的位置,如图Fig 6.36所示。 在游戏运行时测试你的陷阱。你也许期望在碰断绊绳后锤将落下来。 Fig. 6.33:选定陷阱仓库中的锤陷阱 Fig. 6.34:用触发条件链接锤和绊绳 Fig. 6.36:最后效果 非预设陷阱 不是所有的陷阱都需要预设或分开布置伏击点,如下图Figs 6.49a-d所示。如同其它目标一样,它们能在空间中被布置任何方向,它们也能自动工作。尽管它们非预设,它们仍然能在"WarehouseTraps"可见。 要有清晰的思路,从而使陷阱与环境、角色以及其它陷阱结合。例如,一个充满油的房间里应布置一个可以下落并点燃油的油灯。 你也能使陷阱与不同的触发盒混合或配合,例如平台、拉杆、按钮。你可以自由地使用本章学习到的内容试验。 Fig. 6.49a:TrapOilPool01. 火焰将随着连续的油扩散 Fig. 6.49b:BearTrap01是触发盒的一部分(橙色标记) Fig. 6.49c:TrapNorFirePlate01. 红色标记危险区域 Fig. 6.49d:连锁陷阱 其它预置 预置并不只是用在陷阱和伏击点上,也存在其它类型预置,例如矿脉和扉。 现在你的地下城已有一些游戏性,现在是考虑剧情的时候了。我们将在下章讲到优化-无论多么出色,如果不能运行就将功亏一篑。          
  5. 概述 本章将讲解如何通过手动方法和自动方法来创造导航网格。 你将学到: "导航网格"的定义及其重要性 如何通过基本重做法自动制作导航网格 如何手动编辑导航网格 修复导航网格的缺陷 制作基本贴图 什么是导航网格? 当一块区域已建造且被布置后,你必须确定哪些区域角色可进入,这时就要用到导航网格。简单地说,导航网格就是告诉哪些区域你能进入的多边形网格的集合。 当我们运行游戏时我们可以接受到很多信息,我们大脑分析看到的细节,并不断地得出结论和进行推测。然而,人工智能系统没有人脑那么发达,而且必须遵循我们设定的信息。导航网格告诉AI角色哪些空间可以进入。因此导航网格在控制AI行为方面起到关键作用。 导航网格编辑器是一个健全的工具-尽管在初次使用时会遇到障碍。我们将按步骤依次学到如何运用它。一旦你了解使用法则并加以练习使用,那么你将会迅速成为导航网格专家。 GECK用户注意:部分手动创建导航网格方法的改变将会给你造成不便。然而,新增加的自动创建方法将会收录在教程中,例如所有的新"Recast"产生方法。 湮灭或晨风Construction Set的用户注意:导航网格完全替代这些工具中的寻路系统。 自动生成导航网格 Fig. 4.1: 导航网格按钮 Fig. 4.2: 导航网格工具栏 所有关于导航网格的工作将用一个多样化编辑模块。想要进入这个模块,点击主工具栏上的Navmesh按钮(Fig. 4.1),或者按"CTRL+E"。导航网格工具栏将会出现。 事实上我们不必使用导航网格工具栏上的每个按钮来自动制作导航网格;它很方便。我们只需在导航网格模式下使用自动制作,然而,这些按钮的存在让我们知道它们的作用。 这里有4种自动制作的方法:目标法, Havok法, 重做法和高级。对于大多数情况,你将会使用重做法并手动修补导航网格。本教程将不会着重讲解目标法和Havok法,高级用户会选择手动制作所有的导航网格。一些情况下手动制作明显快于自动制作再修补的方法-一旦你能熟练运用这个系统你将会选择它。 GECK用户注意:目标法和重做法是Creation Kit的新特性。 让我们用重做法在Lokir坟墓的导航网格上创造一条路径: 点击渲染窗口来确定它处于活动状态 确信你没有在渲染窗口中选择任何东西(如果你不确定,在空白处点击鼠标左键)。 在主工具栏内的导航网格目录下选择重做法:Navmesh > Generation > Recast Based Generation 重做法对话框将出现。(Fig 4.3) 不要担心每个数值的意思。请确信你的数值和图Fig 4.3中的相同,点击"OK"。 点击"OK"。将会等待一些时间来运行,等待时间取决于你的电脑。 一旦完成,导航网格应该和图Fig 4.4 Fig4.5里的看起来一样。 Fig. 4.3: 导航网格重做窗口 Fig. 4.4:导航网格重做法运行的结果 Fig. 4.5:导航网格重做, 只有导航网格 有时制作导航网格时你要面对一个平面的一大堆选项,但是你不要删除这个平面的其它导航网格。你能通过按住CTRL+ALT并点击鼠标左键来拖动所有你要建立导航网格空间的引用窗口。一旦你选择查询窗口,点击重做法将只对这些引用的东西有效。 手动制作导航网格 现在我们制作一个导航网格,我们现在必须花费一些时间来修补它。最简单快捷的方法是手动解决,在我们修补它之前,花费一些时间找寻重做法做得不到位的地方。 如果你在观察导航网格时遇到麻烦请尝试三种导航网格模式:正常、透明、只有导航网格。按住"W"轮流尝试三种不同模式,或使用工具栏上的按钮,如图所示: 删除导航网格的一部分 当你修复自动生成的导航网格时,最好从删除不能进入区域的导航网格冗余开始这样将使我们看剩余导航网格时容易一些。 洞穴和腐蚀的楼梯是产生导航网格冗余最多的地方。你将会看到许多孤立的导航网格。不要担心这些,一旦你修复主要的导航网格,那么移除多余的导航网格将会很容易。 对于想清除导航网格冗余的高级用户而言,在每个你想保留的导航网格上CTRL+鼠标左键点击三角形然后点击'"I",这是运用反向选择。一旦你按住"I"键所有导航网格冗余将被选择,通过"R"或"DELETE"键删除它们。这种方法在手动删除大量导航网格冗余时非常迅速,但是也很容易误删有用的导航网格-务必小心谨慎! 首先要注意洞穴的选择,然后我们将清理导航网格冗余。现在你只需知道导航网格工具栏左边的3个按钮: Fig. 4.6:区域选择按钮 Fig. 4.7:顶点选择按钮 Fig. 4.8:边选择按钮 修复玩洞穴选项,按住鼠标左键并拖动选择洞穴的最高点。记住通过可见标记("W")来获取你的方位。不要担心破坏洞穴区域的导航网格:接下来将重建它。确信你至少选择顶点选择(Fig 4.7)及区域选择 (Fig4.6)之中的一个。你也可以通过选择它们来修复洞穴区域。Fig 4.9a展示有顶点和三角形的导航网格的洞穴选项并准备删除操作。 一旦对你要删除的区域选择顶点选择或区域选择,点击"R"或"del"进行删除操作。区域冗余被删除后,你将看到如图Fig 4.9b所示。 Fig. 4.9a: 导航网格,洞穴区域选择 Fig. 4.9b: 导航网格,洞穴区域移除 导航网格区域制作 现在我们将学习手动制作导航网格的方法。 在制作导航网格的过程中你将逐步使用导航网格热键来代替先前的工具栏。 T 开关导航网格区域选择 V 开关导航网格顶点选择 G 开关导航网格边界选择 要想打开顶点选择,在导航网格工具栏上点击,或者在洞穴区域开始处按热键"V"。在地面上右击鼠标。注意一个顶点将出现。然后重复相同的步骤制作至少2个顶点。如图Fig 4.10所示,你将会在地面上看到3个黄色(或2黄1绿)顶点。 确信你没有放置坐标方格,在制作导航网格时放置坐标方格不仅没有必要还会给你带来麻烦。   和其它Creation Kit编辑器相比,导航网格编辑器使用独立的热键设置。也许你会不适应,例如热键"T"是上下预览,但是在导航网格编辑模块中是重新设置按键。 注意你制作的顶点的颜色。绿色的是当前已被选择的。黄色的是未选择且无系统(不是任何区域的一部分)的。红色的是有系统但未选择的。如果一切完成,那么黄色顶点没有任何作用并可以安全被删除。 要想创造一个区域,通过在每个顶点上使用CTRL+左击鼠标选择3个无系统的顶点。然后按"A"键或点击来用3个顶点创造一个三角形区域,如图Fig 4.11所示。注意热键"A"只在3个顶点并非完全选择时工作。 Fig. 4.10: 3个黄色(无系统)顶点 Fig. 4.11: 3个顶点的三角形区域 注意:在创建一个三角形区域后仍有2个顶点保持绿色(已选择)。在这种情况下,你要通过CTRL+右击鼠标来创建其它三角形区域。在你点击鼠标的的地方将会出现一个新顶点并自动创造一个新的三角形。 洞穴区域的导航网格 现在我们已经创建首个三角形区域,我们必须用更多三角形来填充洞穴区域。最简单的方法是选择2个顶点再点击ctrl+右击鼠标来创造第三个顶点。这将在2个已选择的顶点和新顶点之间自动创建一个新三角形。 注意一旦新三角形被创建,2个顶点将保持绿色(已选择)通过使用ctrl+右击鼠标来创建另外一个三角形。通过不断练习,你能熟练运用CTRL和鼠标右键来填充空间。如果绿色的顶点并非你要选择的,通过按"Tab"键来重新选择顶点。一切就绪,我们填充洞穴区域。当你完成后,洞穴区域的导航网格将如图Fig 4.12和Fig 4.13所示。 (注意:你的实际结果将与布置的杂物多少相关) 通过学习这项技术,你能迅速运用CTRL-右击鼠标把地面覆盖导航网格。我们把这项技术称为"走狗"。 Fig. 4.12:导航网格重做法的结果 Fig. 4.13:只有导航网格 古诺德遗迹楼梯的导航网格 我们将导航网格布置到房间里,然后看一下古诺德楼梯NorHallSm1wayStairs128。在使用自动法时楼梯是典型的问题区域。你的楼梯没有导航网格,如图Fig 4.14a所示。 幸运的是,Creation Kit提供轻松解决这个问题的方法。如下: 例如,我们要清理楼梯顶部和底部的边界,可以这样做: 删除所有楼梯的顶点,如果适用。 重新布置楼梯顶部和底部的顶点,在每一边创造一个干净开放的边界。 如果哪一边有至少三个顶点,合并顶点使得每边只剩两个。-要想合并顶点,选择你要合并的,然后按"Q"键或点击工具栏上的。 确定已点选边界选择("G") 双击每一边,并各选2个顶点-它们将变为绿色。 在2个顶点被选择后,你的结果将会如下图Fig 4.14b所示。 Fig. 4.14a: 重做法之后的楼梯 Fig. 4.14b: 清理楼梯的导航网格 在其余平面布置导航网格 当导航网格布置在洞穴区域及楼梯后,你要知道必不可少的是要继续在房间空白处布置导航网格。如果你遇到问题,以下内容或许有帮助。 制作一个简洁明了的导航网格,这将更易于编辑且提升游戏运行速度。 牢记热键! 不要对制作AI不使用的导航网格感到厌烦。例如家具之间的狭小空间。路径测试将会用到。 记住:在两个接近的物品之间自动绘制导航网格时,打开边界:双击一个边界,然后CTRL+双击另一个边界。 利用多重预览模式。用循环通过,按"W"或使用工具栏按钮 删除一个三角形,按"T",选择要丢弃的三角形并按DELETE或R。你也能通过CTRL+Z撤销选择。 删除任意一个顶点将删除它所在的边和三角形。 记住点选区域选择、顶点选择、边界选择的热键(分别是T, V, G)。 要合并顶点,选择至少两个顶点并按Q或点击,所有选择的顶点将合并到“首先”选择的顶点。 双击一个边(边界选择开启状态, "G")来迅速选择它的两个端点。 记住创造一个三角形区域后会有2个顶点依然保持已选择状态。你能通过按"TAB"取消选择。 你能通过按"R"键删除已选择的目标, 尽量创建一个肥大的三角形区域-并尽量是等边的。这能提高效率。要尽力避免创建瘦长的三角形区域。使用边界旋转(热键"S"或)来达到理想的结果,如图Fig 4.15所示。 如果想看有系统的区域网格,但却看到黄色边界,这意味着有2个顶点重合。修复方法是,拖动顶点然后合并它们("Q")。如果黄色边界依旧保持,对边界另一侧上的点重复相同步骤。如果黄色边界消失,你要合并顶点并链接导航网格。 右击边界来在边界上创建顶点。这种方法被称为爆破边界。 Fig. 4.15a: 典型的无效率方法, "放射型"边界 Fig. 4.15b: 边界处理后的效果 这是平面上杂物附近区域的导航网格的样子。注意图Fig 4.16中启用物理效果(可移动/动态)的目标下的导航网格。你不必对Havok目标周围制作导航网格,导航网格将在游戏运行时注意到它。例如,这个场景内的Havok目标是铲子、木推车、陶罐。注意这些目标附近的导航网格。木桶不是Havok目标因此我们必须在它周围布置导航网格。 Fig. 4.16: 导航网格适当的避开静态物体除了可移动的目标 图Fig 4.17大致展示出你完成之后的导航网格样子。在这个例子中,我们剔除微小、隔离的导航网格冗余。它们很容易被删除。 在主要导航网格中选择任意三角形,然后按"I"或 通过反向选择选取未被选择的导航网格。如果你的可玩区域是一个连续的导航网格,不必要的区域高亮绿色。 选择导航网格冗余后,按"R"或"DELETE"删除。 最后的导航网格应如图Fig 4.18所示。 Fig. 4.17: 手动选取主要/冗余的导航网格 Fig. 4.18: 最终的导航网格 修复导航网格错误 当你完成后你必须检查导航网格的错误。这是一个简单的过程。 你也许会在保存时发现一些错误。无论保存时发现错误与否,你都应该运行"检查导航网格"。 打开三角形选择对话框,如图fig 4.18所示。这里有3种方法: 使用导航网格工具栏上的三角形查找按钮 按热键"CTRL+F" 通过主目录:导航网格 > 三角形选择 一旦弹出对话框,选择"检查导航网格" 警告,如图fig 4.19所示,弹出对话框询问你是否要删除全部。 接下来,处理问题三角形,这里有两种方法: 选择"是"将删除所有问题三角形-但是你将必须记住这些三角形位置并修补它们(通常通过创造一个新的三角形)。你只能用肉眼查找,尽管这对于复杂的导航网格来说非常困难,特别面对很多零碎的三角形。 我们建议手动修复每个错误,在这种情况下你选择"否"。这将关闭警告对话框。 选择警告对话框的下一个警告 。渲染窗口拍摄将自动拍摄问题三角形并高亮它们。通过删除和重建它们大多数错误都能轻而易举地解决。 如果有超过一个警告,再次选择检查导航网格,重复修复步骤直到所有问题被解决。(在每次删除后你应再次检查,因为每次修复错误都会影响三角形导航网格索引—— 一切完成后,保存你的插件。 Fig. 4.18: 三角形选择对话框 Fig. 4.19: 导航网格警告对话框 如果你不想手动修复每个错误,在点击"下一个警告"按钮后点击每个坏三角形,然后选择"是"删除所有问题网格。它将让你在删除后知道那些导航网格的位置。 制作边界顶盖 边界顶盖包括一些数据,例如顶盖的高度。NPC在战斗和寻路时将会用到它。 要想自动发现顶盖,从主工具栏上依次选择导航网格>发现顶盖,或使用导航网格工具栏上的按钮。 发现顶盖需要一定的时间,具体时间长度取决于你的硬件水平和导航网格的复杂程度。 一切完成后,你将注意到导航网格边界颜色的变化。每种颜色代表不同的高度。现在你的导航网格应如图Fig 4.20所示。 要想使你的顶盖有很好的视觉效果,依次选择导航网格>绘制顶盖(如图Fig 4.21所示)。 一切完成后,角色将能在空间中寻路,制作高质量的导航网格是要花费一定的时间,现在将要在游戏中重复枯燥的过程-任何坏导航网格的后果将在游戏运行时体现出来。 Fig. 4.20:典型含有顶盖数据的导航网格 Fig. 4.21:已绘制顶盖的导航网格 顶盖信息对于变化的战斗来说可能有用-例如在从敌人处逃跑时它将确定AI运动的方向。因此尽量保留这些信息,尽管有时它们看起来并不重要。 刚刚绘制顶盖后导航网格将保留很多信息-它将标记诸如水、首选、下拉狭长部分的边界。更多信息请点击这里。 对于更好地调整顶盖数据也很有用,尽管这些细节并不重要。调整合适的顶盖数据,选择边界并按"E"。将弹出"边界顶盖"对话框。 合适的导航网格是NPC寻路和战斗的基础。在下一章中我们将面对这些问题。          
  6. 概述 在这一章我们将学习在地牢摆放静止和运动的物体,添加家具和添加物品。 我们将学到: 不同种类的物体 如何整齐地摆放物品 添加家具 布置门 基本物品 现在你拥有一个非常基本的布局的地牢,它包括空的房间和墙。这一节我们将讲到如何很好地将空间拆分为几个房间。 我们将用多种可见物品放入房间并举出一些例子。一旦你完成,你都可以按照你的意思在Lokir的墓穴里摆放物体。 这种方法对于制作有特定用途或剧情的房间特别有帮助,它将使房间变得有内涵而非仅仅将房间塞满而已,它同样能使玩家创造出一个有特定原因而必须存在的房间。 Search & Replace: 交换物体 首先,在房间安放物品的最简单方法是换出重复的部件,选择要换出的物体,按下热键 'Ctrl+F',将立即弹出Search & Replace。 Fig. 3.1: 搜索并替换弹窗 让我们现在试着将一堵墙换成其它物体 在地牢任意一处地方选择NorRmSmWallSide01。 按下“Ctrl+F”弹出“Search & Replace”对话框。 在这种情况下,按照名称排序的下一个物体 - NorRmSmWallSide02,将会默认被选择。 按下 OK 交换物品. 只要你有至少一个待选择的物品, 就可以通过替换来改变物体。 如果你搜索可用物品清单,你将注意到诺德的墙有8种类型,包括已被损坏的形式。使用Search & Replace工具换出不需要的墙,换入已被损坏的墙将使地牢有种破败的感觉。 你可以通过使用Search & Replace查看目标物品的其它类型,例如对诺德地板使用这种方法,你将发现你可以用柱子更换平坦的地面。如果物体已被命名,那么它在search & replace中可以迅速通过名称排序找到并替换。 在Search & Replace对话框的底部,有默认选择的 "Selection Only"。这是你多数情况下所需的物品,但是你依然可以用房间或空间交换这个物体的所有形态。你甚至可以用三个盒子里的所有物品来交换,不过请你谨慎地选择这种方法。Ctrl和F必须同时按下!静态物体 静态物体在游戏中占了很大一部分。它们在设计时就已经被安放好且无法移动,换句话说,它不会被玩家影响。静态物体有很多,例如桌子、工作台、书柜、石柱。在以后学习中,我们将会经常面对这些物体。 安放目标 如果你想安放一个物体,你可以通过ctrl+D从周围环境中复制一个物体并用它进行替换。这在高级MOD制作中是一项很普通的过程,和从目标窗口中搜索相比它无疑更为迅速。 心动不如行动,让我们把一个餐桌放入房间。 从地下城的第一间房子里任选一块地面 用ctrl+D复制它-“注意:在复制时,新的物体将会自动被选择” 使用ctrl+F打开Search & Replace 用RuinsAltar替换它。-“你可以手动在列表中找到它,但你可以将其加入偏好物体列表从而迅速找到它” 将物体放置第一个房间的角落 因为复制品继承原物品的位置,所以原物品的旋转角度和尺寸也被复制品保留,我们很容易错误地将复制品遗忘在空间中。因为空间冲突,当复制品与原物品重叠时将会发生碰撞效果,这是很常见的错误,我们必须认真以避免这种错误。物体经常有一个名称,尽管它不具有代码的排他性。如果你从所有的诺德物体中寻找,你将找到许多含有Ruins的物体。在这里你将找到长凳、餐桌、桌子以及其它物体。 然而并不是所有物体可以作为新物品或障碍物添加进空间。平坦的地面可以营造出一种干净整齐的感觉。地面块和碎石可以创造出破败的感觉。 安放一些NorRmSmFloorRaised01和02s在地面上 不要担心把地面变得平整-可以通过旋转以达到最佳视觉效果 也放一些NorRubblePile01-08 碎石可以安放在墙与地面交界处,从而使环境更为自然。 对于添加效果而言,使用破损的地面有助于营造出一种被破坏的感觉。你也可以造出整洁的通向目标的通道。你可以随心所欲地将物体摆放在房间里,当人们欣赏这个房间时他们不会意识到这是一个坟墓的入口,尝试发现将道具变为入口的方法。 缩放目标 Fig. 3.2: 属性查询窗口的规模值 在布置静态目标时缩放和旋转是很有用的工具。通过恰当的缩放和旋转,你能将一小部分物体按照倍数缩放并回收利用。 在保持物体原有形态的前提下,你只能将它的放缩倍数控制在0.5至1.5倍之间。这个规则不只是为了逻辑和视觉形态,还为了防止物体局部贴图形态变得比例失调。 如何缩放一个目标 接下来,我们将在入口周围布置一些ruinsFloorCandles,并通过缩放和旋转使它们变得不同。这里有一些不同的方法来缩放目标。试着用不同的方法使目标达到预期形态。 手动缩放 这种方法适用于精确缩放。 双击目标并打开属性查询窗口 你将在3D Data标签下发现缩放(Scale) 输入缩放倍数 自由缩放 这种方法能方便快捷地缩放目标。 选择你要放缩的目标 按住S键不放 拖动鼠标指针放缩目标 工具缩放 这种方法提供一个任意放缩一个或多个目标的工具。注意:“尽管放缩工具有坐标轴,但在处理诸如triggers和roombounds的早期创作编辑时仍有可能出现不均匀的放缩。 选择要放缩的目标 点击2键弹出缩放工具 拖动鼠标指针放缩目标 再点击2键完成缩放 动态物体 "动态"物体是可以在游戏中被玩家移动的物体。这种物体能使游戏显得更为自由。不同类型的动态物体将在下方的列表中展现。注意所有没有物理数据的目标不能做为动态物体。 杂物 杂物占动态物体的很大一部分,它包括盘子、器皿、高脚酒杯、木桶等等。 杂物的属性定义 游戏中玩家可以随意控制的目标 玩家可以捡起并拿走它们 一些杂物可见并可用,还要考虑它们布置位置和平衡因素。 在房间里布置一些杂物,例如: Fig. 3.3:木桶01 Fig. 3.4:油灯01 Fig. 3.5:皮革01 Fig. 3.6:亚麻围巾01 Fig. 3.7:腐蚀小刀 Fig. 3.8:腐蚀剪刀 可移动静态物体 可移动静态物体玩家可以将其移动,但不能拿走。例如:骨头、烹饪罐、一些碎石瓦砾。 可移动静态物体的属性定义 玩家可以操纵并触碰它们 玩家不能带走它们并加入物品列表 尝试将以下可移动静态物体放入房间周围: Fig. 3.9:BoneDeerSkull01 Fig. 3.10:BoneHumanRibcage Fig. 3.11:HandCartWheel Fig. 3.12:Hook Fig. 3.13:RuinsPot01 Fig. 3.14:WagonWheel 物理效果设置 Fig. 3.15: "Run Havok Sim"按钮 物理效果设置是一个非常重要的工具。它将帮助你在脚本编辑中添加物理效果。含有物理效果的目标将与环境相互影响,相比手动布置所有目标,这种方法更为迅速且有时更为精确。 Havok预览按钮 点击Run Havok Sim按钮,如图Fig. 3.15所示,选择一个可移动静态物体并让它在游戏中按照预期运动。这是让物体随机摆放的最好方法,或者在游戏运行过程中看它如何运动。 Fig. 3.16: Don't Havok Settle 勾选框 选择一个已布置的ruinsPot*s 将它移动到半空中并旋转 在目标处于已选择状态的情况下点击Havok按钮让这个罐做自由落体运动并在下落过程中旋转它 当它安全着陆后,再返回 与先前Bethesda不同,含有物理效果的目标在游戏加载过程中自动被安放,目标设置勾选"禁用Havok设置"(Don't Havok Settle),如图Fig 3.16所示,将跳过开头设置,但是游戏引擎依然模拟碰撞效果。例如:勾选"禁用Havok设置",一个半空中的物体在游戏运行中将依旧悬浮在半空。然而一旦被其它诸如箭矢、魔法、玩家之类物体碰撞,它依然会有所反应。对于想禁用物理模拟的高级用户,这里有包括造成这种默认禁用Havok特殊效果的Papyrus脚本,尽管这不在教程教学范围内。 启发式目标 启发式目标是玩家不能影响的可移动静态物体,但是能产生发散粒子或动画。例如旗帜、火焰或水滴。在烛台上布置火焰效果对于制作光源来说是一个很大的进步-尽管你必须添加事实上的光源-映照人物时能产生光影效果这将在稍后的教程中有所讲解。 启发式目标和FX能通过使用主要工具栏中的Animate Lights and Effects按钮展现出效果: 与早期的Bethesda不同,它不必通过特意启用物理模拟来展现效果。启发式目标的属性定义 目标能在空间中保持形态但仍能产生发散粒子或动画 玩家不能在游戏中控制它们 玩家不能将它们拿走并加入物品列表 在房间中布置一些启发式目标,例如: Fig. 3.17:FXFireWithEmbersHeavy Fig. 3.18:GenericBannerBlue01 尸体也能在布置过程中使用物理设置。然而不同于先前的Bethesda工具,简单地将角色的生命值设为0并不能使其在游戏中处于死亡状态。为了使一个角色处于死亡状态(在编辑器中设定),要在属性查询的选项框中勾选"Starts Dead"。如图Fig 3.16所示,在"OK"和"Respawns"上方。在物理模拟器启动的状态下,我们可以让一个含有物理数据的目标适应它的位置,试着在按住Alt键的情况下拖动目标。这对于多项目标链接是非常有用的,如同你利用绳索抓住骨头一样。 战利品 战利品是天际中的核心元素,而且应该在设计地下城的时候被考虑。值得庆幸的是,Creation Kit提供一系列布置战利品的方法,包括自由布置战利品、容器内装入随等级变化的战利品,让我们考虑一下可以用的方法。 自由摆放的战利品 任何目标都可以在空间中任意摆放。自由摆放的战利品也如此,它不但可以在空间中看见而且可以拿走。这里有几种主要的可以自由摆放的战利品: 护甲: 包括护甲值为0的衣服在内,它们都可以被玩家穿上。诸如手套、靴子、头盔之类的护甲都能在编辑器及游戏中看见,但是项链将会被衣服包起来。 书籍: 一般的书籍可以摆放到空间内,玩家可以通过阅读法术书或技能书来获得和提升能力。请记住普通的天际地下城只含有一本技能书。当玩家获取一本书时它将立刻打开但是不会自动给玩家。 配料: 这类目标包括玩家可以食用并产生效果的食物和炼金原料。可以通过炼金产生药剂。 药剂: 与配料类似。 武器: 武器对玩家来说可用。它们在空间中的形态与玩家装备它们时展现的形态相同。 如果不使用Havok设定摆放将会导致武器嵌入墙内。例如在南厅的最后一间房子里有一些武器嵌入到雕塑内。 容器 容器是一个可以被玩家打开并拿走其中物品的目标,例如箱子、瓮。当地图加载的时候箱子内才被装入物品。这样做的好处是能让玩家依据其等级获得对应的物品。按照这种方法你不必在布置物品时考虑玩家级别。 在房间里布置一些如下图所示的容器。注意"Treas"前缀,这是"treasure"(宝藏)的缩写,意味着这些容器可能含有一定数量的物品。 Fig. 3.22:BarrelFood01 Fig. 3.23:MiscSackLarge Fig. 3.24:TreasDraugrChest Fig. 3.25:TreasRuinsUrnLarge02 预览容器内的物品 Creation Kit提供在不同遭遇区域等级下预览容器内可能产生物品的能力。这对于预测玩家可能获得的战利品种类来说很有用,然而预测结果很难接近游戏中随机环境下的实际结果。 双击你想预览的目标 点击Edit Base来查看容器内的基础目标(Fig 3.19) 点击Preview Calculated Result(Fig 3.19a) 出现容器可能产生物品的随机列表(Fig 3.20) 多次重复以上步骤来查看增加的可能物品结果 你可以改变预览等级来看不同等级下可能出现的战利品。注意容器等级由容器所在的遭遇区域决定,它不一定与玩家等级相同 Fig 3.21是一个30级玩家发现物品的例子: Fig. 3.19: 编辑按钮 Fig. 3.19a: 战利品预览窗口 Fig. 3.20: 战利品预览窗口 Fig. 3.20: 30级战利品预览窗口 级别物品 级别物品, 或"模拟"目标,是Creation Kit的新特色。它允许你布置由遭遇区域等级决定的自由物品。请阅读更多信息! Creation Kit有一大堆虚拟目标。它们简明地标记能布置到环境中的不同战利品的大小和形状。模拟目标对它们自身不做任何事,但是一定有一个级别物品列表与实际获得的物品关联。现在让我们通过模拟布置一个等级药剂: Fig. 3.30: 等级物品标签 在一定等级下布置一个模拟药剂。确定标记框已显示(热键"M") 双击目标打开等级物品标签(Fig. 3.30) 你能通过过滤器来找到你想要的药剂 在过滤器(选项)输入restore 将Leveled Item Base Object设置为LItemPotionRestoreHMS "LItem"是Leveled Item的缩写. "HMS"是Health/Magicka/Stamina(生命/法力/耐力)的缩写,这用来说明药剂将恢复三种属性中的哪一种。 你也能选择后缀为"Best"的物品,指示列表将尝试最好的等级物品。 模拟物品按照标记的内容产生-因此确保你使用"M"键标记从而使你能看到它们! 一些模拟目标 接下来将看到你能使用的模拟目标。试着用不同可见Litem列表中的物品布置你的地下城。切记-如果你没有设置等级物品,那么将不会出现任何物品。 同样要记住模拟目标只是起辅助作用,我们不知道关于等级物品列表的任何信息。这意味着如果你错误地用LItemArmorShieldHeavy(重甲盾牌等级物品)列表设置dummyPotion(模拟药剂),例如,物品将在游戏运行时生成一个盾牌,生成的盾牌由模拟物品所在的区域等级决定。 模拟书 模拟靴子 模拟弓 模拟胸甲 模拟匕首 模拟手套 模拟头盔 模拟锤 模拟药剂 模拟盾牌 模拟灵魂石 模拟武器1H - 单手 模拟武器2H - 双手 Fig. 3.26:模拟 书 Fig. 3.27:模拟 头盔 Fig. 3.28:模拟 锤 Fig. 3.29:模拟 药剂 家具 设置标记 当你布置一个家具标记,请务必确定它朝向一个合适的方向。这也许不明显,例如椅子。很容易就错误地布置一个坐在上面的NPC面向墙的一个长凳。这里有一个很简单的方法检查,按热键'M'将显示标记及朝向。现在让我们布置一个长凳。 布置一个ruinsBench01,如图Fig. 3.31所示。 Fig. 3.32 展示一个含有标记的同样的长凳(热键'M') 蓝色特征模型是人物朝向,确信他们面向外侧。 亮蓝色钻石标记进出口,不应该被墙或目标阻挡。 在周围布置更多的家具,包括'下方的Fig. 3.33 和 3.34。 能让角色自由的经过进出口很重要,换句话说角色能使用它们。 Fig. 3.31: 关闭标记的腐蚀长凳 Fig. 3.32: 开启标记的腐蚀长凳 Fig. 3.33:长凳01 Fig. 3.34:王座 交互式家具 一些家具能互动,例如制皮架。它们能像普通家具一样自由布置。布置一些交互式家具,如图Fig. 3.35-3.37这些家具对于有NPC居住的地下城来说很重要。 Fig. 3.35:炼金 Fig. 3.36:制革 Fig. 3.37:魔法 一些生产工具都有桌面。我们能很容易地将标记放置到一个目标上并共用基本功能。请小心地将这些物品和地面排成一条直线,以避免NPC或玩家使用它们时下沉或悬浮。 门 门能在玩家影响它时打开或关闭。它们能被任意等级的锁锁住,包括需要钥匙打开的那种。 门能设置为默认打开状态;要想实现这个,在属性查询对话框中勾选Open By Default。这是一个让门打开的很有用的方法。通常来说,最好让玩家来开门。 默认打开对容器依然有用,尤其对放入物品的箱子及已被搜刮的空箱子来说。 锁住 门 & 容器 Fig. 3.38:对门上锁 试着在地下城中对一个目标上锁,你能按照你的选择锁住门或箱子。 双击门打开Reference窗口 点击Lock标签(Fig. 3.38) 检查Locked选项框 等级决定开锁的难度。需要钥匙意味着锁不能被撬开。 (选项) Key区域是能锁的钥匙。 锁住关键位置的门(“这也能通过空间/探索阻止通过”)通常是一个很坏的主意除非你把钥匙隐藏在某处。同样应避免锁住不应该有锁孔的物体例如袋子或桶。 布置Lokir的坟墓 如你所见,这里有许多布置空间的工具,尝试你学过的方法,并在你大脑里想着每个房间的作用及任务。如果你开始为某个主意奋斗,检查编辑器中的其它房间或已加载的关于Lokir坟墓的案例插件。 一旦你对坟墓的布局及物品满意,你应该开始对其布置敌人和追随者,将在下一章: 导航网格中讲到。 目标调色板 (OPALs) 选修 当复制-替换思想能快速布置物品的时候(特别你能对Creation Kit的命名规则了如指掌),你也想熟练掌握目标调色板工具,它能允许你创造和管理收藏品,并将它们布置到游戏世界。
  7. 概述 本章将进一步探讨布局技巧。具体来说,将着重于更整体的"cave"(洞穴)Kit。 读者将习得: 多个Kit之间的应用和转化 着手工作一个整体的Kit 不开启网格工作和使用参照物对齐 整体关卡设计 你可通过在网格上连接各个Kit块来简要地创造关卡,所有连接都需要为90°直角。这对类似诺德遗迹或帝国塔楼这类的“建筑的”地牢效果很好。不过实际上游戏的环境特点要更加整体,因此在你创造的时候,你可能会发现自己想脱离网格,特别是在创造自然类的环境像是洞穴这类的地方。幸运的是,许多可使用的Kit在建立时都考虑到了这样一个工作流程。在下一部分中,我们将在诺德地牢里使用Cave(洞穴)Kit。 Cave Kit Cave kit(洞穴)的功用本质上和Nordic Ruins kit差不多——它有着许多不遵循对齐规则的子Kit——但这些洞穴有着更整体化的自由度,如在天际各地所见。这个Kit的多用性让设计者可创造所能想象到的任何类型的房间。不过,这种工作方式和我们之前建造此地牢的Nordic部分略有不同。 有着多个可使用的Cave Kit变量:Ice(冰),Mine (矿洞)和Caves(洞穴)。我们将使用Green Cave kit。当在你的对象窗口中搜索时,你可在所有搜索前加上前缀caveg*,这样可以避免出现Ice和Mine的变量。创造一个外形 我们将用一个洞穴房间替代Lokir's Tomb最后的房间。删除如图2.1所示的区域,使其看起来更像是图2.2。 我们将用一个简单的"Shell"房间取代被删除的房间。当使用Caves时,最好由一个非常基本的形状开始,然后再逐步创造一个看起来很自然的空间。我们的外形将会是一个大型的4x4房间。找到以下的块并将其拖至渲染窗口。在图2.3中会给予你一个关于这些块的图解。 CaveGLRoomCorner01 CaveGLRoomCorner02 CaveGLRoomCorner03 CaveGLRoomCorner04 CaveGLRoomWall01 (x2) CaveGLRoomWall02 (x2) CaveGLRoomWall03 (x2) CaveGLRoomWall04 (x2) CaveGLRoomMid01 (x4) 将这些散乱的块组合起来,如图2.4所示。这将会是我们最后那个房间的起步。我们将删除几个块,从而使得玩家可以出入这个房间,但现在,我们只需要关注我们所定义的这个房间的大略形状。 图2.1:最后房间的起始布局 图2.2:在一部分被移除之后 图2.3:建立新的洞穴外形所需要的全部块的分解图 图2.4:构架好的洞穴外形 转换 当使用多个Kit时,你需要建立从一个Kit到下一个之间的转换。一些块特别为此目的而存在(如NorHallBg1wayToCaveG01),不过如果没有一个满足你所想要的块,别担心,你可以轻易地制造你自己的转换,有着视觉上的独特优势。 由通过另一个块延伸Large Nordic Hall来开始这个转换。创造一个NorHallBG1way01的复制品(选择已存在的块并按ctrl+D),然后将其加到大厅的末端。 要创造大厅的转换,你需要下列的CaveG块: CaveGLWallDoorL01 CaveGLWallDoorLB01 CaveGLHall1Way01 如 图2.5所示组合这些块。确定你的对齐网格是开启的。过会儿我们可以随意地摆放这些块,不过现在,我们需要它们对齐到网格。 当转换区域创建好之后,关闭对齐网格 。选择全部三个块,并将它们移到与Large Nordic Hall一致。调整位置直到你觉得不错看起来不错为止;图2.6提供了一个例子。在继续下去之前,不妨到游戏中去实际地看一看,注意检查一下任何的遗漏和缺失。最好现在就修复它们,在这个还只需要调整几个块的时候! 同时选定移动多个Kit块有时候会导致小的缝隙,有时候被认为是"蛛网",如图2.8所示。这可通过对齐参照来修复,我们将在下一节讨论。 图2.5:转换块的单个以及组合(插图)的分解图 图2.6:恰当的转换 不通过网格工作 想要创建更自然整体的空间,那么,熟悉“关闭网格”来工作就很重要了。不过,这并非意味着我们会无视对齐规则。不通过网格工作的意思是,我们将独立主要世界网格来组合关卡的各部分,并通过转换过渡,如我们刚创造的那个,来将它们组合。这可以让我们创造出更加自然的空间。 我们在前一教程中所创造的Nordic Ruins是“开启网格”时的。而我们现在要建立的洞穴房间将是“不通过网格”。让我们现在看看工作流程吧。 首先,我们要确保snap to grid 和snap to angle 都处于关闭状态。 在我们刚创建的外形中选择所有的块,并将其旋转大约45°角,如图2.7所示。 删除离Large Nordic Hallway(大型诺德走廊)最近的大型房间的拐角点。 再次选择房间的其他部分并将其移动直到你的房间的墙壁与部分Cave Transition块相交,同样,如图2.7所示。 我们将会把洞穴外形向下移动一点,让玩家会比地面高的地方进入房间。这并非精确科学——只是将房间沿着Z轴向下移动,直到转换过渡看起来和图2.7所示差不多为止。 图2.7:过渡块的分解图,分离的和组合的。 图2.8:一个蛛网的例子 开启对齐参照工作 在关闭网格时放置一整个房间是一回事,但如果在放置这个房间之后,我们无法对其工作,就不太好了。下面的练习将会告诉你在关闭网格的情况下工作如何实现——并非常关键。 现在我们已经得到了我们想要放置的房间,试着抓住洞穴房间的一个拐角点并使用ctrl+K将它的旋转重置。将对齐打开,并试着对齐该快将其放回原位。你应该很快就会发现到,这根本无法完成——这个重置的块正与全局网格对齐。 重新点击渲染窗口并按下Shift+Q。注意,你的鼠标图标将会变成一个十字光标,如以下所示。 在空白处,光标将显示为红色:,而当处于一个有效的参考时则为白色:。 在我们放置的这个不平衡的房间的任意位置盘旋,并当十字光标变为白色时点击。 这所做的是暂时将全局网格和角度定向调整至符合你刚才选择的块——被称为对齐参照。试着选择我们在本章开始时重置的块,并将其对齐回原位。这是一个关闭通常的网格时工作的好办法,但要确保在讲你的块对齐组合的时候,没有任何可见的异常情况或蛛网(图2.8)。 无论何时,如果你已经不需要你的对齐参照,并想回去使用全局网格,只需按下shift+Q并在任何灰白处点击红色十字光标即可。 改造洞穴 你现在所有的是一个相当标准的大型洞穴房间。洞穴Kit是特别为将像这种的房间转换为更自然整体的空间而设计的。想法就是先创造一个比我们所需要的更大的空间,并开始一点一点用附加块将其吞噬——因此,让我们把它往里建立一点来改变这个房间的形状。天际中的每个Kit都带有这种设计理念,而洞穴Kit成为了这一点的例证。 洞穴Kit带有很多课帮助你改造你的游戏空间的不同块。放入一下松散的柱形物和自由的墙壁,如图2.9所示。 CaveGLPillar01 CaveGLPillar02 CaveGLPillar03 2xCaveGLWallStraight01 我们将使用这些块,将这个房间南边的墙壁移进来。为了获得这个自然整体的效果,关闭网格对齐 和角度对齐,并开始移动块来构成墙壁。使用柱子来隐藏任何墙壁之间的缝隙。(见图2.10和2.11) 花一点儿时间,并不断在游戏中检查你的工作,找出任何缺口和缝隙。 图2.9:在洞穴墙壁中应用的单个块 图2.10:在放置新的洞穴墙壁之前 图2.11:放置墙壁洞穴块T 现在,我们已经改造了墙壁,让我们使用两个洞穴特定的"cliff"块,来调整关卡的垂直性,如图2.12所示。 CaveGCliffsCornerOut01NS CaveGCliffs01 一个关卡中的垂直性让遭遇战比在一个大的平整的房间内战斗更加有趣,也让其更加自然,看起来更加顺眼。(见图2.13 and 2.14) 请自由地试验其他的墙壁,岩石,地面和柱子,来找到正确的感觉,并运用在你的房间内。并不需要和本文范例完全一致;测试,并表达出你自己的观点和想法。 图2.12:Cave cliff pieces 图2.13:在新的洞穴峭壁放置之前B 图2.14:放置洞穴峭壁 填充缺口 现在,你已经获得了一个可游戏的空间,你可能会注意到一些很大的缺口,呈现灰白色。在完成这部分之前,你会想覆盖这些缺口。以下是我们将会在此范例中使用的块,如图2.15所示。 CaveGLPillar01 CaveGBoulderL01 CaveGBoulderL04 CaveGRockPileS01 CaveGDirtMound01 柱子和砾石可运用在天花板和墙壁上的更大的缺口上。灰尘和碎石在不同Kit之间的地面转换过渡上效果不错。(见图2.16和2.17) 和之前一样,尝试一些你想要的块,直到你找到那些你认为效果很好的块。 图2.15:Cave cliff 块 图2.16:放置新的洞穴峭壁之前 图2.17:放置洞穴峭壁 当你对这个房间的布局和墓穴的大致布局感到满意时,你已经准备好开始更小的细节——杂项。
  8. 概述 Bethesda Game Studios和 Valve已经合作将天际的Mod移至Steam Workshop。上古卷轴和辐射的Mod团队在这些年对Bethesda的PC游戏的独创性和创造性都是一个极好的确实证明,而现在Bethesda很乐意将你们的Mod带给更多的玩家。 Creation Kit为创造者提供了新的功能,让Mod的分配和上传更加简洁便利。 图1: 上传你的Mod到Steam Workshop Plugin的上传过程 上传你的Mod到Steam Workshop并不难。只需依照下列说明进行: 确保你的网络已经连接并且登入你的Steam账户。按下Ctrl+S保存你的Plugin。如果这是一个新文件,你会被提示创建你的Plugin。在主工具栏中选择File > Upload Plugin and Archive to Steam。你将被提示手动添加文件到Archive。任何与你的Mod直接关联的文件将会被自动存档。选择"No",除非你确定你需要手动保存特定的文件。查看Create Archive了解如何打包所有你需要的文件。Steam Workshop上传对话框将才出现。(已插图,右侧)Mod名称 (Mod Title):你的Mod的名称,它将对用户显示。选择你将坚持做的那些!描述 (Description):你的Mod的一个简单介绍,会在Steam Workshop可见。标签 (Tags):对你的内容标上相关的标签。它们可以帮助用户找到你的Mod。预览图 (Preview Image):为你的Mod选择一个jpg格式的图像文件。512x512的图像文件效果最佳!当你对这些设定感到满意时,点击"Upload"(上传)。对话框的标题栏现在将变成 "Sharing mod with Steam Cloud..."将会有一个延迟,延迟时间取决于你的Mod的大小,当Mod上传成功后,你将会收到通知。上传Archive-Only Mods 图2: 手动档案生成器。从你的Skyrim/Date文件夹中拖曳即可 并非所有的Mod开发者需要使用这个编辑器。比方说,纹理重置的Mod,只需要覆盖现有的纹理文件。Creation Kit也使得上传这类Mod到Steam Workshop成为可能。 上传过程与之前的完全一样,除了你会被提示手动添加问价到档案 (Archive), 选择"Yes"。 在出现的对话框中,只需拖曳你修改过的文件。当拖曳完之后,选择"Pack Files",并再次进行上述过程。 也可以不通过上传手动创建Archive。只需在主工具栏中选择"File > Create Archive"。 在Steam Workshop进入并管理你的Mod 图3: 在Steam Workshop查看你的Mod 将你的Mod上传至Steam Workshop后,它将开放给所有的天际PC版玩家。这一段内容是如何在Workshop找到并管理你的Mod。注意,上传后,有时候会需要一点时间之后才会在Steam Workshop上看到你的Mod,不过通常都是即时的。 确定你登入了你上传Mod时所使用的Steam账户。前往Steam profile community 页面。在"Actions"部分,点击"View Workshop Files"。找到你刚上传的Mod并点击进入。这个页面包括了所有的标准Mod工具,以及只对你的几个特殊工具。(仅对作者)删除 (Delete): 如果你想手动从Steam Workshop删除你的Mod,使用这个按钮。(仅对作者)私人开发者评论 (Private Developer Comments): 之对你和开发者可见,这部分可让开发者在你的Mod上互动和评论。(仅对作者)修订历史 (Revision History): 你还可以查看你的Mod之前的更新日志。注意只有最近的版本会被保存在云库 (Cloud)。在本地保存备份也会更有把握。评级 (Rate): 给你的Mod一个赞赏或否定的评价。举报 (Report): 可举报带有攻击性或不恰当的Mod。被举报的Mod可能会被禁止。被禁止的Mod并非是从Steam Workshop上被删除了;只是对一般大众不可见。订阅 (Subscribe): 对你的Mod的订阅将会在打开天际(游戏)时自动从Workshop下载最新版本。注意从Workshop下载的版本可能会对文件名称稍作修改——因而你无需担心这会偶然覆盖你的本地的,还在工作中的版本。公共用户评论 (Public User Comments): 确定不时查看一下你的评论 (Comments)部分,看看从社区给你的反馈!创建者 (Created By): 这部分将提供到你其他的Workshop文件和Steam档案的链接。其他信息 (Other Information): 这个页面还包括了一些对所有人可见的信息:文件大小,首次上传时间,最近更新,以及分类标签。在Steam平台更新资源:必须使用跟第一次一样的名字,否则会上传后会变成“新的”资源。如果你对资源设置版本号,比如说MyMod_0_1.esp是你最早上传的mod,但第二次如果你使用MyMod_0_2.esp的话它在Steam平台中就会变成另一个mod了。要让mod在原来的位置上更新,你就要一直使用MyMod_0_1.esp这个名字。常见问题 我不喜欢X,我有改进其的建议! Steam Workshop的存在意义就是为了帮助联系Mod开发者和玩家——满足双方的需求对Steam至关重要。如果你有任何不满,建议或Bug,请在官方Creation Kit论坛让他们知道。 如何更新我的预览图 / Mod描述 / 标签? 你可通过Creation Kit重新上传Mod来更新这些部分,就算Plugin本身不变也可以。 用户如何下载我的Mod的之前版本? 只有Mod的最新版本才会保存在Workshop,并会自动分配给订阅者。最好在本地保存备份,从而在你的Mod的使用者报告不恰当或Bug的时候可以返回至一个之前的版本。 为什么我无法将我的游戏截图作为一个预览文件? 预览图必须是 .jpg格式文件。天际的游戏截图默认保存为 .bmp格式文件。你可以使用免费的图像修改软件,例如Irfanview或Gimp将你的游戏截图转换为可用的预览文件。记住512x512的图像会在Steam Workshop中显示效果最佳。 在我输入我的Mod的更多细节时(在Steam上传对话框,图1),突然被以前的信息覆盖了! 当更新一个之前上传的Mod时,"Getting details on published files…"会在上传对话框的标题栏那出现。一旦Creation Kit完成从Steam Workshop读取你已经上传的细节,它将自动在对话框内填上你现在的信息。如果你在这个过程结束之前输入心的细节信息,它们可能会被覆盖。走运的是这个过程只需要几秒钟的时间。 为何我无法将我的文件拖入Manual Archive Creator?(图2) 为了成为已归档的,文件必须率属于"Skyrim/Data"文件夹,并为一个恰当的,可被Creation Kit解读的格式(例如DDS,NIF,WAV,等等)。注意——对于大多数的用户,Data文件夹的路径类似于:C:\Program Files (x86)\Steam\steamapps\common\skyrim\data 我的Mod基于其他的一些Mod。我如何确保玩家获得了他们需要的全部文件? Steam Workshop目前尚未支持这类型的从属性。确保在你的Mod描述部分包括了所有特殊的说明或要求。 我正在进行我的Mod的一个更新,但是启动文件 (Launcher)下载了我之前的老版本! 如果你在Steam Workshop订阅了你自己的Mod,天际的启动文件 (Launcher)将会将你的Plugin同步至线上保存的版本——不管你的版本是不是更新。不订阅自己的Mod比较好,或者定期备份。
  9. 概述 本章将指导初学者体验他们在Creation Kit的首航,以及让有经验的Mod开发者重温工具界面。 使用者可习得: Creation Kit界面的基本要素 如何导览3D渲染窗口 按钮的用途 当你安装了编辑器并运行后,首先迫切需要做的就是了解在你面前的东西是什么。在此我们将编辑器的各个要素分开说明,让你可对它们有个大致了解。 你可能会想直接跳过并用你自己的方式来学习,特别是如果你对之前的Bethesda Game Studios的工具很熟悉的情况下。这很好,不过如果你在某处卡住了,记得回来看看。 Creation Kit 元素 主工具栏 主工具栏 (Main Toolbar) 是可开启Creation Kit的大部分特性的部分,可通过菜单或各种按钮来实现。它们的数量相当多,全部在此阐述会有些复杂,而由于有这样做的必要,将阐明各类特性的用途。那些对此有兴趣的人可在主工具栏页面阅读各类菜单和按钮的描述。 在你开始之前,首先你必须载入数据。从“文件”(File)菜单中选择“数据..”(Data..),然后双击“skyrim.esm”。左侧的复选框将被选中,表明它将会被载入。 渲染窗口 渲染窗口 (Render Window) 是我们在此教程中与游戏连接的主要方式。现在它只是一个空白板,不过只要我们载入一个单元,该空间的3D图像将会在此出现,并且可以在此工作。 "单元"是Creation Kit可载入的,游戏中的实体区域。它们可以是由载入门锁分开的内部单元(这也是我们在此教程中将使用的单元类型),也可以是在游戏中无缝连接的外部单元。 单元视野 单元视野有五个需要注意的成分。 世界空间 (World Space) 这个下拉清单指明了哪些单元在它下面的清单中出现。内部区域被认为是一个特殊的世界空间。先不管这部分,因为我们首先将着手一个内部区域。 X/Y 当在一个外部区域上动工时,你可通过坐标手动载入一个单元。这在使用内部区域时无关,并且将会一片灰白,除非已经选择了一个外部的世界空间。 单元清单 (Cell List) 这些是构成你所指定的世界空间的单元。同一时间内,只有一个单元可被认作“已载入”。尽管在某些情况时,可能会显示多个外部空间。 参照清单 (Reference List) 这个清单,目前很可能是空白的,将会显示在所载入单元中存在的所有参照物。你可通过这个清单使用参照物,或通过渲染窗口 (Render Window)来影响它们。 过滤方框 (Filter Box) 在此方框内输入文字将会致使参照表单(Reference List)之显示含有这些字符串的物品。例如,我可能只关注与DA10任务相关的物品,而输入DA10将会只显示那些物品。或者,如另一个例子,如果在我的清单内有三个物体:"CaveGHall01","CaveGFloor",以及"TrollFrost",在过滤方框(Filter Box)输入"Cave"将使其不显示"TrollFrost"。不过,在渲染窗口中它仍然可见。 物件窗口 物件窗口 (Object Window)允许我们去浏览各个我们可用于创造游戏的基础对象。 对象的分类和类型有很多种,而我们将会在教程中需要时涉及它们。 过滤 (Filter) 类似于单元视图,对象窗口有一个过滤字段,我们可用于缩小在下方列表中所显示的对象的范围。 分类列表 (Category List) 在筛选 (Filter)的下面则是许多基础对象的分类。高阶用户可能希望使用“全部 (All)”分类和过滤器以便更快地访问项,不过在本教程中,我们将手动置顶所需要的对象位置。 基于物件的列表 (Based Object List) 在窗口右侧是你可在渲染窗口浏览,编辑和(通常是)放置的各类基础对象。 Creation Kit现在有着可同时开启多个对象窗口的功能,这对高阶用户而言相当便利。只需在对象窗口上单击右键并选择“创建新的物件窗口 (Create New Object Window)” 基础物件 vs 参照 对用户来说,理解一个基础物件和一个参照之间的差异非常重要。 无论何时,只要你放置一个物件到游戏世界中,你将创建一个实例——或参照——到那个基础物件。 举例来说,如果我们将一个“TrollFrost”拖到世界中并修改那个参照,或“参照(ref)”,我们所做的改变将只影响这个特定的Troll。 然而,如果我们直接去产生改变到这个TrollFrost基础物件, 所有的Trolls将被影响,包括那些已经创建的。 鉴于此原因,Mod开发者通常会想避免修改其他Mod可能依赖于的,已经存在的基础物件。渲染窗口导览 图2: 载入AnisesCabin01后的默认视图 控制镜头 无论你的计划为何,你在Creation Kit中所做的任何事情都需要你使用渲染窗口 (Render Window)来查看和操作物件。如果你未曾使用过3D,那么学习熟练操纵镜头将是你的重中之重。 首先,让我们先载入一个已存在的单元,从而使我们可查看某些东西。在单元视野中找到并双击"AnisesCabin01"这个内部单元。这将载入这个单元,让它出现在你的渲染窗口中。 注意: 如果不显示任何东西,但你右手侧的列已经被物件所占据,试着双击这些物件其中之一。这样做不仅仅会选择该物件,还会让镜头(视角)集中于该物件。这在当你“迷路”的时候是个很好的重定位方法。 你最初的视野应当在中心位于一个黄色标记上,如图2所示。(如果并非如此,试着按下"M"键以确定标记都可见。否则,你的镜头(视角)可能会就集中在其他地方。这没关系:只需选择任一物件并跟着它) 单击它选择。你应当会注意到一个由绿色,红色和蓝色的细线组成的轮廓图。这是“边界框”,而目前我们只要知道它会告诉我们所选择的物件为何。试着慢慢地前后滚动你的鼠标滚轮。注意这会缩放镜头(视角)。 如果您在渲染窗口的操纵,比如移动摄像头,按 M 按钮启用/关闭标记,使用其他快捷键或者其他任务,但是什么变化都没有,请先确保渲染窗口是出于激活状态 - 你可以“点击进入”渲染窗口让其活跃。下一步,尝试一下摇动镜头。按压住鼠标中键并移动鼠标可达成。 现在,你可能已经找不到你所选择的物件。我们现在可以重定位镜头。有几种方法: 按下Shift+F。这会使摄像头集中于当前所选择的物件。这不会影响镜头的角度,而如果没有选择物件的话,则会重置镜头中心点。 按下"T"。这会产生一个自上而下的视野。如果已经选择了某个物件,这会缩放镜头(视角)至所选择物件的范围。如果没有任何选择项,这仅仅会让镜头旋转至下(如果在摄像头正下方没有东西的话,很可能会让你失去方向感!) 按下"C"。这会循环几个预设的镜头角度,而在其他方面与“T”的效果完全一致。 可使用鼠标滚轮来缩放。 现在我们将尝试旋转镜头。选定某个物件,按住"Shift"同时移动鼠标来让镜头绕着该物件旋转。所选定的对象将一直保持在你的视野中心位置。如果没有选定项,镜头将绕着自己旋转。这对老手会很有用,但会迷惑初学者,并且通常情况下不需要这么做。 熟练地使用这些操作非常重要,因此,不妨花点儿时间载入各种单元并查看。你的首次尝试可能会比较笨拙,不过无需很长时间,这对你而言就会变成一件非常自然的事。 隐藏物件 如果你继续缩小直到看到整块 AnisesCabin01 ,再点击你之前选择的黄色标记...你会发现这次没法再选到它了。这次你选到的是围绕该空间的一大块粉色方框,在单元视野窗口中你会看到它的名字是defaultSetStageTRIG。 这是一种很常见也很容易解决的问题。选择defaultSetStageTRIG后,敲两次数字1。defaultSetStageTRIG就会消失,再敲一次1,它又会出现。 当你第一次敲数字1时,所选物件会忽略你的鼠标,这样你就能透过该物件。如果它们不是透明的,这边也会变成透明(defaultSetStageTRIG本身就是透明的,所以这边不会有变化),同时单元视野中,该物件的名字会从黑色变成淡蓝色,表示该物件已经被隐藏。 第二次敲1以后,该物件就完全隐藏了,而不是被“鬼隐”。 第三次就会恢复正常,完全可见,可点击,单元视野中名字又是黑色的。 综上,按数字1键直到defaultSetStageTRIG消失,然后选择,隐藏和鬼隐其他物件,比如墙壁和家具,一直到找到你想要编辑的物件。 如果你在物件被鬼隐或者不可见的情况下取消选择,你将无法继续在渲染窗口中选择它,而应该在单元视野选择(因为被鬼隐或者隐藏,此物件现在已经是淡蓝色的了),再点击渲染窗口的标题栏,然后敲数字键1知道该物件再可见。 渲染窗口的许多快捷键中,窗口必须是激活状态(标题栏不是灰色淡出)。如果你选择了其他窗口,比如单元视野窗口,快捷键就无效了。所以,如果你再单元视野中选择了某个物件,你需要再点击渲染窗口的标题栏,然后快捷键才能有效。如果你完全乱了哪个物件被鬼隐,哪个物件没有,你可以敲 F5 件把所有物件恢复到他们的正常可见、可点击状态。 操控物件 现在,我们对如何查看物件已经有了个大致的概念,那么就让我们看看如何移动它们。 注意: 当你练习使用这些操控时,最好是使用一个无关紧要的插件,从而避免保存偶然的修改到所选物件而可能会影响其他的Mod或基本的游戏。你可能会想使用在教程开始时所创建的 testquest.esp 文件。 在渲染窗口 (Render Window)选择任一物件。你可以通过按住鼠标左键并拖曳对象来移动或“平移”它。 你可能会注意到该对象会跳转而非平滑移动——这是因为栅格捕捉正处于开启状态。你可通过快捷键"Q"或在主工具栏的来开关它。我们会在布局章节讲述更多有关栅格捕捉的内容。 同时要注意你现在仅仅是在水平面上移动物件,或者说是游戏时间诶的“X/Y 轴”。你可按住"Z"并拖曳对象来上下移动它。同样地,你可以按住"X"将移动限制在x轴上,或者"C"将移动限制在y轴上。注意是"C"不是"Y",因为"Z"、"X"、"C"三个键是紧挨着的,比较方便! 旋转只需选择一个物件,对其按住“鼠标右键”并移动鼠标来达成。旋转也受到角度捕捉的影响(或者渲染窗口下按Ctrl-Q),也可在旋转时按住X,Y或Z来将旋转限制于这些轴上。你还要注意物件绕着一个由小的黄色的"+"所指示的中心点旋转。对于将会经常使用渲染窗口的用户而言,多加留心中心点很重要。 物件也可以缩放。按住"S"同时用鼠标左键拖曳来缩放。不过缩放的物体可能会看起来不协调(或超出范围),因此除非绝对需要,最好不要使用这个工具。 撤销修改 如果你不小心处理错误,不要担心!你再渲染窗口做的任何操作都是可以通过点击主工具栏的撤销按钮,或者快捷键"Ctrl-Z"来撤销的。快速按Ctrl-Z会撤销许多操作。你可以自己操作看看。 如果你撤销过头了,恢复按钮就是你的好友了,除非你在渲染窗口执行了新的操作,否则任何你撤销但是没恢复的操作都会丢失。 Gizmos 移动,旋转和缩放设置 Creation Kit提供了一些“gizmos”, 或移动,旋转和缩放的可视助手。总的来说,这些Gizmo为移动物件提供了一个可视化的帮助。"Gizmo handles"是这些小装置的可点击的独立部分,激活Gizmo的该部分。 例如,在移动Gizmo中点击绿色箭头将会把移动限制于Y轴。 "E" 开启移动gizmo "W" 开启旋转gizmo "2" 开启缩放gizmo Gizmo的使用上有几条规定: 一次只能开启一个gizmo。 在某个gizmo激活时,正常的物件操纵不可使用。 "G" 可用于开关总体 / 局部的移动和旋转。 总体 / 局部的转换在非gizmo下的旋转时也可使用(但不在移动时)。 稳步前进! 当你已经熟悉在编辑器里“四处溜达”,你已经准备好制造一些内容! 如果你对建立空间感兴趣,不妨到关卡设计教程系列去看看。 或者,如果你对任务和角色更感兴趣,你可以开始阅读]任务设计基础教程系列。
  10. 概述 本教程将展示如何使用脚本提升依靠玩家行为的任务线。我们先前展示过如何给对话编写脚本,但是这里将展示如何将按照意愿编写的脚本与角色和游戏目标关联。 你将学到: 脚本链接目标的基本构架 如何在新的脚本语言中响应事件 (对于脚本的简单介绍,请参阅"Hello, World" 教程) Papyrus Creation Kit使用的脚本语言被称为Papyrus,Papyrus脚本是一个普通的文本文件,编译成字节码后在游戏运行时执行。 新的系统和旧的TESScript很相似,但是思路上要略微改变。你不再可以熟练地控制目标;在脚本编写过程中可能因为困难而中断;你必须有耐心面对一系列括号。简而言之,它更像Lua或Python之类的真正程序语言。如果你很熟悉TESScript,你可以参阅过渡教程。 首先我们要添加关于一个贼被杀死的脚本,任务适当地更新。 添加脚本 打开我们之前创建的GSQThief角色。我们将关注"Papyrus 脚本"区域。 点击“添加”按钮,打开我们可以对这个角色添加的脚本的列表。我们新建一个,在列表顶部双击"[新建脚本]"。 这将会弹出另一个窗口,你可以在这里对脚本命名。输入名称"GSQThiefScript"并点击确认。 现在你已对角色添加脚本。但是你会发现这里没有任何东西。 属性 双击脚本名称打开属性窗口。然而,脚本不知道游戏中的目标及其链接的对象。我们用属性将目标勾在一起并使之影响其它。在此案例中,我们设置GSQ01为阶段20即盗贼被杀之时。所以我们应该对此设计脚本。 点击窗口左下角的“添加属性”按钮。这是我们要创建属性的地方。在输入区域,从下拉选项中选择“任务”,或直接输入“任务”,名称区域输入"TutorialQuest",其余区域空白。 现在我们告诉脚本我们关心的是"TutorialQuest",但是脚本不知道那个任务是什么。点击刚才的那个属性,,并点击窗口右侧的“数值编辑”。一个新的显示所有任务的下拉目录将出现,选择"GSQ01",并点击确定。 编写脚本 现在我们需要添加属性,我们必须添加真正意义上的脚本!邮寄脚本名称并选择“资源编辑”从而打开脚本编辑器 最上方一行(Scriptname GSQThiefScript extends ObjectReference)是我们创建脚本时编辑器提供的模板文件,下面的一些段落是我们制作的属性的版本号。 在文本底部,输入: Event OnDeath(Actor killer) TutorialQuest.SetStage(20) EndEvent 如果你对程序设计或脚本编写了如指掌,你将会很容易看懂这些内容,因此你可以选修“编写道具脚本”。但对于大多数人来说,我们应该按部就班地学习。 Event OnDeath(Actor killer) 这一行表示角色死后脚本的反应。当指定角色死后,游戏将从这里执行代码。游戏将对杀死角色的那个人赋予变量"killer" TutorialQuest.SetStage(20) 如同在对话教程中我们对存在的任务命名SetStage一样,这将会设定阶段20 缩进可以使之易读 EndEvent 这个标志事件的终结 从文件目录选择“保存”(或按Ctrl+S),编辑器将自动编译脚本。输出窗口将弹出。如果哦脚本语法正确,将如下图所示: 如果你在这里看到其它东西,那么脚本肯定错误。重新检查并编写! 一切完成,关闭脚本窗口和角色窗口,并保存插件。现在是对道具编写脚本的时候了。 编写道具脚本 打开上一章我们编写的GSQAmulet项目。脚本编写区域在护甲窗口的右边。 按照先前的方法创造一个新脚本,并命名为GSQAmuletScript。添加一个任务属性,将"TutorialQuest"改为"GSQ01"。 它将自动给属性赋值,由于你给属性一个按照存在ID命名的名称。如果你声明很多属性,这种方法将会节约很多时间。 打开脚本,我们将在此添加一个事件。护身符当然不能死亡,但是我们必须知道什么时候玩家捡起它。事件脚本将如下所示: Event OnContainerChanged(ObjectReference newContainer, ObjectReference oldContainer) if (newContainer == Game.GetPlayer()) GSQ01.SetStage(30) endif EndEvent 编写脚本时有以下注意事项: 声明的参数(newContainer和oldContainer) 将会在脚本处理事件时自动被游戏填写。 因为无论何时这个目标被转手,OnContainerChanged事件都将激发。我们必须确保在玩家谈论它时我们只设定此阶段,因此锁住if/endif Game.GetPlayer()是一个不设定属性而获取玩家的引用的方法,当激发事件时newContainer自动被程序填写,因此我们在第2行做一个比较,从而确定项目被移给玩家。 当编写脚本时,SetStage必定在玩家捡起护身符时被调用。这并不会影响其它游戏内容,因为每个人物阶段只会发生一次,调用SetStage(30)只会在第一次起作用。 这个脚本以后依然可以继续。关闭脚本和护甲窗口并保存插件。 你现在能从头到尾运行这个任务,用控制台输入SQV检查阶段的改变。 虽然玩家不能对此使用控制台,但我们将在下章讲到如何给任务反馈信息。 在将脚本编译为字节码之前脚本将以文本文件的形式存在于data文件夹内。这意味着如果你用合适的编辑器,你能更改这些脚本。我们可以安装Sublime Text和Notepad++,它们能将语法高亮、基本内容自动完成、简便编辑。如果你要大量编辑脚本,这些工具将会使其更为简单。 注意 输入错误的代码后,Papyrus 框可能会变得不稳定。窗口中选择OK,尝试重启CK应该能解决问题。 从 Papyrus 框中移除保存的脚本并不会自动删除游戏目录中的文件,这会对游戏测试产生影响。
  11. 概述 本章将讲述如何创建任务物品--Bendu Olo的护身符。 你将学到: 如何新建一个物品 如何在角色的物品栏中添加物品 创建/复制物品 作为制作MOD的必要过程,你要复制一个已存在的物品并改变它的数值。首先你已经有一个设置好的3D模型,然后你从你已有的工作开始,所以你不必在各种详细设置上浪费时间。 实际上,我们经常在有完整计划之前就直接复制一个已存在的物品。很多如木桶一样的物品都保持原有模样。 对于这个护身符,我们复制黑暗兄弟会任务中的上古会议护符代替。在目标窗口,依次选择Items -> Armor -> AmuletsandRings 双击"ElderCouncilAmulet"打开护甲窗口。 我们对它进行少许改变使其适应我们的任务。 ID: 改为"GSQAmulet" Name: 改为"Bendu Olo's Amulet" Value: 改为250(Bendu严格意义上不是一个人) 保留其余数值选项,然后点击“确定”按钮。由于我们改变了ID,我们将被询问是否按照这个属性创建一个新目标,或者改变一个存在的东西。我们需要制作一个新目标,所以点击Yes按钮。 就是这个样子!现在我们制作的是一个防具,仅仅是使用先前学到的复制物品技术。你能通过改变ID制作新的武器、种族、生物、魔法、配料等等。 我们要做的最后一件事是将这个物品添加到贼的物品列表中。再次打开角色GSQThief,然后点击角色窗口下的物品清单标签。 首先你将发现它是灰色的,这是因为角色是基于模板制作的。但是我们能够简单的忽略一个模板的一部分。在窗口的左下角取消选择“使用物品清单”然后物品清单区域将亮起来。 我们还要给强盗一件外套,由于默认模板已经打开,从默认外套下拉目录中选择"BanditArmorMeleeHeavyOutfit"。 将新增物品添加进角色的物品清单,右击物品清单桌面并选择“新建”,这样在游戏中添加第一个目标(按首字母排序)。在目标下拉目录中添加我们刚刚创建的物品(GSQAmulet),盗贼的物品清单完成!
  12. 概述 本章将讲到Creation kit的脚本语言Papyrus的基础知识。 你将学到: 如何对一个目标设计脚本。如何对一个事件设计脚本。脚本制作首先,我们编写一个新的脚本,并用它关联一个目标。运行Creation kit,然后选择一个测试房间并加载。我们以莫拉格·巴尔的房屋为例。 我们从活动物体列表上选取WETempActivator并放入房间,它只是一个临时的石柱。 双击目标物体打开Reference窗口,点击Scripts标签,这里有我们需要添加的物体。 点击Add按钮,弹出"Add script"窗口。 双击列表内的"[New Script]",打开脚本添加窗口,将名字改为你编写的脚本的名字(在此以"HelloWorldScript"为例),然后点击"OK"。 你将会看到石柱脚本列表里加入了你编写的脚本: 点击"OK"保存你的修改。恭喜你!你已经成功编写一个脚本并将其与目标关联。 添加事件 你的脚本当然可以不包括任何事件-这只是一个等待使用的空模板。 因为这个目标包含事件,当它被玩家点击时必须有所反应,所以我们将学到如何通过脚本使它达到这种效果。 再次打开石柱的Reference窗口,然后右击位于Script标签下的HelloWorldScript。选择"Edit Source",将打开脚本编辑窗口。 然后我们需要用脚本控制目标,这意味着我们必须对脚本添加活动事件,添加以下代码: Event OnActivate(ObjectReference akActionRef) endEvent现在,不要担心活动事件的语法规则很困难-我们将在以后学习如何处理这些问题。现在,我们只须学到如何在游戏中添加脚本。如今,我们的脚本已经将事件关联。然后我们让它出现一个显示"Hello, World!"弹窗,只需在脚本中添加以下代码: Event OnActivate(ObjectReference akActionRef) Debug.MessageBox("Hello, World!") endEvent顺便一说,调试失败将会出现以下内容: 在File目录下点击"Save"(或Ctrl+S)保存并编译你的脚本.如果输入无误,你将看到: "Hello, World" 现在进入游戏并进行试验。 (请事先确保保存你的MO并加载它) 一旦你进入游戏,键入~打开控制台,输入: coc MolagBalVoiceCell进入房间, 走进并激活石碑,你将看到一个新的对话框: 就是这个,你做出一个能按照你设计的语句回答的物体! 接下来,你可以学到使用变量和条件语句让你编写的脚本更完美.
  13. 概述 持久化是保证目标参照停留于游戏中且不被卸载的一种行为。它应用于游戏中包括角色参照在内的所有参照。由于它们持续停留,因此它们要消耗大量的系统资源并使位于相同区域的其它引用消失和卸载。 如何使目标持久化 想知道如何保留一个目标,请看以下内容: 函数 当一个函数运行在目标之上时,这个目标将停留直到函数退出。这同样适用于长期运行的隐藏函数。 范例 Function MyFunction() ; 我们进入此脚本中的一个函数 - 我们要链接的那个目标此时已持久化 Util.Wait(300) ; 我们需要等待300秒 - 目标依旧持久化! ; 现在当我们无需持久化时我们保留一个函数(除非存在持久化的必要) EndFunction属性当一个脚本属性指向编辑器中的一个参照时,目标参照将会被标记为“永久持久化”。换句话说,如果在运行时刻不做任何事那么目标将被卸载。这意味着,如果有可能的话,你不应该直接用属性指向一个参照。如果可以的话,可以从事件或其它位置中拉取参照,从而避免它们永久停留。即使你在游戏运行时重新对属性分配值,原先的参照依旧会停留。 范例 ; 此属性在编辑器中已被设定 - 当游戏运行时它被加载且永久停留! ObjectReference Property OtherReference auto Function MyFunction() ; 处理OtherReference并保证其持续运行 OtherReference.Activate(Game.GetPlayer()) ; 然而,即使清除或重新分配属性,原先目标依旧会停留! OtherReference = None ; Original object doesn't die! EndFunction变量当任何指向参照的变量被加载时,参照会临时持久化。它将持久化直到不再有变量指向它,然后它被卸载。(假设没有其它游戏系统使目标存活)这意味着你不应该在你要需要它们时试图使目标保持一个变量。你可以通过对其分配"None"来清除变量。 范例 ObjectReference myScriptVariable Function MyFunction(ObjectReference myArg1, ObjectReference myArg2, ObjectReference myArg3) ; 只要函数运行,myArg1, myArg2, and myArg3 都会持久化 myArg1.Activate(myArg2) ; 如果对其中任何一个分配其它目标,那么原先的目标都会消失 myArg1 = None ; Original reference may die! ; 如果对其中任何一个分配一个脚本变量,目标将会停留! myScriptVariable = myArg2 ; Will now stick around until myScriptVariable is cleared or re-assigned ; myArg3 将会停止持久化,如果我们不对其分配其它变量 EndFunction注册事件任何被注册接收不同事件的参照都会持久化,直到它取消注册事件。这应用于任何你明确注册的事件(包括但不限于)OnUpdate, OnLOS, OnSleep,和OnAnimationEvent 范例 ; 这个链接的目标在取消注册前都会持久化 RegisterForUpdate(5.0) ; 我们链接的目标将不再持久化(假设没有任何必要使其存活) UnregisterForUpdate()总结当要让其持久化时请记住以下内容: 避免长时间运行的函数尽量避免参照属性参照变量应只指向一个参照只有注册表可以更新
  14. Papyrus是一个'多线程脚本语言。本质上,这意味着游戏可以同时运行多个脚本。这使得Creation引擎能更为有效地控制脚本进程。Papyrus编写者必须大致精通它们的作用。 本章主要面向新手讲解线程。 线程 基本要点如下: 只有一种东西可以在一个脚本中做任何事。 如果多个线程试图同时控制脚本,那么“队列”将会控制它们的先后次序。 如果脚本调用外部脚本的函数,或者调用一个“潜在的”函数(在返回前运行的函数),那么当其它线程停止工作时它将开始下一个。然后一个新线程要占用脚本,当先前线程停止后又想重新继续原先进程时,它必须等待直到新线程完成脚本(或者它调用一个潜在函数或其它脚本的函数) 例如: ObjectA在脚本中使用函数:DoSomething() ObjectB和ObjectC同时调用ObjectA.DoSomething() ObjectB完成运行ObjectA的DoSomething()后ObjectC才运行它。 但是如果ObjectA脚本中的DoSomething()如下所示: Function DoSomething() SomeThing1() ;another function in ObjectA ObjectD.DoSomethingElse() ;a function in a script on ObjectD Something2() ;Another function in ObjectA EndFunction然后第一个ObjectB调用ObjectA.DoSomething(),当ObjectA调用ObjectB的Something1()时,ObjectC会按顺序等待,而非运行ObjectA的DoSomething()。但是一旦因为ObjectD.DoSomethingElse()在不同脚本/目标中调用函数而导致ObjectB的线程调用它,那么ObjectA将会示意正在等待调用DoSomething()的ObjectC 现在ObjectC的线程正在运行SomeThing1(),ObjectB的线程完成ObjectD中的DoSomethingElse,但是在它运行Something2()之前它必须按顺序等待直到ObjectC完成Something1()并移出ObjectD的DoSomethingElse()。一旦ObjectC的线程冲向ObjectD的DoSomethingElse(),ObjectA示意从ObjectD的DoSomethingElse()返回的ObjectB的线程,ObjectA将会被允许继续Something2()等等。 多线程: 一个基本案例 试想一下,玩家激活一个操作杆,游戏将给Papyrus一个激活事件的通知。因为我们的脚本包括一个onActivate()事件,所以游戏创建一个"线程",你可以将其想象为脚本中命令的复制品。按照序列等到那个线程时,它将立即运行。 也许在下一个框架中,游戏将会处理线程。现在我们假设onActivate()事件的内容如下所示: ; Note - this is a non-functional snippet EVENT onActivate() wait(5.0) player.additem(gold001, 10) endEVENT玩家会触发一个开关,这个开关会在5秒之后通过Papyrus创建一个包括一些命令的线程,10个金币将会加入玩家的物品栏。这都是自动完成的。 现在玩家在短时间内多次触发杠杆。这是合法输入,因此脚本可以接受这个信息。在第一个被触发的事件中,线程被创造并开始运行。在紧接而来的第二个活动中,另一个线程将会在第一个之后被短暂建立。只要玩家触发杠杆,它就会继续运行。 在线程正常情况下,脚本也许会在敏感时刻出现问题。Thread #1并不了解Thread #2,因此每一个线程都会等待5秒钟并给予玩家金币。 这会导致快得无秩序,我们必须让一切具有组织性。 以上案例的脚本表现迫使我们要重写它,在第一个后面禁用大量活动。解决之道多种多样,例如我们可以借助状态解决它。 如果要创建用于反应多种情况下的事件的脚本,那么状态将是一个不错的选择。以下就是一个伪脚本案例。 ; 注意 - 这是一个无函数片段 STATE readyState EVENT onActivate() gotoState("emptySTATE") wait(5.0) player.additem(gold001, 10) ; 想要延迟5秒钟触发活动?请在此添加gotoState("readyState") endEVENT endSTATE STATE emptySTATE ; 此状态不包括任何事件 ; 注意 - 其它任何想关联此引用的脚本将会一直运行它们的活动事件 endSTATE注意 "GoToState()"在先前的案例中,一定要注意goToState()的调用。因为additem()是一个外部脚本(和下方案例中的setValue()类似),如果其它线程想要在它预计所在的状态之前运行你的脚本,那么它将对此线程创造一个机会。因此,首先要跳转到你期望的状态。你的线程将会完成调用的gotoState()下方的命令,以后的线程将会把脚本按状态对待 - 例如以下案例中的"XYZ"。 换句话说,不要这么做: gigawatts.setValue(1.21) GoToState("XYZ")而应这么做: GoToState("XYZ") gigawatts.setValue(1.21)其它案例在一封寄往Jeff的电子邮件中有这么一个案例:在“安全线程”一行中,如果多次从外部脚本中调用IncrementMyProperty ...假设这些函数和属性都在相同脚本中。 Function IncrementMyProperty myProperty += 1 endFunction在100%的“安全线程”中,myProperty将会正确地增长。只要在你知情的情况下,此刻它将会解决2个函数的调用。 Function IncrementMyProperty myProperty += 1 if myProperty == 5 ;做某事 endif endFunction[code] 在执行“做某事”时,myProperty将保证达到5。如果“做某事”潜伏,或调用一个在脚本或其父类外部的函数时,myProperty也许会改变。 [code]Function IncrementMyProperty myProperty += 1 if myProperty == 5 myQuest.setStage(10) endif endFunction ;Elsewhere In MyQuest’s stage 10 fragment: If myProperty == 5 ;do something endifSetStage潜伏,因此myPropery也许会改变,即使你使用“SetStage(10)”并使脚本继承一个与之关联的任务。在片段案例中,myProperty完全有可能因为片段的运行而不等于5。记住:你调用的是setStage而非一个函数。片段执行“以后的次数”(将会在大多数事件中发生)。在调用一个函数比调用一个片段更为有效的情况下,这种情况很容易发生。 请注意: 如果myProperty不是你的本地脚本,那么“myProperty += 1”也许不会增加。这是因为它分解为“myProperty.set(myProperty.get() + 1)”,以及myProperty的值也许会在读取调用中有所改变。由于它不是在你自己的脚本中。[ 编辑器提示这是因为你必须在其它脚本中写入一个IncrementMyProperty()函数-- JPD ]
  15. 概述 脚本继承是一个用来指示脚本要做什么的行为,可以在不修改原始脚本的情况下通过修改一些事件或函数来做不同的事情。 术语解释 父类:被继承的原始脚本。子类:继承父类的脚本。继承:如果一个子类在没有更改父类的前提下从父类获得东西,那么它“继承”父类。优先权:如果一个子类脚本通过创建新的版本来改变父类中的一些行为,那么它“优先于”父类版本。如何继承继承一个脚本非常简单:在脚本上方的"scriptname x"后面添加"extends y",y是你要继承的脚本名称。 例如: Scriptname SpikeTrap extends Trap在此情况下,"SpikeTrap"脚本继承"Trap"脚本。你可以认为它们之间有某种联系。继承脚本有何作用 函数和事件 子脚本可以读写父脚本中的所有函数和事件。任何不在子脚本中执行的函数和事件都要通过继承获得。如果函数和事件在父脚本中执行,而你又想让它在子脚本中执行,当子脚本的优先权高于父脚本时,那么任何对函数和事件的调用将使用子脚本的版本代替。注意当你在子函数中执行函数或事件时,你“必须”使参数和返回类型匹配,名称可以除外。如果你的脚本没有汇编。 例如: Scriptname ParentScript Function MyFunction() Debug.Trace("Parent MyFunction!") EndFunction Function MyOtherFunction() Debug.Trace("Parent MyOtherFunction!") EndFunction Scriptname ChildScript extends ParentScript Function MyFunction() Debug.Trace("Child MyFunction!") EndFunction链接子脚本并从中调用MyFunction: Child MyFunction!子脚本具有优先权,并把父脚本中的函数替换为它自己的。链接父脚本并从中调用MyFunction: Parent MyFunction!在此情况下子脚本并不存在,旧的父函数功能保持不变。链接其它脚本并从中调用MyFunction: Parent MyOtherFunction!子脚本对此函数并不具有优先权,因此原始父函数可以使用。从父类中调用函数 如果你希望真正从父脚本中调用函数,并忽略你自己脚本中的函数,那么你可以使用特殊"父"变量。注意此变量只工作在此脚本中,你不能在不同脚本中调用它。如果你想在父脚本完成所有繁重任务之前对某些内容做细微调整,那么可以使用这个方法。 例如: Scriptname ChildScript extends ParentScript Function MyFunction() Debug.Trace("Child MyFunction!") parent.MyFunction() EndFunction在子脚本中调用MyFunction并链接一个目标: Child MyFunction! Parent MyFunction!注意父变量会强迫调用父版本的函数,而非子版本的函数。状态 子脚本的状态会和父脚本的状态相结合。子类的相同状态的函数将会优先于父类中的函数,所有的状态会结合。 父类有状态,子类没有:此状态的父函数将会被使用。子类有状态,父类没有:此状态的子函数将会被使用。父类的函数有状态,子类的没有:父版本将会被使用。子类的函数有状态,父类的没有:子版本将会被使用。所有函数有相同状态:子版本将会被使用。属性属性在父脚本中的表现和在子脚本中的类似。然而,你不能在子脚本中拥有做其它事情的特权。如果你试图做权限以外的事,那么编译器将提示你。 例如: Scriptname ParentScript int Property MyProperty Auto Scriptname ChildScript extends ParentScript Function MyFunction() Debug.Trace("MyProperty = " + MyProperty) EndFunction注意MyProperty在子脚本中使用,即使它存在于父脚本中。变量 变量是私有的且不能被其它包括父脚本和子脚本之类的脚本看到。变量名称和父脚本中的相同将会在读写子脚本函数时隐藏父变量。父类的函数将使用它自己的变量。 例如: Scriptname ParentScript int MyVar = 1 Function MyFunction() Debug.Trace("Parent MyVar = " + MyVar) EndFunction Scriptname ChildScript extends ParentScript string MyVar = "Hello World!" Function MyFunction() Debug.Trace("Child MyVar = " + MyVar) parent.MyFunction() EndFunction调用子脚本中的MyFunction并链接一个目标: Child MyVar = Hello World! Parent MyVar = 1注意所有脚本只能看见变量的本地复制。
  16. 概述 脚本运行在变化的状态下 - 但是脚本在一个时刻只能有一个状态。当调用函数或事件发生时,代码的运行取决于脚本所处的状态。 定义状态 在脚本中定义状态非常简单,只需如下设置: state MyState ; 不同函数在此 endState如果你希望你的脚本以特定状态开始,只需在state前面加上"auto": auto state StartHere ; 函数在此 endState'空状态''空状态',顾名思义,就是所有函数都不在state语句块所属的地方。 function MyFunction() ;此函数处于'空状态' endFunction state MyState function MyFunction() ;此函数处于MyState状态 endFunction endState在状态中定义函数/事件To define a function or event inside a state, simply put the function or event inside the state block. state MyState function SpecialFunction() ; 此函数处于MyState状态 endFunction endState请注意:函数和状态必须以相同的返回类型和相同参数类型定义在'空状态'中,否则编译器将发出错误警告。这是因为游戏解决函数和事件的过程运行于游戏运行时。 function SampleFunction(int myParameter) ; 代码在此 endFunction state SampleState function SampleFunction(int aParameter) ; 此函数无误 - 参数类型和'空状态'相同 endFunction function OtherFunction() ; 错误!其它函数并不存在于'空状态'中 endFunction endState state OtherState int function SampleFunction(int myParameter) ; 错误!此函数返回类型和'空状态'中的所有类型均不匹配 endFunction endState state YetAnotherState function SampleFunction(int myParameter, float parameter2) ; 错误!此函数参数和'空状态'中的所有参数均不匹配 endFunction endState为了方便,只要是你继承的脚本中的空状态,不同种类脚本中的事件都能收到它里面定义的东西(换言之,目标参照、角色等等)。因此当你要在特定情况下修改事件时,你不必在每个空状态内定义事件。如何选择函数 调用一个函数或目标接收事件时,函数选择遵循如下规则: 如果当前状态下脚本含有函数,那么调用它如果当前状态下脚本继承另一个有函数的脚本,那么调用那个脚本中的函数如果在'空状态'下脚本含有函数,那么调用它如果脚本继承另一个在'空状态'下有函数的脚本,那么调用那个脚本中的函数长话短说,状态内的函数优先于'空状态'内的函数,脚本内的函数优先于继承其它脚本得到的函数。如何设置脚本状态 设定脚本状态非常简单,只需调用GotoState和状态的名称。事件顺序如下所示: OnEndState在脚本中标明之前的状态状态转换为所需状态OnBeginState在脚本中标明进入时的状态进入时状态的名称并非敏感事件。如果你想让其进入'空状态',只需简单的使用空字符串: ""同样调用GotoState将不会结束你的函数 - 它将继续运行,因此会受部分代码的影响而改变目标状态。 function MyFunction() GotoState("TurnOffActivate") ; 一堆长期运行的东西 GotoState("") endFunction state TurnOffActivate event OnActivate(ObjectReference akTriggerRef) ; 不做任何事 endEvent endState如何获取脚本状态获取脚本状态依然很简单,调用GetState(),它将返回当前脚本所处的状态名称。请注意状态名称取决于在首个地方设置的状态。字符串比较是'敏感事件,因此请谨慎处理字符串比较。(以后我们将介绍忽略事件的字符串比较 - 现在需要担心) 使用技巧 在特定情况下,如果你想禁用一个事件或函数,那么可以简单地定义事件和函数并将状态设为空。此方法可以帮助你忽略触发事件。如果你认为在一个时刻只有一个状态是一种限制,那么记住你可以把多个脚本关联一个目标 - 每个脚本都有各自的状态!它能帮助你解决复杂的运算。当然你也可以用你自己的状态变量。因为状态名称是简单字符串,所以你能在运行时刻使用"+"运算符构建它们。例如,你可以按如下方法定义"State1", "State2"等等。 Function GotoNumberedState(int number) GotoState("State" + number) EndFunction总结状态是改变脚本行为的一个强大又合理的方法,它不必书写大量的if条件语句或其它复杂代码。问题有时看起来复杂和麻烦,但运用状态可以使其变得简单!
  17. 描述 数组是一种特殊的变量,它可以包括一个以上相同类型的值。你可以通过一个数字顺序索引选择一个值,它的范围是从0到数组长度减一。 声明数组 float[] myFloatArray ObjectReference[] myObjectArray = new ObjectReference[10]myFloatArray是一个浮点型空数组。它的长度为0,且不等于任何数。 myObjectArray也是这样,不同的是它是目标ObjectReference的数组。它对目标ObjectReference指定一个含有10个元素的新数组(全部默认初始值为空值)注意你不能设定数组的元素为数组或一个多维数组。 你当然可以将属性设置为数组,编辑器将会展现一个特殊用户界面,方便你添加、移除和重新设定数组里的元素 例如: Weapon[] Property MyWeapons Auto新的调用必须在函数中完成。你不能将它用于函数以外的目标变量。如果你希望在空数组中设定变量,那么你要把数组设定在初始化事件中。函数参数 读写数组的方式和读写函数参数的方式一样,使用和声明数组相同的语法。 例如: Function MyArrayFunction(int[] myArray, bool someOtherParameter) EndFunction函数返回值将函数值返回到数组中,使用和先前相同的语法。 例如: int[] Function MyReturnArrayFunction() int[] someArray = new int[10] return someArray endFunction创建数组要想创建一个数组,在数组元素类型前使用关键字"new",并在方括号中输入数组长度。数组长度必须是介于1和128之间的整型数 - 不能使用变量。换句话说,在汇编时就要知道数组长度。数组中的默认初始值是0、假、""、空 例如: new bool[5] new Weapon[25]通常情况下你直接声明数组变量,但是如果一个函数需要一个数组,那么你可以在函数调用中创建一个新数组。例如: MyArrayFunction(new int[20])读取/写入元素要想从一个数组中读取写入一个简单值,可以在变量名称后使用包括所需元素个数的括号。下标可以是一个整型变量、整型数或表达式。数组下标是从0(首个元素)到数组长度减1的可用值。 例如: myArray[20] = newValue someRandomValue = myArray[currentIndex] myArray[i * 2] = newValue如果数组元素是其它脚本,你可以按照相同方法存取属性和函数。例如: DoorArray[currentDoor].Lock() objectXPos = ObjectArray[currentObject].X注意:由于数组是通过引用传递和分配的,因此任何对数组元素的修改将会反映在任何与此数组有关的其它变量中。读取长度 你可以很容易地通过调用长度属性获取任何数组的长度。如果你没有给数组分配任何元素,那么它的长度将为0 例如: int ArrayLength = myArray.Length分配/传递数组给数组分配其它数组,或给函数传递数组,方法和变量一样。然而,注意数组是由引用传递和分配,类似于目标。换句话说,如果你将数组分配给其它数组或函数,方法和先前一样 - 如果一个被改变,那么另一个将会受影响。 例如: int[] Array1 = new int[5] int[] Array2 = Array1 ; Array1 and Array2 now look at the same array! Array1[0] = 10 Debug.Trace(Array2[0]) ; Traces out "10", even though you modified the value on Array1分配数组数组可以分配字符串或逻辑值。如果你给数组分配一个字符串,它将会把每个元素放入括号中,并用逗号分开。如果数组特别长,那么它将会在末尾予以省略。如果给数组分配一个逻辑值,那么当长度不为0时它将为真,如果长度为0时就为假。你不能给数组分配不同类型的元素,即使能成功分配。 例如: Debug.Trace(MyArray) ; Traces out "[element1, element2, element3]" or "[element1, element2, ...]" if the array is too large if (MyArray) Debug.Trace("Array has at least one element!") else Debug.Trace("Array has no elements!") endIf常见任务修改每个元素 你也许想多次修改数组中的每个元素。可以简单的通过循环语句设定,然后对每个元素进行修改。 例如: Function DisableAll(ObjectReference[] objects) int currentElement = 0 while (currentElement < objects.Length) objects[currentElement].Disable() currentElement += 1 endWhile EndFunction注意:只要元素序号低于数组长度,循环就会进行下去。这是因为可用元素序号从0到数组长度减1。如果你在循环条件中使用"<=",那么最后一个元素将会出错,因为下标为数组长度的那个元素超出范围。
  18. 描述 变量和属性非常相似,它们都控制着值和目标。变量是私有的,因此只有脚本使用它们并对它们赋值取值。属性本质上是被其它脚本存取的变量,其它脚本可以对其赋值取值。 如果变量或属性控制着一个数值,那么它可以存取返回这个数值。如果变量或属性控制着一个目标,那么你可以存取目标的属性或函数。(类似于从旧的脚本系统中引用变量) 声明变量 float myFloat float myOtherFloat = 13.5MyFloat初始值为0,myOtherFloat初始值为13.5并能通过修改脚本更改。声明属性 完整属性 要想定义一个属性,你首先要输入类别,然后是“属性”,然后是属性的名称。接着你定义2个函数,一个用于返回变量的值,一个用于给属性赋值。然后通过"EndProperty"结束属性的定义。 例如: int myInt_Var = 0 ; Where the property's value is stored int property myInt int function get() return myInt_Var endFunction function set(int value) myInt_Var = value endFunction endProperty如果忽略取值函数,那么此属性只有其他拥有只写权限的人才能对其赋值,但是不能读取它。当然,局部脚本可以读取应用于属性的实际变量。如果忽略赋值函数,那么此属性只有其他拥有只读权限的人才能读取值,但是不能改变它。同样,局部脚本总是可以设定属性返回的变量。你不必总是按照上述方法返回设定值 - 函数可以做任何事。例如,你可以建立一些条件待定函数,从而确保值不会溢出。或者你可以在赋值时播放动画。你不必在任何时刻都有一个实际变量 - 它可以是一个计算值或一个常量 例如: bool property Locked bool function get() return IsLocked() endFunction function set(bool value) Lock(value) endFunction endProperty上述常量隐藏了锁定和已锁定目标参照函数,从而可以通过简单的设定锁定函数值的真假来锁定和解锁目标。例如: int myVar = 5 int property ReadOnly int function get() return myVar endFunction endProperty以上属性是只读权限。外部脚本不能改变值,但是脚本本身可以改变变量的值。例如: int myVar = 5 int property WriteOnly function set(int value) if value >= 0 myVar = value else myVar = 0 endIf endFunction endProperty以上属性是只写权限。外部脚本不能读取值,但是使用前要确保值不低于0自动属性 自动属性书写于你设定的函数的上方,在场景旁边。有时会通过虚拟机中的少量优化使自动属性的运行速度略微提升。要想创建一个自动属性,可以如下所示在属性定义末尾加上"auto"。你可以通过语法规则"= <value>"设定属性初始值。 例如: int property myInt = 5 auto自动只读属性自动只读属性是一种无法改变值的自动属性。如果你想通过使用名称而非数值借代一个属性,或者脚本中的某些数值代指不同的事物,那么设定自动只读属性将会更为方便,你可以在属性定义末尾加上"AutoReadOnly"。这些属性必须通过使用语法规则"= <value>"设定初始值。 例如: int property myReadOnlyInt = 20 autoReadOnly条件属性常规属性不能通过条件声明。由于自动属性事实上在定义时隐藏由条件创建的变量,因此它可以通过条件定义。这就是为什么当你在条件系统中选择一个Papyrus变量时你会看到一个变形的自动属性名称 - 因为你从隐藏变量列表中选择。 例如: int property myVar auto conditional从任务脚本中获得属性属于相同任务的结果脚本 有时你需要获得任务脚本的属性,并将其用于结果脚本。这也许很复杂,但是一旦你理解其内容,那么一切将会豁然开朗。首先观看以下案例,然后我们描述它的效果。 ;打开脚本,内容如下: scriptName MQ01Script extends Quest int property deadCount auto ;结局脚本(属于MQ01)如下: MQ01Script myQuest ;声明一个变量"myQuest",它是MQ01Script里的一个类别 myQuest = GetOwningQuest() as MQ01Script ;设定myQuest为它所属的任务,也就是MQ01Script类别 float myDeadCount ;声明变量"myDeadCount" myDeadCount = myQuest.deadCount ;设定局部变量为任务属性值 ;你也可以设定任务属性为: myQuest.deadCount = 10我们有一个属性"deadCount",它在一个与MQ01关联的脚本"MQ01Script"。我们可以设定一个属于MQ01的脚本(也许是对话结果、包结果或关联别名的脚本)。在结果脚本中,我们创建一个反映任务脚本的变量,它有我们所需的属性(在此情况下MQ01脚本是"DeadCount"属性)。注意我们的变量myQuest由MQ01Script声明。这是因为我们设定我们的脚本"脚本名称 MQ01Script 继承于 任务",我们本质上创建一个新类型目标 - MQ01Script。GetOwningQuest返回一个任务目标(在我们继承它之前)。因此我们也需要分配由诸如"myQuest = GetOwningQuest() as MQ01Script"之类的GetOwningQuest目标返回的任务。因此我们可以存取它的继承属性。如果我们从未对其分配诸如MQ01Script之类的脚本,那么任务目标只包括函数和属性,它不会包括死循环属性。 换句话说,当我们创建一个继承于任务脚本的MQ01Script时,除非我们分配一个由GetOwningQuest返回的目标,它不会包括在新脚本中声明的新变量。 关于kmyQuest 如果我们使用的片段下拉菜单中有一个"kmyquest",那么你可以选择一个脚本并将其关联属于此片段的任务,然后使用kmyQuest"magic variable"参照任务脚本,且未分配它。 上述内容应如下所示: float myDeadCount myDeadCount = kmyQuest.deadCount ;getting property kmyQuest.deadCount = 5 ;setting property通过 Magic Effect 脚本下面是脚本魔法访问任务属性的脚本: Scriptname myQuestNameScript extends Quest Int Property PublicInt Auto ; This value is defined as a property and can be accessed from outside this script Int PrivateInt = 30 ; This value is private to the script and cannot be accessed from outside this script Function DamageTargBasedOnPublic(Actor akTarget) ;This code will damage the akTarget for PublicInt damage akTarget.DamageAV("Health", PublicInt) EndFunction Function DamageTargBasedOnPrivate(Actor akTarget) ;This code will damage the akTarget for PrivateInt damage akTarget.DamageAV("Health", PrivateInt) EndFunction定义了任务脚本并创建属性,我们可以从外面控制。 Scriptname mySpellEffectScript extends activemagiceffect myQuestNameScript Property myQuestRef auto Event OnEffectStart(Actor akTarget, Actor akCaster) myQuestRef.PublicInt = 20 ; This will change the damage for the DamageTargBasedonPublic Function myQuestRef.DamageTargBasedOnPublic(akTarget) ; You can manipulate this damage by changing PublicDamage Prior to calling it myQuestRef.DamageTargBasedOnPrivate(akTarget) ; This will always do 30 damage unless the quest changes the private variable EndEvent然后你需要查看属性窗口了解你的脚本附属于哪里,然后设置myQuestRef的属性为任务脚本相关联的任务。从任何其他脚本 你可以使用上面Magic Effect的脚本参考。你必须在脚本中定义属性,以及你需要访问的对象“类型”。如果你的对象有自定义脚本,定义该类型为对象的脚本名字。注意不要声明为对象名字。所有的对象都可能有多个脚本,所以你必须指定你要访问的脚本名字。 ScriptName ScriptA extends Quest int Property intProperty Auto float Property floatProperty Auto GlobalVariable Property gvProperty Auto Shout Property shoutProperty Auto因为我们想要获取或者修改 ScriptA 的属性,我们需要再新建脚本 ScriptB: ScriptName ScriptB extends Quest int iVar float fVar GlobalVariable gVar ScriptA property MyScriptProperty ScriptA Function Get() If !GetData GetData = True ReturnVal = (Self as Quest) as ScriptA EndIf return ReturnVal EndFunction Function Set(ScriptA NewValue) If !SetData SetData = True ReturnVal = NewValue EndIf EndFunction Endproperty Bool GetData = False Bool SetData = False ScriptA ReturnVal Event OnInit() MyScriptProperty.intProperty = 10 ;sets intProperty to 10 MyScriptProperty.floatProperty = 5.5 ;sets floatProperty to 5.5 MyScriptProperty.gvProperty.SetValue(10) ;sets gvProperty to 10 MyScriptProperty.gvProperty.GetValue() ;returns 10 MyScriptProperty.shoutProperty endEvent游戏中可以使用为类型的所有对象看Script Objects。另外参考:Accessing Functions From Other Scripts 警告 谨慎处理由多个其他脚本扩展的"基础"脚本的变量和自动属性。这是因为可能会遇到多个脚本含有相同的变量或者自动属性关联到相同的对象,然后游戏可能无法可靠的选择变量或者属性。 举个例子,下面的3个短脚本: ScriptName Base extends ObjectReference Int Property MyValue Auto ScriptName Derived1 extends Base ScriptName Derived2 extends Base因为Derived1和Derived2扩展了Base,他们各自都继承了MyValue属性。然后,想象创建一个对象,然后脚本Derived1和Derived2都依附上去。尝试访问MyValue将不会返回相同的数值: (MyObjectReference as Base).MyValue这是声明在本地脚本对象的双真变量和自动属性,比如Actor Script。因为游戏可以在任何需要的时候归属这些到游戏中的对象,所以会创建另一个副本变量或者属性。为了避免这类情况发生,请不要编辑本地脚本对象,并且当单个对象有多个脚本附属并继承相同的属性,请确认你在访问属性之前要把它转换成最直接的表单。在上面的例子中,可以使用: (MyObjectReference as Derived1).MyValue注意属性对话框中的属性列表只在添加新属性或在脚本被汇编后才会更新。
  19. 概述 本章将介绍Papyrus语言中的事件 和 属性 你将学到: 事件和触发事件的基本信息。属性和建立、设置、使用属性的基本信息。如何通过事件控制游戏。如何通过按照属性执行的动作控制游戏。本章教程建立的事件将在Lokir的墓地中进行,它是我们在关卡设计教程中建立的样本地下城。如果没有完成这个教程,你可以下载最后一关插件。但是请你确保你已经熟练掌握关卡设计并掌握以下内容:基本导航编辑。诸如陷阱、激活、触发条件之类的概念。计划现在,当你进入Lokir的坟墓的最后一间房间,名叫尸鬼霸主将从棺材里跳出并攻击你。让我们用特别的脚本元素使这场战斗更为惊心动魄。我们将在房间里添加2个死亡尸鬼霸主,这样我们用勇气术咒文使尸鬼霸主从它的墓地中出现。 设定阶段 首先,让我们设定需要被复活的尸鬼。打开编辑器,打开LokirsTomb房间,然后将视角锁定在洞穴区域;这是BOSS的房间。在目标窗口点击Actors>Actor或者all-然后通过选项过滤器找出"LvlDraugrMissileMale"和"LvlDraugrWarlockMale"。并将它们拖放到房间里。 开始时,让角色(例如这些尸鬼)开始时“存活”,然后我们让它们死亡。依次双击每个角色并在选项框中勾选"Starts Dead"'。 熟悉以前BGS的MOD制作者请注意-简单地设定基本角色的生命为0将不再导致角色开始就处于死亡状态。你必须在选项框中勾选"Starts Dead"。保存你的插件并检查这些新特性 ,你将发现在房间的地面上有两个死亡尸鬼的骨骼贴图。这是我们想要的-但是让我们看Creation Kit的运行结果。 事件策划 事件介绍 我们需要这些尸鬼复活并从坟墓中跳出。当然,我们希望这是通过触发事件产生。 "事件"是Papyrus脚本用以控制游戏的剧情和状态的东西。这里有众多不同的事件,例如: 影响一个目标或人物(升级、开门、扒窃)。捡起物品,装备物品, 卸载装备, 或丢弃物品.进入或离开一个场所。进入或退出一场战斗, 战斗中受到攻击, 死亡.简而言之-如果我们想影响游戏中的事物,事件是一个不错的选择。触发活动事件 你也许不会意识到,我们的案例已用到Papyrus脚本中的事件。这是尸鬼总能在适当时机跳出棺材的原因。如果你已经完成陷阱教程,你会发现尸鬼通过脚步声大小来作为动作的触发条件。当主角的脚步发出声音,声音达到影响尸鬼的大小,尸鬼就会起来。 这里有一些脚本及它们的事件发生情况:玩家触发冲突而触发的事件。含有触发动态剧情的脚本产生的效果。尸鬼霸主处于待触发状态,因此也被激活。让尸鬼霸主跳出坟墓的脚本我们可以使用相同触发条件来控制2个死亡尸鬼的活动。 对于每个尸鬼: 双击尸鬼打开它的属性窗口。锁定并选择Activate Parents标签。右击列表空处并选择'New'。出现活动选择对话框。点击'Select Reference in Render Window'。当十字准线光标出现时,右击触发音量。如果你没有看见触发条件,请记住使用"M"让标记可见。现在当玩家达到触发条件时每个尸鬼将会有活动事件。实际上我们并不能用事件做所有事,然而接下来你将学习编写控制事件的脚本。 编写脚本 开始设计 现在开始脚本编写。打开你先前的文本编辑器。然后建立一个名为"LokirsDraugrReanimate.psc"新文件。 从以下两行开始: scriptName LokirsDraugrReanimate extends Actor {Resurrects the two dead Draugr in Lokir's Tomb.}scriptName LokirsDraugrReanimate -这是脚本的名称,名称必须包括"后缀名"。extends Actor -这个脚本将发生在角色(尸鬼)身上,因此它能做角色能做的任何事。{...} -大双括号{}中的内容是文件描述信息-如同注释一样。这些并非必须填写的,但是它能让脚本后期维护更为方便,我们设定它为活动事件,需要其他实体(例如主角或NPC)触发才能发生。通过“使用”或“激活”目标,在这种情况下,陷阱触发的脚本将被激活。 scriptName LokirsDraugrResurrection extends Actor {Resurrects the two dead Draugr in Lokir's Tomb.} Event OnActivate(ObjectReference akActionRef) ; Cast a Reanimate spell on the Draugr EndEvent展示效果接下来,当活动事件发生时你需要对尸鬼施放勇气术。做完这些,你需要创建一个"属性"。在这种情况下,这是我们将要用到的咒语的属性。也就是要告诉Creation Kit将要使用的特殊咒语。 如果谁曾在早期Bethesda工具用legacy语言编辑脚本,这是一个重要改变。在Papyrus语言中,我们不能简单地将目标ID写入--你必须用到属性。这样可以写出更灵活更易维护的脚本,因为脚本中的每个案例的属性都有不同的数值。"声明"咒语属性,如下: scriptName LokirsDraugrResurrection extends Actor {Resurrects the two dead Draugr in Lokir's Tomb.} Spell property reanimateSpell auto EVENT OnActivate(ObjectReference akActionRef) ; Cast a Reanimate spell on the Draugr EndEVENTSpell - 说明我们将用到的类型是Spell。property - 是reserved word,这里是添加一个新的属性。reanimateSpell - 这是我们给变量的名字,在游戏里,当我们查阅属性时,Papyrus将用到spell.auto - 另一个关键字 - 它告诉Papyrus自动对这个函数调用或赋值.不用担心它,它一般也没有作用。; ... - 分号后的内容是注释,用来标明作用方便维护,在脚本运行中不起作用。我们已经定义一个咒语,但是它并没有效果。咒语属性可用Cast函数确定,接下来我们将用到,在活动事件脚本中插入: scriptName LokirsDraugrResurrection extends Actor {Resurrects the two dead Draugr in Lokir's Tomb.} Spell property reanimateSpell Auto EVENT OnActivate(ObjectReference akActionRef) ; Cast a Reanimate spell on the Draugr reanimateSpell.Cast(Self, Self) EndEVENT如同很多函数一样,我们需要加入很多注解让人们知道这个函数要做什么。在这种情况下,Cast需要资源和对象。例如,我们需要尸鬼对它自己"cast"(施放)咒语。保留字"Self"在这种情况下就显得尤为重要;在其它方面使用它,Papyrus自动知道我们想关联它本身。在这种情况下,"self"如同对将要复活的尸鬼创造一个新的属性-只是和原来的相同。(其实"self"就是自己,意味着受和攻是同一人)保存并编译脚本,然后返回Creation Kit。 关联脚本 返回编辑器,双击其中一个尸鬼来打开属性窗口,然后点击Scripts标签(你也可以按键盘上的'End'键跳到最右方)我们将在这里关联脚本: 点击 Add.在 Add script... 窗口, 输入新脚本的名字 'LokirsDraugrResurrection'.双击脚本并把它添加到尸鬼.这样将脚本与reference关联,但是我们仍然需要告诉Creation Kit要施放的咒语。我们将用到"dunReanimateSelf",一个不可用的特殊事件咒语。在Scripts标签下选择LokirsDraugrResurrection。点击Properties。只有单属性才能出现的列表 - reanimateSpell。选择reanimateSpell并点击Edit Value。注意:在这种情况下这个列表只出现一个正常的表格 - spells选择 dunReanimateSelf。点击 OK。对另一个尸鬼重复相同的步骤。保存你的工作并在游戏中体验成果!调试脚本 第一眼看到脚本,我们会认为所有结果都是意料之中。然而,当你进行游戏,你会注意到任何时候只要触发活动事件复活事件就会发生在尸鬼身上。在玩家搜刮其尸体时也会发生!这并不是严重的问题,而是逻辑上的疏漏导致意料之外的BUG。 这里有成百上千种解决之道,例如通过控制让复活只发生一次。也可以让脚本忽略玩家的行为,不过这样依然会导致NPC的活动触发事件。 取而代之的是,我们可以让复活行为只发生在触发条件发往活动事件时。我们将通过建立另一个属性 -目标引用 - 我们将把它分配给触发条件。 我们用新的属性和OnActivate()提供的参数"akActionRef"作比较。这个参数是一系列特殊变量,只在这个活动事件中可用,Papyrus自动只将复活函数用在这个活动事件。 scriptName LokirsDraugrResurrection extends Actor {This Script lives on the dead minion draugr in Lokir's Tomb. It handles their resurrection} Spell property reanimateSpell Auto ; this is the special self-resurrection spell to use objectReference property myTrigger auto ; This is the reference we are waiting on to send an activate Event OnActivate(ObjectReference akActionRef) ; I've been activated - see if was my trigger if (akActionRef == myTrigger) ; Cast a Reanimate spell on the Draugr reanimateSpell.Cast(Self, Self) EndIf EndEventobjectReference 这是新属性的数据类型myTrigger 这是新属性的名字if 这是"if条件语句"的开始。如果if后面的状态是真实,接下来的脚本将运行,换句话说,如果为真,Papyrus就将运行接下来的脚本直到"endif"。(akActionRef == myTrigger)"=="是一个逻辑运算符,它意味着相等。在这种情况下,如果akActionRef和myTrigger相等,将继续运行一下内容。endIf这对于结束if条件语句十分重要,如果有多个if条件语句嵌套尤其需要注意endIf的对应关系,这对于逻辑的路线来说非常重要。
  20. 概述 请确保在学习本章前已完成Hello World教程和变量和条件语句教程的学习。本章相比前两章较长,但是你将学到更多知识。现在开始吧! 你将学到: 对前两章内容的复习脚本的第一行,也就是“继承”一个脚本如何在脚本中使用{}添加提示工具如何使用道具并将它在编辑器中编辑如何创造并使用一个函数脚本的第一行在本章开始之前,让我们复习一下前两章的内容。 打开前两章学习使用的脚本,如果你认真观察,你会发现脚本正式开始的第一行有这样的段落: Scriptname HelloWorldScript extends ObjectReferenceScriptname HelloWorldScript extends ObjectReference:任何脚本开头都会有这句话。Scriptname HelloWorldScript extends ObjectReference: 在此你可以对你的脚本命名,Scriptname是脚本的名称,在任何情况下紧接名称都要写"HelloWorldScript"(注意之间有空格)。Scriptname HelloWorldScript extends ObjectReference:extends是关键字,它表示继承,也就是这个脚本是以extends右边的项目为基础编写的。Scriptname HelloWorldScript extends ObjectReference: ObjectReference是该脚本所要继承的类,也就是该脚本的基础和前提。脚本类的继承实际中任何你编写的脚本都要“继承”其它的脚本,当你继承一个脚本时,你会说:“我的脚本是在这个父脚本后面添加的内容,只不过独立成为一个脚本。” 被继承的父脚本也可以这样说:“你的脚本是按照我的样式写的。” 例如,你想要在一个已存在的脚本后面添加新的脚本内容(以石柱脚本为例),即在脚本"ObjectReference"后面添加你的新脚本内容。如果你想在脚本”Quest"后面添加一个你编写的名为“YourScript”的脚本,你可以写“YourScript HelloWorldScript extends Quest"。其它亦如此。 如果你能得心应手地面对脚本编写,那么你一定对脚本类的继承并不陌生。现在,你要知道哪个脚本是你要继承的那个。 添加工具栏 现在你肯定在考虑第二行应该怎样写?如下所示: {This is my very first script!}当你制作新的脚本并开始Documentation String的编写时,请看 当你的脚本停留在scripts标签时这个工具框将出现: 当你想改变工具框时,请改变双大括号{}之间的内容. 你也可以借助添加工具栏来管理你的道具。如下: 属性 一个属性就像一个变量,但你可以在编辑器中把它与东西关联。 让我们现在开始吧! 在 "int count"一行上方添加脚本: Message property box1 auto {Points to the message box that is shown on the first activation.} Message property box2 auto {Points to the message box that is shown on the second activation.} Message property box3 auto {Points to the message box that is shown on the third activation.}Message property box1 auto:Message的作用是决定box1的类型,是message(这和我们"count"变量定义"int"类型是一样的)Message property box1 auto:这个message的类型不是变量,而是属性(property)。Message property box1 auto:我们给这个属性命名"box1"(如同我们给变量命名"count")。Message property box1 auto:auto是一个关键字,我们对属性的定义(不要担心这包含什么高深的意思 ,现在只要记住"auto"是用来定义属性)。你要注意到大双括号{}。它将提供工具框,让我们看一下:点击file->save. 关闭script窗口. 然后点击石柱的reference窗口上的properties按钮。你将看到属性列表停留在它们的名称上。 创建消息框 现在我们将创建链接目标的消息框。 目标窗口是类"Miscellaneous"的延伸,点击类"Messages".在messages列表上右击,从弹出目录选择"New"。 输入以下内容: ID: myMessageBox1Message Text: Hello World! This is the first time the player activated the Pillar. 按下图所示创建至少2个消息框: ID: myMessageBox2Message Text: This is the second time the player activated the Pillar.ID: myMessageBox3Message Text: It's been three or more times activating the Pillar.属性与消息框的链接重新打开石柱的属性窗口。 点击"box1"所在的一行然后点击"edit value"按钮。点击"Pick Object"的下拉菜单选择myMessageBox1。然后重复相同的步骤对属性box2,box3分别选择myMessageBox2,myMessageBox3。 然后务必点击"OK"关闭窗口(如果未点击"OK",任何更改将作废),然后保存你的插件。 我们必须在属性box1, box2, box3和消息框myMessageBox1, myMessageBox2, myMessageBox3之间建立链接。 在确信每个消息框和属性建立链接后,我们继续编辑脚本。 在属性中调用函数 现在让我们在游戏中打开这些消息框。 打开并编辑你的脚本,然后将以下这些段落: if count == 1 Debug.MessageBox("Hello, World!") elseif count == 2 Debug.MessageBox("Hello, World. Again!") else Debug.MessageBox("Hello, World. Enough already!") endif替换为: if count == 1 box1.Show() elseif count == 2 box2.Show() else box3.Show() endif我们现在分别在属性box1, box2, box3的消息框中调用函数"Show()",半角句号(.)的右边是被调用的函数("Show()"),左边是需要调用函数的属性(例如:属性"box1")。保存脚本,通过点击OK来关闭关闭编辑参考窗口,然后保存你的插件。然后运行游戏!(COC MolagBalVoiceCell) 你将在每次点击石柱时看到对应的消息框。 (如果出现"cannot call show() on a none object"的警告,请返回确认你的属性与消息框是否已关联,然后再在每个打开的窗口点击OK关闭并保存。 一切顺利完成,接下来我们将学习建立函数! 建立函数 如同我们在程序调试的过程中调用"MessageBox()"函数、以及在属性的对话框里建立"Show()"一样,我们也可以在脚本里建立一个自创的函数,但是我们在调用新函数之前,我们必须创造它。 在脚本底部添加如下内容: Message function GetMessage(int currentCount) Message chosenMessage if currentCount == 1 chosenMessage = box1 elseif currentCount == 2 chosenMessage = box2 else chosenMessage = box3 endif Return chosenMessage endFunction让我们剖析第一行:Message function GetMessage(int currentCount):Message说明这个函数将返回一个消息框。注意这是一个非强制返回函数,不一定要有返回值。Message function GetMessage(int currentCount):我们说这是这是一个函数,如同我们定义属性时用"property"一样。Message function GetMessage(int currentCount):GetMessage是函数的名字,然后我们在需要调用时直接调用名字即可。Message function GetMessage(int currentCount):小括号()里的内容是当我们调用函数时函数的参数,int表明我们要求参数的类型是整型,"currentCount"是参数的名字,我们在需要时只需调用参数的名字,然后函数返回一个数值供我们调用。然后剖析下一行:Message chosenMessage:我们声明变量的类型是Message。(就如同我们定义"count"的类型是"int"或其它目标类型一样。在这种情况下,我们定义其类型为"Message")Message chosenMessage:我们对变量命名"chosenMessage"。接下来我们将分别对属性box1, box2, box3建立选择消息框 。(再次注意"=="和"="之间的区别)最后我们添加: Return chosenMessage这些说明当我们调用函数GetMessage()时,它将给我们属性chosenMessage中的消息框。现在我们将在新脚本中应用函数。 将以下段落: if count == 1 box1.Show() elseif count == 2 box2.Show() else box3.Show() endif替换为: GetMessage(count).Show()我们现在调用GetMessage()并对count赋予参数。因为GetMessage(count)将返回一个消息框,我们可以在这里调用Show(),然后消息框将在游戏中出现。现在你的脚本将如下所示: Scriptname HelloWorldScript extends ObjectReference {This is my very first script!} Message property box1 auto {Points to the message box that is shown on the first activation.} Message property box2 auto {Points to the message box that is shown on the second activation.} Message property box3 auto {Points to the message box that is shown on the third activation.} int count ;stores the number of times this object has been activated Event OnActivate(ObjectReference akActionRef) count = count + 1 GetMessage(count).Show() endEvent Message function GetMessage(int currentCount) Message chosenMessage if currentCount == 1 chosenMessage = box1 elseif currentCount == 2 chosenMessage = box2 else chosenMessage = box3 endif Return chosenMessage endFunction当你运行游戏,激活石柱,我们将看到消息框内容。(COC MolagBalVoiceCell)下章预告 基础教程以学习完毕。下章将学习如何制作BOSS。
  21. 概述 请你在学习本章前确保已完成Hello World教程的学习。 你将学习到: 如何建立及使用变量如何在脚本中加入注释从而方便日后维护如何对变量赋值并在游戏中应用它如何使用if-then-else条件语句添加逻辑判断变量打个比方,变量就如同装载信息或物体的容器,你可以添加它,你也可以向它添加数值,或把它清空。你也可以稍后检索它们的目录。 现在,我们将在Hello World教程中的那个脚本内添加一个保存玩家激活石柱次数的变量。 打开石柱的Reference窗口,然后在Script标签下右击HelloWorldScript,选择"Edit Source",然后将打开脚本编辑窗口。 你的脚本内容将会如下所示: Scriptname HelloWorldScript extends ObjectReference {This is my very first script!} Event OnActivate(ObjectReference akActionRef) Debug.MessageBox("Hello, World!") endEvent一切就绪,我们添加一个变量。在含有"Event OnActivate"一行的上方,添加: int count ;stores the number of times this object has been activatedint count ;stores the number... - int 定义变量的类型是整型,意味着变量是一个整数。int count ;stores the number... - count 是我们给变量起的名称,具有惟一性和排他性。int count ;stores the number... - 分号(;)后面的段落是“注释”。注释在脚本运行过程中并不起作用,只是为了让人们在日后软件维护过程中更为方便。现在让我们添加计算玩家激活石柱次数的语句。在"Event OnActivate..."和"Debug.MessageBox..."两行之间添加: count = count + 1在这里我们要说,count变量的数值在每次运行该段语句后就会增加1,然后运算后的数值重新存入变量count。换句话说,等号左边的变量被等号右边的计算结果赋值。观察变量在运行中的状态 确信在游戏关闭的状态下保存脚本,点击"OK"关闭reference,然后点击工具栏上的save按钮保存插件。 开始游戏并进入运行阶段,打开控制台(按~键)输入: COC MolagBalVoiceCell首先我们必须检查变量"count"的值,在我们未做任何事之前它应为0。打开控制台,点击石柱,你将看到"'WETempActivator'",以及石柱上的一个十六进制数值。 现在在控制台中输入: ShowVars现在你将在控制台中看到: --- Papyrus --------------------- HelloWorldScript: Script state = "" count = 0你将看到"count"的数值是0。点击~关闭控制台,激活石柱3次,换句话说,你看3次显示"Hello World!"的对话框。 现在打开控制台输入"ShowVars",你将看到"count = 3" 这是为什么?你在每次激活石柱时,count变量就会按照脚本代码增加1,关闭游戏,然后重新打开脚本。 条件语句 (if-then-else) 现在我们添加一个"条件语句",它将在脚本中起到逻辑控制作用,将以下一行 "Debug.MessageBox("Hello, World!")" with the following 替换为: if count == 1 Debug.MessageBox("Hello, World!") elseif count == 2 Debug.MessageBox("Hello, World. Again!") else Debug.MessageBox("Hello, World. Enough already!") endif现在我们可以使用变量count来控制脚本的行为。如今对话框将依据石碑激活次数显示不同的信息 。记住逻辑判断中的相等逻辑是用两个等号(==)。然后用相同方法测试。特别提醒: 单等号(=):只表示赋值,将等号右边的值赋给左边的变量双等号 (==) :逻辑判断,判断双等号两边的值是否相等如果因逻辑判断产生错误,系统提示丢失逻辑判断符号,请优先在此处找原因。现在你的脚本应如下所示: Scriptname HelloWorldScript extends ObjectReference {This is my very first script!} int count ;stores the number of times this object has been activated Event OnActivate(ObjectReference akActionRef) count = count + 1 if count == 1 Debug.MessageBox("Hello, World!") elseif count == 2 Debug.MessageBox("Hello, World. Again!") else Debug.MessageBox("Hello, World. Enough already!") endif endEvent再次打开游戏,进入控制台,然后 coc to the cell, 激活石碑。你将在每次都看到不同的对话框!
  22. 概述 本章将讲到如何使用故事管理器在事件控制中创建一个新反应容器。 你将学到: 什么是故事管理器故事管理器如何工作从故事管理器开始管理存在内容是个很好的方法,但是勇敢的mod制作者仍需探索更好的方法。节点上方的节点上方的节点在目标窗口中,角色的上方,点击“SM事件节点”目标类型。 你看到的是时间的根本设定,它将自动启动发散任务,并作出自我解释及以玩家为中心。双击“杀死角色事件” 一切就绪后,双击“事件节点堆栈:杀死角色事件”,这也许看起来有些奇怪,接下来将予以解释。 故事管理器工作在节点系统之上。类似于先前学习的包和消息栈。当故事管理器事件启动时,它将自上而下打开列表。由于并非真正意义上的堆栈,因此决定每个节点是否可用的逻辑判断形成一个树状结构。点击树状结构顶部的添加标记添加一个名为"DA08KillFriendNode"节点,然后点击选择它。此时窗口应如下所示: 我们现在观察用黑檀之刃杀死朋友的这个系统,是DA08任务标记活动的一部分。注意右下方的状态以及目标栏的"E"列表。双击第一个状态并观察它。 注意运行在状态"Event Data: Killer"之上的选项--这是一个关键概念。每种任务管理事件决定它的事件数据设定,因此我们可以测试状态。在此我们要检查玩家用黑檀之刃谋杀行为。 这里有很多关于系统的数据以及所有注释。 一个重要的观点是节点总是链接一个任务--如果经过节点,那么任务将开始。注意并非所有的任务都能进入故事管理器;它特别由反映事件的任务数据标签定义。 任务可以根据事件数据填写别名,例如在特定用途反应每个事件。这可以指引返回找寻玩家丢弃的物品,如何使受害者慷慨支付赏金等等。 在上方的节点的上方 添加复杂的任务节点可以从复杂事件逻辑结构中嵌套组织。这里有2种节点:分支节点(包含其他节点)和任务节点(包含任务)。每一个节点都可以是堆栈(游戏自上而下查找节点)或随机(随机查找节点)。 在大多数情况下,你可以在节点中勾选“共享事件“,如果一个节点管理任务的开始,事件将不会重启,意味着无关节点将不会收到关键通知。因此要想成为一个出色的任务管理者,请共享事件。 总结 如果你已完成任务设计基础系列和中级任务设计系列教程的学习,那么你会对任务设计了如指掌。如果你想学习更多,请参阅Papyrus,我们将学习一个新的脚本语言。 请充分发挥你的才智吧!我们期待更多有志青年运用这些工具创造更多的MOD
  23. 概述 本章将讲解在不同情况下我们创建动态任务内容的灵活系统,并参考和使用它。 你将学到: 如何用条件填写别名如何动态填写基于其它数据的别名趣味性迄今为止我们编写的内容并不是很出色。比如结构太简单、任务太单调、剧情太狗血。总而言之无特别之处,在此就不一一指出了。但是我们必须予以改变。 这里我们要使别名更能体现出角色类型--任务功能取决于别名而非基于的角色,我们可以使其在体现出功能的情况下应用于任何角色。这是一个强有力的想法,并能使我们借助程序来阐述故事。 想象一下我们如何在此任务中创建随机剧情: 地下城改变: 将护身符给一个随机选择的地下城BOSS。由于选择系统更偏向于玩家未涉足的地方,因此可能将它布置到一个新的地方!不同受害者: 我们可以从世界中任意选择一个角色作为丢失护身符的人--或者只选择独处的黑暗精灵,或者只从东境地区选择,等等。我们可以使其常规化或特殊化(种类数量以录制的声音种类为上限)。等等, 等等, 等等.举个例子,我们只需更改地下城的位置就可以了。 条件别名 一个重要的技巧是创建一个以“种子别名”为基础的发散任务。记住,这不是我们发现的那个盗贼--我们只需要知道在玩家地图中添加什么标记,这意味着我们必须知道用什么地下城。因此我们需要担心有关其的三个别名,但是它们都需要依赖盗贼的初始三个选择。然而由于技术上的原因,我们通常从位置处开始(告诉游戏选取哪个BOSS的位置,然后再分配盗贼)。在这种情况下,位置就是“种子别名”。当你在发散的内容中找寻bug时,你就会知道种子别名的重要性了。 点击别名标签,然后右击新建一个别名并命名为"ThiefLocation",位置别名含有一些地方的数据并是“材料”的集合。例如,所处位置可能包含一些生物或者是城市的一部分,或者是特殊区域,或者有一个大门,或者是一个BOSS。我们可以用条件语句测试一些数据并找到一个合适的地点。我们添加的首要条件是"LocationHasRefType",从下拉目录中选择“Boss”。这个条件为真,并针对任何关键字为BOSS的角色。 第二个我们要添加的条件是"LocationHasKeyword",这个将测试位置数据(相对于包括的角色)。此时关键字是"LocTypeBanditCamp"。 如果你对定义数据感兴趣,请点击位置和关键字。正如你所预计的那样,这2个条件将过滤掉所有强盗阵营以外的BOSS。游戏将从可用位置随机选择一个。最后,点击窗口顶部的“保留位置”引用--这将会预防任何包括任务运行时使用的位置的发散内容。新的位置别名应如下所示: 关闭窗口。以下内容很重要:盗贼位置别名将会高亮,使用左箭头按键将它移动到盗贼别名上方。因为别名将自上而下填写,所以顺序很重要,由于盗贼基于别名实现,因此接下来将会讲到。 依赖别名 打开盗贼别名"ThiefLocation",先前它指向一个特殊的引用,但是我们将有所改变并分割我们找到的位置。点击“位置别名引用”的录音按钮,这将会打开位置潜在包含的引用。从首个下拉目录中选择"ThiefLocation",然后从下拉引用类型中选择"Boss"。(注意如果没有选择包括BOSS的位置,那么此别名将会填写失败。如果所有别名填写失败,那么任务将不会开始,因此任务无法开始时首先从这里找问题。如果你需要使别名空白,请标记“选填”。) 现在我们需要对地图标记设置别名。那么设置至少一个别名,并命名为"LocationMarker"。这也是有条件的--如下所示: GetInCurrentLocAlias: 盗贼位置GetIsId: 地图标记最后,我们需要改变20阶段的脚本,添加地图标记而非我们先前设置的代码。因此阶段20的脚本如下所示: SetObjectiveCompleted(10) SetObjectiveDisplayed(20) Alias_ThiefLocation.GetReference().AddToMap()增加趣味性现在我们已经对任务添加一个发散元素。你应该移除任何有关瑞驰之风巢穴的特殊内容(例如:Bendu可能不需要关心它的名称)。否则,任务会很普通! 如果你照着任务目标教程操作,你会发现没有任何目标实际是引用到Reachwind Eyrie的,所以这边没有什么变化,但如果我们想要任务目标告诉玩家盗贼在哪边,甚至当我们也不知道呢?在我们现有的基础上这其实是很容易实现的。首先,回到ThiefLocation别名并勾选'Stores Text'。然后,返回到任务目标标签,修改目标索引10的显示内容:修改 "Kill the thief" 为"Kill the thief at <Alias=ThiefLocation>" 插入别名到文本就是这么简单。我们不需要检查别名是否已勾选,因为(假设我们没有在别名中勾选Optional选择框)如果没有,任务就不会开始。更多高级信息,你可以查看Text Replacement页面。 特别强调 ... 除了将盗贼脚本在盗贼基本目标中保留,而非对其使用适当的别名。现在如果你从开始制作任务数据的中心别名,那么它将会变得很简单!你知道如何清理它吗?那么现在开始吧! 更多内容 如果你已经有Creation Kit任务系统的所有基本图形应用程序包,那么就可以进入任务管理器的学习....
  24. 概述 本章将讲解Creation Kit的新场景功能。场景相对于先前的Bethesda工具来说是一个新的主要特征,它能对角色多重动作的协调性起到高度控制。这对于对话场景、杂物放置及设置比单个包复杂的角色动作来说很有用。 你将学到: 如何创建一个新场景。 如何对场景添加对话、计时器及动作 如何在场景中运行脚本 场景介绍 迄今为止我们已学到如何通过包系统制作角色的简单动作,以及制作对玩家和通常状态下的对话系统。这些在我们面临复杂行为之前已经足够了。如果我们希望他和Gilfre(住在水合磨坊的那位)交流,然后去chop森林一会儿--这将需要通过一个复杂的脚本去解决配置、对话、计时、条件等等。 或者,我们可以简单的编写场景中所有期望的动作。 打开GSQ01任务并进入场景标签,如下所示: 这和进入窗口左侧的场景列表中的对话视窗标签的方法类似,除了实际内容在大区域的右方。 右击左区域并选择“新建”创建一个新场景,命名为"GSQ01BenduGilfreScene"。确信点击它使其高亮。 布置场景 右击并选择“新建角色”。将打开任务中已被创建的角色别名列表。双击Bendu对他添加一个动作场景。尽管在此任务中我们不需要Gilfre的别名,但是你依旧要记住对特殊角色创建别名的方法(别名标签,新建引用别名,命名"Gilfre",独有角色,下拉菜单中选择Gilfre) 一旦你创建新别名,返回场景标签,选择场景,然后右击选择新建角色并在场景中置入Gilfre。现在场景应如下所示: 我们现在要告诉游戏在此场景中出场的角色,但是现在并不让他们活动。 段落 在Creation Kit中,一个场景将被分解为若干个段落,也就是将活动分为几个不相干的部分从而使游戏可以运行它们。右击场景区域并选择“在末尾新建段落”。(我们将此段落添加至场景“末尾”,但是由于没有其它段落,因此它是第一个创建的段落。有时目录选项含有一个很可笑的名称) 首先我们要做的是让Bendu和Gilfre互相靠近。这里有一大堆选择,但是我们让他们在水合磨坊的地图标记处碰面。我们通过使用包让他们同路移动。 动作包 右击场景中标记"Bendu"的方形标签,并从背景目录中选择"新建动作 -> 包"。将会打开场景动作包窗口。 这个列表显示大多数我们可以布置的此别名角色的其它包堆栈。因此如果你学习过包的教程,因此我们可以从基础包上方找到别名包进而发现场景包。因为游戏自上而下查找包,因此场景包总是有优先权(前提是它们有效) 右击包列表并选择"新建"从而创建一个新的包。我们将对这个场景创建一个新的完整动作;然而这看起来做的过分了,但是由于场景的特殊性,因此很难使用常规包完成。在任何情况下,将此包命名为"GSQ01BenduGilfreHitMarks"。默认临时包是“旅行”--保持不变并选择水合磨坊地图标记为目标。设置合理的半径例如250 点击包窗口的确定,然后再次在场景动作包窗口点击确定从而返回合适的场景。你可以在此场景中看见Bendu下方的包。 右击Gilfre对她添加一个动作包。尽管此时并没有通过右击选择“新建”创建一个包,而是重新使用通过在下拉目录中选择“添加”创建一个包。这会在游戏中打开一个包列表。使用顶部的过滤器迅速找到"GSQ01BenduGilfreHitMarks"并在场景中双击添加它。 注意我们并没有在此包中插入任何条件。因为它只在此场景中使用且不会成为正常包堆栈估值的一部分,这样可以更安全。然而游戏会依赖设计者的设计且不会对错误有任何预防措施,因此要谨慎设定包的条件!当场景开始时,它将从首段开始,在别名栈的上方布置这些包,并强制重新评估这些包(在此情况下,选择我们刚刚制作的包)。此段将在它的所有动作完成后完成,在这种情况下旅行包意味着角色已经到达目标。 再次强调:场景在段落的所有动作完成前不会运行。这里有一些类型的包(如下所示)从不会完成,因此当心被这些包欺骗! 当他们碰面后我们不希望场景就此终结,因此添加更多的内容。 第二段 再次右击场景区域,并再次选择“在末尾处添加段落”添加一个新的空段落。右击创建一个新的动作,但是此时要选择“对话”而非“包”。 对话动作 底部的巨大列表表示一个消息栈,就如同对一个"正常"对话编写一个话题一样。双击这里创建第一句为"Oh, um. Hello." (假设Bendu看到了Gilfre,但是Gilfre没有发觉。那么将会很戏剧化。) 由于是对场景编写对话,因此我们不必对其设定条件,由于游戏可以智能的通过赋予的别名找出必要声音类型,并且从不考虑其它对话选项。因此一旦我们编写完对话,只需点击“确定”就可以了。 添加一个新段落,并对Gilfre创建一个对话动作。她会说"Get back to work, you."(可怜的Bendu被无视了),现在你的场景那个应如下所示: (你的场景动作数量也许有所不同,不必为此担心。) 继续添加内容 如果你仔细观察,你会注意到一个问题--我们未对新增段落添加任何包,因此当我们来到这里,所有角色将退回他们的基础包。这回使场景看起来可笑。 右击第2段Bendu的部分并对其添加一个包动作。由于我们只让他在此区域闲逛,因此可以再次使用相同的包。再次选择GSQ01BenduGilfreHitMarks,对Gilfre的第2段添加相同的包。 因为我们希望他们在此场景中活动,而非对第3段添加新的动作,我们可以对此段添加一些动作。如果将鼠标指针移至区域右边缘,那么指针将变为缩放箭头,从而可以改变区域。将它们延伸至第3段,场景应如下所示: 记住在动作结束之前段落不会终止。这只应用于在此段落中完成的动作,所以在Bendu的对话完成后第2段将完成 会面 因为我们不知道角色在对话时会选择什么选项,所以我们不能确定他们如何会面。幸运的是,我们可以简单地打开对话动作(双击动作名称,并非对话),然后设定"路径"区域的值,在这种情况下我们对Bendu设定"Gilfre"的路径目标,他们都必须面对目标。 我们也可以通过对旅行包设定特殊X标记目标来使他们相遇。 计时器行为 你也许会认为Bendu被孤立了,因此让他在Gilfre返回正常包后坚持一会儿。在末尾处至少添加一段,并在此时只针对Bendu的包行为。 右击第4段的Bendu区域,并添加一个新行为-> 计时器。这不完全如你所想--一个用来测量场景的临时间隔。在这种情况下,它将为Bendu在Gilfre离开后的等待设定一个时间。设定为4秒(游戏时间的精确度为微秒级,因此应写为4.000000秒),这个时间已经足够了。 最后的场景应如下所示: 运行场景 我们也可以通过脚本运行场景(通过设定属性并调用Start()),或者在场景标签下选择"从任务起始处开始"选项。 新增功能 我们可以点击标签上方的"数据编辑"或在场景起始处添加脚本。你可以双击段落标题并在某个段落开始和结尾处放置脚本。如你所想,这会打开很多复杂的行为和功能。实际上,天际中任务比场景及适当段落脚本都要小。 最后,“所有行为必须在段落结束前完成”的规则将被打破。因为它只在默认状态下有用。但是如果在场景窗口中,你可以看见对特定段落结束处设定的条件。 警告 一个角色只能在一个时间存在于一个场景中,如果你要在一个场景中使用一个在另一场景中运行的角色,那么此场景将会暂停直到调用该角色的另一场景完成。如果同时调用一个角色将会产生冲突及奇怪行为,因此你应该谨慎使用场景。如果合理利用,场景将会发挥出强大的功能。 课后思考 你能让Bendu会见Gilfre时根据当时时间分别说"Good morning," "Good afternoon,"或"Good evening,"吗? 注意 如果使用TESEdit来复制场景记录时,注意不要删除复件中已存在的对话段落,因为他们表示的是原版信息。最好是移除链接的表单但是不移除段落然后用新的操作对话信息创建新的段落(除非旧的对话需要重复)
  25. 概述 本章将探索对话系统的更多功能,并设定包括战斗状态在内的各种情况下的对话。 你将学到: 什么是特殊话题 如何重置计时器 如何制作战斗任务线 如何创建模块话题 特殊话题 到目前为止,我们已经制作完成玩家和角色之间的普通对话。这显然很重要,但是要想使游戏更为现实,我们必须编写日常对话、战斗对话、回应对话等等。(你也许还想写膝盖中箭式的对话,那么接下来将讲到) 如果你刚学完开放结局,那么你会有一定的基础。和从对话目录选择一个选项不同,特殊话题由程序在事件发生时运行。如果你观察任务窗口的杂项标签(创建问候和告别语的地方),然后你会看见所有的常规事件。 一个话题类型的所有对话都集中在一起。当游戏确定一个角色的对话线时,例如Hello,在定义每个Hello话题时都要先考虑每一个消息。并按照优先级将每一个话题堆栈粘贴排序(优先级为70的将先于优先级为50的被选择)。一旦巨栈被创建,它将从上开始直到选择满足所有条件的消息并启动。 如果没有发现合适的消息,那么角色将不会说话。 如果你是一个程序员,你也许对这种无效率的方法不满意。事实上我们可以用一种复杂的方法提高效率,但是在现阶段,这些已经足够了。重置计时器及单时间线 有一些简单的方法用以创建单线对话。 在消息窗口中,有一系列选项框、下拉菜单、文本区域。 只说一次 顾名思义,选择这个选项意味着此对话只说一次,说完之后不再说。当然你要谨慎,不要将所有的剧情关键对话都设定只说一次,因为玩家有可能会忽略它。这个选项最好用在角色的介绍上。 时间间隔 一旦说完此对话,它将在一定时间内无效。这将限制重复次数,但是本质上将增加编写的对话次数。 随机 试想一下一个特定角色的hello堆栈--我们不希望对话按照顺序不停的重复,因为这样会使他们看起来机械化。因此我们不能用计时器控制。相反,我们可以使之随机化,从而使对话不具有规律。 当然,如果这些问候语针对特定的任务阶段,我们又不想让对话耗尽。因此我们可以将栈底的对话标记为“随机结束”,从而用详细的选择控制消息选择系统。 如果依旧困惑,请看以下案例。 使用方法 打开任务GSQ01并点击杂项标签。打开先前创建的GSQ01Hellos话题。你会注意到当任务完成后Bendu只说"I can't thank you enough for helping me"。这显得不灵活。因此我们使对话多样化。 首先,打开消息"I can't thank you..."并选择随机选项,然后将重置时间设为0.5小时。关闭它并右击选择“复制”从而复制这句话。至少重复2次,产生4行对话。如下编辑新的3行: "I can only imagine how thrilling the fight with that thief was!" "A beautiful piece, isn't it? I'm thrilled to have it back." "Oh, something else?" 现在打开最后一行并标记“随机结束”。注意有意设定其为不必要且乏味的--这告诉玩家此NPC不会在问候语中说其它话。因此他会随机说前三行话,当他感到疲倦时,他将说最后一行话直到计时器重置。 战斗对话 "Like the bite of a flea!" 这是任务窗口中的“战斗”标签,是事件话题联系战斗状态的地方。你可以像建立问候语一样建立它,然后游戏将使用相同的逻辑、时间重置器、堆栈、任务优先权等参量输出它。 Creation Kit系统运行它的方式看起来复杂且不直接,但是当你理解它们时,会明白这是为了适应一系列环境(条件及消息堆栈)。 其它对话话题 默认状态下,当我们在对话视窗中设定一个新话题时,它将设定一个“顶级”话题。这意味着它指定的话题是有效的,当玩家第一次点击它们时它将在角色对话框中以选项的形式出现。 在这种情况下,橙色话题边缘表示它为顶级话题。双击边缘打开对话分支窗口。出现2个选项: 正常: 这个分支将不会出示话题列表除非它明确地链接其它分支。对于有组织的目标或由ForceGreet包触发的分支非常有用。 屏蔽: 当启动一个被屏蔽的限制分支时,只有一个角色可以说的事情。起始话题成为他们的问候语,顶级选择列表被起始话题链接的东西取代。 屏蔽话题对于含有角色重要对话的剧情很有用,我们不想让玩家在听到此对话之前通过普通对话话题。(因此请确信通过改变分支底部的选项来使起始话题无效!) 屏蔽分支在编辑器中是浅绿色。
×
×
  • Create New...