MagicOnion を使って AWS で専用ゲームサーバーを作ろう

2024-03-04
デベロッパーのためのクラウド活用方法

Author : 西坂 信哉

 

2023 年 2 月、Amazon GameLift C# Server SDK が.NET6に対応 しました。このアップデートにより、Unity のヘッドレスサーバーだけでなく、C# ベースの軽量なロジックを持った専用ゲームサーバーの実装もしやすくなりました。物理演算を必ずしも必要としない、モバイル向けのライトなマルチプレイ用サーバーなどでも利用しやすくなっています。

本記事では、2024 年 2 月時点で最新となる MagicOnion v6 を使って作成した専用ゲームサーバーを AWS のゲームサーバーホスティングサービスである Amazon GameLift 上で動かしてみたいと思います。


MagicOnion とは

MagicOnion は .NET ベースの双方向 RPC フレームワークです。サーバーサイド、クライアントサイド共に C# で実装でき、コードの共有等の面で効率的な開発が可能です。クライアントサイドは Unity にも対応しており、ゲーム業界での利用実績も多くあります。

MagicOnion v6 のサーバーサイド SDK は .NET6 以上に対応しており、GameLift C# Server SDK が .NET6 に対応したことから、MagicOnion と GameLift を組み合わせて利用できるようになりました。

本記事ではまず MagicOnion 単体の最小限のサンプルコードを実装し、そこに GameLift Server SDK を組み込む手順で進めていきます。

今回構築する構成

環境

  • Windows 10
  • AWS CLI 2.15.19
  • VSCode 1.86.1
  • .NET SDK 6.0.419 ※本記事執筆時点で最新の .NET バージョンは 8 ですが、GameLift C# Server SDK が対応しているのが .NET6 のため、.NET6 を使用します。
  • GameLift C# Server SDK 5.1.2

AWS CLI、VSCode、.NET SDK のインストール・セットアップ手順についてはそれぞれの公式ドキュメント等を参考に進めてください。

なお AWS CLI を利用する際のアクセスキーの権限は、本記事では簡易的に検証するため Administrator 権限とします。


MagicOnion開発環境のセットアップ

MagicOnion の開発環境のセットアップおよびサンプルコードのチュートリアルは、MagicOnion の GitHub リポジトリの QuickStart がわかりやすいためこちらをベースにした手順を紹介します。

プロジェクト作成

C ドライブ直下など適当な場所に GameLift_MagicOnion という名前のフォルダを作成し、VSCode でフォルダを開きます。
VSCode のターミナル (PowerShell) を開き、以下のコマンドを実行して ASP.NET Core Empty template のプロジェクトを作成します。

dotnet new web -o MagicOnionServer

MagicOnionServer フォルダが作成されるので、フォルダの中に移動し、必要なパッケージをインストールします。

cd MagicOnionServer
dotnet add package Grpc.AspNetCore
dotnet add package MagicOnion.Server

これで環境準備は完了です。

サーバーサイドのコード作成

では実際にコードを書いていきましょう。基本的に MagicOnion のチュートリアル通りに進めます。

まず Program.cs を以下の内容で全て置き換えます。

Program.cs

using MagicOnion;
using MagicOnion.Server;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion();

var app = builder.Build();

app.MapMagicOnionService();

app.Run();

次に、IMyFirstService.cs を Program.cs と同じ階層に作成して、以下の内容とします。

IMyFirstService.cs

using System;
using MagicOnion;

namespace MyApp.Shared
{
    // Defines .NET interface as a Server/Client IDL.
    // The interface is shared between server and client.
    public interface IMyFirstService : IService<IMyFirstService>
    {
        // The return type must be `UnaryResult<T>` or `UnaryResult`.
        UnaryResult<int> SumAsync(int x, int y);
    }
}

同様に MyFirstService.cs を Program.cs と同じ階層に作成して、以下の内容とします。

MyFirstService.cs

