UE网络同步(二) —— 一个项目入门UE网络同步之代码分析

最近在学习UE网络同步,发现了一个非常好的教程,并且附带了项目文件,这里从这个小项目入手,理解UE的网络同步

教程链接:https://www.youtube.com/watch?v=JOJP0CvpB8w
项目链接:https://github.com/awforsythe/Repsi/


Gamemode

从Gamemode入手,Gamemode 只位于服务端,可以用于设置 PlayerControllerClassDefaultPawnClass 等,也可对玩家角色进行相应的初始化操作。

我们在 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_ColorOnRep_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 要同步的属性

扫描二维码关注公众号,回复: 17531630 查看本文章
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);
}

猜你喜欢

转载自blog.csdn.net/weixin_44491423/article/details/141155013