Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
3
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

OpenSiv3DとPhotonを連携して将棋ゲームをオンライン化した話

はじめに

OpenSiv3DでPhotonを扱う際に詰まったところや苦しんだところを共有できればなと思い、今回の記事を書くことにしました。今後OpenSiv3DやSiv3Dでオンラインゲーム作りたい!って人は是非この記事を参考にしていただければと思います。

導入方法

基本的な導入の仕方はpara7さんの記事を参考にしていただければと思います。今回はXCodeでの導入方法と、Visual Studioでの導入方法を書きたいと思います。特にVSの導入はまじで時間かかりました...

macOS(XCode)での導入方法

こ↑こ↓(ちょっとした出来心だったんです許してください何でも)からダウンロードしてください。ダウンロードしたフォルダの中身はこんな感じになってます。
fuga.jpg

このフォルダの名前を「Photon」として、/usr/local/ に置いておくと後々楽かもしれません。(別にどこに置いても問題ないです)

ちなみに上の画像には存在しませんが、このダウンロードしたフォルダの中に「demo」というフォルダがあります。この中に公式で提供してるサンプルがあります。一応存在だけ書いておきました。

次に、OpenSiv3Dのプロジェクトを開きます。WindowsもmacOSもこちらにインストール方法やプロジェクトの開き方が載っています。開いたらPhotonのパスを通していきます。

パスの設定

(今回はダウンロードしたフォルダ名をPhotonにし、/usr/local/ に置いた場合を示してます。)

「Linking」→「Other Linker Flags」に下記を追加します。(DebugとReleaseに同じのを追加してください。)

-lCommon-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)
-lPhoton-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)
-lLoadBalancing-cpp_$(CONFIGURATION)_$(PLATFORM_NAME)

「Search Paths」→「Header Search Paths」に下記を追加します。
/usr/local/Photon

最後に、「Library Paths」→「Library Search Paths」に下記を追加します。

/usr/local/Photon/Common-cpp/lib
/usr/local/Photon/Photon-cpp/lib
/usr/local/Photon/LoadBalancing-cpp/lib

私は「Other Linker Flags」を設定するのを忘れていた為、para7さんに指摘されるまで気付きませんでした。俺の8時間返してくれ

一応上記の追加した状態のスクショを示しておきます。3枚貼りますがご容赦ください。
スクリーンショット 2020-07-09 2.51.40.png
スクリーンショット 2020-07-09 3.08.38.png
スクリーンショット 2020-07-09 2.58.45.png

Windows(Visual Studio)での導入方法

こちらからダウンロードしてください。ダウンロードしたフォルダの中身はこんな感じになってます。
コメント 2020-07-09 031904.jpg

次に、OpenSiv3Dのプロジェクトを開きます。開いたらPhotonのパスを通していきます。

パスの設定

(今回はダウンロードしたフォルダ名をPhotonにし、Documents に置いた場合を示してます。)

「プロパティ」→「C/C++」→「全般」→「追加のインクルードディレクトリ」に下記を追加します。

C:\Users\ユーザ名\Documents\Photon

次に「リンカー」→「全般」→「追加のライブラリディレクトリ」に下記を追加します。

C:\Users\ユーザ名\Documents\Photon\Common-cpp
C:\Users\ユーザ名\Documents\Photon\Photon-cpp
C:\Users\ユーザ名\Documents\Photon\LoadBalancing-cpp
C:\Users\ユーザ名\Documents\Photon\Common-cpp\lib
C:\Users\ユーザ名\Documents\Photon\Photon-cpp\lib
C:\Users\ユーザ名\Documents\Photon\LoadBalancing-cpp\lib

まぁここまではmacOS同様ただ書くだけでした。ここからがPhoton Windows SDK導入で最大の難所です。

約80個のファイルの中から当たりを1つ探す

嘘じゃないです、本当です(真顔)。「リンカー」→「入力」→「追加の依存ファイル」に追加するパスですが、Common-cpp、Photon-cpp、LoadBalancing-cppにあるlibフォルダの中から正解をそれぞれ一つづつ探します。私の場合下記のようになりましたが、VSのバージョンによって変化しますので頑張ってください()

C:\Users\ユーザ名\Documents\Photon\Common-cpp\lib\Common-cpp_vc16_debug_windows_mt_x64.lib
C:\Users\ユーザ名\Documents\Photon\Photon-cpp\lib\Photon-cpp_vc16_debug_windows_mt_x64.lib
C:\Users\ユーザ名\Documents\Photon\LoadBalancing-cpp\lib\LoadBalancing-cpp_vc16_debug_windows_mt_x64.lib

これはDebugモードの場合ですので、Releaseモードに適用する場合は上記の工程をdebugという文字をreleaseにして設定してください。もう一度言いますがVSのバージョンによって変化します。

コードの作成

ここまででようやくPhotonをソースコードで扱えるようになります。しかしまだ注意するところがあります。名前空間です。

PhotonとOpenSiv3Dはそのまま扱うとクラス名や関数名が衝突します。理由としてOpenSiv3D側で「using namespace s3d;」と記述してあるからです。この記述を無効にする為に普段
#include<Siv3D.hpp>
と書いてるところの上に
#define NO_S3D_USING
と記述してください。お察しの方もいると思いますが、OpenSiv3D側のクラスや関数には全て自分でs3d::をつけてください。大量のエラーが出てしまいますので。

