(本文中的故事情節純屬虛構,如有雷同……的話?)
UE4用久了,遇過的怪事不會少只會多。
只是,這次的事件卻令我感到毛骨悚然。
事情該從哪邊開始講起呢?對了,就從功能面的設計面開始吧。
首先,由於我正在製作一個網路對戰型的遊戲,因此希望在其他玩家以「Spectator(觀眾)」的身份進到遊戲的時候,能夠即時讓對戰中的玩家知道目前場上有多少觀眾正在觀賽。
而要決定Client端要不要以「Spectator」的方式登入的方法非常簡單,基本上就是以下二種方法:
- 在OpenLevel的時候帶入參數?SpectatorOnly=1。
-
覆寫AGameModeBase::MustSpectate這個方法,並根據我們遊戲設計的邏輯決定是否回傳true。(例如比賽正在進行中的話,後面就都只能以spectator的方式加入)
這個階段我只是想要確spectator的計數功能是否正常,因此我的想法非常單純:
當Client上的SpectatorPawn成功進到遊戲的時候(觸發PossessedBy)的時候送一個RPC給Server。
由於我已經知道SpectatorPawn本身只存在於Client端,所以他本身是沒有連線功能的。
為了成功將RPC送出,因此必須要藉由PlayerController的連線通道:
https://gist.github.com/dorgonman/9b62afabd399b7fbc855b64ee3e08757
然後在PlayerController上的實作如下:
https://gist.github.com/dorgonman/f9b41c58339b2ffa68773a13ab2d966e
在Server收到SpectatorReady通知之後,馬上去增加AMyGameModeBase::NumReadySpectator的數量並印出來
在編譯完成按下Play之後,可以在Log上發現這段訊息:
LogTemp: NumReadySpectator: 1
嗯,很好!SpectatorPawn的登入通知很順利的傳送給Server了!
正當我滿足於現在的實作,準備進行下一階段功能的設計時,我突然看到了現在MyGameModeBase::MustSpectate中的實作:
https://gist.github.com/dorgonman/bdd1380a8acd64615e6ee913ad2c0669
「有一個觀眾?」眉頭一皺,我感覺到那邊似乎不太對勁。
理論上的MustSpectate回傳都是false的話,那麼NumReadySpectator應該都會是0才是,
而且,pPC->Server_SpectatorReady()應該是完全不會被呼叫到才是。
於是,我馬上把視線移向了editor中的WorldOutliner,想看看計數器中的Spectator到底哪來的:
一看了之後我嚇出了一身冷汗──這個世界中並沒有任何「SpectatorPawn」存在。
就我的理解,World Outliner應該會顯示目前世界上所有存活著的Actor才是,
難不成這個世界還存在著一個看不見的Spectator默默的觀察著玩家?
為了查證SpectatorPawn到底是被誰生出來的,於是我便在Constructor下了斷點──果然,
世界就這麼停留在Spectator被生成的那個瞬間,循著callstack往上追蹤,我發現了一個線索:
當Client端從Server那邊收到SpectatorClass之後,便會觸發PlayerController中的ReceivedSpectatorClass()把SpectatorPawn生成出來。
我感覺更加困惑了:每個PlayerController在收到Server的通知之後都會生成SpectatorPawn出來?
這不是我所認識的UE4阿!證據是,World Outliner並不存在任何的SpectatorPawn。
我心裡感覺到越來越毛了──是誰?到底是誰在偷偷觀察著我?為了糾出這個隱身在黑暗中的觀察者,
於是我決定好好的進行追蹤。
在經過反覆調查之後,我發現spectator被生成流程如下:
- 在AGameModeBase::InitGameState()中會把SpectatorClass指派給GameState,再藉由GameState這個連線通道把該變數replicate給client。
-
Client上的GameState從在Server上收到replicate過來的SpectatorClass之後,便會呼叫PlayerController->ReceivedSpectatorClass()
-
PlayerController會判斷自己是不是在NAME_Spectating這個State,若是的話便會把Spectator生出來。
其中Spectator的生成邏輯如下:
https://gist.github.com/dorgonman/cfdee32feb78b8123e6bed2585f414aa
而StateName也確實的,在APlayerController::PostInitializeComponents中被初始化成NAME_Spectating。
到這裡為止,我幾乎可以確認,每個PlayerController在一開始的時候都會生成一個SpectatorPawn──這件事已經是不可避免了。
但剩下的議題是:到底為什麼在World Outliner看不到這個SpectatorPawn的身影?而且,為什麼UE4要生成這個觀眾?
為了解開這個謎題,於是我便在AMySpectatorPawn::UnPossessed中下了斷點,果然,這個Spectator在某個時機點的時候就被刪掉了。
從call stack中我們可以看到,當client端從Server收到ClientRestart這個RPC的時候,會把從Server傳過來的Pawn設給該PlayerController中的AcknowledgedPawn,並把StateName轉成NAME_Playing後刪掉Local端的SpectatorPawn。
謎題似乎是解明了:原來是為了在收到Server完成Pawn的生成之前,先讓PlayerController生成一個SpectatorPawn阿……
這個就是存在於那裡,但卻看不見的SpectatorPawn的由來。這個SpectatorPawn很好的代替了我們,在大兒子(PlayerController1)跟爸爸取得連線通知之前,好好的看守了我們的家。
到這裡,我完全理解了,本來以為是個鬼故事,但卻完全是個家庭人倫悲劇:
原來是個不被爸爸(GameMode)承認,兒子(PlayerController)沒辦法,只好親手殺掉自己私生子(SpectatorPawn)的故事阿……
為了重新理解一次整個流程以悼念一個生命的消逝,於是我便在APlayerController::PostInitializeComponents()下斷點,想要好好追蹤整個StateName的變化與案件發生的過程。
然而,我卻意外的發現另一件驚人的事實:我完全不知道,這家族到底要殺了多少家族內的成員才會罷休……
會注意到這個案件,是由於APlayerController::PostInitializeComponents()中的斷點,在Client端非常意外的被呼叫了二次。
第二次的呼叫我不意外:Client在收到Server的通知之後便去生成PlayerController,然而問題卻出在第一次……
這件事是發生在世界剛開始,引擎還在讀取Map的階段(UEngine::LoadMap)。隨著callstack往上追蹤,會發現下面這段程式碼:
https://gist.github.com/dorgonman/8f4260d63ac09cde2c5fec60b346544b
一時間我還不知道發生了什麼事,為什麼會有一個非常陌生PlayerController被生了出來?
而且,該PlayerController的Class還不是我們在GameMode中設定的那個。
然而在仔細閱讀了裡面的註解之後,我馬上理解發生了什麼事:「大哥,原來是你!!??」
原來在大兒子(PlayerController1)被生出來之前,曾經還存在著一位大哥(PlayerController0)。
這位大哥非常的為他的弟弟著想,拼命的想跟爸爸(GameMode)取得連繫;因為,只有當世界連上線的時候,他的弟弟(PlayerController1)才有辦法出生。只是,非常不幸的是,這位大哥也是一樣是個不被承認的存在,因此在跟老爸取得連繫的那一瞬間(UNetConnection::HandleClientPlayer)就被世界的惡意給抹殺掉了……
Epic大神有云:「你認為你早就已經有個PlayerController?錯!那是假的、暫時的!」。
在調查完這個家族的來龍去脈之後,我也只能輕聲嘆息:原來人命可以這麼不值錢……
最後,讓我們回到最一開始的設計問題吧。
如果我想要計算場上Spectator的數量時該怎麼辦?
由於在Server上並不存在SpectatorPawn,因此用TActorIterator<ASpectatorPawn>這個方法是不可行的。
而且由於每個Client端在跟Server連上線之前都會先生成一個SpectatorPawn,
因此在ASpectatorPawn::PossessedBy跟ASpectatorPawn::UnPossessed的時候用RPC的方式通知Server是否有觀眾進入跟離開觀賽也不可行。
其實這個問題的答案意外的簡單:我們只要做在AGameModeBase::Login跟AGameModeBase::Logout就行了,其實作如下:
https://gist.github.com/dorgonman/1e141fc4cb10353aa395fa37c9ec8e74
Leave a Reply