你們對力量一無所知

引言

回顧上文,我們談完了World和Level級(jí)別的邏輯操縱控制,如同分離組合的AController一樣,UE在World的層次上也采用了一個(gè)分離的AGameMode來抽離了游戲關(guān)卡邏輯,從而支持了邏輯的組合。本篇我們繼續(xù)上升一個(gè)層次,考慮在World之上,游戲還需要哪些邏輯控制?
暫時(shí)不考慮別的功能系統(tǒng)(如社交系統(tǒng),統(tǒng)計(jì)等各種),單從游戲性來討論,現(xiàn)在閉上眼睛,想象我們已經(jīng)藉著UE的偉力搭建了好了一個(gè)個(gè)LevelWorld,嗯,就像《西部世界》一樣,場景已經(jīng)搭建好了,世界規(guī)則故事也編寫完善,現(xiàn)在需要干些什么?當(dāng)然是開始派玩家進(jìn)去玩啦!
大家都是老玩家了,想想我們之前玩的游戲類型:

  • 玩家數(shù)目是單人還是多人
  • 網(wǎng)絡(luò)環(huán)境是只本地還是聯(lián)網(wǎng)
  • 窗口顯示模式是單屏還是分屏
  • 輸入模式是共用設(shè)備還是分開控制(比如各有手柄)
  • 也許還有別的不同

假如你是個(gè)開發(fā)游戲引擎的,會(huì)怎么支持這些不同的模式?以筆者見識(shí)過的大部分游戲引擎,解決這個(gè)問題的思路就是不解決,要嘛是限制功能,要嘛就是美名其曰讓開發(fā)者自己靈活控制。不過想了一下,這也不能怪他們,畢竟很少有引擎能像UE這樣歷史悠久同時(shí)又能得到足夠多的游戲磨練,才會(huì)有功夫在GamePlay框架上雕琢。大部分引擎還是更關(guān)注于實(shí)現(xiàn)各種絢麗的功能,至于怎么在上面開展游戲邏輯,那就是開發(fā)者自己的事了。一個(gè)引擎的功能是否強(qiáng)大,是基礎(chǔ)比拼指標(biāo);而GamePlay框架作為最高層直面用戶的對接接口,是一個(gè)引擎的臉面。所以有興趣游戲引擎研究的朋友們,區(qū)分一個(gè)引擎是否“優(yōu)秀”,第二個(gè)指標(biāo)是看它是否設(shè)計(jì)了一個(gè)優(yōu)雅的游戲邏輯編寫框架,一般只有基礎(chǔ)功能已經(jīng)做得差不多了的引擎開發(fā)者才會(huì)有精力去開發(fā)GamePlay框架,游戲引擎不止渲染!
言歸正傳,按照軟件工程的理念,沒有什么問題是不能通過加一個(gè)間接層解決的,不行就加兩層!所以既然我們在處理玩家模式的問題,理所當(dāng)然的是加個(gè)間接層,將玩家這個(gè)概念抽象出來。
那么什么是玩家呢?狹義的講,玩家就是真實(shí)的你,和你身旁的小伙伴。廣義來說,按照圖靈測試?yán)碚?,如果你無法分辨另一方是AI還是人,那他其實(shí)就跟玩家毫無區(qū)別,所以并不妨礙我們將網(wǎng)絡(luò)另一端的一條狗當(dāng)作玩家。那么在游戲引擎看來,玩家就是輸入的發(fā)起者。游戲說白了,也只是接受輸入產(chǎn)生輸出的一個(gè)程序。所以有多少輸入,這些輸入歸多少組,就有多少個(gè)玩家。這里的輸入不止包括本地鍵盤手柄等輸入設(shè)備的按鍵,也包括網(wǎng)線里傳過來的信號(hào),是廣義的該游戲能接受到的外界輸入。注意輸出并不是玩家的必要屬性,一個(gè)玩家并不一定需要游戲的輸出,想象你閉上眼睛玩馬里奧或者有個(gè)網(wǎng)絡(luò)連接不斷發(fā)送來控制信號(hào)但是從來不接收反饋,雖然看起來意義不大,但也確實(shí)不能說這就不是游戲。
在UE的眼里,玩家也是如此廣義的一個(gè)概念。本地的玩家是玩家,網(wǎng)絡(luò)聯(lián)機(jī)時(shí)雖然看不見對方,但是對方的網(wǎng)絡(luò)連接也可以看作是個(gè)玩家。當(dāng)然的,本地玩家和網(wǎng)絡(luò)玩家畢竟還是差別很大,所以UE里也對二者進(jìn)行了區(qū)分,才好更好的管理和應(yīng)用到不同場景中去,比如網(wǎng)絡(luò)玩家就跟本地設(shè)備的輸入沒多大關(guān)系了嘛。

