[UE5] Lyra改造計画⑤-1 OnlineSubsystemを使ってEOSでPlayer Data Storage(プレイヤーデータストレージ)を使う

Post 2023年6月23日金曜日

C++ Epic Online Services GASでオンラインシリーズ Lyra改造 UE5 Unreal Engine オンラインゲーム

前回の続き。


お約束
この記事作成にあたって使用した主なUnreal Engine バージョンUE 5.1.1

本日のゴール

Lyraを使って、EOSにログイン後、Player Data Storageを利用できるようにする


重要なお知らせ

このサイト主はろくにC++を理解しておりません。なんでこのような仕組みになってるかを説明することはできませんので、予めご了承ください

勉強しなきゃとは思っていますが……


LyraとEOS

LyraではEOS(Epic Online Service)が標準で搭載されています。

オンにする方法は公式にやり方が書いてあります。また、いくつか他のサイトでもTIPSが紹介されてますので、紹介しておきます。Lyraを使わないという方も参考になるはず。


[UE4] UE4.27 の新機能 Online Subsystem EOS を使ってみる

https://qiita.com/EGJ-Daiki_Terauchi/items/e23b2c60738df13d34c3


【UE4】OnlineSubsystemEOSの使い方まとめhttps://shiratori1221.hatenablog.com/entry/2021/12/23/003928


私もいくつかハマりポイントがあったので、いつかどこかで紹介ページを作りたいです。

Onにすると、Lyra起動時に勝手にEOSにログインするしたら画面が現れ、Epicのアカウントでログインできるようになります。

Lyraで標準搭載している機能は下記です。

・Login

→言わずもがな、オンラインサービスにログインする機能。

・Host Session

→いわゆる、サーバーを仮想上で建てること。プレイルームみたいなのをつくるイメージ。

・Join Session

→先ほど作ったプレイルームに、参加すること。

・Find Session

→誰かが作ったプレイルームを検索する。特定のタグや、フレンドが作った部屋だけを検索することもできるし、全検索することもできるが、標準実装されているのは全リストを表示することだけ。



ユーザー側が追加していく必要があるのは下記

・PlayerDataの保存と取得

→今回のメインテーマ。ユーザーのキャラクターのデータなどを保存できる。

いわゆるセーブデータを保存するのとは違う。セーブデータをクラウドに保存する機能はSteam,Epic Games Storeどちらにも標準であるらしい。なのでRPGなどを作ろうという方はこちらのクラウドセーブ機能を使った方が良さそう。

このため、今回の記事の機能は、どちらかというとオンラインゲームを作ろうとしている層に必要な機能といえそう。

参考: https://qiita.com/O_Y_G/items/d20fbac9f6b666e617b2


・Stat(統計)とAchievement(実績)

→いわゆる実績解除に必要な機能。

・ユーザー情報の取得

→キャラではなく、ユーザーの情報。ユーザーの名前、アイコンなどの情報。これは一部標準で実装済み

・ログイン状態の取得

→フレンドのログイン状態を取得

・オンラインボイスチャット

→オプション。私のゲームでは今のところ実装予定なし。

・マッチメイキング

→Find Sessionで取得したセッションのリストから「最適な」セッションを自動で選択する機能。最適、の定義は人により異なるが、例えばプレイする人の地域が近い方がいいでしょうし、プレイヤーのうまさも近い方がいいでしょうし…。

EOSではこの機能はほぼ未実装のため、外部サービスを使うか自分で作るしかない。(あるいは標準機能のように、自分で選択してもらうか)

・他プレイヤーの通報、プレイ拒否(同伴拒否)、バグの通報

・サーバーの通信が切れた時の対応

→いわゆる切断対応ってやつですね。

・オンラインストア機能

→決済機能。


などなどがあります。マッチメイキングはまぁ、全然目処が立ってないですけどなんとかなるでしょう(投げやり)

上記を全て実装する気はないですが、少なくとも私のゲームではマッチメイキングはないとお話にならないので実装する必要があります。どうするかな……


EOS Player Data Storageとは?

前述の通り、プレイヤーのキャラやグッズをどれだけ持ってるかみたいな情報を格納する機能です。

主な機能は二つで、

・Write

