【mod教程】Papyrus入门框架与手册使用



楼主 射——剑  发布于 2018-07-18 20:26:00 +0800 CST  
papyrus是Bethesda Studio自创脚本语言,用以为上古卷轴/辐射系列提供原始框架无法提供的逻辑关系。了解papyrus是制作任务类mod,绝大多数功能类mod等的必要条件。

楼主 射——剑  发布于 2018-07-18 20:26:00 +0800 CST  
一、基本概念:
Class:类型,包括数据和方法。比如“角色(Actor)”就是一个类型,它的可用方法包含在Actor Script文件里(Skyrim\Data\scripts\Source\Actor Script.psc)。
Object:对象 ,属于某种类型的一个具体物品。比如:weapon是类型,”铁剑“就是对象。一个对象的数据部分一般记录在esm和esp里,可以用CK定义新对象或修改旧的。定义所用的表格被称为“基本表(base form)”。每个基本表都有一个form id。
Objectreference:对象实例,特指对象被实例化,到场景中的状态。
在游戏中,场景里看到的一切都属于对象实例。同一个对象可以有多个实例化,每个实例化的物体有自己独有的参考码(reference id)。比如你可以在游戏里刷一百把铁剑,他们都对应同一个对象,但是有着不同的参考码。
*值得一提的是,容器里的物品(包括角色身上的,老滚的规则是任何角色都自带一个不显示的容器)是用base form定义的,也就是说,如果容器中有“铁剑 (数量)10”这样的表述,此时并非实例化了所有的十个铁剑,相反,它没有实例化任何东西,数据上实现是 form id + 数量这种模式。
**存档中产生的新物体(比如附魔做出来的东西,重命名等等)是另一个问题,按下不表。

楼主 射——剑  发布于 2018-07-18 20:27:00 +0800 CST  
能挂载脚本的类型,都对应一系列可用的属性,函数等,所有这些集合被称为 “类型名字+Script”。比如说Actor(角色)类型,对应的就是Actor Script;武器类型,对应名字就是“Weapon Script”其它同理。

楼主 射——剑  发布于 2018-07-18 20:28:00 +0800 CST  
二、类及其继承


1.了解各类间继承关系


类型间有继承关系,这种关系可以从类型同名脚本文件第一看出来。


比如说,Actor 类型所对应的同名脚本文件Actor Script(Skyrim\Data\scripts\Source\Actor Script.psc)第一行是:
Scriptname Actor extends ObjectReference Hidden
这表明Actor是继承于ObjectReference类型。
*Hidden是给CK看的,第一行加了Hidden标签后,在CK里选脚本文件的时候就不会出现在列表里。
而ObjectReference Script的第一行是:
Scriptname ObjectReference extends Form Hidden
这表明ObjectReference继承于Form类型。
而Form Script的第一行是:
Scriptname Form Hidden
即Form不继承于任何其它类型。


2.继承的意义继承有什么作用?


在面向对象编程的概念里,一个类继承于其父类,就获取了父类的所有属性和方法。


在这里就表现为:子类可以使用它所继承类的所有变量和功能(函数)。


举个例子:
在Form Script里,有个函数叫GetFormID(),作用是返回formID;
如上所述,由于ObjectReference类型继承于Form类型,因而继承了GetFormID()这个函数。同理,由于Actor继承ObjectReference类型,间接继承了GetFormID()这一函数。所以MyActor.GetFormID()这种用法是完全没有问题的。
*上文中,MyActor是Actor类型的一个对象

楼主 射——剑  发布于 2018-07-18 20:30:00 +0800 CST  
三、脚本挂在哪?


了解了上面的情况之后,这个问题就很好回答了。


用几何学来类比:平行四边形是四边形的一种特殊情况,因而拥有四边形的一切特征(比如有四个边,四个角);在此基础上,还具有一些独有的性质,比如对边平行。
而矩形作为一种特殊的平行四边形,也具备四边形的一切特征。也有自己独有的特点。


在对象上挂脚本这个行为,本质上类似于“在平行四边形的基础上建立矩形的概念”——即在原有的类型基础上再创造出一个特例:


因此,挂在Quest上的脚本,第一行应该是:
Scriptname MyScript extends Quest
同理,挂载Actor上的脚本,第一行应该是:
Scriptname MyScript extends Actor
等等
*MyScript是我们脚本的名字。


同样,挂载在某个类型的脚本,能获得其继承链上类型的一切属性和函数。这些就是我们能在脚本中使用的材料。

楼主 射——剑  发布于 2018-07-18 20:30:00 +0800 CST  
还有一节过两天更,因为想配合一个例子。

楼主 射——剑  发布于 2018-07-18 20:33:00 +0800 CST  
四、事件机制(EVENT)
通过添加脚本,modder能够往脚本所挂载对象中添加新的变量和函数。但写脚本的最终目的是为了让代码在恰当地时候执行——这就要用到Event机制。


