從動物森友會聊主機遊戲聯機機制

Joway發表於2020-05-14

最近在玩動物森友會的時候時常會遇到一些迷之聯機問題,在網上一番搜尋,發現大家的答案都趨於用玄學來解釋,於是便有了興致想在原理上搞懂這些問題產生的根源以及動森這款遊戲的一些聯機設定背後的技術原因。

事先宣告,本人並不從事遊戲行業亦非主機遊戲長期玩家,如有紕漏或其他角度的補充,歡迎在評論區告知。

遊戲是如何同步的

我們首先來看看一般遊戲是如何來做同步的。

想象兩個獨立房間裡分別有甲、乙兩個玩家,他們要遠端下一局象棋。他們每下一步前都需要先獲知到當前棋盤的情況,此時能夠有兩種實現方式。

第一種叫做鎖步同步,原理是玩家每操作一步就通知給另外一個玩家,彼此同步當前的操作序列,通過這些有時序的操作,就能夠計算出當前棋局的狀態。但它不允許中間丟失任何一步的資訊,否則就會出現非常大的計算偏差。

第二種叫做狀態同步,顧名思義是玩家每操作一步,就同步整個棋盤的狀態。這種方式可以容忍中間某些狀態丟失,最終得到的狀態依舊還是一致的。

在實際實踐中,針對那種玩家操作非常高頻的遊戲會更多使用鎖步同步,例如王者榮耀。而對於那些卡牌類遊戲更偏向於直接用狀態同步。

遊戲是如何聯機的

通訊架構

無論是上述哪種同步方式,我們都需要通過網路在多個主機間交換資料。我們現在將場景轉換成甲、乙、丙三個人一起下跳棋。為保證三個人最終得到的遊戲狀態都是一致的,我們往往需要有一臺 Host 主機來作為權威主機,其他主機只能通過權威主機下發的資料(狀態/操作序列)來更新自己本地的遊戲資料。

在這裡我們假設甲來做「Host」,乙、丙每操作一步,都需要先傳送給甲確認,無誤後再傳送該操作被確認的資訊給乙、丙,乙、丙此時才能夠認為操作成功並將畫面更新到最新的狀態。甲主機上在任意時刻都存有當前遊戲的真正狀態,其他主機只是在 follow 甲主機的狀態以更新自己的遊戲畫面。

在上述模式下,由於甲主機既要作為遊戲主機,又要作為狀態同步的主機,當聯機使用者數一多,甲主機就會不堪重負,出現所謂的「炸機/炸島」現象。另外,這種模式會需要甲主機一直存活,只能作為短時間內的伺服方案。所以有些遊戲會引入一個外部自建/官方的伺服器來承擔這個狀態同步的功能,例如我的世界。但究其原理是一模一樣的。

NAT 穿透

在瞭解完上面的基礎知識後,我們能夠發現,在不考慮外部伺服器的情況下,我們會對玩家主機間的網路有以下幾點要求:

  1. 甲能夠向乙、丙傳送資料
  2. 乙、丙能夠向甲傳送資料
  3. 乙、丙之間不需要有網路聯通保障

雖然上述要求看起來很容易,但是由於現在網路運營商都會不同程度地使用 NAT 技術,所以導致要讓兩臺家用主機建立雙向通訊並不是一件非常容易的事情。

家用網路一般有四種不同的 NAT 型別:

Full-cone NAT

  • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
  • Any external host can send packets to iAddr:iPort by sending packets to eAddr:ePort.

(Address)-restricted-cone NAT

  • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
  • An external host (hAddr:any) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:any. "Any" means the port number doesn't matter.

Port-restricted-cone NAT

  • Once an internal address (iAddr:iPort) is mapped to an external address (eAddr:ePort), any packets from iAddr:iPort are sent through eAddr:ePort.
  • An external host (hAddr:hPort) can send packets to iAddr:iPort by sending packets to eAddr:ePort only if iAddr:iPort has previously sent a packet to hAddr:hPort.

Symmetric NAT

  • Each request from the same internal IP address and port to a specific destination IP address and port is mapped to a unique external source IP address and port; if the same internal host sends a packet even with the same source address and port but to a different destination, a different mapping is used.
  • Only an external host that receives a packet from an internal host can send a packet back.

上述四種 NAT 型別簡單歸納就是說:

  1. 前三種 cone 型別的 NAT 都能將內網的 iAddr:iPort 對映到一個固定的外網 eAddr:ePort 上。只有 Symmetric 型別對於一樣的 iAddr:iPort 但不同的目的IP和埠也會被對映到不同的 eAddr:ePort 上去。
  2. 前三種 cone 型別的 NAT 的區別可以直接從名字中看出來,address-restricted-cone 表示只有自己對外發出過包的 address 有能力向其傳送包,port-retricted-cone 的意思是隻有自己對外發出過包的 address:port 地址有能力向其傳送包。
  3. 第四種 Symmetric 型別對外部返回報文來源的限制是與 port-retricted-cone 一致的。

