符斗祭的背後(一):自動更新機制的演進
嗯,我貌似又開了個坑,不知道這次能不能堅持填下去。
剛剛搞定了新人物和裝備修改,一下午不知道幹啥(成就系統!商店系統!嘛,坑太大等等吧……),於是決定來寫寫技術文章,畢竟我這麼厲害怎麼能沒有些充門面的東西哈哈哈。
還不知道東方符斗祭的:
符斗祭(簡稱THB)是一個以東方Project為背景的,脫胎於三國殺的卡牌遊戲,目前可以跑在 Windows/Linux/Mac OS X/Android/iOS 上(抱歉了WP用戶,我並沒有WP可以拿來測試……)。人物和卡牌都有自己的設計,而且經過了實戰的檢驗。
儘管黑幕組成員和好基友@沂琳都覺得應該跳出「東方三國殺」這個印象,不過目前看來就是這個樣子了。
東方符斗祭(thbattle.net)
卡牌查看器 - 東方符斗祭(thb.io)
-----------------------------------------------------
因為符斗祭一開始就定位成在線的遊戲,所以在線更新的機制一直是有的。
第一版在線更新是從0自己擼的,非常簡陋。原理也很簡單,每次更新後,製作一份所有文件的列表和對應的CRC32值,然後把這個列表本身的CRC32值作為版本號保存在另一個地方。每次更新的時候,依據同樣的演算法,計算一份同樣的列表和版本號,跟線上的版本號比較,如果不同就說明需要更新,然後根據列表的內容,變化和沒有的下載,不在列表裡的就刪。
因為符斗祭的整個程序是用 Python 寫的(先不說移動端),程序都是1KB~20KB的純文本(Python 解釋器什麼的因為變動不大可以忽略),遊戲資源也沒有打包加密,都是散落在目錄中的文件,所以這個方式還是可以的,每次更新只需要下載很少的數據就可以了,bug修復之類的更新常常只是一個幾KB的文件。
這個方案缺點很多:
- 最大的缺點是,更新並不是原子的。下載失敗是常有的事,而且斷都會斷在更新的中間,留下一半新文件,一半舊文件,然後遊戲就再也打不開了…… 解決方案么,其實是沒解決:我另外的做了一個非常簡陋的修復工具,放在遊戲外面了。遇到這樣情況的玩家去執行一下修復工具……
- 慢。一個文件就是一個 HTTP 請求,外帶一個TCP短連接。因為是 Python,可能一次更新就要10~20文件要修改。即使設置了並發,速度也就那個樣,而且裡面有一個請求慢,就會拖慢整個更新過程。而且沒法保證不會掛掉!即使加了重試也是!
- 重複下載。在測試服測試新功能的時候,玩家會來回在測試服版本和正式服版本更新。更新了遊戲資源的時候這一點十分明顯(圖、BGM),還加劇了第一條。
- 玩家不能在遊戲目錄中放其他的東西。是的,有玩家這麼干,被坑了……
因為覺得還能忍,加上有其他的更重要的事要做(bug、新人物什麼的),然後是最重要的:自己很懶(「Arghh,it barely works……」),自動更新的事就一直這樣了。
直到……
玩家一直在喊要 Replay 功能,畢竟每天都有很多經典局,即使分享出去沒人看,自己存著也很開心啊。喊了很久了,喊的我覺得不做不行了。
但是有個很大的問題,導致了 Replay 功能實現不了。這還要從符斗祭的程序結構說起。
符斗祭的UI層是邏輯層的Observer,UI層能直接看到邏輯層的所有東西,毫無保留,不存在信息丟失。比如,一個傷害事件發生了,UI能看到的就是邏輯層真正在處理的那個傷害事件的對象,類型、來源、目標、由哪個事件引起的以及這個事件的類型、來源、目標…… 這樣UI寫起來非常爽快,但是代價是跟邏輯層的強耦合:UI層不能脫離邏輯層存在。
這樣的結果就是,即使是 Replay,在核心裡也要有一份真正的遊戲邏輯在跑。
最符合直覺的解決方案當然是……解耦!
計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決。
Any problem in computer science can be solved by another layer of indirection.
但是評估之後發現,代價是在是有點大。
- 在出現這個問題的時候,遊戲是已經成型了的。幾十張卡牌、30+的人物中的所有邏輯都要定義一份可以給UI使用的動作數據,所有的UI相關的代碼都要重寫。這麼大規模的改動肯定又會牽扯到相當多的基礎設施的改動,然後,各種 bug 就都來了…… 還干別的不?!
- 即使,poof,第一個問題不知道怎麼就搞定了,系統增加的複雜度也夠我喝一壺的了。邏輯和 UI 之間的介面跟邏輯層關係太大,強行解耦可能會把太多邏輯的東西體現在介面上,維護起來也是蛋疼。
- 人物/卡牌推倒重來了怎麼辦?看起來完全就是兩個東西哦,怎麼做兼容?老代碼和老資源不能刪么……卧槽……
總之這條路事倍功半,為了一個並不是主要 feature 的東西付出這麼大的代價…… 反正我是提不起幹勁了。
既然這一條路被否了,那麼就還剩下一條路:總是用對應版本的客戶端去播放 replay。這樣什麼解耦不解耦的都無所謂了。
在 Replay 機制之前,還有觀戰和斷線重連機制,實現機制也很簡單,伺服器會把發向一個用戶的所有的數據都記錄下來,然後在觀戰/重連的時候再發送給用戶,實際上就是客戶端用超快的速度把當前的遊戲再玩一邊。代碼里寫的函數名還是 replay 呢。
對,其實 replay 早就有了,而且服務端會保存每一場遊戲 replay 用作調試用,只不過這不是一個用戶可見的功能。原因嘛…… 相信各位已經能猜到了:這些 replay,一旦版本更新後,就廢掉了。
既然要保持現狀,那麼只能從更新機制入手了。在播放相應的 replay 的時候,程序要切換到相應的版本,也就是更新機制要有保留舊版本的能力。
嘿,是不是發現了什麼?版本管理誒,這不是 git 乾的事么?
發現了這個之後,我就去調研了一下用 git 來做版本更新機制的可能性……然後發現,這東西簡直是好用!
- 首先,也是這次重構的目的:基於 git 的版本更新可以很輕鬆的實現保留舊版本,以及版本之間的切換。
- git 的 branch 功能可以用作區分版本(這邊是正式服和測試服兩個版本,測試服會有最新的內容和一些奇怪的設定),之前的解決方案實際上是兩個更新伺服器,現在只需要指定 branch 就可以了。
- 更新過程現在是准原子的了,不會出現下載到一半斷掉後程序處於未知狀態的情況。
- 我不用管更新的協議了。什麼CRC32的列表,更新的元數據信息 blah-blah, git 都幫我做完了,我只需要說,檢查一下有更新沒,有就拉下來,然後切換到新版本。
- git 在傳輸的時候文件是打包+壓縮的,不存在大量小文件的問題。
- 在測試服和正式服之間切換不用再重新下載了,切一下 branch 就好了
實現的話,用到了 pygit2,是 libgit2 的 python binding。
這裡還有個腦洞,我覺得值得說一下。
git 版自動更新上線後,更新的伺服器一直是放在論壇伺服器上的,是日本 Linode。之後 Linode 的訪問速度越來越讓人抱歉(#祝病魔早日戰勝方校長#),於是決定把更新伺服器遷移到國內。找了很多 git 網站,最後放到了 http://coding.net 上(感興趣可以戳這裡,是符斗祭更新用的倉庫,其實並沒有什麼好看的),因為覺得他們的 logo 比較好看233。
因為這個很明顯屬於 abuse 了,一定要有個機制可以無痛切換更新地址。之前試過通過一個URL做302跳轉,但是 libgit2 直接報錯,看了源碼後…… 媽蛋就不支持這種用法…… 後來決定用 DNS 的 TXT Record 來保存更新地址。
? ~ dig TXT src.envolve.thbattle.netnn; <<>> DiG 9.9.5-3ubuntu0.5-Ubuntu <<>> TXT src.envolve.thbattle.netn;; global options: +cmdn;; Got answer:n;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 54410n;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1nn;; OPT PSEUDOSECTION:n; EDNS: version: 0, flags:; udp: 4096n;; QUESTION SECTION:n;src.envolve.thbattle.net.tINtTXTnn;; ANSWER SECTION:nsrc.envolve.thbattle.net. 1800tINtTXTt"https://git.coding.net/proton/thbupdate-src.git"nn;; Query time: 280 msecn;; SERVER: 119.29.29.29#53(119.29.29.29)n;; WHEN: Mon Dec 21 00:40:22 CST 2015n;; MSG SIZE rcvd: 113nn? ~ n
嗯 就是這個腦洞,我覺得挺 cool 的。
更新和切換版本的問題解決後,Replay 功能就很順理成章了。
class Replay(object):n __slots__ = (n version, # 版本,結構變化版本號也變n client_version, # 客戶端版本,就是 git 的 commit sha1,播放的時候會先切換到這個 commit 上。n game_mode, # 遊戲模式n game_params, # 遊戲模式的參數n gamedata, # 遊戲的數據,「把這張牌的牌面告訴那個玩家」這樣的內容。n users, # 用戶昵稱、頭像、稱號什麼的n me_index, # 「我(錄像所有者)」的座次n track_info, # 遊戲id,調試用n )n
然後,git版的自動更新的主要代碼在這裡
thbattle/autoupdate.py at master · feisuzhu/thbattle · GitHub
之前的拿衣服的版本在這裡
thbattle/autoupdate.py at 41997a70291c229e6af3a4e3b3247c9c2d719f15 · feisuzhu/thbattle · GitHub
-----------------------------------------------------
(題圖:西行寺幽幽子,在符斗祭中還沒有出場,但是約稿已經有了。illustrator:和茶)
推薦閱讀:
※部分免費Python免費視頻
※9、續7--文章的編寫頁面(略)
※Python入門——針對零基礎學習者的資料推薦
※(如何(用Python)寫一個(Lisp)解釋器(上))
※用python多進程,fork()之後創建了新進程,原來上下文裡面的局部變數也會再創建值完全一樣的么?