using MagicOnion;
using MagicOnion.Server;
using MyApp.Shared;

namespace MyApp.Services;

// Implements RPC service in the server project.
// The implementation class must inherit `ServiceBase<IMyFirstService>` and `IMyFirstService`
public class MyFirstService : ServiceBase<IMyFirstService>, IMyFirstService
{
    // `UnaryResult<T>` allows the method to be treated as `async` method.
    public async UnaryResult<int> SumAsync(int x, int y)
    {
        Console.WriteLine($"Received:{x}, {y}");
        return x + y;
    }
}

最後に、appsettings.json に以下の内容の “Kestrel” のブロックを追加します。

appsettings.json

{
    "Logging": {
    ...(省略)...
    },
    "Kestrel": {
        "Endpoints": {
            "Grpc": {
                "Url": "http://localhost:5000",
                "Protocols": "Http2"
            }
        }
    },
    "AllowedHosts": "*"
}

以上です!ではサーバーを実行してみましょう。
ターミナルで dotnet run を実行するとビルド後にサーバーが実行されます。

> dotnet run
Building...
C:\GameLift_MagicOnion\MagicOnionServer\MyFirstService.cs(12,35): warning CS1998: This async method lacks 'await' operators and will run synchronously.
 Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread. [C:\GameLif
t_MagicOnion\MagicOnionServer\MagicOnionServer.csproj]
warn: Microsoft.AspNetCore.Server.Kestrel[0]
      Overriding address(es) 'https://localhost:7184, http://localhost:5030'. Binding to endpoints defined via IConfiguration and/or UseKestrel() instead.
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\GameLift_MagicOnion\MagicOnionServer\   

クライアントサイドのプロジェクト・コード作成

次はクライアントサイドです。この記事では簡易な方法で検証するため、シンプルなコンソールプログラムのクライアントで動作確認します。こちらも MagicOnion のチュートリアル通りに ConsoleClient を作る流れになります。

まず VSCode で GameLift_MagicOnion フォルダを開き、ターミナルを開いてください。(サーバーサイドの作業の流れで MagicOnionServer フォルダにいるまま作業しないよう注意してください !)

以下のコマンドでコンソールのプロジェクトフォルダを作成します。

dotnet new console -o MagicOnionClient 

作成後、できたフォルダに入って必要なパッケージをインストールします。

cd MagicOnionClient
dotnet add package MagicOnion.Client

ここからコーディングになりますが、MagicOnion のインターフェースのコード共有については、本記事では簡易な方法とするためファイルコピーで済ませます。
IMyFirstService.cs を MagicOnionServer フォルダから MagicOnionClient フォルダへコピーしてください。

次に、Program.cs を以下の内容で全て置き換えます。

Program.cs

using Grpc.Net.Client;
using MagicOnion.Client;
using MyApp.Shared;

// Connect to the server using gRPC channel.
var channel = GrpcChannel.ForAddress("http://localhost:5000");

// NOTE: If your project targets non-.NET Standard 2.1, use `Grpc.Core.Channel` class instead.
// var channel = new Channel("localhost", 5001, new SslCredentials());

// Create a proxy to call the server transparently.
var client = MagicOnionClient.Create<IMyFirstService>(channel);

// Call the server-side method using the proxy.
var result = await client.SumAsync(123, 456);
Console.WriteLine($"Result: {result}");

一旦は HTTP (TLS 暗号化なし) での接続でテストします。※本番環境では要件に応じて HTTPS 接続構成を検討してください!

これでクライアントサイドのコーディングも完了です。

ローカルでMagicOnionの接続テスト

では、サーバーが起動していることを再確認し、クライアントを起動して接続してみましょう。

MagicOnionClient フォルダにいる状態で以下のコマンドを実行します。

dotnet run 

うまく接続出来れば以下の出力が確認できるはずです。

サーバー側:

Received:123, 456

クライアント側:

Result: 579

