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以便与在蓝图中获取:

获取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按键绑定,当开镜时可以调整放大缩小:



榴弹炮
创建炮弹类:

允许炮弹弹跳:





换弹动画(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号