SceneManagerを変更する

OpenSiv3Dでゲームを作ってる人なら多分知ってるであろう機能の「SceneManager」。これを使うことによってシーン遷移や共有データの管理がとても簡単になりますね。という事でSceneManagerに変更を加えてPhotonを使いやすくしよう、という寸法です。流石にSceneManager本体を変更してしまうと今後の制作活動に影響が出てしまいますので、新しくSceneMaster.hpp等を作成し、そこにSceneManagerをコピペして変更する事にしました。

まずは私が変更し終わったSceneMasterクラスをご覧ください。

長いので折り畳んでます。
SceneMaster.hpp
#pragma once
#define NO_S3D_USING
#include <Siv3D.hpp>  // OpenSiv3D v0.4.3
#include <LoadBalancing-cpp/inc/Client.h>

using s3d::int32;
using s3d::uint32;
using s3d::uint64;


namespace mak1a {
    /// <summary>
    /// ExitGames::Common::JStringからs3d::Stringに変換する
    /// </summary>
    /// <param name="str">変換したい文字列</param>
    /// <returns>s3d::Stringに変換した文字列</returns>
    [[nodiscard]] inline s3d::String ConvertJStringToString(const ExitGames::Common::JString& str) {
        return s3d::Unicode::FromWString(std::wstring(str));
    }

    /// <summary>
    /// s3d::StringからExitGames::Common::JStringに変換する
    /// </summary>
    /// <param name="str">変換したい文字列</param>
    /// <returns>ExitGames::Common::JStringに変換した文字列</returns>
    [[nodiscard]] inline ExitGames::Common::JString ConvertStringToJString(const s3d::String& str) {
        return ExitGames::Common::JString(str.toWstr().c_str());
    }

    /// <summary>
    /// シーン管理
    /// </summary>
    /// <remarks>
    /// State にはシーンを区別するキーの型、Data にはシーン間で共有するデータの型を指定します。
    /// </remarks>
    template<class State, class Data>
    class SceneMaster;

    namespace detail {
        struct EmptyData {};
    }  // namespace detail

    /// <summary>
    /// シーン・インタフェース
    /// </summary>
    template<class State, class Data>
    class IScene : s3d::Uncopyable {
    public:
        using State_t = State;

        using Data_t = Data;

        struct InitData {
            State_t state;

            std::shared_ptr<Data_t> _s;

            SceneMaster<State_t, Data_t>* _m;

            InitData() = default;

            InitData(const State_t& _state, const std::shared_ptr<Data_t>& data, SceneMaster<State_t, Data_t>* manager) : state(_state), _s(data), _m(manager) {}
        };

    private:
        State_t m_state;

        std::shared_ptr<Data_t> m_data;

        SceneMaster<State_t, Data_t>* m_manager;

    public:
        /// <summary>
        /// SceneMasterクラスのコールバック関数が呼び出されたら下記のメンバ関数が呼び出される
        /// 各シーンでオーバーライドする事により、そのメンバ関数の実装が書ける。
        /// </summary>

        virtual void DebugReturn(int /*debugLevel*/, const ExitGames::Common::JString& /*string*/) {}

        virtual void ConnectionErrorReturn(int /*errorCode*/) {}

        virtual void ClientErrorReturn(int /*errorCode*/) {}

        virtual void WarningReturn(int /*warningCode*/) {}

        virtual void ServerErrorReturn(int /*errorCode*/) {}

        virtual void JoinRoomEventAction(int /*playerNr*/, const ExitGames::Common::JVector<int>& /*playernrs*/, const ExitGames::LoadBalancing::Player& /*player*/) {}

        virtual void LeaveRoomEventAction(int /*playerNr*/, bool /*isInactive*/) {}

        virtual void CustomEventAction(int /*playerNr*/, nByte /*eventCode*/, const ExitGames::Common::Object& /*eventContent*/) {}

        virtual void ConnectReturn(int /*errorCode*/,
                                   const ExitGames::Common::JString& /*errorString*/,
                                   const ExitGames::Common::JString& /*region*/,
                                   const ExitGames::Common::JString& /*cluster*/) {}

        virtual void DisconnectReturn() {}

        virtual void LeaveRoomReturn(int /*errorCode*/, const ExitGames::Common::JString& /*errorString*/) {}

        virtual void CreateRoomReturn(int /*localPlayerNr*/,
                                      const ExitGames::Common::Hashtable& /*roomProperties*/,
                                      const ExitGames::Common::Hashtable& /*playerProperties*/,
                                      int /*errorCode*/,
                                      const ExitGames::Common::JString& /*errorString*/) {}

        virtual void JoinRandomRoomReturn(int /*localPlayerNr*/,
                                          const ExitGames::Common::Hashtable& /*roomProperties*/,
                                          const ExitGames::Common::Hashtable& /*playerProperties*/,
                                          int /*errorCode*/,
                                          const ExitGames::Common::JString& /*errorString*/) {}