1.什么是事件机制
Event是一类特殊的原版自带的函数,其参数由脚本挂载的对象和执行时的具体情况来决定(所以为了方便写作,在写事件地时候参数是固定死地,直接从手册上复制下来即可)。同样地,事件也是各个原版类里的成员,同样存在继承关系。系统预设定了事件函数在什么情况下执行。
在papyrus中,不妨把事件看作是一种语境,“事件名表达:在某时;事件内容表达:做某事”。


举个例子:
OnActivate是ObjectReference里的一个事件。意为“在挂载对象被激活时”。它只有一个ObjectReference型参数akActionRef,代表激活它的对象。


下面的代码功能是:当脚本被挂载的对象被激活时,检测激活它的对象是否是玩家:若是,则刷一行“Been actived by player ”的info格式的log。
Event OnActivate(ObjectReference akActionRef)
if akActionRef== game.getPlayer()
Debug.Trace("Been actived by player",0)
endif
EndEvent


*Debug.Trace("字符串",0/1/2)的作用是输出一行log,常用于脚本调试,输出内容是第一个参数里的字符串,后面的数字表示类型:0表示info,1表示warning,2表示error。


*一个脚本中,同一个事件一般只有一个;例外的话就涉及脚本的多状功能,在此按下不表。

楼主 射——剑  发布于 2018-07-23 17:16:00 +0800 CST  
2.事件寄存


有时候我们无法把脚本挂载在想要地方。
举个例子,如果想用如下的逻辑实现剑气:“玩家进行’重击‘动作的时候,释放一个作为剑气的魔法。”
看起来似乎很容易:只要在“玩家”这个Actor型对象下,找到某个事件描述 "当做XX动作时",然后再在这个事件中添加“释放魔法”的操作即可。
实际操作就会发现这行不通:Actor确实可以添加脚本,确实也有个OnAnimationEvent的事件来表示“当作某某动作时”,但是玩家控制角色作为一个Actor类型对象时,是无法挂载脚本的。因为它需要在存档中被再次编辑,后面的编辑会将前面的覆盖掉。所以只能把脚本放在别的地方——比如一个使perk中,因为不会被存档再次编辑。


但那样就会产生相反一个问题:Perk,Magic Effect,Quest等并不是一个角色类型,因此在这里挂载脚本中包含了OnAnimationEvent,由于事件的参数是固定的无法特指,所以系统无从知晓到底是哪个对象,发生了什么动作,因此这个功能肯定无法完成。


如何才能将事件与它的“主语”和特定的动作联系起来?


老滚提供的解决方法是,事件寄存机制——


事件寄存机制主要通过事件对应的寄存函数来实现:
事件寄存函数是一系列以布尔型返回值的(即返回0或者1,表示操作成功或失败),名称为“RegisterFor+对应事件名”的函数;
事件寄存函数的意义是,通过自己的参数,将要侦听的对象和触发情况(有时还有额外信息)传递给对应事件。

楼主 射——剑  发布于 2018-07-23 17:17:00 +0800 CST  
在上面“剑气”的例子中,首先“当玩家进行‘重击’动作的时候”,描述这个事件有两个参数:“玩家”和“重击动作”。于是在执行事件之前,寄存函数部分应该这样写:


Event OnInit()
RegisterForAnimationEvent(Game.getplayer(), "重击动作")
endEvent
*OnInit事件无参数,含义是在脚本开始执行后立刻被启动的事件。写在OnInit事件内能保证动作事件注册函数先于对应事件发生。
这样处理之后,就可以在下面放心地使用动作事件寄存函数所对应的事件OnAnimationEvent了:


Event OnAnimationEvent(ObjectReference akSource, string asEventName)
if(akSource==Game.GetPlayer())&&(asEventName=="重击动作")
#发射魔法的操作#
endIf
endEvent
*一般来说,事件注册函数所对应的时间,需要在一开始用if来确认一下传递的参数无误。这是为了避免寄存多个同一事件导致混乱。


有寄存函数(Register)自然就要有释放寄存函数(Unregister),不然寄存信息会一直保留在内存中,造成不可预料的后果。这两个函数需要在同一个脚本中。

楼主 射——剑  发布于 2018-07-23 17:17:00 +0800 CST  
五、属性


属性是一种特殊的变量,可以是老滚自己定义的类型,比如weapon,actor,keyword等,也可以是int ,float这样的原子类型。
作为一种变量,属性的特殊之处体现在psc被编译成pex并挂载之后,可以在直接ck中给脚本里的属性赋值。这样就大大提高了脚本的复用性,可以在不看源码的情况下了解到脚本使用了哪些参数 。


定义属性的部分一般位于脚本文件的最上端。在定义了脚本名和所依附的类型之后。定义格式为:
类型 property 变量名 标签


标签有两个:
hidden,添加的话就无法在CK中直接看到和编辑该属性,这是为了大型脚本的时候隐藏一些属性来做的,一般不用。
auto,如果不添加这个就要手动构造属性,我们不用那么复杂,所以一般都加上。


