最近項(xiàng)目針對buff系統(tǒng)做了一些重構(gòu)糠雨,主要針對屬性修改的邏輯辣恋,這里做一些總結(jié)潘酗。
之前的設(shè)計(jì)
目前屬性是有依賴關(guān)系的,程序這邊提供了公式的配置方式揪阿,比如當(dāng)屬性A是需要用其他屬性來計(jì)算時疗我,可以配置B + C * 0.1
表示A 和 B,C的公式關(guān)系南捂,然后通過導(dǎo)表可以得出所有屬性的依賴關(guān)系吴裤,順便檢測下死循環(huán)等。
在制作Buff時的效果時黑毅,有時會需要修改屬性的值嚼摩,修改方式有按固定值的,也有按百分比的。最開始設(shè)計(jì)的時候枕面,給策劃提供了set_attr和get_attr兩個函數(shù)愿卒,策劃可以寫出很多種邏輯:
- 增加10:
set_attr(A, get_attr(擁有者, A) + 10)
- 變?yōu)?0%:
set_attr(A, get_attr(擁有者, A) * 0.8)
- 關(guān)聯(lián)另一個屬性:
set_attr(A, get_attr(擁有者, A) - get_attr(擁有者, A) * 0.2)
- 關(guān)聯(lián)另一個角色的屬性:
set_attr(生命, get_attr(擁有者, 生命) - get_attr(攻擊者, 力量) * 0.2)
我們把配置轉(zhuǎn)換成了可執(zhí)行的代碼,每個影響屬性的效果潮秘,會到目標(biāo)屬性下注冊一個函數(shù)琼开,buff消失后,就會注銷相關(guān)的函數(shù)枕荞,然后按需重算屬性柜候,大概過程如下:
按公式計(jì)算屬性的基礎(chǔ)值
遍歷buff的相關(guān)修改函數(shù)
return 結(jié)果
這種設(shè)計(jì)起初也挺好用,策劃寫起來比較自由躏精,但隨著復(fù)雜度的增加渣刷,開始發(fā)現(xiàn)一些問題。
修改函數(shù)執(zhí)行順序?qū)Y(jié)果的影響比較大
當(dāng)存在多個效果作用于同一個屬性時矗烛,計(jì)算的順序會導(dǎo)致計(jì)算結(jié)果不一致辅柴。比如先加再乘和先乘再加會有明顯的不同。關(guān)于還存一個set的功能瞭吃,策劃希望的是直接修改為指定的值碌嘀,比如攻擊變?yōu)?點(diǎn)之類的,這種優(yōu)先級會超過公式計(jì)算歪架,和buff的加成股冗,雖然目前也可以用set_attr函數(shù)達(dá)到相同的效果,但必須放最后一個后才行和蚪。
對于這個問題止状,我們把屬性計(jì)算的過程重新梳理了一下。buff不再按順序執(zhí)行惠呼,而是y = a * x + b的方式导俘,buff改變只是這個公式中的乘法因子a和加法因子b峦耘,而x繼續(xù)套用原本的公式邏輯剔蹋,過程大概如下:
if buff 中存在set操作 then
return set操作的值
end
按公式計(jì)算屬性的基礎(chǔ)值x
遍歷buff計(jì)算出a和b的值
return a * x + b
對于set,用流程保證優(yōu)先級辅髓,然后再增加了兩個函數(shù)用于修改a和b值泣崩。為了防止利用set函數(shù)填寫出加法或者乘法的邏輯,或者用加法函數(shù)寫出乘法的邏輯洛口,則直接利用語法解析器矫付,在導(dǎo)表檢查時禁止掉了這種寫法。
屬性關(guān)聯(lián)
對于一個屬性來說第焰,除了基礎(chǔ)的公式买优,buff的效果相當(dāng)會新增屬性之間的依賴關(guān)系,特別當(dāng)該效果是光環(huán)類型時(卸載時要還原效果)。除了對自身的屬性會有依賴杀赢,還可能對其他對象的屬性產(chǎn)生依賴烘跺,我們對此進(jìn)行了一些討論,做了下如下分類:
- 依賴其他對象的屬性
為了降低復(fù)雜度脂崔,做成了執(zhí)行一次立即緩存滤淳,不再其他對象的屬性更新,而導(dǎo)致buff這邊連鎖更新
- 依賴當(dāng)前對象的屬性本身
通過導(dǎo)表檢查和修改函數(shù)的改進(jìn)砌左,已經(jīng)填不出來脖咐,所以不再有這種情況
- 依賴當(dāng)前對象的其他屬性,要繼續(xù)分兩種情況:
- 執(zhí)行結(jié)果是不需要還原的汇歹。比如每隔1s按自己生命上限的10%增加HP屁擅。這種是不會因?yàn)樾遁dbuff而需要扣血,所以不會有依賴
- 執(zhí)行結(jié)果是需要還原的产弹,比如按自己力量的10%增加攻擊力煤蹭。如果想做成力量變化了,對應(yīng)增加的攻擊力也變化取视,意味著增加一種攻擊力和力量的關(guān)系硝皂。這種關(guān)系和之前公式關(guān)系類似,只不過是動態(tài)增刪的作谭,并且會隨著buff動態(tài)增加和刪除的稽物,并且可能帶來循環(huán)依賴。我們認(rèn)真查了查目前項(xiàng)目的幾百種buff折欠,發(fā)現(xiàn)目前并沒有這種情況贝或。實(shí)在有需求,也存在還有個簡單的解法锐秦,就是新增一些屬性咪奖,作為要改變屬性公式的因子,然后buff去改變這個因子酱床,這樣就不會產(chǎn)生動態(tài)的依賴規(guī)則了羊赵,更新觸發(fā)的邏輯也會有。
DSL解析
這幾天除了討論改進(jìn)方案扇谣,最有意思還是DSL解析這塊昧捷。準(zhǔn)確來說,其實(shí)之前策劃填寫的是lua罐寨,只不過這次希望解析這些lua靡挥,方便做更加精準(zhǔn)的控制。比如只允許用指定的語法鸯绿,不允許填出一些依賴或者死循環(huán)等跋破,將填好的set_attr配置替換成其他的函數(shù)等簸淀。
主要元素是函數(shù),變量毒返,字符串啃擦,數(shù)字,和 加減乘除 操作之類的饿悬,最后選擇了lpeg庫令蛉,剛開始用的時候需要多想一想,熟悉后發(fā)現(xiàn)還是挺方便的狡恬,相對正則表達(dá)式好用太多珠叔。導(dǎo)出數(shù)據(jù)結(jié)果是類似lisp的方式,即[op, arg1, arg2, ...]
的形式弟劲,這樣后期處理變得非常方便祷安。比如想禁止set_attr函數(shù)的第二個參數(shù)(可能是一個表達(dá)式)中包含第一個參數(shù),只要遍歷語法樹檢查就行了兔乞。再比如汇鞭,想把do_action(exp_a, exp_b, exp_c)
中的3個表達(dá)式拆分出來,只要先轉(zhuǎn)成語法樹庸追,然后根據(jù)語法樹反轉(zhuǎn)為文本就好霍骄。具體的代碼可以看這里,PEG定義的規(guī)則大概如下:
local syntax = P {
"Exp",
Func = namedpat("func", name * Space * P"(" * Space * pat_list(V"Exp", P",") * P")" * Space),
List = namedpat("list", P"{" * Space * pat_list(V"Exp", P",") * P"}" * Space),
Exp = V"OrTerm",
OrTerm = namedpat("or", lpeg.Ct(V"AndTerm" * (OrOp * V"AndTerm")^0)),
AndTerm = namedpat("and", lpeg.Ct(V"CmpTerm" * (AndOp * V"CmpTerm")^0)),
CmpTerm = namedpat("cmp", lpeg.Ct(V"AddSubTerm" * (CmpOp * V"AddSubTerm")^-1)),
AddSubTerm = namedpat("addsub", lpeg.Ct(V"MulDivTerm" * (AddSubOp * V"MulDivTerm")^0)),
MulDivTerm = namedpat("muldiv", lpeg.Ct(V"Factor" * (MulDivOp * V"Factor")^0)),
Factor = V("Func") + V("List") + V"Str" + V("Bool") + V("Var") + V("Num") + Open * V"Exp" * Close,
Var = namedpat("var", lpeg.Cmt((C((alpha+"."+alnum)^1)-(name+Number)), error_var) + name) * Space,
Bool = namedpat("bool", Boolean) * Space,
Str = namedpat("str", String) * Space,
Num = namedpat("num", Number) * Space,
}
期間和公司的另一個項(xiàng)目的同事聊了聊淡溯,他們由于支持的語法規(guī)則比較復(fù)雜读整,出于不想維護(hù)過于復(fù)雜DSL解析器和提高解析效率,直接利用靜態(tài)檢查工具luacheck提取出的語法樹咱娶,然后就可以各種魔改了米间。感覺這個思路也不錯,唯一的問題就是需要熟悉下沒有官方說明的語法樹膘侮,以后有需求可以試試屈糊。