Unity3dでのクライアントサーバー通信

みなさんこんにちは! 私は常に、他の人の実際の経験に関する記事を読んで、プレーサーレーキまたはレーキを通過することに非常に興味があります。 したがって、この記事を始めて、ユニットでのゲームの世界でのささやかな経験を共有し、ユニットでの作業の他の人の経験についてさらに学びたいと思います。



そこで、昨年の11月に、私たちのチームはクライアントセッションセッションmnogoshochkaを作り始めました-車に乗って、敵を撃ちます。 私は、チームがユニットで成功したプロジェクトの経験をすでに持っていたと言わなければなりません、それは接触のための3Dレースでした。 そのため、ユニット内の車のテーマはすでによく知られており、これを節約することが計画されていました。 最初に決められた最初のことは、概念実証を可能な限り迅速に作成することでした-ゲームのデモを可能な限り正確に示すことです。 このイベントの目的は理解できる-ゲームに収まらないすべてのものをできるだけ早く遮断することです。 さらに、サーバーエンジンも選択しました。 クライアントですぐにすべてが明らかになりました。Unity3dがすべてですが、サーバーエンジンとして何を選ぶべきでしょうか。 それが問題です。 これについてさらに詳しく説明します。





そのため、次の応募者が見つかりました。

1. フォトン

2. Smartfox

3. 組み込みのUnityネットワーク

4. ElektroServer

5. uLink

6. CrystalEngine



Unityの組み込みネットワークはすぐに拒否しました。 このソリューションはサーバー上の物理学をサポートしていますが、これは完全に権威あるサーバーを意味しますが、この場合、新しいマッチルームを開始するには、サーバー上で新しいUnityインスタンスを作成する必要があります。これは非常に高価です。

ElektroServerは、選択の時点では、他の応募者ほどペースが速くないように思えたので、心の底から良いかもしれません。 さらに、Webサイトを見ると、大多数の顧客はElektroServerを作成したスタジオのサービスを注文しており、サーバーソリューションをリモートで注文した顧客は少数です。 したがって、ElektroServerも拒否しました。

uLinkは興味深いものですが、現時点では非常に新しいものです。 さらに、私が覚えている限りでは、uLinkはユニット内に存在し、クライアント、サーバー、したがってサーバーパフォーマンスの問題が発生します。 原則として、たとえばカウンターのように、プレーヤー自身が試合のためにサーバーを上げることができるようにした場合、ソリューションは良いはずです。 外観では、ユニットに組み込まれたネットワークよりもuLinkの方がはるかに多くの利点があります。

CrystalEngineは若い、進化するエンジンです。 利点-運用前のデモを作成する時点で、私たちのチームはこのエンジンの主な開発者でした。 マイナス面のうち、エンジンは若すぎて、ドキュメントの欠如と大きなコミュニティの欠如に直面しています。 したがって、このエンジンを使用しないことにしました。



合計で、2人のファイナリストが残っています。

Photon vs Smartfox



残念ながら、運用前に両方のソリューションをテストする時間はありませんでした。 両方のエンジンが尊敬を呼び起こし、ゲーム、ドキュメント、活気のあるコミュニティをリリースしました。 そのため、エンジンを選択する上で重要な役割を果たしたのは、私がすでにスマートフォンに精通していて、それを良い面でしか知っていなかったという事実です。 思慮深く、理解しやすいIPA、膨大な数の立ち上げプロジェクトがLinuxで動作します。 一般的に、Smartfoxが勝ちました。 サーバーをテストするためのデモも作成しましたが、オフィスから700クライアントまでしか起動しませんでしたが、その後、オフィスネットワークはすでにゆっくりと死に始め、サーバーはほとんど負荷を感じませんでした。 これに関して、私は質問があります-誰かがゲームサーバーの負荷テストをしましたか? もしそうなら、どのようにテストされましたか?



それでは先に進みましょう。 プログラマーのデモの作成の最初に、チームには3つのメインサーバーがありました。メインサーバーであり、狭いサークルで広く知られているNeodrop(Unity3dおよびサイトunity3d.ruのロシアのインターネットコミュニティの創設者)。 サーバーエンジンとしてSmartfoxが選択された後、私たちのサーバーの人は去りました。 そこで、サーバープログラマのふりをし始めました。

プリプロダクションの1か月半の間、次のデモを開始することが判明しました。

1.共有チャットルームのあるガレージがあります。

2. 2チームの戦いを開始できます。

3.各プレイヤーは4台のうち1台目に乗ります。

