A little bee Advanced Version は現在フレーム同期ロジックを SDK に抽出しています。プロジェクト https://github.com/dudu502/littlebee_libs を参照してください。これはフレーム同期ゲームの例であり、ゲーム内の数百のオブジェクトと数千の状態を同期します。 . ゲームの背景は惑星系でのシューティングゲームです。 以下は動画リンクです。
【演奏動画(Youtube)を見る】
【動画をリプレイで見る(Youtube)】
【動画を再生して見る(bilibili)】
【動画をリプレイして見る(bilibili)】
フレーム同期 | 状態の同期 | |
---|---|---|
一貫性 | 必然的な一貫性は設計レベルによって決まります | 一貫性を保証できる |
プレイヤー数 | 限定的なマルチプレイヤーのサポート | 複数のプレイヤーには利点があります |
クロスプラットフォーム | 浮動小数点演算の一貫性を考慮する必要がある | 主な計算はサーバー上で行われるため、クロスプラットフォームの問題はありません。 |
不正行為防止 | 不正行為は簡単ですが、最適化は可能です | 不正行為を防ぐのに非常に優れている可能性があります |
切断して再接続する | 実現は難しいけど不可能ではない | メッセージを再送信する必要があるのは 1 回だけであり、実装が簡単です |
再生要件 | 完璧に実現できる | 達成できません |
ゲームを一時停止する | 実装が簡単 | 実装は簡単ではない |
ネットワーク通信量 | 比較的小さい | 比較的大きい |
開発の難易度 | 比較的複雑な | 比較的単純な |
RTS ゲーム | 適切な | 不適切 |
格闘ゲーム | 適切な | 不適切 |
MOBA ゲーム | 適切な | 不適切 |
MMO ゲーム | 不適切 | 適切な |
フレーム同期の開発プロセスで克服すべき困難を理解した後、次に、より良い実装方法、つまり開発フレームワークの選択を検討します。フレーム同期開発ではデータと性能を分離する必要がありますが、どこまで分離すればよいのでしょうか?データ計算部分を別のスレッドに配置することもできます。この方法でロジックを記述する利点は、サーバーを実行してゲームを迅速に再生する機能も実現できることです。このレベルの分離を実現できるのは ECS だけだと思います。フレーム同期と ECS はまさに完璧なパートナーです。
まず、ECS を紹介する必要があります。ECS はまったく新しいテクノロジーではなく、Unity によって最初に提案されたものでもありません。この用語は非常に早くから登場し、近年Blizzardの『オーバーウォッチ』の影響で一躍有名になりました。 「オーバーウォッチ」のサーバーおよびクライアント フレームワークは完全に ECS に基づいて構築されており、ゲーム メカニクス、ネットワーク、レンダリングにおいて優れたパフォーマンスを備えています。率直に言って、ECS はデザイン パターンのようなものではありません。これまでに使用したデザイン パターンはすべてオブジェクト指向設計の下で議論されており、ECS はオブジェクト指向ではありません。 Unity にも ECS があります。実際、Unity 独自のコンポーネントも ECS の一種ですが、十分に純粋ではありません。 ECS は特にゲームプレイに適しています。 ECS には多くの亜種がありますが、ここでは若干の変更を加えた ECS を紹介します。
これはフレーム同期ゲームの機能です。ゲームにリプレイ システムがある場合、ゲームはフレーム同期を通じて実装される必要があります。再生はビデオ録画とも呼ばれますが、ビデオ ファイルを使用した再生とは大きく異なります。ビデオは通常、再生プロセス中にウィンドウを切り替えることができないため、盗まれたり、悪用されたりする可能性があります。変更され、圧縮され、品質が低下するため、ビデオの再生には大きな欠点があります。フレーム同期再生ではファイルが非常に小さくなり、再生プロセス中にユーザーが任意にウィンドウを切り替えることができます。フレーム同期ゲームに必要なシステムはリプレイシステムと言えます。
ここでは RevenantX/LiteNetLib をお勧めします。このライブラリは非常に強力で使いやすく、まさに私が望んでいたものです。 ネットワーク通信にはさまざまなデータ プロトコルを選択できますが、主な機能はシリアル化と逆シリアル化です。この構造体のフィールドはオプションです。 この PtRoom 構造のように:
//Template auto generator:[AutoGenPt] v1.0
//Creation time:2021/1/28 16:43:48
using System ;
using System . Collections ;
using System . Collections . Generic ;
namespace Net . Pt
{
public class PtRoom
{
public byte __tag__ { get ; private set ; }
public uint RoomId { get ; private set ; }
public byte Status { get ; private set ; }
public uint MapId { get ; private set ; }
public string RoomOwnerUserId { get ; private set ; }
public byte MaxPlayerCount { get ; private set ; }
public List < PtRoomPlayer > Players { get ; private set ; }
public PtRoom SetRoomId ( uint value ) { RoomId = value ; __tag__ |= 1 ; return this ; }
public PtRoom SetStatus ( byte value ) { Status = value ; __tag__ |= 2 ; return this ; }
public PtRoom SetMapId ( uint value ) { MapId = value ; __tag__ |= 4 ; return this ; }
public PtRoom SetRoomOwnerUserId ( string value ) { RoomOwnerUserId = value ; __tag__ |= 8 ; return this ; }
public PtRoom SetMaxPlayerCount ( byte value ) { MaxPlayerCount = value ; __tag__ |= 16 ; return this ; }
public PtRoom SetPlayers ( List < PtRoomPlayer > value ) { Players = value ; __tag__ |= 32 ; return this ; }
public bool HasRoomId ( ) { return ( __tag__ & 1 ) == 1 ; }
public bool HasStatus ( ) { return ( __tag__ & 2 ) == 2 ; }
public bool HasMapId ( ) { return ( __tag__ & 4 ) == 4 ; }
public bool HasRoomOwnerUserId ( ) { return ( __tag__ & 8 ) == 8 ; }
public bool HasMaxPlayerCount ( ) { return ( __tag__ & 16 ) == 16 ; }
public bool HasPlayers ( ) { return ( __tag__ & 32 ) == 32 ; }
public static byte [ ] Write ( PtRoom data )
{
using ( ByteBuffer buffer = new ByteBuffer ( ) )
{
buffer . WriteByte ( data . __tag__ ) ;
if ( data . HasRoomId ( ) ) buffer . WriteUInt32 ( data . RoomId ) ;
if ( data . HasStatus ( ) ) buffer . WriteByte ( data . Status ) ;
if ( data . HasMapId ( ) ) buffer . WriteUInt32 ( data . MapId ) ;
if ( data . HasRoomOwnerUserId ( ) ) buffer . WriteString ( data . RoomOwnerUserId ) ;
if ( data . HasMaxPlayerCount ( ) ) buffer . WriteByte ( data . MaxPlayerCount ) ;
if ( data . HasPlayers ( ) ) buffer . WriteCollection ( data . Players , ( element ) => PtRoomPlayer . Write ( element ) ) ;
return buffer . Getbuffer ( ) ;
}
}
public static PtRoom Read ( byte [ ] bytes )
{
using ( ByteBuffer buffer = new ByteBuffer ( bytes ) )
{
PtRoom data = new PtRoom ( ) ;
data . __tag__ = buffer . ReadByte ( ) ;
if ( data . HasRoomId ( ) ) data . RoomId = buffer . ReadUInt32 ( ) ;
if ( data . HasStatus ( ) ) data . Status = buffer . ReadByte ( ) ;
if ( data . HasMapId ( ) ) data . MapId = buffer . ReadUInt32 ( ) ;
if ( data . HasRoomOwnerUserId ( ) ) data . RoomOwnerUserId = buffer . ReadString ( ) ;
if ( data . HasMaxPlayerCount ( ) ) data . MaxPlayerCount = buffer . ReadByte ( ) ;
if ( data . HasPlayers ( ) ) data . Players = buffer . ReadCollection ( ( rBytes ) => PtRoomPlayer . Read ( rBytes ) ) ;
return data ;
}
}
}
}
これはフレーム同期に基づいた Unity プロジェクトです
一部のツール: Pt 構造生成ツール、Excel2Json 生成ツール、一般ライブラリ プロジェクト、ServerDll ライブラリ プロジェクト
設計書:概要設計書、試作設計書、構成表。
次の 3 つの図は、3 つの異なるシナリオでのフレーム同期シミュレーターの使用法を示しています。
以下の図は、クライアントとサーバーの一般的な動作を同時に示しており、再生ロジックも同じ動作に対応しています。
この図は、各論理 TICK でロジックを実行するクライアントとサーバーを示しています。上部はクライアントが実行する必要があるロジックに ECSR 部分が含まれ、下部はサーバー部分です。
最後の図は、再生の各論理フレームを説明します。
これらの画像と特定の種類のゲームを通じて、関連するロジックを処理するカスタム システムとコンポーネントをセットアップできます。
WebServer、GateServer、RoomServerなどのサービスを集めたプロジェクトです。