你們對力量一無所知
引言
回顧上文,我們談完了World和Level級別的邏輯操縱控制,如同分離組合的AController一樣,UE在World的層次上也采用了一個分離的AGameMode來抽離了游戲關卡邏輯,從而支持了邏輯的組合。本篇我們繼續上升一個層次,考慮在World之上,游戲還需要哪些邏輯控制?
暫時不考慮別的功能系統(如社交系統,統計等各種),單從游戲性來討論,現在閉上眼睛,想象我們已經藉着UE的偉力搭建了好了一個個LevelWorld,嗯,就像《西部世界》一樣,場景已經搭建好了,世界規則故事也編寫完善,現在需要干些什么?當然是開始派玩家進去玩啦!
大家都是老玩家了,想想我們之前玩的游戲類型:
- 玩家數目是單人還是多人
- 網絡環境是只本地還是聯網
- 窗口顯示模式是單屏還是分屏
- 輸入模式是共用設備還是分開控制(比如各有手柄)
- 也許還有別的不同
假如你是個開發游戲引擎的,會怎么支持這些不同的模式?以筆者見識過的大部分游戲引擎,解決這個問題的思路就是不解決,要嘛是限制功能,要嘛就是美名其曰讓開發者自己靈活控制。不過想了一下,這也不能怪他們,畢竟很少有引擎能像UE這樣歷史悠久同時又能得到足夠多的游戲磨練,才會有功夫在GamePlay框架上雕琢。大部分引擎還是更關注於實現各種絢麗的功能,至於怎么在上面開展游戲邏輯,那就是開發者自己的事了。一個引擎的功能是否強大,是基礎比拼指標;而GamePlay框架作為最高層直面用戶的對接接口,是一個引擎的臉面。所以有興趣游戲引擎研究的朋友們,區分一個引擎是否“優秀”,第二個指標是看它是否設計了一個優雅的游戲邏輯編寫框架,一般只有基礎功能已經做得差不多了的引擎開發者才會有精力去開發GamePlay框架,游戲引擎不止渲染!
言歸正傳,按照軟件工程的理念,沒有什么問題是不能通過加一個間接層解決的,不行就加兩層!所以既然我們在處理玩家模式的問題,理所當然的是加個間接層,將玩家這個概念抽象出來。
那么什么是玩家呢?狹義的講,玩家就是真實的你,和你身旁的小伙伴。廣義來說,按照圖靈測試理論,如果你無法分辨另一方是AI還是人,那他其實就跟玩家毫無區別,所以並不妨礙我們將網絡另一端的一條狗當作玩家。那么在游戲引擎看來,玩家就是輸入的發起者。游戲說白了,也只是接受輸入產生輸出的一個程序。所以有多少輸入,這些輸入歸多少組,就有多少個玩家。這里的輸入不止包括本地鍵盤手柄等輸入設備的按鍵,也包括網線里傳過來的信號,是廣義的該游戲能接受到的外界輸入。注意輸出並不是玩家的必要屬性,一個玩家並不一定需要游戲的輸出,想象你閉上眼睛玩馬里奧或者有個網絡連接不斷發送來控制信號但是從來不接收反饋,雖然看起來意義不大,但也確實不能說這就不是游戲。
在UE的眼里,玩家也是如此廣義的一個概念。本地的玩家是玩家,網絡聯機時雖然看不見對方,但是對方的網絡連接也可以看作是個玩家。當然的,本地玩家和網絡玩家畢竟還是差別很大,所以UE里也對二者進行了區分,才好更好的管理和應用到不同場景中去,比如網絡玩家就跟本地設備的輸入沒多大關系了嘛。
UPlayer
讓我們假裝自己是UE,開始編寫Player類吧。為了利用上UObject的那些現有特性,所以肯定是得從UObject繼承了。那能否是AActor呢?Actor是必須在World中才能存在的,而Player卻是比World更高一級的對象。玩游戲的過程中,LevelWorld在不停的切換,但是玩家的模式卻是脫離不變的。另外,Player也不需要被擺放在Level中,也不需要各種Component組裝,所以從AActor繼承並不合適。那還是保持簡單吧:
如圖可見,Player和一個PlayerController關聯起來,因此UE引擎就可以把輸入和PlayerController關聯起來,這也符合了前文說過的PlayerController接受玩家輸入的描述。因為不管是本地玩家還是遠程玩家,都是需要控制一個玩家Pawn的,所以自然也就需要為每個玩家分配一個PlayerController,所以把PlayerController放在UPlayer基類里是合理的。
ULocalPlayer
然后是本地玩家,從Player中派生下來LocalPlayer類。對本地環境中,一個本地玩家關聯着輸入,也一般需要關聯着輸出(無輸出的玩家畢竟還是非常少見)。玩家對象的上層就是引擎了,所以會在GameInstance里保存有LocalPlayer列表。
UE4里的ULocalPlayer也如圖所見,ULocalPlayer比UPlayer多了Viewport相關的配置(Viewport相關的內容在渲染章節講述),也終於用SpawnPlayerActor實現了創建出PlayerController的功能。GameInstance里有LocalPlayers的信息之后,就可以方便的遍歷訪問,來實現跟本地玩家相關操作。
關於游戲的詳細加載流程目前不多講述(按慣例在相應引擎流程章節講述),現在簡單了解一下LocalPlayer是怎么在游戲的引擎的各個環節發揮作用的。UE在初始化GameInstance的時候,會先默認創建出一個GameViewportClient,然后在內部再轉發到GameInstance的CreateLocalPlayer:
ULocalPlayer* UGameInstance::CreateLocalPlayer(int32 ControllerId, FString& OutError, bool bSpawnActor)
{
ULocalPlayer* NewPlayer = NULL;
int32 InsertIndex = INDEX_NONE;
const int32 MaxSplitscreenPlayers = (GetGameViewportClient() != NULL) ? GetGameViewportClient()->MaxSplitscreenPlayers : 1;
//已略去錯誤驗證代碼,MaxSplitscreenPlayers默認為4
NewPlayer = NewObject<ULocalPlayer>(GetEngine(), GetEngine()->LocalPlayerClass);
InsertIndex = AddLocalPlayer(NewPlayer, ControllerId);
if (bSpawnActor && InsertIndex != INDEX_NONE && GetWorld() != NULL)
{
if (GetWorld()->GetNetMode() != NM_Client)
{
// server; spawn a new PlayerController immediately
if (!NewPlayer->SpawnPlayActor("", OutError, GetWorld()))
{
RemoveLocalPlayer(NewPlayer);
NewPlayer = NULL;
}
}
else
{
// client; ask the server to let the new player join
NewPlayer->SendSplitJoin();
}
}
return NewPlayer;
}
可以看到,如果是在Server模式,會直接創建出ULocalPlayer,然后創建出相應的PlayerController。而如果是Client(比如Play的時候選擇NumberPlayer=2,則有一個為Client),則會先發送JoinSplit消息到服務器,在載入服務器上的Map之后,再為LocalPlayer創建出PlayerController。
而在每個PlayerController創建的過程中,在其內部會調用InitPlayerState:
void AController::InitPlayerState()
{
if ( GetNetMode() != NM_Client )
{
UWorld* const World = GetWorld();
const AGameModeBase* GameMode = World ? World->GetAuthGameMode() : NULL;
//已省略其他驗證和無關部分
if (GameMode != NULL)
{
FActorSpawnParameters SpawnInfo;
SpawnInfo.Owner = this;
SpawnInfo.Instigator = Instigator;
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
SpawnInfo.ObjectFlags |= RF_Transient; // We never want player states to save into a map
PlayerState = World->SpawnActor<APlayerState>(GameMode->PlayerStateClass, SpawnInfo );
// force a default player name if necessary
if (PlayerState && PlayerState->PlayerName.IsEmpty())
{
// don't call SetPlayerName() as that will broadcast entry messages but the GameMode hasn't had a chance
// to potentially apply a player/bot name yet
PlayerState->PlayerName = GameMode->DefaultPlayerName.ToString();
}
}
}
}
這樣LocalPlayer最終就和PlayerState對應了起來。而網絡聯機時其他玩家的PlayerState是通過Replicated過來的。
我們談了那么久的玩家就是輸入,體現在在每個PlayerController接受Player的時候:
void APlayerController::SetPlayer( UPlayer* InPlayer )
{
//[...]
// Set the viewport.
Player = InPlayer;
InPlayer->PlayerController = this;
// initializations only for local players
ULocalPlayer *LP = Cast<ULocalPlayer>(InPlayer);
if (LP != NULL)
{
// Clients need this marked as local (server already knew at construction time)
SetAsLocalPlayerController();
LP->InitOnlineSession();
InitInputSystem();
}
else
{
NetConnection = Cast<UNetConnection>(InPlayer);
if (NetConnection)
{
NetConnection->OwningActor = this;
}
}
UpdateStateInputComponents();
// notify script that we've been assigned a valid player
ReceivedPlayer();
}
可見,對於ULocalPlayer,APlayerController內部會開始InitInputSystem(),接着會創建相應的UPlayerInput,BuildInputStack等初始化出和Input相關的組件對象。現在先明白到LocalPlayer才是PlayerController產生的源頭,也因此才有了Input就夠了,特定的Input事件流程分析在后續章節再細述。
思考:為何不在LocalPlayer里編寫邏輯?
作為游戲開發者,相信大家都有這么個體會,往往在游戲邏輯代碼中總會有一個自己的Player類,里面放着這個玩家的相關數據和邏輯業務。可是在UE里為何就不見了這么個結構?也沒見UE在文檔里有描述推薦你怎么創建自己的Player。
這個可能有兩個原因,一是UE從FPS-Specify游戲起家,不像現在的各種手游有非常重的玩家系統,在UE的眼中,Level和World才是最應該關注的對象,因此UE的視角就在於怎么在Level中處理好Player的邏輯,而非在World之外的額外操作。二是因為在一個World中,上文提到其實已經有了Pawn-PlayerController和PlayerState的組合了,表示、邏輯和數據都齊備了,也就沒必要再在Level摻和進Player什么事了。當然你也可以理解為PlayerController就是Player在Level中的話事人。
凡事留一線,日后好相見。盡管如此,UE還是給了我們自定義ULocalPlayer子類的機會:
//class UEngine:
/** The class to use for local players. */
UPROPERTY()
TSubclassOf<class ULocalPlayer> LocalPlayerClass;
/** @todo document */
UPROPERTY(globalconfig, noclear, EditAnywhere, Category=DefaultClasses, meta=(MetaClass="LocalPlayer", DisplayName="Local Player Class"))
FStringClassReference LocalPlayerClassName;
你可以在配置中寫上LocalPlayer的子類名稱,讓UE為你生成你的子類。然后再在里面寫上一些特定玩家的數據和邏輯也未嘗不可,不過這部分額外擴展的功能就得用C++來實現了。
UNetConnection
非常耐人尋味的是,在UE里,一個網絡連接也是個Player:
包含Socket的IpConnection也是玩家,甚至對於一些平台的特定實現如OculusNet的連接也可以當作玩家,因為對於玩家,只要能提供輸入信號,就可以當作一個玩家。
追根溯源,UNetConnection的列表保存在UNetDriver,再到FWorldContext,最后也依然是UGameInstance,所以和LocalPlayer的列表一樣,是在World上層的對象。
本篇先前瞻一下結構,對於網絡部分不再細述。
總結
本篇我們抽象出了Player的概念,並依據使用場景派生出了LocalPlayer和NetConnection這兩個子類,從此Player就不再是一個虛無縹緲的概念,而是UE里的邏輯實體。UE可以根據生成的Player對象的數量和類型的不同,在此上實現出不同的玩家控制模式,LocalPlayer作為源頭Spawn出PlayerController繼而PlayerState就是實證之一。而在網絡聯機時,把一個網絡連接看作是一個玩家這個概念,把在World之上的輸入實體用Player統一了起來,從而可以實現出靈活的本地遠程不同玩家模式策略。
盡管如此,UPlayer卻像是深藏在UE里的幕后功臣,UE也並不推薦直接在Player里編程,而是利用Player作為源頭,來產生構建一系列相關的機制。但對於我們游戲開發者而言,知道並了解UE里的Player的概念,是把現實生活同游戲世界串聯起來的很重要的紐帶。我們在一個個World里向上仰望,還能清楚的看見一個個LocalPlayer或NetConnection仿佛在注視着這片大地,是他們為World注入了生機。
已經到頭了?並沒有,我們繼續向上逆風飛翔,終將得見游戲里的神:GameInstance。
引用
UE4.14
知乎專欄:InsideUE4
UE4深入學習QQ群: 456247757(非新手入門群,請先學習完官方文檔和視頻教程)
個人原創,未經授權,謝絕轉載!