6.2K Views
December 13, 23
スライド概要
2023/8/23〜25に開催された CEDEC 2023 の講演スライドです。
講師:タエ・マルコス・インキ(ユニティ・テクノロジーズ・ジャパン株式会社)
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
C++ で作ったゲームを Unity Gaming Services で ホストする⽅法 2023
はじめに ⾃⼰紹介 • タエ‧マルコス • Unity Gaming Services のソリューションアーキテクト • ゲームホスティングのみならず、インゲームコミュニケ ーション、ゲームオペレーションなどのサービスを担当 • 概念実証から運⽤まで
はじめに 動機 • • • • 皆で⼀緒にゲームやるのは楽しい! 独りでしたら寂しい… オンラインゲームができる! 運⽤するのは複雑そう…
はじめに • ネットワークの基礎 • P2P • クライアント‧ホスト • リレーサーバー • 専⽤サーバー • ゲームホスティングのインフラ 本⽇のアジェンダ • サーバーの割り当て • グループマッチ • ロビー • マッチメーカー • ゲームホスティングの実装 • Unity Gaming Services • ロビー+リレーの実装 • マッチメーカー+専⽤サーバー の実装 • C++ でのサンプルコード
ネットワークの基礎 ネットワークの基礎
ネットワークの基礎 P2P パケットロス ⾮同期化
ネットワークの基礎 クライアント‧ ホスト ホスト
ネットワークの基礎 XXX.XXX.XXX.XXX 実際の環境 NAT インターネット NAT NAT Firewall YYY.YYY.YYY.YYY
ネットワークの基礎 パブリックIP リレーサーバー XXms インターネット YYms 遅延= XX + YY ms
パブリックIP ネットワークの基礎 専⽤サーバー インターネット 遅延= XXms
ネットワークの基礎 リレーサーバー リレーサーバー • 安い • 遅延 • ホストの脆弱性 • ターンベース • 協⼒プレイ x 専⽤サーバー 専⽤サーバー • パフォーマンスが⾼い • リソースが⾼い • サーバーオーケストレーション • ハイペースアクション • 競争
ゲームホスティングのインフラ ゲームホスティングのインフラ
ゲームホスティングのインフラ ハードウェアの種類 ベアメタル • スペックが⾼くても値段は抑えられる • バンド幅の料⾦も⼊っている • 時間契約が必要 • スケーラビリティが低い クラウドサーバー • スケーラビリティが⾼い • ハードウェア管理不要 • マシンの設定変更不可能 • 利⽤時間が⻑いほど⾼くなる
ゲームホスティングのインフラ サーバープール サーバー 配置 地域1 地域2 地域3
ゲームホスティングのインフラ サーバープール サービス 品質 遅延 パケットロス サービス品質 (QoS) サーバー 地域1 サービス品質 (QoS) サーバー 地域2 サービス品質 (QoS) サーバー 地域3
ゲームホスティングのインフラ サーバープール サーバー 割り当て “qosResults”: [ {region1id, XXms, XX%}, {region2id, YYms, YY%}, {region3id, ZZms, ZZ%} ] サービス品質 (QoS) サーバー 地域1 サービス品質 (QoS) サーバー 地域2 サービス品質 (QoS) サーバー 地域3 サーバー インスタ ンス
ゲームホスティングのインフラ ロビー グループマッチ: ロビー RoomCode: “XYEB”
ゲームホスティングのインフラ グループマッチ: マッチメーカー “attributes”: { “skill” : XXX, “rank” : YYY, “role” : ZZZ } マッチ メーカー “result”: { “matchId” : XXX } “rule”: { “playerCount”: XXX, “skillDifference” : YYY, “rank” : ZZZ, “role” : AAA }
ゲームホスティングの実装 ゲームホスティングの実装
ゲームホスティングの実装 ホスティングサービス ホスティングインフラは複製可能なシステム • 1 回実装すれば様々なゲームに使える • 改善やメンテナンスが必要 • 時間とリソースがかかる
ゲームホスティングの実装 ホスティングサービス 外部サービスとして提供している プラットフォームサービス • プラットフォームの様々な機能との相性 • ⼤体無料 • プラットフォームの環境内だけで利⽤可能 サードパーティーソリューション • プラットフォームに依存しない多様なサービス • ⼤体有料 • クロスプラットフォーム可能 • 例:Amazon GameLift, Microsoft Playfab, Epic Online Services, Unity Gaming Services
ゲームホスティングの実装 Unity Gaming Services (UGS) Unity Gaming Services は、マルチプレイヤーサービス、 ゲーム運営、ユーザー獲得、マネタイズを含む単⼀のモ ジュール式プラットフォームで、ライブゲーム構築にお ける開発者の課題を解決します。エンジンを問わず、専 ⽤ SDK や API ベースサービスで利⽤でき、無料枠もあ りますので、お気軽に試していただけます
ゲームホスティングの実装 Unity Gaming Services 概念 基盤を作る プレイヤーを満⾜させる ゲームを成⻑させる ゲームを構築し、成⻑に合わせて反復 するためのソリューション。 プレイヤーを理解し、魅⼒的な体験を 提供し、LTVを最⼤化する。 新規プレイヤーを獲得し、 収益を上げる。 Accounts • Authentication • Cloud Save Multiplayer • Game Server Hosting (Multiplay) • Relay • Matchmaker • Lobby • Netcode Configure and manage • Remote Config • Cloud Code • Cloud Content Delivery • Economy • Schedules and triggers (alpha) DevOps • Version Control • Build Automation Analytics tools • Analytics • Data Explorer • Funnels • Event Manager • Event Browser • SQL Data Explorer Player engagement • Game Overrides • Push Notifications • User Generated Content (alpha) Monitor performance • Cloud Diagnostics • Cloud Diagnostics Advanced Community solutions • Text Chat (Vivox) • Voice Chat (Vivox) • Friends and Leaderboards (beta) Monetization • Unity and ironSource Ads networks • Mediation (Unity LevelPlay) • IAP • Offerwall User Acquisition • Ad ROAS campaigns • IAP ROAS campaigns • Testing tools
ゲームホスティングの実装 ロビー+リレー
ゲームホスティングの実装
ロビー+リレー
実装
Auth
サービス
// HTTPリクエストのライブラリー
#include <curl/curl.h>
// JSON処理のライブラリー
#include <nlohmann/json.hpp>
// ソケットのライブラリー
#include <winsock2.h>
// レスポンスを保存するためのバッファー
std::string readBuffer;
// URL設定
curl_easy_setopt(req, CURLOPT_URL, "https://playerauth.services.api.unity.com/v1/authentication/anonymous");
curl_easy_setopt(req, CURLOPT_FOLLOWLOCATION, 1L);
// ヘッダー設定
list = curl_slist_append(list, "Accept: application/json");
list = curl_slist_append(list, "Content-Type: application/json");
list = curl_slist_append(list, "charset: utf-8");
list = curl_slist_append(list, ProjectID.c_str());
// POSTリクエストの引数
curl_easy_setopt(req, CURLOPT_HTTPHEADER, list);
curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, "{}");
curl_easy_setopt(req, CURLOPT_VERBOSE, 1L);
curl_easy_setopt(req, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(req, CURLOPT_WRITEDATA, &readBuffer);
// リクエストの実行
res = curl_easy_perform(req);
// エラーチェック
if (res != CURLE_OK)
{
fprintf(stderr, "curl_easy_operation() failed : %s\n", curl_easy_strerror(res));
curl_easy_cleanup(req);
return;
}
// レスポンスを読み込む
json response = json::parse(readBuffer);
bearerToken = response["idToken"];
ゲームホスティングの実装
QoS
ディスカバリ
サービス
ロビー+リレー
実装
// HTTPリクエストのトークンの設定
std::string BearerToken = "Authorization:
Bearer " + bearerToken;
//..//
curl_easy_setopt(req, CURLOPT_URL,
"https://qosdiscovery.services.api.unity.com/v1/servers?
services");
// UDPソケットを作成
int client = socket(AF_INET, SOCK_DGRAM, 0);
if (client < 0) {
std::cout << "Error establishing connection..." <<
std::endl;
exit(1);
}
std::cout << "Socket created..." << std::endl;
// アドレスを設定
struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_addr.sin_addr);
// サーバーに接続
if (connect(client, (struct sockaddr*)&server_addr,
sizeof(server_addr)) == 0) {
//..//
list = curl_slist_append(list,
BearerToken.c_str());
serverList = response["data"]["servers"];
}
// サーバーにデータを送信
send(client, (char*)message.data(), message.size(), 0);
const int bufsize = 1024;
unsigned char buffer[bufsize];
// サーバーにデータを受信
recv(client, (char*)buffer, bufsize, 0);
サーバープール
QoS
サーバー
ゲームホスティングの実装 QoS リクエストパケット Name Size Value Type 1 byte 0x59 VerAndFlow 1 byte [0x00-0xF0] Title varies varies 例: Custom varies varies Name Size Value Sequence 1 byte [0x00-0xFF] Identifier varies varies Timestamp varies varies
ゲームホスティングの実装 QoS レスポンスパケット Name Size Value Type 1 byte 0x59 VerAndFlow 1 byte [0x00-0xF0] Custom varies varies リクエストのコピー 例: Name Size Value Sequence 1 byte [0x00-0xFF] Identifier varies varies Timestamp varies varies
ゲームホスティングの実装 Relay 割り当て サービス ロビー+リレー 実装 サーバープール //...// curl_easy_setopt(req, CURLOPT_URL, "https://relayallocations.services.api.unity.com/v1/allocate"); //...// json body; body["maxConnections"] = maxConnections; body["region"] = "us-east1"; //...// curl_easy_setopt(req, CURLOPT_URL, "https://relayallocations.services.api.unity.com/v1/joincode"); //...// json body; body["allocationId"] = allocationId; //...// curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, body.dump().c_str()); //...// curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, body.dump().c_str()); //...// res = curl_easy_perform(req); //...// res = curl_easy_perform(req); //...// allocation = response["data"]["allocation"]; //...// joinCode = response["joinCode"]; リレー インスタ ンス
ゲームホスティングの実装
/15s
ロビー+リレー
実装
//...//
curl_easy_setopt(req, CURLOPT_URL,
"https://lobby.services.api.unity.com/v1/create");
//...//
json body;
body["name"] = name;
body["maxPlayers"] = maxPlayers;
body["data"]["joinCode"]["visibility"] = "member";
body["data"]["joinCode"]["value"] = joinCode;
//...//
curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS,
body.dump().c_str());
//...//
res = curl_easy_perform(req);
//...//
lobbyCode= response["lobbyCode"];
Lobby
サービス
サーバープール
//...//
curl_easy_setopt(req, CURLOPT_URL,
"https://lobby.services.api.unity.com/v1/{lo
bbyId}/heartbeat");
//...//
curl_easy_setopt(req,
CURLOPT_COPYPOSTFIELDS,
body.dump().c_str());
//...//
res = curl_easy_perform(req);
リレー
インスタ
ンス
ゲームホスティングの実装 ロビー+リレー 実装 //...// curl_easy_setopt(req, CURLOPT_URL, "https://lobby.services.api.unity.com/v1/joinbycode"); //...// json body; body["lobbyCode"] = lobbyCode; //...// curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, body.dump().c_str()); //...// res = curl_easy_perform(req); //...// joinCode = response["data"]["joinCode"]["value"]; Lobby サービス サーバープール サーバー インスタ ンス
ゲームホスティングの実装 ロビー+リレー 実装 //...// curl_easy_setopt(req, CURLOPT_URL, "https://relayallocations.services.api.unity.com/v1/join"); //...// json body; body["joinCode"] = joinCode; //...// curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, body.dump().c_str()); //...// res = curl_easy_perform(req); allocation = response["data"]["allocation"]; //...// hostConnectionData = response["data"]["allocation"]["hostConnectionData"]; connectionData = response[“data”][“allocation”][“connectionData"]; Relay 割り当て サービス Lobby サービス サーバープール リレー インスタ ンス リレー インスタ ンス
ゲームホスティングの実装 Relay Message Protocol コード 名前 0 BIND 1 BIND_RECEIVED 説明 BIND メッセージは、クライアントからリレーサーバーに送られるバインドリクエストを表しま す。 BIND_RECEIVED メッセージは、BIND リクエストを受け取ったリレーサーバーからクライアン トへの応答が成功したことを⽰します。 PING PING メッセージとは、クライアントとリレーサーバー(双⽅向)の間で、接続を維持するため に送信されるメッセージのことを⽰します。 CONNECT_REQUEST CONNECT_REQUEST メッセージは、あるプレイヤー(リクエストクライアント)から他のプ レイヤー(ターゲットクライアント)への接続要求を⽰します。 6 ACCEPTED ACCEPTED メッセージは、ターゲットクライアントとの接続に成功した後、リレーサーバーか らリクエストクライアントに送信される確認メッセージを⽰します。 9 DISCONNECT DISCONNECT メッセージは、接続している 2 ⼈のプレーヤーを切断する要求を⽰します。 2 3 10 RELAY 11 CLOSE 12 ERROR RELAY メッセージは、2 ⼈のプレイヤー(クライアント)間でメッセージを送信する要求を⽰ します。 CLOSE メッセージは、クライアントがリレーサーバーからのバインドを解除する要求を⽰しま す。 ERROR メッセージはエラーが発⽣したことを⽰します。
ゲームホスティングの実装
// メッセージのバイトのベクトル
std::vector<unsigned char> message;
Relay Message Protocol
ヘッダー
Bytes
Purpose
1 .. 2
Signature 0xDA72
3
Version
4
5..
Type
Body
Value
0
varies
varies
// ヘッダー
//// Signature 2 bytes
message.push_back(0xDA);
message.push_back(0x72);
//// Version 1 byte
message.push_back(version);
//// Message type 1 byte
message.push_back(MESSAGE_TYPE::BIND);
// ボディー
//// Accept Mode 1 byte
message.push_back(acceptMode);
//// Nonce 2 bytes
message.push_back((nonce >> 8) & 0xff);
message.push_back(nonce & 0xff);
//// Connection Data Length 1 byte
message.push_back(connectionData->size());
//// Connection Data Length max 255 bytes
message.insert(message.end(), connectionData->begin(),connectionData->end());
// HMAC 計算
unsigned char hmac[SHA256_HASH_SIZE];
hmac_sha256(
hmacKey->data(), hmacKey->size(),
message.data(), message.size(),
hmac, SHA256_HASH_SIZE
);
//// HMAC 32 bytes
message.insert(message.end(), hmac, hmac + SHA256_HASH_SIZE);
// サーバーにメッセージを送信
send(server, (char*)message.data(), message.size(), 0);
ゲームホスティングの実装 割り当て サービス ホスト リレー1 リクエスト:allocate(maxPlayer) リクエスト:allocate(maxPlayer) レスポンス:ip, port, key, connectionData… BIND BIND_RECEIVED PING PING リクエスト:joinCode レスポンス:joinCode(XB87SK) レスポンス:key
ゲームホスティングの実装 ホスト 割り当て サービス クライアント リクエスト:join(XB87SK) リクエスト:join レスポンス:ip, port, key, connectionData, hostConnectionData… レスポンス:key BIND BIND_RECEIVED PING PING リレー1 リレー2
ゲームホスティングの実装 ホスト クライアント リレー1 CONNECT_REQUEST(hostConnectionData) CONNECT ACCEPT ACCEPTED RELAY (data, to:hostAllocationId, from:clientAllocationId) RELAY RELAY (data, to:hostAllocationId, from:clientAllocationId) RELAY (data, to:clientAllocationId, from: hostAllocationId) RELAY RELAY (data, to:clientAllocationId, from: hostAllocationId) リレー2
ゲームホスティングの実装 マッチメーカー+ 専⽤サーバー
ゲームホスティングの実装 マッチメーカー+ 専⽤サーバー Auth サービス 実装 QoS ディスカバリ サービス サーバープール QoS サーバー
ゲームホスティングの実装 マッチメーカー+ 専⽤サーバー 実装 //...// curl_easy_setopt(req, CURLOPT_URL, "https://matchmaker.services.api.unity.com/v2/tickets"); //...// json body; //...// body["queueName"] = "4x4"; body["players"][0]["customData"]["skill"] = skill; //...// body["players"][0]["qosResult"][0]["packetLoss"] = qosResult.usEast.packetLoss; body["players"][0]["qosResult"][0]["latency"] = qosResult.usEast.latency; //...// curl_easy_setopt(req, CURLOPT_COPYPOSTFIELDS, body.dump().c_str()); //...// res = curl_easy_perform(req); ticketId = response["id"]; MM サービス サーバープール
ゲームホスティングの実装 Matchmaker ルールの例 {"Name": "60 Players", "MatchDefinition": { "Teams": [{ "Name": "Team", "TeamCount": {"Min": 1,"Max": 1}, "PlayerCount": {"Min": 60,"Max": 60, "Relaxations": [ { "Type": "RangeControl.ReplaceMin", "AgeType": "Oldest", "Value": 30, "AtSeconds": 15 }] }, "TeamRules": [ { "Type": "Equality", "Source": "Players.CustomData.Character", //...// } ] }], "MatchRules": [ { "Type": "Difference", "Source": "Players.QosResults.Latency", "Reference": 20, //...// } ] }, "BackfillEnabled": true }
ゲームホスティングの実装
マッチメーカー+
専⽤サーバー
実装
//...//
curl_easy_setopt(req, CURLOPT_URL,
"https://matchmaker.services.api.unity.
com/v2/tickets/status?id={ticketId}");
//...//
res = curl_easy_perform(req);
ticketStatus = response["status"];
MM
サービス
GSH
割り当て
サービス
サーバープール
ゲームホスティングの実装 マッチメーカー+ 専⽤サーバー 実装 MM サービス GSH 割り当て サービス サーバープール
ゲームホスティングの実装 GSH の割り当て サーバー合計数 割り当てたサーバー数 クラウド ベアメタル 時間
ゲームホスティングの実装
マッチメーカー+
専⽤サーバー
実装
//...//
curl_easy_setopt(req, CURLOPT_URL,
"https://matchmaker.services.api.unity.
com/v2/tickets/status?id={ticketId}");
//...//
res = curl_easy_perform(req);
ticketStatus = response["status"];
//...//
ip = response[“ip"];
port = response[“port"];
MM
サービス
GSH
割り当て
サービス
サーバープール
ENET
Photon
NGO
UE
Replicant
結論 結論 無敵なソリューションがありません、メリットとデメリットがあります。 • リレーは経済的ですが、パフォーマンスとアンチチートに注意、メッセージプロ トコルも必要 • 専⽤サーバーは監視できる強⼒なマシンを利⽤しますが、運⽤コストが⾼く、サ ーバー配置は運⽤コストに影響するため、定期的にパフォーマンスを確認し、配 置調整する必要があります。 ゲームのニーズと要件により、適切なソリューションは異なります。
C++ で作ったゲームを Unity Gaming Services でホストする⽅法 講演アンケート ご協⼒のお願い Unityの講演アンケートに回答いただいた⽅へ、 抽選で10名様にUnityプレミアムパーカーを プレゼントいたします! 締切:2023年9⽉4⽇(⽉) まで 回答はこちらから https://questant.jp/q/unity03