UPlayer

讓我們假裝自己是UE,開始編寫Player類吧。為了利用上UObject的那些現(xiàn)有特性,所以肯定是得從UObject繼承了。那能否是AActor呢?Actor是必須在World中才能存在的,而Player卻是比World更高一級(jí)的對象。玩游戲的過程中,LevelWorld在不停的切換,但是玩家的模式卻是脫離不變的。另外,Player也不需要被擺放在Level中,也不需要各種Component組裝,所以從AActor繼承并不合適。那還是保持簡單吧:

如圖可見,Player和一個(gè)PlayerController關(guān)聯(lián)起來,因此UE引擎就可以把輸入和PlayerController關(guān)聯(lián)起來,這也符合了前文說過的PlayerController接受玩家輸入的描述。因?yàn)椴还苁潜镜赝婕疫€是遠(yuǎn)程玩家,都是需要控制一個(gè)玩家Pawn的,所以自然也就需要為每個(gè)玩家分配一個(gè)PlayerController,所以把PlayerController放在UPlayer基類里是合理的。

ULocalPlayer

然后是本地玩家,從Player中派生下來LocalPlayer類。對本地環(huán)境中,一個(gè)本地玩家關(guān)聯(lián)著輸入,也一般需要關(guān)聯(lián)著輸出(無輸出的玩家畢竟還是非常少見)。玩家對象的上層就是引擎了,所以會(huì)在GameInstance里保存有LocalPlayer列表。

UE4里的ULocalPlayer也如圖所見,ULocalPlayer比UPlayer多了Viewport相關(guān)的配置(Viewport相關(guān)的內(nèi)容在渲染章節(jié)講述),也終于用SpawnPlayerActor實(shí)現(xiàn)了創(chuàng)建出PlayerController的功能。GameInstance里有LocalPlayers的信息之后,就可以方便的遍歷訪問,來實(shí)現(xiàn)跟本地玩家相關(guān)操作。
關(guān)于游戲的詳細(xì)加載流程目前不多講述(按慣例在相應(yīng)引擎流程章節(jié)講述),現(xiàn)在簡單了解一下LocalPlayer是怎么在游戲的引擎的各個(gè)環(huán)節(jié)發(fā)揮作用的。UE在初始化GameInstance的時(shí)候,會(huì)先默認(rèn)創(chuàng)建出一個(gè)GameViewportClient,然后在內(nèi)部再轉(zhuǎn)發(fā)到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; //已略去錯(cuò)誤驗(yàn)證代碼,MaxSplitscreenPlayers默認(rèn)為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模式,會(huì)直接創(chuàng)建出ULocalPlayer,然后創(chuàng)建出相應(yīng)的PlayerController。而如果是Client(比如Play的時(shí)候選擇NumberPlayer=2,則有一個(gè)為Client),則會(huì)先發(fā)送JoinSplit消息到服務(wù)器,在載入服務(wù)器上的Map之后,再為LocalPlayer創(chuàng)建出PlayerController。
而在每個(gè)PlayerController創(chuàng)建的過程中,在其內(nèi)部會(huì)調(diào)用InitPlayerState:

