icedream 210 Report post Posted February 16, 2015 本文转自《破衣框架BUFramework_v1.01解析》,原作者为newalbert。字符串,数组,进而是用户自定义函数,obse团队为脚本打造了一个完整的支持平台,按理说脚本类MOD现在才真正到了发力的时候。但现实往往就是这么让人嗟叹,当我们把所有的工具都准备好时,回首一望,却人去楼空,只留下前辈们夺目的艺术成果,和一个个离去的脚印。。。基本没有更新的wiki,豪华璀璨但后继乏力的T网,论坛中已经神隐或即将神隐的高人。。。 这些残酷的现状都在告诉俺们,上古卷轴4的路已经快走完了,辉煌的顶点已经离我们远去,一切都将成为过眼烟云。。。俺真的很扼腕,如此晚才接触这样的神作,错过了如此多的高人,错过了如此开放共享的团队。。。不过俺仍然一直在努力,既是为下一次机会未雨绸缪,也是为有兴趣加入游戏modder行列的新人们降低一点点门槛。。。俺知道能看完这样一大篇纯文字帖子的人不多,俺只是对从各位高人手里伸手拿东西的一点点小回报。。。好了,废话说完,进入正题!!!为何拿破衣框架来说事?并不是因为这个带点邪恶性质的mod能吸引眼球(说实话,俺觉得还是有点)。。。而是这个小小的mod使用到了obse新的数据结构string,array和强大的用户自定义函数function,对于脚本进阶的童鞋有重要的参考和学习意义。。。作者将框架代码和使用代码作为txt开放出来,对阅读和分析应该没有做限制,俺使用1.01版为大家做个介绍 首先对作者表示由衷的感谢,BUFramework是作为一个esm存在的,也就是说任何esp都可以很方便的使用其中定义的内容和函数,就像它们本来就存在与老滚中一样。。。 破衣框架使用原文: ; 個々の装備に貼り付けるオブジェクトスクリプト scn BUDWDScript array_var param int ret ref rEquip array_var aEquip array_var aMesP array_var aMesA Begin gamemode let ret := Call BUCmnFnc param aEquip aMesP aMesA if (ret == 0) return endif if (ret == 2) let param := aEquip := aMesP := aMesA := ar_Null return endif ; 初期化処理 let aEquip := ar_Construct Array let aMesP := ar_Construct Array let aMesA := ar_Construct Array ; 個別に変更が必要なのはここから↓ ; 対象装備のEditorIDを指定(EditorIDが数字で始まる場合は""でくくる) set rEquip to BUDWD ; 各破壊段階でのnifファイルを指定。段階数は任意。 ; 初期状態 let aEquip[ar_Size aEquip] := "BreakUndiesDWDShortDressE1.nif" let aEquip[ar_Size aEquip] := "BreakUndiesDWDShortDressE2.nif" let aEquip[ar_Size aEquip] := "BreakUndiesDWDShortDressE3.nif" let aEquip[ar_Size aEquip] := "BreakUndiesDWDShortDressE4.nif" ; 耐久度0の状態 ; 上の段階数-1個分のメッセージを設定(プレイヤー側) let aMesP[ar_Size aMesP] := "激しい戦闘で服が破けた…!(1回目)" let aMesP[ar_Size aMesP] := "激しい戦闘で服が破けた…!(2回目)" let aMesP[ar_Size aMesP] := "激しい戦闘で服が破けた…!(3回目)" ; メッセージ設定(敵側) let aMesA[ar_Size aMesA] := "激しい戦闘で敵の服が破けた…!(1回目)" let aMesA[ar_Size aMesA] := "激しい戦闘で敵の服が破けた…!(2回目)" let aMesA[ar_Size aMesA] := "激しい戦闘で敵の服が破けた…!(3回目)" ; ↑ここまで let param := ar_List 0, 0, 0, (ar_Size aEquip) - 1, rEquip ; 初期化処理ここまで End 首先来看看怎么使用,使用破衣框架很简单: 1:在你的装备上做一个Object script 2:将上面的原文贴进去,把第一行的scn BUDWDScript换成你自己取的名字 scn XXX 3:在aEquip数组中填入装备各阶段的nif路径 4:将rEquip赋值为自己装备的Editor ID set rEquip to XXX 好了,搞定了,就这么简单,你的装备现在具有了破衣属性,阶段数就是你填入aEquip数组的元素个数,其它一切都不需要你操心,破衣框架会自动帮你完成。 当然,你也可以把破衣时输出的提示换成其它文字,中文英文日文德文任君所好。。。 如果你是个装备modder,只想使用破衣框架来制作破损盔甲,那么按上面这样使用,就一切都ok了。 如果你是个script modder,想研究学习破衣框架的实现细节,并对obse中较新的几种数据结构和强大的用户自定义函数感兴趣,那么请往下看 下文需要一些编程基础知识,虽然老滚本身的脚本很直白,但obse的扩展,不借助一些术语确实不好解释,抱歉。。。一:obse新数据结构1,字符串定义方式: string_var s赋值: 字符串可以显式赋值,如 string_var string1 set string1 to sv_Construct "First string" 或者通过隐式赋值,如破衣框架中的 let s := aEquip[0] set语法是老滚脚本默认支持的赋值语句。 let,eval等语法是obse的扩展语句,当使用字符串,数组等obse扩展数据类型时,最好使用obse扩展赋值语句。说明: 字符串是OBSE v0016新定义的数据类型,在v0017中完善,使用字符串可以很灵活的处理文本和信息。注意:* 释放 字符串使用完后,需要释放掉,否则导致内存泄漏。 当没有参考指向一个字符串的内存时,obse会自行释放,不过作为一个好的习惯,当一个字符串不需要时,最好主动释放: sv_Destruct s * 拷贝和赋值 字符串变量间的直接赋值使用的是引用拷贝,而非内存拷贝,例如 string_var string1 string_var string1 set string1 to sv_Construct "First string" set string2 to string1 string2和string1都会指向数据"First string",改变其中一个同样也会改变另外一个 set string2 to sv_Construct "First string" 这时,虽然string1和string2都是内容"First string",但指向了不同的内存地址,改变一个不会影响另一个tips: 当遇到需要使用字符串作为参数的老滚函数时,可以使用“$”来将一个字符串变量变成一个立即数,框架中同样也用到了2,数组定义方式: array_var a赋值: 数组可以显式赋值,如 array_var a let a := ar_Construct Array let a := ar_Construct Map let a := ar_Construct StringMap 或者通过隐式赋值,某些obse的函数会返回一个数组类型 let a := 某个返回数组的函数说明: 数组是OBSE v0017新定义的数据类型,在v0018中完善,obse的数组是一个包含传统array和关联array的强大数据结构 array_val类型有三种内部实现,Array(普通数组,以正整数为key),Map(以负数或浮点数作为key的map表),StringMap(以string作为key的map表)注意:* 释放 数组使用完后,需要释放掉,否则导致内存泄漏。 当没有参考指向一个数组的内存时,obse会自行释放,同样,好的习惯是当一个数组不需要时,主动释放掉: let a := ar_Null * 拷贝和赋值 跟字符串一样,数组间的直接赋值使用的是引用拷贝,而非内存拷贝二:用户自定义函数感谢obse伟大的努力! 在obse v0018中,实现了长久以来的script modder的一个怨念:自定义函数!!! 熟悉编程的朋友都知道,函数库是代码共享和复用的根本,老滚现在终于有了可复用的自定义函数结构,使用函数的好处俺就不多废话了,直接看语法1,定义 函数脚本必须是Object script,且只能有一个块:Begin Function,参数使用{}确定,参数的类型使用预声明的本地变量来指定scn BUCmnFnc array_var param array_var aEquip array_var aMesP array_var aMesA Begin Function { param aEquip aMesP aMesA } SetFunctionValue 0 return End 这就是一个完整的函数定义2,参数 所有使用的参数必须都预先声明出类型,参数个数最多10个3,返回值 SetFunctionValue语句设置函数的返回值 任何obse支持的类型都可以作为返回值,包括string和array 如果没有指定任何返回值,函数默认返回数字04,tips: 自定义函数支持递归,但不要多于10层 自定义函数因为是Object类的脚本,所以无法使用fquestdelaytime,执行频率取决了外部调用5,调用 使用Call语句来调用自定义函数,例如: let ret := Call BUCmnFnc param aEquip aMesP aMesA ret保存为函数的返回值 Call后的第一个参数BUCmnFnc是函数名,也就是函数脚本第一行的scn BUCmnFnc 函数名后的参数是真正传递给函数的参数,务必与函数定义中{}中的参数个数和类型相符,虽然函数定义中的参数以“,”分隔,但调用的时候使用空格来分开各个参数 破衣框架原文:; Break Undies Common Function ; 各装備には状態保持用の配列と初期化処理、最低限のコードだけを貼り付け ; 大半の処理はここで行う。 scn BUCmnFnc array_var param array_var aEquip array_var aMesP array_var aMesA ref who int nEquip ref rEquip int break int state string_var s Begin Function { param aEquip aMesP aMesA } set who to getcontainer if (who.IsActor == 0 || who.GetDead) ; アクターが所持していないか死んでる場合 if (param == ar_Null) SetFunctionValue 0 return endif if eval(param[2] != 0) ; break? ; 念のため装備のNif指定を初期に戻す。 let rEquip := param[4] let s := aEquip[0] ; 装備し直さないのでその場で表示は変わらない。 SetFemaleBipedPath $s rEquip sv_Destruct s let param[2] := 0 endif SetFunctionValue 2 ; 解放 return endif if (param == ar_Null) SetFunctionValue 1 ; 初期化が必要 return endif SetFunctionValue 0 if eval(param[0] > 0) ; need wait? let param[0] := param[0] - 1 return endif ; 再装備の必要があるか? if eval(param[1] & 1) ; change? let rEquip := param[4] who.equipItemNS rEquip let param[0] := 1 ; wait let param[1] := param[1] & 2147483646 ; flags &= 0x7FFFFFFE return endif if eval((param[1] & 2) == 0) let param[1] := param[1] | 2 ; flags |= 2 let s := aEquip[0] let rEquip := param[4] SetFemaleBipedPath $s rEquip sv_Destruct s if (who.GetEquipped rEquip) ; 表示を反映させるために脱着。 who.UnequipItemNS rEquip let param[0] := 1 ; wait let param[1] := param[1] | 1 ; flags |= 1 return endif endif ; 現在の装備の状態 let nEquip := param[3] set state to nEquip * (1.0 - (GetCurrentHealth / GetObjectHealth)) ; 上限下限チェック if (state < 0) set state to 0 elseif (state > nEquip) set state to nEquip endif let break := param[2] if (GetGameLoaded == 1) let break := 0 endif if (break != state) ; 表示と状態が異なる場合 if (break < state) ; 破壊が進行 if (who.IsInCombat) if (who == player) ;-- 戦闘中で、プレイヤーが着ていたら以下を実行 let s := aMesP[break] else ;-- Actorであり戦闘中ならメッセージを流す let s := aMesA[break] endif message $s ; effectの発生 effectBreakArmor を0.3秒発生させる who.pms effectBreakArmor 0.3 endif let break += 1 else ; 修復した場合 let break -= 1 endif ; 後で装備した時に表示が一致するように ; 装備しているかどうかに関わらずパスを変えておく。 let rEquip := param[4] let s := aEquip[break] SetFemaleBipedPath $s rEquip sv_Destruct s if (who.GetEquipped rEquip && break == state) ; 表示を反映させるために脱着。 who.UnequipItemNS rEquip let param[0] := 1 ; wait let param[1] := param[1] | 1 ; flags |= 1 endif endif let param[2] := break End 有了string,array和function的基础知识,上面这段代码自然也就比较容易读懂了,需要注意的几个地方如下:1,位操作 obse支持位操作,逻辑与逻辑或什么的,但不支持16进制立即数,所以,要将最后一位置0,只能这样干 let param[1] := param[1] & 2147483646 后面的注释写得很明确,这行的意思跟let param[1] := param[1] & 0x7FFFFFFE一样2,SetFemaleBipedPath 这是设定一件装备的nif文件路径的函数,当装甲需要破损时,使用这个函数将装备的nif文件设置成下一阶段 需要先卸下再装备,效果才能出现3,字符串变量转为立即数 SetFemaleBipedPath $s rEquip SetFemaleBipedPath函数的第一个参数需要指定一个字符串立即数,也就是用""包含的文本,obse字符串的“$”操作符可以将string变量转换为立即数4,效果渲染 who.pms effectBreakArmor 0.3 pms是PlayMagicShaderVisuals函数的简称,意思是播放一段可视化效果 效果名为第一个参数,这里是effectBreakArmor,这是破衣框架自己实现的一个效果:碎片四散 播放时间是第二个参数,这里是0.3秒,俺个人认为改成1秒更好5,注意IsInCombat 这个函数是用来判断一个actor是否处于战斗状态,使用时一定注意用actorRef.IsInCombat的语法,不要用IsInCombat actorRef 虽然cs不会报错,但你的所有脚本将会失效,这是个bug ! Share this post Link to post Share on other sites