    public:
        explicit IScene(const InitData& init) : m_state(init.state), m_data(init._s), m_manager(init._m) {}

        /// <summary>
        /// Photonと接続する
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void Connect() {
            m_manager->GetClient().setAutoJoinLobby(true);

            if (!m_manager->GetClient().connect(ExitGames::LoadBalancing::AuthenticationValues().setUserID(ExitGames::Common::JString() + GETTIMEMS()))) {
                return;
            }

            m_manager->UsePhoton(true);
        }

        /// <summary>
        /// Photonから切断する
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void Disconnect() {
            m_manager->GetClient().disconnect();
        }

        virtual ~IScene() {}

        /// <summary>
        /// serviceを呼び出す際にデバッグがしたい場合オーバーライドしてください
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void UpdatePhoton() {}

        /// <summary>
        /// serviceの呼び出し
        /// UpdatePhotonも呼び出す
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void RunPhoton() {
            UpdatePhoton();
            m_manager->GetClient().service();
        }

        /// <summary>
        /// ルーム作成
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void CreateRoom(const ExitGames::Common::JString& roomName_, const ExitGames::Common::Hashtable& properties_, const nByte maxPlayers_) {
            m_manager->GetClient().opCreateRoom(roomName_, ExitGames::LoadBalancing::RoomOptions().setMaxPlayers(maxPlayers_).setCustomRoomProperties(properties_));
        }

        /// <summary>
        /// ExitGames::LoadBalancing::Clientの呼び出し
        /// </summary>
        /// <returns>
        /// ExitGames::LoadBalancing::Clientクラスの参照
        /// </returns>
        ExitGames::LoadBalancing::Client& GetClient() {
            return m_manager->GetClient();
        }

        /// <summary>
        /// フェードイン時の更新
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void updateFadeIn(double) {}

        /// <summary>
        /// 通常時の更新
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void update() {}

        /// <summary>
        /// フェードアウト時の更新
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void updateFadeOut(double) {}

        /// <summary>
        /// 通常時の描画
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        virtual void draw() const {}

        /// <summary>
        /// フェードイン時の描画
        /// </summary>
        /// <param name="t">
        /// フェードインの経過 (0.0 → 1.0)
        /// </param>
        /// <returns>
        /// なし
        /// </returns>
        virtual void drawFadeIn(double t) const {
            draw();

            s3d::Transformer2D transform(s3d::Mat3x2::Identity(), s3d::Transformer2D::Target::SetLocal);

            s3d::Scene::Rect().draw(s3d::ColorF(m_manager->getFadeColor()).setA(1.0 - t));
        }

        /// <summary>
        /// フェードイアウト時の描画
        /// </summary>
        /// <param name="t">
        /// フェードアウトの経過 (0.0 → 1.0)
        /// </param>
        /// <returns>
        /// なし
        /// </returns>
        virtual void drawFadeOut(double t) const {
            draw();

            s3d::Transformer2D transform(s3d::Mat3x2::Identity(), s3d::Transformer2D::Target::SetLocal);

            s3d::Scene::Rect().draw(s3d::ColorF(m_manager->getFadeColor()).setA(t));
        }

    protected:
        [[nodiscard]] const State_t& getState() const {
            return m_state;
        }

        /// <summary>
        /// 共有データへの参照を取得します。
        /// </summary>
        /// <returns>
        /// 共有データへの参照
        /// </returns>
        [[nodiscard]] Data_t& getData() const {
            return *m_data;
        }

        /// <summary>
        /// シーンの変更を通知します。
        /// </summary>
        /// <param name="state">
        /// 次のシーンのキー
        /// </param>
        /// <param name="transitionTime">
        /// フェードイン・アウトの時間
        /// </param>
        /// <param name="crossFade">
        /// クロスフェードを有効にするか
        /// </param>
        /// <returns>
        /// シーンの変更が可能でフェードイン・アウトが開始される場合 true, それ以外の場合は false
        /// </returns>
        bool changeScene(const State_t& state, const s3d::Duration& transitionTime = s3d::MillisecondsF(1000), bool crossFade = false) {
            return changeScene(state, static_cast<s3d::int32>(transitionTime.count() * 1000), crossFade);
        }

        /// <summary>
        /// シーンの変更を通知します。
        /// </summary>
        /// <param name="state">
        /// 次のシーンのキー
        /// </param>
        /// <param name="transitionTimeMillisec">
        /// フェードイン・アウトの時間(ミリ秒)
        /// </param>
        /// <param name="crossFade">
        /// クロスフェードを有効にするか
        /// </param>
        /// <returns>
        /// シーンの変更が可能でフェードイン・アウトが開始される場合 true, それ以外の場合は false
        /// </returns>
        bool changeScene(const State_t& state, s3d::int32 transitionTimeMillisec, bool crossFade = false) {
            return m_manager->changeScene(state, transitionTimeMillisec, crossFade);
        }

        /// <summary>
        /// エラーの発生を通知します。
        /// </summary>
        /// <remarks>
        /// この関数を呼ぶと、以降の SceneMaster::update() / updateAndDraw() が false を返します。
        /// </remarks>
        /// <returns>
        /// なし
        /// </returns>
        void notifyError() {
            return m_manager->notifyError();
        }
    };