これで MagicOnion のサーバー・クライアントの最小限のサンプルが動作しました !


MagicOnion サーバーへの GameLift Server SDK の組み込み

ここからが本記事の核心部分です。上記で実装した MagicOnion サーバーへ、GameLift Server SDK を組み込みましょう!

GameLift Server SDK のダウンロード

GameLift Server SDK は複数の言語向けに提供されていますが、MagicOnion への組み込みには C# 版のバージョン 5.x を使用します。こちらのリンク からダウンロードしてください。この記事で利用したのはバージョン 5.1.2 です。

クリックすると拡大します

GameLift Server SDK のビルド

SDK はソースコードで提供されているため、組み込みにあたりビルドが必要です。基本的には同梱の Readme の内容に従ってビルドしてください。
ここでは手順の簡素化のため、.NET CLI を使ってビルドする手順を紹介します。

ダウンロードした SDK を解凍したフォルダをターミナルで開き、以下のコマンドを実行します。

dotnet build -f net6.0 -c Debug .\GameLiftServerSDK.sln

※Debug の部分を Release にすることで Release ビルドも可能です。

これで src/GameLiftServerSDK/bin/x64/Debug(またはRelease)/net6.0 GameLiftServerSDK.dll が生成されます。

GameLiftServerSDK.dll の組み込み

サーバーサイドのプロジェクトフォルダ MagicOnionServer の中に lib フォルダを作成し、上記手順で作成された GameLiftServerSDK.dll をフォルダの中へコピーします。

配置した dll ファイルをプロジェクトでロードするために、 MagicOnionServer.csproj <Project> タグ内に以下の <Reference> 部分を追記します。

<Project Sdk="Microsoft.NET.Sdk.Web">
    ...(省略)...
  <ItemGroup>
    <Reference Include="GameLiftServerSDK">
      <HintPath>lib/GameLiftServerSDK.dll</HintPath>
    </Reference>
  </ItemGroup>
</Project>

次に、 GameLiftServerSDK.dll が依存しているパッケージのインストールを行います。

VSCode のターミナルにて、MagicOnionServer フォルダの中にいる状態で以下のコマンドを実行してください。

dotnet add package log4net
dotnet add package websocketsharp.core
dotnet add package Polly
dotnet add package NewtonSoft.Json
dotnet add package SonarAnalyzer.CSharp 

MagicOnionサーバーコードの変更

MagicOnionServer フォルダ直下に GameLiftServerSDK.cs を作成し以下のコードをコピーします。
このコードでは、GameLift Server SDK の Readmeに載っているコードから以下のような点を変更しています。

  • 各種パラメータを環境変数から取得できるようにしている
  • SDK に通知するポート番号を引数で指定できるようにしている
  • 各種メソッド・コールバックの実行が確認できるようデバッグ用の Console.WriteLine を追加している

GameLiftServerSDK.cs

using Aws.GameLift.Server;
using Aws.GameLift.Server.Model;


public class GameLiftServer
{
    private bool initialized = false;

    public ServerParameters initParameters(){
        //WebSocketUrl from RegisterHost call
        var webSocketUrl = Environment.GetEnvironmentVariable("websocketurl") ?? "wss://ap-northeast-1.api.amazongamelift.com";

        //Unique identifier for this process
        var processId = Guid.NewGuid().ToString();

        //Unique identifier for your host that this process belongs to
        var hostId = Environment.GetEnvironmentVariable("hostid") ?? "";

        //Unique identifier for your fleet that this host belongs to
        var fleetId = Environment.GetEnvironmentVariable("fleetid") ?? "";

        //Authentication token for this host process.
        var authToken = Environment.GetEnvironmentVariable("authtoken") ?? "";

        return new ServerParameters(
            webSocketUrl,
            processId,
            hostId,
            fleetId,
            authToken);
    }

