Multiplayer Shooting Game
GASP+Lyra Animation
視頻
Project Mega Sample (5.5)
Alternate Link
新 多人游戲插件
-
Step1:下載多人游戲模板并將其中的兩個文件托到項目Content文件夾下
下載多人游戲模板:
https://drive.google.com/file/d/12j9EUjPwGQ3hJMCIdi6dQMhLRBRm23ik/view
多人游戲模板講解:
https://www.youtube.com/watch?v=Iefxj6tgDgI&t=258s -
Step2:根據教程下載插件連接Steam
連接Steam教程:
https://www.youtube.com/watch?v=r3UWKE4x-6o
Steam插件下載地址:
https://vreue4.com/advanced-sessions-binaries
將教程中的內容替換為下面代碼:
[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"
- Step3:配置一些屬性
在Project中設置 Map 和 Game Instance Class:

設置List Of Map:

修改模板中的GameMode和PlayerController:

將父類改為自己寫的C++類:


按下面圖片更改Map的GameMode:

攀爬系統
下載地址+使用文檔:
https://drive.google.com/file/d/1GQg4gi1L4m3DTJd-Bvo4qbmJ3E4y9qdo/view
Replicate 和 RPC 和 OnRep
在服務器執行:
1.服務器執行函數 并 在函數中改變復制變量
2.觸發各個客戶端的OnRep_函數
3.讓所有客戶端正確表現服務器裝備行為
在客戶端執行:
1.客戶端A執行PRC發請求給服務器
2.服務器執行函數 并 在函數中改變復制變量(收到這個更新后,所有客戶端(包含 A 自己)都會觸發 OnRep_函數)
3.觸發各個客戶端的OnRep_函數(包含 A 自己)
4.讓 所有客戶端 和 客戶端A 正確表現 客戶端A 的行為
// 如果不希望客戶端A在 RepNotify 里做某些處理,可以在回調里加一個判斷:
void UCombatComponent::OnRep_EquippedWeapon()
{
// 只讓遠端客戶端執行
if (!ShootCharacter->IsLocallyControlled())
{
}
}
Launch game in settings

添加多人游戲
設置游戲人數:

選擇網絡模式:

Play As Listen Server:其中一臺有人游玩的機器充當服務器,需要圖形渲染
Play As Client:指定一臺機器作為服務器,沒有人實際在這臺機器上游玩游戲,無需圖形渲染(大型多人游戲)
配置Project連接到Steam
啟用Steam插件:


在DefaultEngine.ini中添加代碼:



[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

添加完成后關閉UE和VS并刪除指定文件重構代碼:


獲取在線子系統(OnlineSubsystem)并打印該名稱“Steam”:

遇到該錯誤需要重構代碼:

在編輯器中運行時,連接到的是名為“null”的子系統,只有打包后的項目才能連接到Steam在線子系統
選擇多人模式并Package項目才能生效:


Create Game Session
委托(delegate):可以綁定回調函數(CallbackFuction),當游戲中某個特定事件發生時調用該函數

在線會話接口(Online Session Interface):使用委托來處理創建和加入游戲會話時需要的信息傳輸
步驟:

1.定義:
會話接口(OnlineSessionInterface)、
創建會話的函數(CreateGameSession( ))、
委托變量(CreateSessionCompleteDelegate)、
回調函數(OnCreateSessionComplete(FName SessionName,bool bWasSuccessful))


2.綁定回調函數到委托變量上:OnCreateSessionComplete()

3.將委托添加到會話接口(Session Interface)的委托列表(Delegate List)中,使得當會話接口被創建時調用委托的CallbackFunction
4.會話接口(Session Interface)調用創建會話函數(CreateSession())來連接Steam并創建游戲會話

5.當會話創建完成時,Steam將會把信息發送回來,回調函數(callbackFuction)被觸發,并通過該函數打印出調試信息以驗證會話已成功創建


Find Game Session


需要添加頭文件才能使用:SEARCE_PRESENCE




Join Game Session
創建一個大廳(Lobby Level):

創建Join Session的委托和回調函數:

初始化委托:

添加SessionSetting的Set函數:


Create Session時,改變服務器地圖:

Find session的回調函數:

Join session的回調函數:

需要保證Steam在同一下載地區:

Create a Plugin
創建多人游戲插件:



啟用在線子系統插件:

添加在線子系統模塊名稱:

Create our own subsystem
GameInstance和GameInstanceSubsystem

創建GameInstanceSubsystem:


在子系統中
1.添加公共函數以處理會話創建、查找、加入、銷毀和啟動等操作
2.添加委托變量
3.創建對應的回調函數
4.創建委托句柄以存儲對每個委托的引用,用于從會話接口中移除不再需要的委托


Create Widget
創建C++Widget類:





創建Widget藍圖類:



初始化Menu類并添加按鈕回調函數:




添加刪除Widget函數,并在NativeDestruct函數中調用:
添加NumPublicConnections和MatchType:






添加自定義委托
創建委托和回調函數并將回調函數綁定到委托上:





如果創建失敗,則清除委托并廣播自定義委托為false;如果成功,則在回調函數中清除委托并廣播自定義委托為true


將HostButtonClicked函數中的ServerTravel函數放到自定義委托的回調函數中,若創建成功則前往大廳:

添加更多委托和回調函數并進行綁定:



完善FindSession和JoinSession函數:




通過游戲模式跟蹤加入和退出的玩家
創建GameModeBase C++類:






創建GameMode 藍圖類:




Path to lobby




完善Destroy函數
由于網絡傳輸需要時間,立即調用CreateSession函數時可能Session還未被銷毀
在DestroySession函數中先銷毀session,若成功則調用CreateSession函數重新創建:




Disable Menu button
當創建時禁用按鈕,創建失敗時啟用按鈕:




Steps to use plugin
Plugins.zip
鏈接: https://pan.baidu.com/s/13PgLgU_LrBP4z0X3XMbFCQ?pwd=nx32 提取碼: nx32
將測試里的Plugin壓縮:

解壓到桌面:

直接拖到需要的Project中:


啟用Steam插件:

在DefaultEngine.ini中添加代碼:



[/Script/Engine.GameEngine]
+NetDriverDefinitions=(DefName="GameNetDriver",DriverClassName="OnlineSubsystemSteam.SteamNetDriver",DriverClassNameFallback="OnlineSubsystemUtils.IpNetDriver")
[OnlineSubsystem]
DefaultPlatformService=Steam
[OnlineSubsystemSteam]
bEnabled=true
SteamDevAppId=480
bInitServerOnClient=true
[/Script/OnlineSubsystemSteam.SteamNetDriver]
NetConnectionClassName="OnlineSubsystemSteam.SteamNetConnection"

在DefaultGame.ini中添加代碼:


[/Script/Engine.GameSession]
MaxPlayer=100
創建Lobby地圖并復制該路徑:

打開Level Blueprint:

創建Widget并調用Menu Setup函數:

刪除這些文件以重構代碼:


打包時在List of maps添加這兩個地圖:

steam需要在同一地區才能聯機:

Create a Character
創建Character C++類:


創建Character 藍圖類:

添加彈簧臂組件和相機組件并初始化:

將彈簧臂組件附加到Mesh而不是Capsule上,確保蹲下改變膠囊尺寸時,不會影響彈簧臂高度

Add Character Movement
Pitch,Roll,Yaw:
Pitch:俯仰
Roll:滾轉
Yaw:偏航

將旋轉前的Rotation坐標與旋轉矩陣相乘即可求出旋轉后的Rotation坐標:





Create Animation
先創建AnimInstance C++類:

創建并初始化1+3個變量:


創建Animation 藍圖類:


先創建一個State Machine:


IdleWalkRun:

IdleWalkRun -> JumpStart:

JumpStart -> Falling:

Falling -> JumpStop:

JumpStop -> IdleWalkRun:


使用鍵盤操縱角色移動方向而不是鼠標:


From Lobby Map to Game Map
為Lobby Map創建GameMode C++類:


當進入Lobby的玩家數量達到2個時,無縫Travel到Game Map中:


為Lobby Map創建GameMode 藍圖類:

在藍圖中也需要設置為無縫Travel:

將Lobby的GameMode設置為BP_LobbyGameMode:

創建一個空白的Transition Map:

設置Transition Map:

NetWork Role
網絡角色(Network Role)包括本地角色和遠程角色
1.角色權威(Authority):存在于服務器上的角色,具有最高權限
2.模擬代理(Simulated Proxy):存在于客戶端上且由服務器或另一個客戶端控制的角色
3.自主代理(Autonomous Proxy):存在于客戶端上且由當前玩家控制的角色
4.無角色(None):未定義角色的演員

創建在角色頭頂顯示角色信息的Widget C++類:




在Character中創建并初始化Widget:


創建藍圖類:

配置Character中的Overhead Widget并調用Show Player Met Role函數:


Create a Weapon
創建C++類:


為武器類添加一個枚舉(Enum)來定義武器的狀態(初始狀態、裝備狀態、掉落狀態):




創建藍圖類:


Create PickUpWidget
創建Widget藍圖類:


設置Text:

在ShootCharacter C++中添加UWidgetComponent變量并設置只有重疊時才顯示該Widget:
OnComponentBeginOverlap:



僅在服務器上生成重疊事件,即當服務器或客戶端與武器重疊時,都只有服務器顯示Widget:

在ShootCharacter藍圖中配置Widget:


Problem:服務器或客戶端與武器重疊時,都只有服務器顯示Widget
Show PickUpWidget using Replication
解決上述問題,確保誰與武器重疊,誰show widget:
創建復制變量并綁定調用函數:

創建配置函數并配置復制變量:


創建調用函數:

創建SetOverlappingWeapon函數:


在Weapon的OnSphereOverlap函數中調用SetOverlappingWeapon函數:


Hide PickUpWidget using Replication
添加OnSphereEndOverlap函數,結束重疊時將OverlappingWeapon設置為NULL:



處理客戶端的結束重疊事件:
在處理客戶端的回調函數中添加變量LastWeapon,表示復制之前的Weapon的值
因為當OverlappingWeapon為NULL時,無法調用ShowPickUpWidget函數


處理服務器的結束重疊事件:

Equip Weapon
在骨骼上添加Socket:


創建CombatComponent組件并在Character中初始化:




在Character中初始化:





綁定裝備武器的按鍵:




目前之有服務器才能裝備武器
Remote Procedure Call:遠程過程調用(RPC)
在客戶端調用,在服務器執行:是客戶端也能Equip Weapon



Problem:還需要隱藏Widget并禁用Overlap Event
解決方法:
雖然服務器和客戶端調用的都是EquipWeapon函數,但客戶端無法直接修改服務器狀態,需通過 RPC 或變量復制同步


將WeaponState設置為復制變量:




Set Equip Animation Pose
先將EquippedWeapon設置為復制變量,確保當一個客戶端改變時,所有客戶端都能更新其值,從而確保所有客戶端都能看見Equip Animation Pose:


在ShootCharacter中創建IsWeaponEquipped函數:


在ShootAnimInstance中創建布爾類型變量bWeaponEquipped,并通過IsWeaponEquipped函數初始化其值:


在Animation中創建Equipped的State Machine,并通過Weapon Equipped調用Blend Poses by bool來判斷是否裝備了武器:


Add Crouch
添加按鍵綁定:




在C++中設置為可以下蹲:

在動畫實例中創建并初始化bIsCrouch變量:


添加Crouch節點:

在藍圖中設置Crouch變量:


Add Aim
添加按鍵綁定:

創建復制變量bAiming:用于服務器上Aim時,可以復制到所有客戶端,從而能在其他客戶端看見
創建RPC函數:用于客戶端Aim時,調用服務器的Aim,從而復制到所有客戶端,從而能在其他客戶端看見



綁定到函數:



創建函數用于在AnimInstance中調用:


在ShootAnimInstance中創建并初始化bAiming:


添加瞄準動作:


若沒有設置為復制變量,或沒有添加RPC函數,則會導致動作不同步的現象:

正確現象:

Blend Space Animation
若缺少向左前、右前、左后、右后的動畫,可以通過調整其他動畫的旋轉來獲得
創建8個方向的混和動畫:
視頻


設置Weight Speed防止移動抽搐:


需要添加Module


Shift加速
添加按鍵綁定:


在CombatComponent中添加 bShift復制變量 和 SetShift函數 以及 RPC函數 :



在ShootCharacter中添加SetShift函數:

設置Camera不阻擋角色視角

設置AimOffset
根據玩家鼠標移動調整角色的頭部和武器方向
通過在Tick中調用AimOffset函數來實時獲取Yaw和Pitch的值:




在ShootAnimInstance中創建并初始化Yaw和Pitch:


在虛幻中創建AimOffset:

將需要用到的動畫的Base Pose全部設置為Idle動畫:

設置AimOffset:

存儲Equipped:


動畫混合:將Lower body的動畫設置為Equipped,將Upper body的動畫設置為頭部的移動:



設置Hand IK
確保左手在武器的合適位置
在武器上分別添加Idle和Aim時的LeftHandSocket:


在Weapon中添加獲取WeaponMesh的函數:

在ShootCharacter中獲取EquippedWeapon:


創建LeftHandTransform并初始化:


設置FABRIK:




實時調整Socket,確保左手在合適位置:

Turn in place
Turn Animations
創建Meradata用于判斷是否處于該動畫狀態:


Force Root Lock:




創建站立和蹲下的Idle和Aim的State Machine:



創建Turn Left和Turn right:

混合動畫:




Rotate Root Bone
角色轉向時移動方向
初始化Interp(插值變量)AO_Yaw:


平滑轉動:



設置網絡更新頻率




[/Script/OnlineSubsystemUtils.IpNetDriver]
NetServerMaxTickRate=120
在不同地形設置不同的Meta Sounds
創建Meta sounds:

在設置中添加不同的材質名稱:

創建不同的物理材質:


將物理材質的Suface Type改為對應設置中的名稱:

在藍圖中創建Anim Notify:




在C++中創建Anim Notify:


創建BP類:

添加Sync Marker(同步標記):

添加Anim Notify:


在實際地圖中設置不同的物理材質:

Projectile Weapon

投射武器:
1.生成彈丸:發射具體的彈藥。
2.具有速度:彈丸有一定的運動速度。
3.可能有/沒有重力:彈丸的運動可能受到重力影響,也可能不受影響。
4.命中事件:形成特定的命中效果。
5.追蹤粒子:通常在飛行過程中可見的軌跡。
命中掃描武器:
1.執行直線追蹤:通過射線檢測目標。
2.瞬時命中:發射后立即造成傷害。
3.光束粒子:通常以光束形式表現。
創建一個Weapon子C++類:


創建子彈的C++類:



Fire Montage
綁定Fire按鍵:

設置Fire和Fire_Aim的Addictive Settings:

創建Fire Montage動畫,并Add Slot:

在Aim Offsets之前使用蒙太奇動畫:


在CombatComponent中創建 FireButtonPressed函數 和 bFireButtonPressed變量:


在ShootCharacter中綁定輸出,并創建PlayFireMontage函數和變量




Fire Effects Animation
創建Fire動畫函數并調用:



設置Weapon動畫:

添加Projectile Weapon藍圖:


使用NetMulticast RPC
使服務器和客戶端都能看見Fire:



Component Tick Problem
問題:Component的Tick函數失效
原因:在編輯器打開的情況下創建一個組件并進行編譯,構造函數不會再次運行,因為它在你第一次打開編輯器時已經運行過了。
由于構造函數沒有第二次運行,因此創建的默認對象和庫(包括靜態庫和動態鏈接庫)不會被更新,導致編輯器中沒有反映出任何更改。
藍圖知道組件的存在,因為C++告訴它有這個組件,但除此之外,藍圖并不知道其他信息。
通過更改組件的名稱并重新編譯,可以將更改推送到CDO和庫中,從而使更改在編輯器中反映出來**

通過十字準星獲取Hit Target



設置偏移量,確保準星在人物右上方:


Spawn Projectile
在槍口位置添加Socket:

添加HitTarget變量并初始化:


在ProjectileWeapon中重載Weapon的Fire函數并Spawn子彈:


創建子彈的藍圖類:



Set Projectile Movement Component
設置子彈的運動組件:

使子彈的旋轉跟隨其速度方向:

設置子彈的速度:


添加Tracer:



設置子彈的Replicated屬性
將Fire函數設置為僅在服務器執行:

將子彈設置為可復制:當子彈被發射時,它的狀態(位置、速度、碰撞等)需要在所有客戶端上保持一致

Replicate Hit Target


FVector_NetQuantize:用于網絡傳輸向量數據的一種類型,旨在減少網絡帶寬的使用并提高性能
通過在RPC中使用FVector_NetQuantize來復制Hit Target:


添加子彈擊中音效和粒子




生成彈殼
在武器中添加彈殼的Socket:

創建彈殼的C++類:


創建彈殼的Mesh:


在Weapon中Spawn彈殼:


創建彈殼的藍圖類:


為彈殼添加速度和音效
修改彈殼的顏色:
將父類混合材質的Emissive Color設置為變量:

將子類混合材質復制一份單獨給彈殼使用:


修改彈殼顏色:

為彈殼添加速度和音效:


添加十字準星
PlayerController中有獲取HUD的函數,HUD中可以繪制HUD:

創建Player Controller C++類:


創建HUD C++類:


創建Player Controller 藍圖類:


創建HUD 藍圖類:


在ShootingGameMode中配置PlayerController和HUD:

在Weapon中設置準星:確保每個武器可以配置不同的準星樣式

在 ShootHUD 中創建準星貼圖Struct并在 CombatComponent 組件中配置準星貼圖,在由 DrawHUD 根據配置每秒繪制各個方向的準星:


先獲取PlayerController,再通過PlayerController來獲取HUD:

先獲取到HUD,再通過HUD->SetHUDPackage()函數來配置從Weapon中獲取到的準星材質,最后每幀通過戰斗組件的TickComponent調用SetHUDCrosshairs:

導入素材并在Weapon中配置:


制作動態準星
創建不同的準星擴散:

根據角色狀態調整準星擴散值:


再HUD中應用擴散值:


設置射擊時擴散準星:



修正槍口方向與準星方向一致
在Combat的Tick函數中時刻獲取HitTarget:


在ShootCharacter中添加獲取Target的函數:


在AnimInstance中添加RightHandRotation用于在藍圖中調整:

只允許當地角色調整槍口方向與準星方向一致:

重構藍圖實例:

在Transform_Hand中添加FRABRIC中的內容并添加修正右手的內容:
只允許當地角色調整槍口方向與準星方向一致:


刪除FABRIC的state machine,重新使用Aim Offset:


通過調整RightHandSocket來確保兩線重合:

設置相機FOV實現瞄準時視角縮放
設置腰射瞄準時Camera Boon的Offset
不需要調整Camera
需要將Camera Boom的Location調整到對著角色的腦袋,并在C++中設置Camera Boom的Socket Offset:

初始化Socket Offset:

在Combat中定義Camera Boom的默認Offset和瞄準Offset:


在Tick函數中時刻更新:

使用插值函數VInterpTo來實現Camera Boom Offset的平滑移動:

必須要保證Y軸和Z軸偏移量相同,否則瞄準時準星將偏移:

效果:


Change Crosshairs Color
擴展準星開始位置修復準星Bug
由于Trace Start從相機開始,所以當有物體在Camera和Character之間時,會導致準星錯誤的識別到后面的物體:



修復:在Start基礎上加上 相機到角色的距離 和 額外的距離:

設置近距離不穿模

將Near Clip Plane降低為2并重啟UE:

Add Hit Reactions
設置蒙太奇動畫:





Play Montage動畫:




由于Projectile忽略了Pawn,但如果Block Pawn,則會導致子彈對Capsule有碰撞,需要的是對Mesh有碰撞

需要自定義一個Object Type:SkeletalMesh:

在藍圖中將Mesh的Object Type設置為自定義Type:

在MP_Shoot中定義ECC_GameTraceChannel1為SkeletalMesh,便于使用:

在C++中將Mesh的Object Type設置為自定義Type:

在Projectile中設置子彈忽略自定義Channel:

Automatic Fire
在Weapon中添加 延遲 和 是否自動射擊 變量:

添加計時器,防止頻繁射擊:


設置平滑相機
游戲框架




Add Health
由于Player State網絡更新較慢,所以在Character中更新Health:

創建Widget C++類:


創建Widget藍圖類:



在ShootCharacter中初始化Health:


在CharacterOverlay中初始化變量:

在ShootHUD中Create Widget:


Update Health
HUD中能Create Widget,PlayerController能獲取HUD,在PlayerController中創建更新Health的函數,在Character中能獲取PlayerController并調用該函數:

在PlayerController中創建更新Health的函數:


在Character中獲取PlayerController并調用該函數:


Damage
創建Projectile的C++子類,用于實現不同Damage的邏輯:


在Projectile的projected部分創建Damage變量:

在子類中繼承父類的OnHit函數:

調用ApplyDamage函數:

在ShootCharacter中創建ReceiveDamage函數:

綁定ReceivedDamage函數:

利用復制變量來更新Health HUD:

創建藍圖類:


在步槍中將子彈改為BP_ProjectileBullet:

Elimination
創建ShootGameMode來處理淘汰邏輯:


修改BP_ShootingGameMode的父類為ShootGameMode C++類:

GameMode 的 PlayerEliminated 函數負責調用角色上的 Elim 函數:


在ShootCharacter中創建淘汰動畫函數和RPC函數:


創建是否淘汰變量:


在Health為0時,調用GameMode的淘汰函數:

在AnimInstance中創建淘汰變量:


創建淘汰蒙太奇動畫:

防止角色死亡后站起:

在動畫藍圖中使用Slot:

Respawn
在GameMode中創建復活函數:


在ShootCharacter中創建延遲計時器:


確保角色總能Spawn成功:


Disable Collision and Movement
淘汰時禁用移動和射擊,并設置為NoCollision:

Drop Weapon
在Combat中創建EquippedWeapon的回調函數,確保在其改變時,調用該函數:


在Weapon中添加丟棄武器函數:

設置Weapon State,確保調用WeaponState的回調函數:

配置丟棄武器時,武器的狀態:

在淘汰函數中調用丟棄武器函數:

修復復活后血條未初始化問題
在Character中獲取Health和MaxHealth:

在PlayerController中重載OnPossess方法:當控制器擁有一個新的Pawn時,會調用這個方法:

在該函數中更新Health HUD:

修復當Travel時,由于Health未初始化導致Travel失敗的問題

Dissolve Material
創建Material:


創建Material Instance:


Score
在CharacterOverlay中添加Score:

創建PlayerState C++類 用于記錄得分:


在CharacterOverlay中創建Score Text:

在PlayerController中設置HUD的Score Text:


在PlayerState中存儲Score并調用Controller中的函數來顯示:


在角色死亡時,為AttackCharacter調用PlayerState中的AddToScore函數:

因為PlayerState無法在BeginPlay中初始化,所以創建PollInit函數初始化Score為0并在Tick中調用:




創建PlayerState 藍圖類:

在GameMode中配置PlayerState:

Defeats
在CharacterOverlay中添加Defeats:

在CharacterOverlay中創建Defeats Text:

在PlayerController中設置HUD的Defeats Text:


在PlayerState中存儲復制變量Defeats并調用Controller中的函數來顯示:



在角色死亡時,為AttackCharacter調用PlayerState中的AddToDefeats函數:

因為PlayerState無法在BeginPlay中初始化,所以創建PollInit函數初始化Defeats為0并在Tick中調用:

Weapon Ammo
先在Character Overlay中創建Ammo Text:

在Character Overlay中初始化Ammo Text:

在Controller中創建配置HUD的函數:


在Weapon中創建 使用Controller中函數 的函數 并 重載Owner的回調函數 用于新角色撿起武器時更新Ammo:

創建Ammo,Ammo的回調函數,消耗一發子彈的函數,Mag Capacity(彈匣的容量):

復制Ammo:


在Fire函數中調用SpendRound函數,即消耗一發子彈:

若存在武器,則丟棄原有武器,更新Server的彈藥:

角色淘汰時,隱藏Ammo:

初始化Ammo和Mag Capacity(彈匣的容量):

當武器子彈為0時,不可射擊:



Carried Ammo
先在Character Overlay中創建CarriedAmmo Text:

在Character Overlay中初始化CarriedAmmo Text:

在Controller中創建配置HUD的函數:


創建WeaponType的枚舉類型,從而根據不同類型的武器,來配置不同的攜帶子彈:


在Weapon中創建WeaponType并在編輯器中配置:

在Combat中創建Map來連接 武器類型 和 對應的攜帶子彈數量,使用StartingAmmo初始化不同武器的攜帶子彈數量,使用復制變量CarriedAmmo來配置當前武器的攜帶子彈數量:

當前僅初始化了步槍的攜帶子彈數量:



Reload
創建換彈Input:

創建CombatState枚舉類型:


在Character中創建Play Montage動畫:

將Combat設置為BlueprintReadOnly以便與在藍圖中獲?。?/strong>

獲取CombatState:

綁定Input:


在AnimInstance藍圖中使用該函數:


添加復制變量CombatState:


在Server中Play Reload Montage并改變CombatState從而觸發復制函數,同步到客戶端:

在AnimInstance中創建bool,是否使用左手的FABRIC,AimOffset和右手Transform:

Reload時,不使用:

創建蒙太奇動畫:

將Slot設置為WeaponSlot:

創建Reload Finished Notify:

當到達該Notify時,調用函數結束Reload:

將Transform Hands分為Right Hand 和 Both Hand:

Transform Hands,若在Reload,則不使用FABRIC:

Right Hand,若在Reload,則不使用Transform Bone:

Both Hand:

Aim Offsets,若在Reload,則不使用AimOffset:

換彈中持續按開火鍵,結束后執行Fire:

換彈時,不可以Fire:

Update Ammo
換彈時,計算子彈數量:

在Weapon中創建添加子彈,獲取子彈和彈夾中子彈數量:



獲取換彈的子彈數量,更新子彈:

在完成換彈后更新子彈:

Add Weapon Sounds
在Weapon中添加Equip Weapon音效:

在Combat的EquipWeapon函數中Play Sounds:

在復制函數中Play Sounds:

添加換彈音效:

Auto Reload
自動換彈:


滿彈夾不換彈:

Game Time
添加比賽倒計時:





在Overlay中創建Text:

ServerRequestServerTime 和 ClientReportServerTime 函數一起工作,通過測量網絡往返時間來估算客戶端和服務器之間的時間差,并將這個時間差存儲在 ClientServerDelta 變量中。 GetServerTime 函數使用這個時間差來返回一個與服務器同步的時間
ReceivedPlayer函數在玩家連接后立即啟動時間同步過程:
CheckTimeSync函數在Tick中每隔TimeSyncFrequency秒更新一次時間:




熱身時間
GameMode是GameModeBase的子類,具有GameModeBase的屬性以及Match State:
若要使用Match State,則需要使用GameMode類:

Match States有自帶的變量和函數:

可以在InProgress中創建自定義變量:

等待階段->熱身時間->開始游戲:

可以通過AGameMode查看自帶的變量和函數:

創建熱身時間:

設置bDelayedStart為true,確保不自動Start Match:
剩余時間結束后調用Start Match函數來Spawn Character:

熱身時間不顯示SlashOverlay
GameMode中的OnMatchStateSet函數:當MatchState改變時調用對應的函數

在ShootGameMode中重載該函數

遍歷所有Controller,并調用Controller中的OnMatchStateSet函數以用于判斷是否顯示HUD:

在Controller中添加OnMatchStateSet函數:

添加復制變量MatchState:


設置MatchState變量,若MatchState為InProgress,則顯示SlashOverlay:

刪除BeginPlay中的AddCharacterOverlay函數,只有在InProgress時才顯示SlashOverlay:

修復游戲開始時沒有初始化Overlay的Bug
由于CharacterOverlay最后才初始化,所以無法在CharacterOverlay初始化之前設置HUD的值
創建變量用于各種存儲HUD的值:

若沒有初始化,則暫存各個HUD的變量值:

如果CharacterOverlay沒有初始化,則在Tick中直到其初始化,設置CharacterOverlay中HUD的值:

顯示熱身時間
創建Announcement C++類:



創建藍圖類:


創建熱身時間變量:

在ShootHUD中添加該類和變量以及函數,用于顯示Announcement Widget到屏幕上:


在PlayerController的BeginPlay中調用HUD的函數添加熱身時間到屏幕:

在PlayerController中的InProgress中隱藏熱身時間:

更新熱身時間
在GameMode中初始化需要的時間:

在Controller中創建設置熱身時間HUD的函數:

創建服務器和客戶端檢查MatchState的函數 和 用于存儲時間的變量:




Cooldown Match State
根據GameMode中的Match State添加自定義Match State:

添加結算狀態的Match State并添加結算時間:

初始化結算狀態,在Tick中若剩余時間為0則將MatchState設置為Cooldown:

在Controller中設置Handle Cooldown的函數:

在OnMatchStateSet中若MatchState為Cooldown,則進入該函數:

更新Cooldown Time
創建Announcement Text:


在GameMode中獲取倒計時:

設置Cooldown時的倒計時:

設置Cooldown Time:


若倒計時為負數,則不顯示倒計時Text:

服務器直接從GameMode中獲取倒計時,否則會有延遲:

添加Cooldown Text:

Restart Game
冷卻時間內,設置角色靜止且無法操作:
添加復制變量bDisableGameMode:


除了Turn和LookUp,禁用其他按鍵綁定:

禁用AimOffset,并且將bUseControllerRotationYaw設置為false,TurningInPlace設置為NotTurning:

在角色淘汰時,將bDisableGamePlay設置為True,并且若淘汰時正在Fire,則將FireButtonPressed設置為false:

在冷卻時間內,將bDisableGamePlay設置為True,將FireButtonPressed設置為false,SetAiming為false:

當冷卻時間結束時,調用自帶函數RestartGame來重啟游戲:

Game State
在Game State中存儲得分最高的玩家 并 在冷卻時間在屏幕上顯示最高得分玩家:
創建GameState C++類:


創建GameState 藍圖類:


在GameMode中設置Gaem State Class:

在GameState中創建最高得分玩家的數組 和 更新該數組的函數:


在GameMode中的淘汰函數中添加 更新最高得分玩家的函數:

在處理Cooldown函數中,根據最高得分玩家來設置Announcement中的Info Text:

火箭筒
創建Projectile的C++子類:

創建炮彈:

創建藍圖類炮彈:

添加重載函數OnHit:

在OnHit中執行范圍傷害:

在WeaponType中添加火箭筒變量:

在Combat中初始化攜帶的炮彈數量:


設置火箭筒的換彈動畫:

創建火箭筒武器的藍圖類:


創建LeftHandSocket并調整:

Rocket Trail
修復火箭筒發射時炸到自己
為火箭筒單獨創建一個子彈的Movement Component:


重載處理碰撞邏輯的函數:

當火箭筒遇到阻擋時,繼續前進:

在Rocket中初始化RocketMovementComponent:


若Hit的是自己,則return:

將Weapon的Movement設置為復制的,防止武器位置不匹配:

將Projectile中的MovementComponent移動到Protected中,并刪除其初始化,讓每個Projectile都有唯一的Movement組件:

在ProjectileBullet中初始化Movement組件:

修復火箭筒爆炸時TrailSmoke瞬間消失
延遲銷毀炮彈:


設置定時器后,立即銷毀Mesh,Box,停止TrailSmoke生成新的粒子但沒有消失,過3秒后,讓TrailSmoke消失:

Hit Scan Weapon
通過射線檢測實現:當玩家按下開火鍵時,子彈會瞬間命中目標,不需要模擬真實的彈道飛行時間
創建Scan Weapon的C++類:


創建Scan Weapon的藍圖類:

創建子彈類:




為武器的帶子創建物理模型,讓其飄動
散彈槍和隨機散射
添加ShotGunWeapon的C++類:


添加ShotGunWeapon的藍圖類:

在Weapon中創建設置散射的變量 和 獲取隨機散射角度的函數:


在ProjectileWeapon中使用隨機彈道:

在ShotGunWeapon中創建散彈槍碎片數量:

使用for循環來Spawn多個Projectile:

添加瞄準鏡
在Animation中設置新的Aim Offset,之前的Aim Offset會導致開鏡后無法上下動:




添加一個圓柱體到瞄準鏡里面,用做鏡片:

將兩者合并為一個StaticMesh:

創建瞄準鏡的準星Texture和Material:

將準星材質應用到鏡頭的材質上,圖中是1,并記住鏡片材質的編號,圖中是2:

為每個武器創建不同的Camera的Socket,Scope的Socket,以及SceneCapture2D的Socket:

實現右擊開鏡的邏輯:
在Weapon中添加開鏡時間,開鏡后移動的速度降低,添加武器的Camera:

在Combat中添加是否開鏡的變量和函數:



在Character中添加按鍵綁定并判斷是點擊還是長按:

點按:Scoping+Aiming
長按:Aiming

若開鏡射擊,則必須射擊到瞄準鏡準星上
若開鏡射擊,則將射擊點設置到SceneCapture2D上,并沿著瞄準鏡方向發射子彈:

添加配件的C++類:

添加Attachment的子類:Scope類,用作瞄準鏡

創建用于鏡片的材質:

將Texture的名字更改為ScopeTexture,下面會在C++中用到:

將材質應用到BP_Scope上:

設置Attachment基類:


設置Scope子類:

設置鏡片的材質:


在Weapon中添加SceneCaptureComponent2D,用于決定瞄準鏡的觀察:

初始化武器相機位置,即不裝備瞄準鏡時相機的位置:

調用Attachment的多態函數EquipToWeapon,實際上調用的是Scope的重載函數:

在ShootCharacter中創建OverlappingAttachment,用于顯示PickUpWidget:






添加Zoom按鍵綁定,當開鏡時可以調整放大縮?。?/strong>



榴彈炮
創建炮彈類:

允許炮彈彈跳:





換彈動畫(Reload Animations)
創建Mag_Hand Socket:

創建Anim Notify:

在Weapon中創建函數:




在Combat中使用函數:



添加彈夾和空彈夾:

散彈槍換彈動畫(Shotgun Reload Animation)
每一發進行換彈,并在換彈時可以射擊:

創建ShotgunShellReload,在AnimNotify中使用:


每次Update,子彈+1 -1,若子彈滿了或沒有備彈了,則在服務器JumpToEnd:

若沒有備彈了,則在客戶端JumpToEnd:

若子彈滿了,則在客戶端JumpToEnd:

散彈槍在換彈時可以Fire


在Montage中添加多個Shell和Loop和ShotGunEnd:
將Shell的TickType設置為Branching Point,防止客戶端由于動畫中使用Blend per bone導致一個動畫使用兩次從而導致散彈槍在客戶端一次reload兩個子彈:

發光輪廓
創建Post Volume并設置為無限大:

Material:https://github.com/DruidMech/MultiplayerCourseBlasterGame/tree/main/GameAssets/Materials
設置Post Volume的Materials:

在藍圖中設置Custom Depth:

在C++中設置:
將Custom Depth Stencil Pass設置為Enabled with Stencil:

添加不同顏色:
武器與角色重合時發光,裝備后不發光:








使用數組修復PickupWidget和Equip Weapon




問題:Montage動畫被打斷導致無法到達AnimNotify
當為蒙太奇動畫創建AnimNotify時,由于蒙太奇動畫會被其他動畫打斷,所以AnimNotify可能永遠不會執行:
為每個有AnimNotify的蒙太奇動畫創建 打斷/結束 事件:


手榴彈
ShootCharacter.h:
創建擊中點Mesh和樣條線,以及裝備的手雷:


ShootCharacter.cpp:
初始化組件:

裝備手雷函數:




右鍵按下時切換手雷 低拋/高拋 瞄準:

左鍵按下時手雷瞄準,R鍵按下時手雷拉環:

死亡后清除軌跡和擊中點:

Play手雷的蒙太奇動畫:

Grenade.h:


Grenade.cpp:

倒計時結束后實行范圍傷害:


CombatComponent.h:




CombatComponent.cpp:









設置Montage Animation:


設置樣條線軌跡和擊中點:




后坐力系統




第一人稱Arm和Weapon









High Ping Warning







方法一:簡單快速:

方法二:可以在打包后進行測試:

添加 Local Fire 減輕延遲影響



Show PickupWidget Locally

Client-Side Prediction
Prediction For Ammo
取消Ammo的復制屬性,并添加RPC和Sequence用于預測客戶端子彈消耗:



Prediction For Aiming
解決快速Aiming時由于Lag導致本地玩家的瞄準狀態被網絡復制的值覆蓋:



Prediction For Reloading





Server Side Rewind 服務器端倒帶
添加延遲補償組件:

添加Box組件:


讓角色禁止:

先隱藏所有Box組件:

為每個Box組件設置合適的位置與Extent而不是放大縮小:

完成后在游戲中展示所有Box:

查看蹲下等動作的Box是否合適:

創建Box的Struct以存儲信息,并創建每一幀的Struct用于記錄每一幀Box的位置:

設置LagCompensationComponent并初始化:



添加Name:Box的Map用于存儲Box:

將每個Box添加到Map中:

創建保存FramePackage和展示FramePackage的函數:

先創建BoxInformation獲取所有Box的信息,再將BoxInformation存儲到FramePackage中:


創建雙向鏈表用于存儲FramePackage:

在Tick中在Head存儲每幀的FramePackage,若超過最大記錄時間,則在Tail刪除節點:


Server Side Rewind,先創建一個Struct用于判斷是否擊中 并 判斷是否為HeadShot:

確定FrameToCheck:

通過插值函數來找到YoungerTime與OlderTime之間的HitTime的Package:

存取當前HitCharacter的Box位置,然后將HitCharacter的Box通過MoveBoxes函數移動到FrameToCheck的位置,射線判斷結束后還原Box的位置并將Box的Collision設置為NoCollision:

通過LineTranceSingleByChannel確定是否擊中了Box,需要先隱藏Mesh的Collision,顯示Box的Collision。先判斷是否為HeadShot,再判斷其他:

添加ServerScoreRequest函數:

若倒帶函數判斷擊中了,則ApplyDamage:

將Tick中的代碼整合到SaveFramePackage函數中:

創建客戶端到服務器傳輸數據的單程時間:


創建是否使用倒帶變量:

在Fire函數中,若為服務器 并 不使用倒帶,則直接ApplyDamage;若不為服務器 并 使用倒帶,則調用ServerScoreRequest函數:

重構ServerSideRewind:


如果ShotGun是HitScanWeapon,則對ShotGun進行如下Server Rewind:







更改HitBoxCollisionType的屬性



更改LagComponent中所有的ECC_Visibility為ECC_HitBox:


在C++中實現在編輯器同步更改屬性




為Projectile Weapon實現Server Side Rewind
先復制ProjectileBullet為ServerSideRewind-ProjectileBullet:

然后取消它的復制屬性:

初始化變量:

預測子彈軌跡:

創建一個新的ProjectileClass:

重構整合成一個函數:


武器啟用SSR:
1.服務器邏輯:
如果角色是服務器且由本地控制(如主機玩家),生成復制的子彈(同步到其他客戶端)。
如果角色是服務器但不由本地控制(如其他玩家的角色),生成非復制的子彈(僅服務器可見)。
2.客戶端邏輯:
如果角色是客戶端且由本地控制,生成非復制的子彈并啟用SSR。
如果角色是客戶端但不由本地控制,生成非復制的子彈但不啟用SSR。
武器未啟用SSR:
僅在服務器上生成投射物,客戶端不生成任何子彈(依賴服務器同步)
Fire函數:


ServerSideRewind函數:

ConfirmHit函數:


ScoreRequest函數:

在子彈的OnHit函數中使用ScoreRequest函數:

為Projectile ShotGunWeapon實現Server Side Rewind

limit Server Side Rewind
聲明委托:



若高于閾值,則Broadcast:




在裝備武器時綁定委托并檢查Ping值:

切換武器動畫
創建Montage動畫:


該變量用于判斷切換動畫是否結束:



在藍圖AnimNotify中使用:




在AnimInstance中,若切換武器,則不使用Fabric:




修復Shogun的Bug



Cheating and Validation
使用驗證程序來確保重要數據未被修改:



Elim Announcement
創建藍圖類:

創建Widget:


在ShootHUD中創建將ElimAnnouncement添加到ViewPort的函數:


在PlayerController中實現服務器淘汰事件發送給所有客戶端,客戶端根據不同的淘汰場景在 HUD 上顯示相應的信息:


在ShootGameMode的PlayerEliminated函數中循環每個控制器來調用函數:

實現動態消息
創建漸變動畫:


舊消息網上走:


HeadShot For Weapon







浙公網安備 33010602011771號