Amazon GameLift In C# 06: 簡単なチャットソフトクライアントを作ります

Amazon GameLift Walkthrough in C# (with Unity) Tutorial 06

概要

前回の記事で簡単なチャットソフトのサーバー部分を作りました。チャットソフトはサーバーだけでは成立できないので、今回の記事はチャットソフトのクライアントを作ります。

既に作成したプロジェクトはGitHubに上がっています:
AGLW-CSharp-BasicChatClientSample

 

マシン環境

CPU : Intel i7-6920HQ
GPU : AMD Radeon Pro 460
Memory : 16GB
System : Windows 10 (with .NET 5 installed)
IDE : Visual Studio 2019 Community
Editor : Visual Studio Code
Terminal : Windows Terminal (or Command Prompt)

 

プロジェクト作成

Sampleを作成した際のプロジェクト作成部分読んでない方はそちに参考してください。

こちの場合は既にAGLW-CSharp-BasicChatClientSampleどいうフォルダを用意して、それに入ってからコマンドツールでdotnet new consoleを実行してプロジェクトを作りました。

(スキップOK)自分で必要な.dllファイルのダウンロードしたい場合は Obtaining assemblies for the AWS SDK for .NET にあるaws-sdk-netstandard2.0.zipをダウンロードして、下記の.dllファイルを取り出します。 - AWSSDK.SecurityToken.dll
- AWSSDK.GameLift.dll
- AWSSDK.Core.dll
- AWSSDK.CognitoSync.dll
- AWSSDK.CognitoIdentity.dll

GameLift Client用のライブラリを探すのは大変なので、事前に必要なライブラリを揃ってNuGetにアップロードしました: Amazon GameLift Client Lib Collection

コマンドツールでプロジェクトパスに入ってる状態で
dotnet add package devio.amazon.gamelift.client.lib.collection --version 0.0.1
というコマンドを実行すれば、ライブラリの追加は完了となります。

 

Chat Clientの作成

Amazon GameLiftにの接続はAmazon Cognitoというユーザー権限検証が必要なので、今回はまずGameLift Localでクライアントを作ります。Amazon Cognitoの獲得手順次回のブログで紹介します。

ソースコード

ChatClient.csというファイルをプロジェクト内作成します。
ChatClientというクラスのソースコードを貼って置きます。

using System;

using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;

using Amazon;
using Amazon.GameLift;
using Amazon.GameLift.Model;
using Amazon.CognitoIdentity;


namespace AGLW_CSharp_BasicChatClientSample
{
    class ChatClient
    {
        // A UTF-8 encoder to process byte[] <-> string conversion
        public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;

        // .NET Socket TCP client
        private TcpClient client = null;

        // NetworkStream(derives from Stream class) of a TCP client
        private NetworkStream stream = null;
        
        // A unique player id will be generated later
        private string playerId = string.Empty;

        // All the GameLift related things will be processed by this AmazonGameLiftClient
        private AmazonGameLiftClient gameLiftClient = null;

        // Sessions contain general information
        private GameSession gameSession = null;
        private PlayerSession playerSession = null;

        // A instance's status flag of this class 
        public bool IsAlive { get; private set; } = false;


        public ChatClient()
        {
            IsAlive = true;

            playerId = Guid.NewGuid().ToString();

            Console.WriteLine($"Client : playerId {playerId}");

            // Create GameLift Local client
            gameLiftClient = new AmazonGameLiftClient("fakeAccessKeyId", "fakeSecretAccessKey", new AmazonGameLiftConfig() { ServiceURL = "http://localhost:9080" });
        }

        public void Start()
        {
            // Create GameSession(async) -> Create PlayerSession(async) -> Connect ()
            CreateSessionsAndConnect();
        }

        async private Task CreateSessionsAndConnect()
        {
            await CreateGameSessionAsync();
            await CreatePlayerSessionAsync();

            // Connect to the IP provided by PlayerSession
            Connect();
        }

