Extraiga la parte central de sincronización de cuadros del proyecto de sincronización de cuadros https://github.com/dudu502/LittleBee en el SDK. Este SDK se basa en netstandard2.0. Proporcionar piezas del SDK de servidor y cliente. Actualmente, los principales casos de código utilizan Unity para escribir el cliente y el programa de consola dotnetcore es el servidor.
Docs/
Diagrams/
: Documento de descripción.Protocols/
: Herramientas y configuraciones de protocolos.Engine/
: directorio SDK.Client/
: Biblioteca para proyectos de clientes, que hace referencia al proyecto común, basada en el proyecto netstandard2.0.Common/
: la biblioteca de referencia básica para proyectos de Cliente y Servidor, basada en el proyecto netstandard2.0.Server/
: una biblioteca utilizada por proyectos del lado del servidor, que hace referencia al proyecto común, basada en el proyecto netstandard2.0.Examples/
: Proyectos de casos.Clients/
: Actualmente existe un proyecto de caso desarrollado usando Unity.Servers/
: Actualmente un programa de consola netcore. Encuentre la clase Context en el SDK. Esta es la clase principal del marco. Esta clase es muy importante. Es la entrada a todo el SDK. Esta clase se utiliza como entrada del programa tanto en el cliente como en el servidor. Usos similares de API. El nombre predeterminado de Context se divide en servidor y cliente. Al usarlo, seleccione el tipo de cliente o servidor según sus necesidades para construir una instancia de Context.
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
Pase el nombre en el constructor para especificar el contexto correspondiente:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
Método de adquisición correspondiente:
Context context = Context . Retrieve ( name ) ;
El objeto Context se puede obtener mediante el método anterior en el SDK o en un juego personalizado. Luego podrá obtener fácilmente otros objetos y datos a través del objeto Context.
Este es el diagrama de clases de Context. Context contiene todas las entradas de módulos funcionales proporcionadas por el SDK a los desarrolladores.
diagrama de clases
clase INetworkServer{
+Enviar(IPEndPoint ep,ushort messageId,byte[] datos)
+Enviar(int clientId,ushort messageId,byte[] datos)
+Enviar(int[] clientIds,ushort messageId,byte[] datos)
+Enviar(uID de mensaje corto,byte[] datos)
+Ejecutar(puerto int)
+int ObtenerPuertoActivo()
}
clase INetworkClient{
+Enviar(uID de mensaje corto,byte[] datos)
+Conectar()
+Conectar (ip de cadena, puerto int, clave de cadena)
+Cerrar()
int GetActivePort()
}
clase Controlador de simulación{
+GetFrameMsLength()
+GetFrameLerp()
+UpdateFrameMsLength(factor de flotación)
+CrearSimulación(Simulación sim,ISimulativeBehaviour[] bhs)
+Inicio(FechaHora sHora,int hist_kf_cout,Acción<float> progreso,Acción corredor)
+Detener()
+ObtenerSimulación()
+DisposeSimulación()
}
simulación de clase {
+Inicio()
+ObtenerComportamiento()
+ContainBehaviour(ISimulativeBehaviour beh)
+AgregarComportamiento(ISimulativoComportamiento beh)
+RemoveBehaviour(ISimulatorBehaviour beh)
+Ejecutar()
}
clase IComportamientoSimulativo{
+Inicio()
+Actualizar()
+Detener()
}
contexto de clase {
+Servidor INetworkServer
+Cliente INetworkClient
+ILogger Registrador
+nombre de cadena
+ Contexto (nombre de cadena, cliente INetworkClient, registrador ILogger)
+ Contexto (nombre de cadena, servidor INetworkServer, registrador ILogger)
+ Contexto (nombre de cadena, registrador ILogger)
+Recuperación estática(nombre de cadena)
+Context SetSimulationController(controlador SimulationController)
+Controlador de simulación ObtenerControlador de simulación()
+GetMeta(nombre de cadena,valor predeterminado de cadena)
+SetMeta(nombre de cadena,valor de cadena)
+Context SetModule (módulo AbstractModule)
+M ObtenerMódulo<M>()
+Contexto RemoveModule(Tipo tipo)
}
Los tipos integrados definidos en ContextMetaId son los siguientes. Estos tipos se usarán internamente en el SDK. Los usuarios también pueden almacenar y leer tipos personalizados a través de Context.SetMeta y 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" ;
}
El servidor se divide en dos partes: Gate y Battle. El servicio Gate tiene como objetivo proporcionar a los usuarios un servicio de lobby y permitirles iniciar sesión. Cada usuario tiene un uid diferente. Este uid actualmente solo se prueba con diferentes cadenas. debe ser Guid en la base de datos para garantizar que la identificación de cada usuario sea única. Este servicio Gate proporciona a los usuarios servicios relacionados con el equipo, como crear salas, unirse a salas, salir de salas, etc. Cuando varios usuarios inician una batalla en la sala, el servicio Gate iniciará un nuevo proceso del servicio Battle y notificará a estos usuarios que ingresen al servicio Battle. Cada batalla abrirá un nuevo proceso de batalla. En el servicio Battle, se implementa el servicio de sincronización de fotogramas clave para todos los usuarios.
El siguiente es un ejemplo de código del servidor 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 ( ) ;
}
El siguiente es el caso del proyecto 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 ( ) ;
}
El código del lado del cliente es similar al del lado del servidor y se configura a través de la clase principal Context. El siguiente es el código para el caso 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的构造
}
Este caso muestra el resultado de múltiples entidades ejecutándose simultáneamente en dos clientes. Los objetos no se pueden controlar durante la operación, por lo que este caso también es el más simple. El movimiento de todos los objetos cambia con el marco lógico en el momento de la inicialización.
Este caso muestra que los usuarios pueden controlar el movimiento de objetos y sincronizar los cambios en el movimiento de objetos entre múltiples clientes.
Todos los proyectos de casos hacen referencia a la biblioteca https://github.com/omid3098/OpenTerminal para mostrar comandos de prueba para facilitar la depuración de API. Presione '`' en el teclado para abrir la línea de comandos.
Comando AbrirTerminal | API en SDK | Observación |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID, uid); | Establecer datos de usuario en contexto |
conectar | contextInst.Client.Connect(127.0.0.1, 9030, "sala de entrada"); | Conectarse al servidor de forma predeterminada |
conectar-ip-clave-de-puerto | contextInst.Client.Connect(ip, puerto, clave); | Conéctese al servidor especificado |
crear | contextInst.GetModule().RequestCreateRoom(mapid); | Crear una sala después de conectarse al servidor |
unirse | contextInst.GetModule().RequestJoinRoom(roomId); | Únase a una sala designada |
dejar | contextInst.GetModule().RequestLeaveRoom(); | Salir de la habitación designada |
lista de habitaciones | contextInst.GetModule().RequestRoomList(); | Actualizar lista de habitaciones |
lanzamiento | contextInst.GetModule().RequestLaunchGame(); | iniciar juego |
equipo de actualización | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | Actualizar los datos de la habitación dentro de una habitación |
actualizar mapa | contextInst.GetModule().RequestUpdateMap(roomId, mapId, maxPlayerCount); | Cambiar mapa en la habitación |
detener | Verifique el método Stop en Sample.cs y sus subclases. | final del juego |
mapa | Verifique el método DrawMap en Sample.cs y sus subclases. | dibujar un mapa |
ahorrar | Verifique el método SaveReplay en Sample.cs y sus subclases. | Guardar reproducción (grabación) |
repetición | Verifique el método PlayReplay en Sample.cs y sus subclases. | Guardar reproducción (grabación) |
A continuación se enumeran algunas estructuras de datos clave en el SDK. Estas también pueden convertirse en estructuras de protocolo. Se pueden serializar y deserializar y admiten estructuras de datos opcionales de campo que se usan ampliamente en el SDK. Puede ser generado por herramientas en Docs/Protocols/, en la forma:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
nombre de clase | Campo | Observación |
---|---|---|
MarcoPt | cadena ID de entidad Actualizador de PtComponentUpdaterList byte[] NuevasEntidadesRaw | datos de un fotograma clave |
MarcosPt | int Idx de marco Lista de marcos clave | El conjunto de todos los fotogramas clave en un determinado fotograma. |
PtMap | Versión de cadena Entidades de lista de entidades | datos del mapa |
PtRepetición | Versión de cadena ID de mapa uint ListaEntidadesInit Marcos de lista | Grabación (reproducción) de datos |
El mecanismo de grabación (reproducción) de video es el mecanismo más distintivo en la tecnología de sincronización de cuadros y también es un punto inevitable. El SDK también tiene la capacidad de guardar y cargar vídeos.
El simulador de sincronización de cuadros es una parte clave del SDK para realizar la sincronización de cuadros. El punto clave es que todos los dispositivos deben mantener una cantidad constante de cuadros después de un período de tiempo después del inicio. Esto requiere una calibración de Fecha y Hora en cada tic. la relación entre el lapso de tiempo local y el TICK lógico. El código detallado se puede ver abriendo el archivo SimulationController.cs.