프레임 동기화 프로젝트 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 프로젝트를 기반으로 Common 프로젝트를 참조하는 서버 측 프로젝트에서 사용되는 라이브러리입니다.Examples/
: 사례 프로젝트.Clients/
: 현재 Unity를 사용하여 개발된 사례 프로젝트가 있습니다.Servers/
: 현재 netcore 콘솔 프로그램입니다. SDK에서 Context 클래스를 찾으세요. 이것은 프레임워크의 메인 클래스이며, 전체 SDK에 대한 입구입니다. 이 클래스는 클라이언트와 서버 모두에서 프로그램 입구로 사용됩니다. 유사한 API 사용법입니다. Context의 기본 이름은 서버와 클라이언트로 구분됩니다. 사용 시 필요에 따라 클라이언트 또는 서버 유형을 선택하여 Context 인스턴스를 구성합니다.
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 개체를 통해 다른 개체와 데이터를 쉽게 얻을 수 있습니다.
Context의 클래스 다이어그램은 SDK에서 개발자에게 제공하는 모든 기능 모듈 입구를 포함합니다.
클래스 다이어그램
클래스 INetworkServer{
+보내기(IPEndPoint ep,u짧은 메시지Id,바이트[] 데이터)
+Send(int clientId,ushort messageId,byte[] 데이터)
+Send(int[] clientIds,ushort messageId,byte[] 데이터)
+보내기(u짧은 메시지Id,바이트[] 데이터)
+실행(int 포트)
+int GetActivePort()
}
클래스 INetworkClient{
+보내기(u짧은 메시지Id,바이트[] 데이터)
+연결()
+Connect(문자열 IP, 정수 포트, 문자열 키)
+닫기()
int GetActivePort()
}
클래스 SimulationController{
+GetFrameMs길이()
+GetFrameLerp()
+UpdateFrameMsLength(부동 인자)
+CreateSimulation(시뮬레이션 시뮬레이션, ISimulativeBehaviour[] bhs)
+Start(DateTime sTime,int hist_kf_cout,Action<float> 진행,Action runer)
+정지()
+시뮬레이션 받기()
+Dispose시뮬레이션()
}
수업 시뮬레이션{
+시작()
+GetBehaviour()
+ContainBehaviour(ISimulativeBehaviour beh)
+AddBehaviour(ISimulativeBehaviour beh)
+RemoveBehaviour(ISimulativeBehaviour beh)
+실행()
}
클래스 ISimulativeBehaviour{
+시작()
+업데이트()
+정지()
}
클래스 컨텍스트 {
+INetworkServer 서버
+INetworkClient 클라이언트
+ILogger 로거
+문자열 이름
+Context(문자열 이름, INetworkClient 클라이언트, ILogger 로거)
+Context(문자열 이름, INetworkServer 서버, ILogger 로거)
+Context(문자열 이름, ILogger 로거)
+정적 검색(문자열 이름)
+Context SetSimulationController(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 서비스는 사용자에게 로비 서비스를 제공하고 사용자가 로그인할 수 있도록 하는 것을 목표로 합니다. 각 사용자는 서로 다른 uid를 가지고 있으며 현재는 다른 문자열로만 테스트되고 있습니다. 각 사용자의 ID가 고유한지 확인하려면 데이터베이스에 Guid가 있어야 합니다. Gate 서비스는 사용자에게 방 생성, 방 참여, 방 퇴장 등의 팀 관련 서비스를 제공합니다. 여러 사용자가 방에서 전투를 시작하면 Gate 서비스는 새로운 전투 서비스 프로세스를 시작하고 해당 사용자에게 전투 서비스에 입장하도록 알립니다. 각 전투에서는 새로운 전투 프로세스가 열립니다. 배틀 서비스에서는 키 프레임을 모든 사용자에게 동기화하는 서비스가 구현됩니다.
다음은 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 디버깅을 용이하게 하는 테스트 명령을 표시합니다. 키보드에서 '`'를 눌러 명령줄을 엽니다.
오픈터미널 명령 | SDK의 API | 주목 |
---|---|---|
세트이드 | 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); | 객실 내 지도 변경 |
멈추다 | Sample.cs 및 해당 하위 클래스에서 Stop 메서드를 확인하세요. | 게임 종료 |
드로맵 | Sample.cs 및 해당 하위 클래스에서 DrawMap 메서드를 확인하세요. | 지도 그리기 |
saverep | Sample.cs 및 해당 하위 클래스에서 SaveReplay 메서드를 확인하세요. | 재생 저장(녹음) |
플레이렙 | Sample.cs 및 해당 하위 클래스에서 PlayReplay 메서드를 확인하세요. | 재생 저장(녹음) |
아래에는 SDK의 일부 주요 데이터 구조가 나열되어 있으며 이는 프로토콜 구조가 될 수도 있으며 SDK에서 널리 사용되는 필드 선택적 데이터 구조를 지원할 수도 있습니다. Docs/Protocols/의 도구를 사용하여 다음 형식으로 생성할 수 있습니다.
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
수업명 | 필드 | 주목 |
---|---|---|
Pt프레임 | 문자열 엔터티 ID PtComponentUpdaterList 업데이터 바이트[] NewEntitiesRaw | 키프레임 데이터 |
PtFrame | 정수 FrameIdx ListKeyFrames | 특정 프레임의 모든 키프레임 세트 |
PtMap | 문자열 버전 EntityList 엔터티 | 지도 데이터 |
PtReplay | 문자열 버전 단위 MapId ListInit엔티티 목록 프레임 | 녹음(재생) 데이터 |
영상 녹화(재생) 메커니즘은 프레임 동기화 기술의 가장 특징적인 메커니즘이자, 피할 수 없는 점이기도 하다. SDK에는 비디오를 저장하고 로드하는 기능도 있습니다.
프레임 동기화 시뮬레이터는 프레임 동기화를 수행하는 SDK의 핵심 부분입니다. 핵심은 시작 후 일정 시간이 지난 후에도 일정한 수의 프레임을 유지해야 한다는 것입니다. 이를 위해서는 각 틱마다 날짜 시간 보정이 필요합니다. 로컬 시간 경과와 논리적 TICK 간의 관계. 자세한 코드는 SimulationController.cs 파일을 열어서 볼 수 있습니다.