        async private Task CreateGameSessionAsync()
        {
            Console.WriteLine($"Client : CreateGameSessionAsync() start");

            // GameSession gameSession = await CreateGameSessionAsync();
            var request = new CreateGameSessionRequest();
            request.FleetId = "fleet-fakeId";
            request.CreatorId = playerId;
            request.MaximumPlayerSessionCount = 1;

            Console.WriteLine($"Client : Sending request and await");
            var response = await gameLiftClient.CreateGameSessionAsync(request);
            Console.WriteLine($"Client : request sent");

            if (response.GameSession != null)
            {
                Console.WriteLine($"Client : GameSession Created!");
                Console.WriteLine($"Client : GameSession ID {response.GameSession.GameSessionId}!");
                gameSession = response.GameSession;
            }
            else
            {
                Console.Error.WriteLine($"Client : Failed creating GameSession!");
                IsAlive = false;
            }
        }

        async private Task CreatePlayerSessionAsync()
        {
            Console.WriteLine($"Client : CreatePlayerSessionAsync() start");
            if (gameSession == null) return;

            var request = new CreatePlayerSessionRequest();
            request.GameSessionId = gameSession.GameSessionId;
            request.PlayerId = playerId;

            Console.WriteLine($"Client : Sleep for a while");
            Thread.Sleep(10000);

            Console.WriteLine($"Client : Sending request and await");
            var response = await gameLiftClient.CreatePlayerSessionAsync(request);
            Console.WriteLine($"Client : request sent");

            if (response.PlayerSession != null)
            {
                Console.WriteLine($"Client : PlayerSession Created!");
                Console.WriteLine($"Client : PlayerSession ID {response.PlayerSession.PlayerSessionId}!");
                playerSession = response.PlayerSession;
            }
            else
            {
                Console.Error.WriteLine($"Client : Failed creating PlayerSession!");
                IsAlive = false;
            }
        }


        // Connect to the IP which PlayerSession provides
        // When client connects : 
        // 1) Receive the msg sent by server
        // 2) Send another msg back
        // 3) Close the connection
        private void Connect()
        {
            Console.WriteLine($"Client : Connect() start");
            if (playerSession == null) return;

            Console.WriteLine($"Client : Try to connect");
            client = new TcpClient(playerSession.IpAddress, playerSession.Port);

            if (client.Connected)
            {
                Console.WriteLine($"Client : Connected");

                stream = client.GetStream();

                // A binary buffer for message
                byte[] msg = new byte[256];

                // Read the message (Stream.Read() blocks)
                Console.WriteLine($"Client : Wait to read from stream");
                while (stream.Read(msg) > 0)
                {
                    // Decode the binary msg to string
                    string str = Encoder.GetString(msg);
                    Console.WriteLine($"Received : {str}");

                    // Encode the string to binary message and send it
                    msg = Encoder.GetBytes("Hello Server, this is Client.");
                    stream.Write(msg);

                    Console.WriteLine($"Client : Message sent");

                    break;
                }

            }
            
            // After a successfule connection, close it
            client.Close();

            IsAlive = false;
        }
    }
}

using宣言

using System;

using System.Threading;
using System.Threading.Tasks;
using System.Net.Sockets;

using Amazon;
using Amazon.GameLift;
using Amazon.GameLift.Model;
using Amazon.CognitoIdentity;

main threadはクライアント全体プログラムを実行するため、通信の部分は新しいThreadに動かすので(今回は一つメッセージだけを送信するので、しなくても大丈夫)、一部非同期処理を含めて、System.ThreadingSystem.Threading.Tasksを追加しました。

.NET Socket 通信クラスを使うためSystem.Net.Socketsを追加しました。

Amazon.GameLiftAmazon.GameLift.ModelにはGameLiftのクラスが含まれます。

AmazonAmazon.CognitoIdentityはさっき言ったユーザー権限検証の部分に関わるライブラリです。今回使わないですが、次回使うので、一応宣言しておきます。

メンバー定義

// A UTF-8 encoder to process byte[] <-> string conversion
public readonly System.Text.Encoding Encoder = System.Text.Encoding.UTF8;

// .NET Socket TCP client
private TcpClient client = null;

// NetworkStream(derives from Stream class) of a TCP client
private NetworkStream stream = null;

// A unique player id will be generated later
private string playerId = string.Empty;

UTF8のデータを送信したいですが、毎回System.Text.Encoding.UTF8のように関数を使うのは長くて読みにくいので、先にEncoderに定義します。

そして .NET Socket のTCP通信用のクライアントクラスTcpClientを定義し、それと同時に通信データ受送信作業を担当するNetworkStreamを定義します。