    /// <summary>
    /// シーン管理
    /// </summary>
    /// <remarks>
    /// State にはシーンを区別するキーの型、Data にはシーン間で共有するデータの型を指定します。
    /// SceneMasterクラスはExitGames::LoadBalancing::Listenerクラスを継承してます。
    /// </remarks>
    template<class State, class Data = detail::EmptyData>
    class SceneMaster : public ExitGames::LoadBalancing::Listener {
    private:
        using Scene_t = std::shared_ptr<IScene<State, Data>>;

        using FactoryFunction_t = std::function<Scene_t()>;

        s3d::HashTable<State, FactoryFunction_t> m_factories;

        std::shared_ptr<Data> m_data;

        Scene_t m_current;

        Scene_t m_next;

        Scene_t m_previous;

        State m_currentState;

        State m_nextState;

        s3d::Optional<State> m_first;

        enum class TransitionState {
            None,

            FadeIn,

            Active,

            FadeOut,

            FadeInOut,

        } m_transitionState
            = TransitionState::None;

        s3d::Stopwatch m_stopwatch;

        s3d::int32 m_transitionTimeMillisec = 1000;

        s3d::ColorF m_fadeColor = s3d::Palette::Black;

        bool m_crossFade = false;

        bool m_error = false;

        bool m_usePhoton;

        ExitGames::LoadBalancing::Client m_loadBalancingClient;

        bool updateSingle() {
            double elapsed = m_stopwatch.msF();

            if (m_transitionState == TransitionState::FadeOut && elapsed >= m_transitionTimeMillisec) {
                m_current = nullptr;

                m_current = m_factories[m_nextState]();

                if (hasError()) {
                    return false;
                }

                m_currentState = m_nextState;

                m_transitionState = TransitionState::FadeIn;

                m_stopwatch.restart();

                elapsed = 0.0;
            }

            if (m_transitionState == TransitionState::FadeIn && elapsed >= m_transitionTimeMillisec) {
                m_stopwatch.reset();

                m_transitionState = TransitionState::Active;
            }

            switch (m_transitionState) {
            case TransitionState::FadeIn:
                assert(m_transitionTimeMillisec);
                m_current->updateFadeIn(elapsed / m_transitionTimeMillisec);
                return !hasError();
            case TransitionState::Active:
                m_current->update();
                if (UsePhoton()) {
                    m_current->RunPhoton(); // Photonを使用する場合呼び出される。
                }
                return !hasError();
            case TransitionState::FadeOut:
                assert(m_transitionTimeMillisec);
                m_current->updateFadeOut(elapsed / m_transitionTimeMillisec);
                return !hasError();
            default:
                return false;
            }
        }

        bool updateCross() {
            const double elapsed = m_stopwatch.msF();

            if (m_transitionState == TransitionState::FadeInOut) {
                if (elapsed >= m_transitionTimeMillisec) {
                    m_current = m_next;

                    m_next = nullptr;

                    m_stopwatch.reset();

                    m_transitionState = TransitionState::Active;
                }
            }

            if (m_transitionState == TransitionState::Active) {
                m_current->update();

                return !hasError();
            }
            else {
                assert(m_transitionTimeMillisec);

                const double t = elapsed / m_transitionTimeMillisec;

                m_current->updateFadeOut(t);

                if (hasError()) {
                    return false;
                }

                m_next->updateFadeIn(t);

                return !hasError();
            }
        }

        [[nodiscard]] bool hasError() const noexcept {
            return m_error;
        }

    public:
        /// <summary>
        /// シーンのインタフェース
        /// </summary>
        using Scene = IScene<State, Data>;

        /// <summary>
        /// シーン管理を初期化します。
        /// </summary>
        /// <param name="appID_">
        /// PhotonのappID
        /// </param>
        /// <param name="appVersion_">
        /// バージョン
        /// </param>
        SceneMaster(const ExitGames::Common::JString& appID_, const ExitGames::Common::JString& appVersion_)
        : m_data(s3d::MakeShared<Data>()), m_loadBalancingClient(*this, appID_, appVersion_), m_usePhoton(false) {}

        /// <summary>
        /// シーン管理を初期化します。
        /// </summary>
        /// <param name="data">
        /// 共有データ
        /// </param>
        /// <param name="appID_">
        /// PhotonのappID
        /// </param>
        /// <param name="appVersion_">
        /// バージョン
        /// </param>
        explicit SceneMaster(const std::shared_ptr<Data>& data, const ExitGames::Common::JString& appID_, const ExitGames::Common::JString& appVersion_)
        : m_data(data), m_loadBalancingClient(*this, appID_, appVersion_), m_usePhoton(false) {}

        /// <summary>
        /// Photonを使うかどうか
        /// </summary>
        /// <returns>
        /// 使う場合true, それ以外の場合はfalse
        /// </returns>
        bool UsePhoton() {
            return m_usePhoton;
        }
        /// <summary>
        /// Photonを使用してるかを設定する
        /// </summary>
        /// <param name="appID_">
        /// Photonを使用してるか
        /// </param>
        void UsePhoton(const bool use_) {
            m_usePhoton = use_;
        }