    //This is an example of a simple integration with Amazon GameLift server SDK that will make game server processes go active on Amazon GameLift!
    public void start(int port, ServerParameters serverParameters)
    {
        //Identify port number (hard coded here for simplicity) the game server is listening on for player connections
        var listeningPort = port;

        //InitSDK will establish a local connection with Amazon GameLift's agent to enable further communication.
        var initSDKOutcome = GameLiftServerAPI.InitSDK(serverParameters);
        if (initSDKOutcome.Success)
        {
            ProcessParameters processParameters = new ProcessParameters(
                (GameSession gameSession) =>
                {
                    //When a game session is created, Amazon GameLift sends an activation request to the game server and passes along the game session object containing game properties and other settings.
                    //Here is where a game server should take action based on the game session object.
                    //Once the game server is ready to receive incoming player connections, it should invoke GameLiftServerAPI.ActivateGameSession()
                    Console.WriteLine("ActivateGameSession");
                    GameLiftServerAPI.ActivateGameSession();
                },
                (UpdateGameSession updateGameSession) =>
                {
                    //When a game session is updated (e.g. by FlexMatch backfill), Amazon GameLift sends a request to the game
                    //server containing the updated game session object.  The game server can then examine the provided
                    //matchmakerData and handle new incoming players appropriately.
                    //updateReason is the reason this update is being supplied.
                    Console.WriteLine("updateGameSession");

                },
                () =>
                {
                    //OnProcessTerminate callback. Amazon GameLift will invoke this callback before shutting down an instance hosting this game server.
                    //It gives this game server a chance to save its state, communicate with services, etc., before being shut down.
                    //In this case, we simply tell Amazon GameLift we are indeed going to shutdown.
                    GameLiftServerAPI.ProcessEnding();
                    Console.WriteLine("ProcessEnding");
                },
                () =>
                {
                    //This is the HealthCheck callback.
                    //GameLift will invoke this callback every 60 seconds or so.
                    //Here, a game server might want to check the health of dependencies and such.
                    //Simply return true if healthy, false otherwise.
                    //The game server has 60 seconds to respond with its health status. Amazon GameLift will default to 'false' if the game server doesn't respond in time.
                    //In this case, we're always healthy!
                    Console.WriteLine("healthcheck");
                    return true;
                },
                listeningPort, //This game server tells Amazon GameLift that it will listen on port 7777 for incoming player connections.
                new LogParameters(new List<string>()
                {
                    //Here, the game server tells Amazon GameLift what set of files to upload when the game session ends.
                    //Amazon GameLift will upload everything specified here for the developers to fetch later.
                    "/local/game/logs/myserver.log"
                }));

            //Calling ProcessReady tells Amazon GameLift this game server is ready to receive incoming game sessions!
            var processReadyOutcome = GameLiftServerAPI.ProcessReady(processParameters);
            if (processReadyOutcome.Success)
            {
                Console.WriteLine("ProcessReady success.");
                this.initialized = true;
            }
            else
            {
                Console.WriteLine("ProcessReady failure : " + processReadyOutcome.Error.ToString());
            }
        }
        else
        {
            Console.WriteLine("InitSDK failure : " + initSDKOutcome.Error.ToString());
        }
    }
}

次に Program.cs に以下を追加します。

Program.cs

using MagicOnion;
using MagicOnion.Server;

// 追加
var gl = new GameLiftServer();
var serverParameters = gl.initParameters();
gl.start(5000, serverParameters);
// 追加ここまで

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddGrpc();
builder.Services.AddMagicOnion();

var app = builder.Build();

app.MapMagicOnionService();

app.Run();

追加の部分では、GameLift Server SDK の InitSDK と ProcessReady を実行する start メソッドを実行しています。サーバープログラム開始時点で実行することで、このサーバープロセスが GameLift の管理下に置かれます。ここで指定したポート番号は、ゲームセッション作成時に接続先ポート番号としてクライアントに通知される番号となるので、MagicOnion クライアントがサーバーに接続する際に使うポート番号を指定します。

