Extraia a parte principal da sincronização de quadros do projeto de sincronização de quadros https://github.com/dudu502/LittleBee no SDK. Este SDK é baseado em netstandard2.0. Fornece peças SDK de servidor e cliente. Atualmente, os principais casos de código usam o Unity para escrever o cliente, e o programa do console dotnetcore é o servidor.
Docs/
Diagrams/
: Documento de descrição.Protocols/
: Ferramentas e configurações de protocolo.Engine/
: diretório SDK.Client/
: Biblioteca para projetos clientes, referenciando o projeto Common, baseado no projeto netstandard2.0.Common/
: A biblioteca de referência básica para projetos Cliente e Servidor, baseada no projeto netstandard2.0.Server/
: Uma biblioteca usada por projetos do lado do servidor, referenciando o projeto Common, baseado no projeto netstandard2.0.Examples/
: Projetos de caso.Clients/
: Atualmente existe um projeto de case desenvolvido em Unity.Servers/
: Atualmente um programa de console netcore. Encontre a classe Context no SDK Esta é a classe principal do framework. Esta classe é muito importante. Esta classe é usada como entrada do programa tanto no cliente quanto no servidor. usos de API semelhantes. O nome padrão do Contexto é dividido em servidor e cliente. Ao usá-lo, selecione o tipo de cliente ou servidor de acordo com suas necessidades para construir uma instância de Contexto.
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
Passe name no construtor para especificar o Contexto correspondente:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
Método de aquisição correspondente:
Context context = Context . Retrieve ( name ) ;
O objeto Context pode ser obtido através do método acima no SDK ou em um jogo personalizado. Você pode então obter facilmente outros objetos e dados por meio do objeto Context.
Este é o diagrama de classes do Context Context que contém todas as entradas do módulo funcional fornecidas pelo SDK aos desenvolvedores.
diagrama de classe
classe INetworkServer{
+Enviar(IPEndPoint ep,ushort messageId,byte[] dados)
+Enviar(int clientId,ushort messageId,byte[] dados)
+Enviar(int[] clientIds,ushort messageId,byte[] dados)
+Enviar(umensagem curtaId,byte[] dados)
+Executar(porta interna)
+int GetActivePort()
}
classe INNetworkClient{
+Enviar(umensagem curtaId,byte[] dados)
+Conectar()
+Conectar(string ip, porta int, chave string)
+Fechar()
int GetActivePort()
}
classe SimulaçãoController{
+GetFrameMsLength()
+GetFrameLerp()
+UpdateFrameMsLength(fator flutuante)
+CreateSimulation(Simulação sim,ISimulativeBehaviour[] bhs)
+Start(DateTime sTime,int hist_kf_cout,Action<float> progresso,Action runner)
+Parar()
+GetSimulação()
+DisposeSimulação()
}
simulação de classe{
+Iniciar()
+GetBehaviour()
+ContainBehaviour(ISimulativeBehaviour beh)
+AddBehaviour(ISimulativeBehaviour beh)
+RemoverComportamento(ISimulativeBehaviour beh)
+Executar()
}
class ISSimulativeBehaviour{
+Iniciar()
+Atualizar()
+Parar()
}
classe Contexto {
+ Servidor INNetworkServer
+Cliente INetworkClient
+ILogger Registrador
+ nome da string
+Context(nome da string, cliente INetworkClient, registrador ILogger)
+Context(nome da string, servidor INetworkServer, registrador ILogger)
+ Contexto (nome da string, registrador ILogger)
+ Recuperação estática (nome da string)
+Context SetSimulationController(controlador SimulationController)
+SimulationController GetSimulationController()
+GetMeta(string nome,string defaultValue)
+SetMeta(nome da string, valor da string)
+Context SetModule (módulo AbstractModule)
+M GetModule<M>()
+Context RemoveModule (tipo de tipo)
}
Os tipos integrados definidos em ContextMetaId são os seguintes. Esses tipos serão usados internamente no SDK. Os usuários também podem armazenar e ler tipos personalizados por meio de Context.SetMeta e 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" ;
}
O servidor é dividido em duas partes: Gate e Battle O serviço Gate visa fornecer aos usuários um serviço de lobby e permitir que os usuários façam login. Cada usuário possui um uid diferente. Atualmente, esse uid é testado apenas com strings diferentes. deve ser Guid no banco de dados para garantir que o ID de cada usuário seja único. Este serviço Gate oferece aos usuários serviços relacionados à equipe, como criação de salas, entrada em salas, saída de salas, etc. Quando vários usuários iniciam uma batalha na sala, o serviço Gate iniciará um novo processo do serviço Battle e notificará esses usuários para entrarem no serviço Battle. Cada batalha abrirá um novo processo de Batalha. No serviço Battle, é implementado o serviço de sincronização de quadros-chave para todos os usuários.
A seguir está um exemplo de código do 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 ( ) ;
}
A seguir está o caso do projeto 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 ( ) ;
}
O código do lado do cliente é semelhante ao do lado do servidor e é definido por meio da classe principal Context. A seguir está o código para o 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 mostra o resultado de múltiplas entidades rodando simultaneamente em dois clientes. Os objetos não podem ser controlados durante a operação, portanto este caso também é o caso mais simples. O movimento de todos os objetos muda com o quadro lógico no momento da inicialização.
Este caso mostra que os usuários podem controlar o movimento de objetos e sincronizar alterações no movimento de objetos em vários clientes.
Todos os projetos de caso fazem referência à biblioteca https://github.com/omid3098/OpenTerminal para exibir comandos de teste para facilitar a depuração da API. Pressione '`' no teclado para abrir a linha de comando.
Comando OpenTerminal | API no SDK | Observação |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID,uid); | Definir dados do usuário no contexto |
conectar | contextInst.Client.Connect(127.0.0.1, 9030, "sala de entrada"); | Conecte-se ao servidor por padrão |
conectar-ip-porta-chave | contextInst.Client.Connect(ip, porta, chave); | Conecte-se ao servidor especificado |
criar | contextInst.GetModule().RequestCreateRoom(mapid); | Crie uma sala após conectar-se ao servidor |
juntar | contextInst.GetModule().RequestJoinRoom(roomId); | Junte-se a uma sala designada |
deixar | contextInst.GetModule().RequestLeaveRoom(); | Sair da sala designada |
lista de salas | contextInst.GetModule().RequestRoomList(); | Atualizar lista de salas |
lançar | contextInst.GetModule().RequestLaunchGame(); | Iniciar jogo |
equipe de atualização | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | Atualizar dados de sala dentro de uma sala |
atualizar mapa | contextInst.GetModule().RequestUpdateMap(roomId, mapId, maxPlayerCount); | Alterar mapa na sala |
parar | Verifique o método Stop em Sample.cs e suas subclasses | fim do jogo |
mapa de desenho | Verifique o método DrawMap em Sample.cs e suas subclasses | Desenhe um mapa |
salvarep | Verifique o método SaveReplay em Sample.cs e suas subclasses | Salvar reprodução (gravação) |
representante de reprodução | Verifique o método PlayReplay em Sample.cs e suas subclasses | Salvar reprodução (gravação) |
Listadas abaixo estão algumas estruturas de dados importantes no SDK. Elas também podem se tornar estruturas de protocolo. Elas podem ser serializadas e desserializadas e dar suporte a estruturas de dados opcionais de campo que são amplamente usadas no SDK. Pode ser gerado por ferramentas em Docs/Protocols/, no formato:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
Nome da classe | Campo | Observação |
---|---|---|
PtFrame | string EntityId Atualizador PtComponentUpdaterList byte[] NewEntitiesRaw | dados de quadro-chave |
PtFrames | interno FrameIdx ListaKeyFrames | O conjunto de todos os quadros-chave em um determinado quadro |
PtMap | Versão da string Entidades EntityList | dados do mapa |
PtReplay | Versão da string uint MapID ListaInitEntidades Quadros de lista | Gravação (reprodução) de dados |
O mecanismo de gravação (reprodução) de vídeo é o mecanismo mais distinto na tecnologia de sincronização de quadros e também é um ponto inevitável. O SDK também tem a capacidade de salvar e carregar vídeos.
O simulador de sincronização de quadros é uma parte fundamental do SDK para realizar a sincronização de quadros. O ponto principal é que todos os dispositivos devem manter um número consistente de quadros após um período de tempo após a inicialização. Isso requer calibração de DateTime em cada tick. a relação entre o lapso de tempo local e o TICK lógico. O código detalhado pode ser visualizado abrindo o arquivo SimulationController.cs.