        /// <summary>
        /// シーンを追加します。
        /// </summary>
        /// <param name="state">
        /// シーンのキー
        /// </param>
        /// <returns>
        /// 追加に成功した場合 true, それ以外の場合は false
        /// </returns>
        template<class Scene>
        SceneMaster& add(const State& state) {
            typename Scene::InitData initData(state, m_data, this);

            auto factory = [=]() { return std::make_shared<Scene>(initData); };

            auto it = m_factories.find(state);

            if (it != m_factories.end()) {
                it.value() = factory;
            }
            else {
                m_factories.emplace(state, factory);

                if (!m_first) {
                    m_first = state;
                }
            }

            return *this;
        }

        /// <summary>
        /// 最初のシーンを初期化します。
        /// </summary>
        /// <param name="state">
        /// 最初のシーン
        /// </param>
        /// <returns>
        /// 初期化に成功した場合 true, それ以外の場合は false
        /// </returns>
        bool init(const State& state) {
            if (m_current) {
                return false;
            }

            auto it = m_factories.find(state);

            if (it == m_factories.end()) {
                return false;
            }

            m_currentState = state;

            m_current = it->second();

            if (hasError()) {
                return false;
            }

            m_transitionState = TransitionState::FadeIn;

            m_stopwatch.restart();

            return true;
        }

        /// <summary>
        /// シーンを更新します。
        /// </summary>
        /// <returns>
        /// シーンの更新に成功した場合 true, それ以外の場合は false
        /// </returns>
        bool updateScene() {
            if (hasError()) {
                return false;
            }

            if (!m_current) {
                if (!m_first) {
                    return true;
                }
                else if (!init(m_first.value())) {
                    return false;
                }
            }

            if (m_crossFade) {
                return updateCross();
            }
            else {
                return updateSingle();
            }
        }

        /// <summary>
        /// シーンを描画します。
        /// </summary>
        /// <returns>
        /// なし
        /// </returns>
        void drawScene() const {
            if (!m_current) {
                return;
            }

            if (m_transitionState == TransitionState::Active || !m_transitionTimeMillisec) {
                m_current->draw();
            }

            const double elapsed = m_stopwatch.msF();

            if (m_transitionState == TransitionState::FadeIn) {
                m_current->drawFadeIn(elapsed / m_transitionTimeMillisec);
            }
            else if (m_transitionState == TransitionState::FadeOut) {
                m_current->drawFadeOut(elapsed / m_transitionTimeMillisec);
            }
            else if (m_transitionState == TransitionState::FadeInOut) {
                m_current->drawFadeOut(elapsed / m_transitionTimeMillisec);

                if (m_next) {
                    m_next->drawFadeIn(elapsed / m_transitionTimeMillisec);
                }
            }
        }

        /// <summary>
        /// シーンの更新と描画を行います。
        /// </summary>
        /// <returns>
        /// シーンの更新に成功した場合 true, それ以外の場合は false
        /// </returns>
        bool update() {
            if (!updateScene()) {
                return false;
            }

            drawScene();

            return true;
        }

        /// <summary>
        /// 共有データを取得します。
        /// </summary>
        /// <returns>
        /// 共有データへのポインタ
        /// </returns>
        [[nodiscard]] std::shared_ptr<Data> get() {
            return m_data;
        }

        /// <summary>
        /// 共有データを取得します。
        /// </summary>
        /// <returns>
        /// 共有データへのポインタ
        /// </returns>
        [[nodiscard]] const std::shared_ptr<const Data> get() const {
            return m_data;
        }

        /// <summary>
        /// シーンを変更します。
        /// </summary>
        /// <param name="state">
        /// 次のシーンのキー
        /// </param>
        /// <param name="transitionTimeMillisec">
        /// フェードイン・アウトの時間(ミリ秒)
        /// </param>
        /// <param name="crossFade">
        /// クロスフェードを有効にするか
        /// </param>
        /// <returns>
        /// シーンの変更が可能でフェードイン・アウトが開始される場合 true, それ以外の場合は false
        /// </returns>
        bool changeScene(const State& state, s3d::int32 transitionTimeMillisec, bool crossFade) {
            if (state == m_currentState) {
                crossFade = false;
            }

            if (m_factories.find(state) == m_factories.end()) {
                return false;
            }

            m_nextState = state;

            m_crossFade = crossFade;

            if (crossFade) {
                m_transitionTimeMillisec = transitionTimeMillisec;

                m_transitionState = TransitionState::FadeInOut;

                m_next = m_factories[m_nextState]();

                if (hasError()) {
                    return false;
                }

                m_currentState = m_nextState;

                m_stopwatch.restart();
            }
            else {
                m_transitionTimeMillisec = (transitionTimeMillisec / 2);

                m_transitionState = TransitionState::FadeOut;

                m_stopwatch.restart();
            }

            return true;
        }

        /// <summary>
        /// フェードイン・アウトのデフォルトの色を設定します。
        /// </summary>
        /// <param name="color">
        /// フェードイン・アウトのデフォルトの色
        /// </param>
        /// <returns>
        /// なし
        /// </returns>
        SceneMaster& setFadeColor(const s3d::ColorF& color) {
            m_fadeColor = color;
            return *this;
        }

