Help us understand the problem. What is going on with this article?

GameLift RealTimeServerで遊んでみよう for Unity(Unity編)

はじめに

前回からのつづき

前回はこちら
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フォルダ以下に配置
スクリーンショット 2019-12-08 15.27.35.png

UnityのApi Compatibility Levelを.Net 4.xにするのを忘れないように
スクリーンショット 2019-12-08 15.50.04.png
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のスクリプトを編集しアップロードしてください

スクリーンショット 2019-12-11 18.34.15.png

AmazonGameLiftClientクラスの初期化をする

Lobby.cs
         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クラスの初期化に使用する値を間違えないように
※リリースするようなものであれば、コードに直に書いたりはしないよう注意

ルームの作成

Lobby.cs
    // ルームの作成
    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に渡せば簡単に作成できる。

ルームの検索

Lobby.cs
    //ルームの検索
    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に渡せば簡単に検索できる。

ルームへの参加

Lobby.cs
    // ルームへの参加
    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;
    }

RealTimeClient.cs
   /// 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ポートとれなかったので、別実装で取得

データ送受信の確認

送るイベントのクライアント側定義

RealTimeClient.cs
        public const int SendTest1 = 10;
        public const int SendTest2 = 11;

        public const int RecieveTest1 = 31;
        public const int RecieveTest2 = 32

送るイベントのサーバー側定義

server.js
const SendTest1 = 10;
const SendTest2 = 11;

const RecieveTest1 = 31;
const RecieveTest2 = 32

  • イベントやメッセージの送信処理
RealTimeClient.cs
    /// <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で試してみる。

  • サーバー側ハンドリング処理
server.js
// 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;
        }
    }
}
  • クライアント受信処理
RealTimeClient.cs
     /**
     *  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を作成しビルド。
以下は別々のアプリで起動し
ルーム作成→ルーム検索→ルームへ参加→イベントを送る
と試したところ
test.gif
見栄えよくないですが
「誰かが送ったメッセージをトリガーにし、UDPでつながっているすべてのユーザーにデータを送る」
の目標は達成したので今回はここまで

再度の注意点

作成した

  • スクリプト
  • フリート
  • エイリアス

に関して使わないときは削除するようにしてください。
でないとお金がかかっちゃうので。

おわりに

走った説明になっちゃいました。
所感としては確かに簡単にリアルタイム通信ができると感じたが、
無料で気軽に試せないのがつらいところ

これから遊びの部分を作って行こうと思いましたが、金額がかかっちゃうので気が向いた時にでも

明日は @e73ryo さんのUIElementsで開発するときの問題と解決です!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした