最近在学习UE网络同步,发现了一个非常好的教程,并且附带了项目文件,这里从这个小项目入手,理解UE的网络同步
教程链接:https://www.youtube.com/watch?v=JOJP0CvpB8w
项目链接:https://github.com/awforsythe/Repsi/
文章目录
Gamemode
从Gamemode入手,Gamemode
只位于服务端,可以用于设置 PlayerControllerClass
,DefaultPawnClass
等,也可对玩家角色进行相应的初始化操作。
我们在 ARepsiGameMode
中设置默认一些类,并且设置一个颜色数组用于对玩家设置相应的颜色,使用 SetPlayerDefaults
设置 PlayerPawn
相应颜色,因为 GameMode
只位于服务端,所以 RepsiPawn->AuthSetColor(PlayerColors[PlayerColorIndex]);
只在服务端调用,具体代码放到 Pawn 部分分析。
ARepsiGameMode::ARepsiGameMode(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
PlayerControllerClass = ARepsiPlayerController::StaticClass();
DefaultPawnClass = ARepsiPawn::StaticClass();
HUDClass = ARepsiHUD::StaticClass();
// Establish a sequence of arbitrary colors that we'll apply to each player
// pawn, round-robin in the order in which they're spawned
PlayerColors.Add(FLinearColor(0.30f, 0.02f, 0.02f));
PlayerColors.Add(FLinearColor(0.02f, 0.30f, 0.02f));
PlayerColors.Add(FLinearColor(0.02f, 0.02f, 0.30f));
LastPlayerColorIndex = -1;
}
void ARepsiGameMode::SetPlayerDefaults(APawn* PlayerPawn)
{
Super::SetPlayerDefaults(PlayerPawn);
// If we're initializing a newly-spawned player pawn, assign it a color
ARepsiPawn* RepsiPawn = Cast<ARepsiPawn>(PlayerPawn);
if (RepsiPawn)
{
// Use the next color in our sequence
const int32 PlayerColorIndex = (LastPlayerColorIndex + 1) % PlayerColors.Num();
if (PlayerColors.IsValidIndex(PlayerColorIndex))
{
// The AuthSetColor function is meant to be called exclusively on
// the server. We know this code only runs with authority, because
// the GameMode itself only exists on the server.
RepsiPawn->AuthSetColor(PlayerColors[PlayerColorIndex]);
LastPlayerColorIndex = PlayerColorIndex;
}
}
}
Pawn
Pawn内部的构造函数、PostInitializeComponents
会在服务端和每个客户端调用,这里 PostInitializeComponents
提供了针对 authority 的特殊执行流程,用于在服务端生成武器,让武器拥有指定的 Owner,所有权关系确定 -> 确定 relevance,这里武器的生成是用 Rep_Notify
的方式同步到客户端的,所以在服务端也加上了 OnRep_Weapon
让服务端也调用相应的代码。
void ARepsiPawn::PostInitializeComponents()
{
Super::PostInitializeComponents();
// If we're running on the server, spawn a Weapon actor, attach it to the
// WeaponHandle, and make sure we're its owner
if (HasAuthority())
{
// Ensure that we're the Owner, both so that bNetUseOwnerRelevancy works
// as expected on the Weapon, and so that the Weapon can trace its
// ownership back to a PlayerController that possesses this Pawn
FActorSpawnParameters SpawnInfo;
SpawnInfo.Owner = this;
SpawnInfo.Instigator = this;
SpawnInfo.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
// Spawn the Weapon - since AWeapon is replicated, the newly-spawned
// actor will be replicated to all clients where this pawn is relevant.
// Our Pawn's Weapon property is replicated, so that reference will be
// replicated to any client-side Pawns as well.
const FVector SpawnLocation = WeaponHandle->GetComponentLocation();
const FRotator SpawnRotation = WeaponHandle->GetComponentRotation();
Weapon = GetWorld()->SpawnActor<AWeapon>(SpawnLocation, SpawnRotation, SpawnInfo);
// Attach the weapon to the Pawn's WeaponHandle and add a tick
// dependency to make sure that this Pawn will always tick before the
// associated Weapon
OnRep_Weapon();
}
// Create a dynamic material instance so we can change the color of our
// cube on the fly. Again, this is the sort of thing that's better done in
// Blueprints, but we're handling it here for simplicity.
USkeletalMeshComponent* MeshComponent = GetMesh();
if (MeshComponent)
{
MeshMID = MeshComponent->CreateDynamicMaterialInstance(0);
}
}
OnRep_Weapon
主要将 Weapon
添加到对应组件下方,同时设置 Tick 依赖,保证 Weapon
的 Tick 依赖当前 Pawn 的 Tick。
void ARepsiPawn::OnRep_Weapon()
{
if (Weapon)
{
// Fix the weapon into place beneath our WeaponHandle component, and
// make sure that the Pawn ticks before the Weapon, since the Weapon's
// aiming motion is dependent on the aim location computed by the Pawn
Weapon->AttachToComponent(WeaponHandle, FAttachmentTransformRules::SnapToTargetIncludingScale);
Weapon->AddTickPrerequisiteActor(this);
}
}
AuthSetColor
在服务端调用用于设置 Pawn 的颜色,设置完颜色后会同步到客户端,客户端同步后会调用 OnRep_Color
,OnRep_Color
中就是设置mesh上材质的颜色
void ARepsiPawn::AuthSetColor(const FLinearColor& InColor)
{
// The "Auth" prefix is an unofficial convention used to indicate that a
// function is only meant to be called with authority - i.e. it's not a
// Server RPC, it's a function that should only be called by code that's
// already running on the server
checkf(HasAuthority(), TEXT("ARepsiPawn::AuthSetColor called on client"));
// Update our replicated Color property: this change will propagate to
// clients via replication, calling their OnRep_Color function as a notify
Color = InColor;
// Since we're the server, there's no replication to wait for: we want to
// immediately update our MID to reflect the new Color value. Since that's
// exactly what happens in our notify function, we can just call that
// function directly.
OnRep_Color();
}
OnFire
执行开火的逻辑,Weapon
调用服务端的 RPC,因为当前是在一个客户端开火(因为每个客户端只有一个属于自己的PlayerController),根据所有权关系,Weapon
是可以调用服务端的RPC的
void ARepsiPawn::OnFire()
{
// Forward the input to the weapon: input is processed on the client, so
// the weapon will have to issue a Server RPC in order to notify the server
// that the player wants to fire the weapon. The weapon is able to issue
// Server RPCs because it's owned by the client connection: the
// PlayerController owns this Pawn, and this Pawn owns the Weapon.
if (Weapon)
{
Weapon->HandleFireInput();
}
}
GetLifetimeReplicatedProps
中也设置了该 Pawn 要同步的属性

void ARepsiPawn::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ARepsiPawn, Weapon);
DOREPLIFETIME(ARepsiPawn, Color);
}
Tick
中执行线条检测,更新Weapon的瞄准位置,这在客户端和服务端都执行,因为这并不像处理伤害的线条检测,不影响gameplay,只是更新 Weapon 的旋转,所以客户端不用等待服务端的通知,它们可以通过pawn的旋转自动计算得到。
void ARepsiPawn::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// If we have a Weapon, run a per-frame line trace into the scene to
// determine what we're aiming at - i.e., find the closest blocking geometry
// that's centered in front of our view. Note that this happens
// independently on the server and all clients: this trace isn't
// server-authoritative; it's just cosmetic. In other words, the results of
// this trace don't affect gameplay (unlike, for example, a trace that deals
// damage when the weapon is fired); it's simply used to update the rotation
// of the weapon to show what the player is aiming at. Therefore clients
// don't need to wait on the server to tell them where to aim, they can just
// figure out for themselves based on replicated information that they
// already have (namely, the transform of the pawn).
if (Weapon)
{
// Get our view location and forward vector: on clients, the mesh
// component moves smoothly with client prediction and interpolation
// (whereas the actor itself will have stuttery movement), so it's
// preferable to get our forward vector from it
const FVector ViewLocation = GetPawnViewLocation();
const FTransform ViewTransform = GetMesh() ? GetMesh()->GetComponentTransform() : GetActorTransform();
const FVector ViewForward = ViewTransform.GetUnitAxis(EAxis::X);
// Prepare a line trace to find the first blocking primitive beneath the
// center of our view
const FVector& TraceStart = ViewLocation;
const FVector TraceEnd = TraceStart + (ViewForward * AimTraceDistance);
const FName ProfileName = UCollisionProfile::BlockAllDynamic_ProfileName;
const FCollisionQueryParams QueryParams(TEXT("PlayerAim"), false, this);
// Run the trace and convey the resulting location to the Weapon. If we
// hit something, that's what we're aiming at; if we don't hit anything,
// then just aim downrange toward the point where our trace stopped.
FHitResult Hit;
FVector WorldAimLocation;
if (GetWorld()->LineTraceSingleByProfile(Hit, TraceStart, TraceEnd, ProfileName, QueryParams))
{
WorldAimLocation = Hit.ImpactPoint;
}
else
{
WorldAimLocation = TraceEnd;
}
// Transform our world location into view space so that our weapon can
// make an informed decision about whether that location is valid for
// it to actually point to (it may be too close, or at an extreme angle)
const FVector ViewAimLocation = ViewTransform.InverseTransformPosition(WorldAimLocation);
Weapon->UpdateAimLocation(WorldAimLocation, ViewAimLocation);
}
}
Weapon
Weapon 和 Pawn一样,其所有者是对应的 Pawn,其在服务端生成,同步复制到客户端。
其在构造函数中设置复制相关属性
// Make sure that weapons will be replicated as long as their owning Pawn
// is replicated
bReplicates = true;
bNetUseOwnerRelevancy = true;
Tick
在每一帧更新Weapon的旋转
void AWeapon::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
if (bAimLocationIsValid)
{
const FVector AimDisplacement = AimLocation - GetActorLocation();
const FVector AimDirection = AimDisplacement.GetSafeNormal();
const FQuat TargetRotation = AimDirection.ToOrientationQuat();
const FQuat NewRotation = FMath::QInterpTo(GetActorQuat(), TargetRotation, DeltaSeconds, AimInterpSpeed);
SetActorRotation(NewRotation);
}
else
{
AActor* AttachParent = GetAttachParentActor();
const FQuat TargetRotation = AttachParent ? AttachParent->GetActorTransform().TransformRotation(FQuat(DropRotation)) : FQuat(DropRotation);
const FQuat NewRotation = FMath::QInterpTo(GetActorQuat(), TargetRotation, DeltaSeconds, DropInterpSpeed);
SetActorRotation(NewRotation);
}
}
GetLifetimeReplicatedProps
设置要同步的属性,这里的 COND_SkipOwner
表示有条件同步,忽略掉Owner所在的客户端,这里的Owner在之前的Pawn里设置(服务端设置)
void AWeapon::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME_CONDITION(AWeapon, LastFirePacket, COND_SkipOwner);
}
HandleFireInput
用于处理开火输入,调用 ServerRPC Server_TryFire
,这里 !HasAuthority
中的语句主要用于 Standartalone模式和Listen Server模式,此时是播放音效和粒子效果已经Server端处理过了(这里服务端和客户端是在一台机器上),不需要再执行一次,所以放在 !HasAuthority
下。
void AWeapon::HandleFireInput()
{
// This function is being called on the local client to let us know that
// they've pressed the fire button. We'll do an initial client-side cooldown
// check just to avoid spamming the server with unnecessary RPCs, but this
// isn't an authoritative cooldown check: that's up to the server
const float CurrentTime = GetWorld()->GetTimeSeconds();
const float ElapsedSinceLastFire = CurrentTime - LastFireTime;
if (ElapsedSinceLastFire >= FireCooldown)
{
// Issue a Server RPC to notify the server that the client wants to
// fire: the server will then make its own authoritative decision and
// update game state accordingly. Note that if we're playing standalone,
// this call just runs locally: it doesn't travel over the network, but
// the end result is the same (and technically the separate client and
// server cooldown checks are redundant in that case, but that's not
// significant enough to warrant special-case logic for single-player)
const FVector MuzzleLocation = MuzzleHandle->GetComponentLocation();
const FVector Direction = MuzzleHandle->GetComponentQuat().Vector();
Server_TryFire(MuzzleLocation, Direction);
LastFireTime = CurrentTime;
// If this weapon, belonging to the locally controlled player, has
// authority, that means we're either running standalone or as a listen
// server. In that case, Server_TryFire will execute right away, and it
// will spawn cosmetic effects right away. But if we *don't* have
// authority, we're running as a client, which means there's latency
// between when the player fires the weapon and when the server decides
// where the shot lands. So if we're a client, we want to spawn cosmetic
// effects (particles, sound) right away, rather than waiting for the
// server to tell us with 100% certainty that we've successfully fired).
// This means our effects are non-authoritative (i.e. we're speculating
// about what we hit and hoping that we're right, but the server has the
// final say and might disagree), but it gives the player immediate
// visual feedback, without which their game would feel laggy.
if (!HasAuthority())
{
PlayFireEffects();
// Run a cosmetic line trace just to see whether we should spawn an
// impact effect
FHitResult Hit;
if (RunFireTrace(Hit))
{
const bool bWillProbablyCauseDamage = Hit.Actor.IsValid() && Hit.Actor->CanBeDamaged();
PlayImpactEffects(Hit.ImpactPoint, Hit.ImpactNormal, bWillProbablyCauseDamage);
}
}
}
else
{
// If the weapon is still on cooldown, play a click sound
PlayUnableToFireEffects();
}
}
Server_TryFire_Implementation
服务端RPC,执行开火的伤害应用,同时更新 LastFirePacket
供其他非owner客户端同步,播放音效和粒子效果,因为这里使用的是 UGameplayStatics
对服务端不会产生(Dedicated Server的情况)影响,所以不用放在 !IsRunningDedicatedServer()
下。
void AWeapon::Server_TryFire_Implementation(const FVector& MuzzleLocation, const FVector& Direction)
{
// We're now running with authority: whereas HandleFireInput is repsonsible
// for responding to the input by deciding if we should ask the server to
// fire, Server_TryFire is responsible for making the final, authoritative
// decision about whether we should fire a shot. We need to do our own
// cooldown check with authority; we can't simply trust the client, since
// simple hacks would allow a player to bypass the cooldown check and issue
// server RPCs whenever they wanted.
const float CurrentTime = GetWorld()->GetTimeSeconds();
const float ElapsedSinceLastFire = CurrentTime - LastFireTime;
if (ElapsedSinceLastFire >= FireCooldown)
{
// Cache our last fire time: note that LastFireTime isn't replicated;
// it's updated independently on the server and on clients
LastFireTime = CurrentTime;
// Update our LastFirePacket to reflect that the server has allowed the
// weapon to fire: this will replicate to non-owning clients (i.e. it
// won't be sent to the player who originally issued this RPC), causing
// their OnRep_LastFirePacket function to be called
LastFirePacket.ServerFireTime = CurrentTime;
// Run a server-authoritative line trace to determine if there's
// any blocking geometry in the path of the shot, and if so, whether
// that's a primitive belonging to an actor who we might deal damage to
FHitResult Hit;
if (RunFireTrace(Hit))
{
// If we hit a damageable actor, attempt to damage it
float DamageCaused = 0.0f;
if (Hit.Actor.IsValid() && Hit.Actor->CanBeDamaged())
{
const float BaseDamage = 1.0f;
const FPointDamageEvent DamageEvent(BaseDamage, Hit, Direction, UDamageType_WeaponFire::StaticClass());
DamageCaused = Hit.Actor->TakeDamage(BaseDamage, DamageEvent, GetInstigatorController(), this);
}
// Spawn particle effects and audio server-side: note that we're
// using UGameplayStatics functions which have no effect on
// dedicated server, so while we could gate these calls behind
// IsRunningDedicatedServer(), it's not strictly necessary. Either
// way, these function calls take effect if we're running standalone
// or as listen server.
PlayFireEffects();
PlayImpactEffects(Hit.ImpactPoint, Hit.ImpactNormal, DamageCaused > 0.0f);
// Propagate the details of our hit to non-owning clients: this
// gives them everything they need to know in order to spawn their
// own cosmetic effectst
LastFirePacket.bCausedDamage = DamageCaused > 0.0f;
LastFirePacket.ImpactPoint = Hit.ImpactPoint;
LastFirePacket.ImpactNormal = Hit.ImpactNormal;
}
else
{
// Set ImpactNormal to zero as a sentinel to indicate that this
// shot didn't hit anything
LastFirePacket.ImpactNormal = FVector::ZeroVector;
}
}
}
TargetSphere
TargetSphere
的构造函数内设置了网络同步的剔除距离
// Enable replication, and set a relatively short cull distance for testing
bReplicates = true;
NetCullDistanceSquared = FMath::Square(1500.0f);
GetLifetimeReplicatedProps
设置 Color
为要同步的属性
void ATargetSphere::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ATargetSphere, Color);
}
Tick
逐渐改变颜色,并在颜色更新完成后关闭 Tick
void ATargetSphere::Tick(float DeltaSeconds)
{
Super::Tick(DeltaSeconds);
// Compute our relative progress in the color change animation
const float CurrentTime = GetWorld()->GetTimeSeconds();
const float ColorChangeElapsed = CurrentTime - LastColorChangeTime;
const float ColorChangeAlpha = ColorChangeDuration < KINDA_SMALL_NUMBER ? 1.0f : FMath::Min(1.0f, ColorChangeElapsed / ColorChangeDuration);
// Start interpolating toward a brighter version of new color, settling on
// the unmodified color at the end
const FLinearColor TargetColor = Color * FMath::Lerp(10.0f, 1.0f, ColorChangeAlpha);
const FLinearColor NewColor = FLinearColor::LerpUsingHSV(PreviousColor, TargetColor, ColorChangeAlpha);
if (MeshMID)
{
MeshMID->SetVectorParameterValue(TEXT("Color"), NewColor);
}
// Disable ticking once the animation is finished
if (ColorChangeAlpha >= 1.0f)
{
SetActorTickEnabled(false);
}
}
这里 Color 的同步也是通过 Rep_Notify
的方式,所以对应的 OnRep_Color
如下,在 Color 同步时启动 Tick
void ATargetSphere::OnRep_Color()
{
if (MeshMID)
{
PreviousColor = MeshMID->K2_GetVectorParameterValue(TEXT("Color"));
}
LastColorChangeTime = GetWorld()->GetTimeSeconds();
SetActorTickEnabled(true);
}
InternalTakePointDamage
在应用点伤害时调用的方法,这里仅在服务端调用
float ATargetSphere::InternalTakePointDamage(float Damage, FPointDamageEvent const& PointDamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
// If shot with a Weapon, accept the damage event and randomize the sphere's Color
if (PointDamageEvent.DamageTypeClass == UDamageType_WeaponFire::StaticClass())
{
const ARepsiPawn* Pawn = EventInstigator ? EventInstigator->GetPawn<ARepsiPawn>() : nullptr;
if (Pawn)
{
Color = Pawn->Color;
OnRep_Color();
}
return Damage;
}
return Super::InternalTakePointDamage(Damage, PointDamageEvent, EventInstigator, DamageCauser);
}