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, //歌词传递方式

⭐New Shoot Game Notes

复制变量

在服务器上,所有人都是Authority:
屏幕截图 2026-02-12 191145
在其他客户端上,只有自己是AutonomouseProxy,其他人都是SimulatedProxy:
屏幕截图 2026-02-12 191224

//.h
UPROPERTY(ReplicatedUsing = OnRep_Test)
AActor* Test;
	
UFUNCTION()
void OnRep_Test();

//.cpp
void ALyraCharacter::GetLifetimeReplicatedProps(TArray<class FLifetimeProperty>& OutLifetimeProps) const
{
    Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	
    DOREPLIFETIME(ThisClass, Test);
}

void ALyraCharacter::OnSphereBeginOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
    if (!GetOwner() || !HasAuthority()) return;
	
    Test = OtherActor;
    if (IsLocallyControlled())
    {
        OnRep_Test();
    }
}

void ALyraCharacter::OnRep_Test()
{
    GetMesh()->SetVisibility(true);
}

屏幕截图 2026-02-12 220255
这里分别有ABC三个人,C是服务器控制的角色,AB均为客户端
但是在Server C上,ABC均是HasAuthority
在OnSphereBeginOverlap中,!HasAuthority()则返回
OnRep只能由服务器传递到客户端,服务器上不会自动调用OnRep,需要手动调用




{266C6636-4F17-4A72-9306-EB53EC936F1B}
当A触发BeginOverlap时,Client A中的A和Server C中的A都会调用该函数,但由于!HasAuthority()则返回,所以只有Server C中的A正真执行了
所以服务器上的A改变了Server C中的A的Test的值,但服务器上没有调用OnRep函数
由于值改变了,触发所有客户端的OnRep函数,从而导致Client A中的A和Client B中的A不仅改变了Test值,还调用的OnRep函数
结果:Server C上看不见A改变了,但是Client A和Client B都能看见A变化了




{878D2013-DC0B-4100-9313-92BB42D78298}
当C触发BeginOverlap时,由于C是服务器控制的角色,所以只有Server C中的C执行了
所以服务器上的C改变了Server C中的C的Test的值,并且由于if (IsLocallyControlled()) OnRep_Test(); 所以服务器C他自己也能调用OnRep函数,从而让他自己看见变化
由于值改变了,触发所有客户端的OnRep函数,从而导致Client A中的A和Client B中的A不仅改变了Test值,还调用的OnRep函数
结果:Server C上看见C自己变化了,并且Client A和Client B都能看见C变化了




// 如果稍微修改代码,只让本地角色能看见变化(比如一些本地才能看见的Widget)
// 只需要将DOREPLIFETIME(ThisClass, Test); 改为:
DOREPLIFETIME_CONDITION(ThisClass, Test, COND_OwnerOnly); // 只复制给角色的拥有者

{25FFDC89-854C-45D1-83AF-757B0709298C}
当A触发BeginOverlap时,只有A的拥有者Client A才会收到复制的值并调用OnRep函数
结果:只有Client A上的A自己看见了变化




屏幕截图 2026-02-12 223828
当C触发BeginOverlap时,C的拥有者就是他自己,所以不会复制到其他客户端,但是由于if (IsLocallyControlled()) OnRep_Test(); 所以服务器C他自己调用OnRep函数
结果:只有Server C上的C自己看见了变化




// 如果想让所有人都能看见变化(现在客户端执行时,服务器是看不见变化的)
// 只需要将删除 if (IsLocallyControlled())判断即可
// if (IsLocallyControlled())
// {
       OnRep_Test();
// }

屏幕截图 2026-02-12 221658
现在,服务器上的A改变了Server C中的A的Test的值,并且直接调用了OnRep函数
结果:所有人都能看见A发生了变化

RPC

Client RPC:Server → 拥有者客户端(1对1)
Server RPC:拥有者客户端 → Server(1对1)
Multicast RPC:Server → 所有客户端 + Server 自身(1对多)

1.Server RPC
实现:现在需要实现当按下F时,装备武器
由于!HasAuthority() return;所以客户端无法执行任何逻辑,
这里跟OnSphereBeginOverlap不同,因为当客户端A与之重叠时,虽然不会执行,但是服务器上的A会进行重叠,
而这里客户端不会执行逻辑,只有服务器自己能够装备武器

屏幕截图 2026-02-12 220255
这时候需要在客户端A调用Server RPC从而让Server C上的A调用对应的逻辑,但此时Client A上的A不会执行任何逻辑
Server C上的A此时在本地调用所有逻辑,
由于武器的bReplicated = true,有些变量自带复制属性,所以会导致有些函数在服务器执行后,会直接通过自带的OnRep复制到客户端(比如AttachActor(), SetOwner()),
此时两个客户端执行了部分逻辑

但其他函数不会被执行,只能在服务器A上执行,这时候就需要另外添加一个复制变量来调用OnRep来复制这些函数,
现在两个客户端执行了所有的逻辑

Fabrik

在EventBlueprintUpdateAnimation中创建一个设置FabrikTransform的函数:
屏幕截图 2026-02-14 203313
通过获取武器上的LeftHandSocket的Transform,并获取自己的CharacterMesh来调用TransformToBoneSpace函数将Transform变为Bone的Transform:
屏幕截图 2026-02-14 203356
添加FABRIK,并配置根据右手的位置来移动左手和左臂:
屏幕截图 2026-02-14 203449
为武器添加LeftHandSocket:
屏幕截图 2026-02-14 203529

设置RightHand使武器始终转向瞄准方向(一般不用)

