さて、時間経ちすぎてるだろというつっこみは置いておいて、前の記事で書きましたが、第16回ぷちコンで制作して、まだver0.9.0だった「太極雙陸」のデータが消失しました(まだ少しだけ残ってるけど)。
まぁ自分の開発体制なども振り返ってみると起こるべくして起こったというか、起こって当然というか、なんならこのまま続けたらまた同じことが起きるぞ、という感じです。
反省としてソースコントロールを学んだのと、GitHubについても学ぶので勘弁してつかあさい!
さて!ここ一ヶ月というもの「太極雙陸」のオンライン化を進めてきました。一応、完成まではできました!(モノはありませんが…)
ということでここ最近ネットワークについて手に入れた知識をざっとではありますがまとめていきたいと思います。
お品書き
・Server, Client, Dedicated, Listen
・ネットワークをテストする
・ActorReplicate
・ReplicateMovement
・VariableReplication
・RPC(RemoteProceduralCall)
・Reliable
・ネットワークにおける各クラスの立ち位置
・Client, Serverで処理を分けたい場合
・Lobbyについて
・SeamlessTravel
・ありがちな落とし穴
・どうやったらコツを掴めるか
・サンプルプロジェクト
・Server, Client, Dedicated, Listen
UEではネットワークにはServer-Clientというモデルを採用しています。これは中央にあるServerの役割を担うマシンがゲームを統括し、ClientはServerから渡される情報を元にして各マシン上でゲームを構成する、というモデルです。形態には二種類あり、ServerがServerとしての役割のみを果たすDedicatedServerModeと、Clientのマシンが同時にServerとしての役割を果たすListenServerModeがあります。
DedicatedServerの利点は負荷の低さと、それに伴う通信品質の向上、そしてClient側は皆平等な品質が保証されることです。さらに構造自体はListenよりも単純なため、実装の手間も若干低いです。(ただしDedicatedServerを使用するプロジェクト自体がある程度大きい場合が多いです)
逆にListenServerの最大の強みはレンタルサーバーなどサーバーコストが一切掛からないということです。そのためインディーズやフリーゲームなどはどうしてもこちらになりがちですが、Serverになったマシンの負荷が高く、ちゃんと設計しないとラグくてどうしようもなくなったりします。またClient側のマシンが通信を切断してもゲームが継続できるのに対し、Server側が通信を切断するとゲームが続行不可能だったりと、様々な面でServer側とClient側の差異が出てきてしまいます。
・ネットワークをテストする
Playのオプションで手軽にPIEでネットモードを試せます。
NumberOfPlayersはPlayerの人数です。これが2であることを前提にすると…
PlayStandalone->デフォルト設定です。2つの全く違うマシンとして実行します。マシン同士の接続からテストする際によく使用します。
PlayAsListenServer->2つのマシンの片方がListenServer、片方がClientとして実行されます。とりあえず手軽にListenServerの状態をテストしたい時に使用します。
PlayAsClient->双方がClientとして実行されます。DedicatedServerのテストなどの際に使用します。
いくつかのサンプルプロジェクトはネットワークに最初から対応しています。例えばThirdPersonTemplateなどは作ってからすぐにネットワーク対応ができていますので、PlayAsListenServerやPlayAsClientで体験してみるのが良いかと思います。
・ActorReplicate
UEのネットワークの最小単位はActorです。Actorには「Replicates」というBooleanが付いています。
これをチェックすることによってServer-Client間でこのActorが共有されます。このままだとReplicateされるのはSpawnおよびDestroyされた時の処理のみです。実際このActorをレベルに配置すると、Client側だとDestroyが通らず、Server側でDestroyした場合にはServer側のActorと同時にClient側のActorも消えます。(当然何かしらで可視化してないと観察できませんw)
これに限らず共有は基本的に上位であるServerから下位であるClientに行われ、Client側から直接共有されているActorにアクセスしようとしても拒否されて何も起きません。これをするためにはClient側からServerに「このActorにこういう動作をさせてください」という「お伺い」を立てる必要があります。
さて、このままだとSpawn-Destroy以外の動作や数値などなにも共有されないので、これらを共有するためには他にも色々する必要があります。ただし、以下のものはまずActorReplicateが動作している(つまりこれがチェックされている)ことが前提です。
ActorReplicateをチェックせずに以下をやっても何も起きないので注意しましょう。
・ReplicateMovement
同じくActorに付いているBooleanとしてReplicateMovementというパラメーターが存在します。
これはActorの位置情報などをReplicateするものです。かなり便利な機能ですが、通信する度にいちいち情報の共有が行われるため帯域の圧迫が比較的大きいです。「太極雙陸」ではネットワークで石の動きをこれで共有しようとしたため、かなりカックカクになってしまいました。なので動作の共有に関してはよく考える必要があると思います。
・VariableReplication
Variable変数にも共有機能があります。Replicationというパラメーターです。
3つのEnumが使用されており、それぞれ
None->共有しない。
Replicated->共有する。
RepNotify->共有する。更に共有した際に特定の関数を(Server, Client双方で)実行する。
となっています。RepNotifyを選択すると自動的にOn_Repホニャララという関数が作成され、それが共有時に実行されます。
共有するタイミングは基本的に変数が変更された際ですが、RepNotifyはちょっと変わった動作があります。"Set"以外で変数が変更された場合(例としてはArrayを操作するAdd、Remove、Clear、Resize、Reverse、SetArrayElemなど)、On_Rep関数はClient側でしか動作しません。
恐らく基本的にRepNotifyはSetで変更されることを前提に動いているためだと思われます。
・RPC(RemoteProceduralCall)
Replicationが変数の共有ならば、こちらは動作(Event)の共有です。これもEnumでいくつか種類が用意されています。
NotReplicated->共有しない
Multicast->基本的にServer側で使用。ServerとClient双方で同じEventを実行する。
RunOnServer->基本的にClient側で使用。Server側で実行させる。イメージとしてServerに「お伺い」を立てるために使用するEvent。
RunOnOwningClient->Actorを所有しているClient側で実行させる。UI関連で使用することが多い。
どれもかなり重要な動作になりますが、一番使用頻度が高いのはRunOnServerでしょうか?MulticastとReplicationはほぼ同じ動作ができますが、信頼性としてはMulticastの方が高く遅延しにくいです。(もちろんその分帯域を食います)
・Reliable
更にRPCにはReliableというBooleanパラメーターが用意されています。
Reliableとそうでない場合にはそもそも通信に関する動作が全く違うのですが、それを説明するととても複雑なので、基本的にReliableがチェックされている場合は遅れたとしてもその動作は確実に実行される。逆にチェックされていないと実行されない場合がある。ただしReliableの方が帯域を食う。という感じに覚えておけばいいかなと思います。もっと詳しく知りたい方は以下のスライドが参考になります。
・ネットワークにおける各クラスの立ち位置
シングルプレイでのゲームばかり開発していると、疑問に思うことがあります。「GameModeとPlayerControllerって一緒にしてもいいんじゃない?」「このBPの候補PlayerStateとかGameStateってActorは何?」など。
ネットワークに触ってみると、GameModeとPlayerControllerは実は全く違う動作をするクラスなのだと分かります。
このようにシングルプレイと
マルチプレイのゲームではそれぞれのクラスの役割がかなり変わっています。なのでこれらがどのような役割を担っているのかを見直していきます。
・GameMode
GameModeの最大の特徴は全マシン上でもServerに一つしか存在しないという点です。なのでClient側でGetGameModeを呼び出してもnullptrが返ってきます。GameModeはその唯一性からターン情報や勝利条件、ゲームのセッティングなど、全てのClientが共通で持つべき情報を所有するのに適しています。ちなみにGameModeBaseクラスはGameModeクラスの機能縮小版です。
特に重要なイベントノードとしてOnPostLogin、OnSwapPlayerControllersが挙げられます。
ともにレベル遷移の際に使用するノードなのですが、OnPostLoginは通常のレベル遷移、OnSwapPlayerControllersはSeamlessTravel(後述)の際に使用します。どちらもアクセスしてきたPlayerControllerが得られるのが特徴で、ClientがGameModeから各PlayerControllerにアクセスできるよう、通常はこれをArrayにして
保有するというのが基本になります。
こんな感じに。
・PlayerController
PlayerControllerはそれぞれのPlayerが入力などを行うために所有するクラスです。ServerとClientの橋渡しをする役目を担っており、
マルチプレイゲームを作成するとかなりごちゃごちゃします。RunOnServerイベントは基本的にこのクラスから発信されることが多いです。
PlayerControllerはお互いのPlayerControllerを知ることができないため、他のPlayerControllerに何かしてもらいたい場合には一旦RunOnServerでGameModeをGetし、そこからPlayerControllerにアクセスしてもらうというようなやり方をします。
・GameState、PlayerState
今回はあまり使用していないです。
こちらの記事がとても分かりやすいので割愛。
UMGを始めとする
Widget、UIの類は基本的にPlayer同士で共有しません(するゲームもあるとは思いますが)。つまり共有されていないものにアクセスすることになるので、これらにServer側からアクセスするには基本的にRunOnOwningClientを使用します。
・その他の共有されたActorなど
これらはServer側が所有権を持っているため、Clientからいくらアクセスしても何もしません。Server側から変更した変数のReplicationや、RepNotify、もしくはMulticastで動作させることができます。
・Client, Serverで処理を分けたい場合
ClientとServerで処理を分けたい場合にはActorの場合SwitchHasAuthority、UMGの場合はIsServerを使用します。
・Lobbyについて
PIEでのテストの場合、いきなり最初から何人かのプレイヤーが存在しているような状態になっていますが、実際のゲーム上ではそんな事はありえません。プレイヤーが”集合”する場所を作成する必要があります。
大体のゲームではこれにLobbyというLevelを使用しています。Lobbyの作成及び接続にはCreateSession、FindSessions、JoinSessionなどのノードが使用されます。
Lobbyへの接続形態としては、基本的にServer側はCreateSessionを実行してOnSuccessでLobbyLevelにOpenLevelしてそこに待機し、Client側はFindSessionでみつけたSessionArrayから参加できるSessionを探してJoinSessionノードを実行する、という流れになります。(Join後にLevel遷移を処理する必要はありません。自動的に遷移します)
またSessionから抜けるためにはDestroySessionを使用します。
Server側が落ちた際にSessionから抜け出せていないと、新たにSessionに接続できなくなるので、例えばEventEndPlayなどにDestroySessionを繋げばそのような動作を防ぐことができます。
・SeamlessTravel
さて、LobbyLevelは単なる集合場所に過ぎないので、そこからさらにゲームを実際に行うLevelまで移動しなければなりません。その時に使用するのがSeamlessTravel(ServerTravel)です。
原理的な説明はこのスライドが詳しいです。実際にSeamlessTravelはBPには公開されていないためConsoleCommandを叩く必要性があります。使い方は以下のように…
Serverがこれを実行すればClientごとSeamlessTravelを行います。
その後SeamlessTravelした先のGameModeのOnSwapPlayerControllersに処理が流れます。
これが実質的なSeamlessTravelにおけるOnPostLoginとなります。
SeamlessTravelはPIEに対応していない(対応してくれ…マジで…)ため、これ以降をテストする際にはStandaloneで実行する必要があります。
・ありがちな落とし穴
▼なぜかServer側でしか実行されない。
GameModeなどの参照をBeginPlayした時に保存して持っておく、というのは結構やったりしますが、Client側だとGetGameModeはnullptrが返ってきます。上のようにすると、当然Castには失敗し、CastFailedに処理が流れているためDoSomethingには処理が流れません。
Initialize処理などがServerでしか走らない場合、こうなっていたりします。エラーが出ないのが本当に恐ろしい…
▼OnSwapPlayerControllers後のGetPlayerController
OnSwapPlayerControllersが走っている間はまだ前のPlayerControllerが登録されているため、それを考慮しないとGetPlayerControllerなどが妙な動作をしてしまいます。処理が終わればきちんと無くなりますので、Delayを使用するのも手です。
▼MaterialInstanceDynamicはReplicateしてはいけない
ContentSampleの信号機のサンプルでもMIDはReplicationがされていません。これを試しにReplicationすると分かるのですが、エラーが出ます。
どうやらMIDはReplicateしてはいけないらしいです。余談ですが「太極雙陸」ではまだネットワークに対しての理解が浅かったので、爻(こう)の各個情報としてMIDをStructureに含めてしまったため、
他の爻の情報をReplicateすればMIDがReplicateされ、MIDをReplicateしなければ他の情報がReplicateできないということになり、結局MID情報をStructureから切り離すハメになりました。
↑消えたプロジェクトの残滓から発掘したStructure情報。上からStaticMeshComponent、現在の色、置いてある石、MID、石を整列させる時に使用するSpline。なんとなくやったこんなことが後に悲劇を生むとは…
・どうやったらコツを掴めるか
手を動かす。
才能のある人ならともかくとして、僕の場合はどうしても頭で理解するのには限界があるので、実践で感覚的なものを掴んでいくしかない感じでした。
実際、一つ作ってみると覚束ないながらもだんだんと分かっていくので、おすすめです。なんというか、プログラミング分野にもなんだかんだ「感覚で覚える」ってのはあるんですよねぇ…
・サンプルプロジェクト
最後にお土産です。
Githubにチャット機能のサンプルプログラムを公開いたしました!簡単なものですが、皆様のご参考になれば幸いです!チャット機能は比較的簡単に実装できますので、ネットワークの初学としてオススメです。(SeamlessTravel(
デバッグがとても面倒臭い!)をする必要もないし)
ご指摘、ご質問、ご意見などあればコメントにお願いします。