・Read

この二つです。書き込みと読み込みですね。実にシンプルです。これらはAsyncつまり非同期で行われます。つまりゲームをプレイしながら裏で読み込みとか書き込みができるわけです。

一方で読み込みが完了しないままプレイがスタートしたらそれはそれで困るので、読み込み完了したよというデリゲートが用意されてます。このデリゲートを使ってプレイ開始すれば良いですね。

という簡単な機能のはずなのに……

C++しか実装できない!?

公式のリファレンスがわけわからない!?

という有様でございまして。中々にハードモード。


こういう時には、便利な機能に頼るしかないんです。Online Subsystemです。


Online Subsystemとは?

これは多分に私の推測や解釈が入ってますが、

オンラインシステムにはSteamだのEOSだの、NintendoだのPS Networkだの色々とあり過ぎるわけです。

それぞれに実装をするには骨が折れるわけです。

そこで、ある程度実装を共通化して、各システムに対応しようという思想が生まれます。

OnlineSubsystemは各システムと、我々ゲーム作成者を繋ぐ架け橋なわけです。

実際には、OnlineSubsystem一つで共通化できるわけではなく、それぞれのシステムにそれぞれのOnlineSubsystemが存在します。例えばOnlineSubsystemSteamとか。大体実装のやり方は同じっぽいんですが。

で、これだと面倒だってことで開発されたのがOnlineServicesってこと……でいいんですかね?(よく分かってない)(紛らわしいですがEOSとは別)


Host Sessionから学んでみよう

しかし、Online Subsystemはなぜか教材が少ないのでちんぷんかんぷんです。C++の時点で仕方ないことは仕方ないのですが。

なので、Lyraの中でHost Sessionがどのように実装されているかを学ぶことで、Player Dataの弄り方も学べそうです。

ということで、LyraのCommon Session Subsystemを見てみましょう。

Host Sessionを書いてあるところを見てみると……。

ふむ、訳わからんですね。でもなんとなく分かるのは、

①オンラインサブシステムを取り出すOnline::GetSubsystem(this->GetWorld());

②オンラインサブシステムから各ポインタを取り出す(①を飛ばして直接取り出しても良い)

③取り出したポインタから、各関数を呼び出して機能を実装する

という流れです。

Join Sessionと見比べてみてもこの流れは間違いなさそう。

ならあとはPlayerDataStorageの機能で、どんなポインタとどんな関数を使うのかさえわかればOKということです。なんだ、思ったより簡単じゃん。

そして1ヶ月が経ちました。


PlayerDataStorageのWriteの実装

やっっと本題。

Common Session Subsystem.hに下記を追加していきます。

Writeの機能を呼び出す関数を作ります。5行目のやつは、Writeが終わった時に呼び出す関数です。とりあえず特になにも今のところはやらないので、空白ですが。

  1. /** UploadSaveData */
  2. UFUNCTION(BlueprintCallable, Category = Session)
  3. void UploadPlayerData(FString FileName, TArray<uint8> ArrayRef);
  4. void OnUploadFileComplete(bool bSuccess, const FUniqueNetId& UserID, const FString& FileName);

次に、今度はcppファイルに内容を作ります。

  1. void UCommonSessionSubsystem::UploadPlayerData(FString FileName, USaveGame* SavedGame)
  2. {
  3.     check(SavedGame);
  4.     TArray<uint8> ArrayRef = UCommonSessionSubsystem::ConvertSaveFileToInt(SavedGame);
  5.     IOnlineIdentityPtr IdentityPointerRef = Online::GetIdentityInterface(GetWorld());
  6.     check(IdentityPointerRef);
  7.     IOnlineUserCloudPtr CloudPointerRef = Online::GetUserCloudInterface(GetWorld());
  8.     check(CloudPointerRef);
  9.     TSharedPtr<const FUniqueNetId> UserIDRef = IdentityPointerRef->GetUniquePlayerId(0).ToSharedRef();
  10.     CloudPointerRef->OnWriteUserFileCompleteDelegates.AddUObject(this, &UCommonSessionSubsystem::OnUploadFileComplete);
  11.     CloudPointerRef->WriteUserFile(*UserIDRef, FileName, ArrayRef);
  12. }
  13. void UCommonSessionSubsystem::OnUploadFileComplete(bool bSuccess, const FUniqueNetId& UserID, const FString& FileName)
  14. {
  15. }


