协议数据单元

网络同步包最小单元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,而不需要每时每刻都发送

图下两条绿线即为轨道

unity3d:网络同步,状态同步,源码,C#服务器demo_状态同步

当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改变的类型

在均速直线运动阶段,产生的网络包较少

unity3d:网络同步,状态同步,源码,C#服务器demo_状态同步_02

源码

https://github.com/luoyikun/UnityForTest 先启动服务器
UnityForTest\Server\MultiServer.sln运行
unity3d:网络同步,状态同步,源码,C#服务器demo_网络同步_03
unity3d:网络同步,状态同步,源码,C#服务器demo_服务器_04

在局域网下,服务器会定时向局域网UDP广播TCP服务器的端口号

客户端接到了TCP的端口号,连接服务器

客户端场景

UnityForTest\Assets\NetSync\gdePvp\GdeNetSync.unity

点击运行,等待连接上服务器即可

unity3d:网络同步,状态同步,源码,C#服务器demo_服务器_05

按ws前进后退,ad转向