载具:

  1. 联机后,载具无法移动
    发现
  2. 联机后,载具1p下移动卡顿
    2.1 尝试将ReplicatePhysicstoAutonomousProxy 设成False。1p移动平滑,但1p和3p位置差距越来越大
    2.2 还原ReplicatePhysicstoAutonomousProxy为True。使用p.NetShowCorrections 1指令观察同步来的目标点,和官方版对比。发现官方版同步的目标点距离较近,我们版本里目标点比较远。
    2.3 怀疑同步频率过低导致。在官方版里就同步频率将为10次/s,重现了我们版本里的卡顿。
  3. 上述条件下,尝试官方版里设置PhysicsReplicationMode为PredictiveInterpolation(预测插值方式),官方版表现顺滑。同样设置后,我们版本里表现也顺滑。
  4. 上述条件下,关闭ProjectSetting-Physics-bEnablePhysicsPrediction,会回退到老式同步方式,官方版卡顿明显。
    结论:需要开启网络预测,并且设置PhysicsReplicationMode为PredictiveInterpolation,可以改善低同步频率的物理表现。

如果Server上不模拟的话,则需要1p上传状态,驱动上面执行。
同步部分涉及NetworkPhysicsComponent,

FNetworkVehicleInputs
FNetworkVehicleStates

TNetRewindHistory<>

开启物理预测情况下,服务器和客户端之间一直在交互ServerFame和LocalFrame。

在PlayerContoller上,客户端每帧通过RPC告诉服务器最新的LocalFrame(ServerSendLatestAsyncPhysicsTimestamp),服务器上每帧通过RPC将最新的ServerFame和客户端下发的最早的LocalFrame发给客户端(AsyncPhysicsTickActor,SendClientAdjustment)。
客户端会记录LocalToServerAsyncPhysicsTickOffset ,但是是最大值(不明白)
为什么使用最大值策略
设计原理:网络物理同步中的时间偏移计算采用保守策略,确保客户端不会超前于服务器。
关键原因:
防止客户端超前:
如果客户端比服务器超前,会导致严重的同步问题
使用FMath::Max确保偏移量只增不减,避免客户端时间超前服务器
网络延迟的保守估计:
网络延迟可能波动,取最大值确保在最差网络条件下也能保持同步
避免频繁调整偏移量导致的物理重模拟

/Game/VehicleTemplate/Blueprints/OffroadCar/OffroadCar_Pawn.OffroadCar_Pawn_C

Server:

FPhysicsSolverProcessPushDataTask::ProcessPushData()

UNetworkPhysicsComponent::OnPreProcessInputsInternal 
	if (!HasLocalController() || bIsSolverResim)
	{
		FNetworkPhysicsDatas* PhysicsDatas = InputsDatas.Get();
		PhysicsDatas->LocalFrame = PhysicsStep;
#if DEBUG_NETWORK_PHYSICS
		UE_LOG(LogTemp, Log, TEXT("		Extracting history inputs at frame %d | Component = %s"), PhysicsStep, *GetFullName());
#endif
		if (InputsHistory->ExtractDatas(PhysicsStep, bIsSolverReset, PhysicsDatas))
		{ 
			PhysicsDatas->ApplyDatas(ActorComponent);
				VehicleSimulation->VehicleInputs = VehicleInputs;
		}
	}

FChaosVehicleManagerAsyncCallback::ProcessInputs_Internal

如果不启用网络预测:
ChaosVehicleMovementComponent 通过UpdateState,向Server同步输入。Server通过填充ReplicatedState将输入同步给3p。
物理状态的纠正通过RepMovement将RootComponent的物理状态同步给客户端。在客户端上做插值处理。

FPhysicsReplication::ApplyRigidBodyState 处理当前状态到Server上的目标状态的插值
首先计算误差 const float Error = (LinDiffSize ErrorPerLinearDiff) + (AngDiffSize ErrorPerAngularDiff)

  1. 如果误差在容忍值,则不做处理
  2. 否则,
    2.1如果误差过大,直接修改BI的线速度,角速度,以及Transform
    2.2 如果不使用PhysicsReplicationAsync,则直接插值线速度,角速度,Transform,更改BI
    2.3 如果使用PhysicsReplicationAsync,则通过AsyncInput,将目标状态存入InputData中,异步处理。

插值设置BI的方式由碰撞穿插的风险。

如果启用网络预测:
上面逻辑不在工作。
1p通过SendLocalInputsDatas将Input记录在InputsHistory中, 上传输入给Server。
server 上通过记录ReplicatedInputs,将输入同步给3p。也会更新最新的States给1/3p,通过SendLocalStatesDatas。
1p接收State
3p接收Input和State。

State结构主要包括
轮子转速,悬挂信息(弹簧,位移,长度)
引擎的转速。
Root位置依然通过RepMovement将RootComponent的物理状态同步给客户端。在客户端上做插值处理,但处理方式不同。