これで dotnet run を実行すればサーバーが動くはずです!さっそくやってみたいところですが…、GameLift Server SDK の動作確認をするには GameLift サービス側との通信ができるように準備する必要があります。※InitSDK や ProcessReady といったメソッドが通信する相手のセットアップが必要、というイメージです。

この動作確認のためには GameLift Anywhere という機能が利用できます。ここから Anywhere のセットアップをしていきましょう。


GameLift Anywhere を使った動作確認

GameLift Anywhere は 2022 年の re:Invent で発表された、GameLift Managed Hosting の機能の一つです。任意のマシンを GameLift の管理対象とすることができるため、ローカルの開発PCを GameLift に登録することができ、デバッグ作業に役立ちます。
GameLift Anywhere については過去の記事で詳しく紹介されておりますので、是非参考にしてください。
Amazon GameLift Anywhere でサクッとマルチプレイゲームを開発しよう

本記事では、AWS リージョンは東京リージョン (ap-northeast-1) を利用します。

Anywhere Fleet 作成

AWS マネジメントコンソールから GameLift を選択し、左側メニューから 「Fleets」を選択します。

クリックすると拡大します

Fleet の画面で「Create fleet」を押します。

クリックすると拡大します

Anywhere タイプを選んで「Next」を押します。

クリックすると拡大します

Fleet 名は TestFleet とし、他の項目は変えずに「Next」を押します。

クリックすると拡大します

Select custom locations 画面ではまだ location がないため、右上の「Create location」を押します。

クリックすると拡大します

Location name は custom-laptoppc とします。custom- までは固定で指定されるので、テキストボックスに laptoppc と入力して「Create」を押します。

クリックすると拡大します

custom-laptoppc という location がリストされているのが確認できたらチェックボックスを選択し、「Next」を押します。

クリックすると拡大します

Tag は指定せず「Next」を押し、最後のレビュー画面で内容を確認したら「Submit」を押します。

これで Anywhere fleet の作成完了です!

クリックすると拡大します

ここまでの手順で作成した Anywhere fleet 関連の情報をプログラムから利用するために、MagicOnion のサーバーを起動するターミナルにて環境変数の設定をします。

FLEETID は Anywhere fleet 作成完了後の画面に表示されています。

$env:FLEETID="fleet-**************************"
$env:LOCATION="custom-laptoppc"
$env:HOSTID="localpc"

開発用 PC をコンピュートリソースとして登録 (register-compute)

次に、皆さんの開発用 PC を Anywhere fleet で使うコンピュートリソースとして登録します。

aws gamelift register-compute --fleet-id $env:FLEETID --compute-name $env:HOSTID --location $env:LOCATION --ip-address 127.0.0.1

以下のコマンドを実行して、localpc という ComputeName のリソースが表示されれば正常に登録できています。

aws gamelift list-compute --fleet-id $env:FLEETID
{
    "ComputeList": [
        {
            "FleetId": "fleet-********************",     
            "FleetArn": "arn:aws:gamelift:ap-northeast-1:*************:fleet/fleet-*************",
            "ComputeName": "localpc",
            "ComputeArn": "arn:aws:gamelift:ap-northeast-1:*************:compute/localpc",
            "IpAddress": "127.0.0.1",
            "ComputeStatus": "Active",
            "Location": "custom-laptoppc",
            "CreationTime": "2024-02-14T07:36:37.972000+00:00",
            "GameLiftServiceSdkEndpoint": "wss://ap-northeast-1.api.amazongamelift.com"
        }
    ]
}

これで、お使いの開発用 PC を使ってデバッグ作業をする準備が整いました。

なお、Anywhere fleetの作成~開発用 PC の登録の作業は一度だけやっておけば、日々のデバッグ作業のたびに毎回実施する必要はありません。

GameLift Anywhere 機能を使ってデバッグ作業開始

さあ、ここからデバッグ作業開始です。最初に、サーバープログラムが GameLift に通信するためのトークンをゲットします。以下のコマンドを実行してください。

