Извлеките основную часть синхронизации кадров из проекта синхронизации кадров https://github.com/dudu502/LittleBee в SDK. Этот SDK основан на netstandard2.0. Предоставьте серверную и клиентскую части SDK. В настоящее время в основных случаях кода для написания клиента используется Unity, а консольная программа dotnetcore — сервер.
Docs/
Diagrams/
: Документ с описанием.Protocols/
: инструменты и конфигурации протоколов.Engine/
: каталог SDK.Client/
: библиотека для клиентских проектов, ссылающаяся на общий проект, основанный на проекте netstandard2.0.Common/
: базовая справочная библиотека для проектов клиента и сервера, основанная на проекте netstandard2.0.Server/
: библиотека, используемая серверными проектами, ссылающаяся на общий проект, основанный на проекте netstandard2.0.Examples/
: Кейс-проекты.Clients/
: В настоящее время существует кейс-проект, разработанный с использованием Unity.Servers/
: в настоящее время консольная программа Netcore. Найдите класс Context в SDK. Это основной класс платформы. Это вход во весь SDK. Этот класс используется как вход в программу как на клиенте, так и на сервере. аналогичное использование API. Имя контекста по умолчанию. Оно разделено на сервер и клиент. При его использовании выберите тип клиента или сервера в соответствии с вашими потребностями для создания экземпляра контекста.
public const string CLIENT = "client" ;
public const string SERVER = "server" ;
Передайте имя в конструкторе, чтобы указать соответствующий контекст:
Context clientContext = new Context ( Context . CLIENT ) ;
Context serverContext = new Context ( Context . SERVER ) ;
Соответствующий метод получения:
Context context = Context . Retrieve ( name ) ;
Объект Context можно получить описанным выше методом в SDK или в пользовательской игре. Затем вы можете легко получить другие объекты и данные через объект Context.
Это диаграмма классов контекста. Контекст содержит все входы в функциональные модули, предоставляемые SDK разработчикам.
классДиаграмма
класс INetworkServer{
+Send(IPEndPoint ep,ushort messageId,byte[] данные)
+Send(int clientId,ushort messageId,данные byte[])
+Send(int[] clientIds,ushort messageId,byte[] данные)
+Send(ushort messageId, данные byte[])
+Выполнить(интервал порта)
+int GetActivePort()
}
класс INetworkClient {
+Send(ushort messageId, данные byte[])
+Подключиться()
+Connect (строка ip, int порт, строковый ключ)
+Закрыть()
int GetActivePort()
}
класс SimulationController{
+GetFrameMsLength()
+GetFrameLerp()
+UpdateFrameMsLength (коэффициент с плавающей запятой)
+CreateSimulation(Simulation sim,ISimulativeBehaviour[] bhs)
+Start(DateTime sTime,int hist_kf_cout,Action<float> прогресс,Выполнитель действий)
+Стоп()
+Получить симуляцию()
+DisposeSimulation()
}
класс Моделирование {
+Старт()
+Получить поведение()
+ContainBehaviour(ISimulativeBehaviour, да)
+AddBehaviour(ISimulativeBehaviour, да)
+RemoveBehaviour (ISimulativeBehaviour, да)
+Выполнить()
}
класс ISimulativeBehaviour{
+Старт()
+Обновить()
+Стоп()
}
Контекст класса {
+INetworkServer Сервер
+Клиент INetworkClient
+ILogger Регистратор
+имя строки
+Контекст (имя строки, клиент INetworkClient, регистратор ILogger)
+Контекст (имя строки, сервер INetworkServer, регистратор ILogger)
+Контекст (имя строки, регистратор ILogger)
+статическое получение (имя строки)
+Context SetSimulationController (контроллер SimulationController)
+SimulationController GetSimulationController()
+GetMeta(имя строки, значение строки по умолчанию)
+SetMeta(имя строки, значение строки)
+Context SetModule (модуль AbstractModule)
+M GetModule<M>()
+Context RemoveModule (тип типа)
}
В 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 является предоставление пользователям услуги лобби и возможность входа в систему. Каждый пользователь имеет свой собственный идентификатор. Этот идентификатор в настоящее время тестируется только с разными строками. Официальный проект. в базе данных должен быть Guid, чтобы гарантировать уникальность идентификатора каждого пользователя. Этот сервис Gate предоставляет пользователям сервисы, связанные с командой, такие как создание комнат, присоединение к комнатам, выход из комнат и т. д. Когда несколько пользователей начинают битву в комнате, служба ворот запускает новый процесс службы битвы и уведомляет этих пользователей о необходимости войти в службу битвы. Каждая битва будет открывать новый боевой процесс. В сервисе 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 | API в SDK | Примечание |
---|---|---|
setuid | contextInst.SetMeta(ContextMetaId.USER_ID, uid); | Установите пользовательские данные в контексте |
соединять | contextInst.Client.Connect(127.0.0.1, 9030, "ворота"); | Подключиться к серверу по умолчанию |
ключ подключения-IP-порта | contextInst.Client.Connect(ip, порт, ключ); | Подключиться к указанному серверу |
создавать | contextInst.GetModule().RequestCreateRoom(mapid); | Создать комнату после подключения к серверу |
присоединиться | contextInst.GetModule().RequestJoinRoom(roomId); | Присоединяйтесь к указанной комнате |
оставлять | contextInst.GetModule().RequestLeaveRoom(); | Покинуть назначенную комнату |
список комнат | contextInst.GetModule().RequestRoomList(); | Обновить список номеров |
запуск | contextInst.GetModule().RequestLaunchGame(); | Начать игру |
команда обновлений | contextInst.GetModule().RequestUpdatePlayerTeam(roomId, userId, teamId); | Обновить данные комнаты внутри комнаты |
карта обновления | contextInst.GetModule().RequestUpdateMap(roomId, MapId, maxPlayerCount); | Сменить карту в номере |
останавливаться | Пожалуйста, проверьте метод Stop в Sample.cs и его подклассах. | конец игры |
карта чертежа | Пожалуйста, проверьте метод DrawMap в Sample.cs и его подклассах. | Нарисовать карту |
сохранить повтор | Пожалуйста, проверьте метод SaveReplay в Sample.cs и его подклассах. | Сохранить воспроизведение (запись) |
игровой представитель | Пожалуйста, проверьте метод PlayReplay в Sample.cs и его подклассах. | Сохранить воспроизведение (запись) |
Ниже перечислены некоторые ключевые структуры данных в SDK. Они также могут стать структурами протокола. Их можно сериализовать и десериализовать, а также поддерживать структуры данных с дополнительными полями, которые широко используются в SDK. Его можно создать с помощью инструментов в Docs/Protocols/ в виде:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
Имя класса | Поле | Примечание |
---|---|---|
ПтФрейм | строка EntityId Средство обновления PtComponentUpdaterList байт[] NewEntitiesRaw | данные ключевого кадра |
ПтФреймс | int FrameIdx ListKeyFrames | Набор всех ключевых кадров в определенном кадре |
ПтМап | строка Версия Объекты списка сущностей | данные карты |
PtReplay | строка Версия uint MapId ListInitEntities Список кадров | Запись (воспроизведение) данных |
Механизм записи (воспроизведения) видео является наиболее характерным механизмом в технологии кадровой синхронизации, а также неизбежным моментом. SDK также имеет возможность сохранять и загружать видео.
Симулятор синхронизации кадров является ключевой частью SDK для выполнения синхронизации кадров. Ключевым моментом является то, что все устройства должны поддерживать постоянное количество кадров через определенный период времени после запуска. Для этого требуется калибровка DateTime на каждом такте. взаимосвязь между локальным интервалом времени и логическим ТИКОМ. Подробный код можно просмотреть, открыв файл SimulationController.cs.