6行目の関数はあとで作りますので一旦スルー

8行目でIdentityのポインタを取り出し、9行目でチェックします。(ユーザーの指定に必要)

10行目でCloudのポインタを取り出し、11行目でチェックします。

14行目 処理が終わった際に先ほど作った関数が呼ぶためのものです。

15行目が、いわゆる書き込みの関数なのですが、書き込み関数に必要なのは、FUniqueNetIDのTsharedしたやつなので、10行目でそれを実施しています。TSharedがなんなのかは私に聞かないでください。


PlayerDataStorageのWriteの実装(2)

いまのままだと、訳わからないデータTArray<int8>しかアップロードできませんので、

セーブデータをアップロードできるよう実装していきます。セーブデータをint8のアレイに変換してやればいいですね。

まずはセーブデータを扱えるように必要なテンプレートを読み込みます。Common Session Subsystem.h

  1. #include "GameFramework/SaveGame.h" //Add
  2. #include "Kismet/GameplayStatics.h" //Add

続いて同じファイル内で関数を実装します。

  1. /** SaveFileConversion */
  2. UFUNCTION(BlueprintCallable, Category = Session)
  3. TArray<uint8> ConvertSaveFileToInt(USaveGame* SavedGame);
  4. /** SaveFileConversion */
  5. UFUNCTION(BlueprintCallable, Category = Session)
  6. USaveGame* ConvertIntToSaveFile(TArray<uint8> Array);

Common Session Subsystem.cppの方に下記を追加していきます。

  1. TArray<uint8> UCommonSessionSubsystem::ConvertSaveFileToInt(USaveGame* SavedGame)
  2. {
  3.     TArray<uint8> LocalArray;
  4.     if (SavedGame) {
  5.         UGameplayStatics::SaveGameToMemory(SavedGame, LocalArray);
  6.     }
  7.     return LocalArray;
  8. }
  9. USaveGame* UCommonSessionSubsystem::ConvertIntToSaveFile(TArray<uint8> Array)
  10. {
  11.     if (!Array.IsEmpty()) {
  12.         USaveGame* LocalObject;
  13.         LocalObject = UGameplayStatics::LoadGameFromMemory(Array);
  14.         return LocalObject;
  15.     }
  16.     else {
  17.         return nullptr;
  18.     }
  19. }

やってることは、ライブラリで実装済みの関数を使っているだけなので、省略!

あとはこれを実際に使ってセーブファイルを変換してアップロードできるようにします


これで、ブループリントから呼び出すことができるようになりました。

では早速ブループリントから呼び出してみましょう。

データの内容はとりあえずなんでもいいので、適当に書き込んでみましょう。

こんな感じに実装しました。ファイル名称はわかりやすい名前を適当に決めてください。


では、早速Lyraを起動します。


Lyraのテスト

今回、EOSの設定をオンにした状態でテストをする必要があるので、Visual StudioのUvtoolを使って設定ファイルなるものを作ってテストします。6300は適当にポート番号を決めちゃってください。

プロジェクト名 -game -customconfig=EOS -AUTH_LOGIN=localhost:6300 -AUTH_PASSWORD=適当に決める -AUTH_TYPE=developer

最初プロジェクト名がいると知らなくて設定ファイル作るのに3日くらいかかりました。


EOS Dev Portal上でチェックする

Dev Portal上でプレイヤーデータストレージのデータに変化がないかみてみます。

Dev Auth Toolで表示されるやたら長いユーザー名を入れる

すると……


あ!!!!データがある!!!




PlayerDataStorageのReadの実装

今度はさっきと同じ流れでReadを実装します。

しかし、Readの方は少し面倒です。Readで取り出した情報を読み出さなければなりません。

今後のことを考えると、ブループリントで情報を取り出せるのがベストです。

ということで下記の流れで実装をしていきます。

Readの命令(非同期でデータのダウンロードが始まる)

データのダウンロードが完了

ダウンロードしたデータをローカルメモリにコピー

コピー完了

