Extrahieren Sie den Kernteil der Frame-Synchronisierung des Frame-Synchronisierungsprojekts https://github.com/dudu502/LittleBee in das SDK. Dieses SDK basiert auf netstandard2.0. Bereitstellung von Server- und Client-SDK-Teilen. Derzeit verwenden die wichtigsten Codefälle Unity zum Schreiben des Clients, und das Dotnetcore-Konsolenprogramm ist der Server.
Docs/
Diagrams/
: Beschreibungsdokument.Protocols/
: Protokolltools und -konfigurationen.Engine/
: SDK-Verzeichnis.Client/
: Bibliothek für Client-Projekte, die auf das Common-Projekt verweist, basierend auf dem netstandard2.0-Projekt.Common/
: Die grundlegende Referenzbibliothek für Client- und Serverprojekte, basierend auf dem netstandard2.0-Projekt.Server/
: Eine Bibliothek, die von serverseitigen Projekten verwendet wird und auf das Common-Projekt verweist, basierend auf dem netstandard2.0-Projekt.Examples/
: Fallprojekte.Clients/
: Derzeit gibt es ein Fallprojekt, das mit Unity entwickelt wurde.Servers/
: Derzeit ein Netcore-Konsolenprogramm. Finden Sie die Kontextklasse im SDK. Diese Klasse ist der Einstieg in das gesamte SDK. Diese Klasse wird sowohl im Client als auch im Server verwendet Ähnliche API-Verwendungen. Der Standardname von Context ist in Server und Client unterteilt. Wählen Sie bei der Verwendung den Client- oder Servertyp entsprechend Ihren Anforderungen aus, um eine Context-Instanz zu erstellen.
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
Übergeben Sie den Namen im Konstruktor, um den entsprechenden Kontext anzugeben:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
Entsprechende Erfassungsmethode:
Context context = Context . Retrieve ( name ) ;
Das Kontextobjekt kann über die obige Methode im SDK oder in einem benutzerdefinierten Spiel abgerufen werden. Über das Kontextobjekt können Sie dann problemlos andere Objekte und Daten abrufen.
Dies ist das Klassendiagramm von Context. Context enthält alle funktionalen Moduleingänge, die das SDK den Entwicklern bereitstellt.
Klassendiagramm
Klasse 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()
}
Klasse INNetworkClient{
+Send(ushort messageId,byte[] data)
+Verbinden()
+Connect(string ip,int port,string key)
+Schließen()
int GetActivePort()
}
Klasse SimulationController{
+GetFrameMsLength()
+GetFrameLerp()
+UpdateFrameMsLength(Float-Faktor)
+CreateSimulation(Simulation sim,ISimulativeBehaviour[] bhs)
+Start(DateTime sTime,int hist_kf_cout,Action<float> progress,Action runner)
+Stop()
+GetSimulation()
+DisposeSimulation()
}
Klasse Simulation{
+Start()
+GetBehaviour()
+ContainBehaviour(ISimulativeBehaviour beh)
+AddBehaviour(ISimulativeBehaviour beh)
+RemoveBehaviour(ISimulativeBehaviour beh)
+Run()
}
Klasse ISimulativeBehaviour{
+Start()
+Update()
+Stop()
}
Klassenkontext {
+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)
+statisch Abrufen(String-Name)
+Context SetSimulationController(SimulationController-Controller)
+SimulationController GetSimulationController()
+GetMeta(String-Name,String-Standardwert)
+SetMeta(String-Name,String-Wert)
+Context SetModule(AbstractModule-Modul)
+M GetModule<M>()
+Context RemoveModule(Typtyp)
}
Die in ContextMetaId definierten integrierten Typen sind wie folgt: Diese Typen werden intern im SDK verwendet. Benutzer können auch benutzerdefinierte Typen über Context.SetMeta und Context.GetMeta speichern.
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" ;
}
Der Server ist in zwei Teile unterteilt: Gate und Battle. Ziel des Gate-Dienstes ist es, Benutzern einen Lobby-Service bereitzustellen und Benutzern die Anmeldung zu ermöglichen. Diese UID wird derzeit nur mit unterschiedlichen Zeichenfolgen getestet sollte Guid in der Datenbank sein, um sicherzustellen, dass die ID jedes Benutzers eindeutig ist. Dieser Gate-Dienst bietet Benutzern teambezogene Dienste wie das Erstellen von Räumen, das Beitreten zu Räumen, das Verlassen von Räumen usw. Wenn mehrere Benutzer einen Kampf im Raum starten, startet der Gate-Dienst einen neuen Prozess des Kampfdienstes und fordert diese Benutzer auf, dem Kampfdienst beizutreten. Jeder Kampf eröffnet einen neuen Kampfprozess. Im Battle-Dienst ist der Dienst zum Synchronisieren von Schlüsselbildern für alle Benutzer implementiert.
Das Folgende ist ein Codebeispiel des Gate-Servers:
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 ( ) ;
}
Als nächstes kommt der Battle-Projektfall:
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 ( ) ;
}
Der Code auf der Clientseite ähnelt dem auf der Serverseite und wird über die Hauptklasse „Context“ festgelegt. Das Folgende ist der Code für Fall 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的构造
}
Dieser Fall zeigt das Ergebnis der gleichzeitigen Ausführung mehrerer Entitäten in zwei Clients. Objekte können während des Betriebs nicht gesteuert werden, daher ist dieser Fall auch der einfachste Fall. Die Bewegung aller Objekte ändert sich mit dem logischen Rahmen zum Zeitpunkt der Initialisierung.
Dieser Fall zeigt, dass Benutzer die Bewegung von Objekten steuern und Änderungen in der Objektbewegung über mehrere Clients hinweg synchronisieren können.
Alle Fallprojekte verweisen auf die Bibliothek https://github.com/omid3098/OpenTerminal, um Testbefehle anzuzeigen und das API-Debugging zu erleichtern. Drücken Sie „“ auf der Tastatur, um die Befehlszeile zu öffnen.
OpenTerminal-Befehl | API im SDK | Bemerkung |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID, uid); | Legen Sie Benutzerdaten im Kontext fest |
verbinden | contextInst.Client.Connect(127.0.0.1, 9030, "gate-room"); | Standardmäßig eine Verbindung zum Server herstellen |
connect-ip-port-key | contextInst.Client.Connect(ip, port, key); | Stellen Sie eine Verbindung zum angegebenen Server her |
erstellen | contextInst.GetModule().RequestCreateRoom(mapid); | Erstellen Sie einen Raum, nachdem Sie eine Verbindung zum Server hergestellt haben |
verbinden | contextInst.GetModule().RequestJoinRoom(roomId); | Treten Sie einem bestimmten Raum bei |
verlassen | contextInst.GetModule().RequestLeaveRoom(); | Verlassen Sie den vorgesehenen Raum |
Zimmerliste | contextInst.GetModule().RequestRoomList(); | Raumliste aktualisieren |
Start | contextInst.GetModule().RequestLaunchGame(); | Spiel starten |
Updateteam | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | Raumdaten innerhalb eines Raumes aktualisieren |
Updatemap | contextInst.GetModule().RequestUpdateMap(roomId, mapId, maxPlayerCount); | Karte im Raum ändern |
stoppen | Bitte überprüfen Sie die Stop-Methode in Sample.cs und seinen Unterklassen | Endspiel |
Drawmap | Bitte überprüfen Sie die DrawMap-Methode in Sample.cs und ihre Unterklassen | Zeichne eine Karte |
saverep | Bitte überprüfen Sie die SaveReplay-Methode in Sample.cs und ihren Unterklassen | Wiedergabe (Aufnahme) speichern |
Spielreport | Bitte überprüfen Sie die PlayReplay-Methode in Sample.cs und ihren Unterklassen | Wiedergabe (Aufnahme) speichern |
Nachfolgend sind einige wichtige Datenstrukturen im SDK aufgeführt. Diese können auch zu Protokollstrukturen werden und unterstützen feldoptionale Datenstrukturen, die im SDK weit verbreitet sind. Es kann von Tools in Docs/Protocols/ in der Form generiert werden:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
Klassenname | Feld | Bemerkung |
---|---|---|
PtFrame | string EntityId PtComponentUpdaterList-Updater byte[] NewEntitiesRaw | ein Keyframe-Daten |
PtFrames | int FrameIdx ListKeyFrames | Die Menge aller Keyframes in einem bestimmten Frame |
PtMap | string-Version EntityList-Entitäten | Kartendaten |
PtReplay | string-Version uint MapId ListInitEntities Frames auflisten | Aufzeichnen (Wiedergeben) von Daten |
Der Videoaufzeichnungs- (Wiedergabe-)Mechanismus ist der markanteste Mechanismus in der Frame-Synchronisationstechnologie und auch ein unvermeidlicher Punkt. Das SDK bietet auch die Möglichkeit, Videos zu speichern und zu laden.
Der Frame-Synchronisationssimulator ist ein wichtiger Bestandteil des SDK, um die Frame-Synchronisierung durchzuführen. Der entscheidende Punkt ist, dass alle Geräte nach einem bestimmten Zeitraum nach dem Start eine konsistente Anzahl von Frames beibehalten müssen. Dies erfordert eine Auflösung die Beziehung zwischen lokalem Zeitraffer und logischem TICK. Der detaillierte Code kann durch Öffnen der Datei SimulationController.cs angezeigt werden.