在上一篇文章中,已經(jīng)可以在服務(wù)器上直接根據(jù)服務(wù)器自己的操作指令,模擬得出結(jié)果,修改球的位置了,接下來(lái),將要考慮如何將服務(wù)器模擬的位置如何同步到客戶(hù)端.
1.服務(wù)器向客戶(hù)端發(fā)送單位實(shí)體(Entity)狀態(tài)
首先需要設(shè)定一個(gè)發(fā)包的頻率(SendRate),目前設(shè)置的是每10個(gè)模擬幀發(fā)送一次,對(duì)于60模擬幀每秒的游戲世界來(lái)說(shuō),這也相當(dāng)于6個(gè)包每秒.這個(gè)包的數(shù)據(jù)應(yīng)該是描述Entity在當(dāng)前模擬幀的狀態(tài).
public class State
{
public int frame; //模擬的幀號(hào)
public Entity entity; //所屬的Entity
public List<Property> properties; //需要描述的屬性
public int Pack(Packet packet)
{
packet.Write(frame);
//將屬性數(shù)據(jù)寫(xiě)入消息包packet
}
public int Read(Packet packet)
{
frame = packet.ReadInt();
//從消息包中取出屬性數(shù)據(jù)
}
}
發(fā)送的方法:
public void FixedUpdate()
{
if (Core.frame % SendRate == 0) //每隔10幀發(fā)送一次
{
foreach (var conn in connections)
{
conn.Send();
}
}
}
//connection中發(fā)送的方法
public void Send()
{
Packet packet = PacketPool.Get();
foreach(Entity entity in entities)
{
entity.currentState.Pack(packet); //將當(dāng)前狀態(tài)數(shù)據(jù)寫(xiě)入消息包
}
_connection.Send(CustomMsgTypes.InGameMsg, packet.msg_untiy); //通過(guò)UnityEngine.Networking組件的Connection發(fā)送數(shù)據(jù)
}
這樣就把Entity的狀態(tài)打包發(fā)向所有的客戶(hù)端了.
2.客戶(hù)端接收到服務(wù)端的狀態(tài)包
客戶(hù)端接收到服務(wù)端的數(shù)據(jù)包,然后從數(shù)據(jù)包中拿到描述Entity狀態(tài)的數(shù)據(jù)后,需要考慮的是,如果是第一個(gè)狀態(tài),可以直接拿來(lái)應(yīng)用到Entity上,如果不是第一個(gè)狀態(tài)的話,那就不能直接應(yīng)用,因?yàn)榫W(wǎng)絡(luò)傳輸抖動(dòng)的因素,服務(wù)端雖然是每隔10幀發(fā)一個(gè)包,但是客戶(hù)端收包頻率不一定是每隔10幀就收到的,如果直接應(yīng)用的話,必然會(huì)導(dǎo)致抖動(dòng).這個(gè)時(shí)候,我們就需要在客戶(hù)端對(duì)服務(wù)器端進(jìn)行狀態(tài)緩存(StateBuffer)和狀態(tài)插值(StateInterpolation).
1.為什么需要狀態(tài)緩存和狀態(tài)插值
客戶(hù)端收到的狀態(tài)包都是帶幀號(hào)(Frame),幀號(hào)表示了這個(gè)狀態(tài)是服務(wù)器在那幀模擬得到的狀態(tài),客戶(hù)端想要,去除抖動(dòng),平滑的渡過(guò)的狀態(tài)之間的時(shí)間的話.就需要在State_A與State_B進(jìn)行插值計(jì)算.插值計(jì)算的公式應(yīng)該是這樣
Current = MathUtils.Interpolate(State_A, State_B,
????
/ (State_B.frame -State_A.frame ))
在公式右側(cè),除了????
,其他都是已知的,想要得到插值結(jié)果,那么????
應(yīng)該是什么呢?
因?yàn)榉帜傅?strong>兩個(gè)狀態(tài)的幀號(hào)差,所以分子應(yīng)該也是幀號(hào)才對(duì),客戶(hù)端的幀號(hào)跟服務(wù)端幀號(hào)不一致(因?yàn)榉?wù)器肯定早就啟動(dòng)了,客戶(hù)端是后來(lái)才連接服務(wù)器的),這個(gè)時(shí)候就要新增一個(gè)變量用來(lái)表示客戶(hù)端估算出來(lái)的服務(wù)器幀(RemoteEstimatedFrame).
這個(gè)估算幀用來(lái)表示客戶(hù)端在本地估測(cè)服務(wù)器模擬的幀號(hào),它的第一次賦值應(yīng)該是客戶(hù)端收到服務(wù)器的幀號(hào)時(shí),
// 調(diào)整遠(yuǎn)程估算幀
public void AdjustRemoteEstimatedFrame()
{
if (packetsReceived == 1)
remoteEstimatedFrame = remoteActualFrame; //當(dāng)收到第一個(gè)包時(shí),將包的幀號(hào)賦值給估算幀
}
估算幀也是按照模擬頻率一直累加的,但是估算不一定總是準(zhǔn)的,有時(shí)提前收到包,有時(shí)延遲收到包,甚至丟包.所以如果收到的包幀號(hào)跟估算幀相差太大的時(shí)候,就需要對(duì)估算幀重新調(diào)整
public void AdjustRemoteEstimatedFrame()
{
if (packetsReceived == 1)
remoteEstimatedFrame = remoteActualFrame; //當(dāng)收到第一個(gè)包時(shí),將包的幀號(hào)賦值給估算幀
else
{
remoteDiffFrame = remoteActualFrame - remoteEstimatedFrame;// 差異=實(shí)際收到的幀號(hào)-估算幀
if (remoteDiffFrame < minDiff || (remoteDiffFrame > maxDiff) //如果差異太大的話,估算幀就要重新賦值
{
remoteEstimatedFrame = remoteActualFrame;
}
}
}
效果如下:
從這個(gè)圖可以看出,服務(wù)器移動(dòng)很平滑,但是客戶(hù)端移動(dòng)可以明顯看出抖動(dòng)的情況,問(wèn)題在哪呢?其實(shí)問(wèn)題是出在估算幀的設(shè)置問(wèn)題,從狀態(tài)A插值到狀態(tài)B的過(guò)程,由于估算幀等于(或者接近)狀態(tài)A的幀號(hào),而狀態(tài)B的包客戶(hù)端還沒(méi)有收到,這就造成了在狀態(tài)B到來(lái)之前,客戶(hù)端沒(méi)辦法插值,只好原地等待,當(dāng)狀態(tài)B的包到來(lái)的時(shí)候,立即設(shè)置了位置,所以造成了抖動(dòng),那么如何解決這個(gè)問(wèn)題呢?
做法是故意讓估算幀的幀號(hào)在實(shí)際的狀態(tài)包幀號(hào)之前,讓客戶(hù)端滯后:
public void AdjustRemoteEstimatedFrame()
{
if (packetsReceived == 1)
remoteEstimatedFrame = remoteActualFrame - delay; //當(dāng)收到第一個(gè)包時(shí),估算幀 = 包幀號(hào) - 延遲
else
{
remoteDiffFrame = remoteActualFrame - remoteEstimatedFrame;// 差異=實(shí)際收到的幀號(hào)-估算幀
if (remoteDiffFrame < minDiff || (remoteDiffFrame > maxDiff) //如果差異太大的話,估算幀就要重新賦值
{
remoteEstimatedFrame = remoteActualFrame - delay;
}
}
}
將delay = 10
(因?yàn)榉?wù)器每10幀發(fā)個(gè)包)這樣盡可能的預(yù)留出一個(gè)狀態(tài)包用來(lái)做插值計(jì)算了,看看效果:
可以看到客戶(hù)端的抖動(dòng)幾乎看不出來(lái)了,但是代價(jià)是延遲比較大了(為了更好的表現(xiàn),這個(gè)犧牲是必要的)
3.小結(jié)
服務(wù)端模擬結(jié)果,下發(fā)狀態(tài)給客戶(hù)端基本就完成了,需要補(bǔ)充的是,在估算幀的計(jì)算中,可以根據(jù)估算幀和實(shí)際幀的差距動(dòng)態(tài)的調(diào)整本地模擬的頻率,比如:
如果估算幀滯后太多了,那客戶(hù)端就每幀加2,甚至加3(默認(rèn)是每個(gè)模擬幀加1)來(lái)追趕.
如果估算幀超前很多,那客戶(hù)端就估算幀的累加可以暫停來(lái)等待,通過(guò)這樣的方式來(lái)緩和.
現(xiàn)在客戶(hù)端通過(guò)插值,實(shí)現(xiàn)了比較平滑的表現(xiàn),但是有比較明顯的延遲,這個(gè)可以通過(guò)加大發(fā)包的頻率來(lái)緩解這個(gè)問(wèn)題.
后續(xù)實(shí)現(xiàn)了客戶(hù)端的預(yù)表現(xiàn)后,這個(gè)問(wèn)題也就不那么重要了.