[GS2+Unity]GS2-Realtimeを使ったリアルタイム PvP ゲームの実装

[GS2+Unity]GS2-Realtimeを使ったリアルタイム PvP ゲームの実装

GS2-Realtime を使用して、リアルタイムで動作する対人戦ピンポンゲームを実装する例を紹介します。 2 人のプレイヤーがそれぞれバーを操作し、ボールを打ち合うシンプルなゲームです。

対象読者

  • Unity でのゲーム開発経験がある方
  • GS2 (Game Server Services) を使ったオンラインゲーム開発に興味がある方
  • リアルタイム通信を実装したい方

完成イメージ

  • 2 人のプレイヤーによるリアルタイム対戦
  • バーを操作しボールを打ち返す。相手の陣地にボールが入れば得点

参考ページ

作業環境
Unity: 6000.0.34f1
GS2 SDK for Unity: 2025.2.5
UniTask: 2.5.10

GS2-SDK のインポート

Unity プロジェクトを作成したら、 GS2のサービスを利用するために GS2-SDKをインポートします。 SDKのセットアップ | Game Server Services | Docs の手順に従います。

  1. UnityPackege 形式のインストーラー をダウンロードしインポートします
  2. Unity Editor のメニューから「Window > Game Server Services > SDK Installer」を選択します
  3. SDK インストーラーのウィンドウが表示されるので「Install」を押下します
  4. インストールが完了すると、プロジェクトの「Packeges」に SDK がインストールされます。 Package Manager でインストールされていることが確認できます。

UniTask のインポート

非同期処理を簡潔に書くために、UniTaskをインポートします。

  1. UniTask のリリースページ から最新版をダウンロード
  2. ダウンロードした .unitypackage ファイルをダブルクリックしてインポート
  3. すべての項目にチェックを入れて「Import」をクリック

スクリプティング定義シンボル GS2_ENABLE_UNITASK の追加

GS2-SDK で UniTask を使用するために、スクリプティング定義シンボルを追加します。

  1. Edit > Project Settings > Player を開く
  2. Other Settings > Scripting Define Symbols に GS2_ENABLE_UNITASK を追加

GS2 マネジメントコンソールでの設定

GS2 アカウントの作成

ゲームで GS2 の機能を使えるようにするために、開発者が自分のアカウントを作成します。

  1. GS2 マネジメントコンソール にアクセス
  2. アカウントを持っていない場合は新規登録
料金について
プラン名称 対象 価格
Individual 過去12か月間の売上高が1000万円未満の法人または個人 無料
Professional Individual プランに該当しない法人または個人 利用料金に準ずる
Enterprise Individual プランに該当しない法人または個人 ASK

※ 料金について 詳しくはこちら

GS2-Account の設定

ゲームのプレイヤーが匿名アカウントを作成できるようにするために必要な設定です。 GS2-Realtime のルームに接続する際に使用します。

  1. GS2 マネジメントコンソールで「Account」サービスを選択
  2. 「ネームスペース作成」をクリック
  3. 名前を「SamplePingPong」などに設定して作成

GS2-Account-Namespace

GS2-Realtime の設定

リアルタイム通信を行えるようにするための設定を行います。

ネームスペース作成

  1. GS2 マネジメントコンソールで「Realtime」サービスを選択
  2. 「ネームスペース作成」をクリック
  3. 名前を「SamplePingPong」などに設定して作成

GS2-Realtime-Namespace

サービスごとの追加料金について

GS2-Realtime の料金

スペック 単位 価格
realtime1.nano 1分 0.04円
realtime1.micro 1分 0.06円
realtime1.small 1分 0.12円
realtime1.medium 1分 0.24円
realtime1.large 1分 0.5円

※ サービスごとの追加料金について 詳しくはこちら

ルーム作成

  1. 作成したネームスペースを選択し、「ルーム作成」をクリック
  2. 名前を「SampleRoom」などに設定して作成

GS2-Realtime-Room

Unity エディタでの実装

プロジェクト構造