playerIdという文字列は唯一のPlayer IDとし、後で生成します。

 

// All the GameLift related things will be processed by this AmazonGameLiftClient
private AmazonGameLiftClient gameLiftClient = null;

// Sessions contain general information
private GameSession gameSession = null;
private PlayerSession playerSession = null;

AmazonGameLiftClientはほぼすべてGameLiftと関わる作業の担当です。
GameSessionはサーバー情報周りを管理するクラスです。
PlayerSessionはプレイヤーの接続情報を管理しています。

構造体

public ChatClient()
{
    IsAlive = true;

    playerId = Guid.NewGuid().ToString();

    Console.WriteLine($"Client : playerId {playerId}");

    // Create GameLift Local client
    gameLiftClient = new AmazonGameLiftClient("fakeAccessKeyId", "fakeSecretAccessKey", new AmazonGameLiftConfig() { ServiceURL = "http://localhost:9080" });
}

Guid.NewGuid()という関数でPlayer IDを生成します。Documentationに書いてる通り、この関数が保証できるのは、Guid.Emptyにならない番号だけです。「唯一」という保証はされてないですが、長いHex番号が生成され、一つゲームセッションに同じ番号が存在する可能性ほぼないので、効果的には「唯一」と思っても大丈夫と思います。

new AmazonGameLiftClientになんでもよく仮情報を渡して、そしてnew AmazonGameLiftConfig()の部分はlocalhostで初期化します。

GameSessionの獲得

async private Task CreateGameSessionAsync()
{
    Console.WriteLine($"Client : CreateGameSessionAsync() start");

    // GameSession gameSession = await CreateGameSessionAsync();
    var request = new CreateGameSessionRequest();
    request.FleetId = "fleet-fakeId";
    request.CreatorId = playerId;
    request.MaximumPlayerSessionCount = 1;

    Console.WriteLine($"Client : Sending request and await");
    var response = await gameLiftClient.CreateGameSessionAsync(request);
    Console.WriteLine($"Client : request sent");

    if (response.GameSession != null)
    {
        Console.WriteLine($"Client : GameSession Created!");
        Console.WriteLine($"Client : GameSession ID {response.GameSession.GameSessionId}!");
        gameSession = response.GameSession;
    }
    else
    {
        Console.Error.WriteLine($"Client : Failed creating GameSession!");
        IsAlive = false;
    }
}

GameSessionの獲得には非同期処理の関数で待つので、CreateGameSessionAsync()async Taskにしました。

 

var request = new CreateGameSessionRequest();
request.FleetId = "fleet-fakeId";
request.CreatorId = playerId;
request.MaximumPlayerSessionCount = 1;

テストのため、初期状態はGameSessionがないので、Fleetに確認する必要なく直接新しいGameSessionを作る請求を出します。GameLift Fleetに提出するリクエストにはFleetId(GameLift Localのため、なんの文字列でもOK)、CreatorId(Player ID)、MaximumPlayerSessionCount(GameSessionに接続する最大Player数)が必要です。今回は一人クライアントなので、1にしました。

 

var response = await gameLiftClient.CreateGameSessionAsync(request);

/// ...省略

gameSession = response.GameSession;

非同期関数CreateGameSessionAsync()にリクエストを渡して、Fleetに承認されるまで待ちます。
問題なくresponseを返してくれたら、メンバーに保存します。

PlayerSessionの獲得

async private Task CreatePlayerSessionAsync()
{
    Console.WriteLine($"Client : CreatePlayerSessionAsync() start");
    if (gameSession == null) return;

    var request = new CreatePlayerSessionRequest();
    request.GameSessionId = gameSession.GameSessionId;
    request.PlayerId = playerId;

    Console.WriteLine($"Client : Sleep for a while");
    Thread.Sleep(10000);

    Console.WriteLine($"Client : Sending request and await");
    var response = await gameLiftClient.CreatePlayerSessionAsync(request);
    Console.WriteLine($"Client : request sent");

    if (response.PlayerSession != null)
    {
        Console.WriteLine($"Client : PlayerSession Created!");
        Console.WriteLine($"Client : PlayerSession ID {response.PlayerSession.PlayerSessionId}!");
        playerSession = response.PlayerSession;
    }
    else
    {
        Console.Error.WriteLine($"Client : Failed creating PlayerSession!");
        IsAlive = false;
    }
}