主機上的網路測試功能能夠告訴我們當前網路的 NAT 型別。Switch 上的 Type A、B、C、D 分別對映到上面四種型別,而 PS4 上則是 Type 1(直連,無 NAT)、2(非 Symmetric NAT)、3(Symmetric NAT)。為方便下文敘述,我們用 Switch 的 ABCD 指代上述四種網路型別。

理解了四種 NAT 型別各自的限制,我們就能夠通過推導判斷出,哪兩個 NAT 型別的網路是不可能建立雙向通訊的,而不再需要去人肉嘗試。這裡我們分別舉例來介紹不同 NAT 型別下的不同情況,甲作為 Host 主機,並且我們有一個外部的聯機服務可以獲取到甲乙的外網 IP 資訊。所謂的聯機服務是一個第三方伺服器,甲乙都能通過訪問它去搜尋到對方的外網IP和埠號資訊,同時也可以將自己的外網IP和埠號資訊給註冊到上面。所以這裡甲、乙能夠在通訊前就知道彼此的通訊地址資訊。

如果甲的 NAT 型別是 A :

  • 無論乙的型別是 A/B/C/D,乙都能夠直接向甲的 eAddr:port 傳送資料,而當甲已經收到乙的資料時,也能夠獲得到乙的 eAddr2:port2,以及向乙傳送資料的資格,從而建立雙向通訊。

如果甲的 NAT 型別是 B :

  • 當乙的 NAT 型別是 B/C/D :甲先使用 192.168.1.1:10001 => 1.1.1.1:10002(甲外網出口) => 2.2.2.2:20002(乙外網入口) 向乙嘗試傳送資料,雖被乙拒絕,但在乙的路由器上留下了訪問記錄,從而使得乙具備了向甲傳送資料的能力。而當乙傳送完資料,又會使得甲獲得向乙傳送資料的能力,從而建立雙向通訊。
  • 當乙的 NAT 型別是 A 時同上甲為 A 時邏輯

如果甲的 NAT 型別是 C :

  • 當乙的 NAT 型別是 D :乙嘗試連線甲的時候會被拒絕,並且甲也沒法知道乙對映的埠號是哪一個所以亦無法連線到乙。無法建立任一方向的通訊。
  • 當乙的 NAT 型別是 B :C-B 的連線同上面 B-C 的連線。
  • 當乙的 NAT 型別是 C :C-C 和 C-B/B-C 的區別僅在於要求雙方出口的埠要一直保持一致,要求更加嚴格,但依舊能夠建立雙向通訊。
  • 當乙的 NAT 型別是 A 時亦能夠建立雙向通訊。

如果甲的 NAT 型別是 D :

  • 當乙的 NAT 型別是 D:無法建立任一方向的通訊。
  • 當乙的 NAT 型別是 C:同 C-D,無法建立任一方向的通訊。
  • 當乙的 NAT 型別是 A/B:同 A-D 和 B-D,能夠建立雙向通訊。

綜上推導,可以有以下結論:

  1. 只有 C-D,D-C,D-D 的組合是沒有機會能夠建立雙向通訊的,其他組合在 NAT 層面上都能夠具備雙向通訊的能力。
  2. 型別為 A/B 的玩家理論上連其他任何型別的玩家都不會有 NAT 上的問題。

當然上述都是理論,實際中是否真的能夠連線上還取決於其他網路狀況甚至是程式編寫邏輯的因素。

動森是如何做聯機的

許多主機遊戲在聯機的時候都會有一些在玩家看來非常奇怪甚至奇葩的設定,這些設定都和上面講的同步機制和聯機網路問題相關。

動森的聯機模式也有諸多有意思(惱人)的設定,例如:

  • 聯機狀態下無法更改島的裝飾
  • 當一個玩家上島時,會需要所有人暫停近很長時間來等其成功加入
  • 當一個玩家離開時,同樣需要大家同步目送其離開,並且在離開時會儲存當前時刻的資料進度
  • 當有個玩家掉線/強行退出時,所有人的資料會回滾島上一次玩家登島/正常離開時的版本
  • 當島內有玩家開啟了對話視窗時,人不能正常離開也不能上島

以下分析僅僅是我在玩了 200 個小時遊戲後,結合自己的軟體工程經驗對動森實現方式的猜測。在沒有看程式碼前誰也無法保證這種猜測的絕對正確性,況且相比正確性,我更在乎這個猜測過程的開心,所以大家可以更多以工程角度來思考而不是糾結於其是否的確是這麼實現的。

我們可以把聯機遊戲的過程分以下幾個環節來分別討論:

1. 甲開啟聯機許可權(即所謂的開門),用自己主機作為 Host

這一步甲將自己的外網 IP 和埠號(如 1.1.1.1:10001)註冊在了 Switch 的聯機服務中。

2. 乙通過搜尋找到甲,嘗試加入

  • 乙通過聯機服務先將自己的外網 IP 和埠號(如 2.2.2.2:10002)註冊上去。(即遊戲裡詢問是否要聯機的那一步)
  • 再通過搜尋得到甲的 1.1.1.1:10001 (即動森裡搜尋好友的那一步),嘗試連線。

    注意,此時甲主機在後臺也通過聯機服務知道了乙在連它,並且甲也會根據 NAT 型別的不同,用乙的 2.2.2.2:10002 去連乙,嘗試打通雙向通訊。