デリゲートで通知&ブループリントでEvent Dispatcherみたいに情報を取り出し



実際に実装してみる

Common Session Subsystem.hの#include群の直下にこのDeclareをします。デリゲートの準備です。

  1. DECLARE_DYNAMIC_MULTICAST_DELEGATE_TwoParams(FFileDataStream, FString, FileName, const TArray<uint8>&, FileContents);
  2. DECLARE_DYNAMIC_MULTICAST_DELEGATE(FFaliledToLoadData);


なんでconstと&が必要なのかは全くわかりませんが、これがないとビルドは通りますが、ブループリントの方でコンパイルエラーが出ます。誰か原理を教えてください。

Common Session Subsystem.h のPublicのなかに下記を実装します。

  1.     /** DownloadSaveData */
  2.     UFUNCTION(BlueprintCallable, Category = Session)
  3.     void GetPlayerData(FString FileName);
  4.     /** InvestSaveData */
  5.     UFUNCTION(BlueprintCallable, Category = Session)
  6.     void InvestPlayerData(FString FileName);
  7.     UPROPERTY(BlueprintAssignable, Category = Session)
  8.     FFileDataStream DelegateFilesGet;
  9.     UPROPERTY(BlueprintAssignable, Category = Session)
  10.     FFaliledToLoadData FileStream;
  11.     void OnGetFileComplete(bool bSuccess, const FUniqueNetId& UserID, const FString& FileName);

Common Session Subsystem.cppに実装していきます。

  1. void UCommonSessionSubsystem::GetPlayerData(FString FileName)
  2. {
  3.     UE_LOG(LogTemp, Warning, TEXT("GetPlayerDataStarted"));
  4.     IOnlineIdentityPtr IdentityPointerRef = Online::GetIdentityInterface(GetWorld());
  5.     check(IdentityPointerRef);
  6.     IOnlineUserCloudPtr CloudPointerRef = Online::GetUserCloudInterface(GetWorld());
  7.     check(CloudPointerRef);
  8.     TSharedPtr<const FUniqueNetId> UserIDRef = IdentityPointerRef->GetUniquePlayerId(0).ToSharedRef();
  9.     CloudPointerRef->OnReadUserFileCompleteDelegates.AddUObject(this, &UCommonSessionSubsystem::OnGetFileComplete);
  10.     CloudPointerRef->ReadUserFile(*UserIDRef, FileName);
  11. }
  12. void UCommonSessionSubsystem::InvestPlayerData(FString FileName)
  13. {
  14.     IOnlineIdentityPtr IdentityPointerRef = Online::GetIdentityInterface(GetWorld());
  15.     check(IdentityPointerRef);
  16.     IOnlineUserCloudPtr CloudPointerRef = Online::GetUserCloudInterface(GetWorld());
  17.     check(CloudPointerRef);
  18.     TSharedPtr<const FUniqueNetId> UserIDRef = IdentityPointerRef->GetUniquePlayerId(0).ToSharedRef();
  19.     TArray<uint8> FileContents;
  20.     CloudPointerRef->GetFileContents(*UserIDRef, FileName, FileContents);
  21.     FileStream.Broadcast(FileName, FileContents);
  22. }
  23. void UCommonSessionSubsystem::OnGetFileComplete(bool bSuccess, const FUniqueNetId& UserID, const FString& FileName)
  24. {
  25.     if (bSuccess) {
  26.         UE_LOG(LogTemp, Warning, TEXT("File was gotten"));
  27.         InvestPlayerData(FileName);
  28.     } else     DelegatedFailedFlag.Broadcast();
  29. }

まあ今のままだとint8でしか情報をGetできないので、先程作った、ConvertIntToSaveFile を使ってSaveGameに変換して上げれば使いやすいですね。


ブループリント側の実装

基本どこか好きなブループリントのEvent BeginやEvent Construction のところにデリゲートを繋いで、適当なタイミングでRead命令をするだけです。

デリゲートの実装

左側のデリゲートで、データのReadに失敗した=データがない場合にデータをアップロードします。右側のデリゲートでデータを取得した際にその内容を表示します。

取得したいタイミングで、GetPlayerDataを呼び出します


今回は以上です。次回はStatsについてを予定しています。