协议数据单元
网络同步包最小单元PDU
// 预测的基础数据类型
public class PDU
{
public uint UID; //玩家的唯一id
public PDUType type; //PDU类型
public Vector3 position; // 位置
public Vector3 forward; // 朝向
public float speed; // 速度: 速度为0表示静止
public float time; // PDU发出的时间
public string anim; // 当前的动作
}
需要发送PDU的情况,即是状态改变时情况
public enum PDUType
{
None = 0, // 没有产生任何改动
OutOrbit = 1, // 超出轨道
OverThreshold = 2, // 和本地模拟超过一定的阈值
SpeedChange = 4, // 速度发生改变
ActChange = 8, // 动作发生改变
All = OutOrbit | OverThreshold | SpeedChange | ActChange,
};
超出轨道
a背后有个观察者b,b对着a运动的方向,发射一个带有宽度的轨道。a超过了轨道即发送PDU,好处是在玩家速度,方向不变时,只需要发送一次PDU,而不需要每时每刻都发送
图下两条绿线即为轨道
当a相对b的本地坐标.x超过了轨道轨道宽度的一半,即触发了超过轨道
// 检查是否还在轨道内,每帧调用
bool inOrbitJudge()
{
Vector3 currentPos = gameObject.transform.position;
Quaternion rot = transform.localRotation;
Vector3 vct = m_PDUCreater.transform.InverseTransformPoint(currentPos);
if ( Mathf.Abs(vct.x) > orbitWidth/2.0)
{
return false;
}
return true;
}
同时更新b的位置与朝向
Vector3 dir = transform.position - lastPosition;
m_PDUCreater.transform.position = transform.position;
m_PDUCreater.transform.LookAt(transform.position + dir.normalized * 5);
本地模拟超过一定的阈值
本地模拟出的位置b(根据发出的pdu的朝向,速度每帧计算出),与发送者的位置a偏差超过阈值。这么做的原因是玩家原地转向也能识别到,一般手游都是摇杆,还是比较难做到原地转向
localSimulatedPosition += currentPDU.forward * currentPDU.speed * Time.deltaTime;
if ((localSimulatedPosition - transform.position).magnitude > DistanceTolerance)// 如果和本地模拟超过一定的阈值也要发送PDU
{
iPDUType |= PDUType.OverThreshold;
}
客户端同步服务器时间
每个客户端每隔1s同步服务器时间,得到时间s后,会在本地进行update模拟累加
发送时会记录发送时间戳
//向服务器发送请求服务器时间
void SendSyncTime()
{
sendSyncTime = Time.time;
GameSocket.Instance.SendMsgProtoVoid(MsgIdDefine.ReqHeartBeat);
}
接收时,记录接收时间戳,假设一次传输的时间延迟发送,接收各占一半,此时服务器时间为
void OnRspHeartBeat(PtLong data)
{
float reciveNetTimeDiff = Time.time - sendSyncTime;
float serverTime = (float)data.value + reciveNetTimeDiff * 0.5f;
TimeManager.self.currentTime = serverTime;
}
远程玩家
远程玩家是个镜像,当有新PDU传入时,做插值运动到预测的位置
没有时,按照上一次的PDU状态运动,例如上一次有速度时,按照速度*朝向移动;上一次是没速度时,持续禁止状态
新PDU传入
远程的位置应该为 PDU传输过来的位置 + 朝向 * 速度 * (插值时间 + 消息延迟)
//当新PDU传入时改变远程玩家位置,朝向,动画,速度
if(newPDUComing)
{
//DeterminStateByAnimation(realPDU.anim);
//float curTime = pvpWJY.ServerTime.currentTime;
float curTime = TimeManager.self.currentTime;
float oldTime = realPDU.time;
// 消息延迟时间
float timeDiffer = curTime - oldTime;
if(timeDiffer < 0 || timeDiffer > 2)
Debug.LogError("server time error : " + timeDiffer);
timeDiffer = Mathf.Clamp(timeDiffer, 0, 2);
smoothTime = realSmoothTime;
// 公式:插值的目标位置 = PDU传输过来的位置 + 朝向 * 速度 * (插值时间 + 消息延迟)
targetPosition = realPDU.position + realPDU.forward * realPDU.speed * timeDiffer;
targetForward = realPDU.forward;
startLerpPosition = transform.position;
startLerpForward = transform.forward;
newPDUComing = false;
transform.position = targetPosition;
}
//当还剩下平滑插值时间,继续插值
if (smoothTime > 0)
{
smoothTime -= Time.deltaTime;
transform.position = Vector3.Lerp(targetPosition, startLerpPosition, smoothTime / realSmoothTime);
transform.forward = Vector3.Slerp(targetForward, startLerpForward, smoothTime / realSmoothTime);
}
else
{
if (realPDU != null)
{
transform.position += realPDU.forward * realPDU.speed * Time.deltaTime;
}
}
Demo演示
白色是玩家,发送数据给服务器
黑色是远程镜像,接收到服务器PDU包进行模拟运动
type为PDU改变的类型
在均速直线运动阶段,产生的网络包较少
源码
https://github.com/luoyikun/UnityForTest 先启动服务器
UnityForTest\Server\MultiServer.sln运行
在局域网下,服务器会定时向局域网UDP广播TCP服务器的端口号
客户端接到了TCP的端口号,连接服务器
客户端场景
UnityForTest\Assets\NetSync\gdePvp\GdeNetSync.unity
点击运行,等待连接上服务器即可
按ws前进后退,ad转向