这篇文章主要以问题的形式,针对UE同步的各个方面的内容,做一个详细而充分的讨论。对于新手理解UE的同步机制非常有帮助,对于有一定的基础而没有深入的UE程序也或许有一些启发。如果想深入了解同步的实现原理,可以参考 UE4网络同步(二)——深入同步细节
问题一:如何理解Actor与其所属连接?
附加:1. Actor的Role是ROLE_Authority就是服务端么?
问题二:你真的会用RPC么?
附加:1. 多播MultiCast RPC会发送给所有客户端么?
问题三:COND_InitialOnly怎么用?
问题四:客户端与服务器一致么?
问题五:属性同步的基本规则是?
附加:1. 结构体的属性同步有什么特别的?
问题六:组件同步的基本规则是?
Tips:同步注意的一些小细节
https://api.unrealengine.com/CHN/Gameplay/Networking/Actors/OwningConnections/index.html。UE4官网关于网络链接这一块其实已经将的比较详细了,不过有一些内容没有经验的读者看起来可能还是比较吃力。
按照官网的顺序,我一点点给出我的分析与理解。首先,大家要简单了解一些客户端的连接过程。
主要步骤如下:
1.客户端发送连接请求。
2.如果服务器接受连接,则发送当前地图。
3.服务器等待客户端加载此地图。
4.加载之后,服务器将在本地调用 AGameMode::PreLogin。这样可以使 GameMode 有机会拒绝连接
5.如果接受连接,服务器将调用 AGameMode::Login该函数的作用是创建一个 PlayerController,可用于在今后复制到新连接的客户端。成功接收后,这个 PlayerController 将替代客户端的临时PlayerController (之前被用作连接过程中的占位符)。
此时将调用 APlayerController::BeginPlay。应当注意的是,在此 actor 上调用RPC 函数尚存在安全风险。您应当等待 AGameMode::PostLogin 被调用完成。
6.如果一切顺利,AGameMode::PostLogin 将被调用。
这时,可以放心的让服务器在此 PlayerController 上开始调用RPC 函数。
那么这里面第5点需要重点强调一下。我们知道所谓连接,不过就是客户端连接到一个服务器,在维持着这个连接的条件下,我们才能真正的玩“网络游戏”。通常,如果我们想让服务器把某些特定的信息发送给特定的客户端,我们就需要找到服务器与客户端之间的这个连接。这个链接的信息就存储在PlayerController的里面,而这个PlayerController不能是随随便便创建的PlayerController,一定是客户端第一次链接到服务器,服务器同步过来的这个PlayerController(也就是上面的第五点,后面称其为拥有连接的PlayerController)。进一步来说,这个Controller里面包含着相关的NetDriver,Connection以及Session信息。
对于任何一个Actor(客户端上),他可以有连接,也可以无连接。一旦Actor有连接,他的Role(控制权限)就是ROLE_AutonomousProxy,如果没有连接,他的Role(控制权限)就是ROLE_SimulatedProxy 。
那么对于一个Actor,他有三种方法来得到这个连接(或者说让自己属于这个连接):
1.设置自己的owner为拥有连接的PlayerController,或者自己owner的owner为拥有连接的PlayerController。也就说官方文档说的查找他最外层的owner是否是PlayerController而且这个PlayerController拥有连接。
2.这个Actor必须是Pawn并且Possess了拥有连接的PlayerController。这个例子就是我们打开例子程序时,开始控制一个角色的情况。我们控制的这个角色就拥有这个连接。
3.这个Actor设置自己的owner为拥有连接的Pawn。这个区别于第一点的就是,Pawn与Controller的绑定方式不是通过Owner这个属性。而是Pawn本身就拥有Controller这个属性。所以Pawn的Owner可能为空。 (Owner这个属性在Actor里面,蓝图也可以通过GetOwner来获取)
对于组件来说,那就是先获取到他所归属的那个Actor,然后再通过上面的条件来判断。
我这里举几个例子,玩家PlayerState的owner就是拥有连接的PlayerController,Hud的owner是拥有连接的PlayerController,CameraActor的owner也是拥有连接的PlayerController。而客户端上的其他NPC(一定是在服务器创建的)是都没有owner的Actor,所以这些NPC都是没有连接的,他们的Role就为ROLE_SimulatedProxy。
所以我们发现这些与客户端玩家控制息息相关的Actor才拥有所谓的连接。不过,进一步来讲,我们要这连接还有什么用?好吧,照搬官方文档。
连接所有权是以下情形中的重要因素:
1.RPC需要确定哪个客户端将执行运行于客户端的 RPC
2.Actor复制与连接相关性
3.在涉及所有者时的 Actor 属性复制条件
对于RPC,我们知道,UE4里面在Actor上调用RPC函数,可以实现类似在客户端与服务器之间发送可执行的函数的功能。最基本的,当我一个客户端拥有ROLE_AutonomousProxy权限的Actor在服务器代码里调用RPC函数(UFUNCTION(Reliable,Client))时,我怎么知道应该去众多的客户端的哪一个里面执行这个函数。(RPC的用法不细说,参考官方文档)答案就是通过这个Actor所包含的连接。关于RPC进一步的内容,下个问题里再详细描述。
第二点,Actor本身是可以同步的,他的属性当然也是。这与连接所有权也是息息相关。因为有的东西我们只需要同步给特定的客户端,其他的客户端不需要知道,(比如我当前的摄像机相关内容)。
对于第三点,其实就是Actor的属性是否同步可以进一步根据条件来做限制,有时候我们想限制某个属性只在拥有ROLE_AutonomousProxy的Actor使用,那么我们对这个Actor的属性ReplicatedMovement写成下面的格式就可以了。
voidAActor::GetLifetimeReplicatedProps(TArray &OutLifetimeProps )const
{
DOREPLIFETIME_CONDITION( AActor, ReplicatedMovement,COND_AutonomousOnly );
}
而经过前面的讨论我们知道ROLE_AutonomousProxy与所属连接是密不可分的。
最后,这里留一个思考问题:如果我在客户端创建出一个Actor,然后把它的Owner设置为带连接的PlayerController,那么他也有连接么?这个问题在下面的一节中回答。
并不是,有了前面的讲述,我们已经可以理解,如果我在客户端创建一个独有的Actor(不能勾选bReplicate,参考第五条思考)。那么这个Actor的Role就是ROLE_Authority,所以这时候你就不能通过判断他的Role来确定当前调试的是客户端还是服务器。这时候最准确的办法是获取到NetDiver,然后通过NetDiver找到Connection。(事实上,GetNetMode()函数就是通过这个方法来判断当前是否是服务器的)对于服务器来说,他只有N个ClientConnections,对于客户端来说只有一个serverConnection。
如何找到NetDriver呢?可以参考下面的图片,从Outer获取到当前的Level,然后通过Level找到World。World里面就有一个NetDiver。当然,方法不止这一个了,如果有Playercontroller的话,Playercontroller上面也有NetConnection,可以再通过NetConnection再获取到NetDiver。还可以通过堆栈,找到World。
在看下面的图之前,先提出一个问题:
对于一个形如UFUNCTION(Reliable,Client)的RPC函数,我们知道这个函数应该在服务器调用,在客户端执行。可是如果我在Standalone的端上执行该函数的时候会发生什么呢?
答案是在服务器上执行。其实这个结果完全可以参考下面的这个官方图片。
刚接触RPC的朋友可能只是简单的记住这个函数应该从哪里调用,然后在哪里执行。不过要知道,即使我声明一个在服务器调用的RPC我还是可以不按套路的在客户端去调用(有的时候并不是我们故意的,而是编写者没有理解透彻),其实这种不合理的情况UE早就帮我想到并且处理了。比如说你让自己客户端上的其他玩家去调用一个通知服务器来执行的RPC,这肯定是不合理的,因为这意味着你可以假装其他客户端随意给服务器发消息,这种操作与作弊没有区别~所以RPC机制就会果断丢弃这个操作。
所以大家可以仔细去看看上面的这个图片,对照着理解一下各个情况的执行结果,无非就是三个变量:1、在哪个端调用2、当前执行RPC的Actor归属于哪个连接3、RPC的类型是什么。
不过看到这里,再结合上一节结尾提到的问题,如果我在客户端创建一个Actor。把这个Actor的Owner设置为一个带连接PlayerController会怎么样呢?如果在这里调用RPC呢?
我们确实可以通过下面这种方式在客户端给新生成的Actor指定一个Owner。
好吧,关键时候还是得搬出来官方文档的内容。
您必须满足一些要求才能充分发挥 RPC 的作用:
1. 它们必须从 Actor 上调用。
2. Actor必须被复制。
3. 如果 RPC 是从服务器调用并在客户端上执行,则只有实际拥有这个 Actor 的客户端才会执行函数。
4. 如果 RPC 是从客户端调用并在服务器上执行,客户端就必须拥有调用 RPC 的 Actor。
5. 多播 RPC 则是个例外:
o 如果它们是从服务器调用,服务器将在本地和所有已连接的客户端上执行它们。
o 如果它们是从客户端调用,则只在本地而非服务器上执行。
o 现在,我们有了一个简单的多播事件限制机制:在特定 Actor 的网络更新期内,多播函数将不会复制两次以上。按长期计划,我们会对此进行改善,同时更好的支持跨通道流量管理与限制。
看完第二条,其实你就能理解了,你的Actor必须要被复制,也就是说必须是bReplicate属性为true,Actor是从服务器创建并同步给客户端的(客户端如果勾选了bReplicate就无法在客户端上正常创建,参考第四条问题)。所以,这时候调用RPC是失效的。我们不妨去思考一下,连接存在的意义本身就是一个客户端到服务器的关联,这个关联的主要目的就是为了执行同步。如果我只是在客户端创建一个给自己看的Actor,根本就不需要网络的连接信息(当然你也没有权限把它同步给服务器),所以就算他符合连接的条件,仍然是一个没有意义的连接。同时,我们可以进一步观察这个Actor的属性,除了Role以外,Actor身上还有一个RemoteRole来表示他的对应端(如果当前端是客户端,对应端就是服务器,当前端是服务器,对应端就是客户端)。你会发现这个在客户端创建的Actor,他的Role是ROLE_Authority(并不是ROLE_AutonomousProxy),而他的RemoteRole是ROLE_None。这也说明了,这个Actor只存在于当前的客户端内。
下面我们讨论一下RPC与同步直接的关系,这里先提出一个问题,
问题:服务器ActorA在创建一个新的ActorB的函数里同时执行自身的一个Client的RPC函数,RPC与ActorB的同步哪个先执行?
答案是RPC先执行。你可以这样理解,我在创建一个Actor的同时立刻执行了RPC,那么RPC相关的操作会先封装到网络传输的包中,当这个函数执行完毕后,服务器再去调用同步函数并将相关信息封装到网络包中。所以RPC的消息是靠前的。
那么这个问题会造成什么后果呢?
1. 当你创建一个新的Actor的同时(比如在一个函数内),你将这个Actor作为RPC的参数传到客户端去执行,这时候你会发现客户端的RPC函数的参数为NULL。
2. 你设置了一个bool类型属性A并用UProperty标记了一个回调函数OnRep_Use。你先在服务器里面修改了A为true,同时你调用了一个RPC函数让客户端把A置为true。结果就导致你的OnRep_Use函数没有执行。但实际上,这会导致你的OnRep_Use函数里面还有其他的操作没有执行。
如果你觉得上面的情况从来没有出现过,那很好,说明暂时你的代码没有类似的问题,
但是我觉得有必要提醒一下大家,因为UE4代码里面本身就有这样的问题,你以后也很有可能遇到。下面举例说明实际可能出现的问题:
情况1:当我在服务器创建一个NPC的时候,我想让我的角色去骑在NPC上并控制这个NPC,所以我立刻就让我的Controller去Possess这个NPC。在这个过程中,PlayerController就会执行UFUNCTION(Reliable,Client) void ClientRestart (APawn*NewPawn)函数。当客户端收到这个RPC函数回调的时候就发现我的APlayerController::ClientRestart_Implementation (APawn* NewPawn)里面的参数为空~原因就是因为这个NPC刚在服务器创建还没有同步过来。
情况2:对于Pawn里面的Controller成员声明如下
UPROPERTY(replicatedUsing = OnRep_Controller)
AController* Controller;
OnRep_Controller回调函数里面回去执行Controller->SetPawnFromRep(this);进而执行
Pawn = InPawn;OnRep_Pawn();
下面重点来了,OnRep_Pawn函数里面会执行OldPawn->Controller=NULL;将客户端之前Controller控制的角色的Controller设置为空。到现在来看没有什么问题。那么现在结合上面第二个问题,如果一个RPC函数执行的时候在客户端的Controller同步前就修改为正确的Controller,那么OnRep_Controller回调函数就不会执行。所以客户端的原来Controller控制的OldPawn的Controller就不会置为空,导致的结果是客户端和服务器竟然不一样。
实际上,确实存在这么一个函数,这个RPC函数就是ClientRestart。这看起来就很奇怪,因为ClientRestart如果没有正常执行的话,OnRep_Controller就会执行,进而导致客户端的oldPawn的Controller为空(与服务器不同,因为服务器并没有去设置OldPawn的Controller)。我不清楚这是不是UE4本身设计上的BUG。(不要妄想用AlwaysReplicate宏去解决,参考第八条有关AlwaysReplicate的使用)
不管怎么说,你需要清楚的是RPC的执行与同步的执行是有先后关系的,而这种关系会影响到代码的逻辑,所以之后的代码有必要考虑到这一点。
最后,对使用RPC的朋友做一个提醒,有些时候我们在使用UPROPERTY标记Server的函数时,可能是从客户端调用,也可能是从服务器调用。虽然结果都是在服务器执行,但是过程可完全不同。从客户端调用的在实际运行时是通过网络来处理的,一定会有延迟。而从服务器调用的则会立刻执行。
看到这个问题,你可能想这还用说么?不发给所有客户端那要多播干什么?但事实上确实不一定。
考虑到服务器上的一个NPC,在地图的最北面,有两个客户端玩家。一个玩家A在这个NPC附近,另一个玩家B在最南边看不到这个NPC(实际上就是由于距离太远,服务器没有把这个Actor同步到这个B玩家的客户端)。我们现在在这个NPC上调用多播RPC通知所有客户端上显示一个提示消失“NPC发现了宝藏”。这个消息会不会发送到B客户端上面?
情况一:会。多播顾名思义就是通知所有客户端,不需要考虑发送到哪一个客户端,直接遍历所有的连接发送即可。
情况二:不会。RPC本来就是基于Actor的,在客户端B上面连这个Actor都没有,我还可以使用RPC不会很奇怪?
第一种情况强化了多播的概念,淡化了RPC基于Actor的机制,情况二则相反。所以看起来都有道理。实际上,UE4里面更偏向第二种情况,处理如下:
如果一个多播标记为Reliable,那么他默认会给所有的客户端执行该多播事件,如果其标记的是unreliable,他就会检测该NPC与客户端B的网络相关性(即在客户端B上是否同步)。但实际上,UE还是认为开发者不应该声明一个Reliable的多播函数。下面给出UE针对这个问题的相关注释:(相关的细节在另一篇进一步探索UE网络同步的文档里面去分析)
// Do relevancy check if unreliable.
// Reliables will always go out. This is oddbehavior. On one hand we wish to garuntee "reliables always getthere". On the other
// hand, replicating a reliable to something on theother side of the map that is non relevant seems weird.
// Multicast reliables should probably never beused in gameplay code for actors that have relevancy checks. If they are, the
// rpc will go through and the channel will be closedsoon after due to relevancy failing.
前面提到过,Actor的属性同步可以通过这种方式来实现。
声明一个属性并标记
UPROPERTY(Replicated)
uint8 bWeapon: 1;
UPROPERTY(Replicated)
uint8 bIsTargeting: 1;
voidCharacter::GetLifetimeReplicatedProps(TArray &OutLifetimeProps ) const
{
DOREPLIFETIME(Character,bWeapon );
DOREPLIFETIME_CONDITION(Character, bIsTargeting, COND_InitialOnly);
}
这里面的第一个属性一般的属性复制,第二个就是条件属性复制。条件属性复制无非就是告诉引擎,这个属性在哪些情况下同步,哪些情况下不同步。这些条件都是引擎事先提供好的。
这里我想着重的提一下COND_InitialOnly这个条件宏,汉语的官方文档是这样描述的:该属性仅在初始数据组尝试发送。而英文是这样描述的:This property will only attempt to send on theinitial bunch。对比一下,果然还是英文看起来更直观一点。
经过测试,这个条件的效果就是这个宏声明的属性只会在Actor初始化的时候同步一次,接下来的游戏过程中不会再同步。所以,我们大概能想到这个东西在有些时候确实用的到,比如同步玩家的姓名,是男还是女等,这些游戏开始到结束一般都不会改变的属性。那么在方舟里面,我还发现动物状态组件的同步状态上限ReplicatedGlobalMaxStatusValues是通过COND_InitialOnly条件来进行复制的。也就是说,上限一般调整的次数很少,如果真的有调整并需要同步,他会手动调用函数去同步该属性。这样就可以减少同步带来的压力。 然而,一旦你声明为COND_InitialOnly。你就要清楚,同步只会执行一次,客户端的OnRep回调函数就会执行一次。所以,当你在服务器创建了一个新的Actor的时候你需要第一时间把需要改变的值修改好,一旦你在下一帧(或是下一秒)去执行那么这个属性就无法正确的同步到客户端了。
我们已经知道UE4的客户端与服务器公用一套代码,那么我们在每次写代码的时候就有必要提醒一下自己。这段代码在哪个端执行,客户端与服务器执行与表现是否一致?
虽然,我很早之前就知道这个问题,但是写代码的时候还是总是忽略这个问题,而且程序功能经常看起来运行的没什么问题。不过看起来正常不代表逻辑正常,有的时候同步机制帮你同步一些东西,有时候会删除一些东西,有时候又会生成一些东西,然而你可能一点都没发现。
举个例子,我在一个ActorBeginPlay的时候给他创建一个粒子Emiter。代码大概如下:
voidAGate::BeginPlay()
{
Super::BeginPlay();
//单纯的在当前位置创建粒子发射器
GetWorld()->SpawnActor(SpawnEmitter,GetActorLocation(),UVictory
Core::RTransform(SpawnEmitterRotationOffset,GetActorRotation()));
}
代码很简单,不过也值得我们分析一下。
首先,服务器下,当Actor创建的时候就会执行BeginPlay,然后在服务器创建了一个粒子发射器。这一步在服务器(DedicateServer)创建的粒子其实就是不需要的,所以一般来说,这种纯客户端表现的内容我们不需要在专用服务器上创建。
再来看一下客户端,当创建一个Gate的时候,服务器会同步到客户端一个Gate,然后客户端的Gate执行BeginPlay,创建粒子。这时候我们已经发现二者执行BeginPlay的时机不一样了。进一步测试,发现当玩家远离Gate的时候,由于UE的同步机制(只会同步一定范围内的Actor),客户端的Gate会被销毁,而粒子发射器也会销毁。而当玩家再次靠近的时候,Gate又被同步过来了,原来的粒子发射器也被同步过来。而因为客户端再次执行了BeginPlay,又创建了一个新的粒子,这样就会导致不断的创建新的粒子。
你觉得上面的描述准确么?
并不准确,因为上述逻辑的执行还需要一个前置条件——这个粒子的bReplicate属性是为false的。有的时候,我们可能一不小心就写出来上面这种代码,但是表现上确实正常的,为什么?因为SpawnActor是否成功是有条件限制的,在生成过程中有一个函数
bool AActor::TemplateAllowActorSpawn(UWorld* World,const FVector& AtLocation, const FRotator& AtRotation, const structFActorSpawnParameters& SpawnParameters)
{
return !bReplicates || SpawnParameters.bRemoteOwned||World->GetNetMode() != NM_Client;
}
如果你是在客户端,且这个Actor勾选了bReplicate的话,TemplateAllowActorSpawn就会返回false,创建Actor就会失败。如果这个Actor没有勾选bReplicate的话,那么服务器只会创建一个,客户端就可能不断的创建,而且服务器上的这个Actor与客户端的Actor没有任何关系。
另外,还有一种常见的错误。就是我们的代码执行是有条件的,然而这个条件在客户端与服务器是不一样的(没同步)。如,
voidGate::CreateParticle(int32 ID)
{
if(GateID!= ID)
{
FActorSpawnParameters SpawnInfo;
GetWorld()->SpawnActor(SpawnEmitter, GetActorLocation(), GetActorRotation(), SpawnInfo);
}
}
这个GateID是我们在GateBeginPlay的时候随机初始化的,然而这个GateID只在服务器与客户端是不同的。所以需要服务器同步到客户端,才能按照我们理想的逻辑去执行
单纯的非休眠状态Actor的属性同步比较简单,但是一旦涉及到休眠状态,回调函数的执行,还是值得总结一下的。
非休眠状态下的Actor的属性同步:只在服务器属性值发生改变的情况下执行
回调函数执行条件:服务器同步过来的数值与客户端不同
休眠的ACtor:不同步
首先要认识到,同步操作触发是由服务器决定的,所以不管客户端是什么值,服务器觉得该同步就会把数据同步到客户端。而回调操作是客户端执行,所以客户端会判断与当前的值是否相同来决定是否产生回调。
然后是属性同步,属性同步的基本原理就是服务器在创建同步通道的时候给每一个Actor对象创建一个属性变化表(这里面涉及到FObjectReplicator,FRepLayout,FRepState,FRepChangedPropertyTracker相关的类,有兴趣可以进一步了解,我也会在另一个博客里面去讲解),里面会记录一个当前默认的Actor属性值。之后,每次属性发生变化的时候,服务器都会判断新的值与当前属性变化表里面的值是否相同,如果不同就把数据同步到客户端并修改属性变化表里的数据。对于一个非休眠且保持连接的Actor,他的属性变化表是一直存在的,所以他的表现出来的同步规则也很简单,只要服务器变化就同步。
动态数组TArray在网络中是可以正常同步的,系统会检测到你的数组长度是否发生了变化,并通知客户端改变。
注意,UE里面UStruct类型的结构体在反射系统中对应的是UScriptStruct,他本身可以被标记Replicated并且结构体内的数据默认都会被同步,而且如果里面有还子结构体的话也仍然会递归的进行同步。如果不想同步的话,需要在对应的属性标记NotReplicated,而且这个标记只对UStruct有效,对UClass无效。另外,如果是Ustruct数组一定要在内部属性标记Uproperty,否在在数组同步的时候就会产生崩溃。
有一点特别的是,Struct结构内的数据是不能标记Replicated的。如果你给Struct里面的属性标记replicated,UHT在编译的时候就会提醒你编译失败。
最后,UE里面的UStruct不可以以成员指针的方式在类中声明。
组件在同步上分为两大类:静态组件与动态组件。
对于静态组件:一旦一个Actor被标记为同步,那么这个Actor身上默认所挂载的组件也会随Actor一起同步到客户端(也需要序列化发送)。什么是默认挂载的组件?就是C++构造函数里面创建的默认组件或者在蓝图里面添加构建的组件。所以,这个过程与该组件是否标记为Replicate是没有关系的。
对于动态组件:就是我们在游戏运行的时候,服务器创建或者删除的组件。比如,当玩家走进一个洞穴时,给洞穴里面的火把生成一个粒子特效组件,然后同步到客户端上,当玩家离开的时候再删除这个组件,玩家的客户端上也随之删除这个组件。
对于动态组件,我们必须要设置他的Replicate属性为true,即通过函数 AActorComponent::SetIsReplicated(true)
来操作。而对于静态组件,如果我们不想同步组件上面的属性,我们就没有必要设置
Replicate属性。
一旦我们执行了SetIsReplicated(true)
。那么
组件在属性同步以及RPC上与Actor的同步几乎没有区别,组件上也需要设置GetLifetimeReplicatedProps
来执行属性同步
,Actor同步的时候会遍历他的子组件查看是否标记Replicate以及是否有属性要同步。
boolAActor::ReplicateSubobjects(UActorChannel *Channel, FOutBunch *Bunch, FReplicationFlags *RepFlags)
{
......
boolWroteSomething = false;
for(UActorComponent* ActorComp : ReplicatedComponents)
{
if(ActorComp && ActorComp->GetIsReplicated())
{
//Lets the component add subobjects before replicating its own properties.
WroteSomething|= ActorComp->ReplicateSubobjects(Channel, Bunch,RepFlags);
//(this makes those subobjects 'supported', and from here on those objects mayhave reference replicated) 子对象(包括子组件)的同步,其实是在ActorChannel里进行
WroteSomething |= Channel->ReplicateSubobject(ActorComp,*Bunch,*RepFlags);
}
}
returnWroteSomething;
}
对于C++默认的组件,需要放在构造函数里面构造并设置同步,UE给出了一个例子:
ACharacter::ACharacter()
{
// Etc...
CharacterMovement = CreateDefaultSubobject(TEXT("CharMoveComp");
if (CharacterMovement)
{
CharacterMovement->UpdatedComponent = CapsuleComponent;
CharacterMovement->GetNavAgentProperties()->bCanJump = true;
CharacterMovement->GetNavAgentProperties()->bCanWalk = true;
CharacterMovement->SetJumpAllowed(true);
//Make DSO components net addressable 实际上如果设置了Replicate之后,这句代码就没有必要执行了
CharacterMovement->SetNetAddressable();
// Enable replication by default
CharacterMovement->SetIsReplicated(true);
}
}
如果想进一步的深入网络同步的相关细节,我会在下一篇博客里面进一步分析讲解。
1.当前新版的Server RPC好像要求必须加 reliable/unreliable ,以及WithValidation
一旦加上WithValidation,还必须要添加一个验证函数。像下面这样,
UFUNCTION(Server, unreliable, WithValidation)
void ServerSpawnTestActor();
virtual bool ServerSpawnTestActor_Validate();
2.有属性同步我们知道必须要添加GetLifetimeReplicatedProps,但是同时要在.cpp里面添加头文件#include "Net/UnrealNetwork.h",否则找不到FLifetimeProperty
void ALevelTestCharacter::GetLifetimeReplicatedProps(TArray
{
Super::GetLifetimeReplicatedProps(OutLifetimeProps);
DOREPLIFETIME(ALevelTestCharacter, TestRepActor);
}
3.看编译错误不要看VS的错误窗口,会看晕的,一定要看输出窗口的错误提示
4.所有的Tick事件的注册都是在AActor::BeginPlay()里面完成的,所以重写各种Actor函数时一定别忘了加Super::XXXXX();
原文链接(转载请标明):http://blog.csdn.net/u012999985/article/details/78244492