3. 建立連線,上島

當上面一步確認能夠建立雙向通訊後,就可以開始上島了。上島又分為以下幾個步驟:

3.1 Host 打包當前所有遊戲狀態

在上島前,甲主機(Host)會把當前時刻的所有人的遊戲資料給打包一份 snapshot。

這裡的 snapshot 資料內容包括島嶼資料和玩家資料。

3.2 下載對方島的 snapshot

動森上島時會彈出一個顯示進度的動畫,這個動畫的過程就是在下載目標島的 snapshot 資料,很明顯能夠發現如果在中國連美國的玩家,這個過程會非常漫長,這個是由於跨境網速慢導致的。

3.3 其他人同步等待,直到新玩家上島

之所以其他玩家要等待新玩家上島是因為上一步儲存的 snapshot 必須是最新的結果,這也意味著其他玩家不能再有增量操作,否則新玩家上島時狀態就不一致了。

4. 開始遊戲

當上島完畢,就可以開始正常開始遊戲了。這時候就會遇到一個如何同步玩家彼此運算元據的問題。

這裡我們把玩家的操作分為兩種型別:

  1. 影響遊戲資料(低頻,有時序要求,需要落盤)
  2. 影響遊戲畫面但不影響遊戲資料(高頻,無時序要求,不需要落盤)

如果仔細體驗會發現,當我們在挖坑、和 NPC 對話、丟物品等會對全域性遊戲資料產生直接影響的操作時,偶爾會出現一下卡頓,這是因為這些會影響全域性狀態的操作在渲染畫面前都需要先向 Host 主機請求是否允許,這裡如果出現網路抖動的話就會出現卡頓/失敗的情況。但是我們跑步的時候卻很少出現卡頓,但有時會出現「閃回」,因為跑動隻影響了玩家當前位置,不影響遊戲資料,就算出現閃回也是能夠接受的,而且還不需要強制保證時序性。況且如果跑步也要去 Host 端詢問就會導致整個遊戲體驗都非常卡。但是像丟物品這種操作如果資料錯亂或者時序錯亂的話,整個狀態就不一致了,會非常嚴重。

所以這裡的同步方式其實是鎖步同步。只不過分別對低頻和高頻做了分別的策略。

5. 玩家退出/強退/掉線

玩家如果正常退出遊戲,會觸發一個「儲存資料」介面。要理解這個儲存資料的含義,我們要把遊戲裡的資料分為兩類:

  1. 島嶼的狀態
  2. 每個玩家的每一步運算元據

首先對於主機遊戲來說,其真正有效的資料都是要最終落盤到主機本地儲存裡。但試想如果每一次更新都觸發玩家本地主機儲存的更新,到時候要回滾起來也會變得異常麻煩,更不用說磁碟的 IO 還非常慢。所以這裡更可能的實現方式其實是,Host 主機的記憶體裡存放著當前遊戲的權威資料,包括島嶼狀態和玩家操作。另外,無論玩家用什麼方式退出,我們都必須確保結束遊戲後,所有玩家本地的存檔加上 Host 主機上的存檔都是某一個時間點上的真正遊戲狀態。遊戲資料的正確性優先順序是高於使用者體驗的。下面會有例子來解釋這點的重要性。

當玩家正常退出觸發「儲存資料」時,Host 主機首先會開啟一個當前時刻的 snapshot,然後每個玩家的主機都會向 Host 主機去下載屬於其的運算元據,並落盤到本地。

但當有玩家異常退出時,由於其並沒有下載屬於他的資料,所以他的本地遊戲資料還停留在上一次儲存的時間點 T1 上,為了滿足我們前面說的資料正確性的保證,雖然島上其他玩家沒有掉線,並且他們遊戲裡的狀態都是最新的也是正確的資料,但不得不為了讓這個傢伙的資料是正確的而把其他所有人的資料都回滾到了時間 T1 上,這就是為什麼動森會出現掉線回檔的原因。

常見問題

任天堂聯機服務垃圾嗎?

通過上面的解釋能夠理解,這些看似奇葩的聯機體驗背後,的確是有著非常多技術難題的。而且任天堂畢竟是一家遊戲公司不是專業分散式資料庫公司,雖然目前的技術實現方案有諸多可以改進的地方,但是也是要算上 ROI 的,所以談「垃圾」還是算不上。

為什麼遊戲廠商不自建伺服器來提升體驗?

遊戲玩家來自全球各地,如果要用自建伺服器來提升體驗,那也得在全球都鋪設伺服器,這個成本相當大且實現難度也相當大的。的確現在會有那種全球同服的解決方案,但是一般都是像網路遊戲這種就靠著聯網來掙錢的公司會有能力和意願採用。主機遊戲的商業模式註定了他們不會花非常高的成本去提升網路體驗。當然主機生產商自己搞一個全球伺服器就是另說了。

參考資料

相關文章