はじめに
前回からのつづき
前回はこちら
GameLift RealTimeServerで遊んでみよう for Unity(AWS設定編)
対象者
- AWSのアカウントを持っている方
- GameLiftでとりあえず遊んでみたいと考えているUnityエンジニアの方
- AWSの無料利用枠がまだある方、もしくは使用金額を払ってでもやりたい方(この辺りは自己責任で)
- 今回作成するものをきちんとクリーンアップできる方
試した環境
- MacBook Pro (13-inch, 2017)
- OSバージョン10.14.6
- Unity2019.2.15f
#このページで行うこと
Unity設定をし、RealTimeServerでのデータの送受信まで。
「誰かが送ったメッセージをトリガーにし、UDPでつながっているすべてのユーザーにデータを送る」
ことをやってみる。
####Unityでの設定
- パッケージのインストール
####Unityでの実装
- AmazonGameLiftClientクラスの初期化をする
- ルームを作成
- ルームを検索
- ルームへの参加
- データ送受信を確認
##パッケージのインストール
###AWS.NET SDK
AWSSDK.Core(3.3.104を使用)
AWSSDK.GameLift(3.3.104.18を使用)
ともにこちらからダウンロード。
(自分はNuGetの方を選択)
.Net4.5対応のものを使ってください。
###GameLift Realtime Client SDK
こちらのRealtime Client SDKをダウンロードしVisualStudioなどでビルド
上記で入手したライブラリをUnityのPluginsフォルダ以下に配置
UnityのApi Compatibility Levelを.Net 4.xにするのを忘れないように
Build Setting -> Player Settings -> Player -> Configuration -> Other Settings -> API Compatibility Level
#今回説明するスクリプト
主要となるスクリプトを載せております。
UIに関しては各々で実装していただければ。
###クライアント側
こちらのサンプルを元に作成
Lobby.cs
using System;
using System.Linq;
using System.Diagnostics;
using System.Collections.Generic;
using System.Net.NetworkInformation;
using UnityEngine;
using Amazon;
using Amazon.GameLift;
using Amazon.GameLift.Model;
using Aws.GameLift.Realtime.Types;
public class Lobby : MonoBehaviour
{
class GameLiftConfig
{
public RegionEndpoint RegionEndPoint { get; set; }
public string AccessKeyId { get; set; }
public string SecretAccessKey { get; set; }
public string GameLiftAliasId { get; set; }
}
GameLiftConfig config;
AmazonGameLiftClient gameLiftClient;
RealTimeClient realTimeClient;
[SerializeField]
LobbyUI ui;
// Start is called before the first frame update
void Start()
{
UnityEngine.Debug.Log("start");
initialize();
}
void initialize()
{
config = new GameLiftConfig
{
RegionEndPoint = RegionEndpoint.APNortheast1, //東京の場合
AccessKeyId = "", // ダウンロードしたcsvのAccess key IDの値
SecretAccessKey = "", // ダウンロードしたcsvのSecret access keyの値
GameLiftAliasId = "" // 作成したAliasのID alias- から始まるID
};
// AmazonGameLiftClientクラスの初期化
gameLiftClient = new AmazonGameLiftClient(config.AccessKeyId, config.SecretAccessKey, config.RegionEndPoint);
ui.CreateRoomButton.onClick.AddListener(() =>
{
CreateRoom();
});
ui.SearchRoomButton.onClick.AddListener(() =>
{
var sessions = SearchRooms();
ui.ClearAllPanels();
ui.CreateSessionPanels(sessions, JoinRoom);
});
ui.SendTest1Button.onClick.AddListener(() =>
{
if (realTimeClient != null) realTimeClient.SendMessage(DeliveryIntent.Reliable, "test");
});
ui.SendTest2Button.onClick.AddListener(() =>
{
if (realTimeClient != null) realTimeClient.SendEvent(RealTimeClient.OpCode.SendTest2);
});
}
// ルームの作成
void CreateRoom(string roomName = "")
{
UnityEngine.Debug.Log("CreateRoom");
if (string.IsNullOrEmpty(roomName)) roomName = Guid.NewGuid().ToString();
var request = new CreateGameSessionRequest
{
AliasId = config.GameLiftAliasId,
MaximumPlayerSessionCount = 2,
Name = roomName
};
var response = gameLiftClient.CreateGameSession(request);
ui.InfoText.text += "CreateRoom\n";
}
//ルームの検索
public List<GameSession> SearchRooms()
{
UnityEngine.Debug.Log("SearchRooms");
var response = gameLiftClient.SearchGameSessions(new SearchGameSessionsRequest
{
AliasId = config.GameLiftAliasId,
});
ui.InfoText.text += "SearchRoom\n";
return response.GameSessions;
}
// ルームへの参加
void JoinRoom(string sessionId)
{
UnityEngine.Debug.Log("JoinRoom");
var response = gameLiftClient.CreatePlayerSession(new CreatePlayerSessionRequest
{
GameSessionId = sessionId,
PlayerId = SystemInfo.deviceUniqueIdentifier,
});
var playerSession = response.PlayerSession;
ushort DefaultUdpPort = 7777;
var udpPort = SearchAvailableUdpPort(DefaultUdpPort, DefaultUdpPort + 100);
realTimeClient = new RealTimeClient(
playerSession.IpAddress,
playerSession.Port,
udpPort,
ConnectionType.RT_OVER_WS_UDP_UNSECURED,
playerSession.PlayerSessionId,
null);
ui.InfoText.text += "JoinRoom\n";
realTimeClient.OnDataReceivedCallback = OnDataReceivedCallback;
}
public void OnDataReceivedCallback(object sender, Aws.GameLift.Realtime.Event.DataReceivedEventArgs e)
{
if (ui.InfoText != null)
{
ui.InfoText.text += $"{e.OpCode}\n";
}
}
int SearchAvailableUdpPort(int from = 1024, int to = ushort.MaxValue)
{
from = Mathf.Clamp(from, 1, ushort.MaxValue);
to = Mathf.Clamp(to, 1, ushort.MaxValue);
#if UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
var set = LsofUdpPorts(from, to);
#else
var set = GetActiveUdpPorts();
#endif
for (int port = from; port <= to; port++)
if (!set.Contains(port))
return port;
return -1;
}
HashSet<int> LsofUdpPorts(int from, int to)
{
var set = new HashSet<int>();
string command = string.Join(" | ",
$"lsof -nP -iUDP:{from.ToString()}-{to.ToString()}",
"sed -E 's/->[0-9.:]+$//g'",
@"grep -Eo '\d+$'");
var process = Process.Start(new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"{command}\"",
RedirectStandardOutput = true,
UseShellExecute = false,
});
if (process != null)
{
process.WaitForExit();
var stream = process.StandardOutput;
while (!stream.EndOfStream)
if (int.TryParse(stream.ReadLine(), out int port))
set.Add(port);
}
return set;
}
HashSet<int> GetActiveUdpPorts()
{
return new HashSet<int>(IPGlobalProperties.GetIPGlobalProperties()
.GetActiveUdpListeners().Select(listener => listener.Port));
}
}
RealTimeClient.cs
using System;
using System.Text;
using Aws.GameLift.Realtime;
using Aws.GameLift.Realtime.Event;
using Aws.GameLift.Realtime.Types;
public class RealTimeClient
{
public Aws.GameLift.Realtime.Client Client { get; private set; }
public Action<object, DataReceivedEventArgs> OnDataReceivedCallback { get; set; }
// An opcode defined by client and your server script that represents a custom message type
public static class OpCode
{
public const int SendTest1 = 10;
public const int SendTest2 = 11;
public const int RecieveTest1 = 31;
public const int RecieveTest2 = 32;
}
/// Initialize a client for GameLift Realtime and connect to a player session.
/// <param name="endpoint">The DNS name that is assigned to Realtime server</param>
/// <param name="remoteTcpPort">A TCP port for the Realtime server</param>
/// <param name="listeningUdpPort">A local port for listening to UDP traffic</param>
/// <param name="connectionType">Type of connection to establish between client and the Realtime server</param>
/// <param name="playerSessionId">The player session ID that is assigned to the game client for a game session </param>
/// <param name="connectionPayload">Developer-defined data to be used during client connection, such as for player authentication</param>
public RealTimeClient(string endpoint, int remoteTcpPort, int listeningUdpPort, ConnectionType connectionType,
string playerSessionId, byte[] connectionPayload)
{
// Create a client configuration to specify a secure or unsecure connection type
// Best practice is to set up a secure connection using the connection type RT_OVER_WSS_DTLS_TLS12.
ClientConfiguration clientConfiguration = new ClientConfiguration()
{
// C# notation to set the field ConnectionType in the new instance of ClientConfiguration
ConnectionType = connectionType
};
// Create a Realtime client with the client configuration
Client = new Client(clientConfiguration);
// Initialize event handlers for the Realtime client
Client.ConnectionOpen += OnOpenEvent;
Client.ConnectionClose += OnCloseEvent;
Client.GroupMembershipUpdated += OnGroupMembershipUpdate;
Client.DataReceived += OnDataReceived;
// Create a connection token to authenticate the client with the Realtime server
// Player session IDs can be retrieved using AWS SDK for GameLift
ConnectionToken connectionToken = new ConnectionToken(playerSessionId, connectionPayload);
// Initiate a connection with the Realtime server with the given connection information
Client.Connect(endpoint, remoteTcpPort, listeningUdpPort, connectionToken);
}
public void Disconnect()
{
if (Client.Connected)
{
Client.Disconnect();
}
}
public bool IsConnected()
{
return Client.Connected;
}
/// <summary>
/// Example of sending to a custom message to the server.
///
/// Server could be replaced by known peer Id etc.
/// </summary>
/// <param name="intent">Choice of delivery intent ie Reliable, Fast etc. </param>
/// <param name="payload">Custom payload to send with message</param>
public void SendMessage(DeliveryIntent intent, string payload)
{
UnityEngine.Debug.Log("SendMessage");
Client.SendMessage(Client.NewMessage(OpCode.SendTest1)
.WithDeliveryIntent(intent)
.WithTargetPlayer(Constants.PLAYER_ID_SERVER)
.WithPayload(StringToBytes(payload)));
}
/**
* Handle connection open events
*/
public void OnOpenEvent(object sender, EventArgs e)
{
UnityEngine.Debug.Log("OnOpenEvent");
}
/**
* Handle connection close events
*/
public void OnCloseEvent(object sender, EventArgs e)
{
UnityEngine.Debug.Log("OnCloseEvent");
}
/**
* Handle Group membership update events
*/
public void OnGroupMembershipUpdate(object sender, GroupMembershipEventArgs e)
{
UnityEngine.Debug.Log("OnGroupMembershipUpdate");
}
/**
* Handle data received from the Realtime server
*/
public virtual void OnDataReceived(object sender, DataReceivedEventArgs e)
{
UnityEngine.Debug.Log("OnDataReceived");
UnityEngine.Debug.Log($"OpCode = {e.OpCode}");
switch (e.OpCode)
{
// handle message based on OpCode
default:
break;
}
if (OnDataReceivedCallback != null) OnDataReceivedCallback(sender, e);
}
/**
* Helper method to simplify task of sending/receiving payloads.
*/
public static byte[] StringToBytes(string str)
{
return Encoding.UTF8.GetBytes(str);
}
/**
* Helper method to simplify task of sending/receiving payloads.
*/
public static string BytesToString(byte[] bytes)
{
return Encoding.UTF8.GetString(bytes);
}
public void SendEvent(int opCode)
{
UnityEngine.Debug.Log("SendEvent");
if (!IsConnected()) return;
Client.SendEvent(opCode);
}
}
###サーバー側
こちらのサンプルを元に作成
server.js
'use strict';
const util = require('util');
const tickTime = 1000;
const minimumElapsedTime = 120;
const SendTest1 = 10;
const SendTest2 = 11;
const RecieveTest1 = 31;
const RecieveTest2 = 32;
// The Realtime server session object
var session;
var logger;
var activePlayers = 0; // Records the number of connected players
var startTime; // Records the time the process started
function init(rtSession) {
session = rtSession;
logger = session.getLogger();
}
// A simple tick loop example
// Checks to see if a minimum amount of time has passed before seeing if the game has ended
async function tickLoop() {
const elapsedTime = getTimeInS() - startTime;
logger.info("Tick... " + elapsedTime + " activePlayers: " + activePlayers);
// In Tick loop - see if all players have left early after a minimum period of time has passed
// Call processEnding() to terminate the process and quit
if ((activePlayers == 0) && (elapsedTime > minimumElapsedTime)) {
logger.info("All players disconnected. Ending game");
const outcome = await session.processEnding();
logger.info("Completed process ending with: " + outcome);
process.exit(0);
}
else {
setTimeout(tickLoop, tickTime);
}
}
// Calculates the current time in seconds
function getTimeInS() {
return Math.round(new Date().getTime() / 1000);
}
function onProcessStarted(args) {
logger.info(`[onProcessStarted]`);
return true;
}
function onStartGameSession(gameSession) {
// Complete any game session set-up
logger.info(`[onStartGameSession]`);
// tryDelayExit();
startTime = getTimeInS();
tickLoop();
}
// Handle process termination if the process is being terminated by GameLift
// You do not need to call ProcessEnding here
function onProcessTerminate() {
// Perform any clean up
}
// On Player Connect is called when a player has passed initial validation
// Return true if player should connect, false to reject
function onPlayerConnect(connectMsg) {
logger.info(`[onPlayerConnect]`);
return true;
}
// Called when a Player is accepted into the game
function onPlayerAccepted(player) {
logger.info(`[onPlayerAccepted]`);
activePlayers++;
}
// On Player Disconnect is called when a player has left or been forcibly terminated
// Is only called for players that actually connected to the server and not those rejected by validation
// This is called before the player is removed from the player list
function onPlayerDisconnect(peerId) {
logger.info(`[onPlayerDisconnect]`);
activePlayers--;
// tryDelayExit();
}
// Return true if the player is allowed to join the group
function onPlayerJoinGroup(groupId, peerId) {
return true;
}
// Return true if the player is allowed to leave the group
function onPlayerLeaveGroup(groupId, peerId) {
return true;
}
// Return true if the send should be allowed
function onSendToPlayer(gameMessage) {
return true;
}
// Return true if the send to group should be allowed
// Use gameMessage.getPayloadAsText() to get the message contents
function onSendToGroup(gameMessage) {
logger.info(`[onSendToGroup]`);
return true;
}
// Handle a message to the server
function onMessage(gameMessage) {
logger.info(`[onMessage]`);
switch (gameMessage.opCode) {
case SendTest1: {
// do operation 1 with gameMessage.payload for example sendToGroup
const outMessage = session.newTextGameMessage(RecieveTest1, session.getServerId(), gameMessage.payload);
session.sendGroupMessage(outMessage, -1);
break;
}
case SendTest2: {
// do operation 1 with gameMessage.payload for example sendToGroup
const outMessage = session.newTextGameMessage(RecieveTest2, session.getServerId(), gameMessage.payload);
session.sendGroupMessage(outMessage, -1);
break;
}
}
}
// Return true if the process is healthy
function onHealthCheck() {
return true;
}
exports.ssExports = {
init: init,
onProcessStarted: onProcessStarted,
onStartGameSession: onStartGameSession,
onProcessTerminate: onProcessTerminate,
onPlayerConnect: onPlayerConnect,
onPlayerAccepted: onPlayerAccepted,
onPlayerDisconnect: onPlayerDisconnect,
onPlayerJoinGroup: onPlayerJoinGroup,
onPlayerLeaveGroup: onPlayerLeaveGroup,
onSendToPlayer: onSendToPlayer,
onSendToGroup: onSendToGroup,
onMessage: onMessage,
onHealthCheck: onHealthCheck
};
リファレンスはこちら
前回作成したものに追記いただければ。
server.jsを更新する際はGameLiftのスクリプトを編集しアップロードしてください
AmazonGameLiftClientクラスの初期化をする
config = new GameLiftConfig
{
RegionEndPoint = RegionEndpoint.APNortheast1, //東京の場合
AccessKeyId = "", // ダウンロードしたcsvのAccess key IDの値
SecretAccessKey = "", // ダウンロードしたcsvのSecret access keyの値
GameLiftAliasId = "" // 作成したAliasのID alias- から始まるID
};
// AmazonGameLiftClientクラスの初期化
gameLiftClient = new AmazonGameLiftClient(config.AccessKeyId, config.SecretAccessKey, config.RegionEndPoint);
#####引っかかりやすいポイント
AmazonGameLiftClientクラスの初期化に使用する値を間違えないように
※リリースするようなものであれば、コードに直に書いたりはしないよう注意
ルームの作成
// ルームの作成
void CreateRoom(string roomName = "")
{
UnityEngine.Debug.Log("CreateRoom");
if (string.IsNullOrEmpty(roomName)) roomName = Guid.NewGuid().ToString();
var request = new CreateGameSessionRequest
{
AliasId = config.GameLiftAliasId,
MaximumPlayerSessionCount = 2,
Name = roomName
};
var response = gameLiftClient.CreateGameSession(request);
ui.InfoText.text += "CreateRoom\n";
}
CreateGameSessionRequestを作成しCreateGameSessionに渡せば簡単に作成できる。
##ルームの検索
//ルームの検索
public List<GameSession> SearchRooms()
{
UnityEngine.Debug.Log("SearchRooms");
var response = gameLiftClient.SearchGameSessions(new SearchGameSessionsRequest
{
AliasId = config.GameLiftAliasId,
});
ui.InfoText.text += "SearchRoom\n";
return response.GameSessions;
}
SearchGameSessionsRequestをAliasIdを引数とし作成し
SearchGameSessionsに渡せば簡単に検索できる。
##ルームへの参加
// ルームへの参加
void JoinRoom(string sessionId)
{
UnityEngine.Debug.Log("JoinRoom");
var response = gameLiftClient.CreatePlayerSession(new CreatePlayerSessionRequest
{
GameSessionId = sessionId,
PlayerId = SystemInfo.deviceUniqueIdentifier,
});
var playerSession = response.PlayerSession;
ushort DefaultUdpPort = 7777;
var udpPort = SearchAvailableUdpPort(DefaultUdpPort, DefaultUdpPort + 100);
realTimeClient = new RealTimeClient(
playerSession.IpAddress,
playerSession.Port,
udpPort,
ConnectionType.RT_OVER_WS_UDP_UNSECURED,
playerSession.PlayerSessionId,
null);
ui.InfoText.text += "JoinRoom\n";
realTimeClient.OnDataReceivedCallback = OnDataReceivedCallback;
}
/// Initialize a client for GameLift Realtime and connect to a player session.
/// <param name="endpoint">The DNS name that is assigned to Realtime server</param>
/// <param name="remoteTcpPort">A TCP port for the Realtime server</param>
/// <param name="listeningUdpPort">A local port for listening to UDP traffic</param>
/// <param name="connectionType">Type of connection to establish between client and the Realtime server</param>
/// <param name="playerSessionId">The player session ID that is assigned to the game client for a game session </param>
/// <param name="connectionPayload">Developer-defined data to be used during client connection, such as for player authentication</param>
public RealTimeClient(string endpoint, int remoteTcpPort, int listeningUdpPort, ConnectionType connectionType,
string playerSessionId, byte[] connectionPayload)
{
// Create a client configuration to specify a secure or unsecure connection type
// Best practice is to set up a secure connection using the connection type RT_OVER_WSS_DTLS_TLS12.
ClientConfiguration clientConfiguration = new ClientConfiguration()
{
// C# notation to set the field ConnectionType in the new instance of ClientConfiguration
ConnectionType = connectionType
};
// Create a Realtime client with the client configuration
Client = new Client(clientConfiguration);
// Initialize event handlers for the Realtime client
Client.ConnectionOpen += OnOpenEvent;
Client.ConnectionClose += OnCloseEvent;
Client.GroupMembershipUpdated += OnGroupMembershipUpdate;
Client.DataReceived += OnDataReceived;
// Create a connection token to authenticate the client with the Realtime server
// Player session IDs can be retrieved using AWS SDK for GameLift
ConnectionToken connectionToken = new ConnectionToken(playerSessionId, connectionPayload);
// Initiate a connection with the Realtime server with the given connection information
Client.Connect(endpoint, remoteTcpPort, listeningUdpPort, connectionToken);
}
Clientで新しいクライアントを初期化し
Client.Connectでゲームセッションをホストしているサーバープロセスへの接続をリクエストしルームへ参加する。
MacだとIPGlobalProperties.GetIPGlobalProperties().GetActiveUdpListeners()
でうまく有効なUDPポートとれなかったので、別実装で取得
##データ送受信の確認
送るイベントのクライアント側定義
public const int SendTest1 = 10;
public const int SendTest2 = 11;
public const int RecieveTest1 = 31;
public const int RecieveTest2 = 32
送るイベントのサーバー側定義
const SendTest1 = 10;
const SendTest2 = 11;
const RecieveTest1 = 31;
const RecieveTest2 = 32
- イベントやメッセージの送信処理
/// <summary>
/// Example of sending to a custom message to the server.
///
/// Server could be replaced by known peer Id etc.
/// </summary>
/// <param name="intent">Choice of delivery intent ie Reliable, Fast etc. </param>
/// <param name="payload">Custom payload to send with message</param>
public void SendMessage(DeliveryIntent intent, string payload)
{
UnityEngine.Debug.Log("SendMessage");
Client.SendMessage(Client.NewMessage(OpCode.SendTest1)
.WithDeliveryIntent(intent)
.WithTargetPlayer(Constants.PLAYER_ID_SERVER)
.WithPayload(StringToBytes(payload)));
}
public void SendEvent(int opCode)
{
UnityEngine.Debug.Log("SendEvent");
if (!IsConnected()) return;
Client.SendEvent(opCode);
}
SendMessageとSendEventで試してみる。
- サーバー側ハンドリング処理
// Handle a message to the server
function onMessage(gameMessage) {
logger.info(`[onMessage]`);
switch (gameMessage.opCode) {
case SendTest1: {
// do operation 1 with gameMessage.payload for example sendToGroup
const outMessage = session.newTextGameMessage(RecieveTest1, session.getServerId(), gameMessage.payload);
session.sendGroupMessage(outMessage, -1);
break;
}
case SendTest2: {
// do operation 1 with gameMessage.payload for example sendToGroup
const outMessage = session.newTextGameMessage(RecieveTest2, session.getServerId(), gameMessage.payload);
session.sendGroupMessage(outMessage, -1);
break;
}
}
}
- クライアント受信処理
/**
* Handle data received from the Realtime server
*/
public virtual void OnDataReceived(object sender, DataReceivedEventArgs e)
{
UnityEngine.Debug.Log("OnDataReceived");
UnityEngine.Debug.Log($"OpCode = {e.OpCode}");
switch (e.OpCode)
{
// handle message based on OpCode
default:
break;
}
if (OnDataReceivedCallback != null) OnDataReceivedCallback(sender, e);
}
別途UIを作成しビルド。
以下は別々のアプリで起動し
ルーム作成→ルーム検索→ルームへ参加→イベントを送る
と試したところ
見栄えよくないですが
「誰かが送ったメッセージをトリガーにし、UDPでつながっているすべてのユーザーにデータを送る」
の目標は達成したので今回はここまで
##再度の注意点
作成した
- スクリプト
- フリート
- エイリアス
に関して使わないときは削除するようにしてください。
でないとお金がかかっちゃうので。
##おわりに
走った説明になっちゃいました。
所感としては確かに簡単にリアルタイム通信ができると感じたが、
無料で気軽に試せないのがつらいところ
これから遊びの部分を作って行こうと思いましたが、金額がかかっちゃうので気が向いた時にでも
明日は @e73ryo さんのUIElementsで開発するときの問題と解決です!