        /// <summary>
        /// フェードイン・アウトのデフォルトの色を取得します。
        /// </summary>
        /// <returns>
        /// フェードイン・アウトのデフォルトの色
        /// </returns>
        const s3d::ColorF& getFadeColor() const {
            return m_fadeColor;
        }

        /// <summary>
        /// Clientクラスの参照
        /// ISceneクラスで呼び出す
        /// </summary>
        ExitGames::LoadBalancing::Client& GetClient() {
            return m_loadBalancingClient;
        }

        /// <summary>
        /// エラーの発生を通知します。
        /// </summary>
        /// <remarks>
        /// この関数を呼ぶと、以降の SceneMaster::update() / updateAndDraw() が false を返します。
        /// </remarks>
        /// <returns>
        /// なし
        /// </returns>
        void notifyError() {
            m_error = true;
        }

    private:
        /// <summary>
        /// 継承したクラスはインターフェースなので以下のメンバ関数をオーバーライドする必要がある。
        /// また、以下のメンバ関数は全てコールバック関数なので勝手に呼び出される。
        /// </summary>

        virtual void debugReturn(int debugLevel, const ExitGames::Common::JString& string) override {
            if (m_current) {
                m_current->DebugReturn(debugLevel, string);
            }
        }

        virtual void connectionErrorReturn(int errorCode) override {
            m_current->ConnectionErrorReturn(errorCode);
        }

        virtual void clientErrorReturn(int errorCode) override {
            if (m_current) {
                m_current->ClientErrorReturn(errorCode);
            }
        }

        virtual void warningReturn(int warningCode) override {
            m_current->WarningReturn(warningCode);
        }

        virtual void serverErrorReturn(int errorCode) override {
            m_current->ServerErrorReturn(errorCode);
        }

        virtual void joinRoomEventAction(int playerNr, const ExitGames::Common::JVector<int>& playernrs, const ExitGames::LoadBalancing::Player& player) override {
            m_current->JoinRoomEventAction(playerNr, playernrs, player);
        }

        virtual void leaveRoomEventAction(int playerNr, bool isInactive) override {
            m_current->LeaveRoomEventAction(playerNr, isInactive);
        }

        virtual void customEventAction(int playerNr, nByte eventCode, const ExitGames::Common::Object& eventContent) override {
            m_current->CustomEventAction(playerNr, eventCode, eventContent);
        }

        virtual void connectReturn(int errorCode,
                                   const ExitGames::Common::JString& errorString,
                                   const ExitGames::Common::JString& region,
                                   const ExitGames::Common::JString& cluster) override {
            m_current->ConnectReturn(errorCode, errorString, region, cluster);
        }

        virtual void disconnectReturn() override {
            m_current->DisconnectReturn();
            m_usePhoton = false;
        }

        virtual void leaveRoomReturn(int errorCode, const ExitGames::Common::JString& errorString) override {
            m_current->LeaveRoomReturn(errorCode, errorString);
        }

        virtual void createRoomReturn(int localPlayerNr,
                                      const ExitGames::Common::Hashtable& roomProperties,
                                      const ExitGames::Common::Hashtable& playerProperties,
                                      int errorCode,
                                      const ExitGames::Common::JString& errorString) override {
            m_current->CreateRoomReturn(localPlayerNr, roomProperties, playerProperties, errorCode, errorString);
        }

        virtual void joinRandomRoomReturn(int localPlayerNr,
                                          const ExitGames::Common::Hashtable& roomProperties,
                                          const ExitGames::Common::Hashtable& playerProperties,
                                          int errorCode,
                                          const ExitGames::Common::JString& errorString) override {
            m_current->JoinRandomRoomReturn(localPlayerNr, roomProperties, playerProperties, errorCode, errorString);
        }
    };
}  // namespace mak1a

今書いた記事の中で知っておけばいい部分を紹介します。まずはSceneMasterのコンストラクタです。

/// <summary>
/// シーン管理を初期化します。
/// </summary>
/// <param name="appID_">
/// PhotonのappID
/// </param>
/// <param name="appVersion_">
/// バージョン
/// </param>
SceneMaster(const ExitGames::Common::JString& appID_, const ExitGames::Common::JString& appVersion_)
: m_data(s3d::MakeShared<Data>()), m_loadBalancingClient(*this, appID_, appVersion_), m_usePhoton(false) {}

コメントに書いてる通り、コンストラクタの引数でPhotonのappIDとバージョンを指定してます。appIDの取得方法については上述しました通りpara7さんの記事を参考にどうぞ。

ちなみにこのappIDをGitHub等にアップする事のないよう注意してください。私の場合は下記のように文字列変換関数を用いたり、.gitignoreで実装部分がcommitされないように隠したりしています。

このm_loadBalancingClientですが、第3引数を指定しない場合Photonサーバーへの接続に使用するプロトコルがUDPになります。もしTCPを使いたい場合は第3引数にPhoton::ConnectionProtocol::TCP,と書き込んでください。使い方は知りません。

