PlayerPawn的Physwalking分析
游戏引擎的物理是用来仿真物件运动情况的,比如虚幻用Physwalking仿真物件走动,用Physflying仿真物件飞行,用Physswimming仿真物件在水里游动的情况。这种仿真能最大限度地逼近真实世界的物理。这里主要介绍Physwalking,即走动物理,这是玩家Pawn最常见的物理状态。
对于多人网络游戏来说,玩家PlayerPawn将有三种存在形式:第一种是服务器上的Pawn,它的运算结果是决定性的;第二种是客户端本地操作的Pawn,它是自治的,接受玩家的输入控制,并把玩家输入上报给服务器;第三种是客户端看到的其他玩家的Pawn,它们是纯仿真的。根据控制力和权威性的不同,我们给三个地方的Pawn分配不同的角色,服务器上的Pawn所扮演的角色是Role_Authority,可以理解为权威Pawn;本地控制的Pawn所扮演的角色是Role_Autonomous,可以理解为自治Pawn;而本地仿真其他Pawn所扮演的角色是Role_Simulated,可以理解为仿真Pawn。
由于所扮演的角色不同以及服务端和客户端帧率的不同,这三个地方的Pawn所走的物理运算也各有不同,下面分别来说明。
服务器Pawn在TickAuthoritative中执行了APawn::physWalking,本地自治Pawn则是在AutonomousPhysics中执行了APawn::physWalking,这两种情况最终走的逻辑是非常相近的;本地仿真Pawn则走的是TickSimulated的分支,这块的逻辑与前两者是有较大不同的。
先看下服务器和自治Pawn走的physWalking逻辑,我将它总结成五步:
第一步:时间分片。虽然每个Tick的时间DeltaTime已经非常短了,但在这里为了更精确地刻画运动情况,还是进一步将DeltaTime分成了更小的份。这里可以根据游戏模式进行自定义地划分,一般情况下PVE的精度需求要低于PVP模式。
第二步:计算速度。这里会计算当前时间片的速度,也会计算下一个时间片的速度。不同的游戏会有不同的计算方法,以实现不同的游戏感觉,下面给出一个示例:
• 当前时间片速度=初始速度 + 加速度*时间片*(1-武器惩罚因子)*移动因子
• 下一时间片速度=当前时间片速度 * exp(-5.75*时间片)
注意下一个时间片的速度是一个指数衰减
武器惩罚因子是指玩家所携带的武器的负重,举个例子,玩家持有匕首的惩罚因子要小于持有狙击枪的惩罚因子。
移动因子是指玩家当前的移动状态,正常行走时移动因子大,而选择徐行或者蹲下行走时移动因子会小。
在这里也会计算Pawn可以达到的最大速度,速度计算的结果不能超过这个值。比如玩家按了前进键,引擎会转换成给定Pawn一个前进方向的加速度,物理运算的结果是这个Pawn的速度会慢慢提升,但这个提升并不能没有限制,最大速度就是玩家Pawn可以达到的速度上限了。游戏玩法里添加的一些快速移动技能BUFF,就可以通过提升玩家短时加速度和移动速度上限来实现。
第三步:执行移动。
移动是计算速度的最终目的,不同游戏会有不同的手感,下面给出一个示例:
微小移动量 = 当前时间片速度* (1-exp(-5.75 * 时间片) ) / 5.75
移动函数:GWorld->MoveActor(this, Delta, Rotation, 0, Hit)
其中,this为当前Pawn,Delta为微小移动量(含方向),Rotation(暂未使用到)以及Hit表示碰撞信息。在MoveActor中:
1、验证Actor是非静态的且可以移动
2、如果Actor自身有碰撞(bCollideActorsbCollideWorld两者至少居一且CollisionComponent非空),则执行碰撞检测
3、MultiLineCheck执行碰撞检测,返回FirstHit包含有碰撞信息,据此移动Actor。
4、更新Actor的位置信息:Actor->Location += 真实可移动距离
5、移动Actor的CollisionComponent以及其他Attach到Actor身上的物件(比如角色挂件等)
6、 碰撞事件通知:NotifyBumpLevel,NotifyBump,BeginTouch等。
第四步:阻碍移动碰撞物分析。
• 若该碰撞物可以走上去(bCanStepUpOn==true),那么执行stepUp逻辑。
• 若该碰撞物不可以走上去,执行撞墙逻辑。
先来看下stepUp(上坡)逻辑:
• 先将Pawn向上移动,再向Rotation方向移动微小移动量。
• 若移动过程中还有碰到障碍物,且障碍物可以走上去,则将Pawn向下回移,再递归调用stepUp()。
• 若障碍物不可以走上去,则执行撞墙逻辑。
• 将Pawn向下回移。
再说下撞墙逻辑:
撞墙可以分成两种情况,一种是墙夹角为钝角,一种是墙夹角为锐角。
• 脚本事件通知processHitWall()。
• 将微小移动量分解为垂直于墙的分量和平行于墙的分量,扣除垂直于墙的分量。
•考虑到延墙分量移动仍可能碰撞到另一面墙,所以在TwoWallAdjust()进一步处理:若两墙夹角小于90度,则将碰第二面墙后的剩余移动分量再去掉与第二面墙垂直的分量;若两墙夹角大于90度,则允许玩家向Z方向上移
第五步:更新Base(即脚踏之物)。
• 向负Z方向(重力加速度方向)做SingleLineCheck,获得碰撞信息
• 如果Hit.Actor与前Base不一样,则更新SetBase(Hit.Actor, Hit.Normal)
• 如果Hit.Actor与前Base一样,则将Pawn移动到离地面合适的高度处 (average of MAXFLOORDIST and MINFLOORDIST above floor)
前面说的是服务器和客户端自治Pawn走的移动物理,下面来看下客户端仿真其他Pawn走的移动物理,我把他分成四步:
• 加速度限制:只有同步过来加速度大小超过一定值时(比如400),才会用来更新本地的加速度大小。
• CalcVelocity仿真运动速度
• moveSmooth移动Pawn
•下落检测
其中CalcVelocity的流程如下:
•考虑摩擦力对速度的衰减:DeltaV=(初始速度-加速度方向*速度大小)*摩擦力,速度=初始速度-DeltaV*DeltaT
•更新速度:速度=原速度*流体衰减因子+加速度*DeltaT
•最大速度限制。
moveSmooth的流程如下:
•调用MoveActor移动小段距离
下落检测的流程如下:
•若没有碰撞,则将Physics切换为PhysFalling;
•若有碰撞,则将Pawn抬至与地面高度0.5f*(MINFLOORDIST+MAXFLOORDIST)处。
最后总结一下,服务端与客户端自控Pawn走的是ATGPawn::physWalking,而客户端仿真非控Pawn却走的是ATGPawn::TickSimulated,两者有不同点也有相同点,相同点是流程大致相同,先计算速度,再由速度乘以DeltaT向前移动一小段距离,如果发生了碰撞则考虑走上坡逻辑还是撞墙逻辑,最后做重力方向检测。
不同点有:
1、服务器/自控Pawn进行了时间分片处理,而非控Pawn没有。
2、服务器/自控Pawn在计算速度时考虑到了武器惩罚等因子,不仅计算了当前时间片的速度,而且预测了下一个时间片的速度(有速度衰减),而非控Pawn仅计算了当前时刻的速度
向下检测时服务器/自控Pawn有更新Walking的Base,但非控Pawn无此更新。
出现上述不同,是因为服务器是速度/加速度的决策者,自控Pawn要走与服务器相同的逻辑,否则会出现两端不一致而出现的位置拉扯。仿真Pawn是同步速度/加速度的被动接受者,只需仿真当前时刻的情况,后续时刻会被网络同步校准。