深入解析《魔獸兵團》:計算迷你棋經驗值
兵團玩家們,大家好!
我是 Andy Lim,也是《魔獸兵團》的首席伺服器工程師。只要是你想得到的相關服務,都在我們伺服器團隊的負責範圍內,像是網路、雲端演算與儲存空間服務等等,但我們也會建立遊戲功能,像是事件進度與任務等等。我想為大家揭開綠幕,並分享我們是如何儲存經驗值,並用來計算每個迷你棋等級的一些資訊。
認識 Cassandra
注意!下方會有許多技術相關的細節,內容或許會有些沉悶,還請多多包涵。
那我們就先以認識資料庫來當開頭吧。我們使用的是 Cassandra,這讓我們能為同時在線的許多使用者追蹤不斷變換的玩家資料。Cassandra 是個公開資源、高人氣、能大幅調整,以及帶有分散架構的資料庫,讓我們能在資料一致性與可取性之間取得適切平衡。以資料來說,Cassandra 無須硬性綱目就能妥善處理龐大的資料。我們打造了工具讓工程師定義資料庫表格與表格綱目,藉此符合各項功能的需求,也讓我們能以預想的方式打造更彈性的架構與組織。我們便可以輕鬆地編寫與核准我們的綱目與資料查詢。
什麼是綱目?一個資料庫綱目定義了相關資料庫中的資料是如何編列的,像是你在表格中找到的資料一樣。
以分類帳儲存經驗值
Cassandra 編寫資料的速度是出了名的快,但解讀資料卻有些緩慢。更新原地資料通常會使用先讀再寫的模式,所以更新的速度也會有所減緩。為了能緩和這個問題,我們把玩家資料設定成分類帳的形式來儲存。分類帳中,你會寫下一條敘述來記下變動事項。在讀取的時候,你會解讀所有的條目,並做出一些運算。
像是大家的信用卡就是能夠展示分類帳的絕佳例子。每一筆交易會以正值或負值記錄下來,並以條目顯示。當你使用信用卡時,帳本中就會有負值的交易紀錄。反之,當你付清款項之後,就會獲得正值的交易紀錄。那你該如何知道是否有積欠款項呢?你需要解讀整個分類帳,並把數值加總或是分開計算。
現在把原地資料想成可以儲存單一內容的物體。每當你想改變數值時,會需要解讀、改變數值,並寫下新的數值。所以,如果這筆資料是一場足球對戰的比分,那麼每當有一個隊伍得分時,你會需要解讀過去的總分是多少、添加新的得分,並把最後的總分寫回比分上。
《魔獸兵團》中的實際例子就是每個迷你棋的經驗值條目。每一個任務結束後,一行條目就會為一個迷你棋添加到分類帳中,並指出該迷你棋獲得了多少經驗值。完成五個任務之後,分類帳的樣子應該會如下所示:
表格 1
玩家 |
迷你棋 |
數值 |
時間 |
---|---|---|---|
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 |
Andy |
獅鷲騎士 |
3 |
星期一下午 2 點 5 分 |
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 10 分 |
Andy |
S.A.F.E.駕駛員 |
3 |
星期一下午 2 點 15 分 |
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 20 分 |
最後,若想知道迷你棋總計獲得的經驗值,我們只需要把分類帳中的所有條目資料收集起來,並以每個迷你棋分門別類來獲得加總的數值。結果會如下所示:
表格 2
玩家 |
迷你棋 |
總計 |
---|---|---|
Andy |
豺狼人蠻卒 |
9 |
Andy |
獅鷲騎士 |
3 |
Andy |
S.A.F.E.駕駛員 |
3 |
加快彙整過程的速度
既然現在我們已經建立了經驗值的儲存方式,那就來看看每個迷你棋的計算過程能如何改善吧。儲存每個迷你棋的經驗值獲取條目在讀取的時候會變得很難處理。這會是好幾個條目的資料,而且表格會得很長…無限長那種。假設你一天遊玩《魔獸兵團》的平均組合是 15 個任務或 PvP 對戰,外加一週為了金幣進行 20 次奔騰對戰,以及每週為了軍隊升級進行的地城戰鬥好了。這樣每週就會有 100 筆的資料。玩了 3 個月之後,你就會獲得帶有大約 1260 筆條目資料的分類帳。現在,要加總的時候,Cassandra 系統會先緩慢讀取,我們之後才能取得資料。這可不妙。
我們便設計了一個解決方式,那就是:彙整。彙整是截至一個時間點為數值進行的計算。以遊戲來說,我們會把數值加總,並以單一一列的形式表示。而且我們會把這筆資料儲存到第二個表格中。現在你就會發現時間戳記的有用之處了。以經驗值來說,關鍵會是玩家與迷你棋,而計算的過程便是把這些資料加總。 假設我們想彙整星期二凌晨 12 點之前的條目,並把這些結果儲存到第二的 Cassandra 表格好了。那這些條目可能會是這樣:
表格 3
玩家 |
迷你棋 |
總計 |
結束日期 |
---|---|---|---|
Andy |
豺狼人蠻卒 |
9 |
星期二凌晨 12 點 |
Andy |
獅鷲騎士 |
3 |
星期二凌晨 12 點 |
Andy |
S.A.F.E.駕駛員 |
3 |
星期二凌晨 12 點 |
你在星期三玩遊戲之後就會產生更多的經驗值條目。而原本的經驗值表格就會增加。
表格 4
玩家 |
迷你棋 |
數值 |
時間 |
---|---|---|---|
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 |
Andy |
獅鷲騎士 |
3 |
星期一下午 2 點 5 分 |
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 10 分 |
Andy |
S.A.F.E.駕駛員 |
3 |
星期一下午 2 點 15 分 |
Andy |
豺狼人蠻卒 |
3 |
星期一下午 2 點 20 分 |
Andy |
S.A.F.E.駕駛員 |
3 |
星期三中午 12 點 |
Andy |
閃電鏈 |
3 |
星期三下午 12 點 5 分 |
Andy |
獅鷲騎士 |
3 |
星期三下午 12 點 10 分 |
現在在完成這幾場對戰後,我們想獲得完整的資料並重新計算迷你棋的實際等級。我們會從兩個表格提取資料,一個是彙整表格的單一條目,其他的是在特定時間點後的所有條目。最後把數值加總後就會獲得經驗值總計的最終表格。所以,我們會讀取表格 3 的資料,接著再查詢表格 4 中出現在表格 3 後的資料。以下是我們會進行的步驟:
- 讀取表格 3 的條目
獅鷲騎士 |
3 |
星期二凌晨 12 點 |
- 從表格 4 提取星期二凌晨 12 點之後的獅鷲騎士資料
獅鷲騎士 |
3 |
星期三下午 12 點 10 分 |
- 接著把兩筆資料總合起來就會得到:
獅鷲騎士 |
6 |
你可以發現這個方法是如何縮減表格 4 的讀取過程的。
你也許會想說,那我們在儲存新的分類帳條目之後只需儲存新計算的資料就好。這表面上聽起來很美好,但實際運作的時候會讓資料不一致,並且會有效能降低的問題。效能降低的一個可能範例是,系統會因為先讀在寫的模式,而忙碌地重新計算與儲存資料。但我們需要在資料演算與為所有玩家以適切的效能執行遊戲之間取得平衡。
資料不一致的例子則是對戰結束資料的運算處理。我們的伺服器架構與平台支援了延遲送達訊息。像是兩個資料中心(A 與 B)之間的短暫問題,而玩家迷你棋獲得經驗值的訊息已經 A 發出,但卻還沒有抵達 B 的狀況。想像一下以下情境:你在半夜之前進行了一場對戰。玩完之後你也贏得勝利,而時間是星期三晚上 11 點 59 分了。彙整處理器被設定為每日進行運算,來彙整截至當天的所有資料。所以,系統在半夜的時候便會開始計算截至星期四凌晨 12 點之前的經驗值總計。這筆資料會儲存在第二個表格中,而新的經驗值會讀取最後的數值,並加總星期四凌晨 12 點後發生的所有條目。而延遲送達訊息會有的問題是,我們有收到這筆資料,並且會發現對戰的完成時間是星期三晚上 11 點 59 分,而經驗值條目也會記錄這個時間。但彙整表格不會納入這個條目,在之後的資料提取時也不會顯示這筆資料。這個資料的不完整便會造成問題。那麼延遲好幾天的訊息又怎麼辦呢?我們有監控與警示系統,讓這筆新資料幾乎總能保證在下一次的彙整之前能及時進行處理。
計算迷你棋等級
回顧上方的經驗值總計表格,你會發現其中都沒有等級的計算。我們只有獲得經驗值的總計資料。分開來說,遊戲設計師們設計了另一個靜態表格看起來會像這樣。一個迷你棋從等級 1 開始,當它獲得 1 點經驗值之後就會變成等級 2 ,接著它會需要 3 點經驗值來升成等級 3。
等級 |
升至下一等級所需的數量 |
---|---|
1 |
1 |
2 |
3 |
3 |
6 |
4 |
10 |
5 |
20 |
… |
… |
10 |
250 |
將兩個表格結合在一起之後,我們可以透過總計來計算出一個迷你棋的實際等級,並得到最後的資料如下:
迷你棋 |
等級 |
經驗值 |
升至下一等級所需的數量 |
---|---|---|---|
豺狼人蠻卒 |
3 |
5 |
1 |
獅鷲騎士 |
3 |
2 |
4 |
S.A.F.E.駕駛員 |
3 |
2 |
4 |
閃電鏈 |
2 |
2 |
1 |
遊戲設計師有很大的彈性可以改變升級所需的經驗值。他們可能覺得低等級的時候很難升級,並降低了升至下一等級所需的經驗值。這代表無須對資料庫中的玩家資料作出改動,迷你棋就可以稍稍提高經驗值進度,升至下一級的門檻也會有所降低。反之亦然。若遊戲設計師覺得升級的速度太快了,便決定要提高經驗值門檻。我們就不用給予額外經驗值來彌補改動之後的落差。但這不代表說我們無法給予設計師選項來提供一些經驗值,並幫助迷你棋減輕升級所帶來的痛苦。
考慮替代方案
在使用當前的運算方法之前,我們也去了解了其他儲存經驗值與計算迷你棋等級的多樣方式。有些方法可能會讓伺服器關閉更久,才能讓我們改變玩家的經驗值資料。而有些也提供了恰到好處的運算體驗,同時也能跟上玩家的等級進度。
「一直儲存迷你棋等級、經驗值與升至下一級的資料」
那我們以標準的 SQL 模式,並使用交易更新呢?交易更新讓你能夠以資料整體進行更新,只有全部都更新或是完全不更新這兩個選項。若有一部分的資料覆寫失敗,那麼所有的資料就會全部重頭再寫一次。這樣就有 A.C.I.D. 的四大特性了,分別是單元性(Atomicity)、一致性(Consistency)、孤立性(Isolation)與永久性(Durability)。
這樣的方式只需要一行條目就可以保持每個迷你棋的經驗值與總等級,讓讀取的負擔大大減輕。
隨著迷你棋獲得經驗值,假設這個迷你棋升至等級 3 前獲得了 2 點經驗值,這樣還需 8 點才能升級。這樣的改動勢必會需要讀取資料、重新計算,並覆寫當前的資料庫。但這個模式與 Cassandra 和其長處並不是那麼的搭。這個方法的另外一個問題是若要改變升級所需的經驗值,我們會需要關閉遊戲來為所有玩家的全部迷你棋調整資料。
「以百分比儲存」
我們也想過以百分比的方式來儲存等級資料,舉例來說:
迷你棋 |
等級 |
升至下一等級進度 |
---|---|---|
豺狼人蠻卒 |
3 |
20% |
獅鷲騎士 |
2 |
20% |
S.A.F.E.駕駛員 |
2 |
20% |
完成一場對戰後,我們就會進行一些快速計算。例如:豺狼人蠻卒獲得了 3 點經驗值,也就是 3/10,因此我們就會給予豺狼人蠻卒 30% 的經驗值。結果如下所示:
迷你棋 |
等級 |
升至下一等級進度 |
---|---|---|
豺狼人蠻卒 |
3 |
50% |
獅鷲騎士 |
2 |
20% |
S.A.F.E.駕駛員 |
2 |
20% |
如果改變了迷你棋的等級曲線,我們就有彈性可以調整,對迷你棋的等級也不會造成任何影響。我們在遊戲使用者介面中也會更清楚了解,因為只要把經驗值條增加 30% 即可,無需做額外的計算。但是,這樣的運算模式也導致在較高等級時,百分比數值會很小的狀況。當迷你棋從每場對戰獲得的經驗值是 10 點,且需要 100,000 點才能升級時,百分比的表示法就會是 0.01%。中央處理器(簡稱 CPU)可以處理浮點數,但如果我們不考慮浮點運算的性質,精準度也會引發錯誤問題。
下次再會…
資料庫技術中有很多方面要考慮,像是工具提供的功能,以及最終的目的,就是如何儲存資料與運算。而這一切都還在開發當中,如果我們未來找到更適合的運算方式,也許就會改變也說不定,但目前的遊戲運算方式看起來是個不錯的起點。
那就感謝你閱讀我們這次的科技工程更新!
~ Andy Lim
順帶一提,我在此正式聲明我不是大眼睛盜賊。我第一天加入這個團隊的時候,盜賊就鎖定我的全新螢幕顯示器了。他們會整天盯著我…一…整天。