A little bee Advanced Version 目前正在把幀同步邏輯提取SDK,請看項目https://github.com/dudu502/littlebee_libs 這是一個幀同步遊戲實例,在遊戲中同步上百個物體,上千個狀態,遊戲背景是一個行星系統下的射擊遊戲,以下是視訊連結。
[Watch playing the video(Youtube)]
[Watch replaying the video (Youtube)]
[Watch playing the video(bilibili)]
[Watch replaying the video(bilibili)]
影格同步 | 狀態同步 | |
---|---|---|
一致性 | 設計層面決定了必然一致 | 可以保證一致性 |
玩家數 | 對多玩家支援有限 | 多玩家有優勢 |
跨平台 | 需要考慮浮點運算的一致性 | 由於主要的計算都在伺服器,因此不存在跨平台問題 |
防作弊 | 容易作弊,但可以優化 | 可以很好的防作弊 |
斷線重連 | 實現比較難,但不是不能 | 只需要重新傳送一次訊息即可,好實現 |
回放需求 | 能完美實現 | 無法實現 |
暫停遊戲 | 好實現 | 不好實現 |
網路傳輸量 | 比較小 | 比較大 |
開發難度 | 相對複雜 | 相對簡單 |
RTS類遊戲 | 適合 | 不適合 |
格鬥遊戲 | 適合 | 不適合 |
MOBA類遊戲 | 適合 | 不適合 |
MMO類遊戲 | 不適合 | 適合 |
了解了幀同步開發過程中需要克服的困難,我們接下來就要考慮選用一種比較好的實作方式,或者說是一種開發框架。由於幀同步開發非常需要資料和表現分離,分離到什麼程度呢?就是資料計算部分甚至可以放在一個單獨的執行緒裡。這樣編寫邏輯的好處還可以讓伺服器運作以達到快速複盤遊戲的功能,能做到這種程度的分離我想只有ECS了。幀同步加上ECS絕對是完美搭檔。
首先要介紹一下ECS,ECS並非一種全新的技術,也不是Unity先提出來的。這種名詞的出現非常早,而近幾年突然火爆,是因為暴雪的《鬥陣特攻》。 《鬥陣特攻》的伺服器和客戶端框架完全基於ECS構建,在遊戲機制、網路、渲染方面都有非常出色的表現。坦白說ECS不像是設計模式,我們以前用的設計模式都是在物件導向設計下談論的,ECS都不是物件導向。 Unity也有ECS,其實Unity本身的組件也是一種ECS,只不過還不夠純粹。 ECS特別適合做Gameplay。關於ECS的變種也有很多,我這裡也是稍微做了一些修改過的ECS。
這是幀同步遊戲的一個特點,如果一個遊戲有回放系統,那麼這個遊戲必然是透過幀同步實現的。回放也可以稱為錄影,但是與視訊錄影有著巨大的區別,以視訊檔案為載體的回放通常檔案佔用巨大,並且在播放過程中無法切換視窗,視訊極易被盜用濫用,惡意修改,壓縮,降低品質,因此視訊回放有著很大的劣勢。幀同步的回放可以做到檔案極小,無法竄改,播放過程使用者可以任意切換視窗。可以說幀同步遊戲必備系統就是回放系統。
這裡推薦RevenantX/LiteNetLib,這個庫很強大且用法很簡潔,它提供了可靠UDP傳輸,這正是我想要的。 網路通訊的資料協定可以選擇的有很多,我這裡使用的是自製二進位流協議,主要實現的功能是序列化與反序列化,結構體內的字段支援可選。 就像這個PtRoom結構:
//Template auto generator:[AutoGenPt] v1.0
//Creation time:2021/1/28 16:43:48
using System ;
using System . Collections ;
using System . Collections . Generic ;
namespace Net . Pt
{
public class PtRoom
{
public byte __tag__ { get ; private set ; }
public uint RoomId { get ; private set ; }
public byte Status { get ; private set ; }
public uint MapId { get ; private set ; }
public string RoomOwnerUserId { get ; private set ; }
public byte MaxPlayerCount { get ; private set ; }
public List < PtRoomPlayer > Players { get ; private set ; }
public PtRoom SetRoomId ( uint value ) { RoomId = value ; __tag__ |= 1 ; return this ; }
public PtRoom SetStatus ( byte value ) { Status = value ; __tag__ |= 2 ; return this ; }
public PtRoom SetMapId ( uint value ) { MapId = value ; __tag__ |= 4 ; return this ; }
public PtRoom SetRoomOwnerUserId ( string value ) { RoomOwnerUserId = value ; __tag__ |= 8 ; return this ; }
public PtRoom SetMaxPlayerCount ( byte value ) { MaxPlayerCount = value ; __tag__ |= 16 ; return this ; }
public PtRoom SetPlayers ( List < PtRoomPlayer > value ) { Players = value ; __tag__ |= 32 ; return this ; }
public bool HasRoomId ( ) { return ( __tag__ & 1 ) == 1 ; }
public bool HasStatus ( ) { return ( __tag__ & 2 ) == 2 ; }
public bool HasMapId ( ) { return ( __tag__ & 4 ) == 4 ; }
public bool HasRoomOwnerUserId ( ) { return ( __tag__ & 8 ) == 8 ; }
public bool HasMaxPlayerCount ( ) { return ( __tag__ & 16 ) == 16 ; }
public bool HasPlayers ( ) { return ( __tag__ & 32 ) == 32 ; }
public static byte [ ] Write ( PtRoom data )
{
using ( ByteBuffer buffer = new ByteBuffer ( ) )
{
buffer . WriteByte ( data . __tag__ ) ;
if ( data . HasRoomId ( ) ) buffer . WriteUInt32 ( data . RoomId ) ;
if ( data . HasStatus ( ) ) buffer . WriteByte ( data . Status ) ;
if ( data . HasMapId ( ) ) buffer . WriteUInt32 ( data . MapId ) ;
if ( data . HasRoomOwnerUserId ( ) ) buffer . WriteString ( data . RoomOwnerUserId ) ;
if ( data . HasMaxPlayerCount ( ) ) buffer . WriteByte ( data . MaxPlayerCount ) ;
if ( data . HasPlayers ( ) ) buffer . WriteCollection ( data . Players , ( element ) => PtRoomPlayer . Write ( element ) ) ;
return buffer . Getbuffer ( ) ;
}
}
public static PtRoom Read ( byte [ ] bytes )
{
using ( ByteBuffer buffer = new ByteBuffer ( bytes ) )
{
PtRoom data = new PtRoom ( ) ;
data . __tag__ = buffer . ReadByte ( ) ;
if ( data . HasRoomId ( ) ) data . RoomId = buffer . ReadUInt32 ( ) ;
if ( data . HasStatus ( ) ) data . Status = buffer . ReadByte ( ) ;
if ( data . HasMapId ( ) ) data . MapId = buffer . ReadUInt32 ( ) ;
if ( data . HasRoomOwnerUserId ( ) ) data . RoomOwnerUserId = buffer . ReadString ( ) ;
if ( data . HasMaxPlayerCount ( ) ) data . MaxPlayerCount = buffer . ReadByte ( ) ;
if ( data . HasPlayers ( ) ) data . Players = buffer . ReadCollection ( ( rBytes ) => PtRoomPlayer . Read ( rBytes ) ) ;
return data ;
}
}
}
}
這是一個基於幀同步的Unity工程
一些工具:Pt結構體產生工具,Excel2Json產生工具,General庫項目,ServerDll庫項目
設計文件:大綱設計文檔,原型設計文檔,配置表。
以下三張圖分別描述了幀同步模擬器在三種不同場景下的使用情況。
下圖表示客戶端,服務端在同一時刻的大致行為,還有回放邏輯也是對應一致的行為。
這張圖是客戶端和服務端在每個邏輯TICK中執行邏輯。上半部是客戶端,客戶端需要執行的邏輯包含ECSR部分,下半部是服務端部分。
最後一張圖是描述回放的每一個邏輯影格。
透過這幾張圖結合具體做什麼類型的遊戲,我們可以設定自訂System和Component來處理相關邏輯。
這是一個服務集合項目,包括WebServer,GateServer,RoomServer等