mini: false, //迷你模式 autoplay: false, //自动播放 theme: '#FADFA3', //主题色 loop: 'all', //音频循环播放, 可选值: 'all'全部循环, 'one'单曲循环, 'none'不循环 order: 'random', //音频循环顺序, 可选值: 'list'列表循环, 'random'随机循环 preload: 'auto', //预加载,可选值: 'none', 'metadata', 'auto' volume: 0.7, //默认音量,请注意播放器会记忆用户设置,用户手动设置音量后默认音量即失效 mutex: true, //互斥,阻止多个播放器同时播放,当前播放器播放时暂停其他播放器 listFolded: false, //列表默认折叠 listMaxHeight: 90, //列表最大高度 lrcType: 3, //歌词传递方式

Multiplayer Shooting Game

GASP+Lyra Animation

视频
Project Mega Sample (5.5)
Alternate Link

新 多人游戏插件

[/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的蒙太奇动画创建 打断/结束 事件:
屏幕截图 2025-07-13 182555
屏幕截图 2025-07-13 182615

手榴弹

ShootCharacter.h:

创建击中点Mesh和样条线,以及装备的手雷:


ShootCharacter.cpp:

初始化组件:

装备手雷函数:




右键按下时切换手雷 低抛/高抛 瞄准:

左键按下时手雷瞄准,R键按下时手雷拉环:

死亡后清除轨迹和击中点:

Play手雷的蒙太奇动画:

Grenade.h:


Grenade.cpp:

倒计时结束后实行范围伤害:


CombatComponent.h:




CombatComponent.cpp:









设置Montage Animation:


设置样条线轨迹和击中点:



后坐力系统




第一人称Arm和Weapon









High Ping Warning








方法一:简单快速:

方法二:可以在打包后进行测试:
屏幕截图 2025-07-03 213827

添加 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 服务器端倒带

添加延迟补偿组件:
屏幕截图 2025-06-26 134025
添加Box组件:
屏幕截图 2025-06-26 200108
屏幕截图 2025-06-26 200120
让角色禁止:
屏幕截图 2025-06-26 184634
先隐藏所有Box组件:
屏幕截图 2025-06-26 184816
为每个Box组件设置合适的位置与Extent而不是放大缩小:
屏幕截图 2025-06-26 200013
完成后在游戏中展示所有Box:
屏幕截图 2025-06-28 164105
查看蹲下等动作的Box是否合适:
屏幕截图 2025-06-28 164129
创建Box的Struct以存储信息,并创建每一帧的Struct用于记录每一帧Box的位置:
屏幕截图 2025-06-28 173428
设置LagCompensationComponent并初始化:
屏幕截图 2025-06-28 163027
屏幕截图 2025-06-28 163055
屏幕截图 2025-06-28 163109
添加Name:Box的Map用于存储Box:
屏幕截图 2025-06-28 174746
将每个Box添加到Map中:
屏幕截图 2025-06-28 174849
创建保存FramePackage和展示FramePackage的函数:
屏幕截图 2025-06-28 174953
先创建BoxInformation获取所有Box的信息,再将BoxInformation存储到FramePackage中:
屏幕截图 2025-06-28 175517
屏幕截图 2025-06-28 173307
创建双向链表用于存储FramePackage:
屏幕截图 2025-06-28 190757
在Tick中在Head存储每帧的FramePackage,若超过最大记录时间,则在Tail删除节点:
屏幕截图 2025-06-28 190934
屏幕截图 2025-06-28 190644
Server Side Rewind,先创建一个Struct用于判断是否击中 并 判断是否为HeadShot:
屏幕截图 2025-06-29 190638
确定FrameToCheck:
屏幕截图 2025-06-29 190704
通过插值函数来找到YoungerTime与OlderTime之间的HitTime的Package:
屏幕截图 2025-06-29 190720
存取当前HitCharacter的Box位置,然后将HitCharacter的Box通过MoveBoxes函数移动到FrameToCheck的位置,射线判断结束后还原Box的位置并将Box的Collision设置为NoCollision:
屏幕截图 2025-06-29 190803
通过LineTranceSingleByChannel确定是否击中了Box,需要先隐藏Mesh的Collision,显示Box的Collision。先判断是否为HeadShot,再判断其他:
屏幕截图 2025-06-29 190732

添加ServerScoreRequest函数:
屏幕截图 2025-07-03 212556
若倒带函数判断击中了,则ApplyDamage:
屏幕截图 2025-07-03 212626
将Tick中的代码整合到SaveFramePackage函数中:
屏幕截图 2025-07-03 212610
创建客户端到服务器传输数据的单程时间:
屏幕截图 2025-07-03 212726
屏幕截图 2025-07-03 212732
创建是否使用倒带变量:
屏幕截图 2025-07-03 212852
在Fire函数中,若为服务器 并 不使用倒带,则直接ApplyDamage;若不为服务器 并 使用倒带,则调用ServerScoreRequest函数:
屏幕截图 2025-07-03 212829
重构ServerSideRewind:
屏幕截图 2025-07-05 234540
屏幕截图 2025-07-05 234530

如果ShotGun是HitScanWeapon,则对ShotGun进行如下Server Rewind:
屏幕截图 2025-07-05 234335
屏幕截图 2025-07-05 234352
屏幕截图 2025-07-05 234549
屏幕截图 2025-07-05 234559
屏幕截图 2025-07-05 234610
屏幕截图 2025-07-05 234636
屏幕截图 2025-07-05 234645

更改HitBoxCollisionType的属性

屏幕截图 2025-07-11 161504
屏幕截图 2025-07-11 161917
屏幕截图 2025-07-11 172453
更改LagComponent中所有的ECC_Visibility为ECC_HitBox:
屏幕截图 2025-07-11 172534
屏幕截图 2025-07-11 172559

在C++中实现在编辑器同步更改属性

屏幕截图 2025-07-10 201825
屏幕截图 2025-07-10 201834
屏幕截图 2025-07-10 201854
屏幕截图 2025-07-10 201915

为Projectile Weapon实现Server Side Rewind

先复制ProjectileBullet为ServerSideRewind-ProjectileBullet:
屏幕截图 2025-07-11 110856
然后取消它的复制属性:
屏幕截图 2025-07-11 111028
初始化变量:
屏幕截图 2025-07-11 155131
预测子弹轨迹:
屏幕截图 2025-07-11 155429
创建一个新的ProjectileClass:
屏幕截图 2025-07-11 155518
重构整合成一个函数:
屏幕截图 2025-07-11 155541
屏幕截图 2025-07-12 140551

武器启用SSR:
1.服务器逻辑:
如果角色是服务器且由本地控制(如主机玩家),生成复制的子弹(同步到其他客户端)。
如果角色是服务器但不由本地控制(如其他玩家的角色),生成非复制的子弹(仅服务器可见)。

2.客户端逻辑:
如果角色是客户端且由本地控制,生成非复制的子弹并启用SSR。
如果角色是客户端但不由本地控制,生成非复制的子弹但不启用SSR。

武器未启用SSR:
仅在服务器上生成投射物,客户端不生成任何子弹(依赖服务器同步)
Fire函数:
屏幕截图 2025-07-11 155712
屏幕截图 2025-07-12 140801
ServerSideRewind函数:
屏幕截图 2025-07-12 135939
ConfirmHit函数:
屏幕截图 2025-07-12 140054
屏幕截图 2025-07-12 140106
ScoreRequest函数:
屏幕截图 2025-07-12 140119
在子弹的OnHit函数中使用ScoreRequest函数:
屏幕截图 2025-07-12 140213

为Projectile ShotGunWeapon实现Server Side Rewind

屏幕截图 2025-07-12 140958

limit Server Side Rewind

声明委托:
屏幕截图 2025-07-12 193452
屏幕截图 2025-07-12 193500
屏幕截图 2025-07-12 193510
若高于阈值,则Broadcast:
屏幕截图 2025-07-12 193542
屏幕截图 2025-07-12 193640
屏幕截图 2025-07-12 193704
屏幕截图 2025-07-12 193720
在装备武器时绑定委托并检查Ping值:
屏幕截图 2025-07-12 194223

切换武器动画

创建Montage动画:
屏幕截图 2025-07-13 162517
屏幕截图 2025-07-13 162526
该变量用于判断切换动画是否结束:
屏幕截图 2025-07-13 162535
image
屏幕截图 2025-07-13 162614
在蓝图AnimNotify中使用:
屏幕截图 2025-07-13 162637
屏幕截图 2025-07-13 162657
屏幕截图 2025-07-13 162739
屏幕截图 2025-07-13 162924
在AnimInstance中,若切换武器,则不使用Fabric:
屏幕截图 2025-07-13 162949
屏幕截图 2025-07-13 163215
屏幕截图 2025-07-13 164009
屏幕截图 2025-07-13 163238

修复Shogun的Bug

屏幕截图 2025-07-13 163024
屏幕截图 2025-07-13 163041
屏幕截图 2025-07-13 180054

Cheating and Validation

使用验证程序来确保重要数据未被修改:
屏幕截图 2025-07-13 174248
屏幕截图 2025-07-13 174257
屏幕截图 2025-07-13 174318

Elim Announcement

创建蓝图类:
image
创建Widget:
屏幕截图 2025-07-14 180403
屏幕截图 2025-07-14 180413
在ShootHUD中创建将ElimAnnouncement添加到ViewPort的函数:
屏幕截图 2025-07-14 180504
屏幕截图 2025-07-14 180547
在PlayerController中实现服务器淘汰事件发送给所有客户端,客户端根据不同的淘汰场景在 HUD 上显示相应的信息:
屏幕截图 2025-07-14 180617
屏幕截图 2025-07-14 180649
在ShootGameMode的PlayerEliminated函数中循环每个控制器来调用函数:
屏幕截图 2025-07-14 180744

实现动态消息

创建渐变动画:
屏幕截图 2025-07-14 190732
屏幕截图 2025-07-14 190742
旧消息网上走:
屏幕截图 2025-07-14 190913
屏幕截图 2025-07-14 190952

HeadShot For Weapon

屏幕截图 2025-07-15 115122
屏幕截图 2025-07-15 115155
屏幕截图 2025-07-15 115226
屏幕截图 2025-07-15 115241
屏幕截图 2025-07-15 115303
屏幕截图 2025-07-15 150752

posted @ 2025-01-23 16:26  pone1  阅读(114)  评论(0)    收藏  举报