· 

マルチプレイ対戦と課金機能を実装するまでの過程話 - Unity【とある個人開発者の作業進捗報告 2023年9月-10月分】【一部コード解説付きPUN2での実装例/Stripe活用簡単お手軽購入・決済実装方法】

Unityアイコン

 マルチプレイ対戦と課金機能を実装するまでの過程話 | とある個人開発者の作業進捗報告

個人開発者の日常

開発歴4年以上のハヤデビがお送りする、ものづくりの日々。

こんにちは、みなさん!最近のお仕事や作業の進捗はどうですか?
私は色々と頼まれごとが重なって、ここ2週間はほぼ毎日徹夜状態です。。orz

。。さてそんなブラックな作業話はさておき、今回のブログ記事内容としては

トピック:

とある個人開発者の作業進捗報告

(2023年9月-10月分)

。。について、お話ししてまいりましょう。
これから話す順番としては以下の通りです。

■報告分期間中の主な作業時間割り当て優先度合

■Unityでの開発作業進捗報告

■配信済みなアプリゲーム等や公式サイトの運営話

■報告分期間中の依頼例や依頼件数

■終わりに

途中、より分かりやすく伝えれるように一部スクリプト内容の軽い解説もいたしますので、

良ければ最後まで見ていってくださいませ。


■主な作業時間割り当て優先度合

-作業割合-

*自由な時間での作業割り当て優先度合

・個人のゲーム開発(ツイート対象)……1.5割

・配信済みアプリゲームや公式サイト等の運営……2.5割

・ココナラ等での収益に大きく関わる仕事作業……5割

・その他上記に含まれない作業……1割

 

作業優先度合い円グラフ

-割合概要-

9月後半から10月にかけて依頼件数が増え始め、依頼を終えてもまたすぐ埋まる状態がしばらく続いておりました;

そのため、個人の開発時間が中々とることができなかった感じですね。。

また、丁度2匹はスライム兄弟メーカーのイベント期間とも重なっていたので、その運営開発作業と各公開先への配信準備にも時間を要してしまいました。

 

11月からは運営含めても開発の余裕ができるように、ココナラでの活動方針を変えたので、少なくとも週に3-4回のツイートができれば…と考えています;


■Unityでの開発作業進捗報告

忍者蛙と小さな騒音用に、Photon Unity Networking 2(PUN2)を用いたマルチプレイ対戦モードの実装作業と動作テスト。

■収益化に向けて、Stripe統合とgreeのWebView、ニフクラのデータストアを組み合わせた課金機能の実装と、WebGLビルドでDショップを開発。

2匹はスライム兄弟メーカーでの運営・開発。

■その他ご依頼内容等の開発作業

9-10月は主に上記開発作業をしておりました。

各作業ごとに順を追って見せていきましょう。

■PUN2を用いたマルチプレイ対戦モードの実装

マルチプレイの実装といっても、PUN2やMLAPI、Netcode等々…調べてみると魅力的な実装方法がいくつかあったため、

事前に別環境で、それぞれの実装手段での試作もしていました。

最終的には、不測の事態でも対応しやすい、ドキュメントや記事が圧倒的に豊富なPUN2を用いることに決めた感じです。

以下より表示するコード内容は、そんなPUN2を用いて実装して、実際に忍者蛙と小さな騒音で使っているマルチプレイにおいてのルーム等を管理するスクリプトです。

※外部スクリプトの値も一部も参照しているため、コピペだけでは実装できません。
 あくまで参考程度に。

▲のボタンをクリックして表示されるサイトマップから再びこのページに戻ると、
記事の続きが表示されます


メソッドごとに軽く解説していくと…

NormalRoomMatch()は(忍者蛙と小さな騒音においての)通常対戦ルールを始めるうえで最初に呼び出す必要があるメソッドです。

後の接続成功時のために、先にnew TypedLobbyでルームを詳細に設定して、