/// <summary>
/// appIDを正常な文字列に直す
/// </summary>
/// <param name=str>修正前のappID</param>
/// <returns>正常なappID</returns>
/// <remarks>
/// 実装部分は書きません。ご了承ください。
/// </remarks>
[[nodiscard]] ExitGames::Common::JString ChangeAppIDString(s3d::String str);

次にISceneクラスの紹介です。OpenSiv3Dのゲームテンプレートを利用してる方は分かると思いますが、ISceneクラスは各シーンで継承します。

/// <summary>
/// SceneMasterクラスのコールバック関数が呼び出されたら下記のメンバ関数が呼び出される
/// 各シーンでオーバーライドする事により、そのメンバ関数の実装が書ける。
/// </summary>

virtual void DebugReturn(int /*debugLevel*/, const ExitGames::Common::JString& /*string*/) {}

virtual void ConnectionErrorReturn(int /*errorCode*/) {}

virtual void ClientErrorReturn(int /*errorCode*/) {}

virtual void WarningReturn(int /*warningCode*/) {}

virtual void ServerErrorReturn(int /*errorCode*/) {}

virtual void JoinRoomEventAction(int /*playerNr*/, const ExitGames::Common::JVector<int>& /*playernrs*/, const ExitGames::LoadBalancing::Player& /*player*/) {}

virtual void LeaveRoomEventAction(int /*playerNr*/, bool /*isInactive*/) {}

virtual void CustomEventAction(int /*playerNr*/, nByte /*eventCode*/, const ExitGames::Common::Object& /*eventContent*/) {}

virtual void ConnectReturn(int /*errorCode*/,
                           const ExitGames::Common::JString& /*errorString*/,
                           const ExitGames::Common::JString& /*region*/,
                           const ExitGames::Common::JString& /*cluster*/) {}

virtual void DisconnectReturn() {}

virtual void LeaveRoomReturn(int /*errorCode*/, const ExitGames::Common::JString& /*errorString*/) {}

virtual void CreateRoomReturn(int /*localPlayerNr*/,
                              const ExitGames::Common::Hashtable& /*roomProperties*/,
                              const ExitGames::Common::Hashtable& /*playerProperties*/,
                              int /*errorCode*/,
                              const ExitGames::Common::JString& /*errorString*/) {}

virtual void JoinRandomRoomReturn(int /*localPlayerNr*/,
                                  const ExitGames::Common::Hashtable& /*roomProperties*/,
                                  const ExitGames::Common::Hashtable& /*playerProperties*/,
                                  int /*errorCode*/,
                                  const ExitGames::Common::JString& /*errorString*/) {}

上記のメンバ関数はコメントの通りSceneMasterクラスのコールバック関数が呼び出されたタイミングでそれぞれ呼び出されます。例えば、DisconnectReturn() はSceneMasterクラスの disconnectReturn() がコールバックで呼び出されたタイミングで呼ばれます。
ユーザ側が呼び出されたタイミングで何か処理をしたい場合は各シーンでオーバーライドしてください

次はPhotonと接続、切断をするメンバ関数です。

/// <summary>
/// Photonと接続する
/// </summary>
/// <returns>
/// なし
/// </returns>
virtual void Connect() {
    m_manager->GetClient().setAutoJoinLobby(true);

    if (!m_manager->GetClient().connect(ExitGames::LoadBalancing::AuthenticationValues().setUserID(ExitGames::Common::JString() + GETTIMEMS()))) {
        return;
    }

    m_manager->UsePhoton(true);
}

/// <summary>
/// Photonから切断する
/// </summary>
/// <returns>
/// なし
/// </returns>
virtual void Disconnect() {
    m_manager->GetClient().disconnect();
}

各シーンクラスで呼び出したいタイミングでそれぞれConnect(); Disconnect();と書けば接続、切断ができます。
また、公式チュートリアルで書いてる通りserviceを常に呼び出す必要があります。

/// <summary>
/// serviceを呼び出す際にデバッグがしたい場合オーバーライドしてください
/// </summary>
/// <returns>
/// なし
/// </returns>
virtual void UpdatePhoton() {}

/// <summary>
/// serviceの呼び出し
/// UpdatePhotonも呼び出す
/// </summary>
/// <returns>
/// なし
/// </returns>
virtual void RunPhoton() {
    UpdatePhoton();
    m_manager->GetClient().service();
}

しかし、上記の通りSceneMasterクラスでPhotonが使用された際は毎フレームRunPhoton()が呼び出されます。これにより、各シーンで呼び出さなくて大丈夫になります。

さらに、Photonの機能をもっとフルに活用する為にSceneMasterクラスで宣言したClientクラスを各シーンで呼び出せるようにしました。

/// <summary>
/// Clientクラスの参照
/// ISceneクラスで呼び出す
/// </summary>
ExitGames::LoadBalancing::Client& GetClient() {
    return m_loadBalancingClient;
}

例えばマッチングシーンで部屋に参加したい!という場合は、マッチングシーン内でGetClient().opJoinRandomRoom(getData().GetCustomProperties(), 2);という風に記述します。

実際にSceneMasterを使う