スカウト、重装甲車、ミサイル発射機、またはエンジニアなど、すべてが元気に運転し、向きを変え、射殺し、殺し合いました。 ああ、World of Tanksに似た視界システムがまだありましたが、もちろんそれほどクールではありませんでした。



将来を見据えて、次の事実に注目する価値があります。このようなデモの作成には、1か月もかかりませんでした。 その後、生産のためにコードをよりきれいにするためにすべてをゼロから書き直すことにし、一般にモジュールや機器の変更などの必要な機能がすべて可能になったとき、そのようなデモのプレイアビリティのレベルに到達できるのは2〜3か月後です。 さらに、2人ではなく6-7人がプログラムに参加しました。 これは、デモでは実行時にモジュールや機器を変更することが不可能だったために起こりました。この可能性は最終製品にあるはずです。 そのため、1つの機能で人件費が増加する場合があることがわかりました。 (もちろん、それは1つだけではありません。最終製品では、管理パネルで管理できるものとすべきでないものはすべて、それも楽しいです。)



何かが歌詞を傷つけることが判明したので、もう少し技術的な詳細を追加してみます。 プロジェクト中、かなりの数の問題や問題が表面化しました。これらは小さなプロジェクトにはほとんど関係がないか、ほとんど無関係ですが、大規模なプロジェクトでは作業が遅くならないように解決する必要があります。



最初に遭遇した問題は、クライアントとサーバー間の通信プロトコルです。 スマートフォンでは、すべてが次のように配置されます。

1.クライアントコードは、次のタイプのイベントにサブスクライブします。



smartFox.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse);
      
      







2.サーバーからコマンドを呼び出すことにより、クライアント上のすべてのサブスクライバーは、サーバーからパラメーターを送信するメソッドを持ちます。 サーバーからのパラメーターは、コマンド名文字列と、ハッシュテーブルに類似した特定のSFSObject



の形式で送信され、サーバーからクライアントに送信されたすべてのデータが含まれます。



したがって、サーバーからコマンドを実行するには、適切な場所でサーバーメッセージをサブスクライブする必要があり、メソッド内でこれが必要なコマンドであるかどうかを確認し、これが正しいコマンドである場合、 SFSObject



から必要なすべてのデータをSFSObject



、最後にコマンドを実行します。 当然、私は常にペンでこれをしたくありませんでした。 すぐに私に起こった最も簡単なことは、このイベントに対して1つのゲームコントローラに署名し、今後は明示的に個別のコマンド、個別のパラメーターを含むイベントを作成することでした。 次のようになりました:



 public class SFSExtensionsController : MonoBehaviour { public delegate void ExtensionResponceDelegate(string cmd, SFSObject parameters); public static event ExtensionResponceDelegate onExtensionResponce = null; void Start() { smartFox.AddEventListener(SFSEvent.EXTENSION_RESPONSE, OnExtensionResponse); } static void OnExtensionResponse(BaseEvent evt) { string cmd = (string)evt.Params["cmd"]; SFSObject sfsParameters = (SFSObject)evt.Params["params"]; if (onExtensionResponce != null) { onExtensionResponce(cmd, sfsParameters); } } }
      
      







そもそも悪くはありませんが、各受信者の解析パラメーターの問題は依然として残っています。 したがって、次に行われたのは、 onExtensionResponce



イベントにサブスクライブするコントローラーの導入でした。コントローラー内では、論理的に不可欠なチームの一部をインターセプトします。たとえば、移動のみ、または射撃のみを担当するチームをインターセプトし、ゲームロジックの準備が整ったデータでイベントを提供します。

このようなソリューションには利点があります-ゲームロジック内では、必要なフィールドが存在するかどうSFSObject



毎回確認する必要はありません。 しかし、十分なマイナスがあります-イベントが狭く専門化されていて、そのサブスクライバーが1人だけである場合-これのために、私はまったく大騒ぎしたくありません。 さらに、そのようなコントローラーが多数ある場合-目的のイベントが配置されているコントローラーを見つける方法は? また、 SFSObject



には自動SFSObject



がないことも忘れないでください。これまでのところ、クライアントとサーバーの両方で目的のコードを記述することにより、各クラスを手動でシリアル化する必要があります。 (はい、 SFSObject



にはSFSObject



任意のクラスをSFSObject



化するメソッドがありますが、この機能のようなものはSFSObject



ませんでした。整理する時間はありませんでした。)クライアントおよびサーバー上のコマンドの文字列名のIDをペンで維持することも必要です。 神は禁じられています、誰かがどこかに封印されています。 そのため、SmartFoxのパワーと思慮深さにもかかわらず、それをそのまま使用して、複数の人が行うことのできないプロジェクトの楽しみに使用することはできません。