屏幕截图 2026-02-23 191302
屏幕截图 2026-02-23 191309
将TraceUnderCrosshairs放到Tick中,并将AimTargetLocation传递到动画蓝图中:
屏幕截图 2026-02-23 191332
在动画蓝图中设置从右手到目标位置的Rotation:
屏幕截图 2026-02-23 190729
添加Transform Bone,从而设置右手让武器对准瞄准的方向,在跑步时不启用,因为跑步时角色的动画中武器不会瞄准:
屏幕截图 2026-02-23 190649

游戏框架

屏幕截图 2026-02-27 113143
屏幕截图 2026-02-27 113823
屏幕截图 2026-02-27 114053

创建一个PlayerController,一个用于管理Widget的HUDManager
在PlayerController中NewObject这个HUDManager,这样任何人都能通过PlayerController来控制Widget
创建一个GameMode,在GameMode中配置当前游戏规则所需要的WidgetClass
创建一个PlayerState,由于GameMode只存在与服务器,所以PlayerState用于将WidgetClass复制到客户端,并通过PlayerController初始化
PlayerState还管理在玩家死亡/重生或换 Pawn 时保留的数据

void AMPS_GameMode::OnPostLogin(AController* NewPlayer)
{
	Super::OnPostLogin(NewPlayer);
	
	if (AMPS_PlayerState* PlayerState = NewPlayer->GetPlayerState<AMPS_PlayerState>())
	{
		PlayerState->HealthWidgetClass = HealthWidgetClass;
		PlayerState->NumberOfKillsWidgetClass = NumberOfKillsWidgetClass;
		PlayerState->bHUDConfigured = true; // 标志已经完成复制
		
		PlayerState->OnRep_bHUDConfigured();
	}
}

void AMPS_PlayerState::OnRep_bHUDConfigured()
{
	if (!bHUDConfigured) return;
	
	AMPS_PlayerController* PC = Cast<AMPS_PlayerController>(GetOwner());
	if (PC)
	{
		PC->InitHUD(this);
	}
}

客户端预测

模拟延迟:
屏幕截图 2026-04-07 114322
https://www.bilibili.com/video/BV1JM411U71c?t=771.8&p=48

屏幕截图 2026-04-08 193516
首先,在SpendRound在所有端都执行,所有端先直接减一。
如果是服务器,则调用ClientRPC告诉客户端权威值,如果是客户端,则使用Sequence来记录客户端已经预测了一次,
当服务器的ClientRPC到达客户端时,客户端已经预测了多次了,如果现在强型更新Ammo,就会导致客户端的Ammo被自己先前的RPC强制退回,然后又更新,导致抖动,
所以,在ClientRPC中,客户端的值必须要减去还没预测的值,也就是--Sequence

Server-Side-Rewind

服务器倒带算法的核心总结:服务器端记录一段时间内所有角色的位置信息(使用BoxComponent保存信息),客户端记录击中时间并RPC调用服务器倒带算法,此时的客户端击中的是角色的物理资产,服务器还原击中时间下的被击中角色位置信息,即服务器判断是否击中的BoxComponent,用于验证客户端是否击中角色。
在Character中添加BoxComponent:
屏幕截图 2026-04-19 181128
用于存储BoxComponent的Map:
屏幕截图 2026-04-19 181200
将BoxComponent全部添加到Map中,并设置其碰撞:
屏幕截图 2026-04-19 181218
在蓝图中设置Box的大小和位置:
屏幕截图 2026-04-19 181244
创建LagCompensationComponent并添加到角色中:
创建存储单个Box信息和一帧的对应时间和所有Box信息的Struct:
屏幕截图 2026-04-19 181327
创建每个角色对应的帧历史,使用双向链表存储:
屏幕截图 2026-04-19 181347
在Tick中每帧存储对应的Box信息到FrameHistory,当FrameHistory的时间跨度大于最大需要记录的时间,则删除链表尾部:
屏幕截图 2026-04-19 181420
创建服务器回溯的结果:
屏幕截图 2026-04-19 194601
本地预测的子弹攻击到对应的角色时,请求服务器回溯验证:
屏幕截图 2026-04-19 181649
调用ServerSideRewind从而获取到Result,然后执行ApplyDamage:
ServerSideRewind中先通过HitTime计算出对应时间的TargetFrame:
屏幕截图 2026-04-19 181745
根据HitTime计算出TargetFrame:
屏幕截图 2026-04-19 181802
使用插值计算TargetFrame:
屏幕截图 2026-04-19 182013
先存储当前时间的HitBox到CurrentFramePackage,然后将Box移动到TargetFrame,之后使用PredictProjectilePath来检测TargetFrame时刻是否被HitBox阻挡,检测完毕后将HitBox移动到原始位置CurrentFramePackage:
屏幕截图 2026-04-19 181915
存储当前的HitBox:
移动当前的HitBox:
屏幕截图 2026-04-19 182035
散弹枪本地预测的子弹攻击到对应的角色时,收集所有子弹后统一请求服务器回溯验证:
屏幕截图 2026-04-19 182254
散弹枪击中一个角色的结果,散弹枪所有子弹的结果,单个散弹枪子弹的命中数据:
屏幕截图 2026-04-19 194611
若击中角色,先将所有的子弹的数据存储到ShotGunPelletDatas,并延迟一段时间等待所有子弹都击中了之后,调用SendAllPelletsToRPC来调用Server_RequestShotGunServerSideRewind发送所有子弹的数据到服务器回溯验证:
屏幕截图 2026-04-19 182337
将所有被击中的角色和对应击中该角色的所有子弹发送到ShotGunServerSideRewind,ShotGunServerSideRewind遍历每个角色并计算所有子弹的伤害,最后叠加传入ShotGunResult:
屏幕截图 2026-04-19 182408

posted @ 2026-02-12 15:22  pone1  阅读(27)  评论(0)    收藏  举报