แยกส่วนหลักของการซิงโครไนซ์เฟรมของโปรเจ็กต์การซิงโครไนซ์เฟรม https://github.com/dudu502/LittleBee ลงใน SDK จัดเตรียมชิ้นส่วน SDK ของเซิร์ฟเวอร์และไคลเอ็นต์ ปัจจุบัน กรณีโค้ดหลักใช้ Unity เพื่อเขียนไคลเอ็นต์ และโปรแกรมคอนโซล dotnetcore คือเซิร์ฟเวอร์
Docs/
Diagrams/
: เอกสารคำอธิบายProtocols/
: เครื่องมือโปรโตคอลและการกำหนดค่าEngine/
: ไดเรกทอรี SDKClient/
: ไลบรารีสำหรับโปรเจ็กต์ไคลเอ็นต์ อ้างอิงถึงโปรเจ็กต์ Common ตามโปรเจ็กต์ netstandard2.0Common/
: ไลบรารีอ้างอิงพื้นฐานสำหรับโปรเจ็กต์ไคลเอนต์และเซิร์ฟเวอร์ อิงตามโปรเจ็กต์ netstandard2.0Server/
: ไลบรารีที่ใช้โดยโปรเจ็กต์ฝั่งเซิร์ฟเวอร์ อ้างอิงโปรเจ็กต์ทั่วไป ตามโปรเจ็กต์ netstandard2.0Examples/
: กรณีโครงการClients/
: ปัจจุบันมีเคสโปรเจ็กต์ที่พัฒนาโดยใช้ UnityServers/
: ปัจจุบันเป็นโปรแกรมคอนโซล netcore ค้นหาคลาสบริบทใน SDK นี่คือคลาสหลักของเฟรมเวิร์ก มันคือ ทางเข้าไปยัง SDK ทั้งหมด การใช้งาน API ที่คล้ายกัน ชื่อเริ่มต้นของ 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 ) ;
สามารถรับออบเจ็กต์บริบทได้ด้วยวิธีการข้างต้นใน SDK หรือในเกมที่กำหนดเอง จากนั้นคุณสามารถรับออบเจ็กต์และข้อมูลอื่น ๆ ได้อย่างง่ายดายผ่านออบเจ็กต์บริบท
นี่คือแผนภาพคลาสของบริบท บริบทประกอบด้วยทางเข้าโมดูลการทำงานทั้งหมดที่ SDK มอบให้กับนักพัฒนา
คลาสไดอะแกรม
คลาส INetworkServer{
+ส่ง(IPEndPoint ep,ushort messageId,byte[] data)
+ส่ง(int clientId,ushort messageId,byte[] data)
+ส่ง(int[] clientIds,ushort messageId,byte[] data)
+ส่ง(ushort messageId,ไบต์[] ข้อมูล)
+ วิ่ง (พอร์ต int)
+int GetActivePort()
-
คลาส INetworkClient{
+ส่ง(ushort messageId,ไบต์[] ข้อมูล)
+เชื่อมต่อ()
+ เชื่อมต่อ (สตริง ip, พอร์ต int, คีย์สตริง)
+ปิด()
int GetActivePort()
-
คลาส SimulationController{
+GetFrameMsLength()
+GetFrameLerp()
+ UpdateFrameMsLength (ปัจจัยลอยตัว)
+ สร้างการจำลอง (ซิมจำลอง, พฤติกรรมจำลอง [] bhs)
+เริ่มต้น(DateTime sTime,int hist_kf_cout,การดำเนินการ<float>ความคืบหน้า,การดำเนินการดำเนินการ)
+หยุด()
+รับการจำลอง()
+กำจัดการจำลอง()
-
การจำลองคลาส{
+เริ่ม()
+รับพฤติกรรม()
+บรรจุพฤติกรรม(พฤติกรรมจำลอง)
+เพิ่มพฤติกรรม(พฤติกรรมจำลอง)
+ ลบพฤติกรรม (พฤติกรรมแบบ ISimulative beh)
+รัน()
-
คลาส ISimulativeBehavior{
+เริ่ม()
+อัปเดต()
+หยุด()
-
บริบทของคลาส {
+เซิร์ฟเวอร์เครือข่าย INetwork
+ไคลเอ็นต์ INetworkClient
+ILogger คนตัดไม้
+ชื่อสตริง
+บริบท(ชื่อสตริง,ไคลเอ็นต์ INetworkClient,ตัวบันทึก ILogger)
+บริบท(ชื่อสตริง,เซิร์ฟเวอร์ INetworkServer,ตัวบันทึก ILogger)
+บริบท(ชื่อสตริง,ตัวบันทึก ILogger)
+ การดึงข้อมูลแบบคงที่ (ชื่อสตริง)
+ชุดบริบทSimulationController(ตัวควบคุม SimulationController)
+ตัวควบคุมการจำลอง GetSimulationController()
+GetMeta(ชื่อสตริง,ค่าเริ่มต้นของสตริง)
+SetMeta(ชื่อสตริง,ค่าสตริง)
+โมดูลชุดบริบท(โมดูลโมดูลนามธรรม)
+M รับโมดูล<M>()
+ บริบท RemoveModule (ประเภทประเภท)
-
ประเภทบิวท์อินที่กำหนดไว้ใน ContextMetaId มีดังนี้
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 มีจุดมุ่งหมายเพื่อให้ผู้ใช้บริการล็อบบี้และอนุญาตให้ผู้ใช้เข้าสู่ระบบ ผู้ใช้แต่ละคนมี uid ที่แตกต่างกัน ขณะนี้ได้รับการทดสอบด้วยสตริงที่แตกต่างกันเท่านั้น ควรเป็น Guid ในฐานข้อมูลเพื่อให้แน่ใจว่า ID ของผู้ใช้แต่ละคนไม่ซ้ำกัน บริการ Gate นี้ให้บริการที่เกี่ยวข้องกับทีมแก่ผู้ใช้ เช่น การสร้างห้อง การเข้าร่วมห้อง การออกจากห้อง เป็นต้น เมื่อผู้ใช้หลายคนเริ่มการต่อสู้ในห้อง บริการ Gate จะเริ่มกระบวนการใหม่ของบริการ Battle และแจ้งให้ผู้ใช้เหล่านี้เข้าสู่บริการ Battle แต่ละการต่อสู้จะเปิดกระบวนการต่อสู้ใหม่ ในบริการ 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 ( ) ;
}
รหัสบนฝั่งไคลเอ็นต์จะคล้ายกับรหัสบนฝั่งเซิร์ฟเวอร์ และตั้งค่าผ่านคลาสหลักของบริบท ต่อไปนี้คือรหัสสำหรับกรณีที่ 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 | หมายเหตุ |
---|---|---|
เซตุอิด | 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); | เข้าร่วมห้องที่กำหนด |
ออกจาก | บริบทInst.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 สิ่งเหล่านี้สามารถกลายเป็นโครงสร้างโปรโตคอลได้ สามารถสร้างได้โดยเครื่องมือใน Docs/Protocols/ ในรูปแบบ:
public class PtMyData
{
//Fields
public static byte [ ] Write ( PtMyData value ) { }
public static PtMyData Read ( byte [ ] bytes ) { }
}
ชื่อชั้นเรียน | สนาม | หมายเหตุ |
---|---|---|
PtFrame | สตริง EntityId ตัวอัพเดต PtComponentUpdaterList ไบต์ [] NewEntitiesRaw | ข้อมูลคีย์เฟรม |
PtFrames | int FrameIdx รายการคีย์เฟรม | ชุดของคีย์เฟรมทั้งหมดในเฟรมใดเฟรมหนึ่ง |
ปตท | เวอร์ชันสตริง เอนทิตีรายการเอนทิตี | ข้อมูลแผนที่ |
PtReplay | เวอร์ชันสตริง ไม่ใช่ MapId ListInitEntities รายการเฟรม | การบันทึก (เล่น) ข้อมูล |
กลไกการบันทึกวิดีโอ (เล่นภาพ) เป็นกลไกที่โดดเด่นที่สุดในเทคโนโลยีการซิงโครไนซ์เฟรม และยังเป็นจุดที่หลีกเลี่ยงไม่ได้อีกด้วย SDK ยังมีความสามารถในการบันทึกและโหลดวิดีโออีกด้วย
ตัวจำลองการซิงโครไนซ์เฟรมเป็นส่วนสำคัญของ SDK ในการซิงโครไนซ์เฟรม ประเด็นสำคัญคืออุปกรณ์ทั้งหมดจะต้องรักษาจำนวนเฟรมที่สอดคล้องกันหลังจากช่วงระยะเวลาหนึ่งหลังจากการเริ่มต้นระบบ ซึ่งจำเป็นต้องมีการปรับเทียบ DateTime ในแต่ละขีด ความสัมพันธ์ระหว่างไทม์แลปส์ในท้องถิ่นและ TICK แบบลอจิคัล สามารถดูโค้ดโดยละเอียดได้โดยเปิดไฟล์ SimulationController.cs