※以前発行したトークンが期限切れになってしまってこの手順に戻ってきた方は、環境変数 ( $env:FLEETID , $env:LOCATION , $env:HOSTID) が正しくセットされていることを再度確認ください !

aws gamelift get-compute-auth-token --fleet-id $env:FLEETID --compute-name $env:HOSTID

出力された値の中に AuthToken という項目があると思います。その値を環境変数にセットします。

{
    "FleetId": "fleet-*************",
    "FleetArn": "arn:aws:gamelift:ap-northeast-1:*************:fleet/fleet-*************",
    "ComputeName": "localpc",
    "ComputeArn": "arn:aws:gamelift:ap-northeast-1:*************:compute/localpc",
    "AuthToken": "aaaaaaaa-1111-2222-3333-012345678900",
    "ExpirationTimestamp": "2024-02-14T10:38:51+00:00"
}
$env:AUTHTOKEN="< AuthToken に記載の文字列>"

これで準備完了です。サーバープログラム中では環境変数にセットした AuthToken の値を使って GameLift サービスへ接続できるようになります。

ではサーバーを起動しましょう ! MagicOnionServer フォルダ内で以下のコマンドを実行します。

dotnet run

※もし前の手順ですでにサーバーが起動している場合は、一度終了(Ctrl-C)してから再度起動してください。

うまく起動していれば、出力に以下の文字列が確認できるはずです。正常に InitSDK および ProcessReady が成功し、ヘルスチェックのコールバックも動作していることがわかります。

ProcessReady success.
healthcheck

GameLift API の動作確認

サーバープログラムが GameLift API 呼び出しに応じて正常にコールバック関数を実行することを確認しましょう。
まずはお使いの AWS アカウントを AWS CLI で操作できるようセットアップが済んでいることを確認ください。

確認できたら、以下のコマンドでゲームセッションを作成してみます。

aws gamelift create-game-session --fleet-id $env:FLEETID --maximum-player-session-count 2 --location $env:LOCATION

※別のターミナルを開いた方、環境変数 ($env:FLEETID , $env:LOCATION , $env:HOSTID) が正しくセットされていることを再度確認ください !

ゲームセッションが正常に作成されると、以下のコマンドで情報が返ってきます。

aws gamelift describe-game-sessions --fleet-id $env:FLEETID

このときサーバーの出力には以下の文字列が確認できるはずです。

ActivateGameSession

これはゲームセッション作成を契機にサーバーの OnStartGameSession コールバック関数が実行されたためです。
これで GameLift Server SDK が正常に動作していることが確認できました!

※ヘルスチェックは GameLift にて定期的に実行されるため、healthcheck の出力も定期的に発生します。

後片付け

不要なコスト発生を防ぐため、リソースの後片付けをしっかりやりましょう。

  • サーバープログラムの停止
  • GameLift Anywhere の Fleet の削除 (Fleet 画面の Delete fleet ボタンから可能)

クリックすると拡大します


まとめ

本記事では、MagicOnion のシンプルなサーバープログラムに GameLift C# Server SDK を組み込み、GameLift Anywhere の機能を使ってローカルで基本的な動作確認を行いました。
GameLift Server SDK が提供する機能自体はシンプルなため、本記事での組み込み方をベースとすれば、あとは MagicOnion や ASP.NET Core のお作法に則った形で思いのままに拡張していけると思います。

MagicOnion x GameLift の構成を試すはじめの一歩として本記事がお役に立てばうれしいです!


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

西坂 信哉
アマゾン ウェブ サービス ジャパン合同会社
ゲームソリューションアーキテクト

SIer のインフラエンジニアを経て 2019 年にアマゾンウェブサービスジャパン合同会社に入社。現在は主にゲーム業界のお客様の技術支援を担当しています。
二児の父として育児奮闘中。ときどきアフタヌーンティーに出かけます。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する