PhotonNetwork.ConnectUsingSettings();でマスターサーバへの接続を試みます。

  1.     public void NormalRoomMatch()//ノーマルモード
  2.     {
  3.         if (playername.text == "") playername.text = "Player";
  4.         PhotonNetwork.NickName = playername.text;
  5.         sql_roomtype = "Normal";
  6.         sqlLobby = new TypedLobby(sql_roomtype, LobbyType.SqlLobby);
  7.         if (audioSource && mese) audioSource.PlayOneShot(mese);
  8.         if (GManager.instance.isEnglish == 0)
  9.         {
  10.             currentstatus.fontSize = 14;
  11.             currentstatus.text = "対戦相手が見つかるを待っています。";
  12.         }
  13.         else if (GManager.instance.isEnglish != 0)
  14.         {
  15.             currentstatus.fontSize = 12;
  16.             currentstatus.text = "We are waiting for you to find an opponent.";
  17.         }
  18.         matchfrogname[0].text = PhotonNetwork.NickName;
  19.         matchfrogimg[0].sprite = GManager.instance.all_frog[GManager.instance.set_playerselect].frog_image;
  20.         matchanim.SetInteger("trg", 1);
  21.         // 未接続の場合は接続する
  22.         PhotonNetwork.ConnectUsingSettings();
  23.     }

接続に成功すると、OnConnectedToMaster()が呼ばれます。

new RoomOptions()からルーム内容も設定します。

PhotonNetwork.JoinRandomOrCreateRoom、またはPhotonNetwork.JoinOrCreateRoomでルームの作成や参加に試みる際、先程new TypedLobbyでした設定も適用することができます。

これで人数を絞ったルームや、モードごとにマッチング出来たりするのです。

  1.     // マスターサーバーへの接続が成功した時に呼ばれるコールバック
  2.     public override void OnConnectedToMaster()
  3.     {
  4.         // ルームの参加人数を2人に設定する
  5.         roomOptions = new RoomOptions();
  6.         roomOptions.MaxPlayers = 2;
  7.         if (roomname.text == "") PhotonNetwork.JoinRandomOrCreateRoom(null, 2, MatchmakingMode.FillRoom, sqlLobby, null, null, roomOptions);
  8.         else PhotonNetwork.JoinOrCreateRoom(roomname.text, roomOptions, sqlLobby);
  9.         //PhotonNetwork.GetCustomRoomList(sqlLobby, sql_roomtype);
  10.     }
  1.     // ランダムで参加できるルームが存在しないなら、新規でルームを作成する
  2.     public override void OnJoinRandomFailed(short returnCode, string message)
  3.     {
  4.         // ルームの参加人数を2人に設定する
  5.         var roomOptions = new RoomOptions();
  6.         roomOptions.MaxPlayers = 2;
  7.         PhotonNetwork.CreateRoom(null, roomOptions, sqlLobby);
  8.     }

ルーム、ゲームサーバーへの接続に成功するとOnJoinedRoom()が呼ばれます。

ここでは開発ゲームに合う適した処理をすればよいと思います。

