0. 扯淡
Meh. 上次開篇就說“開了個新坑,不知道能不能堅持下去”,結果發現已經有半年了…… 坑還是要填的,嗯。
最近實在是狀态不佳,看着一堆需求一點想要搞的欲望都沒有。于是就來填坑了。
不知道東方符鬥祭(THB)的,可以看以下系列文章上一篇的開頭介紹,這裡不重複了。
上一篇:符鬥祭的背後(一):自動更新機制的演進 - 的碎碎念 - 知乎專欄
1. 這遊戲怎麼立項的?
大學的時候參加了一個“工作室”,說是工作室其實就是每天瞎折騰,順便幫學校打打工,賺個學校給分配的辦公室(有據點的感覺不錯呢)。
跟工作室的小夥伴磨合的時候,被帶進了 DotA 坑。然而玩的時候不僅手殘,也沒有大菊觀,每次都是“卧槽你的大呢?”或者“卧槽你的大怎麼這麼早就交了?”……啊,還有“每回你說你要死了,都特别準”…… 經曆了諸如“第六人”等等讓人撓頭的評價之後,我決定不按他們的遊戲規則玩了!我去試着對 DotA 地圖做2次開發,添加自己想要的英雄。到現在還記得當時拆的地圖的版本(6.67c)。
中間的過程不表,還算是挺有意思的,跟近10萬行的混淆過的 Jass 代碼作鬥争。這個開發過程中,最刷新我世界觀的,是對 War3 的邏輯層的結算的認識:所有的機器都運行相同的邏輯,而且所有的遊戲對象、随機數都要同步,真正需要通信的隻有用戶的輸入!我不知道其他人怎麼樣,但是當時的我對下面這種代碼都會很不舒服:
var_a = var_b = 0
while True:
var_a += 123
var_b += 123
...
我一定會寫成這樣:
var_a = var_b = 0
while True:
var_a += 123
var_b = var_a
...
盡管代碼沒意義但是應該能理解我的意思,我總會覺得第一種跑着跑着就會跑偏(盡管對整數來說并不會)。
直到符鬥祭上線好久之後,我才直到這個逆天的全節點同步的結構有個高大上的名字叫 (戳這裡:魔獸争霸3技術分析資源彙總)。
世界觀刷新以後,就特别想找個機會跟之前的做法做個決裂。
然後呢,我又通過室友的介紹入了英雄殺的坑(對……騰訊的狗熊殺……當然跟同學面殺的時候用的三國殺)。這裡倒是沒什麼神奇的經曆。
玩的久了,自然會有一些自己的關于人物設計的想法,作為東方狗自然就開始往東方人物上面套了。
某天,我的 Dell D630 突然罷工了(還記得顯卡門麼……?)。報修。3天的時間無所事事,于是開始踐行之前的腦洞,做了些設計,之後便一發不可收拾了。
晚上睡覺看着天花闆,思考怎麼對三國殺的遊戲規則建模,也要滿足能聯網玩這個要求,于是自然想到了 War3 的結構,發現真的可以直接套上去。正好對于編程方面,當時正好處在不知道幹啥的階段。在學 但是非常擔心堅持不下去,所以在積極的找可以做的項目(CRUD就免了……),于是當即立 flag,我TM要把這個寫出來!畢竟我這麼牛X,怎麼能沒有個能拿出手的項目呢(逃
PS: 學一門新語言能找到合适的項目真的非常重要。 之前學過 ,感覺非常好…… 但是野雞大學自己玩的大學生哪有什麼分布式的項目讓你折騰啊,于是就棄坑了。現在還想學 Rust 和 Lua,然而找不到什麼可以搞的東西(或者說,感興趣的)。
1. 總體的架構
THB 的結算的核心很簡單,就是兩個概念: 和 (大概有的人看完了這兩個名字就知道我想說啥了)。
總之, 就是遊戲中的某個動作。結算的時候 會形成一個棧。比如,出牌階段,一名角色使用彈幕(殺)對另一名角色造成了傷害時,在這個鍊條上,整個的結算棧是這樣:
5 Damage(a, b, 1) # a 對 b 造成了一點傷害
4 Attack(a, b) # a 對 b 彈幕效果
3 LaunchCard(a, b, AttackCard) # a 對 b 使用一張彈幕(殺)
2 ActionStage(a) # a 的出牌階段
1 PlayerTurn(a) # a 的行動回合
0 Game # 遊戲邏輯開始(整個遊戲是一個巨大的 Action)
用來截獲并處理事件。
一個典型的 長這樣(天狗盾的,跟三國殺中仁王盾等價):
class MomijiShieldHandler(EventHandler):
# EventHandler 的優先級,這個需要在蓬萊玉枝的前面執行
# 因為蓬萊玉枝會改變彈幕的性質,會導緻攔截失效
execute_before = ('HouraiJewelHandler', )
def handle(self, evt_type, act):
# 如果是彈幕效果的“事件發生前”時點
if evt_type == 'action_before' and isinstance(act, Attack):
tgt = act.target
# 如果彈幕目标沒裝備天狗盾,就不做處理了
if not tgt.has_skill(MomijiShieldSkill):
return act
# 如果彈幕顔色不是黑色,就不做處理了
if not act.associated_card.color == Card.BLACK:
return act
g = Game.getgame()
# 攔截這個彈幕
g.process_action(MomijiShield(act))
# 原樣返回當前的 Action
# (被攔截的話,當前的 Action 會被取消)
return act
可以被當作事件的東西有很多,比如每一個 在觸發的時候都會産生 、apply、after 這幾個事件,分别代表事件發生前、事件發生時,事件發生後。聽起來貌似 和 apply 差不多,實際上有微妙的區别:按照約定,事件發生前, 是可以插入結算另外一個 或者任意修改當前的 的,更改屬性(在目标臉黑的情況下你造成的傷害+1),直接取消掉(對方不想造成傷害,并且向你扔了一隻狗),甚至替換成一個其他的 (樓觀劍的實現有用到過);事件發生時的時候,整個 就是闆上釘釘了,不允許任何修改,隻允許在這裡插入結算。
然後就是上文說到的,所有的客戶端和服務端同步運行這些邏輯。
2. 跟 War3 的正版 的區别
1) THB 并不是字面意義上的 ,或者說 “”。THB 中并沒有時間片,所有體現了Lock(或者說同步)的地方,都是在需要通信的地方,比如用戶輸入和隐藏信息的揭示,這裡會對通信做編号,可以當作 中的對時間片的編号。
2)THB 中用戶輸入實際上是業務邏輯,是類似于“選一張牌”或者“選一個角色”這樣的輸入,而不是“鼠标在 (x,y) 點了一下”這樣業務無關的信息。所以用戶輸入是在遊戲邏輯中顯式請求的,但是 War3 的用戶輸入的處理是引擎完成的,Jass 腳本中無法對 UI 輸入做什麼。
3)War3 中所有的 peer 都知悉所有的遊戲狀态,THB 中不是,隻有服務器知悉完整的遊戲狀态,各個客戶端隻知道跟自己有關的信息:隐藏卡牌在邏輯代碼中真的是隐藏卡牌,并不是 UI 做的處理,跟 War3 的戰争迷霧不一樣,也就不存在“全圖”的可能性。
4) THB 中沒有做檢測 的機制,在 的第一時間有可能不會出問題,但是之後就會炸掉,就像 C 程序的 bug 一樣,非常難搞。(本來想甩鍋給“哎呀我們的邏輯代碼又不是跑在 VM 上這個沒法搞啊”,但是想了下貌似搞起來沒什麼問題……)
5) THB 中的随機數不是同步的,而是在服務器端生成,然後告訴相關的客戶端的。因為存在洗牌,不可能讓随機數同步的。
3. 結算和通信
做了個動畫,希望能幫助理解
技能叫 ,主要設定是“目标需要選擇一張牌交給我,否則我對目标造成一點傷害”。
動畫麼沒有涉及 的内容,不過我覺得不用解釋了。
4. UI 層
UI 層的展現和處理用戶輸入也是通過 來處理的。客戶端的 UI 會在 列表中的最前面插入一個用來截獲各種事件的 ,轉發給 UI 層,然後 UI 層根據這些信息來處理用戶輸入的時候會發一個 事件,然後在需要用戶輸入的時候 會通知 UI,并且挂起邏輯層的結算,UI 等玩家輸入後将結果填回,再将挂起的邏輯層結算恢複。
5. 坑
要說坑的話……
1) 首先各種 ! 所有用到 dict、set 之類的無序集合的時候都要萬分小心,千萬不能遍曆!一遍曆就 ,因為在每個機器上元素在集合内的位置可能不同,遍曆時每個機器上的順序就會不同,服務器是 Linux,客戶端是 ,會顯得更明顯。自己測的時候卻不容易測出來。而且沒有什麼辦法可以禁止類似的遍曆。
2) 在手機版上因為是 Unity 引擎,沒法像 PC 版把 UI 信息的傳遞做成同步的(涉及 event loop 的切換,開銷巨大,後面的文章會講),于是就是異步的了,這樣會有非常惱人的問題:比如方片周讓玩家 a 猜,無論怎麼樣,這張牌會展示給所有人,結算,然後交給玩家 a 後…… 洗牌!如果 ui 在這個過程中都沒有機會做什麼的話,看到的就會是“隐藏卡牌”…… 有辦法,但是懶得修了…… _(:3」∠)_
6. 其他的有趣的事實
1) 洗牌算法一開始寫的又臭又長(貌似超過100行了),還滿是 bug,各種 。後來想通了,縮減到兩行了(省略了支持代碼):
seed = sync_primitive(g.random.getrandbits(63), a) # 服務器與客戶端 a 同步一個随機數
random.Random(seed).shuffle(cardlist) # 用這個随機數做種子,用标準庫的 shuffle 洗牌
2) 之前學算法的時候學到拓撲排序,總覺得沒啥用。然後在這裡面居然用上了,用來給 們排序。前文中能看到 之間是有先後順序的。
歡迎問問題!
------
題圖:火焰貓燐,東方地靈殿中出場人物。:和茶
有話要說...