void AController::InitPlayerState()
{ if ( GetNetMode() != NM_Client )
    {
        UWorld* const World = GetWorld(); const AGameModeBase* GameMode = World ? World->GetAuthGameMode() : NULL; //已省略其他驗(yàn)證和無關(guān)部分 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對應(yīng)了起來。而網(wǎng)絡(luò)聯(lián)機(jī)時(shí)其他玩家的PlayerState是通過Replicated過來的。
我們談了那么久的玩家就是輸入,體現(xiàn)在在每個(gè)PlayerController接受Player的時(shí)候:

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內(nèi)部會(huì)開始InitInputSystem(),接著會(huì)創(chuàng)建相應(yīng)的UPlayerInput,BuildInputStack等初始化出和Input相關(guān)的組件對象?,F(xiàn)在先明白到LocalPlayer才是PlayerController產(chǎn)生的源頭,也因此才有了Input就夠了,特定的Input事件流程分析在后續(xù)章節(jié)再細(xì)述。

思考:為何不在LocalPlayer里編寫邏輯?
作為游戲開發(fā)者,相信大家都有這么個(gè)體會(huì),往往在游戲邏輯代碼中總會(huì)有一個(gè)自己的Player類,里面放著這個(gè)玩家的相關(guān)數(shù)據(jù)和邏輯業(yè)務(wù)??墒窃赨E里為何就不見了這么個(gè)結(jié)構(gòu)?也沒見UE在文檔里有描述推薦你怎么創(chuàng)建自己的Player。
這個(gè)可能有兩個(gè)原因,一是UE從FPS-Specify游戲起家,不像現(xiàn)在的各種手游有非常重的玩家系統(tǒng),在UE的眼中,Level和World才是最應(yīng)該關(guān)注的對象,因此UE的視角就在于怎么在Level中處理好Player的邏輯,而非在World之外的額外操作。二是因?yàn)樵谝粋€(gè)World中,上文提到其實(shí)已經(jīng)有了Pawn-PlayerController和PlayerState的組合了,表示、邏輯和數(shù)據(jù)都齊備了,也就沒必要再在Level摻和進(jìn)Player什么事了。當(dāng)然你也可以理解為PlayerController就是Player在Level中的話事人。
凡事留一線,日后好相見。盡管如此,UE還是給了我們自定義ULocalPlayer子類的機(jī)會(huì):

//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為你生成你的子類。然后再在里面寫上一些特定玩家的數(shù)據(jù)和邏輯也未嘗不可,不過這部分額外擴(kuò)展的功能就得用C++來實(shí)現(xiàn)了。

UNetConnection

非常耐人尋味的是,在UE里,一個(gè)網(wǎng)絡(luò)連接也是個(gè)Player:

包含Socket的IpConnection也是玩家,甚至對于一些平臺(tái)的特定實(shí)現(xiàn)如OculusNet的連接也可以當(dāng)作玩家,因?yàn)閷τ谕婕?,只要能提供輸入信?hào),就可以當(dāng)作一個(gè)玩家。
追根溯源,UNetConnection的列表保存在UNetDriver,再到FWorldContext,最后也依然是UGameInstance,所以和LocalPlayer的列表一樣,是在World上層的對象。
本篇先前瞻一下結(jié)構(gòu),對于網(wǎng)絡(luò)部分不再細(xì)述。

總結(jié)

本篇我們抽象出了Player的概念,并依據(jù)使用場景派生出了LocalPlayer和NetConnection這兩個(gè)子類,從此Player就不再是一個(gè)虛無縹緲的概念,而是UE里的邏輯實(shí)體。UE可以根據(jù)生成的Player對象的數(shù)量和類型的不同,在此上實(shí)現(xiàn)出不同的玩家控制模式,LocalPlayer作為源頭Spawn出PlayerController繼而PlayerState就是實(shí)證之一。而在網(wǎng)絡(luò)聯(lián)機(jī)時(shí),把一個(gè)網(wǎng)絡(luò)連接看作是一個(gè)玩家這個(gè)概念,把在World之上的輸入實(shí)體用Player統(tǒng)一了起來,從而可以實(shí)現(xiàn)出靈活的本地遠(yuǎn)程不同玩家模式策略。
盡管如此,UPlayer卻像是深藏在UE里的幕后功臣,UE也并不推薦直接在Player里編程,而是利用Player作為源頭,來產(chǎn)生構(gòu)建一系列相關(guān)的機(jī)制。但對于我們游戲開發(fā)者而言,知道并了解UE里的Player的概念,是把現(xiàn)實(shí)生活同游戲世界串聯(lián)起來的很重要的紐帶。我們在一個(gè)個(gè)World里向上仰望,還能清楚的看見一個(gè)個(gè)LocalPlayer或NetConnection仿佛在注視著這片大地,是他們?yōu)閃orld注入了生機(jī)。
已經(jīng)到頭了?并沒有,我們繼續(xù)向上逆風(fēng)飛翔,終將得見游戲里的神:GameInstance。