忍者蛙と小さな騒音の場合は取得や設定、満員後に別メソッドMatchPlay()を呼び出して、対戦を開始しようとしていますね。

  1.     // ゲームサーバーへの接続が成功した時に呼ばれるコールバック
  2.     public override void OnJoinedRoom()
  3.     {
  4.         PlayerPrefs.SetString("multiname", playername.text);
  5.         PlayerPrefs.Save();
  6.         //自分の蛙情報を登録
  7.         if (PhotonNetwork.IsConnected && PhotonNetwork.InRoom)
  8.         {
  9.             // ルームのID(名前)を取得
  10.             roomID = PhotonNetwork.CurrentRoom.Name;
  11.             if (PhotonNetwork.LocalPlayer.IsMasterClient)
  12.             {
  13.                 thismatchindex = PhotonNetwork.LocalPlayer.ActorNumber;
  14.                 SetRoomCustomProperty("MasterFrog", GManager.instance.set_playerselect.ToString());
  15.                 selectstage = UnityEngine.Random.Range(0, MultiStages.Length);
  16.                 SetRoomCustomProperty("MasterStage", selectstage.ToString());
  17.             }
  18.             else
  19.             {
  20.                 thismatchindex = PhotonNetwork.LocalPlayer.ActorNumber;
  21.                 SetRoomCustomProperty(PhotonNetwork.LocalPlayer.ActorNumber.ToString()+ "Frog", GManager.instance.set_playerselect.ToString());
  22.                 object gameMode;
  23.                 if (PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue("MasterStage", out gameMode))
  24.                 {
  25.                     string tmpstage = gameMode.ToString();
  26.                     selectstage = int.Parse(tmpstage);
  27.                 }
  28.             }
  29.             //ここからマッチとステージの準備・生成
  30.             MultiStages[selectstage].stageall.SetActive(true);
  31.             if (PhotonNetwork.LocalPlayer.IsMasterClient)
  32.             {
  33.                 var tmpselectstage = UnityEngine.Random.Range(0, MultiStages[selectstage].masterstartpos.Length);
  34.                 Vector3 position = MultiStages[selectstage].masterstartpos[tmpselectstage].position;
  35.                 MultiStages[selectstage].currentplobj = PhotonNetwork.Instantiate(MultiStages[selectstage].player.name, position, MultiStages[selectstage].player.transform.rotation);
  36.                 MultiStages[selectstage].currentplobj.name = "Player";
  37.             }
  38.             else
  39.             {
  40.                 var tmpselectstage = UnityEngine.Random.Range(0, MultiStages[selectstage].otherstartpos.Length);
  41.                 Vector3 position = MultiStages[selectstage].otherstartpos[tmpselectstage].position;
  42.                 MultiStages[selectstage].currentplobj = PhotonNetwork.Instantiate(MultiStages[selectstage].player.name, position, MultiStages[selectstage].player.transform.rotation);
  43.                 MultiStages[selectstage].currentplobj.name = "Player";
  44.             }
  45.             // ルームが満員になったら、以降そのルームへの参加を不許可にする
  46.             if (PhotonNetwork.CurrentRoom.PlayerCount == PhotonNetwork.CurrentRoom.MaxPlayers)
  47.             {
  48.                 GManager.instance.ismatch = true;
  49.                 Invoke(nameof(MatchPlay), 0.3f);
  50.                 PhotonNetwork.CurrentRoom.IsOpen = false;
  51.             }
  52.         }
  53.     }

この記事で挙げた箇所以外にはマッチから抜ける処理や、PUN2での変数等の取得方法もありますので、コード内容全体から参考にしてみてください。

(これ以上は長くなるし、あくまで今回は進捗報告なため他解説割愛)

Ads Image


■課金機能の実装とDショップ開発

実際のコードを見せたり解説したりすると、Dショップで不正行為される可能性が出てくるので。。

ここでは使用したものと実装する上での考え方だけを軽く説明します。

◆Stripe 統合|https://dashboard.stripe.com/dashboard

・参考記事&ドキュメント

https://stripe.com/docs

◆gree WebView|https://github.com/gree/unity-webview

・参考記事&ドキュメント

https://developers.wonderpla.net/entry/2020/06/11/102725

◆ニフクラ データストア|https://console.mbaas.nifcloud.com/login

・参考記事&ドキュメント

https://mbaas.nifcloud.com/doc/current/datastore/basic_usage_unity.html

当初はPlayFabを用いたPaypalでの決済方法でやろうとしていたのですが、そもそもPlayFabが自分の開発環境に合わず、さらにPaypalの方が2-3回の実装ミスで永久BANをくらったからです;

そのためPaypal決済の代用手段として目を付けたのが、Stripe統合にあるPayment Linksです。

最初に販売用アカウントとして書類や審査等の準備が必要ではありますが、Paypalと比べると審査が優しくて、ミスがあってもメールで丁寧に対応してくれました。

なので最初の準備は期間は要しつつも、問題なく進めれるかと思います。

審査後は大体の要素が使えるようになるので、Payment Linksで商品や課金用の決済リンクを作り、Unityでボタンからそのリンクへ飛べるようにするだけでDL商品の購入機能自体は完結します。

(その結果Dショップで有料ゲームを購入できるというわけです。)

 

ただ、課金機能も実装するとなるとどこかしらのタイミングでゲーム内課金アイテムのデータ、変数とやり取りする必要が出てくると思います。

Payment Linksは簡単な分、あくまで購入後に指定のリンクへ飛ぶだけなので、こちらで飛ぶリンク先で一工夫する必要があるのです。

その際に使用するのが、ニフクラのデータストアです。

これは私が普段のゲーム開発でもランキング機能などで使用している、変数の値やアカウント、共有ファイルといった管理ができるバックエンド手段です。

