AActor* UWorld::SpawnActor( UClass* Class, FVector const* Location, FRotator const* Rotation, const FActorSpawnParameters& SpawnParameters ) { ... AActor* const Actor = NewObject<AActor>(LevelToSpawnIn, Class, NewActorName, ActorFlags, Template, false/*bCopyTransientsFromClassDefaults*/, nullptr/*InInstanceGraph*/, ExternalPackage); // Add this newly spawned actor to the network actor list. Do this after PostSpawnInitialize so that actor has "finished" spawning. AddNetworkActor( Actor ); return Actor; }
int32 UNetDriver::ServerReplicateActors(float DeltaSeconds) { const int32 NumClientsToTick = ServerReplicateActors_PrepConnections( DeltaSeconds ); // Build the consider list (actors that are ready to replicate) ServerReplicateActors_BuildConsiderList( ConsiderList, ServerTickTime );
for ( int32 i=0; i < ClientConnections.Num(); i++ ) { constbool bProcessConsiderListIsBound = OnProcessConsiderListOverride.IsBound(); if (Connection->ViewTarget) { if (!bProcessConsiderListIsBound) { FActorPriority* PriorityList = NULL; FActorPriority** PriorityActors = NULL;
// Get a sorted list of actors for this connection const int32 FinalSortedCount = ServerReplicateActors_PrioritizeActors(Connection, ConnectionViewers, ConsiderList, bCPUSaturated, PriorityList, PriorityActors);
// Process the sorted list of actors for this connection TInterval<int32> ActorsIndexRange(0, FinalSortedCount); const int32 LastProcessedActor = ServerReplicateActors_ProcessPrioritizedActorsRange(Connection, ConnectionViewers, PriorityActors, ActorsIndexRange, Updated);
floatAActor::GetNetPriority(const FVector& ViewPos, const FVector& ViewDir, AActor* Viewer, AActor* ViewTarget, UActorChannel* InChannel, float Time, bool bLowBandwidth) { if (bNetUseOwnerRelevancy && Owner) { // If we should use our owner's priority, pass it through return Owner->GetNetPriority(ViewPos, ViewDir, Viewer, ViewTarget, InChannel, Time, bLowBandwidth); }
if (ViewTarget && (this == ViewTarget || GetInstigator() == ViewTarget)) { // If we're the view target or owned by the view target, use a high priority Time *= 4.f; } elseif (!IsHidden() && GetRootComponent() != NULL) { // If this actor has a location, adjust priority based on location FVector Dir = GetActorLocation() - ViewPos; float DistSq = Dir.SizeSquared();
// Adjust priority based on distance and whether actor is in front of viewer if ((ViewDir | Dir) < 0.f) { if (DistSq > NEARSIGHTTHRESHOLDSQUARED) { Time *= 0.2f; } elseif (DistSq > CLOSEPROXIMITYSQUARED) { Time *= 0.4f; } } elseif ((DistSq < FARSIGHTTHRESHOLDSQUARED) && (FMath::Square(ViewDir | Dir) > 0.5f * DistSq)) { // Compute the amount of distance along the ViewDir vector. Dir is not normalized // Increase priority if we're being looked directly at Time *= 2.f; } elseif (DistSq > MEDSIGHTTHRESHOLDSQUARED) { Time *= 0.4f; } }
return NetPriority * Time; }
默认会根据 Actor 处于观察者的位置来计算优先级,如果 DotProduct < 0 则是背面,根据距离来调整优先级,若在正面,且视线相近则放大。
前面几篇提到过,Actor 是基于 ActorChannel 同步的,服务端需要通知客户端创建一个 ActorChannel,然后专门为该 Actor 进行同步。
首次同步,会为该 Actor 在本地创建 ActorChannel。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
int32 UNetDriver::ServerReplicateActors_ProcessPrioritizedActorsRange( UNetConnection* Connection, const TArray<FNetViewer>& ConnectionViewers, FActorPriority** PriorityActors, const TInterval<int32>& ActorsIndexRange, int32& OutUpdated, bool bIgnoreSaturation ) { for (...) { // Create a new channel for this actor. Channel = (UActorChannel*)Connection->CreateChannelByName( NAME_Actor, EChannelCreateFlags::OpenedLocally ); if ( Channel ) { Channel->SetChannelActor(Actor, ESetChannelActorFlags::None); } if ( Channel->ReplicateActor() ) { } } }
// Write out NetGUID to caller if necessary if (OutNetGUID) { *OutNetGUID = NetGUID; }
// Write object NetGUID to the given FArchive InternalWriteObject( Ar, NetGUID, Object, TEXT( "" ), NULL );
// If we need to export this GUID (its new or hasnt been ACKd, do so here) if (!NetGUID.IsDefault() && Object && ShouldSendFullPath(Object, NetGUID)) { if ( !ExportNetGUID( NetGUID, Object, TEXT(""), NULL ) ) { UE_LOG( LogNetPackageMap, Verbose, TEXT( "Failed to export in ::SerializeObject %s"), *Object->GetName() ); } }
if (ExportFlags.bHasPath) { if (Object != nullptr) { // If the object isn't nullptr, expect an empty path name, then fill it out with the actual info check(ObjectOuter == nullptr); check(ObjectPathName.IsEmpty()); ObjectPathName = Object->GetName(); ObjectOuter = Object->GetOuter(); }
到这里 Actor 同步的主要流程就都清楚了,后续就是补充上面未提到的一些东西。在首次序列化 Actor 时 允许重写 OnSerializeNewActor 来追加你想传递的信息,比如 PlayerController 就追加了 NetPlayerIndex ,当首次同步 Actor 时,可以通过 OnActorChannelOpen 将其读出。
1 2 3 4 5 6 7 8 9 10 11 12
/** * SerializeNewActor has just been called on the actor before network replication (server side) * @param OutBunch Bunch containing serialized contents of actor prior to replication */ virtualvoidAActor::OnSerializeNewActor(class FOutBunch& OutBunch){};
/** * Allows for a specific response from the actor when the actor channel is opened (client side) * @param InBunch Bunch received at time of open * @param Connection the connection associated with this actor */ virtualvoidAActor::OnActorChannelOpen(class FInBunch& InBunch, class UNetConnection* Connection){};
读取同步 Actor 的 Bunch逻辑在 void UActorChannel::ProcessBunch( FInBunch & Bunch ) 此处就不再重复了,都是同样的几个函数,根据 Ar 读取写入模式来区分逻辑。
/** Creates a new FRepLayout for the given class. */ ENGINE_API static TSharedPtr<FRepLayout> CreateFromClass(UClass* InObjectClass, const UNetConnection* ServerConnection = nullptr, const ECreateRepLayoutFlags Flags = ECreateRepLayoutFlags::None);
/** Creates a new FRepLayout for the given struct. */ ENGINE_API static TSharedPtr<FRepLayout> CreateFromStruct(UStruct * InStruct, const UNetConnection* ServerConnection = nullptr, const ECreateRepLayoutFlags Flags = ECreateRepLayoutFlags::None);
/** Creates a new FRepLayout for the given function. */ static TSharedPtr<FRepLayout> CreateFromFunction(UFunction* InFunction, const UNetConnection* ServerConnection = nullptr, const ECreateRepLayoutFlags Flags = ECreateRepLayoutFlags::None);
// Track properties so me can ensure they are sorted by offsets at the end TArray<FProperty*> NetProperties; for (TFieldIterator<FField> It(this, EFieldIteratorFlags::ExcludeSuper); It; ++It) { if (FProperty* Prop = CastField<FProperty>(*It)) { if ((Prop->PropertyFlags & CPF_Net) && Prop->GetOwner<UObject>() == this) { NetProperties.Add(Prop); } } } }
这里找出所有 RPC函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
for(TFieldIterator<UField> It(this,EFieldIteratorFlags::ExcludeSuper); It; ++It) { if (UFunction * Func = Cast<UFunction>(*It)) { // When loading reflection data (e.g. from blueprints), we may have references to placeholder functions, or reflection data // in children may be out of date. In that case we cannot enforce this check, but that is ok because reflection data will // be regenerated by compile on load anyway: constbool bCanCheck = (!GIsEditor && !IsRunningCommandlet()) || !Func->HasAnyFlags(RF_WasLoaded); check(!bCanCheck || (!Func->GetSuperFunction() || (Func->GetSuperFunction()->FunctionFlags&FUNC_NetFuncFlags) == (Func->FunctionFlags&FUNC_NetFuncFlags))); if ((Func->FunctionFlags&FUNC_Net) && !Func->GetSuperFunction()) { NetFields.Add(Func); } } }
/** * If the Property is a C-Style fixed size array, then a command will be created for every element in the array. * This is the index of the element in the array for which the command represents. * * This will always be 0 for non array properties. */ int32 ArrayIndex;
/** Absolute offset of property in Object Memory. */ int32 Offset;
/** Absolute offset of property in Shadow Memory. */ int32 ShadowOffset;
/** * CmdStart and CmdEnd define the range of FRepLayoutCommands (by index in FRepLayouts Cmd array) of commands * that are associated with this Parent Command. * * This is used to track and access nested Properties from the parent. */ uint16 CmdStart;
*These structs will not have Child Rep Commands, but they will still have Parent Commands. This is because we generally don't care about their Memory Layout, but we need to be able to initialize them properly.*
*These structs will have a single Child Rep Command for the FStructProperty. Similar to NetDeltaSerialize, we don't really care about the memory layout of NetSerialize structs, but we still need to know where they live so we can diff them, etc.*
StackParams.Offset += GetOffsetForProperty<BuildType>(*StructProp); if (EnumHasAnyFlags(Struct->StructFlags, STRUCT_NetSerializeNative)) { UE_CLOG(EnumHasAnyFlags(Struct->StructFlags, STRUCT_NetDeltaSerializeNative), LogRep, Warning, TEXT("RepLayout InitFromProperty_r: Struct marked both NetSerialize and NetDeltaSerialize: %s"), *StructProp->GetName());
SharedParams.bHasNetSerializeProperties = true; if (ERepBuildType::Class == BuildType && GbTrackNetSerializeObjectReferences && nullptr != SharedParams.NetSerializeLayouts && !EnumHasAnyFlags(Struct->StructFlags, STRUCT_IdenticalNative)) { // We can't directly rely on FProperty::Identical because it's not safe for GC'd objects. // So, we'll recursively build up set of layout commands for this struct, and if any // are Objects, we'll use that for storing items in Shadow State and comparison. // Otherwise, we'll fall back to the old behavior. const int32 PrevCmdNum = SharedParams.Cmds.Num();
if (StackParams.RecursingNetSerializeStruct == NAME_None) { if (NewSharedParams.bHasObjectProperties) { // If this is a top level Net Serialize Struct, and we found any any objects, // then we need to make sure this is tracked in our map. SharedParams.NetSerializeLayouts->Add(SharedParams.Cmds.Num(), MoveTemp(TempCmds)); StackParams.bNetSerializeStructWithObjects = true; } } elseif (!NewSharedParams.bHasObjectProperties) { // If this wasn't a top level Net Serialize Struct, and we didn't find any objects, // we need to remove any nested entries we added to the Net Serialize Struct's layout. // Instead, we'll assume this layout is FProperty safe, and add it as single command (below). SharedParams.Cmds.SetNum(PrevCmdNum); } else { // This wasn't a top level Net Serialize Struct, but we did find some objects. // We want to keep the layout we generated, so keep that layout return NetSerializeStructOffset; } }
// Initialize lifetime props // Properties that replicate for the lifetime of the channel TArray<FLifetimeProperty> LifetimeProps; LifetimeProps.Reserve(Parents.Num());
// Once we hit an array, start using a stack based approach CompareProperties_Array_r(SharedParams, NewStackParams, CmdIndex, Handle); CmdIndex = Cmd.EndCmd - 1; // The -1 to handle the ++ in the for loop continue; } elseif (SharedParams.bForceFail || !PropertiesAreIdentical(Cmd, ShadowData.Data, Data.Data, SharedParams.NetSerializeLayouts)) { StoreProperty(Cmd, ShadowData.Data, Data.Data); StackParams.Changed.Add(Handle); } }
return Handle; }
这里需要特别注意对动态数组的处理,因为动态数组你不知道具体是有多少个,你只能写入有多少个值变更了,然后写入具体变更的 Handle,计算方式也很简单,index * 子元素数量 + 改变的子元素handle,若动态数组存放的是一个 int,则子元素数量为1,可简化为 index + 1。
1 2 3 4
StackParams.Changed.Add(Handle); StackParams.Changed.Add((uint16)NumChangedEntries); // This is so we can jump over the array if we need to StackParams.Changed.Append(ChangedLocal); StackParams.Changed.Add(0);
也有可能数组长度减小,但数组原有的那部分完全一致,就不需要变更。
1 2 3 4 5 6 7 8 9 10
elseif (ArrayNum != ShadowArrayNum) { // If nothing below us changed, we either shrunk, or we grew and our inner was an array that didn't have any elements check(ArrayNum < ShadowArrayNum || SharedParams.Cmds[CmdIndex + 1].Type == ERepLayoutCmdType::DynamicArray);
// Array got smaller, send the array handle to force array size change StackParams.Changed.Add(Handle); StackParams.Changed.Add(0); StackParams.Changed.Add(0); }
boolFRepLayout::ReplicateProperties( FSendingRepState* RESTRICT RepState, FRepChangelistState* RESTRICT RepChangelistState, const FConstRepObjectDataBuffer Data, UClass* ObjectClass, UActorChannel* OwningChannel, FNetBitWriter& Writer, const FReplicationFlags& RepFlags)const { // Gather all change lists that are new since we last looked, and merge them all together into a single CL for (int32 i = RepState->LastChangelistIndex; i < RepChangelistState->HistoryEnd; ++i) { const int32 HistoryIndex = i % FRepChangelistState::MAX_CHANGE_HISTORY;
// Merge in newly active properties so they can be sent. if (NewlyActiveChangelist.Num() > 0) { TArray<uint16> Temp = MoveTemp(Changed); MergeChangeList(Data, NewlyActiveChangelist, Temp, Changed); } }
// Merge in the PreOpenAckHistory (unreliable properties sent before the bunch was initially acked) if (bFlushPreOpenAckHistory) { for (int32 i = 0; i < RepState->PreOpenAckHistory.Num(); i++) { TArray<uint16> Temp = MoveTemp(Changed); MergeChangeList(Data, RepState->PreOpenAckHistory[i].Changed, Temp, Changed); } RepState->PreOpenAckHistory.Empty(); } } else { // Nothing changed and there are no nak's, so just do normal housekeeping and remove acked history items UpdateChangelistHistory(RepState, ObjectClass, Data, OwningChannel->Connection, nullptr); returnfalse; }
一个小优化,共享序列化好的数据,避免反复序列化。
1 2 3 4 5 6 7 8
if (!OwningChannel->Connection->IsInternalAck() && (GNetSharedSerializedData != 0)) { // if no shared serialization info exists, build it if (!RepChangelistState->SharedSerialization.IsValid()) { BuildSharedSerialization(Data, Changed, true, RepChangelistState->SharedSerialization); } }
int64 UActorChannel::ReplicateActor() { if (bWroteSomethingImportant) { // We must exit the collection scope to report data correctly FPacketIdRange PacketRange = SendBunch( &Bunch, 1 );
if (!bIsNewlyReplicationPaused) { for (auto RepComp = ReplicationMap.CreateIterator(); RepComp; ++RepComp) { RepComp.Value()->PostSendBunch(PacketRange, Bunch.bReliable); } } } }
属性同步丢包
收到 Nak 后,会通知到 ActorChannel,随后通知到该 Packet 所携带的 Actor 的 FObjectReplicator 中。
voidFObjectReplicator::ReceivedNak( int32 NakPacketId ) { const UObject* Object = GetObject(); if (!RepLayout->IsEmpty()) { if (FSendingRepState* SendingRepState = RepState.IsValid() ? RepState->GetSendingRepState() : nullptr) { SendingRepState->CustomDeltaChangeIndex--; // Go over properties tracked with histories, and mark them as needing to be resent. for (int32 i = SendingRepState->HistoryStart; i < SendingRepState->HistoryEnd; ++i) { const int32 HistoryIndex = i % FSendingRepState::MAX_CHANGE_HISTORY;