[UE4]UE4靈異現象之不管怎麼算都多出了一個人……

posted in: UnrealEngine | 0

(本文中的故事情節純屬虛構,如有雷同……的話?)

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

Your email address will not be published. Required fields are marked *