OpenSiv3Dのゲーム開発のテンプレートusing MyApp = SceneManager<State, GameData>;
using MyApp = mak1a::SceneMaster<State, GameData>;と変更し、Main関数のMyApp manager;
MyApp manager(L"<no-app-id>", L"1.0");とします。L""は各自のappIDを入力してください。繰り返しますが、くれぐれもGitHubや外部に出さないよう注意してください。

基本的にはテンプレートとほとんど同じようにコードを記述しますが、コールバック関数が呼び出された際の処理を記述する為には前述の通り各シーンクラスでオーバーライドする必要があります。例として、Photonに接続して部屋を検索し、部屋が見つかれば参加し見つからなければ部屋を作成する という処理を記述してみます。今回はMatchシーンとしておきます。
まずはGameDataクラスの書き換えをします。

Common.hpp
/// <summary>
/// ゲームデータ
/// </summary>
class GameData {
private:
    // ランダムルームのフィルター用
    ExitGames::Common::Hashtable m_hashTable;

public:
    GameData() {
        m_hashTable.put(L"version", L"1.0");
    }

    ExitGames::Common::Hashtable& GetCustomProperties() {
        return m_hashTable;
    }
};

それではMatchシーンの実装を記述します。

Match.hpp
class Match : public MyApp::Scene {
private:
    void ConnectReturn(int errorCode, const ExitGames::Common::JString& errorString, const ExitGames::Common::JString& region, const ExitGames::Common::JString& cluster) override {
        if (errorCode) {
            s3d::Print(U"接続出来ませんでした");
            changeScene(State::Title);  // タイトルシーンに戻る
            return;
        }

        s3d::Print(U"接続しました");
        GetClient().opJoinRandomRoom(getData().GetCustomProperties(), 2);   // 第2引数でルームに参加できる人数を設定します。
    }

    void DisconnectReturn() override {
        s3d::Print(U"切断しました");
        changeScene(State::Title);  // タイトルシーンに戻る
    }

    void CreateRoomReturn(int localPlayerNr,
                          const ExitGames::Common::Hashtable& roomProperties,
                          const ExitGames::Common::Hashtable& playerProperties,
                          int errorCode,
                          const ExitGames::Common::JString& errorString) override {
        if (errorCode) {
            s3d::Print(U"部屋を作成出来ませんでした");
            Disconnect();
            return;
        }

        s3d::Print(U"部屋を作成しました!");
    }

    void JoinRandomRoomReturn(int localPlayerNr,
                              const ExitGames::Common::Hashtable& roomProperties,
                              const ExitGames::Common::Hashtable& playerProperties,
                              int errorCode,
                              const ExitGames::Common::JString& errorString) override {
        if (errorCode) {
            s3d::Print(U"部屋が見つかりませんでした");

            CreateRoom(L"", getData().GetCustomProperties(), 2);

            s3d::Print(U"部屋を作成します...");
            return;
        }

        s3d::Print(U"部屋に接続しました!");
        // この下はゲームシーンに進んだり対戦相手が設定したりする処理を書きます。今回はゲームシーンに進む事にします。
        changeScene(State::Game);  // ゲームシーンに進む
    }

    void JoinRoomEventAction(int playerNr, const ExitGames::Common::JVector<int>& playernrs, const ExitGames::LoadBalancing::Player& player) override {
        // 部屋に入室したのが自分の場合、早期リターン
        if (GetClient().getLocalPlayer().getNumber() == player.getNumber()) {
            return;
        }

        s3d::Print(U"対戦相手が入室しました。");
        // この下はゲームシーンに進んだり設定したりする処理を書きます。今回はゲームシーンに進む事にします。
        changeScene(State::Game);  // ゲームシーンに進む
    }

    void LeaveRoomEventAction(int playerNr, bool isInactive) override {
        s3d::Print(U"対戦相手が退室しました。");
        changeScene(State::Title);  // タイトルシーンに戻る
    }

    /// <summary>
    /// 今回はこの関数は必要ない(何も受信しない為)
    /// </summary>
    void CustomEventAction(int playerNr, nByte eventCode, const ExitGames::Common::Object& eventContent) override {
        s3d::Print(U"受信しました");
    }

public:
    Match(const InitData& init) {
        Connect();
        s3d::Print(U"接続中...");

        GetClient().fetchServerTimestamp();
    }

    void update() override {
        if (s3d::KeySpace.down()) {
            Disconnect();
        }
    }

    void draw() const override {}
};

後はGameシーンでGetClient().opRaiseEvent(true, dic, 2);のようにdicに入ってる情報を送信し、オーバーライドしたCustomEventAction関数で受信します。送信や受信の方法については公式チュートリアルもしくは公式リファレンスを参考にしてください。

実際にPhotonを使って将棋ゲームをオンライン化してみた

実際にプレイしたデモ動画を載せます。友達がいなかったから一人二役で動画取りました。孤高の存在ってカッコいいですよね。うん。

こちらの動画のソースコードは私のGitHubで公開しています。もちろんappIDは伏せてるのでご了承ください。

最後に

もし分からないところやここのコードこうした方がいいのでは?という指摘がございましたらご連絡ください。

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
3
Help us understand the problem. What are the problem?