<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>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                [TOC] # 武器系統 ## 功能分析 Shooter Game的武器系統,提供的一套FPS武器的基本功能,列表如下: 1. 裝備/取消裝備武器 2. 開火 3. 上子彈 4. 拾取子彈 而一個武器的基本狀態包括如下: ![](https://box.kancloud.cn/d3db9c67bcee1722fca95013d6b00c44_603x372.png) ## 功能總體設計 請注意,這僅僅只是一個“示意圖”。ShooterGame的武器狀態挺復雜,所以并沒有采用標準的狀態機模式,而是使用了一組狀態變量(bPendingReload、bWantsToFire等)和一個專用的狀態轉換決策函數DetermineWeaponState共同實現。 Shooter Game的武器狀態聲明如下: ~~~ namespace EWeaponState { enum Type { Idle, Firing, Reloading, Equipping, }; } ~~~ 狀態轉換函數DetermineWeaponState如下: ~~~ void AShooterWeapon::DetermineWeaponState() { EWeaponState::Type NewState = EWeaponState::Idle; if (bIsEquipped) { if( bPendingReload ) { if( CanReload() == false ) { NewState = CurrentState; } else { NewState = EWeaponState::Reloading; } } else if ( (bPendingReload == false ) && ( bWantsToFire == true ) && ( CanFire() == true )) { NewState = EWeaponState::Firing; } } else if (bPendingEquip) { NewState = EWeaponState::Equipping; } SetWeaponState(NewState); } ~~~ 這并不符合標準的狀態機模式實現,可能是Shooter Game出于復雜度的考慮。以及筆者也嘗試過整理狀態轉換,但是發現轉換太過復雜,相比之下Shooter Game的實現倒是很簡潔。另外,UT3也不是采用標準狀態機模式來實現的。 武器系統的基本邏輯實際上很好表述: * 一般有一組進入狀態和退出狀態函數,如:StartFire和StopFire;StartReload和StopReload。 1. 由于狀態是持續性的過程,而函數是瞬間完成的調用,因此勢必需要三個元素:開始、持續過程更新函數、結束。 2. 以最重要的開火函數為例,其實際上走了這樣的調用: 1. StartFire實質上是一個“請求開火”的函數,其設置bWantsToFire為真后,交給DetermineWeaponState決定武器真正的狀態。**請求開火不一定能夠開火成功** 2. DetermineWeaponState函數決定了真正狀態后,會通過SetWeaponState函數,真正設置武器狀態 3. 在設置時檢測,如果前一狀態為武器尚未開火,現在狀態為開火,則調用OnBurstStarted函數 1. OnBrustStarted函數根據武器開火間隔,設置一個計時器,定時調用HandleFiring函數。這就是持續更新函數案例。 2. HandleFiring函數真正進行武器開火操作,包括: * 產生開火特效 * 調用FireWeapon函數進行武器開火計算 * 減少彈藥 * 決定是否需要重新裝彈 4. 當StopFire被調用時,設置bWantsToFire為假,交給DetermineWeaponState決定武器真正的狀態。 * 狀態轉換的條件均為數值屬性,能夠持久存儲。這樣設計的原因將會在下文闡述網絡同步時進行解釋。 * 子類通過擴展FireWeapon函數以具體處理武器開火的核心計算 ## 武器系統簡單總結 由于下文分析網絡同時,會再次分析武器系統,故此處只是簡單介紹了武器的整體邏輯。 >Shooter Game告訴我們,FPS類型的武器系統設計可以考慮一個輕量級的狀態機實現,狀態機的條件變量以布爾存儲,然后用一個公用的狀態切換函數在狀態間進行切換。 # 虛幻引擎網絡同步模型簡述 虛幻引擎的網絡同步模式相對來說比較好理解,此處簡單敘述。如果讀者朋友有興趣,可以參考官方文檔:[虛幻引擎網絡聯機與同步文檔](https://docs.unrealengine.com/latest/CHN/Gameplay/Networking/index.html) 1. 服務端客戶端模式: 1. 非P2P聯機,在網絡中存在一個服務端以及若干個客戶端。 2. 服務端為權威,客戶端是服務端的拙劣模仿 3. 服務端可以是一個普通的游戲實例,也可以是一個純命令行的游戲進程(dedicated Server) 2. 客戶端和服務端使用RPC進行消息傳遞,使用狀態拷貝完成同步 1. 客戶端和服務端之間可以互相發送RPC消息 * RPC:遠程過程調用。允許像調用一個普通函數一樣,在本地主機調用位于遠程主機上的某個函數。 2. 服務端權威,客戶端只能從服務端獲取對象狀態并更新本地狀態 1. 服務端的對象如果成員變量有所變化,差異的部分將會被傳輸到各個客戶端,更新客戶端狀態 3. 客戶端是服務端的拙劣模仿 1. 客戶端始終將輸入發送到服務端,以更新服務端狀態 2. 服務端將當前狀態同步到客戶端 3. 客戶端根據服務端狀態同步內容,**選用有效的方式修正以向服務端此時可能的狀態靠近** # Shooter Game的網絡同步設計 回顧前一節,分析Shooter Game的網絡同步設計的核心在于以下問題: 1. 假定存在一個客戶端主機和一個客戶端主機 2. 客戶端操作如何通過RPC調用通知服務端 3. 服務端如何響應當前操作 4. 服務端如何同步狀態到客戶端 而虛幻引擎推薦的網絡設計流程是: 1. 先考慮在本地的邏輯(單機模式下的邏輯) 2. 再考慮跨越網絡的數據同步 接下來我也將按照這樣的范式進行分析。 ## 人物網絡同步設計 ### 跑步 #### 邏輯 如果我們不考慮網絡同步,則邏輯如下圖,非常簡單明了 ![](https://box.kancloud.cn/9c1790f1d81211a136efd7ab04aa40f4_847x515.png) 人物跑步實際上通過兩個部分進行處理: 1. 玩家按下Shift鍵時,設置布爾值bWantsToRun為真 2. 在需要獲取跑步速度時,由繼承自UMovementComponent的UShooterCharacterMovement從Character調用IsRunning,實質上是根據之前設置的布爾值,獲得當前是否應當跑動的信息,然后更新屬于Movement組件的MaxSpeed。 但是這個過程中我們需要注意以下幾個問題: 1. Character的狀態被記錄了: 1. 玩家是否跑步的狀態被布爾值記錄 2. 玩家對Character的操作是對狀態的改變。 有些朋友會詢問,那這不是廢話嗎?我們不妨考慮另一種設計: * * * * * 1. 玩家按下Shift,將當前速度乘以二 2. 玩家松開Shift,將當前速度除以二 * * * * * 請回憶前文中對同步系統的描述:虛幻引擎需要同步狀態,因此我們不能夠采用上文所述的設計。 那么,在有網絡同步的狀態下,情況會變成什么樣子呢?應該按照什么樣的方式將邏輯更新? 簡單來說,就是: 1. 通過RPC在服務器端改變狀態 2. 通過復制機制,將服務端狀態復制到本地 回顧跑步本地圖,則步驟1是指“更新bWantsToRun”這個函數調用,即SetRunning函數;步驟2則不需要任何操作,虛幻引擎將會直接幫助你完成。新的圖如下: ![](https://box.kancloud.cn/aaf3a409148e8b288fb0b8e0dd8f45c7_1866x723.png) 此圖中的重點在于: 1. 當客戶端的Character收到Shift鍵按下的消息時,不僅更新了本地的bWantsToRun這些布爾值,還通過RPC請求,向服務端要求更新這幾個布爾值。圖中有一段網絡時延,就是為了強調RPC調用中的數據延遲。 2. 在接下來的數據同步時機到來時,服務端(UShooterCharacter_server)會將這個布爾值復制到除了源客戶端的Character以外的其他幾個客戶端的Character身上。 3. 于是在接下來的更新過程中,每個客戶端看到的源客戶端的Character,其移動速度都是一致的 從實際角度來說,有一個更容易想到的思維模式,即源客戶端不再本地直接更新bWantsToRun,而是直接通過RPC請求服務端更新,等待服務端復制狀態到本地。過程如下圖: ![](https://box.kancloud.cn/0524db12810da94586c92a687938c7b8_1866x698.png) 這兩種有何不同呢?為何Shooter Game采用了前一張圖而不是后一張圖的模式? 原因很簡單, 注意消息編號。前一張圖的模式中,3號消息更新完畢后,本地的速度已經得到了更新;而后一張圖中,必須要等到5號消息完成時,本地的速度才能得到更新。故前者在高延遲狀況下,本地的體驗會更好(玩家一按跑步就已經跑了起來)。 #### 實現 那么接下來我們要分析的是,前文所述的機制,Shooter Game 是如何實現的。 實際上虛幻引擎的網絡同步框架將會幫我們完成絕大多數的工作。 1. 首先我們需要標記需要進行同步的狀態變量: UPROPERTY的Replicated標記表示當前變量將會被從服務端拷貝到本地端 ~~~ /** current running state */ UPROPERTY(Transient, Replicated) uint8 bWantsToRun : 1; ~~~ 2. 接下來我們需要設定變量的拷貝方式: 我們需要重載GetLifetimeReplicatedProps函數,然后在其中通過DOREPLIFETIME和DOREPLIFETIME_CONDITION宏,設置當前變量的拷貝模式。對于當前變量,我們只需要拷貝到其他除了源客戶端外的客戶端Character中。 ~~~ void AShooterCharacter::GetLifetimeReplicatedProps(TArray< FLifetimeProperty > & OutLifetimeProps) const { Super::GetLifetimeReplicatedProps(OutLifetimeProps); //... DOREPLIFETIME_CONDITION(AShooterCharacter, bWantsToRun, COND_SkipOwner); //... } ~~~ 此處的DOREPLIFETIME_CONDITION即表示條件拷貝,而 COND_SkipOwner表示除了主人以外的客戶端均拷貝。Owner的概念在這里指的是“主人”,想象一場CS,除了當前操控的那個人物,我們具有話語權以外,其他的人物實際上都是布偶,是從服務端那里獲取狀態,然后在本地演戲陪你的布偶,我們不具有對他們的控制權。 通過這兩個步驟,bWantsToRun變量在服務端更新后,將會拷貝到源客戶端以外的其他客戶端。 隨后我們需要設置RPC函數,以完成前文圖中的客戶端向服務端更新的過程。具體方式如下: 1. 標記函數: ~~~ UFUNCTION(reliable, server, WithValidation) void ServerSetRunning(bool bNewRunning, bool bToggle); ~~~ 當前函數被標記:在服務端調用、可靠、帶有驗證。 被這樣標記的函數,必須要提供兩個函數:驗證函數ServerSetRunning_Validate,實現函數ServerSetRunning_Implementation。真正的函數體將會由虛幻引擎自動進行生成。 驗證函數返回值將會決定這次RPC調用的結果是否被采用,此處可以做各種驗證以避免玩家作弊; 實現函數用于在服務端做出具體的工作 ~~~ bool AShooterCharacter::ServerSetRunning_Validate(bool bNewRunning, bool bToggle) { return true; } void AShooterCharacter::ServerSetRunning_Implementation(bool bNewRunning, bool bToggle) { SetRunning(bNewRunning, bToggle); } ~~~ 2. 調用函數: 當玩家按下Shift鍵開始奔跑時,首先在本地設置狀態變量(3號過程),然后通過RPC向服務端請求狀態更新(4號過程),具體函數如下: ~~~ void AShooterCharacter::SetRunning(bool bNewRunning, bool bToggle) { bWantsToRun = bNewRunning; bWantsToRunToggled = bNewRunning && bToggle; if (Role < ROLE_Authority) //是否在服務端的狀態判斷 { ServerSetRunning(bNewRunning, bToggle); } } ~~~ 此處判斷是否在服務端的代碼非常有意思,方法是判斷Role是否小于ROLE_Authority。關于Role的更多信息,請查看文檔[Actor的Roles](https://docs.unrealengine.com/latest/CHN/Gameplay/Networking/Actors/Roles/index.html)。 簡單而言,Role是判斷“復制與同步”的。一個對象擁有Role和RemoteRole,分別表示本地和遠端。如果當前Actor的Role是ROLE_Authority,表示當前Actor處于服務端,是“權威”,是各地客戶端的“木偶”們模仿的對象,會不斷將自身狀態同步到各個客戶端。而此時當前Actor的RemoteRole將會是ROLE_SimulatedProxy或者ROLE_AutonomousProxy,表示遠端的Actor是通過模擬來模仿當前Actor的狀態。 這是因為虛幻引擎并非簡單進行狀態復制,其同樣擁有插值和預測機制,本地端的木偶們在獲得服務端的權威數據后,會試圖**猜測**服務端狀態,并**修正**自身的狀態向服務端靠攏。 對于這些機制的探討已經超過了本文的范圍,有興趣的讀者可以進一步閱讀分析源碼。 概略性地總結以下,對于像跑步這樣的需要立刻反饋的操作,Shooter Game推薦的思路是: 1. 本地端首先更新狀態 2. 通過RPC請求服務端更新狀態 3. 服務端向非源客戶端的其他客戶端復制狀態
                  <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>

                              哎呀哎呀视频在线观看