・初心者に比較的優しく、公式ドキュメントでもう十分に解説が充実

・フリープランでも十分に活用できて、ずっとフリーのままでも大丈夫

・UnityでのC#やJavaScript、その他にも幅広く対応している

今回の場合は、購入後のリンク先で"特定のアカウントが特定のゲーム内アイテムを課金した"という事実をJavaScriptでのデータストア経由で知らせる処理をして、Unityでのデータストアはアカウントと購入アイテム等が一致していればその知らせを受け取って、購入されたゲーム内アイテムを反映させる処理をさせています。

 

こうして文章として語るだけなら簡単そうに思う方もいるかもしれませんが、私の場合実際はかなりの試行錯誤と調整を重ねて1週間以上かかりました。

販売する側である以上、不具合、通信トラブルなどあってはならないですからね。。念入りに対策を重ねて実装できました;

 

WebViewに関しては、アプリ内で決済のリンクを開くためのものです。

いきなり別のアプリ、ブラウザが立ち上がると怖いので、アプリ内だけで完結させたいですからね。

 

以上!あとは上記内容からコード内容を考えて自分だけのショップを作ってみてね。

*法律等に気を付けてください


■2匹はスライム兄弟メーカーの運営開発

◆ナイトメアイベント(再)用の追加Evステージを作成

◆不具合修正等の細かい作業

◆アプリ内だけで完結する課金機能で、共通ゲーム内有償通貨デビコインを買えるようにした

運営用に追加で開発作業したのは上記の通りです。

イベントに関しては内容も少し改善しつつ、ステージを3つ追加しました。

課金機能はgreeのWebViewで表示して、アプリ内で完結させています。

他運営作業については、また次の項目で。。



■配信済みなアプリゲーム、公式サイト等の運営話

2匹はスライム兄弟メーカーの運営開発後は、各公開先へのアプデ配信の準備をしていました。

公開先がGooglePlay、DLsite、Itch.io、BOOTH、PLiCy、Dショップ、GC甲子園と多くあるため、

これだけに1日半以上は要する感じですね。。

あとは公式サイト内のDショップと連携して、イベントに合わせて特別な割引セールを実施したり、

最近では金土日の週末に向けて、毎週10%割引セールを実施しています。

公式サイトは、9-10月中も今回記事のようにブログ運営と、定期的なブラッシュアップを続けていました。

 


X運営垢

X:ハヤデビのゲーム工房運営用アカウント(@unity_hayadebi)

▲@hayadebiとは違い、配信済みゲームの運営に特化しているユーザー向けアカウント。

定期的なゲーム内イベントの開催についてや、その他お知らせ、あと割引セールもたまに実施しています。

 

Dショップ-top

ハヤデビのゲームショップ-Dショップ

▲分散してBOOTHやDLsiteに行かずとも、私の全有料作品やアーリーアクセス作品、共通ゲーム内通貨の課金などが全て集まってるハヤデビ作品専門ショップ。

毎週金曜日から日曜日の間の週末では、全商品10%割引のクーポンを適用できます。


■報告分期間中の依頼例や依頼件数

依頼仕事を受け付けているココナラ
での活動アカウント

主にUnityの開発サポートと、動画の撮影編集をサービスとして

承っております。

もし上記よりサービスに少しでも興味をお持ちいただけましたら、

どうぞいつでも、一度ココナラまでご相談いただけますと幸いです。

 

ここでは流石に、ご依頼の詳細は明かしません;

あくまで例と件数だけです。 

・依頼件数:7件

・実際の依頼例:▶=進行中,✔=納品完了

▶講座用のサンプルプロジェクト開発と解説執筆のご依頼×1

✔ご依頼者様開発作品用のPV作成 単体ご依頼×3

✔ご依頼者様開発作品テストプレイ+PV作成 同時ご依頼×1

✔一部機能の実装のご依頼×1

ゲームプロトタイプ作成のご依頼×1



■終わりに

以上!2023年9月-10月分の作業進捗報告についてでした。

ここまでの閲覧、ありがとうございました。

これからものんびりとブログも更新していきますので、お時間があればまた別の記事も見てみてくださいませ。

 

他開発者様方の皆さんも良き開発ライフを…!

それでは、また次の別の記事で。。

*本ページはプロモーションも含まれています。