しかし... ...すべての欠点について、上記のシステムはタスクを完了し、デモは完全に機能しました。 しかし、この場所は生産段階で最初に手直しされました。 解決すべきいくつかのタスクがありました。



1.データ交換プロトコルのコマンドのID、およびクライアントとサーバー間でSFSObject



れるSFSObject



の構造。

2.ゲームロジックデータの自動シリアル化/逆シリアル化。

3.ゲームロジックを提供するクラスからの便利なメッセージの送受信。



順調に行きましょう。



ポイント2:自動シリアル化/逆シリアル化。 些細なことのように思えます-リフレクションを使用して、クラスの名前などの必要な追加情報をすべてSFSObject



すれば、幸福が得られます。 しかし、おもちゃはリアルタイムであり、機械の位置、速度、タレットの回転など、絶えず変化するデータの大規模なストリームであるため、この場合の幸福は完全にはほど遠いでしょう。 など このデータの多くは、サーバーとクライアントの間で毎秒数回送信されます。各部屋には32人のプレーヤーが計画されています。これらの部屋は、プロジェクトが破産しないように少なくとも50のサーバーに必要です。したがって、ソリューションは額に収まりません。 そして、次のアイデアが私たちの助けになりました。 SFSObject



ゲームデータをパック/アンパックするためのコードは簡単で、プログラムでさえこのコードを作成できます。 合計で、入力で指定された形式のiksmファイルを受け取り、出力でソースコード付きの2つのファイルを出力するユーティリティが作成されました。1つはUnityのC#で、もう1つはSmartfoxのJavaです。 結果のファイルには、 SFSObject ToSFSObject()



void FromSFSObject(SFSObject sfsObject)



2つのメソッドを持つクラスが含まれます。



