Extract the frame synchronization core part of the frame synchronization project https://github.com/dudu502/LittleBee into the SDK. This SDK is based on netstandard2.0. Provide server and client SDK parts. Currently, the main code cases use Unity to write the client, and the dotnetcore console program is the server.
Docs/
Diagrams/
: Description document.Protocols/
: Protocol tools and configurations.Engine/
: SDK directory.Client/
: Library for client projects, referencing the Common project, based on the netstandard2.0 project.Common/
: The basic reference library for Client and Server projects, based on the netstandard2.0 project.Server/
: A library used by server-side projects, referencing the Common project, based on the netstandard2.0 project.Examples/
: Case projects.Clients/
: Currently there is a case project developed using Unity.Servers/
: Currently a netcore console program. Find the Context class in the SDK. This is the main class of the framework. This class is very important. It is the entrance to the entire SDK. This class is used as the program entrance in both the client and the server. They are similar API usages. The default name of Context It is divided into server and client. When using it, select the client or server type according to your needs to construct a Context instance.
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
Pass name in the constructor to specify the corresponding Context:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
Corresponding acquisition method:
Context context = Context . Retrieve ( name ) ;
The Context object can be obtained through the above method in the SDK or in a custom game. You can then easily obtain other objects and data through the Context object.
This is the class diagram of Context. Context contains all the functional module entrances provided by the SDK to developers.
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)
}
The built-in types defined in ContextMetaId are as follows. These types will be used internally in the SDK. Users can also store and read custom types through Context.SetMeta and 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" ;
}
The server is divided into two parts: Gate and Battle. The Gate service aims to provide users with a lobby service and allow users to log in. Each user has a different uid. This uid is currently only tested with different strings. The official project should be Guid in the database to ensure that each user's ID is unique. This Gate service provides users with team-related services such as creating rooms, joining rooms, leaving rooms, etc. When multiple users start a battle in the room, the Gate service will start a new process of the Battle service and notify these users to enter the Battle service. Each battle will open a new Battle process. In the Battle service, the service of synchronizing key frames to all users is implemented.
The following is a code example of the Gate server:
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 ( ) ;
}
Next is the Battle project case:
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 ( ) ;
}
The code on the client side is similar to that on the server side, and is set through the Context main class. The following is the code for case 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的构造
}
This case shows the result of multiple entities running simultaneously in two clients. Objects cannot be controlled during operation, so this case is also the simplest case. The movement of all objects changes with the logical frame at the moment of initialization.
This case shows that users can control the movement of objects and synchronize changes in object movement across multiple clients.
All case projects reference the https://github.com/omid3098/OpenTerminal library to display test commands to facilitate API debugging. Press '`' on the keyboard to open the command line.
OpenTerminal command | API in SDK | Remark |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID, uid); | Set user data in Context |
connect | contextInst.Client.Connect(127.0.0.1, 9030, "gate-room"); | Connect to server by default |
connect-ip-port-key | contextInst.Client.Connect(ip, port, key); | Connect to the specified server |
create | contextInst.GetModule().RequestCreateRoom(mapid); | Create a room after connecting to the server |
join | contextInst.GetModule().RequestJoinRoom(roomId); | Join a designated room |
leave | contextInst.GetModule().RequestLeaveRoom(); | Leave designated room |
roomlist | contextInst.GetModule().RequestRoomList(); | Refresh room list |
launch | contextInst.GetModule().RequestLaunchGame(); | Start game |
updateteam | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | Update room data within a room |
updatemap | contextInst.GetModule().RequestUpdateMap(roomId, mapId, maxPlayerCount); | Change map in room |
stop | Please check the Stop method in Sample.cs and its subclasses | end game |
drawmap | Please check the DrawMap method in Sample.cs and its subclasses | Draw a map |
saverep | Please check the SaveReplay method in Sample.cs and its subclasses | Save playback (recording) |
playrep | Please check the PlayReplay method in Sample.cs and its subclasses | Save playback (recording) |
Listed below are some key data structures in the SDK. These can also become protocol structures. These can be serialized and deserialized and support field optional data structures that are widely used in the SDK. It can be generated by tools in Docs/Protocols/, in the form:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
Class name | Field | Remark |
---|---|---|
PtFrame | string EntityId PtComponentUpdaterList Updater byte[] NewEntitiesRaw | a keyframe data |
PtFrames | int FrameIdx ListKeyFrames | The set of all keyframes in a certain frame |
PtMap | string Version EntityList Entities | map data |
PtReplay | string Version uint MapId ListInitEntities List Frames | Recording (playback) data |
The video recording (playback) mechanism is the most distinctive mechanism in frame synchronization technology, and it is also an unavoidable point. The SDK also has the ability to save and load videos.
The frame synchronization simulator is a key part of the SDK to perform frame synchronization. The key point is that all devices must maintain a consistent number of frames after a period of time after startup. This requires DateTime calibration at each tick. Achieving this requires resolving the relationship between local time lapse and logical TICK. The detailed code can be viewed by opening the SimulationController.cs file.