前言

作者在 2022-2-27 这一天的提交异常地多,涉及的教程内容也很多,这部分看了很久才看完。结合其他的内容横跨 49-75 集,但好在是跟下来了而且目前的效果与作者的基本一致,仅有一个小 bug,这个 bug 会导致角色在射击方向接近自己的脚下时,客户端的不会显示子弹的打击音效与火光。

这一部分的内容基本把角色的持枪动画完善了,还添加了声效与特效,此外针对 TPS 制作了准星以及对应的及其复杂的瞄准偏移动画,这部分设计动画的知识比较多;此外制作了子弹、弹壳与武器,现在一个射击游戏的基本元素已经搭建好了。

开火动作的网络同步

教程中使用了网络多播 RPC 来进行开火时间的广播。

c++
1
2
3
4
5
6
7
8
9
void FireButtonPressed(bool bPressed);

UFUNCTION(Server, Reliable)
// void ServerFire();
void ServerFire(const FVector_NetQuantize& TraceHitTarget);

UFUNCTION(NetMulticast, Reliable)
// void MulticastFire();
void MulticastFire(const FVector_NetQuantize& TraceHitTarget);
c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void UCombatComponent::FireButtonPressed(bool bPressed)
{
bFireButtonPressed = bPressed;
if (bFireButtonPressed)
{
// ServerFire();
FHitResult HitResult;
TraceUnderCrosshairs(HitResult);
ServerFire(HitResult.ImpactPoint);
}
}

void UCombatComponent::ServerFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
if (EquippedWeapon == nullptr) return;
if (Character)
{
MulticastFire(TraceHitTarget);
}
}

void UCombatComponent::MulticastFire_Implementation(const FVector_NetQuantize& TraceHitTarget)
{
Character->PlayFireMontage(bAiming);
EquippedWeapon->Fire(TraceHitTarget);
}

为什么这里要使用多播类型的 RPC 而不是设置一个 bPressedFireButton 来代表是否按下攻击键并标记为同步呢?

这样设计面对单发的武器是有效的,但面对机关枪这种按着鼠标就一直发射的枪械,bPressedFireButton 就只会在按下的瞬间同步一次,ue 在同步时会 因此客户端就只会播放一次射击动画以及仅调用一次 EquippedWeaponFire() 方法。因此这里使用多播 RPC,来让服务器端与客户端全部执行这一函数。但多播也就意味着成本比较大,因此要谨慎使用,避免占用太高的带宽。

射击方向的同步

通过将 HitResult.ImpactPoint 作为参数传递给 MulticastFire() 的方式来将本地的射击方向广播给其他游戏实例,其中的射击方向由以下函数决定,基本思想就是从视口发射射线进行检测,因为视口只有本地才可以获取,因此这个函数会在 FireButtonPressed() 中调用,再将结果传递给后续的函数。

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
void UCombatComponent::TraceUnderCrosshairs(FHitResult& TraceHitResult)
{
FVector2D ViewportSize;
if (GEngine && GEngine->GameViewport)
{
GEngine->GameViewport->GetViewportSize(ViewportSize);
}

FVector2D CrosshairLocation(ViewportSize.X / 2.f, ViewportSize.Y / 2.f);
FVector CrosshairWorldPosition;
FVector CrosshairWorldDirection;
bool bScreenToWorld = UGameplayStatics::DeprojectScreenToWorld(
UGameplayStatics::GetPlayerController(this, 0), // 对于每一个游戏示例来说,0 号玩家都是本地玩家
CrosshairLocation,
CrosshairWorldPosition,
CrosshairWorldDirection
);

if (bScreenToWorld)
{
FVector Start = CrosshairWorldPosition;

FVector End = Start + CrosshairWorldDirection * TRACE_LENGTH;

GetWorld()->LineTraceSingleByChannel(
TraceHitResult,
Start,
End,
ECollisionChannel::ECC_Visibility
);

if (!TraceHitResult.bBlockingHit)
{
TraceHitResult.ImpactPoint = End;
// HitTarget = End;
}
}
}

子弹打击效果的同步

由于碰撞事件只发生在服务器上,如果仅仅在 OnHit() 中播放效果的话仅有服务器才有显示,而我们又希望子弹在击中时执行回调播放音效及特效;教程中的做法是利用了 Destroyed() 函数的特点,该函数会在游戏物体被显式地摧毁时调用,并且已经做好了同步的工作,我们将音效及特效的播放放在这里,就可以实现服务器与客户端的同步了。

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void AProjectile::OnHit(UPrimitiveComponent* HitComp, AActor* OtherActor, UPrimitiveComponent* OtherComp,
FVector NormalImpulse, const FHitResult& Hit)
{
Destroy();
}

/// @brief 会在显式调用 Destroy() 时自动回调,由于 Projectile 被设置为了 replicate,因此在服务器和客户端都会调用
void AProjectile::Destroyed()
{
Super::Destroyed();

if (ImpactParticles)
{
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), ImpactParticles, GetActorTransform());
}
if (ImpactSound)
{
UGameplayStatics::PlaySoundAtLocation(GetWorld(), ImpactSound, GetActorLocation());
}
}

总结

这里只是初步分析了教程中新增加的关于网络同步部分功能的几个点,由于时间跨度较长也没有很仔细地分析,打算后面将游戏的网络同步部分结合整个游戏的框架进行更详细的分析,目前还是继续推进进度优先。