これも非同期関数です。

 

var request = new CreatePlayerSessionRequest();
request.GameSessionId = gameSession.GameSessionId;
request.PlayerId = playerId;

PlayerSessionの獲得にもリクエストが必要です。
さっき貰ったGameSessionGameSessionIdと生成されたplayerIdをリクエストに渡します。

 

var response = await gameLiftClient.CreatePlayerSessionAsync(request);

/// ...省略

playerSession = response.PlayerSession;

CreatePlayerSessionAsync()という非同期関数にリクエストを渡して、Fleetからの返事を待ちます。 無事PlayerSessionをもらったら、メンバーに保存します。

サーバーに接続

// Connect to the IP which PlayerSession provides
// When client connects : 
// 1) Receive the msg sent by server
// 2) Send another msg back
// 3) Close the connection
private void Connect()
{
    Console.WriteLine($"Client : Connect() start");
    if (playerSession == null) return;

    Console.WriteLine($"Client : Try to connect");
    client = new TcpClient(playerSession.IpAddress, playerSession.Port);

    if (client.Connected)
    {
        Console.WriteLine($"Client : Connected");

        stream = client.GetStream();

        // A binary buffer for message
        byte[] msg = new byte[256];

        // Read the message (Stream.Read() blocks)
        Console.WriteLine($"Client : Wait to read from stream");
        while (stream.Read(msg) > 0)
        {
            // Decode the binary msg to string
            string str = Encoder.GetString(msg);
            Console.WriteLine($"Received : {str}");

            // Encode the string to binary message and send it
            msg = Encoder.GetBytes("Hello Server, this is Client.");
            stream.Write(msg);

            Console.WriteLine($"Client : Message sent");

            break;
        }

    }
    
    // After a successfule connection, close it
    client.Close();

    IsAlive = false;
}

GameSessionPlayerSession両方とも問題なくもらったら、Connect()関数を実行します。

 

client = new TcpClient(playerSession.IpAddress, playerSession.Port);

PlayerSessionにあるIpAddressPort情報でTcpClientを作ります。

 

stream = client.GetStream();

// A binary buffer for message
byte[] msg = new byte[256];

client.Connectedtrueの場合はif処理の中に入ります。
まずclient.GetStream()で受送信担当のStreamをメンバーに保存し、byte[]タイプのメッセージバッファを用意ます。

 

while (stream.Read(msg) > 0)
{
    // Decode the binary msg to string
    string str = Encoder.GetString(msg);
    Console.WriteLine($"Received : {str}");

    // Encode the string to binary message and send it
    msg = Encoder.GetBytes("Hello Server, this is Client.");
    stream.Write(msg);

    Console.WriteLine($"Client : Message sent");

    break;
}

// ...省略

client.Close();

stream.Read()関数はメッセージが来ないとそのまま止まります。 メッセージがきたら、whileの中に入って、Encoder.GetString(msg)でメッセージをbyte[]からstringに解析します。
そしてEncoder.GetBytes()"Hello Server, this is Client."という文字列をbyte[]に転換し、stream.Write()でサーバーに送信します。
送信が終わったらbreak;で出て、client.Close()で接続を切断します。

Program.csでChatClientを起動する

class Program
{
    static private ChatClient client = new ChatClient();
    static void Main(string[] args)
    {
        client.Start();

        while(client.IsAlive)
        {

        }

        Console.WriteLine("Program ends.");
    }
}

その前に作ったGameServerChatServerのように、普通の構造体で生成し、Start()関数で起動します。今は何もやってないですがwhile(client.IsAlive)でクライアントの状態を判断し、将来いろいろ処理できると思います。

テスト

前回の記事のようにGameLift LocalChatServerを起動します。

コマンドツールで今回のプロジェクトに入ってdotnet runで起動します。

GameLift LocalとChatClientのPortを必ず一致してください。例のportは9080です

GameLift Local

Chat Server

Chat Client

上記のようなGameLift LocalChat ServerChat Clientのメッセージが出ればテスト完了となります。

おしまい

今回はGameLift Localだけでチャットソフトのサーバーとクライアントのテストができました。クライアントがネット上本物のAmazon GameLiftに接続するには、Amazon Cognitoというユーザー権限検証が必要なので、それを次回のブログでやります。