struct CHAOSVEHICLES_API FNetworkVehicleStates : public FNetworkPhysicsDatas
{
FVector StateLastVelocity = FVector(0,0,0);

TArray WheelsOmega;

TArray<float> WheelsAngularPosition;

TArray<float> SuspensionLastDisplacement;

TArray<float> SuspensionLastSpringLength;

TArray<float> SuspensionAveragedLength;

TArray<int32> SuspensionAveragedCount;

TArray<int32> SuspensionAveragedNum;

float EngineOmega = 0.0;

}

ENetworkPredictionTickingPolicy

Lead
每帧收集State 发送给Server。应用Input

Server
接收State,进行更新。转发State 给Follower

Follower
上传Input,

上传后的Input可以通过UChaosVehicleMovementComponent::ServerUpdateState_Implementation更新并同步

上传后的State可以更新Server,并且分发给

VehicleReplicationComponent

Host
搜集State 发送给Server

Follower
发送本地Input
接收State,作用给所有载具

理想情况是,载具不多的情况(4~6)下。如果客户端上传所有载具状态,那么利用DS的高频转发,3p客户端载具的同步延迟会更低,表现更好。同时提升DS多载具下物理模拟性能。

现实是载具有同步范围,一个客户端上没有所有载具。
两个客户端的载具在彼此客户端同步边界上可能相撞或位置冲突。
解决方法是为每个载具增加一个碰撞预警范围。范围内的载具尽量由一个客户端同步。
将载具看成平面上的点,问题就转换成平面上的一堆点,合理地将它们划分成簇。
簇要满足如下约束

  1. 预警范围内的两个点尽量在一个簇里;
  2. 簇内能找到一点,使簇内的其他点都在该点的一定范围内。

上述约束1,只能尽量满足。比如极端情况,载具在一条线上,彼此都在预警范围内。线的两端距离超出最大同步距离。那么它们一定不会在一个客户端上出现。
1—2—3—4—5—6

同时还需要考虑客户端ping值,期望由网络好的客户端上传信息,这样会提升3p载具的平滑。
但只考虑ping值依然存在问题。拿上面极端情况举例,网络最好客户端可能位于1和6,可能会产生两个簇,比如1—2—3,4—5—6。
假设3,4刚好都不在6,1的同步范围,那么3,4靠近时就可能出现位置冲突问题。

所以载具周围的环境也应该影响簇的划分。
这里使用了启发式方法,使用ping值和载具周围拥挤程度做启发,来智能决策划分簇。
Ping值
拥挤程度,考虑能量扩散模型,距离平方反比。比如每个点都是热源,一个点能感受扩散过来的所有热量。

Crowdedness = Sum(1/(d^2))

每个点的得分S = w1P+w2C {w1+w2=1,w1>0;w2>0}

首先按预警距离划分成各个簇
遍历各个簇S,从S中找到得分最高的点,将该点和该点同步范围内的点作为一个确定簇。
S中剩下的点形成新簇放到迭代队列。

注:当载具不被驾驶时(没有一个成员在载具上),这两个载具刚好都在同步范围边界上,各自因为惯性运动靠近并可能相撞,没有办法找到一个客户端同时同步它俩。
这种情况下给载具找一个最近Agent来协助同步,减少上面情况发生,但无法避免。玩法上,如果下车即停车的话,可以无视这个问题。

如果由不同端上传的载具,碰撞在Server上冲突,则自动抛弃其中一个载具的数据。

优先满足拥挤度高得点,并谨慎扩展

就近合并原则
按拥挤度对点排序,假设这个簇离有n点

取出点先将离它最近点加入到它一起,形成新的簇。
得到n/2个小簇。计算小簇间的拥挤度(利用簇间最近两点的距离)
尝试合并
按簇间拥挤度排序,比如m个簇,两个簇尝试合并
取出最拥挤的簇,优先尝试和最近的簇合并,合并原则是在两个簇内能找到一个点,使两簇所有点在同步范围内。如果找不到则不合并,并加入到最终簇列表离。能合并的话,成为新簇
产生的新簇按上面方式继续迭代,直到没有新簇产生。

可以将初始n个点看作n个簇,则迭代步骤会更加简单统一。

上面方案每次产生新簇后,会从原来簇里删掉。
不一定产生最优方案,比如 1—2—3-----4。迭代可能产生(1,2)(3,4),由于2离4过远导致不能合并。更好的结果可能是(1,2,3)(4)
不过也说明3没有1离2近。只有当3比1更靠近2时,才有可能产生(1,2,3)(4)
如果1,2,3都很近的情况下,依然产生(1,2)(3,4)就不太合理。

如果不删掉,产生新簇后再放回原簇里。
(1,2)3,4 =》(1,2,3),4 结束

对应 等距的情况 1—2—3—4—5,其中1和5超出同步距离
假设2,3,4的拥挤度相同。可能产生多个方案,
比如(遍历顺序2,3,4)=》(1,2),3,4,5=》(1,2,3),4,5=》(1,2,3,4),5
遍历顺序(3,2,4)=》(3,2)1,4,5 =》(3,2,4),1,5 =》 (3,2,4,1),5 或者(3,2,4,5),1

1—2----3---4
期望得到(1,2)(3,4)
(2,1),3,4=》(2,1)(3,4)

比如1----2—3----4。2,3分别1,4的同步范围内,互相看不到。但可能相撞。
迭代得到 (2,3),1,4