この記事はSiv3d Advent Calendar 2021 24日目の記事です。
はじめに
今年の3月にOpenSiv3D Challenge 2021と題してOpenSiv3Dの新機能を作る企画が建てられました。その中の一つである、Challenge 12 | Photon を使ったオンライン対戦ゲーム開発の資料整備の進捗と今後について書きたいなと思います。
Challenge 12の概要(SivPhoton計画)
OpenSiv3Dでのオンラインゲーム製作について
昨年私の記事でPhoton Realtime SDKとOpenSiv3Dを連携してオンラインゲームを製作した話をしましたが、この時に製作したライブラリはhppファイルにPhotonのファイルをincludeしていました。また、完全にOpenSiv3Dのシーンマネージャークラスの中に組み込んでいた為、シーンマネージャーを用いずにPhotonを使用する事ができませんでした。そこで今回の目的は、
-
Photonとシーンマネージャーを分離し、Photonクラス単体として使用できるようにする
-
なるべくPhoton SDKをhppではなくcppのみでincludeするように変更する
という二つが大きな目的です。
その他の変更点
hppにPhoton SDKをincludeしないという仕様上、基本的にPhoton SDKで提供されているクラスや関数はユーザが触る事ができません。そこで、一旦「ユーザはOpenSiv3Dのクラスだけでデータのやり取りができる」ような設計にしました。
リファレンスについて
現状OpenSiv3DとPhotonのリファレンスは存在しておらず、記事についても片手で数える程度しか存在しません。その為、リファレンスも作成してSivPhoton利用者を増やそうという話になっております。リファレンスについては現在Lukeさんに製作していただいておりますので今しばらくお待ちください(上記のシーンマネージャーと連携したライブラリのリファレンスは既に作成していただきました!)。Lukeさんありがとうございます!
SivPhotonのセットアップについて
OpenSiv3Dのバージョンについて
元々v0.4.3向けに製作していましたが、今年秋にOpenSiv3Dv0.6.xが提供された事を受け、基本的にはv0.6.x向けに製作しています。一応v0.4.3でも動作はできますが、下記のサンプルコード(Main.cpp)の一部分はv0.6.x用のコードとなっておりますのでご注意ください。
Photon SDKのバージョンについて
Photonのバージョンについてですが、こちらはv4.xからv5.xで様々な修正がされており、メンバ関数名等の変更もありましたので必ずv5.xをダウンロードするようにお願いします。というかPhoton様お願いだからサイレントアプデしないで...
SivPhotonのパスについて
上記の記事での設定からの変更点がいくつかありますので紹介します。
Windowsについて
(今回はダウンロードしたフォルダ名をPhotonにし、Documents に置いた場合を示してます。)
「プロパティ」→「C/C++」→「全般」→「追加のインクルードディレクトリ」に下記を追加します。
C:\Users\ユーザ名\Documents\Photon
次に、「リンカー」→「全般」→「追加のライブラリディレクトリ」に下記を追加します。
C:\Users\ユーザ名\Documents\Photon
これだけ設定すれば、Windowsで開発する際はSivPhotonが使用できます。気づいた方もいると思いますが、
-
「追加のインクルードディレクトリ」と「追加のライブラリディレクトリ」のパスが同じ
-
「追加の依存ファイル」にパスを追加する必要がなくなった
という2点が修正されました。一応仕組みを説明しますと、前まで追加の依存ファイルにパスを追加しないといけませんでしたが、今回cppファイル(Photon SDKをincludeしているファイル)に下記のような記述をしています。
# if SIV3D_PLATFORM(WINDOWS)
# if SIV3D_BUILD(DEBUG)
# pragma comment (lib, "Common-cpp/lib/Common-cpp_vc16_debug_windows_mt_x64")
# pragma comment (lib, "Photon-cpp/lib/Photon-cpp_vc16_debug_windows_mt_x64")
# pragma comment (lib, "LoadBalancing-cpp/lib/LoadBalancing-cpp_vc16_debug_windows_mt_x64")
# else
# pragma comment (lib, "Common-cpp/lib/Common-cpp_vc16_release_windows_mt_x64")
# pragma comment (lib, "Photon-cpp/lib/Photon-cpp_vc16_release_windows_mt_x64")
# pragma comment (lib, "LoadBalancing-cpp/lib/LoadBalancing-cpp_vc16_release_windows_mt_x64")
# endif
# endif
pragma comment(lib, "パス")と記述することで「追加の依存ファイル」にパスを追加してくれます。意外に使いどころがあるかもなので覚えておくと良いことあるかもです。
macOSについて
(今回はダウンロードしたフォルダ名をPhotonにし、/usr/local/ に置いた場合を示してます。)
上記の記事と変更点は殆どありませんが、バージョン更新により
-
「Other Linker Flags」に
-lcrypto_$(CONFIGURATION)_$(PLATFORM_NAME)
を追記 -
「Library Search Paths」に
/usr/local/Photon-MacOSX-Sdk_v5-0-1-1/3rdparty/lib/apple
を追記
の2点が修正されています。ご注意ください。
SivPhotonの利用方法
今回はライブラリの中身は開発途中なのでお見せできませんが、現段階でのサンプルコードを載せときます。
サンプルコード
# include <Siv3D.hpp> // OpenSiv3D v0.6.3
# include "NetworkSystem.hpp"
# include "ENCRYPTED_PHOTON_APP_ID.SECRET"
class MyNetwork : public SivPhoton
{
public:
using SivPhoton::SivPhoton;
void connectReturn(int32 errorCode, const String& errorString, const String& region, const String& cluster) override
{
Print << U"MyNetwork::connectReturn() [サーバへの接続に成功したときに(?) 呼ばれる]";
Print << U"- error: " << errorString;
Print << U"- region: " << region;
Print << U"- cluster [サブ的な region]: " << cluster;
if (errorCode)
{
return;
}
if (MessageBoxResult::Yes == System::MessageBoxYesNo(U"新しくルームを作りますか?", MessageBoxStyle::Question))
{
const String roomName = m_defaultRoomName;
this->opCreateRoom(roomName, MaxPlayers);
}
}
void connectionErrorReturn(const int32 errorCode) override
{
Print << U"MyNetwork::connectionErrorReturn() [サーバへの接続が失敗したときに呼ばれる]";
Print << U"errorCode: " << errorCode;
}
void joinRandomRoomReturn(const int32 localPlayerID, const int32 errorCode, const String& errorString) override
{
Print << U"MyNetwork::joinRandomRoomReturn() [既存のランダムなルームに参加した結果を処理する]";
Print << U"- localPlayerID [失敗した場合は 0 (?)]: " << localPlayerID;
Print << U"- errorCode: " << errorCode;
Print << U"- errorString: " << errorString;
if (errorCode == NetworkSystem::NoRandomMatchFound) // 100% こうなるはず
{
Print << U"[既存のルームが見つからなかった]";
const String roomName = m_defaultRoomName;
// 自分でルームを新規作成する
this->opCreateRoom(roomName, MaxPlayers);
}
}
void joinRoomReturn(int32 localPlayerID, int32 errorCode, const String& errorString) override
{
Print << U"MyNetwork::joinRoomReturn() [指定したルームに参加した結果を処理する]";
Print << U"- localPlayerID [失敗した場合は 0 (?)]: " << localPlayerID;
Print << U"- errorCode: " << errorCode;
Print << U"- errorString: " << errorString;
if (errorCode)
{
Print << U"[指定したルームに参加できなかった]";
return;
}
Print << U"[指定したルームに参加できた]";
}
/// @brief 受信のコード(サンプル)
/// @param playerID 送信したプレイヤーのID
/// @param eventCode イベントコード(番号によって意味を持たせたい場合有用)
/// @param eventContent 受信した本体
void customEventAction(const int32 playerID, const int32 eventCode, const Vec2& eventContent) override
{
Print << U"MyNetwork::customEventAction(Vec2)";
Print << U"playerID: " << playerID;
Print << U"eventCode: " << eventCode;
Print << U"eventContent: " << eventContent;
}
/// @brief 受信のコード(サンプル)
/// @param playerID 送信したプレイヤーのID
/// @param eventCode イベントコード(番号によって意味を持たせたい場合有用)
/// @param eventContent 受信した本体
void customEventAction(const int32 playerID, const int32 eventCode, const double eventContent) override
{
Print << U"MyNetwork::customEventAction(double)";
Print << U"playerID: " << playerID;
Print << U"eventCode: " << eventCode;
Print << U"eventContent: " << eventContent;
}
/// @brief 受信のコード(Gridサンプル)
/// @param playerID 送信したプレイヤーのID
/// @param eventCode イベントコード(番号によって意味を持たせたい場合有用)
/// @param eventContent 受信した本体
void customEventAction(const int32 playerID, const int32 eventCode, const Grid<Point>& eventContent) override
{
Print << U"MyNetwork::customEventAction(Grid<Point>)";
Print << U"playerID: " << playerID;
Print << U"eventCode: " << eventCode;
Print << U"eventContent: " << eventContent;
}
/// @brief 受信のコード(Gridサンプル)
/// @param playerID 送信したプレイヤーのID
/// @param eventCode イベントコード(番号によって意味を持たせたい場合有用)
/// @param eventContent 受信した本体
void customEventAction(const int32 playerID, const int32 eventCode, const Grid<double>& eventContent) override
{
Print << U"MyNetwork::customEventAction(Grid<double>)";
Print << U"playerID: " << playerID;
Print << U"eventCode: " << eventCode;
Print << U"eventContent: " << eventContent;
}
// サンプル(誰か入ってきたら退室)
//void joinRoomEventAction(int32 localPlayerID, const Array<int>& playerIDs, bool isSelf) override
//{
// if (isSelf) // 自分だったら
// {
// return;
// }
// constexpr bool willComeBack = true;
// this->opLeaveRoom(willComeBack);
//}
private:
static constexpr int32 MaxPlayers = 2;
};
void Main()
{
Window::Resize(1280, 720);
const String encryptedAppID = ENCRYPTED_PHOTON_APP_ID;
const String appID = NetworkSystem::DecryptPhotonAppID(encryptedAppID);
MyNetwork network{ appID, U"1.0" };
network.connect(U"Siv");
const Font font{ 18 };
while (System::Update())
{
font(U"getRoomNameList: {}"_fmt(network.getRoomNameList())).draw(520, 270);
font(U"getName: {}"_fmt(network.getName())).draw(520, 300);
font(U"getUserID: {}"_fmt(network.getUserID())).draw(520, 330);
font(U"isInRoom: {}"_fmt(network.isInRoom())).draw(520, 360);
font(U"getCurrentRoomName: {}"_fmt(network.getCurrentRoomName())).draw(520, 390);
font(U"getPlayerCountInCurrentRoom: {}"_fmt(network.getPlayerCountInCurrentRoom())).draw(520, 420);
font(U"getMaxPlayersInCurrentRoom: {}"_fmt(network.getMaxPlayersInCurrentRoom())).draw(520, 450);
font(U"getIsOpenInCurrentRoom: {}"_fmt(network.getIsOpenInCurrentRoom())).draw(520, 480);
font(U"localPlayerID: {}"_fmt(network.localPlayerID())).draw(520, 510);
font(U"isMasterClient: {}"_fmt(network.isMasterClient())).draw(520, 540);
font(U"getNumber: {}"_fmt(network.getNumber())).draw(520, 570);
if (network.isMasterClient())
{
Scene::SetBackground(Palette::Skyblue);
}
else
{
Scene::SetBackground(Palette::DefaultBackground);
}
if (SimpleGUI::Button(U"Raise Event GridPoint", Vec2{ 800, 20 }))
{
network.opRaiseEvent(33, Grid<Point>{2, 1, Array<Point>{ Point(1280, 720), Point(640, 320) } });
}
if (SimpleGUI::Button(U"Raise Event GridDouble", Vec2{ 800, 70 }))
{
network.opRaiseEvent(34, Grid<double>{2, 1, Array<double>{3.3, 5.5}});
}
if (not network.isInRoom())
{
const Array<String> roomList = network.getRoomNameList();
for (auto [i, roomName] : Indexed(roomList))
{
if (SimpleGUI::Button(roomName, Vec2{ 600, 20 + i * 40 }))
{
network.opJoinRoom(roomName);
}
}
}
if (SimpleGUI::Button(U"Leave Room", Vec2{ 600, 180 }))
{
network.opLeaveRoom();
}
if (SimpleGUI::Button(U"Disconnect", Vec2{ 600, 220 }))
{
network.disconnect();
}
network.update();
}
}
MyNetworkについて
MyNetworkクラスはSivPhotonクラスを継承します。MyNetworkクラスは基本的に送受信を行うクラスとなります。送信については、SivPhotonクラスのメンバ関数(op〜という名前のやつとか、get〜という名前のやつ等)を使います。Main関数内では、network.opRaiseEvent(...);
やnetwork.opLeaveRoom()
は送信のコードとなっております。受信のコードは、SivPhotonクラスの中で自分で処理を書きたいものをオーバーライドします。例えば、
void connectReturn(int32 errorCode, const String& errorString, const String& region, const String& cluster) override
{
Print << U"MyNetwork::connectReturn() [サーバへの接続に成功したときに(?) 呼ばれる]";
Print << U"- error: " << errorString;
Print << U"- region: " << region;
Print << U"- cluster [サブ的な region]: " << cluster;
if (errorCode)
{
return;
}
if (MessageBoxResult::Yes == System::MessageBoxYesNo(U"新しくルームを作りますか?", MessageBoxStyle::Question))
{
const String roomName = m_defaultRoomName;
this->opCreateRoom(roomName, MaxPlayers);
}
}
等のoverride修飾子がついているメンバ関数が受信のコードです。
customEventActionについて
以前書いた記事でのcustomEventActionについてですが、以前はExitGames::Common::Object
を引数として受け取っていましたが、今回のライブラリはユーザがPhoton SDKを直接触る事ができなくなってます。その為、ユーザが送信したデータの型をそのまま引数として受け取れるようにしています。例えばs3d::Vec2型を送信した場合、customEventActionの引数eventContentの型をs3d::Vec2型にするとそのまま受け取る事ができます。受け取れる型については現在プリミティブ型(int, double, float, bool), s3d::String, s3d::Point, s3d::Vec2, s3d::Rect, s3d::Circle
を受け取る事ができます。また、上記の型はs3d::Array、s3d::Grid
で送受信する事もできます。
SceneManagerとの連携
実は現在、アーリーアクセス版を利用してくれてる方がいまして、その方から「OpenSiv3DにあるSceneManagerと連携したい」との相談がありました。実際私も今まで作ったオンラインゲームは全て自作のシーンマネージャークラス(Photonと連携したクラス)を使用していましたし、需要はあるのかなと思います。
という事で、現段階のSivPhotonとSceneManagerを連携したライブラリを公開します(SivPhotonはまだ公開しませんが)。使ってみたい!という方は、Siv3DのSlackチャンネルの「ch12-photon」にて言っていただければと思います。
上記のgistはファイルが二つあります(hppとcpp)ので、使用する際は必ず二つともダウンロードしてください。
使い方については、多少仕様が変わっていますが基本的にこちらのリファレンスをご覧いただければと思います。Lukeさん本当に助かってます!ありがとうございます!
終わりに
という訳で今回はさくっと説明だけで終わってしまいますが、来年以降しっかりとした形で記事やライブラリを公開できればと思いますので、今しばらくお待ちいただければと思います。ライブラリについてはcustomEventActionで送受信できるクラスを増やしていき、一定数確保できたタイミングで公開したいと思います。