C#用にプログラムで生成されたクラスの例



 public class StartMultipleArtilleryShootProxy : ISFSSerializable { public int userId { get; set; } public long barrelId { get; set; } public long projectile { get; set; } public Vec3fProxy position { get; set; } public List<Vec3fProxy> direction { get; set; } public float startSpeed { get; set; } //---------------------------------------------------------------- public StartMultipleArtilleryShootProxy() {} public StartMultipleArtilleryShootProxy(ISFSObject obj) { FromSFSObject(obj); } public StartMultipleArtilleryShootProxy(int userId, long barrelId, long projectile, Vec3fProxy position, List<Vec3fProxy> direction, float startSpeed) { this.userId = userId; this.barrelId = barrelId; this.projectile = projectile; this.position = position; this.direction = direction; this.startSpeed = startSpeed; } //---------------------------------------------------------------- public void FromSFSObject(ISFSObject obj) { this.userId = obj.GetInt("ui"); this.barrelId = obj.GetLong("bi"); this.projectile = obj.GetLong("pr"); this.position = new Vec3fProxy(obj.GetByteArray("p").Bytes); this.direction = new List<Vec3fProxy>(); ISFSArray direction_array = obj.GetSFSArray("d"); for (int i = 0; i < direction_array.Size(); i++) this.direction.Add( new Vec3fProxy(direction_array.GetByteArray(i).Bytes)); this.startSpeed = obj.GetFloat("s"); } //---------------------------------------------------------------- public SFSObject ToSFSObject() { SFSObject obj = new SFSObject(); obj.PutInt("ui", userId); obj.PutLong("bi", barrelId); obj.PutLong("pr", projectile); obj.PutByteArray("p", new ByteArray(position.ToBytes())); if (this.direction == null) Debug.LogError("direction == null in ToSFSObject()"); ISFSArray direction_array = new SFSArray(); for (int i = 0; i < this.direction.Count; i++) direction_array.AddByteArray(new ByteArray(direction[i].ToBytes())); obj.PutSFSArray("d", direction_array); obj.PutFloat("s", startSpeed); return obj; } }
      
      







iksmlファイル内のこのクラスに関するレコード:



 <StartMultipleArtilleryShootProxy userId-ui="int" barrelId-bi="long" projectile-pr="long" position-p="Vec3fProxy" direction-d="Vec3fProxy array" startSpeed-s="float"/>
      
      







プロトコルを変更する場合、xmlファイルを変更し、ユーティリティを実行し、 SFSObject



を必要なクラスにすばやくシリアル化/逆シリアル化できるコードを取得します。 クライアントとサーバーのファイルを置き換えるだけです!



xファイルの形式を開発し、クライアントとサーバー間で送信されるデータだけでなく、コマンドの名前もそこに保存することにより、最初のポイントを解決することは論理的です。 しかし...タイムラインは常にオンになっており、現時点ではそのようなことをする時間はありません。 したがって、チーム名のタイプミスの問題は、パラグラフ3を解決するプロセスで部分的に解決されました。



サーバーからメッセージを受信するための中央コントローラーのアイデアはそれほど悪くなかったので、このコントローラーを残すことにしました。 さらに、サーバーからのイベントごとに、処理するコマンドと出力するデータを正確に把握する独自の小さなクラスを作成することが決定されました。 そのようなクラスをすべて1つの場所にプッシュし、それらに適切な名前を付けて、アプリケーションの開始時にそれらを初期化できます。 したがって、クライアントロジックは、あらゆる種類のスマートフォンについて知る必要はありません。コマンドストアにアクセスするだけで十分です。



 SFSProtocol.Protocol.BATTLE.HitRivalRequest.onGetResponce += OnGetHitRivalResponce;
      
      







イベントハンドラーは、ゲームロジックに必要な既に厳密に型指定されたデータを取得します。



 void OnGetHitRivalResponce(HitResultProxy hitResultProxy) { //    }
      
      







そして今、少し魔法があります。これは、Unityがそれ自体の内部でシングルスレッドコードをプロモートし、サーバーからのイベントも1つのスレッドで呼び出されるという事実に基づいています。 このシングルスレッド操作のおかげで、スマートフォンと直接通信する唯一のコントローラーで、誰かがサーバーからのコマンドを処理したかどうかを確認し、誰も処理していない場合は、すぐにそれをログに叫ぶことができます。 簡単に見える:



 cmdWasCatched = false; if (onExtensionResponce != null) { onExtensionResponce(cmd, sfsParameters); } if (!cmdWasCatched) { Debug.LogError(string.Format("SFSExtensionsController.OnExtensionResponse cmd={0} was not catched!", cmd)); }
      
      







さらに、このようなアーキテクチャにより、クライアントとサーバーの通信に絶対に必要なことを行うことができました:サーバーからの応答のタイムアウトをチェックし、ゲームロジックデータに加えて追加のサービスフィールドを追加します(たとえば、クライアントからの要求が正常に処理されたかどうか、失敗した場合-説明サーバーエラー。)



記事の最後で、踏み込んだもう1つのレーキに言及する価値があります。 プロトコルでの新しいコマンドの作成を簡素化するために、パラメーター化可能なクラスが作成されました。このクラスから、サーバーに送信するデータのタイプ、サーバーから受信するデータのタイプを指定し、コンストラクターをカットできます。 このクラス内のオブジェクトを逆シリアル化するコードは次のようになりました。



 public class SFSRequest<TReqData, TRespData> : DataTransporter where TReqData : ISFSSerializable, new() where TRespData : ISFSSerializable, new() { ….......... protected override void Execute(Sfs2X.Entities.Data.SFSObject sfsParameters) { responceData = new TRespData(); responceData.FromSFSObject(sfsParameters); OnGetResponce(); } }
      
      







そして、すべてがうまくいくように見えますが、1つの大きなことがあります。この場合、 new TRespData()



コンストラクターはリフレクションを使用します。 その結果、彼らは戦っていたものに遭遇しました:反射がまだ使用されており、すでに戦闘で8人のプレイヤーが減速していることが顕著です。 状況を修正するために、チームのクラスにデリゲート用の2つのフィールドが追加されました。これらのフィールドはTReqData



TReqData



作成TRespData



TReqData



。 そのため、基本クラスハンドラは次のようになり始めました。



 protected override void Execute(Sfs2X.Entities.Data.SFSObject sfsParameters) { responceData = respDataConstructor(); responceData.FromSFSObject(sfsParameters); onGetResponce(); }
      
      







そして、特定のコマンドの最終クラスは次のとおりです。



 public class MultipleArtilleryShootStartRequest : SFSRequest<StartMultipleArtilleryShootProxy, StartMultipleArtilleryShootProxy> { public MultipleArtilleryShootStartRequest(string sendCommand, string receiveCommand, Func<StartMultipleArtilleryShootProxy> reqDataConstructor, Func<StartMultipleArtilleryShootProxy> respDataConstructor) : base(sendCommand, receiveCommand, reqDataConstructor, respDataConstructor) { } }
      
      







ちなみに、Habrからの記事は、残念ながら私が失ったリンクを見つけるのに役立ちました。



これらの方法はあまり洗練されていませんが、非常に便利なクライアントサーバー通信であることがわかりました。 質問やコメントがある場合は、お気軽にコメントしてください。 この記事を最後まで読んだ人にとって、小さなパンとは、インターネットで掘り出されたゲームエンジンのパラメーターの概要表です



All Articles