目录: 一、设置变量 二、定义对象 三、创建对象 四、关于动作 五、制作路点 六、用户自定义事件(UserDefinedEvents) 七、完全注释的OnSpawn代码 八、经验值和金钱的获取 九、制作技能检定 十、制作特殊物品 十一、做在椅子里和睡觉 十二、如何使NPC主动交谈 十三、如何使NPC攻击正在与他交谈的玩家 十四、如何设置一个商店 十五、使用MODULE事件 待续……
第一部分:设置变量
你所做的最经常的事就是设置变量并且获取其值。这是决定游戏中大多数事件是否发生或者改变/保持故事发展轨道的唯一方式。 当你设置变量时最普通的语句是:
void SetLocalInt(object oObject, string sVarName, int nValue)
这个语句将会设置一个本地整数形变量('local integer'),本地的意思是这个整形变量指向,或者存储于你指定的对象中。(其实他并不是真正的存储在对象本身上,但是这样说理解起来比较容易)
这里有三件重要的事情你要记住:
a.代码中的oObject必须是一个有效的对象。你将会看到很多语句在它们的开头部分会返回一个对象。(就像这个语句中void在SetLocalInt开头部分或者说前面)这意味着这些语句将会从开头的这些对象中返回(提取)数据。不管这个对象是什么,整形变量都会指向它。如果你正在对话,使用GetPCSpeaker() 将会把正在与NPC对话的玩家作为一个对象返回。你可以使用GetModule()把得到的整形变量存储在模块本身上……你可以使用OBJECT_SELF来指定任何当前正在运行的程序脚本中的对象,等等。 这非常重要,因为你选择的变量对于相对的对象来说是特殊并且唯一的。如果你以VARIABLE1的名字存储一个变量到MODULE中并且设定这个变量的值为1,那么这个变量将会是在这个MODULE存储的唯一一个以VARIABLE1命名的变量。你可以在不同的对象上都有VARIBALE1这个变量,比如说玩家,并且使他们拥有完全不同的值。
b.代码中的'string sVarName' 你赋予该变量的名字。确保其名称是唯一的,像所有字符串变量一样,需要在上面加注释。因为很多时候变量的名字并不是那么于辨认和好记。
c.代码中的'int nValue' 是关于你存储的变量中的信息。既然它是一个整数型变量,那么它必须是一个数字(无小数点)……像1,15或者320。如你想在这里存储不同的信息,你需要不同的语句:
SetLocalFloat =存储一个浮点型变量(可以有小数点) SetLocalString =存储一个字符串(例如一个句子,单词或者对象的名字……它必须要有注释,就像变量名称一样) SetLocalLocation =存储一个地点 SetLocalObject=存储对象数据(例如一个可确定位置的对象,一件物品,一个生物等等)
Q:我如何使用变量?
A:一旦你为一个东西设定了变量,它就会永远在这个东西上除非这个东西被取代或者被消除。除此之外在任何时间你都可以在另外的程序脚本中(或者这个脚本中)获取这个变量并且读取这个变量中的信息。
如果我要激活刚才我为之设置变量的那个整形变量,我会使用这个语句:
int GetLocalInt(object oObject, string sVarName)
除了显示的数据的值之外,这个语句和你刚才用来设置变量的语句完全相同。而这些数据的值正是你想要的东西。 记住这不是个无用的语句……它返回一个整形变量,也就是一个实际上的信息。这意味着你不能单独的使用这个语句,你可以用这个语句来定义一些其他语句(或者其他你设定的变量)中所需要的'int' 数据……如下所示:
int nValue = GetLocalInt(OBJECT_SELF, "VARIABLE1");
上面代码的意思是你刚刚指定了一个名字为nValue的新变量,这个变量现在等于VARIABLE1——一个存储在OBJECT_SELF中的整形变量。
糊涂了?看例子……
这里有一个很好的例子,它显示了一个变量怎样被用来追踪剧情的发展。我需要拥有一个名字为STORY的变量。当玩家接受任务时,我将它设置为1。当玩家完成任务后,我将它设置为2。
1.玩家接受了任务…… 当玩家在对话中选择了接受任务时,我到'Actions Taken'中写了一段代码用来设置STORY变量的值在玩家身上为1。 代码: void main() { SetLocalInt(GetPCSpeaker(), "STORY", 1); }
2.玩家完成了任务…… 我们看到玩家回来了,并且完成了NPC交给的任务,NPC祝贺他并且STORY的在玩家身上的值变为2。 void main() { SetLocalInt(GetPCSpeaker(), "STORY", 2); }
Q:我怎么知道玩家是否接受或是完成了任务?
A:当双方对话开始时,你只需要对各种不同的情况做一个新的节点(NODE)。计算机将会从头开始核对在'Text Appears When' 中所有的代码以确定是否为TRUE(如果在'Text Appears When'没有代码,将会自动默认为TRUE)。如果是TRUE,计算机将会开始运行你先前设置的节点。如果不是,将会忽略。
所以,根据剧情你可以从后向前的列出节点的内容:
#1. (plot done) "Thank you for doing that plot."(任务完成:谢谢) #2. (plot accepted) "Have you finished that plot?"(已接受任务:你完成任务了吗?) #3. (not accepted, not done) "Hello there!"(未接受任务,未完成:你好!)
所以当变量STORY的值为2的时候,计算机判断#1为TRUE;当STORY为1时,判断#2为TRUE;#3没有任何相关的代码,因为此时#1和#2全部为FALSE,那么任务既未被接受也未被完成,系统默认#3为TRUE。 那么任务是否被完成了? 代码: int StartingConditional() { int nDone = GetLocalInt(GetPCSpeaker(), "STORY") == 2; return nDone; }
任务是否被接受了? 代码: int StartingConditional() { int nAccept = GetLocalInt(GetPCSpeaker(), "STORY") == 1; return nAccept; }
所以你得到了: #1.(Text Appears When中为第一段代码)谢谢你完成了任务。 #2.(Text Appears When中为第二段代码)你完成任务了吗? #3.(无代码)你好!
在上述的代码中,我让NPC只给了1个人任务。
刚才我们将STORY变量设置在玩家身上(为1),但更好的办法是将这个值为1的变量设置到OBJECT_SELF(也就是NPC身上),并且将这个变量的名字改为PLOT_GIVEN。
然后当对话从#3开始的时候(因为现在玩家身上并没有STORY变量,因此当他们和NPC对话时对话分支总是从默认的#3开始),你可以使对话分为两部分:
| | -->(如果PLOT_GIVEN在OBJECT_SELF的值为1,那么Text Appears When中的代码判断为TRUE)对不起,今天没你的活儿。 | | -->(Text Appears When中无代码)你愿意为我做件事吗?
一个简单的任务对话
首先记住,所有你在'starting nodes' 列出的NODE(节点)都是以倒序执行的。
我们这样设计:你的NPC给出了3份工作,想象一下NODES的结构:
#1. "I don't have any more jobs for you."(我没有其他事情让你做了) #2. "Are you finished job #3?"(你完成工作3了吗?) #3. "Are you finished job #2?"(你完成工作2了吗?) #4. "Are you finished job #1?"(你完成工作1了吗?) #5. "Would you like a job?"(你愿意找份工作吗?) #6. "Hello!"(你好)
记住,当NPC的对话被点击后,计算机将会扫描'Text Appears When' 中关于这些NODES的代码。如果代码返回结果为TRUE或者没有代码,那么这个NODE(节点,在这里也就相当于其中的一个对话分支)将会被启动,如果返回结果为FALSE,那么计算机将会忽略这个NODE,进行下一个NODE的扫描。
下面是每个NODE代码的纲要:(为了理解起来简单起见,你可以按#6—#1的顺序从下至上阅读下列代码和说明,但紧记在编写时一定要按照从上至下的顺序)
#1.当JOB变量在玩家身上的值为4的时候,'Text Appears When'中的代码返回为TRUE。如下:
int StartingConditional() { int nJob = GetLocalInt(GetPCSpeaker(), "Job") == 4; return nJob; }
#2.当JOB变量在玩家身上的值为3的时候,'Text Appears When'中的代码返回为TRUE。如果玩家已经完成了工作,让NPC给玩家报酬并且在'Text Appears When' 中将JOB变量的值设为4。如下:
void main() { SetLocalInt(GetPCSpeaker(), "Job", 4); }
#3.当JOB变量在玩家身上的值为2的时候,'Text Appears When'中的代码返回为TRUE。如果玩家已经完成了工作,让NPC给玩家报酬并且在'Text Appears When' 中将JOB变量的值设为3。
#4.当JOB变量在玩家身上的值为1的时候,'Text Appears When'中的代码返回为TRUE。如果玩家已经完成了工作,让NPC给玩家报酬并且在'Text Appears When' 中将JOB变量的值设为2。
#5.当TalkedToJoe变量在玩家身上的值为1的时候,在对话中,如果玩家接受#1工作,在'Text Appears When'中将JOB变量在玩家身上的值设为1。
#6.没有代码在'Text Appears When'中(如果上述脚本全部为FALSE,那么这就是玩家第一次与这个NPC对话)。在第一行,写一段代码将TalkedToJoe变量在玩家身上的值设置为1。
这些就是设置变量的基本知识,存储和使用字符串及对象数据则是另外一种更复杂一点的东西……但是一些东西在你能够熟练操作整形变量后将会变得很容易。希望第一部分对你来说是个好的开始。
第二部分 定义对象
这个主题就有点复杂了……我曾不止一次在尝试做module时候在这方面碰壁。
大部分动作要把一个对象作为目标。定义对象最简单的方法是掌握它们的特点……当然这在你知道这些对象是什么和它们的特点是什么的时候很行得通。但如果你不知道呢?如果你想找到一个最近的玩家角色,不管它是谁?如果你想选定一个最近的生物?或者你想测量一些东西之间的距离?
有很多方法能完成上面的任务……我打算把重点放在三个语句上:GetNearestCreature, GetNearestObject和GetDistanceBetween。
A. GetNearestCreature 这个命令允许你通过最多三个的参量来挑选在对象周围指定距离内的生物。记住,选中的目标不一定是离对象最近的符合条件的生物——那只是默认的。举个例子,如果你想定位一个离对象第二接近的非玩家人类盗贼,那么这个语句就是你要用的了。
语法是这样的:
object GetNearestCreature (int nFirstCreatureType, int nFirstParameter, object oTarget=OBJECT_SELF, int nNth=1, int nSecondCreatureType = -1, int nSecondParameter = -1, int nThirdCreatureType = -1, int nThirdParameter = -1 )
可能看起来有点乱,我们一起来过一遍。
nFirstCreatureType和nFirstParameter是你要找的生物的首要性质。它们的意思是说你要找最近的人类。'nFirstCreatureType' 可以归结为属性:CREATURE_TYPE_ RACIAL_TYPE,nFirstParameter可以归结为参量:RACIAL_TYPE_HUMAN.。
明白了?你可以通过三个参数来缩小你搜索的范围……每一个都需要属性和参量。
'oTarget' 对象当然指的是你正在找附近的什么生物。当你在一定范围内搜索生物用来运行代码的话,它可以是OBJECT_SELF(默认)你也可以做点令人胆战心惊的事,例如查找离最近的玩家最近的生物是什么……但是我不想把它写出来。
整形变量'nNth'是用来决定你是否需要距离最近的还是其他距离的。默认的是距离最近的……但如果你指定数字N的话就会变成从离对象最近的那个符合条件的开始,跳过N个所找到到生物。记住在这里任何返回数据为整数的函数都可以使用。举例来说,如果你想随机得到从最近到第四近的生物中的一个,你可以在这里放上d4()。
下面,就是你可以选择的生物类型:
·CREATURE_TYPE_CLASS = 角色或怪物的职业。
·CREATURE_TYPE_DOES_ NOT_HAVE_SPELL_EFFECT = 找一个没有特殊魔法的特殊生物(参考某些永久性魔 法)……我不认为这会经常发生。
·CREATURE_TYPE_SPELL_EFFECT = 与上面相反,找一个拥有特殊魔法的生物。记住不是所有的魔法效果 都表现为施法。
·CREATURE_TYPE_IS_ALIVE = 自己想一下,一般你不会找一个死去的生物吧?
·CREATURE_TYPE_PERCEPTION = 这是比较奇怪的一个,理论上讲,这不是一个生物的属性,但这是一种 作用在对象身上的自然感知力……例如它们是否是可见的或者能被听到。
·CREATURE_TYPE_PLAYER_CHAR = 这个基本上是区分选择的生物是玩家还是非玩家。
·CREATURE_TYPE_RACIAL_TYPE = 生物种族
·CREATURE_TYPE_REPUTATION = 按照小队的声望值级别来选择。
所有的生物的种族,职业和产生效果都可以明显的被这些参量表现出来,下面我就不一一叙述了……尽管有些还不是很明确。
a.CREATURE_TYPE_IS_ALIVE:(是否是活的生物)
TRUE or FALSE (easy)
b.CREATURE_TYPE_PERCEPTION:
PERCEPTION_HEARD(生物可以被听到)
PERCEPTION_HEARD_ AND_NOT_SEEN(生物可以被听到,但不可见)
PERCEPTION_NOT_HEARD(不能被听到的生物)
PERCEPTION_NOT_SEEN(生物不可见)
PERCEPTION_NOT_SEEN_ AND_NOT_HEARD(不可见也不可被听到的生物)
PERCEPTION_SEEN(可见的生物)
PERCEPTION_SEEN_AND_HEARD(可见可听到的生物)
PERCEPTION_SEEN_ AND_NOT_HEARD(可见不可被听到的生物)
c.CREATURE_TYPE_PLAYER_CHAR:
PLAYER_CHAR_IS_PC(玩家控制的生物)
PLAYER_CHAR_NOT_PC(非玩家生物)
d.CREATURE_TYPE_REPUTATION:
REPUTATION_TYPE_ENEMY(生物为敌人)
REPUTATION_TYPE_FRIEND(生物为朋友)
REPUTATION_TYPE_NEUTRAL(生物中立)
--------
Q:我怎样才能找到离我最近的玩家角色?
A:GetNearestCreature (CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC);
Q:我怎样才能找到离我最近的魔法师?
A: GetNearestCreature (CREATURE_TYPE_ REPUTATION, REPUTATION_ENEMY, OBJECT_SELF, 1, CREATURE_TYPE_CLASS, CLASS_TYPE_WIZARD);
Q:离玩家联盟的最近的敌人?
A: GetNearestCreature (CREATURE_TYPE_ REPUTATION, REPUTATION_ENEMY, GetNearestCreature (CREATURE_TYPE_ PLAYER_CHAR, PLAYER_CHAR_IS_PC, OBJECT_SELF, 1, CREATURE_TYPE_ REPUTATION, REPUTATION_FRIENDLY), 1, CREATURE_TYPE_ IS_ALIVE, TRUE);
Q:我能看见的离我最近的非玩家生物?
A: GetNearestCreature (CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_NOT_PC, OBJECT_SELF, 1, CREATURE_TYPE_ PERCEPTION, PERCEPTION_TYPE_ SEEN);
B.GetNearestObject 这个就比上面那个命令容易多了,因为你可以仅仅通过对象的类型来选择对象……这个命令没有那么多复合参量,事实上,这两个命令工作方式差不多。
object GetNearestObject (int nObjectType=OBJECT_TYPE_ALL, object oTarget=OBJECT_SELF, int nNth=1)
'nObjectType' 很显然是你要选择的对象的类型。如果你什么都没指定,那么结果将是最近的任何类型的对象。
'oTarget'对象是指你选择的最近的对象的关系是什么。默认为OBJECT_SELF,当然也可以更改。
'nNth' 是个整数,允许你选择第N最近的生物。
对象的类型是:
OBJECT_TYPE_ALL
OBJECT_TYPE_AREA_OF_EFFECT
OBJECT_TYPE_CREATURE
OBJECT_TYPE_DOOR
OBJECT_TYPE_INVALID
OBJECT_TYPE_ITEM
OBJECT_TYPE_PLACEABLE
OBJECT_TYPE_STORE
OBJECT_TYPE_TRIGGER
OBJECT_TYPE_WAYPOINT
C.GetDistanceBetween 这个语句是用来报告两个有效对象的距离的(以米为单位)。记住,必须是有效对象,否则将会返回0的数值。
float GetDistanceBetween(object oObjectA, object oObjectB)
(什么是浮点?一个浮点,像一个整数,只是个数字……但浮点通常带有小数点,50是一个整数……50.0则是一个浮点。如果你需要,你可以用这个语句int FloatToInt(float fFloat) 将浮点转换为整数。)
如果你只是想简单的察看一个东西离一个生物或者一个对象多么远,并以次来运行代码,事实上还有一个更简单的语句:
float GetDistanceToObject(object oObject)
这样你就不必在GetDistanceBetween语句中不断的指定对象OBJECT_SELF了。
第三部分 创建对象
object CreateItemOnObject(string sItemTemplate, object oTarget = OBJECT_SELF, int nStackSize = 1)
这段语句在oTarget对象的物品栏中创建了一个物品(名称为'sItemTemplate'处指定的),在物品是别人送给玩家的时候,我们常常用到这个语句,另外或者说,NPC应该只在某些必要的时候才拥有对。
假设我们在游戏对话中,一个NPC要送给玩家一把普通的长剑作为报酬。我将会把下面的这段代码防到'Actions Taken' 段中去:
CreateItemOnObject ("NW_WSWLS001", GetPCSpeaker());
如果我要把12支普通的箭放到箱子中去,那么代码应该有一个"CHEST05"的标签:
CreateItemOnObject ("NW_WAMAR001", GetObjectByTag ("CHEST05"), 12);
(注:如果你使用ActionGiveItem来让NPC给玩家一件物品,那么这件物品必须事先真正存在于NPC的物品栏中。ActionGiveItem将会把物品传送到玩家的物品栏中……而CreateItemOnObject则是在物品栏中创一个全新的物品。)
(注2:提醒一下,语句中的sItemTemplate是物品的标签(TAG)……对于游戏本身包含的标准物品这是事实。对不起,这是我的错误:应该说,如果你要处理一件自定义的物品,你必须使用自定义物品的蓝图,而不是标签(TAG)。
--------------
'CreateObject' 有点不一样,事实上这个命令是在地图上创建物品……通常就是指"生成",对象可以是生物或者物品,语句结构如下:
object CreateObject (int nObjectType, string sTemplate, location lLoc, int bUseAppearAnimation = FALSE)
'nObjectType' 是用来定义被创建物品类型的一个常数(下同),举例来说,OBJECT_TYPE_CREATURE或者是OBJECT_TYPE_ITEM。这取决于代码编写者的常量表。
'sTemplate' 是指'blueprint resref'。我知道大多数其他物品都使用标签,这个命令则是引用RESREF,RESREF通常都能在对象的高级TAB下找到。
注:上述的都是为了防止你犯一些经常性的错误,如果在自定义物品中你使用标签来替代resref,那么代码将不会工作。
一个区域是由一块地区,一组xyz矢量坐标和贴图组成的。得到这些信息最容易的方法是使用GetLocation(object oObject)语句,它可以从路点或者其他对象中得到这些信息(如果你将一个生物作为对象,那么它将在最近的"合法"地点生成)。如果需要,你也可以使用Location(object oArea, vector vPosition, float fOrientation)创建一个区域……虽然大家并不经常这样做。
默认的情况下,是不使用标准的appearance animation的,你可以通过将'bUseAppearAnimation'指定为true来使用它。
我们在已存在的路点上创建一个生物吧,将路点的标签(tag)设为"WAYPOINT1":
location spawn1 = GetLocation (GetWaypointByTag ("WAYPOINT1")); CreateObject (OBJECT_TYPE_CREATURE, "NW_BUGBEARA", spawn1);
用带特效的过程创造一个魔法师(标签为wizard01),并让他面对最近的玩家: 代码:
void main() { object oWay = GetWaypointByTag("WAYPOINT1"); vector vPos = GetPosition(oWay); vector vPC = GetNearestCreatureToLocation(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC, vPos); location lPos = Location(GetArea(oWay), vPos, SetFacingPoint(vPC)); CreateObject(OBJECT_TYPE_CREATURE, "wizard01", lPos, TRUE); }
对于标签(tag)我们最后要说的是:记住标签是十分敏感的!!如果你的生物wizard01的标签没有在代码中以WIZARD01或者Wizard01被查阅到,它将不会正确的获得生物对象(在这里是魔法师)。结果是获得了一只刺猬!!为什么是刺猬?那只是一个简单的“默认召唤”,为的是防止在测试中万一出现非法的对象。
第四部分 关于动作
基本上来说,在你为你创造的生物上定制动作的时候,你有两种选择:直接添加动作和使用一般人工智能动作。
A.直接添加动作
在你编写的你的动作代码的时候你首先要明确一件事:不是所有的生物都可以做全部的动作。如果生物是玩家也可以扮演的种族(人类、精灵、矮人、半兽人、侏儒或者半身人),那么他们可以做全部的动作。如果生物属于类人生物(地精之类的),他们可以做一些动作但非全部,非人型生物特别是鸟之类的东西的动作将会非常有限。
动作序列
在编写动作代码的时候要知道的第二件事就是如何使用动作序列,有很多代码语句是以关键字'ACTION'开头的……当这些代码在生物身上运行的时候,它们将会把动作排成一个序列。这些动作会一个一个得被完成……直到所有的动作都被完成或者遇到ClearAllActions()语句。
这里有两个编写动作代码的主要函数:ActionPlayAnimation和PlayAnimation。ActionPlayAnimation将所有动作排成动作序列并且执行它们。PlayAnimation则是当含有该语句的代码在一个生物身上运行的时候,使生物马上执行这个动作,这时候他将不会执行动作序列中的其它动作。
如果我要一个人按照路点移动一段距离然后沉思6秒钟,代码是这样的:
void main() { object oTarget = GetNearestObjectByTag("WAYPOINT1"); ActionMoveToObject(oTarget); ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 6.0); }
这个人会在路点上移动直到他到达目的地,然后开始沉思的动作。
如果我要在他完成所有动作后设置一个变量,我也需要将这个变量添加到动作序列中去。你可以使用ActionDoCommand()来达到这个目的。这个语句会在动作序列中放进一个非动作语句。 代码:
void main() { object oTarget = GetNearestObjectByTag("WAYPOINT1"); ActionMoveToObject(oTarget); ActionPlayAnimation(ANIMATION_LOOPING_MEDITATE, 1.0, 6.0); ActionDoCommand(SetLocalInt OBJECT_SELF, "Done_Meditation", 1); }
如果我将SetLocalInt语句放在动作序列之外执行,那么这个人将会在到达代码中指定的点后马上执行它……甚至也许在这个人到达指定地点之前就执行它。
这两个动作函数如下所示:
void ActionPlayAnimation (int nAnimation, float fSpeed=1.0, float fSeconds=0.0)
-nAnimation是是动作执行的常数。 -fSpeed是指动作执行的速度。你可以让那个人回头回的很快或者很慢。举例来说,1.0是通常的速度。 -fSeconds只是用来决定动作执行时间长短的,如果在这里留空白,那么生物将会持续执行这些动作直到他被告知做别的事。
void PlayAnimation (int nAnimation, float fSpeed=1.0, float fSeconds=0.0)
就像上文所提到的,这个语句基本上和ActionPlayAnimation相同,只是这个语句所执行的动作不会放到动作序列中去而是会马上执行。
动作常数
你可以通过在你的代码编辑器里选择'Constants' 按钮来查看所有的动作常数(它们在语句的nAnimation'字段中使用),所有的动作常数都以ANIMATION_*开头。
有两种AI动作的方式:'fire-and-forget' (或者叫FNF,他们只做一次并且没有持续时间)和'looping' (运行的时间根据需要要多长就可以多长,并且有持续时间)
提醒一下:不是所有的物体都拥有全部的动作,动作列表中的动作不是所有的物体都能做的(例如死亡动作,还有战斗动作等等)列表中只包括当前物体通过代码所能做的动作。
B.使用一般人工智能动作
为了快速和简单的添加一些常见动作到你制作的生物上去,一般人工智能提供了两个函数供你使用。
在一般的OnSpawn代码中("nw_c2_default9")有全部的经过注释的动作代码(这些代码全部用绿色的双斜线"//"开头以防止它们被编译),你只需要简单的去掉双斜线就可以在代码中使用这些动作。 代码:
SetSpawnInCondition(NW_FLAG_AMBIENT_ANIMATIONS);
SetSpawnInCondition(NW_FLAG_IMMOBILE_AMBIENT_ANIMATIONS);
不用管那些与语句同一行的注释,这些注释只是简单的告诉你语句是干什么用的,简单的删除掉注释前面的双斜线。
然后你重新编译这些代码并且存为另外一个文件,好了,你完成全部工作了,这就是所有你要做的了。
它们是干什么的?
基本上说它们被叫做OnHeartbeat事件(意思是说代码每隔6秒钟被激活一次)。代码检查并确定这个生物没有在睡觉,没有在战斗,没有在对话,也没有敌人在视野之内……如果所有这些检查都通过了,他就会执行动作。
'Ambient animations'的意思是这个生物将会随机移动,偶尔停下来转向朋友(与该生物呈现友方状态的生物)并且做一些社交性的动作。(这些动作对所有的生物都有效,它们都只会执行这类生物所可以执行的动作)
'Immobile ambient animation'是一样的……但不是随机移动,而是站在原地。
所以举个例子说,你可以创建几个这样的生物,你可以看到它们在随机的移动,聊天,争论甚至混到一起……
那么可放置物体是否也可以同样的工作?
是的,你可以通过运行ANIMATION_PLACEABLE_OPEN让一个箱子自己打开,或者运行ANIMATION_PLACEABLE_DEACTIVATE让一盏灯熄灭。记住几件事:
1)对于可放置物体来说很多时候它们都带有一个光源(sources),你只是用ANIMATION_PLACEABLE_DEACTIVATE或者ANIMATION_PLACEABLE_ACTIVATE关闭/打开它们是不够的,那只是做用于动作照亮的部分,你必须还要将SetPlaceableIllumination语句设为TRUE来告诉区域RecomputeStaticLighting。(重新计算静态光源)
下面是一个使用可放置物体光源的例子:
// will turn the lightable object on and off when selected // placed in its OnUsed event void main() { if (GetLocalInt (OBJECT_SELF,"NW_L_AMION") == 0) { SetLocalInt (OBJECT_SELF,"NW_L_AMION",1); PlayAnimation (ANIMATION_PLACEABLE_ACTIVATE); SetPlaceableIllumination (OBJECT_SELF, TRUE); RecomputeStaticLighting (GetArea(OBJECT_SELF)); } else { SetLocalInt (OBJECT_SELF,"NW_L_AMION",0); PlayAnimation (ANIMATION_PLACEABLE_DEACTIVATE); SetPlaceableIllumination (OBJECT_SELF, FALSE); RecomputeStaticLighting (GetArea(OBJECT_SELF)); } }
2)门不属于可放置物体,首先你需要知道的是如果门没锁,那么被告知移动到门另一边目的地的生物会自己打开它。
-ActionOpenDoor:如果是生物执行,这个生物将会移动到门并打开它(如果门没锁)如果是门执行(或者是通过AssignCommand语句将ActionOpenDoor发送到门这个对象上),门将会自己打开。
-ActionCloseDoor:像上面一样,不过是关门。
-ActionLockObject:如果在一个生物身上执行,生物将会移动到对象处(可以是门或者可放置物体)并且尝试使用它的开锁技能打开它。 注意:只能在生物身上执行!!
-ActionUnlockObject:像上面一样,不过门或者对象是没被锁上的。
-SetLocked:如果你希望一扇门或者其他对象再没有生物或者其他东西的帮助下自己锁上/打开,那么使用这个命令吧。如果'bLocked'被设置为TRUE,对象将会被锁上;如果设置为FALSE,对象会被打开(例子:如果在一段门执行的代码里使用它,SetLocked (OBJECT_SELF, TRUE)将会使门被锁上。 代码:
// set in a door's OnHeartbeat script, this will cause // it to close and lock itself at dusk // and unlock itself at dawn void main() { if (GetIsDusk() && GetIsOpen (OBJECT_SELF)) { ActionCloseDoor (OBJECT_SELF) // SetLocked is set in an ActionDoCommand because we // want it to be in the door's queue... we want the // ActionCloseDoor to be completed before locking the door ActionDoCommand (SetLocked (OBJECT_SELF, TRUE)) } else if (GetIsDawn() && GetLocked (OBJECT_SELF)) { SetLocked (OBJECT_SELF, FALSE); } }
第五部分 制作路点
路点(waypoint)是一个不可见的对象,它放置在一块区域上来标记精确位置。它拥有自己的标签(tag),这个标签在你创建路点的时候可以设置。这个标签也可以添加到'map note'中去。(举例来说,你可以将一个路点标为'General Store',然后在小地图上就会出现一个表示这个路点的图标,如果玩家的鼠标指针移动到上面,就会出现说明文字'General Store')
由于NWN包含了一般人工智能,所以你可以使你创建的生物自动的在各路点之间走来走去。拿出你希望走路点的生物的标签(TAG),加上前缀"WP_"和后缀"_0X",然后生物就可以自动在它们之间按顺序行走了。
例子:
我有一个NPC卫兵,我给这个卫兵一个标签"GUARD",我在地图上放置了四个路点作为他的巡逻路线,我将这些路点的标签命名为:"WP_GUARD_01", "WP_GUARD_02", "WP_GUARD_03"和"WP_GUARD_04"。/*通过默认的AI|根据默认的人工智能|*/,当游戏开始时他会首先处理离他最近的路点(不管是不是1号路点"WP_GUARD_01"),然后按照数字顺序移动到下一个路点,他继续走,在每个路点上停留1秒钟,一旦他走到"WP_GUARD_04"他就会回到"WP_GUARD_01",如果他看到任何敌对目标,他就会移动到那里并且攻击,当战斗结束后,他会返回继续巡逻。
上文是最简单的一个使用路点的例子,当然,还有很多方法实现它,在路点的制作中,可能会有一些问题:
1.如果我为我的NPC们使用了很多路点,那么屏幕上的路点就太多了!!
答:一个路点不一定只供一个生物使用……它只是供一个标签使用。如果你愿意,同样的生物可以拥有同样的标签。 另外路点还有4种颜色,你可以通过选择这些颜色来区别路点。 尽管如此,你的区域上可能还是有不少路点,这取决于你正在干什么。幸运的是制作路点并不需要运行代码或者消耗系统资源,如果在TOOLSET中它挡住了你的视线,上面有一个按钮可以让它们不被显示。(还有很多同种类的按钮来关闭对象,生物,物品或者任何什么东西)
2.听起来为所有的路点的标签重命名是一件很繁重的工作。
答:如果你希望的话,你可以一个接一个的编辑路点和它们的标签。幸运的是,你可以再按下SHIFT键的同时按照你希望的顺序选择路点,然后右击鼠标选择'Create Set'。如果你提供了标签的名字(在上面的例子中是'WP_GUARD'),系统就会自动改变它们并为你加上数字后缀。
3.如果我想使用闭合式路点圆圈的一部分来穿过/进入某些区域,比如说进一所房子,我应该怎么做?
答:这很好办。我猜你可能有一个通过区域创建向导创建的触发式的出入口,而不是通过自己编写代码创建的出入口。 我最近创建了一个要行走四个路点的平民,其中两个在另一个区域(在房子里)。我把房子建在一个主要区域里并且用门把它们连接起来。这个平民只是在使用这个门的时候不知道关门,其他的一切正常。
更多的高级用法:
POSTS:除了行走用的路点,你也可以让一个生物放哨('POST'),放置单个路点,然后以生物的名字加上前缀'POST_'来命名这个路点。 生物将会停留在这个路点上。如果他离开这个路点去战斗或者干别的什么,一旦完成,他会很快的回来继续停留下去。
Day/Night Activities:昼/夜活动。在一般OnSpawn代码中,你可以运行一个语句:
SetSpawnInCondition (NW_FLAG_DAY_NIGHT_POSTING)
这个语句会告诉生物昼夜的分别。 它怎么对路点产生效果?你可以给你的生物两套不同的路点分别在白天和晚上使用,只要改一下路点标签的前缀:
白天路点:前缀:"WP_" 夜晚路点:前缀:"WN_" 白天POST路点:前缀:"POST_" 夜晚POST路点:前缀:"NIGHT_"
然后你就可以为例子中的卫兵设计一套白天的路点,然后如果你在OnSpawn代码中运行了SetSpawnInCondition (NW_FLAG_DAY_NIGHT_POSTING),你可以将卫兵晚上的路点设为他的兵营,那么到晚上时卫兵就会回到兵营去。
要记住,当你对OnSpawn代码做任何修改之后,一定要将它以不同的文件名存盘然后编译,否则它会对你MODULE中的所有生物都起作用……最后让你的MODULE产生天翻地覆的变化。
第六部分 用户自定义事件(UserDefinedEvents)
严肃的说,很多用户想更深入的编写代码以创造出更加多种多样的和更加个性化的人工智能,他们当中的一些甚至可能自己编写出完整的AI系统。这很好,也并没有错。
好消息是:你不必这样做。NWN官方战役中的所有生物代码98%都可以在用户自定义事件中实现。我曾经和Preston(NWN程序组的程序员,也是AI系统的主要编写者)坐在一起,我们很快的制作了一系列通常只有通过代码才能实现的事件,就像所说的一样,每个人都能很好的驾驭UserDefinedEvents。并且它非常易于执行。
我们先复习和讨论一下什么是事件。基本上说没个生物上都有大量的不同代码,这些代码与各种事件联系在一起。当事件发生时,它告诉生物触发正确的代码。没有事件=代码不运行。
这里提及过很多事件,'OnHeartbeat'事件每六秒钟触发一次,所以代码就这样频繁的被触发。当NPC感知到什么东西的时候,'OnPerceived' 事件发生了,相应的代码也被触发。这段代码又可能触发别的事件,然后这些事件又触发不同的代码……
基本上,用户自定义事件(UserDefinedEvent)允许用户在不打断和干扰生物正常的AI规律和已存在的代码的前提下,访问该生物的标准事件(OnPerception, OnDeath等等)。
使用UserDefinedEvent
有一个每一个生物都有的,前面被提到无数次的,并且将来你还会了解的更清楚的代码,它的名字叫OnSpawn代码。这是每一个生物第一次被创造出来的时候所运行的代码,你可以将它看作初始化代码。
一般的OnSpawn(generic OnSpawn)代码则包含了所有被注释出来的语句……事实上,当用户没有改动它的时候,这段庞大的代码只做三件事:告诉生物按照标签走指定好的路点;设置听模式(这样生物就会对附近NPC战斗时发出的叫喊做出反应);在物品栏里生成一点一般的财产(比如金币之类的)。
它被设计成允许用户进入和设置一般人工智能,让AI变成用户希望的样子。所有用户所要干的就是进入这段代码将所希望的功能前面的注释符号:双斜线"//"去掉。然后将这段代码以不同的名字存盘然后在你需要的地方使用它。瞧,那个生物的行为改变了……
我们需要注意以下几个状态标记:
SetSpawnInCondition (NW_FLAG_PERCIEVE_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1002
SetSpawnInCondition (NW_FLAG_ATTACK_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1005
SetSpawnInCondition (NW_FLAG_DAMAGED_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1006
SetSpawnInCondition (NW_FLAG_DISTURBED_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1008
SetSpawnInCondition (NW_FLAG_END_COMBAT_ROUND_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1003
SetSpawnInCondition (NW_FLAG_ON_DIALOGUE_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1004
SetSpawnInCondition (NW_FLAG_DEATH_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1007
下面呢?
好了,你已经将OnSpawn中的生物行为改成了你需要的样子并且重命名存档了,那么我们怎么使用它?
每个生物都有用户自定义事件(OnUserDefined event),你可以将代码放到这些事件中去。对于你创造的每个生物来说,这个事件目前是完全空白的……这就是你要编写代码的地方了。当你所定义的类型的事件发生时,任何放在这里的代码都会被检查。所以当你定义一个'SetSpawnInCondition (NW_FLAG_DISTURBED_EVENT)'事件时,如果该生物的物品栏受到干扰(例如被盗贼偷窃),放在用户自定义事件(OnUserDefined)中的代码就会运行。
基本上OnUserDefined中的代码和这个是相似的:
void main() { int nUser = GetUserDefinedEventNumber(); if (nUser == the number in the OnSpawn, from 1001 to 1007) { (do something) } }
我们假设让生物和第一个他看见的玩家挥手。我进入OnSpawn设置'SetSpawnInCondition (NW_FLAG_PERCIEVE_EVENT)' 并且将其重命名并且存盘。然后我在用户自定义事件(OnUserDefined)中编写一段代码:
// this script will make the NPC wave to a PC upon perceiving them // remember that if the PC starts off the module in the NPC's perception // range, no event will fire (because perception has not changed) void main() { int nUser = GetUserDefinedEventNumber(); if (nUser == 1002) // OnPerception event { object oCreature = GetLastPerceived(); int nWave = GetLocalInt(OBJECT_SELF, "Wave_Once"); //check to see if I actually saw a PC if (GetLastPerceptionSeen() && GetIsPC(oCreature) && (nWave == 0)) { // wave only once SetLocalInt(OBJECT_SELF, "Wave_Once", 1); // set a timer so that the NPC can wave again 15 seconds later DelayCommand(15.0, SetLocalInt(OBJECT_SELF, "Wave_Once", 0)); // turn to face the PC ActionDoCommand(SetFacingPoint(GetPosition(oCreature))); // wave howdy ActionPlayAnimation(ANIMATION_FIREFORGET_GREETING); SpeakString("Hello!"); } } }
好了,完成了。我也可以做一些其他的设置例如最小距离,看看玩家是否正在面对我……或者作些更复杂的动作……我并不需要去理会那些在OnPerceive事件中的疯狂的AI代码。
第七部分 完全注释的OnSpawn代码
在标准的OnSpawn("nw_c2_default9")代码中,并不是所有的语句行都解释了它们的函数事实上是怎样被使用的。这是一个OnSpawn升级版本,如果你愿意,你可以使用它。
代码:
//:://///////////////////////////////////////////// //:: Custom On Spawn In //:: File Name //:: Copyright (c) 2002 Bioware Corp. //::////////////////////////////////////////////// /* */ //::////////////////////////////////////////////// //:: Created By: //:: Created On: //::////////////////////////////////////////////// #include "NW_O2_CONINCLUDE" #include "NW_I0_GENERIC" void main() { // OPTIONAL BEHAVIORS (Comment In or Out to Activate ) **************************************************************************** //SetSpawnInCondition(NW_FLAG_SPECIAL_CONVERSATION); // This causes the NPC to speak a single line from their dialogue file // upon perceiving a player. Make sure that the line being spoken is at the // very top of the NPC's other dialogue starting nodes and that you have // placed the script "nw_d2_gen_check" in that line's 'Text Appears When' area. // Do not use this flag for hostile creatures. //SetSpawnInCondition(NW_FLAG_SPECIAL_COMBAT_CONVERSATION); // This flag is similar to the above... except that it allows a hostile NPC // to display a single line of dialogue before attacking. Put the line into the // NPC's dialogue file as above, but place "nw_d2_gen_combat" into 'Text Appears When' //SetSpawnInCondition(NW_FLAG_SHOUT_ATTACK_MY_TARGET); // This sets up the NPC so that any NPC of a faction who is friendly to it // who is attacked or attacks an enemy (and is using the generic AI) will issue a shout // that this NPC will now listen and respond to. //SetSpawnInCondition(NW_FLAG_STEALTH); // If the NPC has Hide skill they will automatically be in Stealth Mode // but only when the WalkWayPoints command is called (below) //SetSpawnInCondition(NW_FLAG_SEARCH); // If the NPC has the Search skill they are automatically in Search Mode // but only when the WalkWayPoints command is called (below) //SetSpawnInCondition(NW_FLAG_SET_WARNINGS); // This will set the NPC to give a warning to non-enemies before attacking //SetSpawnInCondition(NW_FLAG_DAY_NIGHT_POSTING); // This seperates the NPC's waypoints into night and day. Normally a waypoint prefix "WP" // or "POST" would be used always. If this flag is set, those prefixes are used in the day // and "WN" or "NIGHT" prefixes are used at night. //SetSpawnInCondition(NW_FLAG_APPEAR_SPAWN_IN_ANIMATION); // when the creature spawns in, it uses EffectAppear() instead of fading in // but only if SetListeningPatterns is called (below) //SetSpawnInCondition(NW_FLAG_IMMOBILE_AMBIENT_ANIMATIONS); // this causes the NPC to use common animations it possesses, and will appear more // social if placed near a friendly NPC (they will turn to each other and use social animations) //SetSpawnInCondition(NW_FLAG_AMBIENT_ANIMATIONS); //This is similar to the above, except that the creature will also move around randomly //NOTE that these animations will play automatically for Encounter Creatures. // NOTE: ONLY ONE OF THE FOLOOWING ESCAPE COMMANDS SHOULD EVER BE ACTIVATED AT ANY ONE TIME. //SetSpawnInCondition(NW_FLAG_ESCAPE_RETURN); // OPTIONAL BEHAVIOR (Flee to a way point and return a short time later.) //SetSpawnInCondition(NW_FLAG_ESCAPE_LEAVE); // OPTIONAL BEHAVIOR (Flee to a way point and do not return.) //SetSpawnInCondition(NW_FLAG_TELEPORT_LEAVE); // OPTIONAL BEHAVIOR (Teleport to safety and do not return.) //SetSpawnInCondition(NW_FLAG_TELEPORT_RETURN); // OPTIONAL BEHAVIOR (Teleport to safety and return a short time later.) // to use 'escape', you need a waypoint the NPC will flee to with the tag "EXIT_" + the NPC's tag // these commands can be activated in a script by calling ActivateFleeToExit(), "NW_IO_GENERIC" must be included // for the escape commands that have 'return', the NPC will be re-spawned at it's starting location // CUSTOM USER DEFINED EVENTS /* The following settings will allow the user to fire one of the blank user defined events in the NW_D2_DefaultD. Like the On Spawn In script this script is meant to be customized by the end user to allow for unique behaviors. The user defined events user 1000 - 1010 */ //SetSpawnInCondition(NW_FLAG_HEARTBEAT_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1001 //SetSpawnInCondition(NW_FLAG_PERCIEVE_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1002 //SetSpawnInCondition(NW_FLAG_ATTACK_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1005 //SetSpawnInCondition(NW_FLAG_DAMAGED_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1006 //SetSpawnInCondition(NW_FLAG_DISTURBED_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1008 //SetSpawnInCondition(NW_FLAG_END_COMBAT_ROUND_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1003 //SetSpawnInCondition(NW_FLAG_ON_DIALOGUE_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1004 //SetSpawnInCondition(NW_FLAG_DEATH_EVENT); //OPTIONAL BEHAVIOR - Fire User Defined Event 1007 // DEFAULT GENERIC BEHAVIOR (DO NOT TOUCH) ***************************************************************************************** SetListeningPatterns(); // Goes through and sets up which shouts the NPC will listen to. WalkWayPoints(); // Optional Parameter: void WalkWayPoints(int nRun = FALSE, float fPause = 1.0) // 1. Looks to see if any Way Points in the module have the tag "WP_" + NPC TAG + "_0X", if so walk them // 2. If the tag of the Way Point is "POST_" + NPC TAG the creature will return this way point after // combat. // To get the NPC to continue walking waypoints after conversation, you must go // into its dialogue file and, under 'Other Files', call this command again in the // 'End' and 'Aborted' script sections GenerateNPCTreasure(); //* Use this to create a small amount of treasure on the creature }
第八部分 经验值和金钱的获取
两个关于这项工作的函数很简单:
void GiveGoldToCreature(object oCreature, int nGP)
void GiveXPToCreature( object oCreature, int nXpAmount)
经验值或者金钱从哪里来的并不重要,关键是它们要到哪里去……所以这两个函数可以在任何地方运行。
给一个正在与NPC对话的玩家100金币的代码是(该代码放置在'Actions Taken'中):
GiveGoldToCreature (GetPCSpeaker(), 100);
给一个小队所有成员100经验值,从最近的成员开始:
object oFirstMember = GetNearestCreature(CREATURE_TYPE_PLAYER_CHAR, PLAYER_CHAR_IS_PC); object oPartyMember = GetFirstFactionMember(oFirstMember, TRUE); while (GetIsObjectValid(oPartyMember) == TRUE) { GiveXPToCreature(oPartyMember,100); oPartyMember = GetNextFactionMember(oFirstMember, TRUE); } }
(推荐把这段代码放进一个函数中去,这样就不必每次都打一遍了)
-当你在冒险日志编辑器中写冒险日志的时候,你可以为每个章节都分配一个'XP Value',这个数据可以通过以下语句得到:
int GetJournalQuestExperience(string szPlotID)
在我们的战役中,我们对每一个任务(其中某些任务要经过数章才能完成)都分配了一个PlotID,每一个PlotID都有一个XP Value,然后我们根据任务完成度按百分比来发放这些经验值,例如完成这个部分将给予任务总经验值的50%,完成那个部分给25%等等。
-如何知道某个人有多少钱?
int GetGold( object oTarget = OBJECT_SELF)
所以如果你想知道正在运行代码的人有多少钱你只要运行GetGold()就可以了,但是这样可能会导致语句定位任何人……
-如果你想让某个人从其他人身上获得金钱,你可以使用这个函数:
void TakeGoldFromCreature( int nAmount, object oCreatureToTakeFrom, int bDestroy = FALSE)
如果'bDestroy'是true,那么获得金钱的生物将不会把钱放进口袋里,这些金钱将会被毁灭,也就是说从这个世界上消失。
第九部分 制作技能检定
首先,如果你不明白D&D第三版规则关于技能检定的设定,那么这里提供了一个基本的说明:在游戏中当你使用一个技能行动的时候,你需要掷1个20面骰,然后将掷得的结果加上与该技能相关的修正(包括技能级别和属性修正),并且和DM提供的困难级别(DC)做比较,如果等于或高于DC,则技能使用成功。
假设玩家要解除一个他探测到的陷阱。DM将困难级别设为25(一个一般难度的数值)。玩家解除陷阱的技能为8级并且有+3的敏捷修正(与解除陷阱相关的属性为敏捷)。他掷20面骰后得到的数值加上11如果大于25,则解除陷阱成功,也就是说他至少要掷出14才可以解除陷阱。
大多数技能检定例如解除陷阱都是由游戏引擎控制的,所需要的就是让DM提供DC的数值。在NWN代码中没有语句让你制做技能检定。尽管如此,如果你想在代码中使用技能检定(例如检定在对话中对方是否被你说服或者其他一些东西)。你就需要编写代码或者使用我们编写的官方战役函数。
官方战役函数
它是关于AutoDC语句的(这个语句在官方战役中被定义为自动生成DC值),它并不是每次都需要DM指定DC值。当代码运行时,它会根据NPC的级别来从DC_EASY,DC_MEDIUM ,DC_HARD (低难度,中等难度,高难度)中选择一个级别生成DC值。
语法如下:
int AutoDC(int DC, int nSkill, object oTarget)
所以如果我想让一个NPC在与玩家的对话中比较难被说服,我可以这样写:
AutoDC(DC_HARD, SKILL_PERSUADE, GetPCSpeaker());
记住,因为这个语句应该返回TRUE或者FALSE,所以技能检定将返回一个整数型变量('int')。当FALSE时(或者说整形变量为0),检定失败。TRUE(整形变量为1)则检定成功。
如果你在对话编辑器中使用技能检定,你需要将对话分支。这意味着同一个玩家的对话要有不同的分支,也就是不同的回应。当对话被选择的时候,系统将会按照从上至下的顺序检查'Text Appears When'中的节点(NODE,详见第一章),系统会运行与遇到的第一个返回值为TRUE的节点相关的代码。
一个简单的制作说服检定的例子:
#include "NW_I0_PLOT" int StartingConditional() { int iCheck = AutoDC(DC_HARD, SKILL_PERSUADE, GetPCSpeaker()) == TRUE; return iCheck; }
所以你可以做出个类似下面的东西:
"Oh, I certainly couldn't tell you that. It's confidential."(哦,我当然不能告诉你,那是秘密) A. <Persuade> "Oh, I'm sure there's no harm in telling me." (说服:我相信告诉我不会有事的) B. <Threaten> "Tell me or suffer the consequences, wench." (恐吓:不告诉我的话你就准备承受后果吧,混蛋) C. "Let me ask you something else, then."(那我问你点别的)
如果选择B,对话将会分为两种路线处理:
#1.(在'Text Appears When'中检查到关于说服的代码)我觉得你是对的,好吧,我告诉你……
#2.(无代码)算了吧,我才不告诉你呢。
上述的#1是成功的方式,#2是失败的方式,如果代码在#1处返回为TRUE,那么#1将会被显示并且玩家成功的说服了NPC。在失败的方式中没有代码,如果成功的方式(#1)返回为FALSE,那么玩家的说服失败了。
编写你自己的函数
编写你自己的技能检定的时候,你只需要按照你在玩纸上游戏时做技能检定的逻辑编写即可。你需要玩家的技能级别、属性修正并且需要玩家掷骰,当然还需要由DM提供的DC值。
可能你会用到以下函数:
int GetSkillRank(int nSkill, object oTarget=OBJECT_SELF) 返回目标(oTarget)的技能级别+属性修正
int GetLevelByClass(int nClassType, object oCreature = OBJECT_SELF) 返回生物的详细级别资料(例如CLASS_TYPE_FIGHTER)
int GetHitDice(object oCreature) 这在你想获得玩家级别而不管他们的职业是什么的时候很有用。
int GetAbilityModifier(int nAbility, object oCreature=OBJECT_SELF) 只接受指定的属性修正。(比如说你只想获得力量的属性修正)
这里有一段自制的技能检定代码,是在对话中使用治疗技能:
int StartingConditional() { int iDC = 20; // or whatever the DM wishes to set it to int iBonus = GetSkillRank(SKILL_HEAL, GetPCSpeaker()); if ((d20() + iBonus) >= iDC) { return TRUE; } return FALSE; }
当然这只是个例子,事实上通常有一个内置的使用治疗技能的办法(使用ActionUseSkill语句)……这类代码是用来满足大家自定义技能检定需要的,我很肯定不少人会这样做。
第十部分 制作特殊物品
我怎样才能制做一件特殊物品?例如我怎么制作一个对环境起反应的物品?我怎么制作一把Wand of Wonder或者会说话的剑?
好的,你需要认识到的第一件事是,不像游戏中的其他东西或者生物,物品本身是没有代码的。你不可能指定物品去使用一件它身上的物品并且运行代码,那是不可能的,物品不能使用任何物品。
你要为物品编写代码的唯一方法就是上升到模块的层面,在module的道具中,你有三个事件可以对物品产生影响。当这些事件发生时,它提示module来运行与物品相关的代码。其中的两个事件是OnAcquireItem和OnUnAcquireItem。它们是指物品被捡起来和丢下时发生的事件。一个事件触发一段道具代码,如果没有代码,什么事都不会发生。
假设当一把特殊的长剑被捡起来的时候,我设定了一个变量(标签[tag]为SWORD01),我可以将这下面的代码放到module的OnAcquireItem段中。 代码:
void main () { object oItem = GetObjectByTag("SWORD01"); int iVar = GetLocalInt(GetItemPossessor(oItem), "VARIABLENAME"); // below is 'if the firer of the event is the one I've specified // and the variable hasn't been set, set the variable on the // item's possessor' if ((GetModuleItemAcquired() == oItem) && (iVar == 0)) { SetLocalInt(GetItemPossessor(oItem), "VARIABLENAME", 1); } }
对你制作的特殊物品来说,这并不复杂。但事实上你还需要更多的module事件来满足更多的需要,这就是第三个module事件:OnActivateItem()
激活一件物品需要一个特定的“激活”事件。当你制作一件物品的时候,你可以给予它一个魔法属性……你有两种选择:Unique Power和Unique Power: Self Only。两者的不同在于后者只是简单的触发一个'OnActivateItem'事件。前者你则需要首先选定一个目标。
像物品的其他属性一样,'Unique Power'在物品的辐射状菜单中,如果你想对使用者说明这些特殊属性是什么,你可以把它们写在物品描述中。(在那里你也可以选择“鉴定的”或者“未被鉴定的”)
当在OnActivateItem()中编写代码时,有几个语句需要记住:
object GetItemActivated() 这个函数将会返回最后一个触发OnActivateItem()的物品。
object GetItemActivatedTarget() 语句返回一个Unique Power的目标。(如果当时目标指向一个特定对象的话)
location GetItemActivatedTargetLocation() 语句返回一个Unique Power的目标。(如果当时目标指向一个地点(而不是一个特定对象))
object GetItemActivator() 语句返回任何一个激活物品的生物对象。
这就是全部你需要的了。用这些语句你想编写多少被激发物品就可以编写多少。当然要记住,这些物品的代码都包含在module中。如果你把这些物品拿到其他module中去,并且其他的module在OnActivateItem()中没有相关的代码,这些物品是没有用的。
注:现在OnActivateItem中有一个bug,对于一次性使用物品(也包括能够使用最后一次的物品),当物品被使用后,它消失了,但事件仍在发生,这时所有的语句像GetItemActivated都会返回非法对象。(因为这个物品不存在了)
第十一部分 做在椅子里和睡觉
1. 如何使玩家做在椅子里?
目前为止还没有做在椅子里的动作。除非你为玩家添加这项功能。使玩家坐在椅子里的代码必须放在对象本身上。
先准备好一个椅子,这个椅子必须是一个可用的(USEABLE)可放置物体(PLACEABLE OBJECT)。并且确定玩家有足够的空间来完成坐在椅子里的动作。然后将下面的代码放在OnUsed事件中去。
void main() { object oChair = OBJECT_SELF; if(!GetIsObjectValid(GetSittingCreature(oChair))) { AssignCommand(GetLastUsedBy(), ActionSit(oChair)); } }
2.当我把一个NPC放到TOOLSET中去的时候,他只是站在那里,有办法可以使他坐下吗?
在TOOLSET中不行,生物必须是站着的。但生物身上可以放一段代码,这段代码让他在游戏开始的以后找到一个椅子并且执行ActionSit语句。
创建一个可放置物体并且给它一个标签(TAG)"CHAIR"。把NPC放在椅子边上,复制一份OnSpawn代码并且以另一个文件名存档,然后在这段代码底部添加一行:
ActionSit (GetNearestObjectByTag ("CHAIR", OBJECT_SELF));
记住当玩家和NPC对话的时候,NPC将会站起来。要让他们坐下,你需要到他们对话的'Other Files'标签中去,你可以看到那里有放置两段代码的地方,他们是用来在对话结束或者放弃的时候运行的。写一段代码放在这些代码的顶端,这样对话一结束他们就会坐回到椅子中去。
3.上面的办法听起来都很不错,但是我怎样才能在TILESET中使NPC一开始就坐在椅子里?
一些玩家告诉我们可以用下面的方法完成,这个方法的操作和上面的办法很相似。不同的是你要寻找一个不可见的可放置物体('invisible object' placeable object )放到一个椅子上。给那个物体一个标签"CHAIR"并且使用相同的ActionSit语句,他们将会坐在椅子中……
4.我如何使我的NPC睡在床上?
目前没有办法真正的使一个生物睡在床上(可放置物体或者TILESET)。一个可放置物体可以放在床上,但不能是一个生物。尽管如此,我们还是可以让一个生物睡在铺盖卷里或者地板上。如果你想让你的NPC在游戏中开始睡觉,你需要复制并以其他名字存档生物的OnSpawn代码,并且在底部添加如下代码:
effect eLieDown = EffectSleep(); effect eSnore = EffectVisualEffect (VFX_IMP_SLEEP); effect eSleep = EffectLinkEffects (eLieDown, eSnore); ApplyEffectToObject (DURATION_TYPE_PERMANENT, eSleep, OBJECT_SELF);
叫醒你的NPC
如果你想让NPC在发生一些事的时候醒来,你必须使用RemoveEffect函数,一旦一个效果起了作用,不可以简单的被定义为另一个变量例如EffectSleep,所以也就不可以被简单的使用RemoveEffec函数删除。RemoveEffec语句的目标必须精确对应所要删除的效果。如果在同一段代码中睡眠效果被作为同一个变量被接受,你可以简单的使用:
RemoveEffect (OBJECT_SELF, eSleep);
但是通常来说,你必须检查对象的全部效果并且按照效果类型找到你所需要的效果并且删除它。下面是一段简单的删除睡眠效果的代码(这段放在OnHeartbeat中的代码会在任何敌人接近到5米之内的时候唤醒NPC):
// wake-up script #include "NW_I0_GENERIC" void main() { // if I am asleep if (GetHasEffect(EFFECT_TYPE_SLEEP)) { // get the nearest enemy creature to me object oTarget = GetNearestCreature(CREATURE_TYPE_REPUTATION, REPUTATION_TYPE_ENEMY); // and if there is one and it is less than 5 meters away if ((GetDistanceToObject(oTarget) < 5.0) && (GetIsObjectValid(oTarget))) { effect eSleep = GetFirstEffect(OBJECT_SELF); // scroll through my current effects while (GetIsEffectValid(eSleep)) { // and if one of them if the effect sleep but but didn't come from a sleep spell if ((GetEffectType(eSleep) == EFFECT_TYPE_SLEEP) && (GetEffectSpellId(eSleep) != SPELL_SLEEP)) { // remove it RemoveEffect(OBJECT_SELF, eSleep); } eSleep = GetNextEffect(OBJECT_SELF); } } } }
第十二部分 如何使NPC主动交谈
好的,首先你要记住:除非你很明确的知道自己在干什么,否则不要改变OnPerception("nw_c2_default2")事件中的代码,别去管它。如果有任何人告诉你改动除了OnSpawn和OnUserDefined之外的任何东西,要是你不确定,那么就不要改动。
就像早先所提到的,用户自定义(UserDefined)事件和一般人工智能系统允许你在不改变默认代码的情况下使用以下事件。
1.使用OnPerceive的方法开始对话。
a.进入你的OnSpawn代码,像以前那样拷贝一份其他名字的副本,然后再副本中改动。在里面找到'SetSpawnInCondition (NW_FLAG_PERCIEVE_EVENT);'这一行,并且激活它(删除该行前面的双斜线"//")编译成一段新代码。
b.在你的NPC对话文件中,到他与玩家对话的第一行去。如果你只想让他与任何一个玩家开始对话,那么在'Actions Taken'中添加以下代码:
void main() { SetLocalInt(GetPCSpeaker(), "Dlg_Init_" + GetTag(OBJECT_SELF), TRUE); }
它在你正在使用的代码中设置了一个变量,所以对话不会再进行第二次。
c.现在在OnUserDefined事件中为你的NPC创建一段新的代码。你可以像下面一样将第二段代码放在这里:
void main() { int nEvent = GetUserDefinedEventNumber(); if (nEvent == 1002) // OnPerceive event { object oPC = GetLastPerceived(); if(GetIsPC(oPC) && GetLocalInt(oPC, "Dlg_Init_" + GetTag(OBJECT_SELF)) == FALSE && !IsInConversation(OBJECT_SELF)) { ClearAllActions(); AssignCommand(oPC, ClearAllActions()); ActionMoveToObject(oPC); ActionStartConversation(oPC); } } }
可能会产生的缺陷:它有可能不管用……为什么?如果游戏开始的时候NPC见过这个玩家,那么就没有OnPerceive事件,这个事件仅仅会在NPC第一次见到玩家的时候才会被触发。
2.使用Trigger触发方法开始对话。
这是我们在官方战役中使用的办法。它允许你控制NPC在什么时候跑向玩家,并且可以决定在NPC放弃对话之前可以追踪玩家多远。
你可以像下面这样做:
a.像上面第二步所做的那样,到你的npc对话的文件中去,如果你想让他与任何玩家对话,那么在'Actions Taken'中添加:
void main() { SetLocalInt(GetPCSpeaker(), "Dlg_Init_" + GetTag(OBJECT_SELF), TRUE); }
b.到你的toolset中去,放置一个路点,使你的NPC在结束对话后到他应该到的地方。将路点的标签命名为"WP_RETURN_"+NPC的标签名(所以如果NPC的标签[TAG]为FRED,就将路点标签命名为"WP_RETURN_Fred"。记住所有的标签都是大小写敏感的!!)
c.在toolset中创建一个新的一般触发事件,并且在你的NPC周围画一个多边形。当玩家进入这个区域的时候,NPC将会跑向玩家去交谈。如果NPC离开了这个区域,他会停止追赶玩家并且回到路点上去。
d.到触发事件代码标签(trigger's 'Scripts' tab)中并且在'OnEnter'添加以下代码:
// this is the on enter script if a trigger that // encompasses the NPC who will be initiating dialouge. // Make sure to replace "NPC_TALKER" with the actual tag of the NPC void main() { object oNPC = GetObjectByTag("NPC_TALKER"); object oPC = GetEnteringObject(); if(GetIsPC(oPC) && GetLocalInt(oPC,"Dlg_Init_" + GetTag(oNPC)) == FALSE && !IsInConversation(oNPC)) { AssignCommand(oPC,ClearAllActions()); AssignCommand(oNPC,ClearAllActions()); AssignCommand(oNPC,ActionMoveToObject(oPC)); AssignCommand(oNPC,ActionStartConversation(oPC)); } }
f.你要防止NPC追赶玩家,将以下代码输入到连锁事件的OnExit事件中去:
// This will return the NPC to a starting position // if he attempts to leave the trigger. // You must replace "NPC_TALKER" with the tag of the NPC. // You must also have a waypoint with the tag "WP_RETURN_" + NPC's Tag. // This should be placed in the spot the NPC starts at. void main() { string sTag = "NPC_TALKER"; object oExit = GetExitingObject(); if(GetTag(oExit) == sTag) { AssignCommand(oExit,ClearAllActions()); AssignCommand(oExit,ActionMoveToObject(GetNearestObjectByTag("WP_RETURN_" + sTag))); } }
这个方法不管玩家隐身与否,穿过区域就会触发NPC的事件,如果你想让NPC看到玩家才触发事件,在OnEnter代码中的'if(GetIsPC(oPC) &&'下面添加:
GetObjectSeen(oPC, oNPC) &&
同样的,在多人游戏中,NPC会用这种办法与每一个玩家都对话,如果你只想让他与其中一个对话,那么如下:
a.在对话代码中,用'OBJECT_SELF'替换'GetPCSpeaker()'。
b.在OnUserDefined代码中(第一种方法中)或者OnEnter代码中(第二种方法),将GetLocalInt语句中的'oPC'改为'oNPC'。
第13部分 如何使NPC攻击正在与他交谈的玩家
如果你想写一段代码把对话中的NPC变为敌对状态,那么将代码放置在对话编辑器的'ActionsTaken'标签底部,保证当你想开始战斗的时候,你有可供选择的节点(NODE)。
如果你想让一个NPC小队或者一群NPC变成敌人,使用一般代码"nw_d1_attonend"
代码:
//:://///////////////////////////////////////////// //:: Attack on End of Conversation //:: NW_D1_AttOnEnd //:: Copyright (c) 2001 Bioware Corp. //::////////////////////////////////////////////// /* This script makes an NPC attack the person they are currently talking with. */ //::////////////////////////////////////////////// //:: Created By: Preston Watamaniuk //:: Created On: Nov 7, 2001 //::////////////////////////////////////////////// #include "NW_I0_GENERIC" void main() { AdjustReputation(GetPCSpeaker(), OBJECT_SELF, -100); DetermineCombatRound(); }
如果你想让单独的NPC变为敌对状态,使用一般代码"nw_d1_attonend02":
//:://///////////////////////////////////////////// //:: Attack on End of Conversation //:: NW_D1_AttOnEnd02 //:: Copyright (c) 2001 Bioware Corp. //::////////////////////////////////////////////// /* This script makes an NPC attack the person they are currently talking with. This will only make the single character hostile not their entire faction. */ //::////////////////////////////////////////////// //:: Created By: Preston Watamaniuk //:: Created On: Nov 7, 2001 //::////////////////////////////////////////////// #include "NW_I0_GENERIC" void main() { SetIsTemporaryEnemy(GetPCSpeaker()); DetermineCombatRound(); }
要小心一件事:如果你在NPC对话的节点(NODE)上使用这些代码,NPC会在你选择这个节点(NODE)的同时发动攻击,如果这是段很长的话,玩家将没有时间把它读完,
如果你对这不介意,那么还好。如果你不喜欢这样,在NPC对话的最后一个节点上使用Add按钮,添加一个玩家的回应。去除掉玩家回应的文字并且按Enter,它将会变成一个“结束对话”的节点。将代码添加到“结束对话”的'Actions Taken'区域中去。这样,玩家将会有时间读完对话,当他按下回车键的时候,NPC开始攻击。
常见问题:确定你要使用的用来发动攻击的NPC不包含Commoner(平民)等级,因为Commoner的AI被设计为从敌人前面逃跑而不是战斗。另外确定当你在OnSpawn代码中使用'Special Behaviors'的时候,没有使用其中的'Herbivore'行为。因为'Herbivore'行为也是被设计为从战斗中逃跑。
第十四部分 如何设置一个商店
设置一个基本的商店很容易,你所有要做的就是用Store Wizard做出一个商店并且在其中设置基本商店对象:你有了一个商店——它包括一件物品,物价上升和下降的百分比,一个名字和一个标签(tag)。然后你该做什么?
第一步:将商店这个对象放到你的module中去。一个商店是一个不可放置的蓝图(blueprint)他不可以被module访问。你事实上要把它放到某个地方。当你在放置它的时候,你会注意到它看起来像路点一样。玩家不会看到这个商店中的物品并且不会以任何方式和它们发生互动。这些商店中的物品只是存在,当然最好靠近能访问它们的对象。(见下文)
第二步:创建你的访问对象。通常这是一个商人NPC。但事实上并不一定必须是商人。事实上,任何可以运行打开商店物品代码的对象都可以。当然使用一个商人NPC还是最简单的方法…… 简单的创建一个商人并且为他创建对话。最简单的对话例如是:你好!来看看我的货物怎么样?然后为玩家创建两个回应:是或不是。
第三步:放置可以打开商店物品的代码。在上面的对话中,你要将代码放置到玩家“是”的回答中的'Actions Taken'段中。 所以当玩家选择“是”的时候,商店物品就会被打开。“不是”就不用管了,他只会简单的结束对话。
在对话中打开商店的基本代码如下:
void main() { object oStore = GetObjectByTag("STORE_TAG_HERE"); OpenStore(oStore, GetPCSpeaker()); }
完成了……当然你可以用你自己喜欢的任何标签名字来替换代码中的"STORE_TAG_HERE"。
如果你看一下OpenStore的结构,你会发现你可以选择性的以百分比的方式改变物价系统。这是为了能让你设计的商店可以因为情况的不同做出不同的反应。下面是一段代码,其作用是:当精灵来买东西时,所有的商品价格上升一倍(我猜店主可能不喜欢他们)。
void main() { object oStore = GetObjectByTag("STORE_TAG_HERE"); if (GetRacialType(GetPCSpeaker()) == RACIAL_TYPE_ELF) { OpenStore(oStore, GetPCSpeaker(), 100); } else OpenStore(oStore, GetPCSpeaker()); }
创建商店最后要注意的几点:1.目前我们提供的标准商店的外观比较有限,将来也许会有改观。2.要知道你不能对商店中每一项单独类型的物品的物价进行微调。店主只会将所有的物品的价格按照统一的百分比提升或者下降。3.你也不能指定商人只能收购/拒绝收购某些种类的物品。要么商人对所有的物品都收购,要么都不收购。
还有一件很重要的事:所有以对象为目标的语句都可以在商店这个对象上运行。记住:是商店对象拥有物品而不是商人NPC拥有。所以在商店对象上运行诸如CreateItemOnObject、GetFirstItemInInventory或者GetNextItemInInventory都是有效的。
第十五部分 使用MODULE事件
当事件被触发的时候,不论它是被module,一个可放置物品还是其他什么触发的,最重要的都是通过代码来确定是什么触发了事件。记住这和用户自定义事件(UserDefinedEvent)是不同的。这是每一个单独的对象中的核心的事件。你为事件分配一段代码,当事件被触发的时候,代码就运行。
module事件列表:
OnAcquireItem:触发:当物品被添加到某个人物物品栏中的时候。如何触发:使用GetModuleItemAcquired()。也在这里使用:GetModuleItemAcquiredFrom()返回(一个对象表示)物品的来源地。GetItemPossessor(object oItem)返回(一个对象表示)物品的拥有者。
OnActivateItem:触发:当附有魔法的物品被使用的时候。连锁反应:使用GetItemActivated()。也在这里使用:GetItemActivatedTarget()如果目标是一个物体则语句来判断哪个物体是目标。GetItemActivatedTargetLocation()如果目标是一个点则语句来判断目标点的位置。GetItemActivator()判断该魔法物品的拥有者。
OnClientEnter:触发:当玩家进入游戏的时候。连锁反应:使用GetEnteringObject()返回一个玩家对象。
OnClientLeave:触发:当玩家离开游戏的时候。连锁反应:使用GetExitingObject()返回一个玩家对象。
OnModuleLoad:触发:当module第一次被启动的时候。连锁反应:module本身。这时候没有明确的对象被载入。从效果上来说,这是一个OnSpawn代码的版本号。只在module载入的时候运行一次。
OnPlayerDeath:触发:一个玩家角色死亡。连锁反应:使用GetLastPlayerDied()来返回一个死亡玩家的对象。默认使用:"nw_o0_death"代码通常被用在这里,来重新搞清小队队员之间的关系并且判断弹出'death GUI panel'让玩家选择离开游戏或者复活。
OnPlayerDying:触发:玩家的生命值降为0的时候。连锁反应:使用GetLastPlayerDying()。与上面不同的是玩家的生命值降为0还不是正式的死亡。默认使用:放在这里的标准代码是"nw_o0_dying",接受玩家的EffectDeath并且正式的杀死玩家,然后触发OnPlayerDeath事件。如果这个代码被删除,玩家将会流血,当生命值达到-10的时候才会正式死亡。
OnPlayerLevelUp:触发:玩家升级的时候。连锁反应:使用GetPCLevellingUp()。语句返回那个升级的玩家……升级无法被打断,它只是一瞬间的事。升级并不是一个像休息那样的动作。
OnPlayerRespawn:触发:当玩家在他的Death GUI panel中选择复活选项的时候。连锁反应:使用GetLastRespawnButtonPresser()。默认使用:代码"nw_o0_respawn",它是用来复活玩家,除去玩家身上的一切不利效果,并且处理玩家的经验值和金钱惩罚。它也判断哪里有一个标签为NW_DEATH_TEMPLE的地方(类似路点)并且将玩家传送过去……如果没有,它将在玩家死亡的地方复活他。
OnPlayerRest:触发:玩家按下休息按钮。连锁反应:使用GetLastPCRested()。也在这里使用:int GetLastRestEventType() ——记住这个事件发生在玩家休息前和休息后,你可以通过一些常量来检查出GetLastRestEventType()的使用情况。REST_EVENTTYPE_REST_CANCELLED(休息被打断), REST_EVENTTYPE_REST_FINISHED(休息结束),REST_EVENTTYPE_REST_INVALID(不准休息), REST_EVENTTYPE_REST_STARTED (休息开始)。如果你想控制玩家什么时候可以休息,你可以检查休息是否开始,如果当时的时间不合适,使用AssignCommand告诉玩家ClearAllActions,这会让他无法休息。
OnUnAcquireItem:触发:一件物品被扔掉或者删除。连锁反应:使用GetModuleItemLost()来返回这个物品。也在这里使用:GetModuleItemLostBy()返回一个曾经拥有这个物品的对象。
OnHeartbeat:触发:在module被载入后,每6秒钟module的心跳就会被触发。这个事件不会被触发,但所有放在这里的代码都会每6秒被运行一次。所以,小心不要把太多的代码放在这里,它们会耗费很多系统资源。
OnUserDefined:触发:一段以module(可能是通过GetModule)为目标的使用SignalEvent语句的代码。发送的事件必须被设定为UserDefined事件。(例子:SignalEvent (GetModule(), EventUserDefined(100)))。在放在这里的代码中,你可以使用GetUserDefinedEventNumber()来观察发出的数字……当它发生时,就会启动你的代码。
待续……
|