举例:
Spell property MySpell auto
这表示定义了一个Spell类型的属性,名称是MySpell。你可以在CK中指定MySpell到底对应哪个Spell对象。auto表示不用再脚本中写构造部分。


有时候有些约定俗称的属性名字可以在CK中的脚本编辑菜单自动填充值,比如Actor playerREF在ck中点auto-filled之后就会让它自动赋值为玩家。即相当于Game.Getplayer()的返回值。

楼主 射——剑  发布于 2018-07-23 17:25:00 +0800 CST  
六、实践
说了那么多,却连基本语法都没有讲。这是因为作为一种使用场景单一的脚本语言,写papyrus的时候一般会严重依赖“字典”(papyrus维基),当需要完成某个功能的时候,直接把标准范例复制过来改一下就可以作为自己的模块了,而且基本上也不会有必须在papyrus层面上实现复杂算法的情况。
下面就把整个例子实现出来,在此过程中将涉及papyrus的主要语法。


1.准备工作:
创作一个涉及脚本mod的时候,第一步应当尽量详细地将需求写出来,比如“设计一种武器,当玩家发动重击动作地时候,释放一个法术”,而不是“实现剑气”,这种笼统说法。
确定需求之后,据此拟定方案。




拟定方案的时分以下几个层级考虑:
1.需要在哪(几)个对象上挂载脚本;
2.脚本有什么事件;
3.事件内各要完成哪些功能。

楼主 射——剑  发布于 2018-07-23 17:27:00 +0800 CST  
直觉来说,所有脚本都应该放在武器上:当武器被拿起的时候,注册动作事件,然后攻击动作发生之后,执行释放魔法的操作。
但实际操作会发现,武器上不能执行registerforanimation()函数——这是很奇怪的,因为这是一个Form类下成员函数;weapon里的脚本继承于objectreference,然后objectreference又继承于Form。不过papyrus就是这样充满了奇奇怪怪的限制。
有什么办法能折中一下呢?有,我们可以参考原版银质武器对不死特别效果的实现方法:拿出武器的时候给一个Perk,卸下武器的时候移除该Perk。动作事件注册和具体魔法释放逻辑放在Perk的脚本中。


现在框架就清楚了,我们的代码需要在两个地方:
1.武器:
a-装备该武器时给角色一个Perk
b-解下的时候移除这个Perk。其中Perk作为属性之一
Scriptname 武器里的脚本名 extends ObjectReference


Perk Property 对应perk auto


Event OnEquipped(Actor akActor)
akActor.AddPerk(对应perk)
EndEvent


Event OnUnEquipped(Actor akActor)
akActor.RemovePerk(对应perk)
EndEvent


2.Perk:
a-首先注册(寄存)动作事件
b-当做出指定动作的时候,释放法术
c-当玩家卸下装备的时候,释放寄存


Scriptname 对应perk里的脚本名 extends Perk


Actor Property playerRef auto
Spell Property MySpell auto;要释放的法术


Event OnInit()
RegisterForAnimationEvent(playerRef, "AttackPowerStanding_FXstart")
endEvent


Event OnAnimationEvent(ObjectReference akSource, string asEventName)
if (akSource == playerRef) && (asEventName == "AttackPowerStanding_FXstart")
MySpell.cast(playerRef)
endIf
endEvent


Event On

楼主 射——剑  发布于 2018-08-01 18:47:00 +0800 CST  
至此mod的脚本部分就已经完成了,但是事情并没有结束,还需要在脚本上添加需要的属性。其中某些属性,比如playerRef,可以直接用Auto-Filled,CK会自动判断出所指是哪个,非常方便。

楼主 射——剑  发布于 2018-08-01 18:49:00 +0800 CST  
七、如何查手册:
上面的例子中用到了很多前文没有讲到的函数和功能,比如用来释放法术的Spell.cast()。可以前往CK维基来查询这些内容(未墙,但是可能要验证码):
https://http://www.creationkit.com/index.php?title=Category:Papyrus
刚点进来的时候可能感觉内容浩如烟海,不过其实有所技巧:
1.开头:
比如说is开头的都是返回值为0-1类型的函数;On开头地都是事件;Get开头的都是获取实例的某个值;
2.有的页面本身就是当汇总用的:
比如Spell Script就提供了所有关于法术的脚本。
3.在游戏中找类似功能,然后用CK看原版/MOD是如何实现的,在此基础上改出自己的mod。

楼主 射——剑  发布于 2018-08-01 18:57:00 +0800 CST  
3DM上有个HDT教程讲的很好:
http://bbs.3dmgame.com/thread-4671265-1-1.html

楼主 射——剑  发布于 2018-08-17 23:36:00 +0800 CST  

楼主:射——剑

字数:6205

发表时间:2018-07-19 04:26:00 +0800 CST

更新时间:2019-03-08 03:56:01 +0800 CST

评论数:129条评论

帖子来源:百度贴吧  访问原帖

 

热门帖子

随机列表

大家在看