ゲームの実装に必要なクラスは主に以下の2つです:

  1. Gs2Manager: GS2 との通信を担当するクラス

    • ユーザー作成、認証、ルーム接続などを処理
    • メッセージの送受信を管理
    • 接続状態の監視と再接続処理
  2. GameManager: ゲームロジックを担当するクラス

    • プレイヤーの入力処理
    • ボールの物理演算と衝突判定
    • スコア管理
    • UI 表示の更新

シーンの編集

解像度設定

Project Settings > Player > Resolution and Presentation の内容を下記に変更します。

設定項目
Resolution
Run In Background true
Fullscreen Mode Windowed
Default Screen Width 400
Default Screen Height 800
Standalone Player Options
Resizable Window false
Visible In Background true
Allow Fullscreen Switch false
Force Single Instance (*1 false

*1: 検証のために 2 つのインスタンスを同時に建てる場合があることから false としています。

Gs2Manager

空の GameObject を作成し、「Gs2Manager」と命名します。 Gs2Manager.cs スクリプトを作成し、これにアタッチします。

Gs2Manager.cs 全文
using System;
using UnityEngine;
using Cysharp.Threading.Tasks;
using RelayRealtimeSession = Gs2.Unity.Gs2Realtime.RelayRealtimeSession;
using Google.Protobuf;
using Gs2Client = Gs2.Unity.Core.Gs2Domain;
using Gs2.Unity.Util;

public class Gs2Manager : MonoBehaviour
{
    /// <summary>
    /// インスタンス
    /// </summary>
    public static Gs2Manager Instance { get; private set; }

    /// <summary>
    /// メッセージ受信イベント
    /// </summary>
    public event Func<string, UniTask> OnMessageReceived;

    /// <summary>
    /// セッションが接続中かどうか
    /// </summary>
    public bool IsConnected { get; private set; }

    // GS2の認証情報
    private const string ClientId = "****";  // GS2 マネジメントコンソールで Home を選択し少し待つと表示されます
    private const string ClientSecret = "****";  // GS2 マネジメントコンソールで Home を選択し少し待つと表示されます

    // 名前空間
    private const string AccountNamespaceName = "SamplePingPong";  // 先の手順で設定した名前
    private const string RealtimeNameSpaceName = "SamplePingPong";  // 先の手順で設定した名前

    // ルーム名
    private const string RoomName = "SampleRoom";  // 先の手順で設定した名前

    // GS2のクライアント
    private Gs2Client _gs2;

    // ユーザー情報
    private string _userId;
    private string _password;
    private string _accessToken;

    // ルーム情報
    private string _ipAddress;
    private int _port;
    private string _encryptionKey;

    // GS2Realtime のセッション
    private RelayRealtimeSession _session;

    /// <summary>
    /// メッセージ送信
    /// </summary>
    /// <param name="message">メッセージ</param>
    public async UniTask Send(string message)
    {
        if (IsConnected == false)
        {
            return;
        }

        try
        {
            await _session.SendAsync(
                ByteString.CopyFromUtf8(message)
            );
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to send message to GS2 room: {e.Message}");
        }
    }

    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    private async void Start()
    {
        await Init();
    }

    private void OnApplicationQuit()
    {
        Term();
    }

    // 初期化
    private async UniTask Init()
    {
        if (IsConnected)
        {
            return;
        }

        try
        {
            await CreateGs2();
            await CreateUser();
            await Login();
            await Connect();
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to initialize GS2: {e.Message}");
        }
    }

    // 終了
    private void Term()
    {
        if (IsConnected == false && _session == null)
        {
            return;
        }

        try
        {
            Disconnect();
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to terminate GS2: {e.Message}");
        }
    }

    // GS2 のクライアントを作成
    private async UniTask CreateGs2()
    {
        _gs2 = await Gs2.Unity.Core.Gs2Client.CreateAsync(
            new Gs2.Core.Model.BasicGs2Credential(
                ClientId,
                ClientSecret
            )
        );

        Debug.Log("Created GS2");
    }

    // GS2 の匿名ユーザーを作成
    private async UniTask CreateUser()
    {
        try
        {
            var result = await _gs2.Account.Namespace(
                namespaceName: AccountNamespaceName
            ).CreateAsync();

            var item = await result.ModelAsync();
            _userId = item.UserId;
            _password = item.Password;

            Debug.Log("Created GS2 user");
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to create GS2 user: {e.Message}");
        }
    }

    // GS2 にログイン
    private async UniTask Login()
    {
        var gameSession = await _gs2.LoginAsync(
            new Gs2AccountAuthenticator(
                accountSetting: new AccountSetting
                {
                    accountNamespaceName = AccountNamespaceName,
                }
            ),
            _userId,
            _password
        );

        // アクセストークンは GS2-Realtime セッションを作成する際に使用する
        _accessToken = gameSession.AccessToken.Token;

        Debug.Log("Logged in GS2");
    }

    // GS2-Realtime に接続
    private async UniTask Connect()
    {
        try
        {
            // ルーム情報の取得
            var room = _gs2.Realtime.Namespace(
                namespaceName: RealtimeNameSpaceName
            ).Room(
                roomName: RoomName
            );
            var itemRoom = await room.ModelAsync();
            _ipAddress = itemRoom.IpAddress;
            _port = itemRoom.Port;
            _encryptionKey = itemRoom.EncryptionKey;

            // GS2Realtime のセッションを作成
            _session = new RelayRealtimeSession(
                _accessToken, // アクセストークン
                _ipAddress, // ゲームサーバのIPアドレス
                _port, // ゲームサーバのポート
                _encryptionKey, // 暗号鍵
                ByteString.CopyFromUtf8(_userId) // プロフィールの初期値
            );

            // 接続
            await _session.ConnectAsync(this);

            // メッセージ受信イベントハンドラを設定
            _session.OnRelayMessage += message =>
            {
                OnMessageReceived?.Invoke(message.Data.ToStringUtf8()).Forget();
            };

            IsConnected = true;

            Debug.Log("Connected to GS2 room");
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to connect to GS2 room: {e.Message}");
        }
    }

    // GS2-Realtime から切断
    private void Disconnect()
    {
        try
        {
            if (IsConnected || _session != null)
            {
                // GS2Realtime のセッションを切断
                _session.Dispose();
                _session = null;
                IsConnected = false;
                Debug.Log("Disconnected from GS2 room");
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to disconnect from GS2 room: {e.Message}");
        }
    }
}

GameManager.cs

空の GameObject を作成し、「GameManager」と命名します。 GameManager.cs スクリプトを作成し、これにアタッチします。

GameManager.cs 全文
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Cysharp.Threading.Tasks;
using TMPro;

public class GameManager : MonoBehaviour
{
    // Gs2Manager
    [SerializeField] private Gs2Manager _gs2Manager;

    // ゲームオブジェクト
    [SerializeField] private GameObject _playerBar;
    [SerializeField] private GameObject _opponentBar;
    [SerializeField] private GameObject _ball;
    [SerializeField] private TextMeshProUGUI _scoreText;
    [SerializeField] private TextMeshProUGUI _statusText;

    // ゲーム設定
    private const float BallSpeed = 5f;
    private const float PlayerBarSpeed = 10f;
    private const float BarWidth = 2f;
    private const float BarHeight = 0.5f;
    private const float BallRadius = 0.25f;
    private const float ScreenWidth = 2f;
    private const float ScreenHeight = 4f;

    // ゲーム状態
    private Vector2 _ballPosition;
    private Vector2 _ballDirection;
    private float _playerBarX;
    private float _opponentBarX;
    private int _playerScore;
    private int _opponentScore;
    private bool _isHost;
    private bool _isOpponentConnected;
    private float _waitingTimer;
    private const float PingInterval = 2.0f; // 対戦相手の確認間隔(秒)

    // ゲームの状態
    private enum GameState
    {
        WaitingForOpponent,
        Playing,
        GameOver
    }

    private GameState _currentState;

    // JSON用のクラス
    [Serializable]
    private class GameMessage
    {
        public float playerBarX;
        public float ballPosX;
        public float ballPosY;
        public float ballDirX;
        public float ballDirY;
        public int playerScore;
        public int opponentScore;
        public bool resetBall;
        public string status;
    }

    private void Awake()
    {
        // Gs2Managerのイベントにメッセージ処理ハンドラを登録
        if (_gs2Manager != null)
        {
            _gs2Manager.OnMessageReceived += OnMessageReceived;
        }
    }

    private void OnDestroy()
    {
        // イベント登録解除
        if (_gs2Manager != null)
        {
            _gs2Manager.OnMessageReceived -= OnMessageReceived;
        }
    }

    private void Start()
    {
        // ゲーム状態の初期化
        _currentState = GameState.WaitingForOpponent;
        _isOpponentConnected = false;
        _waitingTimer = 0f;

        // 初期化
        InitializeGameObjects();

        // 接続状態を表示
        UpdateStatusText();

        // 定期的に接続確認メッセージを送信
        SendPingMessage().Forget();
    }

    private void Update()
    {
        switch (_currentState)
        {
            case GameState.WaitingForOpponent:
                // 対戦相手を待っている状態
                _waitingTimer += Time.deltaTime;
                if (_waitingTimer >= 1.0f)
                {
                    _waitingTimer = 0f;
                    UpdateStatusText();
                }

                // 対戦相手が接続したらゲーム開始
                if (_isOpponentConnected)
                {
                    StartGame();
                }
                break;

            case GameState.Playing:
                if (!_gs2Manager.IsConnected)
                {
                    // 接続が切れた場合
                    _currentState = GameState.WaitingForOpponent;
                    _isOpponentConnected = false;
                    UpdateStatusText();
                    return;
                }

                // プレイヤーバーの移動
                HandlePlayerInput();

                // ホストの場合はボールの動きを計算
                if (_isHost)
                {
                    UpdateBallPosition();
                    CheckCollisions();
                    SendGameState(false);
                }
                else
                {
                    // クライアントは自分のバー位置だけ送信
                    SendPlayerPosition();
                }

                // ゲームオブジェクトの位置を更新
                UpdateGameObjects();
                break;

            case GameState.GameOver:
                // ゲーム終了状態
                break;
        }
    }

    private void InitializeGameObjects()
    {
        // ゲームオブジェクトの初期位置を設定
        _playerBarX = 0f;
        _opponentBarX = 0f;
        _playerBar.transform.position = new Vector3(0f, -ScreenHeight + BarHeight, 0f);
        _opponentBar.transform.position = new Vector3(0f, ScreenHeight - BarHeight, 0f);
        _ball.transform.position = Vector3.zero;

        // ボールを非表示にする(ゲーム開始前)
        _ball.SetActive(false);

        // スコア表示を初期化
        _playerScore = 0;
        _opponentScore = 0;
        UpdateScoreText();
    }

    private void StartGame()
    {
        // ゲーム状態の初期化
        _ballPosition = Vector2.zero;
        _ballDirection = new Vector2(1f, 0.5f).normalized;
        _playerScore = 0;
        _opponentScore = 0;

        // ホストかどうかを決定(最初の接続者がホスト)
        // 実際の実装では接続順などで決定
        _isHost = DetermineIfHost();

        // ボールを表示
        _ball.SetActive(true);

        // スコア表示を更新
        UpdateScoreText();

        // ゲーム状態を更新
        _currentState = GameState.Playing;

        // ステータス表示を更新
        _statusText.text = _isHost ? "You are a host" : "You are a client";

        // ホストの場合は初期ゲーム状態を送信
        if (_isHost)
        {
            SendGameState(true);
        }
    }

    // ホストかどうかを決定するロジック
    private bool DetermineIfHost()
    {
        // 実際の実装では、ユーザーIDの比較や接続順などで決定
        // ここでは簡易的に実装
        return true; // 常にホストとして扱う場合
    }

    private void UpdateStatusText()
    {
        if (_currentState == GameState.WaitingForOpponent)
        {
            // 待機中のアニメーションテキスト
            int dots = (int)(_waitingTimer * 2) % 4;
            string dotsText = new string('.', dots);
            _statusText.text = $"Waiting for opponent{dotsText}";
        }
    }

    // 定期的に接続確認メッセージを送信
    private async UniTaskVoid SendPingMessage()
    {
        while (_currentState == GameState.WaitingForOpponent)
        {
            GameMessage pingMessage = new GameMessage
            {
                status = "ping"
            };

            string message = JsonUtility.ToJson(pingMessage);
            await SendMsg(message);

            // 一定時間待機
            await UniTask.Delay(TimeSpan.FromSeconds(PingInterval));
        }
    }

    private void HandlePlayerInput()
    {
        // 水平方向の入力を取得
        float horizontalInput = Input.GetAxis("Horizontal");

        // プレイヤーバーの位置を更新
        _playerBarX = Mathf.Clamp(_playerBarX + horizontalInput * PlayerBarSpeed * Time.deltaTime, -ScreenWidth + BarWidth/2, ScreenWidth - BarWidth/2);
    }

    private void UpdateBallPosition()
    {
        // ボールの位置を更新
        _ballPosition += _ballDirection * (BallSpeed * Time.deltaTime);
    }

    private void CheckCollisions()
    {
        // 左右の壁との衝突
        if (_ballPosition.x is < -ScreenWidth + BallRadius or > ScreenWidth - BallRadius)
        {
            _ballDirection.x = -_ballDirection.x;
        }

        // プレイヤーバーとの衝突
        if (_ballPosition.y < -ScreenHeight + BarHeight + BallRadius)
        {
            if (Mathf.Abs(_ballPosition.x - _playerBarX) < BarWidth/2)
            {
                // バーに当たった場合は跳ね返る
                _ballDirection.y = Mathf.Abs(_ballDirection.y);

                // バーのどの位置に当たったかで反射角を変える
                float hitFactor = (_ballPosition.x - _playerBarX) / (BarWidth/2);
                _ballDirection.x = hitFactor * 0.75f;
                _ballDirection = _ballDirection.normalized;
            }
            else if (_ballPosition.y < -ScreenHeight)
            {
                // 画面下端を超えた場合は相手の得点
                _opponentScore++;
                UpdateScoreText();
                CheckGameOver();
                ResetBall();
            }
        }

        // 相手バーとの衝突
        if (_ballPosition.y > ScreenHeight - BarHeight - BallRadius)
        {
            if (Mathf.Abs(_ballPosition.x - _opponentBarX) < BarWidth/2)
            {
                // バーに当たった場合は跳ね返る
                _ballDirection.y = -Mathf.Abs(_ballDirection.y);

                // バーのどの位置に当たったかで反射角を変える
                float hitFactor = (_ballPosition.x - _opponentBarX) / (BarWidth/2);
                _ballDirection.x = hitFactor * 0.75f;
                _ballDirection = _ballDirection.normalized;
            }
            else if (_ballPosition.y > ScreenHeight)
            {
                // 画面上端を超えた場合は自分の得点
                _playerScore++;
                UpdateScoreText();
                CheckGameOver();
                ResetBall();
            }
        }
    }

    private void ResetBall()
    {
        // ボールの位置をリセット
        _ballPosition = Vector2.zero;

        // ランダムな方向を設定
        float angle = UnityEngine.Random.Range(-45f, 45f);
        _ballDirection = new Vector2(
            _isHost ? Mathf.Cos(angle * Mathf.Deg2Rad) : -Mathf.Cos(angle * Mathf.Deg2Rad),
            _isHost ? Mathf.Sin(angle * Mathf.Deg2Rad) : -Mathf.Sin(angle * Mathf.Deg2Rad)
        ).normalized;

        // ゲーム状態を送信(ボールリセットフラグをtrueに)
        SendGameState(true);
    }

    private void CheckGameOver()
    {
        if (_playerScore >= 3 || _opponentScore >= 3)
        {
            bool isWin = _playerScore >= 3;
            GameOver(isWin);
        }
    }

    private void GameOver(bool isWin)
    {
        _currentState = GameState.GameOver;
        _statusText.text = isWin ? "Win!" : "Lose...";

        // 3秒後に再開
        UniTask.Delay(3000).ContinueWith(() => {
            InitializeGameObjects();
            _currentState = GameState.WaitingForOpponent;
            _waitingTimer = 0f;
            UpdateStatusText();
        }).Forget();
    }

    private void UpdateGameObjects()
    {
        // ゲームオブジェクトの位置を更新
        _playerBar.transform.position = new Vector3(_playerBarX, -ScreenHeight + BarHeight, 0f);
        _opponentBar.transform.position = new Vector3(_opponentBarX, ScreenHeight - BarHeight, 0f);
        _ball.transform.position = new Vector3(_ballPosition.x, _ballPosition.y, 0f);
    }

    private void UpdateScoreText()
    {
        _scoreText.text = $"Score: {_playerScore} - {_opponentScore}";
    }

    private void SendPlayerPosition()
    {
        GameMessage state = new GameMessage
        {
            playerBarX = _playerBarX,
            status = "position"
        };

        string message = JsonUtility.ToJson(state);
        SendMsg(message).Forget();
    }

    private void SendGameState(bool resetBall)
    {
        GameMessage state = new GameMessage
        {
            playerBarX = _playerBarX,
            ballPosX = _ballPosition.x,
            ballPosY = _ballPosition.y,
            ballDirX = _ballDirection.x,
            ballDirY = _ballDirection.y,
            playerScore = _playerScore,
            opponentScore = _opponentScore,
            resetBall = resetBall,
            status = "gameState"
        };

        string message = JsonUtility.ToJson(state);
        SendMsg(message).Forget();
    }

    private async UniTask SendMsg(string message)
    {
        try
        {
            // Gs2Managerの公開メソッドを呼び出す
            await _gs2Manager.Send(message);
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to send message: {e.Message}");
        }
    }

    // Gs2Managerからのメッセージ受信ハンドラ
    private async UniTask OnMessageReceived(string message)
    {
        try
        {
            // JSONメッセージをパース
            GameMessage state = JsonUtility.FromJson<GameMessage>(message);

            if (state == null) return;

            switch (state.status)
            {
                case "ping":
                    // 接続確認メッセージを受信した場合、応答を返す
                    GameMessage pongMessage = new GameMessage
                    {
                        status = "pong"
                    };
                    await SendMsg(JsonUtility.ToJson(pongMessage));

                                        // 対戦相手が接続していることを確認
                    if (!_isOpponentConnected)
                    {
                        _isOpponentConnected = true;

                        // 待機中だった場合はゲーム開始
                        if (_currentState == GameState.WaitingForOpponent)
                        {
                            StartGame();
                        }
                    }
                    break;

                case "pong":
                    // 接続確認応答を受信した場合
                    if (!_isOpponentConnected)
                    {
                        _isOpponentConnected = true;

                        // 待機中だった場合はゲーム開始
                        if (_currentState == GameState.WaitingForOpponent)
                        {
                            StartGame();
                        }
                    }
                    break;

                case "position":
                    // 相手のバー位置を更新
                    _opponentBarX = state.playerBarX;

                    // 対戦相手が接続していることを確認
                    if (!_isOpponentConnected)
                    {
                        _isOpponentConnected = true;

                        // 待機中だった場合はゲーム開始
                        if (_currentState == GameState.WaitingForOpponent)
                        {
                            StartGame();
                        }
                    }
                    break;

                case "gameState":
                    // 相手のバー位置を更新
                    _opponentBarX = state.playerBarX;

                    // 対戦相手が接続していることを確認
                    if (!_isOpponentConnected)
                    {
                        _isOpponentConnected = true;

                        // 待機中だった場合はゲーム開始
                        if (_currentState == GameState.WaitingForOpponent)
                        {
                            StartGame();
                        }
                    }

                    // ホストでない場合はボールの状態を更新
                    if (!_isHost && _currentState == GameState.Playing)
                    {
                        _ballPosition = new Vector2(state.ballPosX, state.ballPosY);
                        _ballDirection = new Vector2(state.ballDirX, state.ballDirY);

                        // スコアの更新(ホストとクライアントで反転)
                        _playerScore = state.opponentScore;
                        _opponentScore = state.playerScore;
                        UpdateScoreText();

                        // ゲーム終了判定
                        if (_playerScore >= 3 || _opponentScore >= 3)
                        {
                            bool isWin = _playerScore >= 3;
                            GameOver(isWin);
                        }

                        // ボールのリセット
                        if (state.resetBall)
                        {
                            // ボールの位置を更新
                            _ball.transform.position = new Vector3(_ballPosition.x, _ballPosition.y, 0f);
                        }
                    }
                    break;

                case "disconnect":
                    // 相手が切断した場合
                    _isOpponentConnected = false;

                    if (_currentState == GameState.Playing)
                    {
                        // ゲーム中だった場合は待機状態に戻る
                        _currentState = GameState.WaitingForOpponent;
                        _waitingTimer = 0f;
                        _ball.SetActive(false);
                        UpdateStatusText();

                        // 再度接続確認メッセージの送信を開始
                        SendPingMessage().Forget();
                    }
                    break;
            }
        }
        catch (Exception e)
        {
            Debug.LogError($"Failed to process message: {e.Message}");
        }
    }
}

各オブジェクトの作成

プレイヤーバー

  • 2Dオブジェクト > Sprite > Square を作成し、「PlayerBar」と命名
  • 位置を(0, -4, 0)に設定
  • スケールを(2, 0.2, 1)に設定

相手バー

  • 2Dオブジェクト > Sprite > Square を作成し、「OpponentBar」と命名
  • 位置を(0, 4, 0)に設定
  • スケールを(2, 0.2, 1)に設定
  • 色を赤色に変更

ボール

  • 2Dオブジェクト > Sprite > Circle を作成し、「Ball」と命名
  • 位置を(0, 0, 0)に設定
  • スケールを(0.2, 0.2, 1)に設定

UI要素

  • Canvas を作成
  • TextMeshPro - Text を2つ追加し、「ScoreText」と「StatusText」と命名

GameManager へアタッチ

GameManager のインスペクタを開き、オブジェクトをアタッチします。

UnityEditor

動作確認

  1. プロジェクトをビルドします
  2. ビルドしたアプリを実行し「Waiting for opponent...」と表示されるのを確認します
  3. さらにもう一つアプリを実行して、ゲームが開始されることを確認します
  4. それぞれのゲームウインドウでプレイヤーバーを操作し、同期されることを確認します

SamplePingPongAnimation

まとめ

GS2-Realtime を使用して、リアルタイム対戦ピンポンゲームを実装しました。 GS2-Realtime を使うことで、サーバーサイドの複雑な実装なしに、リアルタイム通信機能を持つゲームを簡単に作成できることが分かりました。

実際のゲーム開発では、より複雑な状態同期や、遅延対策、チート対策なども考慮する必要がありますが、 GS2-Realtime ではそれらの基盤となる機能を提供することが可能です。

発展課題

  • マッチメイキング機能の追加(GS2-Matchmaking を使用)
  • ランキング機能の実装(GS2-Ranking2 を使用)
  • 複数のルームを作成して、友達と特定のルームで対戦できるようにする
  • リアクションボタンによるユーザー間コミュニケーション機能の追加

GS2-Realtime を使えば、様々なリアルタイム対戦ゲームを簡単に実装できます。ぜひ挑戦してみてください!

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.