<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ThinkChat2.0新版上線,更智能更精彩,支持會話、畫圖、視頻、閱讀、搜索等,送10W Token,即刻開啟你的AI之旅 廣告
                # 簡述 靜態結構分析中會對Shooter Game中涉及的代碼總體結構進行分析,實質上是解答了“Shooter Game 修改/擴展/實現了哪些功能的問題”。而“如何實現對應功能”的問題的答案在下一章動態結構分析中。 [TOC] # 總體結構 Shooter Game的代碼行數為26674行,其中純代碼行數為18715行。 工程中只有源代碼,沒有專門設計的用于定制引擎的插件。 # 模塊劃分 源代碼分為了兩個模塊:Shooter Game主模塊和Shooter Game Loading Screen模塊。 > 為什么要拆分兩個模塊? 將Loading Screen模塊單獨拆分的原因可以從uproject描述文件得知: ~~~ "FileVersion": 3, "EngineAssociation": "4.15", "Category": "Samples", "Description": "", "Modules": [ { "Name": "ShooterGame", "Type": "Runtime", "LoadingPhase": "Default" }, { "Name": "ShooterGameLoadingScreen", "Type": "Runtime", "LoadingPhase": "PreLoadingScreen" } ], ~~~ 注意觀察,兩個模塊的載入時機并不一致。Loading Screen模塊需要在載入畫面之前載入,才能修改載入畫面。 關于以下問題的回答,請直接閱讀《大象無形》一書中關于模塊機制的闡述: 1. 什么是模塊 2. 如何創建我自己的模塊 3. 模塊的加載 4. 如何引用其他模塊 # 主模塊 ## 文件夾結構的整理和劃分 Shooter Game對源文件通過文件夾按照功能進行了劃分。所以能夠很清晰地看出框架如下: * Bots: 機器人AI相關代碼 * Effects:特效相關代碼 * Online:聯機相關代碼 * Pickups:可拾取的對象相關代碼 * Player:Character、Controller和Movement等與玩家相關的代碼 * Sound :音效相關代碼 * UI:UI相關代碼 * Weapon:武器相關代碼 羅列模塊名的意義是讓手邊沒有源碼的讀者能夠從大體上了解Shooter Game從程序的角度如何劃分模塊,并能夠順暢閱讀接下來的詳細分析。 ## 玩家相關代碼 下文將會對玩家相關的代碼進行靜態的分析,主要目的是回答“這個類在做什么”,而不是回答“這個類如何完成具體實現”,希望讀者注意這一點。 如果讀者對虛幻引擎的玩家框架不是非常了解,此處給出一個簡單的解釋: ![](https://box.kancloud.cn/49aaf780bd47a3d5bef98fc344346012_243x461.png) PlayerController代表玩家輸入和操作的抽象類,而APawn代表被操縱的實體(Pawn的含義是兵卒或者國際象棋棋子)。 PlayerController繼承自Controller,Controller可以通過Possess函數操縱一個特定的Pawn,或者通過Unpossess函數脫離操縱某個Pawn。 這樣的設計允許玩家在不同的Pawn之間切換,例如在活著的時候操作一個第一人稱的Pawn,在死去后操作一個只能飛行的幽靈Pawn。 Shooter Game同樣遵循了這樣的設計范式。 ### Character 對虛幻引擎游戲性編程基本范式有一定了解的朋友,一定知道對玩家操控的人物對象的定義是通過繼承Character類實現。Shooter Game也不例外,其繼承了自己的Character類。 Character類的復雜度相當的高,故采用幾個層面進行解析: #### Character語義 從語義上說,一個Character應該是場景中的“角色”的**實體**描述。 1. 應當能夠系統完整地描述角色本身 2. 應當適度剝離與角色關系不大的屬性 #### Character數據結構 因此,Shooter Game的Character設計,包含了以下幾個基礎部分: 1. Character數值特性: 1. 生命值Health 2. 是否正在死亡bIsDying 2. Character表現屬性: 1. 死亡動畫 2. 死亡聲音 3. 重生特效 4. ... 3. Character狀態信息: 1. 是否正在瞄準bIsTargeting 2. 是否即將開始跑步:bWantsToRun 3. ... 4. 武器管理相關信息: 1. 武器數組Inventory 2. 當前武器CurrentWeapon #### Character方法(訪問器與工具函數) Character在這一套數據結構的基礎上,提供了一系列的方法函數。 由于方法函數頗多,此處不再羅列,在動態分析部分會選取部分進行講解。 #### 一些額外探討 由于Shooter Game本身的體積,導致其有必要控制代碼的復雜程度和抽象程度。在UDK時代的UT3中,采用了一個InventoryManager作為中間層,負責管理大量的武器、裝備和各種物品。 究竟哪一種范式更加正確,取決于項目本身的規模。 ### Player Controller Shooter Game繼承了自己的Player Controller 類。這個類同Character類一起,包含大量的服務端與客戶端的功能。每一個函數的上方注釋中,會給出[Client]或者[Server]這樣的內容,例如: ~~~ /** [server] spawns default inventory */ void SpawnDefaultInventory(); ~~~ 或是在命名中給出執行位置是在Server還是Client的信息,例如: ~~~ /** sets spectator location and rotation */ UFUNCTION(reliable, client) void ClientSetSpectatorCamera(FVector CameraLocation, FRotator CameraRotation); ~~~ 需要提醒讀者的是,虛幻引擎的Server/Client執行方式不是通過注釋指定的。而是通過UFUNCTION宏的標記來完成。 ### Cheat Manager 該類為虛幻引擎用于進行調試(作弊)的類,其基類為UCheatManager,根據官方注釋,該類不會在發布模式(Shipping)下被實例化: ~~~ /** Cheat Manager is a central blueprint to implement test and debug code and actions that are not to ship with the game. As the Cheat Manager is not instanced in shipping builds, it is for debugging purposes only */ class ENGINE_API UCheatManager : public UObject ~~~ 這個類的大量函數被標記為exec,從而支持從控制臺通過輸入命令來執行特定的調試函數,例如: ~~~ /** Pawn no longer collides with the world, and can fly */ UFUNCTION(exec,BlueprintCallable,Category="Cheat Manager") virtual void Ghost(); ~~~ 在控制臺中輸入Chost并回車,能夠關閉玩家和世界的碰撞。 ### Spectator Pawn Shooter Game繼承了這個類以提供“幽靈/觀察者模式”的功能。當Player Controller操縱這個類時,自動進入觀察者模式。 ### Shooter Persistent User Shooter Game為了演示持久化系統,所以提供了一個記錄當前使用者的成績記錄的功能。 ![](https://box.kancloud.cn/c941cc2a6aef4563bcf57c209bd12dba_223x314.png) 如圖所示,UShooterPersistentUser繼承自USaveGame。這個類用于進行數據持久化。 基礎的使用方式如下: 1. 繼承USaveGame創建自己的持久化數據類,在該類中定義需要的數據字段 2. 使用UGameplayStatics::CreateSaveGameObject 創建持久化數據類的實例。該函數需要傳入一個繼承自USaveGame的類作為模板參數。 3. 使用UGameplayStatics::SaveGameToSlot 保存數據,需要一個FString作為索引。 4. 使用UGameplayStatics::LoadGameFromSlot,以事前擬定的索引讀取數據信息 關于USaveGame的更多使用信息,請參考[官方關于SaveGame的文檔頁面](https://docs.unrealengine.com/latest/INT/Gameplay/SaveGame) Shooter Game通過UShooterPersistentUser類,記錄了當前使用者的擊殺數量等信息。然后通過ULocalPlayer::GetNickName作為索引來存儲。 ## 武器系統與可拾取物系統 武器系統包括了幾個核心類: ![](https://box.kancloud.cn/488e8d5c92ece454a541f22f8d39a51d_797x526.png) 該圖看上去十分復雜,請讀者千萬不要看著頭疼。下面我一部分一部分地解釋。 ### 武器系統的核心:AShooterWeapon > 該用什么樣的結構表示武器?是一個UObject的子類,還是一個AActor的子類,還是一個F開頭的純數據類? Epic采用了混合式的設計。首先有一個核心類AShooterWeapon,該類作為一個武器的實體表示。 在《大象無形》一書中,我曾經闡述過,繼承Actor的原因是需要掛載組件,而一個武器實體顯然需要至少一個StaticMeshComponent靜態網格物體組件。因此從語義上說,AShooterWeapon繼承自Actor是完全合理的。 > AShooterWeapon的繼承模式是如何推導出來的? 假如我們此時暫時不考慮代碼,我們不妨想象一下,AShooterWeapon中需要什么樣的成員變量以描述。至少我們應該能夠想到以下部分: 1. 武器自身的數據描述 1. 武器彈藥量 2. 武器單個彈夾數量 3. 武器開火速度 4. 等等... 2. 武器表現性數據描述 1. 武器開火動畫 2. 武器裝備動畫 3. 等等... 3. 一組工具函數 1. 裝備武器 2. 取消裝備武器 3. 開火 4. 等等.. 同時,在《重構》一書中,鼓勵對具有相近語義的一組數據使用數據結構進行封裝,因此Epic將我們前文提到的一些內容進行了有意義的單獨抽象。例如: * 將武器自身的靜態數據描述抽象為了一組單獨的內容,即FWeaponData類,包含最大彈藥量、單彈夾包含子彈數量等等不會隨著游戲過程而改變的信息。 * 將武器在第一人稱和第三人稱模式下的不同動畫,抽象為了一個FWeaponAnim的小數據結構 這就是前面UML圖中FWeaponData、FWeaponAnim兩個數據結構的來歷。 由于這兩個數據結構只是純數值類,因此使用USTRUCT宏進行描述,虛幻的反射系統會自動創建該數據結構的描述性信息,從而允許對這些數據進行序列化和反序列化以存儲和在網絡同步,同時也支持編輯器中生成相應的編輯控件。 ### 兩種不同的武器基類 游戲中出現了兩種具有明顯區別的武器: 1. 一種是開一槍立刻計算是否命中 2. 第二種是開一槍計算是否發射彈丸 故非常自然地,出現了名為AShooterWeapon_Instant和AShooterWeapon_Projectile兩個類。 沿襲剛才的分析,Epic將靜態的、不大會變化的數據,單獨抽象為一個類,即FProjectileWeaponData和FInstantWeaponData。故此處只分析相對復雜的AShooterWeapon_Projectile。 如果看過官方那個打黃豆的案例,應該能夠明白射彈(拋射物)是一個單獨的Actor。 此處同理,AShooterWeapon_Projectile從自己持有的FProjectileWeaponData實例中獲取對應的射彈類,然后生成對應實例。 前文提到,靜態數據單獨抽象的一大好處是配置和修改十分方便。在引擎中能夠看到這樣的界面: ![](https://box.kancloud.cn/b274f9bf66e76e528d11731cd7dc8651_411x425.png) 這里對應代碼如下: ~~~ /** weapon config */ UPROPERTY(EditDefaultsOnly, Category=Config) FInstantWeaponData InstantConfig; ... /** weapon data */ UPROPERTY(EditDefaultsOnly, Category=Config) FWeaponData WeaponConfig; ~~~ 以此提供給數值設計人員一個更加方便、統一的設計位置。并且借助EditDefaultsOnly來有效限制編輯范圍——只能編輯默認值,這也是防御性編程的一個良好范例。 此時再回顧開頭描述的那張UML類圖,是否能夠理清這里的類關系了? ### 可拾取物 實質上可拾取物可以看作一個這樣的Actor: 1. 數值 1. 重新刷新的時間 2. 行為 1. 被碰到后執行響應 3. 支持行為的表現數據 1. 重生特效、聲音 2. 拾取特效、聲音 Shooter Game同樣設計了這樣的邏輯。 ## 游戲邏輯與機制框架 Shooter Game的游戲邏輯與機制框架主要放置在Online文件夾,同時外部的零散文件中也包含了一部分內容。 ![](https://box.kancloud.cn/43a317b37baa045a38ccc534d59ecc32_625x905.png) 強烈建議讀者閱讀[InsideUE4-GamePlay架構](https://zhuanlan.zhihu.com/p/22813908)以清晰了解虛幻引擎部分的框架。此處只給出簡單解釋: * UGameEngine:引擎類 * UGameInstance:游戲單例類。為當前“游戲”的唯一代表,表示當前正在運行的游戲。 * 一個正在運行的游戲實例應當包含一個游戲世界,在進行世界切換時可以用過FWorldContext臨時持有一個以上 * UWorld:游戲世界類。表示現今正處于的游戲世界。請注意,傳統意義上的**切換地圖**實質上是切換游戲世界,即: >玩家在切換PersistentLevel的時候,實際上就相當于切換了一個World。 from《Inside UE4》 * ULevel:世界一部分關卡的**數據表示**,當前關卡中的Actor數據會被保存至Level中。對應了虛幻引擎中的.umap文件。一個UWorld可以持有多個ULevel。 * AGameMode:當前關卡的**行為規則描述**,負責描述游戲的基本邏輯 * AGameState:當前游戲狀態的**數據表示** Shooter Game在這個基礎框架上,主要做出了以下擴展: ### 自己的GameInstance 語義上說,Game Instance的子類負責管理**獨立于關卡**的邏輯。Epic給出的示范中,Shooter Game Instance通過重載Init、Shutdown、StartGameInstance三個函數,完成了對GameInstance的擴展。 擴展引擎的兩個基本思路: 1. 繼承基類,實現包含自己需要的邏輯的子類,然后向引用當前對象的其他類注冊自己。 1. 一般來說其他類對該基類通過TSubclassOf宏進行引用,然后在需要的時候反射生成實例 2. 通過向特定的靜態代理注冊函數,從而實現擴展 Shooter Game Instance采用了第二個思路。在Init函數中向一系列代理進行了注冊: ~~~ void UShooterGameInstance::Init() { Super::Init(); //...省略部分代碼 FCoreDelegates::ApplicationWillDeactivateDelegate.AddUObject(this, &UShooterGameInstance::HandleAppWillDeactivate); FCoreDelegates::ApplicationWillEnterBackgroundDelegate.AddUObject(this, &UShooterGameInstance::HandleAppSuspend); FCoreDelegates::ApplicationHasEnteredForegroundDelegate.AddUObject(this, &UShooterGameInstance::HandleAppResume); FCoreDelegates::OnSafeFrameChangedEvent.AddUObject(this, &UShooterGameInstance::HandleSafeFrameChanged); FCoreDelegates::OnControllerConnectionChange.AddUObject(this, &UShooterGameInstance::HandleControllerConnectionChange); FCoreDelegates::ApplicationLicenseChange.AddUObject(this, &UShooterGameInstance::HandleAppLicenseUpdate); FCoreUObjectDelegates::PreLoadMap.AddUObject(this, &UShooterGameInstance::OnPreLoadMap); FCoreUObjectDelegates::PostLoadMap.AddUObject(this, &UShooterGameInstance::OnPostLoadMap); FCoreUObjectDelegates::PostDemoPlay.AddUObject(this, &UShooterGameInstance::OnPostDemoPlay); } ~~~ 擴展的內容包括: * 通過重載FCoreUObjectDelegates::PostLoadMap,在地圖加載完畢后停止播放載入動畫 * 通過重載UShooterGameInstance::Tick,在使用XBox在分屏模式下游玩時,檢測是否存在控制器脫機的情況,如果存在則提示用戶 通過這兩個擴展案例,可以看出GameInstance擴展的類型:獨立于具體游戲實例的邏輯。甚至可以發現,即使是在菜單地圖中,這些邏輯依然是通用的。 故反向而言,如果有一部分游戲**功能**是完全獨立于特定的邏輯的,則抽象至GameInstance層進行處理。 ### Game Mode Shooter Game創建了兩個GameMode:AShooterGameMode和AShooterGameMode_TeamDeathMatch。 #### AShooterGameMode 作為基類的AShooterGameMode繼承自AGameMode,提供基礎性的功能,包括: 1. 開始游戲 2. 結束游戲 3. 玩家最佳出生點選擇 4. Bot機器人創建 5. 游戲計時 6. ... 通過觀察可以發現,AShooterGameMode和具體的地圖相關。換句話說,AShooterGameMode實際上對應了**比賽地圖**,而作為主菜單界面的地圖,采用的是AShooterGame_Menu,繼承的是AGameModeBase。 AGameMode和AGameModeBase的區別在于AGameMode包含聯機的邏輯。因此作為單機菜單地圖,只需要繼承AGameModeBase就足夠了。 此處可以印證,非全局性邏輯、與特定地圖相關的邏輯,放置于GameMode中。 #### AShooterGameMode_TeamDeathMatch 作為范例,TeamDeathMatch加入了隊伍選擇和判定哪一隊實現的功能。 ### Game State Game State 是游戲當前運行狀態的表示,其主要持有的是**數據**信息和**狀態**信息。 AShooterGameState 存儲了當前的隊伍數量、隊伍分數、剩余時間等信息。 Game Mode與Game State互相持有對方的引用,其區別是,Game State是持久性的數據信息,Game Mode是邏輯。在Shooter Game中,Game Mode 通過 ~~~ AShooterGameState* const MyGameState = Cast<AShooterGameState>(GameState); ~~~ 以獲得當前的游戲狀態。其中GameState是AGameModeBase類的成員變量,引用的是當前游戲狀態。 然后其操作GameState數據,以完成當前游戲狀態的更新。 Game Mode實質上定義了一個很類似于CS的DeathMatch框架。其預定義了一系列的狀態,位于MatchState命名空間中: ~~~ /** Possible state of the current match, where a match is all the gameplay that happens on a single map */ namespace MatchState { extern ENGINE_API const FName EnteringMap; // We are entering this map, actors are not yet ticking extern ENGINE_API const FName WaitingToStart; // Actors are ticking, but the match has not yet started extern ENGINE_API const FName InProgress; // Normal gameplay is occurring. Specific games will have their own state machine inside this state extern ENGINE_API const FName WaitingPostMatch; // Match has ended so we aren't accepting new players, but actors are still ticking extern ENGINE_API const FName LeavingMap; // We are transitioning out of the map to another location extern ENGINE_API const FName Aborted; // Match has failed due to network issues or other problems, cannot continue // If a game needs to add additional states, you may need to override HasMatchStarted and HasMatchEnded to deal with the new states // Do not add any states before WaitingToStart or after WaitingPostMatch } ~~~ 通過AGameMode::GetMatchState可以獲取當前的比賽狀態,通過AGameMode::SetMatchState可以設置當前的比賽狀態。 注意官方的提醒:如果需要擴展自定義的比賽狀態,需要重載HasMatchStarted和HasMatchEnded來處理新的狀態。MatchState并非一個Enum,同時狀態的轉換也是通過設置一個FName實現,因此可以直接添加一個新的FName即可表示。只是需要自己在狀態代碼中做出處理。 需要提醒的是,Unreal Engine的這個Game Mode設定過于傾向于一個聯機射擊類游戲,同時這個機制也有UT3時代的影響,因此如果覺得這部分沒有太大的必要,可以直接繼承自Game Mode Base。 ## 用戶界面與HUD 筆者并不趨向于討論Shooter Game的界面系統。 由于Shooter Game實際上開發于4.0時代,那個時代是沒有UMG系統的,所以個人認為參考意義并不是非常的大。 如果有朋友希望深入研究Slate界面系統,可以自行研究。 筆者推薦界面方案,根據需求規模,如果界面復雜度低則可以考慮更直觀的UMG解決方案,界面復雜度較高則可以考慮自行封裝庫或者HTML5方案。 * * * * * # Loading Screen 曾經嘗試自行實現載入畫面的朋友一定知道,嚴格來說UMG是無法作為載入畫面的。雖然有許多別的方案(創造一個中間關卡之類的),但是實際上并非專門的解決方案。 Shooter Game演示了一個解決方案,代碼位于ShooterGameLoadingScreenModule中,在PreLoadingScreen時加載。其中除了模塊定義類外,只包含了一個類:SShooterLoadingScreen2。 其中模塊定義類通過重載函數,將自定義的Slate加載畫面作為MoviePlayer,從而實現游戲加載和關卡切換時的載入畫面: ~~~ class FShooterGameLoadingScreenModule : public IShooterGameLoadingScreenModule { public: virtual void StartupModule() override { // Load for cooker reference LoadObject<UObject>(NULL, TEXT("/Game/UI/Menu/LoadingScreen.LoadingScreen") ); if (IsMoviePlayerEnabled()) { FLoadingScreenAttributes LoadingScreen; LoadingScreen.bAutoCompleteWhenLoadingCompletes = true; LoadingScreen.MoviePaths.Add(TEXT("LoadingScreen")); GetMoviePlayer()->SetupLoadingScreen(LoadingScreen); } } virtual bool IsGameModule() const override { return true; } virtual void StartInGameLoadingScreen() override { FLoadingScreenAttributes LoadingScreen; LoadingScreen.bAutoCompleteWhenLoadingCompletes = true; LoadingScreen.WidgetLoadingScreen = SNew(SShooterLoadingScreen2); GetMoviePlayer()->SetupLoadingScreen(LoadingScreen); } }; ~~~ 這里可以看作是一個極簡的載入畫面的寫法。且這個框架實際上比官方wiki中的另一個LoadingScreen插件更加簡潔明了。有需要的讀者可以參考這個模塊。 請注意,**該模塊的載入時機必須是PreLoadingScreen**
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看