將幀同步專案https://github.com/dudu502/LittleBee 幀同步核心部分擷取SDK,本SDK基於netstandard2.0。提供服務端,客戶端的SDK部分。目前主要的程式碼案例用Unity編寫客戶端,dotnetcore控制台程式為服務端。
Docs/
Diagrams/
: 說明文件。Protocols/
: 協定工具與設定。Engine/
: SDK目錄。Client/
: 供客戶端項目使用的函式庫,引用Common項目,基於netstandard2.0項目。Common/
: Client和Server專案的基礎參考庫,基於netstandard2.0專案。Server/
: 服務端項目使用的函式庫,引用Common項目,基於netstandard2.0項目。Examples/
: 案例項目。Clients/
: 目前有一個案例項目,使用Unity開發。Servers/
: 目前一個netcore控制台程式。 SDK中找到Context類,這是框架的主類,這個類別很重要它是整個SDK的入口,在客戶端和服務端中都用這個類別作為程式入口,都是相似API的用法,預設Context的name分成服務端和客戶端,使用的時候依需求選擇client或server類型來建構Context實例。
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
在建構方法裡傳入name來指定對應的Context:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
對應的獲取方法:
Context context = Context . Retrieve ( name ) ;
在SDK內或自訂遊戲中均可以透過上述的方法取得Context物件。再透過Context物件可以方便取得其他物件和資料。
這是Context的類別關係圖,Context中包含SDK所有提供給開發者的功能模組入口。
classDiagram
class INetworkServer{
+Send(IPEndPoint ep,ushort messageId,byte[] data)
+Send(int clientId,ushort messageId,byte[] data)
+Send(int[] clientIds,ushort messageId,byte[] data)
+Send(ushort messageId,byte[] data)
+Run(int port)
+int GetActivePort()
}
class INetworkClient{
+Send(ushort messageId,byte[] data)
+Connect()
+Connect(string ip,int port,string key)
+Close()
int GetActivePort()
}
class SimulationController{
+GetFrameMsLength()
+GetFrameLerp()
+UpdateFrameMsLength(float factor)
+CreateSimulation(Simulation sim,ISimulativeBehaviour[] bhs)
+Start(DateTime sTime,int hist_kf_cout,Action<float> progress,Action runner)
+Stop()
+GetSimulation()
+DisposeSimulation()
}
class Simulation{
+Start()
+GetBehaviour()
+ContainBehaviour(ISimulativeBehaviour beh)
+AddBehaviour(ISimulativeBehaviour beh)
+RemoveBehaviour(ISimulativeBehaviour beh)
+Run()
}
class ISimulativeBehaviour{
+Start()
+Update()
+Stop()
}
class Context {
+INetworkServer Server
+INetworkClient Client
+ILogger Logger
+string name
+Context(string name,INetworkClient client,ILogger logger)
+Context(string name,INetworkServer server,ILogger logger)
+Context(string name,ILogger logger)
+static Retrieve(string name)
+Context SetSimulationController(SimulationController controller)
+SimulationController GetSimulationController()
+GetMeta(string name,string defaultValue)
+SetMeta(string name,string value)
+Context SetModule(AbstractModule module)
+M GetModule<M>()
+Context RemoveModule(Type type)
}
ContextMetaId中定義的內建類型如下,這些類型會在SDK內部使用,使用者也可以透過Context.SetMeta和Context.GetMeta來儲存和讀取自訂類型。
public sealed class ContextMetaId
{
public const string USER_ID = "user_id" ;
public const string SERVER_ADDRESS = "server_address" ;
public const string MAX_CONNECTION_COUNT = "max_connection_count" ;
public const string ROOM_MODULE_FULL_PATH = "room_module_full_path" ;
public const string STANDALONE_MODE_PORT = "standalone_mode_port" ;
public const string GATE_SERVER_PORT = "gate_server_port" ;
public const string SELECTED_ROOM_MAP_ID = "selected_room_map_id" ;
public const string PERSISTENT_DATA_PATH = "persistent_data_path" ;
}
伺服器分Gate和Battle兩部分,Gate服務旨在提供使用者一個大廳服務,讓使用者登入進來,每個使用者有一個不同的uid,這個uid目前只用不同的字串差異測試就可以,正式專案應該是資料庫中的guid以確保每個使用者的id是唯一的。在這個Gate服務中提供使用者創建房間,加入房間,離開房間等有關組隊的服務。當多用戶在房間內開啟戰鬥的時候Gate服務會開啟Battle服務的新進程並告知這些用戶進入Battle服務。每一個戰鬥都會開啟一個新的Battle進程。在Battle服務中具體實現了同步關鍵影格給所有使用者的服務。
如下是Gate服務端的程式碼案例:
const string TAG = "gate-room" ;
static void Main ( string [ ] args )
{
// 使用Context.SERVER 构造Context
Context context = new Context ( Context . SERVER , new LiteNetworkServer ( TAG ) , new DefaultConsoleLogger ( TAG ) )
. SetMeta ( ContextMetaId . ROOM_MODULE_FULL_PATH , "battle_dll_path.dll" ) // the battle project's build path.
. SetMeta ( ContextMetaId . MAX_CONNECTION_COUNT , "16" )
. SetMeta ( ContextMetaId . SERVER_ADDRESS , "127.0.0.1" )
. SetModule ( new RoomModule ( ) ) ;
context . Server . Run ( 9030 ) ;
Console . ReadLine ( ) ;
}
接下來是Battle專案案例:
static void Main ( string [ ] args )
{
string key = "SomeConnectionKey" ;
int port = 50000 ;
uint mapId = 1 ;
ushort playerNumber = 100 ;
int gsPort = 9030 ;
// 一些从Gate服务中传入的参数
if ( args . Length > 0 )
{
if ( Array . IndexOf ( args , "-key" ) > - 1 ) key = args [ Array . IndexOf ( args , "-key" ) + 1 ] ;
if ( Array . IndexOf ( args , "-port" ) > - 1 ) port = Convert . ToInt32 ( args [ Array . IndexOf ( args , "-port" ) + 1 ] ) ;
if ( Array . IndexOf ( args , "-mapId" ) > - 1 ) mapId = Convert . ToUInt32 ( args [ Array . IndexOf ( args , "-mapId" ) + 1 ] ) ;
if ( Array . IndexOf ( args , "-playernumber" ) > - 1 ) playerNumber = Convert . ToUInt16 ( args [ Array . IndexOf ( args , "-playernumber" ) + 1 ] ) ;
if ( Array . IndexOf ( args , "-gsPort" ) > - 1 ) gsPort = Convert . ToInt32 ( args [ Array . IndexOf ( args , "-gsPort" ) + 1 ] ) ;
}
Context context = new Context ( Context . SERVER , new LiteNetworkServer ( key ) , new DefaultConsoleLogger ( key ) )
. SetMeta ( ContextMetaId . MAX_CONNECTION_COUNT , playerNumber . ToString ( ) )
. SetMeta ( ContextMetaId . SELECTED_ROOM_MAP_ID , mapId . ToString ( ) )
. SetMeta ( ContextMetaId . GATE_SERVER_PORT , gsPort . ToString ( ) )
. SetModule ( new BattleModule ( ) ) ;
SimulationController simulationController = new SimulationController ( ) ;
simulationController . CreateSimulation ( new Simulation ( ) , new ISimulativeBehaviour [ ] { new ServerLogicFrameBehaviour ( ) } ) ;
context . SetSimulationController ( simulationController ) ;
context . Server . Run ( port ) ;
Console . ReadKey ( ) ;
}
客戶端部分的程式碼也和服務端的類似,都是透過Context主類別來設定的,下面是案例1代碼:
void Awake ( ) {
// 用Context.CLIENT构造Context,客户端的Context还需要指定SimulationController等,因此比服务端的Context稍微复杂一些
MainContext = new Context ( Context . CLIENT , new LiteNetworkClient ( ) , new UnityLogger ( "Unity" ) ) ;
MainContext . SetMeta ( ContextMetaId . STANDALONE_MODE_PORT , "50000" )
. SetMeta ( ContextMetaId . PERSISTENT_DATA_PATH , Application . persistentDataPath ) ;
MainContext . SetModule ( new GateServiceModule ( ) ) //大厅组队相关服务
. SetModule ( new BattleServiceModule ( ) ) ; //帧同步服务
// 构造模拟器控制器,SDK提供了一个Default版本的控制器,一般情况下用Default就可以了
DefaultSimulationController defaultSimulationController = new DefaultSimulationController ( ) ;
MainContext . SetSimulationController ( defaultSimulationController ) ;
defaultSimulationController . CreateSimulation ( new DefaultSimulation ( ) , new EntityWorld ( ) ,
new ISimulativeBehaviour [ ] {
new FrameReceiverBehaviour ( ) , //收取服务器的帧数据并处理
new EntityBehaviour ( ) , //执行ECS中System
} ,
new IEntitySystem [ ]
{
new AppearanceSystem ( ) , //外观显示系统
new MovementSystem ( ) , //自定义移动系统,计算所有Movement组件
new ReboundSystem ( ) , //自定义反弹系统
} ) ;
EntityWorld entityWorld = defaultSimulationController . GetSimulation < DefaultSimulation > ( ) . GetEntityWorld ( ) ;
entityWorld . SetEntityInitializer ( new GameEntityInitializer ( entityWorld ) ) ; // 用于初始化和构造Entity
entityWorld . SetEntityRenderSpawner ( new GameEntityRenderSpawner ( entityWorld , GameContainer ) ) ; //ECSR中Renderer的构造
}
這個案例展示了多個實體在兩個客戶端中同步運行的結果。在運行過程中物體不能被控制,因此這個案例也是最簡單的案例,所有物體在初始化那一刻他們的運動隨邏輯幀的變化都是確定的。
這個案例顯示使用者可以控制物體運動,並且在多個客戶端中都能同步物體運動的變化。
所有的案例項目都引用了https://github.com/omid3098/OpenTerminal 函式庫用來顯示測試指令方便調試API,按下鍵盤'`' 開啟命令列。
OpenTerminal指令 | SDK中的API | 備註 |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID, uid); | 設定Context中的用戶數據 |
connect | contextInst.Client.Connect(127.0.0.1, 9030, "gate-room"); | 預設連接到伺服器 |
connect-ip-port-key | contextInst.Client.Connect(ip, port, key); | 連接到指定伺服器 |
create | contextInst.GetModule().RequestCreateRoom(mapid); | 連接到伺服器後創建房間 |
join | contextInst.GetModule().RequestJoinRoom(roomId); | 加入指定房間 |
leave | contextInst.GetModule().RequestLeaveRoom(); | 離開指定房間 |
roomlist | contextInst.GetModule().RequestRoomList(); | 刷新房間列表 |
launch | contextInst.GetModule().RequestLaunchGame(); | 開始遊戲 |
updateteam | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | 在房間內更新房間數據 |
updatemap | contextInst.GetModule().RequestUpdateMap(roomId, mapId, maxPlayerCount); | 在房間內更換地圖 |
stop | 請查看Sample.cs及其子類別中Stop方法 | 結束遊戲 |
drawmap | 請查看Sample.cs及其子類別中DrawMap方法 | 繪製地圖 |
saverep | 請查看Sample.cs及其子類別中SaveReplay方法 | 保存回放(錄影) |
playrep | 請查看Sample.cs及其子類別中PlayReplay方法 | 保存回放(錄影) |
以下列出SDK中一些關鍵的資料結構,這些也可以成為協定結構體,這些結構可以序列化和反序列化並且支援字段可選資料結構在SDK中大量使用。可以透過Docs/Protocols/中的工俱生成,形如:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
類別名 | 欄位 | 備註 |
---|---|---|
PtFrame | string EntityId PtComponentUpdaterList Updater byte[] NewEntitiesRaw | 一個關鍵影格數據 |
PtFrames | int FrameIdx List KeyFrames | 在某一幀的所有關鍵影格集合 |
PtMap | string Version EntityList Entities | 地圖數據 |
PtReplay | string Version uint MapId List InitEntities List Frames | 錄影(回放)數據 |
錄影(回放)機制是幀同步技術中最有特色的機制,也是一個繞不開的點。 SDK中也有錄影的保存載入。
幀同步模擬器是SDK中執行幀同步的關鍵部分,重點需要不同設備在啟動後經過一段時間後所有的設備都能保持一致的幀數,這就需要在每次tick的時候通過DateTime校準,要做到這一點需要解決本地時間流逝和邏輯TICK之間的關係。詳細程式碼可以開啟SimulationController.cs 檔案查看。