20190821のUnityに関する記事は16件です。

Unityでデータベースと連携させたい!(Firebase編)

Unityをデータベースと連携させたいなと思い、とりあえずFirebaseを使用してScoreデータを取得、更新し、Scoreの値によってSphereの色が変わるサンプルコンテンツを作成しました!

forqiiita3.png

環境

PC: MacBook Air
debug: iPhone8 Plus

参照記事

Unity プロジェクトに Firebase を追加する

Unity の Firebase Realtime Database を使ってみる

How to use Firebase Database in Unity (REST API)

【Unity】スクリプトからGameObjectの色を変更する

【Unity】GetComponentの使い方と注意点

Can't compile: BadImageFormatException

iOS10とXcode8でプロビジョニングファイルを求められた


:baby_tone2:以下作業手順:baby_tone2:

その1

Unityプロジェクトを作成し、バンドルID を取得

まずは適当にUnityプロジェクト(3D)を準備します

  • 今回は最新正式リリースの2019.2.1f1を使用して新規プロジェクトを作成

  • iOSビルドは追加していなかったので、モジュール追加も実施

作成できたら、プロジェクトをクリックしてUnityで開き、プラットフォーム固有の ID を取得します

  • [File]->[Build Settings]でiOSプラットフォームを選択し、左下の[Player Settings...]を押下

  • [Player]タブの右下[Other Settings]をクリック

スクリーンショット 2019-08-21 13.17.14.png

  • [identification]欄の[Bundle Identifier]に記載されているバンドルID(cf: com.DefaultCompany.sample01)をコピーしておく

その2

Firebaseにプロジェクト追加

次に、Unityのデータベースとして使うFirebaseプロジェクトを作成します

  • Firebaseにプロジェクトを作成(今回は[forUnity]という名前で作成)

その3

FirebaseプロジェクトとUnityプロジェクトを連携

作成したFirebaseプロジェクトに、UnityプロジェクトのバンドルIDを登録します

  • Firebaseのコンソール画面に入り、先ほど作成したFirebaseプロジェクト[forUnity]を選択し、「開始するにはアプリを〜」の上に表示されているUnityアイコンをクリック (もし、画面が違う場合、左上のメニューバーから[Project Overview]をクリックしてUnityアイコン表示画面に移動)

スクリーンショット 2019-08-21 11.19.46.png

  • [Register as iOS app]にチェックを入れ、先ほどUnityでコピーしておいたバンドルIDを[iOS バンドルID]の欄に貼り付け

スクリーンショット 2019-08-21 13.25.38.png

  • 適当にアプリのニックネームを入力(省略可だけど今回は[サンプルアプリ]と入力)し、 [アプリを登録]をクリック

  • [GoogleService-Info.plist をダウンロード]をクリックし、保存先を今回作成したUnityプロジェクトのAssets配下の任意の場所(今回はAssets配下にFirebaseというフォルダを作成してその中)に保存し、[次へ]をクリック

  • [Firebase Unity SDK をダウンロード] をクリックし、ダウンロードしたZIPファイルを適切な場所で解凍(プロジェクト固有のものじゃないので、今回はSSDのアセットストアのデータを保存しているディレクトリと同階層に解凍)

  • 開いているUnityプロジェクトに行き、 [Assets]->[Import Package]->[Custom Package] をクリック、[dotnet4]->[FirebaseDatabase.unitypackage]を[open]

スクリーンショット 2019-08-21 14.19.48.png

  • [Import Unity Package] ウィンドウが出てくるので[import]をクリック

  • Firebaseに戻り、[次へ]をクリックで FirebaseプロジェクトとUnityプロジェクトの連携完了なので、[Firebaseコンソールに進む]をクリック

その4

Firebaseでデータベースを準備

FirebaseでRealtime Databaseを作成します

  • Firebaseコンソール画面左側メニューの[Database]をクリック

  • 少しスクロールして、 [または Realtime Database を選択] の[データベースの作成]->[テストモードで開始](※)->[有効にする]

※今回は簡単に連携させてみたいだけなのでテストモードで作成しますprz

スクリーンショット 2019-08-21 14.30.51.png

その5

Unity側でサンプルコンテンツを準備

UnityプロジェクトにFirebaseのデータベースと連携させるサンプルコンテンツを作成します

  •  [Create]->[UI] から、Text,Input Field,Buttonの3つを作成(w160,h30)し適当に横並びにする
  •  TextはTextを「Score:」,Input FieldはPlaceholderを「Your Name」,ButtonはTextを「Submit」に変更
  •  [Create]->[Create Empty]でGameObjectを作成し、[Add Component]から、「PlayerScores」と検索バーに入力し、[NewScript]->[Create and Add]をクリックし、PlayerScores という名前のスクリプトをGameObjectに作成&追加

スクリーンショット 2019-08-21 15.14.00.png

  • 作成したPlayerScores.csを編集
PlayerScores.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    public static int playerScore;
    public static string playerName;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
    }
}
  • ButtonのOnClick()にGameObjectをアサインし、PlayerScoresのOnSubmit()ファンクションを選択

スクリーンショット 2019-08-21 15.58.53.png

  • 次に、REST APIを投げられるように、Rest Client for Unity をAssetStore で検索し [Download] and [Import]

スクリーンショット 2019-08-21 16.12.58.png

  • importまで完了したら、ProjectフォルダのAssetsのなかで、右クリックし、[Create]->[C# Script]でUser.csを作成し、下記のように編集
User.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class User
{
    public string userName;
    public int userScore;

    public User(){
        userName = PlayerScores.playerName;
        userScore = PlayerScores.playerScore;
    }
}
  • PlayerScores.csも下記の箇所を編集
PlayerScores.cs
    public void OnSubmit(){
        playerName = nameText.text;
    }

↓ PostToDatabase()メソッドを追加し、OnSubmit()にも追記

PlayerScores.cs
    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    private void PostToDatabase(){
        User user = new User();
        RestClient.Post("https://forunity-e2ac2.firebaseio.com/.json",user);
    }
  • スクリプトの追加と修正が終わったら、GameObjectのPlayerScoresスクリプトのフィールドの部分に、Score TextにはScoreのTextオブジェクトを、Name TextにはInputFieldオブジェクトをそれぞれアサイン

スクリーンショット 2019-08-21 16.34.08.png

  • アサインできたら、Unityプロジェクトを実行し、Name欄に名前を入れて、Submitをクリックし、Firebaseのデータベースに値が入れば登録は成功

スクリーンショット 2019-08-21 16.45.43.png

  • 次に、firebaseからデータを取得するために、Buttonを複製し、Textを「Get Score」に変更

スクリーンショット 2019-08-21 17.48.29.png

  • PlayerScores.csのPlayerScoresクラス配下にOnGetScore(),UpdateScore(),RetrieveFromDatabase()メソッドを追加し、 User user = new User();を上の方で宣言
PlayerScores(修正後).cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Proyecto26;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    User user = new User();

    public static int playerScore;
    public static string playerName;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    public void OnGetScore(){
        RetrieveFromDatabase();
    }

    private void UpdateScore(){
        scoreText.text = "Score: " + user.userScore;
    }

    private void PostToDatabase(){
        User user = new User();
        //PostではなくPutに変更し、playerNameの配下にデータが入るようにした
        RestClient.Put("https://forunity-e2ac2.firebaseio.com/"+ playerName +".json",user);
    }

    private void RetrieveFromDatabase(){
        RestClient.Get<User>("https://forunity-e2ac2.firebaseio.com/"+ nameText.text +".json").Then(response => 
        {
            user = response;
            UpdateScore();
        });
    }
}

  • Name欄に登録ずみのNameを入力し、[Get Store]でScoreの表示されている値が登録済みのものにさし変わったら、データの取得も完了!

スクリーンショット 2019-08-21 17.48.29.png

最後に、Sphereを作成してscore値によって色を変化させる

  • PlayerScores.csのPlayerScoresクラス配下にChangeColorName()メソッドを追加し、public GameObject targetObject;を上の方で宣言し、UpdateScore(),PostToDatabase()に、引数にscoreを持たせてChangeColorName()を追記
PlayerScores(修正後).cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Proyecto26;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    User user = new User();

    public static int playerScore;
    public static string playerName;

    public GameObject targetObject;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    public void OnGetScore(){
        RetrieveFromDatabase();
    }

    private void UpdateScore(){
        scoreText.text = "Score: " + user.userScore;
        ChangeColorName(user.userScore);
    }

    private void PostToDatabase(){
        User user = new User();
        RestClient.Put("https://forunity-e2ac2.firebaseio.com/"+ playerName +".json",user);
        ChangeColorName(user.userScore);
    }

    private void RetrieveFromDatabase(){
        RestClient.Get<User>("https://forunity-e2ac2.firebaseio.com/"+ nameText.text +".json").Then(response => 
        {
            user = response;
            UpdateScore();
        });
    }

    private void ChangeColorName(int score){
        //Debug.Log(score);
        if(score < 25){
            // red
            targetObject.GetComponent<Renderer>().material.color = Color.red;
        } else if (score < 50){
            // yellow
            targetObject.GetComponent<Renderer>().material.color = Color.yellow;
        } else if (score < 75){
            // green
            targetObject.GetComponent<Renderer>().material.color = Color.green;
        } else {
            // blue
            targetObject.GetComponent<Renderer>().material.color = Color.blue;
        }

    }
}

  • [Create]->[3D Object]->[Sphere]でSphereを作成し、[GameObject]の[Target Object]にアサイン

スクリーンショット 2019-08-21 19.45.25.png

  • Sphereの色がScore値(0~100)によって下記仕様で色が変化すればOK
0~24: red
25~49: yellow
50~74: green
75~100: blue
  • まず、確認しやすくするためFirebaseのデータベースに入っているテストデータをいったん削除

  • 次に、Unityプロジェクトを実行し、Name欄に「Nobunaga」を入力して[Submit]し、Scoreに合った色であることを確認し実行解除、再度Unityプロジェクトを実行し、今度はName欄に「Ieyasu」を入力して[Submit]し、Scoreに合った色であることを確認

  • 確認できたら、Name欄に「Nobunaga」を入力して[Get Score]、正しいScoreと色であることを確認し、続けてName欄に「Ieyasu」を入力して[Get Score]、正しいScoreと色であることも確認できたらサンプルコンテンツ完成

スクリーンショット 2019-08-21 19.57.19.png

確認作業

iOSでビルドしてiPhone実機で確認

完成したUnityプロジェクトをiOS用にビルドして、実機で動作確認する

  • まずは [File]->[Build Settings]からiOSを選択して[Switch Platform]、完了したら、Sceneが追加されてなかったら、[Add Open Scene]でSceneを追加してから、[Build And Run]をクリック

スクリーンショット 2019-08-21 20.10.58.png

  • 保存場所を聞かれるのでSSDのUnityプロジェクトのディレクトリ配下に適当な名前をつけて[Save]するとビルドが始まる

スクリーンショット 2019-08-21 20.19.10.png

...エラー:scream_cat:

BadImageFormatException: Format of the executable (.exe) or library (.dll) is invalid.

調べてみたところ、どうやら保存先が外付けなのがよくない模様。。
試しに、Save先をPC側にして再度実行してみた、、が、同様のエラーに。。

なので今度はPC側に今回作成したUnityプロジェクトを丸っとコピーして、それをプロジェクトリストに追加してUnityエディタで開き、Unity上で問題なく動作することを確認してから、Save先をPC側にして再度実行

そしたら、Xcodeまでは無事行けたけれど、別のエラー

error: "Target名" requires a provisioning profile. Select a provisioning profile for the "Debug" build configuration in the project editor. (in target 'Target名')

これについては、Targetの[General]タブの[Signing]の[Automatically]にチェックを入れて、適切なTeamを選択してから、再度実行(▶︎)マークを押したら、無事にiPhone実機で確認できました!

IMG_F5A40749D0BA-1.jpeg

(※Sphereの位置がボタンとかよりも前過ぎたので、Unityで調節し直してから再度ビルドしたのですが、そこは本題ではないので割愛しますprz)


:alien:振り返り:alien:

またしてもSSD運用のデメリットが見つかってしまい悲しいです。。
とりあえず、 ビルドの段階になったら、

Unityプロジェクトを丸っとPC側にコピーして、プロジェクトリストにPC側にコピーしたものを追加してUnityエディタで開く
↓
BuildのSave先もPC側を指定して、Build And Runして実機確認
↓
諸々デバッグ・調整完了したら、PC側の修正を丸っとSSD側にコピーして反映

という運用で頑張ろうと思います。

コンテンツ自体はとても簡単なものではありますが、REST APIの部分でAssetを使ったこともあり、想像していたよりあっさり実装できたなと感じました。
今度は Rest Client for Unity を使わずに UnityWebRequest を使って実装してみたいです!

以上!

→ UnityWebRequest版 実装しました! 【続】Unityでデータベースと連携させたい!(Firebase編)

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでデータベースと連携させたい!(Firebase編)

Unityをデータベースと連携させたいなと思い、とりあえずFirebaseを使用してScoreデータを取得、更新し、Scoreの値によってSphereの色が変わるサンプルコンテンツを作成しました!

forqiiita3.png

環境

PC: MacBook Air
debug: iPhone8 Plus

参照記事

Unity プロジェクトに Firebase を追加する

Unity の Firebase Realtime Database を使ってみる

How to use Firebase Database in Unity (REST API)

【Unity】スクリプトからGameObjectの色を変更する

【Unity】GetComponentの使い方と注意点


:baby_tone2:以下作業手順:baby_tone2:

その1

Unityプロジェクトを作成し、バンドルID を取得

まずは適当にUnityプロジェクト(3D)を準備します

  • 今回は最新正式リリースの2019.2.1f1を使用して新規プロジェクトを作成

  • iOSビルドは追加していなかったので、モジュール追加も実施

作成できたら、プロジェクトをクリックしてUnityで開き、プラットフォーム固有の ID を取得します

  • [File]->[Build Settings]でiOSプラットフォームを選択し、左下の[Player Settings...]を押下

  • [Player]タブの右下[Other Settings]をクリック

スクリーンショット 2019-08-21 13.17.14.png

  • [identification]欄の[Bundle Identifier]に記載されているバンドルID(cf: com.DefaultCompany.sample01)をコピーしておく

その2

Firebaseにプロジェクト追加

次に、Unityのデータベースとして使うFirebaseプロジェクトを作成します

  • Firebaseにプロジェクトを作成(今回は[forUnity]という名前で作成)

その3

FirebaseプロジェクトとUnityプロジェクトを連携

作成したFirebaseプロジェクトに、UnityプロジェクトのバンドルIDを登録します

  • Firebaseのコンソール画面に入り、先ほど作成したFirebaseプロジェクト[forUnity]を選択し、左上のエリアに表示されているUnityアイコンをクリック (もし、画面が違う場合、左上のメニューバーから[Project Overview]をクリックしてUnityアイコン表示画面に移動)

スクリーンショット 2019-08-21 11.19.46.png

  • [Register as iOS app]にチェックを入れ、先ほどUnityでコピーしておいたバンドルIDを[iOS バンドルID]の欄に貼り付け

スクリーンショット 2019-08-21 13.25.38.png

  • 適当にアプリのニックネームを入力(省略可だけど今回は[サンプルアプリ]と入力)し、 [アプリを登録]をクリック

  • [GoogleService-Info.plist をダウンロード]をクリックし、保存先を今回作成したUnityプロジェクトのAssets配下の任意の場所(今回はAssets配下にFirebaseというフォルダを作成してその中)に保存し、[次へ]をクリック

  • [Firebase Unity SDK をダウンロード] をクリックし、ダウンロードしたZIPファイルを適切な場所で解凍(プロジェクト固有のものじゃないので、今回はSSDのアセットストアのデータを保存しているディレクトリと同階層に解凍)

  • 開いているUnityプロジェクトに行き、 [Assets]->[Import Package]->[Custom Package] をクリック、[dotnet4]->[FirebaseDatabase.unitypackage]を[open]

スクリーンショット 2019-08-21 14.19.48.png

  • [Import Unity Package] ウィンドウが出てくるので[import]をクリック

  • Firebaseに戻り、[次へ]をクリックで FirebaseプロジェクトとUnityプロジェクトの連携完了なので、[Firebaseコンソールに進む]をクリック

その4

Firebaseでデータベースを準備

FirebaseでRealtime Databaseを作成します

  • Firebaseコンソール画面左側メニューの[Database]をクリック -少しスクロールして、 [または Realtime Database を選択] の[データベースの作成]->[テストモードで開始](※)->[有効にする]

※今回は簡単に連携させてみたいだけなのでテストモードで作成しますprz

スクリーンショット 2019-08-21 14.30.51.png

その5

Unity側でサンプルコンテンツを準備

UnityプロジェクトにFirebaseのデータベースと連携させるサンプルコンテンツを作成する

  •  [Create]->[UI] から、Text,Input Field,Buttonの3つを作成(w160,h30)し適当に横並びにする
  •  TextはTextを「Score:」,Input FieldはPlaceholderを「Your Name」,ButtonはTextを「Submit」に変更
  •  [Create]->[Create Empty]でGameObjectを作成し、[Add Component]から、「PlayerScores」と検索バーに入力し、[NewScript]->[Create and Add]をクリックし、PlayerScores という名前のスクリプトをGameObjectに作成&追加

スクリーンショット 2019-08-21 15.14.00.png

  • 作成したPlayerScores.csを編集
PlayerScores.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    public static int playerScore;
    public static string playerName;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
    }
}
  • ButtonのOnClick()にGameObjectをアサインし、PlayerScoresのOnSubmit()ファンクションを選択

スクリーンショット 2019-08-21 15.58.53.png

  • 次に、REST APIを投げられるように、Rest Client for Unity をAssetStore で検索し [Download] and [Import]

スクリーンショット 2019-08-21 16.12.58.png

  • importまで完了したら、ProjectフォルダのAssetsのなかで、右クリックし、[Create]->[C# Script]でUser.csを作成し、下記のように編集
User.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class User
{
    public string userName;
    public int userScore;

    public User(){
        userName = PlayerScores.playerName;
        userScore = PlayerScores.playerScore;
    }
}
  • PlayerScores.csも下記の箇所を編集
PlayerScores.cs
    public void OnSubmit(){
        playerName = nameText.text;
    }

↓ PostToDatabase()メソッドを追加し、OnSubmit()にも追記

PlayerScores.cs
    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    private void PostToDatabase(){
        User user = new User();
        RestClient.Post("https://forunity-e2ac2.firebaseio.com/.json",user);
    }
  • スクリプトの追加と修正が終わったら、GameObjectのPlayerScoresスクリプトのフィールドの部分に、Score TextにはScoreのTextオブジェクトを、Name TextにはInputFieldオブジェクトをそれぞれアサイン

スクリーンショット 2019-08-21 16.34.08.png

  • アサインできたら、Unityプロジェクトを実行し、Name欄に名前を入れて、Submitをクリックし、Firebaseのデータベースに値が入れば登録は成功

スクリーンショット 2019-08-21 16.45.43.png

  • 次に、firebaseからデータを取得するために、Buttonを複製し、Textを「Get Score」に変更

スクリーンショット 2019-08-21 17.48.29.png

- PlayerScores.csのPlayerScoresクラス配下にOnGetScore(),UpdateScore(),RetrieveFromDatabase()メソッドを追加し、
User user = new User();を上の方で宣言

PlayerScores(修正後).cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Proyecto26;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    User user = new User();

    public static int playerScore;
    public static string playerName;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    public void OnGetScore(){
        RetrieveFromDatabase();
    }

    private void UpdateScore(){
        scoreText.text = "Score: " + user.userScore;
    }

    private void PostToDatabase(){
        User user = new User();
        RestClient.Put("https://forunity-e2ac2.firebaseio.com/"+ playerName +".json",user);
    }

    private void RetrieveFromDatabase(){
        RestClient.Get<User>("https://forunity-e2ac2.firebaseio.com/"+ nameText.text +".json").Then(response => 
        {
            user = response;
            UpdateScore();
        });
    }
}

スクリーンショット 2019-08-21 17.48.29.png

  • Name欄に登録ずみのNameを入力し、[Get Store]でScoreの表示されている値が登録済みのものにさし変わったら、データの取得も完了!

最後に、Sphereを作成してscore値によって色を変化させる

- PlayerScores.csのPlayerScoresクラス配下にChangeColorName()メソッドを追加し、public GameObject targetObject;を上の方で宣言し、UpdateScore(),PostToDatabase()に、引数にscoreを持たせてChangeColorName()を追記

PlayerScores(修正後).cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using Proyecto26;

public class PlayerScores : MonoBehaviour
{
    public Text scoreText;
    public InputField nameText;

    private System.Random random = new System.Random();

    User user = new User();

    public static int playerScore;
    public static string playerName;

    public GameObject targetObject;

    // Start is called before the first frame update
    void Start()
    {
        playerScore = random.Next(0, 101);
        scoreText.text = "Score: " + playerScore;
    }

    public void OnSubmit(){
        playerName = nameText.text;
        PostToDatabase();
    }

    public void OnGetScore(){
        RetrieveFromDatabase();
    }

    private void UpdateScore(){
        scoreText.text = "Score: " + user.userScore;
        ChangeColorName(user.userScore);
    }

    private void PostToDatabase(){
        User user = new User();
        RestClient.Put("https://forunity-e2ac2.firebaseio.com/"+ playerName +".json",user);
        ChangeColorName(user.userScore);
    }

    private void RetrieveFromDatabase(){
        RestClient.Get<User>("https://forunity-e2ac2.firebaseio.com/"+ nameText.text +".json").Then(response => 
        {
            user = response;
            UpdateScore();
        });
    }

    private void ChangeColorName(int score){
        //Debug.Log(score);
        if(score < 25){
            // red
            targetObject.GetComponent<Renderer>().material.color = Color.red;
        } else if (score < 50){
            // yellow
            targetObject.GetComponent<Renderer>().material.color = Color.yellow;
        } else if (score < 75){
            // green
            targetObject.GetComponent<Renderer>().material.color = Color.green;
        } else {
            // blue
            targetObject.GetComponent<Renderer>().material.color = Color.blue;
        }

    }
}

  • [Create]->[3D Object]->[Sphere]でSphereを作成し、[GameObject]の[Target Object]にアサイン

スクリーンショット 2019-08-21 19.45.25.png

確認作業

Sphereの色がScoreの値によってきちんと変わるか最終確認

Score値(0~100)は下記仕様で色が変化すればOK

0~24: red
25~49: yellow
50~74: green
75~100: blue
  • まず、確認しやすくするためFirebaseのデータベースに入っているテストデータをいったん削除

  • 次に、Unityプロジェクトを実行し、Name欄に「Nobunaga」を入力して[Submit]し、Scoreに合った色であることを確認し実行解除、再度Unityプロジェクトを実行し、今度はName欄に「Ieyasu」を入力して[Submit]し、Scoreに合った色であることを確認

  • 確認できたら、Name欄に「Nobunaga」を入力して[Get Score]、正しいScoreと色であることを確認し、続けてName欄に「Ieyasu」を入力して[Get Score]、正しいScoreと色であることも確認できたらサンプルコンテンツ完成

スクリーンショット 2019-08-21 19.57.19.png


:alien:振り返り:alien:

とても簡単なサンプルコンテンツではありますが、REST APIの部分でAssetを使ってしまったので、想像していたよりあっさり実装できました。
今度は Rest Client for Unity を使わずに UnityWebRequest を使って実装し直してみたいです!

以上!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity] アニメーションのブレンド

アニメーションを同じステート内でブレンドしながら変更する方法です。
複数のアニメーション間を0-1の数値で表現できるのでいろいろと便利です。

_gif_animation_001.gif

まずは新しいステートを作ります。ここではRun_endとしました。
2019-08-21_175030.png

作成したステートを右クリック->Create BlendTree in State を選択します
2019-08-21_180315.jpg

するとMotionのところがBlend Tree になるので、これをダブルクリック
2019-08-21_180633.jpg

+ボタン-Add Motion Field を押してモーションを追加します
2019-08-21_180814.jpg

モーションを追加すると自動的にブレンドパラメーターを設定してくれます。
2019-08-21_181435.gif

アニメーションのブレンドは"Blend"というパラメータを変更することで行います。

animator.SetFloat("Blend", [0f-1f]);

アニメーションのあるオブジェクトに下記を張り付けて
moveSpeedスライダーバーを動かすとモーションが滑らかにブレンドします。

AnimationBrendTreeTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AnimationBrendTreeTest : MonoBehaviour
{
    [SerializeField,Range(0,1)] float m_moveSpeed=0f;
    Animator m_animator;

    // Start is called before the first frame update
    void Start()
    {
        m_animator = GetComponent<Animator>();
    }

    // Update is called once per frame
    void Update()
    {
        m_animator.SetFloat("Blend", m_moveSpeed);
    }
}

2つ以上のモーションブレンドも可能です
2019-08-21_183148.jpg

gif_animation_004.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity2018.4 + WinMR + MRTK v2.0.0 RC2.1でのVRコンテンツ開発(セットアップ編)

はじめに

SteamVR Plugin v2.xを恐れてHTC ViveからWindows Mixed Reality Headsetに逃げたら、思いのほか情報が少なくて余計に苦労したという記録です。

最終的にはMRTK v2.0.0とWinMRコントローラを使って、以下のようにテレポートしたり物をいい感じに掴めるようにします。
basic interaction

ただし今回の記事では、まずMRTK v2.0.0のサンプルシーンを動かし、シンプルなシーンをビルドできるようにすることを目標とします。

開発環境など

1. 環境構築

最低限必要なソフトウェアなどはMRTKのGetting started guideに載っています。
2019/08/21現在では以下のようになっており、この章では環境構築の手順を書いていきます。なおVisual Studio 2019のインストール手順は省きます。

Required Software

1-1. Windows SDK 18362のインストール

Windows SDK 18362はVisual Studio 2019のUWP開発用のパッケージに含まれています。
Visual Studio Installerを立ち上げ、UWPアプリケーションのビルドに必要なモジュールをインストールしましょう。
既にインストールされている場合はとりあえずVisual StudioやInstallerを最新のものにしておきます。
Visual Studio Installer
Install Windows SDK

1-2. Unity 2018.4.xのインストール

Unity HubからUnity 2018.4.xをインストールします。
Select Unity

このとき、UWPのBuildに必要なモジュールも選択して一緒にインストールしましょう
Build Support

2. MRTK v2のサンプルシーンの実行

この章ではUnityプロジェクトの作成、MRTK v2のダウンロードおよびプロジェクトへのインポートを行い、MRTK v2のサンプルシーンを実行します。

2-1. Unityプロジェクトの作成とUWPプロジェクトのビルド

Unity HubからUnity 2018.4.xを用いたプロジェクトを作成します。
Create Unity Project

作成したプロジェクトの[File] > [Build Setting]を開き、Build TargetをUniversal Windows Platformに変更(Switch Platform)します。
Switch Platform

まずはこの状態で単純にUWPプロジェクトがBuildできるか試してみましょう。
適当にCubeを置いたSceneを作成し、[File] > [Build Setting] > Buildします。Buildする場所はAssetsと同じ階層にBuildsという名前のフォルダを作成し、選択すると良いと思います。
TestScene

ここで、デフォルトの設定では[Edit] > [Project Setting] > [Player] > [Other Setting] > [Configuration] > Scripting BackendがIL2CPPに設定されているので、Visual Studio側で以下のような警告が出るかもしれません。
IL2CPP Warning

その場合は、ウィンドウの指示に従い必要なモジュールをインストールしましょう。
Install C++ UWP

モジュールのインストールが終わったら再びBuilds/***.slnを開きます。
Open sln

ソリューションの構成やプラットフォームを、UnityのBuild Settingでの設定と同じにします。
Setting solution

ローカルコンピュータでDebugを実行し、Unityで作成したシーンが実行されることを確認しましょう。
Build Conform

2-2. MRTK v2のダウンロードとインポート

Microsoft Mixed Reality Toolkit v2.0.0 RC2.1のページの下を見に行き、以下の2つのunitypackageをダウンロードします。

  • Microsoft.MixedReality.Toolkit.Unity.Examples-v2.0.0-RC2.1.unitypackage
  • Microsoft.MixedReality.Toolkit.Unity.Foundation-v2.0.0-RC2.1.unitypackage

次にUnityの[Assets] > [Import Package] > [Custom Package]からダウンロードしたパッケージをインポートします。このとき、アセンブリの依存関係を破壊しないように、インポートはMicrosoft.MixedReality.Toolkit.Unity.Foundation-v2.0.0-RC2.1.unitypackageから先に行いましょう。

2-3. MRTK v2のサンプルシーンの実行

PCにWindows Mixed Reality Headsetを接続しましょう。また、[Project Setting] > [Player] > [XR Setting]でWindows Mixed Realityが選択されていることを確認します。

Assets/MixedRealityToolkit.Examples/Demos/HandTracking/Scenes/HandInteractionExample.sceneを開き、実行すると、以下のようなサンプルシーンを体験できます。
MRTK Sample

3. 独自のシーンの作成とビルド

独自のシーンでWinMRのコントローラを使えるようにしたり、テレポートできるようにするにはひと手間必要になります。この章ではその方法を解説していきます。

3-1. MRTK Configurationの適用

先ほど作ったTestSceneを開き、[Mixed Reality Toolkit] > [Add to Scene and Configure]を選択します。すると「You must choose a profile for the Mixed Reality Toolkit.」というメッセージが出るので、今回はDefaultMixedRealityToolkitConfigurationProfileを選択しましょう。
Select profile

MRTK v2では、このConfiguration Profileを変更することで、コントローラのモデルや挙動を設定したり、テレポート移動を有効化したり...といった設定を行います。

3-2. シーンの実行とビルド

今回選択したデフォルトのProfileではテレポート移動が有効になっているので、シーンにColider付きの床を作成します。SceneにPlaneを作成し、InspectorでPositionを(x, y, z) = (0, 0.1, 0)に設定しましょう。
また、作成してあるCubeのPositionも(0, 1.5, 0)とかにしておきましょう。

この状態で、UnityでSceneをPlayしてみましょう。
コントローラのアナログスティックを倒せばテレポートを行うことができ、ポインターでCubeを選択し、Triggerを押せば(Cubeを動かしたりすることは出来ませんが)ポインターで掴むことは出来ると思います。
My easy scene

最後に、2-2項の要領で今回のSceneをビルドし、成功すれば完了です。

さいごに

今回は、Unity2018.4 + WinMR + MRTK v2.0.0 RC2.1を使って開発をするうえで最低限必要な手順を記述しました。
しかし今回の手順だけでは、オブジェクトを(ポインターではなく)手で掴むことができなかったり、コントローラの3Dモデルが3軸のベクトルモデルしか使えなかったり、まだまだ独自コンテンツの開発には不十分です。

次回の記事で、独自のConfiguration Profileを設定し、コントローラの3Dモデルを差し替えたり、物を手で掴めるようにしたりしていきます。

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

キャラクターを動くようにする

public Rigidbody2D rigitto;
public float moveSpee;

公開 リジットボディ2D型のrigitto
公開 フロート型のmoveSpeed


rigittoはプレイヤーのRigidbody2D、moveSpeedは5をインスペクタで入れる。

~Update~
float xPosi = Input.GetAxisRaw("Horizontal");
float yPosi = Input.GetAxisRaw("Vertical");

フロート型のxPosi = 軸(”平行”)の入力値
フロート型のyPosi = 軸(”垂直”)の入力値

Vector2 moveDidirection = new Vector2(xPosi, yPosi);

ベクトル2型のmoveDirection = 新しいベクトル値(xPosiyPosi

rigitto.velocity = moveDidirection * moveSpeed;

rigittoの速度(方向と速さ) = moveDidirection × moveSpeed

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでオンラインマルチプレイなゲームを作りたい その5 ルーム作成

前回の記事でロビーまで入室できるようになりました。
今回はルームの作成を実装しようと思います。

今回の目標

Untitled Diagram_3.png
画像上の赤〇で囲っている部分を実装していきます。

※イメージ図(UIが小さすぎますが気にしないでください)
キャプチャ.JPG

ルーム

http://www.monobitengine.com/doc/mun/contents/Reference/Glossary.htm#%E3%83%AB%E3%83%BC%E3%83%A0
実際にネットワーク処理で同期/共有を行うところになるようです。

ルーム作成!の前に

すっかり忘れてた処理があるのでそちらのほうを先に追加しようと思います。

プレイヤーネーム

自身に名前を設定できます。ロビー内でプレイヤー検索を行えたり、ルーム内で共有できたりするので、つけておくと便利です。
http://www.monobitengine.com/doc/doxygen/client/html/class_monobit_engine_base_1_1_monobit_network.html#aa6c5e376fd4c53276f36218e2c8409c2
キャプチャ.JPG

 MonobitNetwork.playerName = "お名前";

つけたい名前を代入するだけですね。

ということでタイトル画面に名前入力を追加します。
(本当はロビー画面やルーム作成/入室画面でやりたかったんですが、なぜかサーバー接続後からルーム入室するまでの間で、playerNameが反映されなかったので、タイトル画面で実装しています。リファレンスにはいつ設定しても有効って書いてあったんですが...。)

※プレイヤーネーム入力の個所以外の処理は省いてます

    /// <summary>タイトル画面</summary>
    public class SceneTitle : MonobitEngine.MonoBehaviour
    {
        /// <summary>ユーザー名入力時に呼ばれる</summary>
        /// <param name="userName">入力された文字列</param>
        private void OnInputUserName(string userName)
        {
            MonobitNetwork.playerName = userName;
        }
    }

OnInputUserNameをInputFieldのイベントにでも設定しておいてください。

ルーム作成

忘れてた処理が書けたのでルーム作成に戻ります。

ルームパラメータ

http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomParameter.htm
作成するルームの基本設定を行うものです。

ルームカスタムパラメータ

http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomCustomParameter.htm
ルーム検索時や入室条件等に使える、より詳細な設定を行うものです。

まずはルームの仕様を決め、この二つを設定していきます。

・1ルーム最大4人
・ゲーム開始後の途中参加は不可
・パスワード設定有り
・パスワードを設定した場合はプライベート扱い

とりあえずこんな感じですかね。

実際にコード上で設定します。
※例によってuGUI等の処理は省いてます。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }
}

RoomSettingsに必要な情報を設定するだけです。
※一応MonobitNetwork.inLobby(その3参照)でロビーに入室されているかどうかを出力するようにしています。
この設定を用いてルームを実際に作成する処理を入れます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateRoom.htm

MonobitEngine.MonobitNetwork.CreateRoom("roomName", settings, lobby);

この関数を呼ぶことで実際にルームの作成を行います。

ということでルーム作成をコードに追加します。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>プライベート設定の変更時に呼ばれる</summary>
    /// <param name="isOn">トグルの状態</param>
    private void OnCheckPrivateSetting(bool isOn)
    {
        m_PasswordInputFieldObj.SetActive(isOn);
        if (!isOn) { m_RoomSettings.roomParameters["password"] = "empty"; }
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        if (!m_RoomSettings.roomParameters.ContainsKey("password")) { return; }

        m_RoomSettings.roomParameters["password"] = password;           
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickCreate()
    {
        // 引数にはルーム名、Start()内で設定したルーム設定、ロビーはデフォルトのロビーなためnull、を入れています。
        MonobitNetwork.CreateRoom(m_RoomName, m_RoomSettings, null);
    }
}

ボタンクリック時にルームが作成されるようにしています。
※ついでにルーム名とパスワードを入力する処理も追加しています。

仕上げにルーム作成後のコールバックを追加します。
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreatedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnCreatedRoom();

ルームの作成に成功した際に呼び出されます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreateRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnCreateRoomFailed(object[] codeAndMsg)

ルームの作成に失敗した際に呼び出されます。

この二つ + ルーム作成画面の状態でサーバーから切断した時用にOnDisconnectedFromServer(その3参照)も追加します。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }

    /// <summary>ルームが作成された際に呼ばれるコールバック</summary>
    public void OnCreatedRoom()
    {
        SceneManager.LoadScene("RoomWait");
    }

    /// <summary>ルームの作成に失敗した際に呼ばれるコールバック</summary>
    /// <param name="codeAndMsg">エラーコード</param>
    public void OnCreateRoomFailed(object[] codeAndMsg)
    {
        Debug.Log("OnCreateRoomFailed : errorCode = " + codeAndMsg[0] + ", message = " + codeAndMsg[1]);
    }

    /// <summary>MUNサーバーとの接続を切った際に呼ばれるコールバック</summary>
    public void OnDisconnectedFromServer()
    {
        SceneManager.LoadScene("Title");
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>プライベート設定の変更時に呼ばれる</summary>
    /// <param name="isOn">トグルの状態</param>
    private void OnCheckPrivateSetting(bool isOn)
    {
        m_PasswordInputFieldObj.SetActive(isOn);
        if (!isOn) { m_RoomSettings.roomParameters["password"] = "empty"; }
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        if (!m_RoomSettings.roomParameters.ContainsKey("password")) { return; }

        m_RoomSettings.roomParameters["password"] = password;           
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickCreate()
    {
        // 引数にはルーム名、Start()内で設定したルーム設定、ロビーはデフォルトのロビーなためnull、を入れています。
        MonobitNetwork.CreateRoom(m_RoomName, m_RoomSettings, MonobitNetwork.lobby);
    }

    /// <summary>戻るボタンが押された際に呼ばれる</summary>
    private void OnClickBack()
    {
        SceneManager.LoadScene("Lobby");
    }
}

ルーム作成成功後に次の画面であるRoomWaitに飛ぶようにしてあげてます。
失敗した場合はエラーログを出すだけですね。
サーバーが切断された場合はタイトル画面に戻るようにしています。
ルームを作成した場合、ルームの作成者は自動で入室してくれるので何か別途入室処理を行う必要はないようです。

これでルームの作成が実装できました。
次回は作成されたルームへ入室するところをやっていこうと思います。

資料

http://www.monobitengine.com/doc/mun/contents/Reference/Glossary.htm#%E3%83%AB%E3%83%BC%E3%83%A0
http://www.monobitengine.com/doc/doxygen/client/html/class_monobit_engine_base_1_1_monobit_network.html#aa6c5e376fd4c53276f36218e2c8409c2
http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomParameter.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomCustomParameter.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreatedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreateRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでオンラインマルチプレイなゲームを作りたい その5 ルーム作成

前回の記事でロビーまで入室できるようになりました。
今回はルームの作成を実装しようと思います。

今回の目標

Untitled Diagram_3.png
画像上の赤〇で囲っている部分を実装していきます。

※イメージ図(UIが小さすぎますが気にしないでください)
キャプチャ.JPG

ルーム

http://www.monobitengine.com/doc/mun/contents/Reference/Glossary.htm#%E3%83%AB%E3%83%BC%E3%83%A0
実際にネットワーク処理で同期/共有を行うところになるようです。

ルーム作成!の前に

すっかり忘れてた処理があるのでそちらのほうを先に追加しようと思います。

プレイヤーネーム

自身に名前を設定できます。ロビー内でプレイヤー検索を行えたり、ルーム内で共有できたりするので、つけておくと便利です。
http://www.monobitengine.com/doc/doxygen/client/html/class_monobit_engine_base_1_1_monobit_network.html#aa6c5e376fd4c53276f36218e2c8409c2
キャプチャ.JPG

 MonobitNetwork.playerName = "お名前";

つけたい名前を代入するだけですね。

ということでタイトル画面に名前入力を追加します。
(本当はロビー画面やルーム作成/入室画面でやりたかったんですが、なぜかサーバー接続後からルーム入室するまでの間で、playerNameが反映されなかったので、タイトル画面で実装しています。リファレンスにはいつ設定しても有効って書いてあったんですが...。)

※プレイヤーネーム入力の個所以外の処理は省いてます

    /// <summary>タイトル画面</summary>
    public class SceneTitle : MonobitEngine.MonoBehaviour
    {
        /// <summary>ユーザー名入力時に呼ばれる</summary>
        /// <param name="userName">入力された文字列</param>
        private void OnInputUserName(string userName)
        {
            MonobitNetwork.playerName = userName;
        }
    }

OnInputUserNameをInputFieldのイベントにでも設定しておいてください。

ルーム作成

忘れてた処理が書けたのでルーム作成に戻ります。

ルームパラメータ

http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomParameter.htm
作成するルームの基本設定を行うものです。

ルームカスタムパラメータ

http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomCustomParameter.htm
ルーム検索時や入室条件等に使える、より詳細な設定を行うものです。

まずはルームの仕様を決め、この二つを設定していきます。

・1ルーム最大4人
・ゲーム開始後の途中参加は不可
・パスワード設定有り
・パスワードを設定した場合はプライベート扱い

とりあえずこんな感じですかね。

実際にコード上で設定します。
※例によってuGUI等の処理は省いてます。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }
}

RoomSettingsに必要な情報を設定するだけです。
※一応MonobitNetwork.inLobby(その3参照)でロビーに入室されているかどうかを出力するようにしています。
この設定を用いてルームを実際に作成する処理を入れます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/CreateRoom.htm

MonobitEngine.MonobitNetwork.CreateRoom("roomName", settings, lobby);

この関数を呼ぶことで実際にルームの作成を行います。

ということでルーム作成をコードに追加します。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>プライベート設定の変更時に呼ばれる</summary>
    /// <param name="isOn">トグルの状態</param>
    private void OnCheckPrivateSetting(bool isOn)
    {
        m_PasswordInputFieldObj.SetActive(isOn);
        if (!isOn) { m_RoomSettings.roomParameters["password"] = "empty"; }
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        if (!m_RoomSettings.roomParameters.ContainsKey("password")) { return; }

        m_RoomSettings.roomParameters["password"] = password;           
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickCreate()
    {
        // 引数にはルーム名、Start()内で設定したルーム設定、ロビーはデフォルトのロビーなためnull、を入れています。
        MonobitNetwork.CreateRoom(m_RoomName, m_RoomSettings, null);
    }
}

ボタンクリック時にルームが作成されるようにしています。
※ついでにルーム名とパスワードを入力する処理も追加しています。

仕上げにルーム作成後のコールバックを追加します。
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreatedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnCreatedRoom();

ルームの作成に成功した際に呼び出されます。

http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreateRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

public void OnCreateRoomFailed(object[] codeAndMsg)

ルームの作成に失敗した際に呼び出されます。

この二つ + ルーム作成画面の状態でサーバーから切断した時用にOnDisconnectedFromServer(その3参照)も追加します。

/// <summary>ルーム作成画面</summary>
public class SceneCreateRoom : MonobitEngine.MonoBehaviour
{
    /// <summary>ルーム設定情報</summary>
    static private RoomSettings m_RoomSettings;

    /// <summary>ルーム名</summary>
    private string m_RoomName = string.Empty;

    // Start is called before the first frame update
    void Start()
    {
        if (!MonobitNetwork.inLobby) { Debug.Log("Not in lobby"); }

        m_RoomSettings = new RoomSettings() {
             // 公開か非公開かを設定できる
             isVisible = true,

             // 入室を許可するかを設定できる
             isOpen = true,

             // 作成するルームの最大人数を4人に設定する
             maxPlayers = 4,

             // パスワードとして利用するためのカスタムパラメータを作成
             roomParameters = new Hashtable() { { "password", "empty" }, },

             // 上記だけだと、ルーム外からカスタムパラメータを扱うことが出来ないので、扱えるように設定する。
             lobbyParameters = new string[] { "password" }
         };
    }

    /// <summary>ルームが作成された際に呼ばれるコールバック</summary>
    public void OnCreatedRoom()
    {
        SceneManager.LoadScene("RoomWait");
    }

    /// <summary>ルームの作成に失敗した際に呼ばれるコールバック</summary>
    /// <param name="codeAndMsg">エラーコード</param>
    public void OnCreateRoomFailed(object[] codeAndMsg)
    {
        Debug.Log("OnCreateRoomFailed : errorCode = " + codeAndMsg[0] + ", message = " + codeAndMsg[1]);
    }

    /// <summary>MUNサーバーとの接続を切った際に呼ばれるコールバック</summary>
    public void OnDisconnectedFromServer()
    {
        SceneManager.LoadScene("Title");
    }

    /// <summary>ルーム名入力時に呼ばれる</summary>
    /// <param name="roomName">入力された文字列</param>
    private void OnInputRoomName(string roomName)
    {
        m_RoomName = roomName;
    }

    /// <summary>プライベート設定の変更時に呼ばれる</summary>
    /// <param name="isOn">トグルの状態</param>
    private void OnCheckPrivateSetting(bool isOn)
    {
        m_PasswordInputFieldObj.SetActive(isOn);
        if (!isOn) { m_RoomSettings.roomParameters["password"] = "empty"; }
    }

    /// <summary>パスワード入力痔に呼ばれる</summary>
    /// <param name="password">入力された文字列</param>
    private void OnInputPassword(string password)
    {
        if (!m_RoomSettings.roomParameters.ContainsKey("password")) { return; }

        m_RoomSettings.roomParameters["password"] = password;           
    }

    /// <summary>作成ボタンが押された際に呼ばれる</summary>
    private void OnClickCreate()
    {
        // 引数にはルーム名、Start()内で設定したルーム設定、ロビーはデフォルトのロビーなためnull、を入れています。
        MonobitNetwork.CreateRoom(m_RoomName, m_RoomSettings, MonobitNetwork.lobby);
    }

    /// <summary>戻るボタンが押された際に呼ばれる</summary>
    private void OnClickBack()
    {
        SceneManager.LoadScene("Lobby");
    }
}

ルーム作成成功後に次の画面であるRoomWaitに飛ぶようにしてあげてます。
失敗した場合はエラーログを出すだけですね。
サーバーが切断された場合はタイトル画面に戻るようにしています。
ルームを作成した場合、ルームの作成者は自動で入室してくれるので何か別途入室処理を行う必要はないようです。

これでルームの作成が実装できました。
次回は作成されたルームへ入室するところをやっていこうと思います。

資料

http://www.monobitengine.com/doc/mun/contents/Reference/Glossary.htm#%E3%83%AB%E3%83%BC%E3%83%A0
http://www.monobitengine.com/doc/doxygen/client/html/class_monobit_engine_base_1_1_monobit_network.html#aa6c5e376fd4c53276f36218e2c8409c2
http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomParameter.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/RoomCustomParameter.htm
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreatedRoom%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89
http://www.monobitengine.com/doc/mun/contents/FeatureClient/CallbackFunction.htm#OnCreateRoomFailed%20%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity IAPを試してみた (Yet Another Purchaser.cs)

前提

  • Unity 2018.4.5f1
  • Unity IAP 1.22.0
  • Apple App Store、Google Play Store
  • この記事では、Unity IAPの一部機能を限定的に使用し、汎用性のない部分があります。
    • サーバレス、消費/非消費タイプ使用、購読タイプ未使用
  • この記事のソースは、実際のストアでテストしていますが、製品版での使用実績はありません。
    • ソース中のIDは実際と異なります。

公式ドキュメント

  • マニュアル
  • スクリプトリファレンス
    • 2019/08/02時点で、バージョンによって記述があったりなかったりします。
    • 記述あり 5.6、2017.1、2017.2、2017.3、2018.1、2018.2
    • 記述なし 2017.4、2018.3、2018.4、2019.1

ネームスペース

  • UnityEngine.Purchasing
    • 必須のネームスペースです。
  • UnityEngine.Purchasing.Security
    • レシートの検証で必要なネームスペースです。
    • スクリプトリファレンスに記述が見つかりません。
  • UnityEngine.Purchasing.Extension
    • この記事では扱いません。

初期化

初期化の開始

  • UnityPurchasing.Initialize ()を呼ぶことで、初期化を開始します。
UnityPurchasing.Initialize (Purchaser instance, ConfigurationBuilder builder);
  • 初期化の要求はブロックされず、後に、結果に応じたコールバックがあります。

イベントハンドラ

  • コールバックを受け取るために、IStoreListenerを継承したクラスのインスタンスが必要です。
    • 必ずしもMonoBehaviourを継承する必要はありません。
    • インターフェイスIStoreListenerでは、OnInitialized ()OnInitializeFailed ()OnPurchaseFailed ()ProcessPurchase ()の4つのイベントハンドラが必要になります。

準備

  • Initialize ()を呼ぶためには、ConfigurationBuilder builderのインスタンスを得る必要があります。
  • ConfigurationBuilder.Instance ()を呼ぶためには、IPurchasingModuleを継承したクラスのインスタンスが必要なようですが、この辺りを記載したドキュメントが見つかりません。
  • 付属のDemoでは、StandardPurchasingModuleがそのクラスにあたるようで、そのインスタンスを得て使われています。
  • 得られたインスタンスmoduleにストアの設定を行い、さらにbuilderインスタンスを得ます。
  • 得られたインスタンスbuilderに製品を登録し、検証器を生成して、初期化を開始します。
  • ここでは、クラスのコンストラクタで、準備から初期化の開始までを行っています。
    • コンストラクタがprivateなのは、シングルトンで使うためです。
Purchaser.cs
/// <summary>コンストラクタ</summary>
private Purchaser (IEnumerable<ProductDefinition> products) {
    Debug.Log ("Purchaser.Construct");
    var module = StandardPurchasingModule.Instance ();
    module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
    isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
    isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore;
    validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier);
    var builder = ConfigurationBuilder.Instance (module);
    builder.AddProducts (products);
    UnityPurchasing.Initialize (this, builder);
}

製品定義

  • 先のコンストラクタが受け取ってbuilderに登録した製品定義は、製品のIDとタイプのセットです。
Sample.cs
var products = new [] {
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable),
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable),
    new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable),
};
  • これらはストア・ダッシュボードでの設定と正しく呼応している必要があります。
    • Apple App Storeでは、IDと製品タイプの双方が設定されます。
    • Google Play Storeでは、IDが設定されますが、消費の有無についての設定はありません。
      • 消費タイプでは、アプリを消費したことを申告するだけです。
  • この記事では、消費タイプConsumableと非消費タイプNonConsumableだけを扱い、購読タイプは扱いません。

初期化の完了

  • 初期化に成功したら、得られたIStoreControllerIExtensionProviderを保存します。
Purchaser.cs
/// <summary>初期化完了</summary>
public void OnInitialized (IStoreController controller, IExtensionProvider extensions) {
    Debug.Log ($"Purchaser.Initialized {controller}, {extensions}");
    appleExtensions = extensions.GetExtension<IAppleExtensions> ();
    appleExtensions.RegisterPurchaseDeferredListener (OnDeferred);
    googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> ();
    this.controller = controller;
    this.extensions = extensions;
    Inventory = new Inventory { };
    foreach (var product in controller.products.all) {
        Inventory [product] = possession (product);
    }
}

/// <summary>初期化失敗</summary>
public void OnInitializeFailed (InitializationFailureReason error) {
    Debug.LogError ($"Purchaser.InitializeFailed {error}");
    Unavailable = true;
}
  • iOSの'Ask to buy'関連、OnDeferredはテストできていません。
  • Inventoryについては、後述します。

製品目録

  • 初期化に成功すると、controller.products.allで、製品目録を得ることができます。
Sample.cs
foreach (var product in Purchaser.Products.all) {
    Debug.Log (Purchaser.GetProductProperties (product));
}
Purchaser.cs
/// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary>
public static ProductCollection Products => Valid ? instance.controller.products : null;
Purchaser.cs
/// <summary>製品諸元</summary>
public static string GetProperties (this Product product) {
    return string.Join ("\n", new [] {
        $"id={product.definition.id} ({product.definition.storeSpecificId})",
        $"type={product.definition.type}",
        $"enabled={product.definition.enabled}",
        $"available={product.availableToPurchase}",
        $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})",
        $"localizedDescription={product.metadata.localizedDescription}",
        $"isoCurrencyCode={product.metadata.isoCurrencyCode}",
        $"localizedPrice={product.metadata.localizedPrice}",
        $"localizedPriceString={product.metadata.localizedPriceString}",
        $"transactionID={product.transactionID}",
        $"Receipt has={product.hasReceipt}",
        $"Purchaser.Valid={Purchaser.Valid}",
        $"Receipt validation={Purchaser.ValidateReceipt (product)}",
        $"Possession={Purchaser.Inventory [product]}",
    });
}

目録の謎

※以下は、Google Play StoreとApple App Store (Sandbox)で確認した内容です。製品でのテストではありません。

  • もし、初期化の際に製品定義を渡さなかったらどうなるのでしょうか?
    • その場合、製品目録は基本的に空になります。ただし、購入済みの非消費製品は取得されます。
    • ストアから製品IDのカタログが得られるわけではありません。つまり、ストアに新製品を登録しただけでは、製品に組み込めないのです。
  • ProductDefinition.enabledは、スクリプトリファレンスでは"This flag indicates whether a product should be offered for sale. It is controlled through the cloud catalog dashboard."と説明されています。
    • これを見る限り、ストアのダッシュボードで設定されている有効/無効状態を取得できるように読めますが、実際には常にtrueになります。
    • 例え、ストアに登録されていないIDを指定した場合でもtrueです。全く役に立ちません。
      • ストアにない場合は、Product.availableToPurchaseFalseになります。
    • Play Storeで無効にされている製品を購入しようとすると「原因不明の購入エラー」になります。
    • App Storeで無効にされている製品でも、Sandboxでは購入できてしまいます。
  • つまり、以下の制約が生じます。
    • ストアに登録されている未知の製品を取得することはできません。
    • ストアでの状態(有効/無効)を取得することはできません。
    • 購入の失敗が、ストアでの無効設定によるものと判別できません。
  • その結果、以下のような使い方になります。
    • ストアとは別の手段(あらかじめ組み込む、自前のサーバから取得するなど)で製品定義を保持する必要があります。
    • ストアでの製品の有効/無効は、アプリの使用する製品定義に連動させます。
      • 緊急時以外は、ストア独自に製品を無効化しないようにします。

購入

購入の開始

  • IStoreController.InitiatePurchase ()Productを渡すことで、購入が開始されます。
Purchaser.cs
/// <summary>課金開始</summary>
private bool purchase (Product product) {
    if (product != null && product.Valid ()) {
        Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}");
        controller.InitiatePurchase (product);
        return true;
    }
    return false;
}
  • 購入の要求はブロックされず、後に、結果に応じたコールバックがあります。

購入の完了

  • 課金結果のコールバックでは、購入に関わる処理が全て完了したら、PurchaseProcessingResult.Completeを返します。
    • 消費タイプの場合は、消費が実行されます。
Purchaser.cs
/// <summary>課金失敗</summary>
public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) {
    Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}");
}

/// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary>
public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) {
    var validated = ValidateReceipt (eventArgs.purchasedProduct);
    Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}");
    Inventory [eventArgs.purchasedProduct] = validated;
    return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete;
}
  • このコードでは、消費タイプではPurchaseProcessingResult.Pendingを返し、それ以外ではCompleteを返します。
    • Pendingを返すと、消費は保留されます。
  • この保留状態は、(謎の)クラウドで保持されるためアプリが中断しても失われず、起動毎にProcessPurchase ()へのコールバックが繰り返されます。

    • 保留状態を終わらせるには、ProcessPurchase ()Completeを返すか、別途IStoreController.ConfirmPendingPurchase (product)を呼びます。
  • Product.hasReceiptは、起動直後に未購入または消費済みであればfalseとなり、購入完了時にはtrueに変化します。

    • しかし、Completeを返した場合も、消費を促すConfirmPendingPurchase (product)を行おうとも、その場ではfalseには戻りません。
    • つまり、hasReceiptを見て消費完了を知ることはできません。
    • また、ConfirmPendingPurchase (product)には、結果を知らせるコールバックがありません。
  • 従って、保留と消費の状態を判別するためには、Unity-IAPの外側で所持状態を管理する必要があります。

  • なお、非消費タイプでは、購入済みのhasReceiptは常にtrueになります。

所有状態の管理

  • このコードでは、InventoryというDictionary派生クラスを用意して、製品所有状態を管理しています。
    • 初期化完了のコールバック中で初期化しています。
    • Inventory [string 製品ID]またはInventory [Product 製品]で真偽値を得ることができます。
Purchaser.cs
/// <summary>productID基準でProductの在庫を表現する辞書</summary>
public class Inventory : Dictionary<string, bool> {

    /// <summary>Productによるアクセス</summary>
    public bool this [Product product] {
        get { return base [product.definition.id]; }
        set { base [product.definition.id] = value; }
    }

}

復元

  • Appleの既定では、ユーザーがこの処理を明示的に行える必要があるのですが、これを呼ばなくてもUnity-IAPが自動的に復元をしているようなので、それ以上の意味はないように思われます。正直よく分かりません。
Purchaser.cs
/// <summary>復元</summary>
private void restore (Action<bool> onRestored = null) {
    Debug.Log ("Purchaser.Restore");
    Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); };
    if (isGooglePlayStoreSelected) {
        googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored);
    } else if (isAppleAppStoreSelected) {
        appleExtensions.RestoreTransactions (onTransactionsRestored);
    }
}
Purchaser.cs
/// <summary>復元完了</summary>
private void OnTransactionsRestored (bool success) {
    Debug.Log ($"Purchaser.Restored {success}");
}

コード全容

Purchaser.cs
//  Copyright© tetr4lab.

using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Purchasing;
using UnityEngine.Purchasing.Security;

/// <summary>UnityIAPを使う</summary>
namespace UnityInAppPuchaser {

    /// <summary>課金処理</summary>
    public class Purchaser : IStoreListener {

#region Static

        /// <summary>シングルトン</summary>
        private static Purchaser instance;

        /// <summary>在庫目録 製品の課金状況一覧、消費タイプは未消費を表す</summary>
        public static Inventory Inventory { get; private set; }

        /// <summary>有効 初期化が完了している</summary>
        public static bool Valid => (instance != null && instance.valid);

        /// <summary>使用不能 初期化に失敗した</summary>
        public static bool Unavailable { get; private set; }

        /// <summary>製品目録 初期化時の製品IDに対してストアから得た情報</summary>
        public static ProductCollection Products => Valid ? instance.controller.products : null;

        /// <summary>クラス初期化 製品のIDとタイプの一覧を渡す</summary>
        public static void Init (IEnumerable<ProductDefinition> products) {
            if (instance == null || Unavailable) {
                instance = new Purchaser (products);
            }
        }

        /// <summary>所有検証 有効なレシートが存在する</summary>
        private static bool possession (Product product) {
            return product.hasReceipt && Purchaser.ValidateReceipt (product);
        }

        /// <summary>レシート検証</summary>
        public static bool ValidateReceipt (string productID) {
            return (!string.IsNullOrEmpty (productID) && instance.validateReceipt (instance.controller.products.WithID (productID)));
        }

        /// <summary>レシート検証</summary>
        public static bool ValidateReceipt (Product product) {
            return (instance != null && instance.validateReceipt (product));
        }

        /// <summary>課金 指定製品の課金処理を開始する</summary>
        public static bool Purchase (string productID) {
            if (!string.IsNullOrEmpty (productID) && Valid) {
                return instance.purchase (instance.controller.products.WithID (productID));
            }
            return false;
        }

        /// <summary>課金 指定製品の課金処理を開始する</summary>
        public static bool Purchase (Product product) {
            if (product != null && Valid) {
                return instance.purchase (product);
            }
            return false;
        }

        /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary>
        public static bool ConfirmPendingPurchase (string productID) {
            if (!string.IsNullOrEmpty (productID) && Valid) {
                return instance.confirmPendingPurchase (instance.controller.products.WithID (productID));
            }
            return false;
        }

        /// <summary>保留した課金の完了 消費タイプの指定製品の保留していた消費を完了する</summary>
        public static bool ConfirmPendingPurchase (Product product) {
            if (product != null && Valid) {
                return instance.confirmPendingPurchase (product);
            }
            return false;
        }

        /// <summary>復元 課金情報の復元を行い、結果のコールバックを得ることができる</summary>
        public static void Restore (Action<bool> onRestored = null) {
            if (Valid) { instance.restore (onRestored); }
        }

#endregion

        /// <summary>コントローラー</summary>
        private IStoreController controller;
        /// <summary>拡張プロバイダ</summary>
        private IExtensionProvider extensions;
        /// <summary>Apple拡張</summary>
        private IAppleExtensions appleExtensions;
        /// <summary>Google拡張</summary>
        private IGooglePlayStoreExtensions googlePlayStoreExtensions;
        /// <summary>AppleAppStore</summary>
        private bool isAppleAppStoreSelected;
        /// <summary>GooglePlayStore</summary>
        private bool isGooglePlayStoreSelected;
        /// <summary>検証機構</summary>
        private CrossPlatformValidator validator;
        /// <summary>有効</summary>
        private bool valid => (controller != null && controller.products != null);

        /// <summary>コンストラクタ</summary>
        private Purchaser (IEnumerable<ProductDefinition> products) {
            Debug.Log ("Purchaser.Construct");
            var module = StandardPurchasingModule.Instance ();
            module.useFakeStoreUIMode = FakeStoreUIMode.StandardUser;
            isGooglePlayStoreSelected = Application.platform == RuntimePlatform.Android && module.appStore == AppStore.GooglePlay;
            isAppleAppStoreSelected = Application.platform == RuntimePlatform.IPhonePlayer && module.appStore == AppStore.AppleAppStore;
            validator = new CrossPlatformValidator (GooglePlayTangle.Data (), AppleTangle.Data (), Application.identifier);
            var builder = ConfigurationBuilder.Instance (module);
            builder.AddProducts (products);
            UnityPurchasing.Initialize (this, builder);
        }

        /// <summary>レシート検証</summary>
        private bool validateReceipt (Product product) {
            if (!valid || !product.hasReceipt) { return false; }
#if UNITY_EDITOR
            return true;
#else
            try {
                var result = validator.Validate (product.receipt);
                Debug.Log ("Purchaser.validateReceipt Receipt is valid. Contents:");
                return true;
            } catch (IAPSecurityException ex) {
                Debug.LogError ($"Purchaser.validateReceipt Invalid receipt {product.definition.id}, not unlocking content. {ex}");
                return false;
            }
#endif
        }

        /// <summary>課金開始</summary>
        private bool purchase (Product product) {
            if (product != null && product.Valid ()) {
                Debug.Log ($"Purchaser.InitiatePurchase {product.definition.id} {product.metadata.localizedTitle} {product.metadata.localizedPriceString}");
                controller.InitiatePurchase (product);
                return true;
            }
            return false;
        }

        /// <summary>保留した課金の完了</summary>
        private bool confirmPendingPurchase (Product product) {
            if (product != null && Inventory [product] && possession (product)) {
                controller.ConfirmPendingPurchase (product);
                Inventory [product] = false;
                Debug.Log ($"Purchaser.ConfirmPendingPurchase {product.GetProperties ()}");
                return true;
            }
            return false;
        }

        /// <summary>復元</summary>
        private void restore (Action<bool> onRestored = null) {
            Debug.Log ("Purchaser.Restore");
            Action<bool> onTransactionsRestored = success => { OnTransactionsRestored (success); onRestored?.Invoke (success); };
            if (isGooglePlayStoreSelected) {
                googlePlayStoreExtensions.RestoreTransactions (onTransactionsRestored);
            } else if (isAppleAppStoreSelected) {
                appleExtensions.RestoreTransactions (onTransactionsRestored);
            }
        }

#region Event Handler

        /// <summary>復元完了</summary>
        private void OnTransactionsRestored (bool success) {
            Debug.Log ($"Purchaser.Restored {success}");
        }

        /// <summary>iOS 'Ask to buy' 未成年者の「承認と購入のリクエスト」 承認または却下されると通常の購入イベントが発生する</summary>
        private void OnDeferred (Product product) {
            Debug.Log ($"Purchaser.Deferred {product.GetProperties ()}");
        }

        /// <summary>初期化完了</summary>
        public void OnInitialized (IStoreController controller, IExtensionProvider extensions) {
            Debug.Log ($"Purchaser.Initialized {controller}, {extensions}");
            appleExtensions = extensions.GetExtension<IAppleExtensions> ();
            appleExtensions.RegisterPurchaseDeferredListener (OnDeferred);
            googlePlayStoreExtensions = extensions.GetExtension<IGooglePlayStoreExtensions> ();
            this.controller = controller;
            this.extensions = extensions;
            Inventory = new Inventory { };
            foreach (var product in controller.products.all) {
                Inventory [product] = possession (product);
            }
        }

        /// <summary>初期化失敗</summary>
        public void OnInitializeFailed (InitializationFailureReason error) {
            Debug.LogError ($"Purchaser.InitializeFailed {error}");
            Unavailable = true;
        }

        /// <summary>課金失敗</summary>
        public void OnPurchaseFailed (Product product, PurchaseFailureReason reason) {
            Debug.LogError ($"Purchaser.PurchaseFailed Reason={reason}\n{product.GetProperties ()}");
        }

        /// <summary>課金結果 有効な消耗品なら保留、それ以外は完了とする</summary>
        public PurchaseProcessingResult ProcessPurchase (PurchaseEventArgs eventArgs) {
            var validated = ValidateReceipt (eventArgs.purchasedProduct);
            Debug.Log ($"Purchaser.ProcessPurchase {(validated ? "Validated" : "ValidationError")} {eventArgs.purchasedProduct.GetProperties ()}");
            Inventory [eventArgs.purchasedProduct] = validated;
            return (validated && eventArgs.purchasedProduct.definition.type == ProductType.Consumable) ? PurchaseProcessingResult.Pending : PurchaseProcessingResult.Complete;
        }

        /// <summary>破棄</summary>
        ~Purchaser () {
            Debug.Log ("Purchaser.Destruct");
            if (instance == this) {
                instance = null;
                Inventory = null;
                Unavailable = false;
            }
        }

#endregion

    }

    /// <summary>製品拡張</summary>
    public static class ProductExtentions {

        /// <summary>製品諸元</summary>
        public static string GetProperties (this Product product) {
            return string.Join ("\n", new [] {
                $"id={product.definition.id} ({product.definition.storeSpecificId})",
                $"type={product.definition.type}",
                $"enabled={product.definition.enabled}",
                $"available={product.availableToPurchase}",
                $"localizedTitle={product.metadata.localizedTitle}({product.metadata.shortTitle ()})",
                $"localizedDescription={product.metadata.localizedDescription}",
                $"isoCurrencyCode={product.metadata.isoCurrencyCode}",
                $"localizedPrice={product.metadata.localizedPrice}",
                $"localizedPriceString={product.metadata.localizedPriceString}",
                $"transactionID={product.transactionID}",
                $"Receipt has={product.hasReceipt}",
                $"Purchaser.Valid={Purchaser.Valid}",
                $"Receipt validation={Purchaser.ValidateReceipt (product)}",
                $"Possession={Purchaser.Inventory [product]}",
            });
        }

        /// <summary>有効性 製品がストアに登録されていることを示すが、ストアで有効かどうかには拠らない</summary>
        public static bool Valid (this Product product) {
            return (product.definition.enabled && product.availableToPurchase);
        }

        /// <summary>アプリ名を含まないタイトル</summary>
        public static string shortTitle (this ProductMetadata metadata) {
            return (metadata != null && !string.IsNullOrEmpty (metadata.localizedTitle)) ? (new Regex (@"\s*\(.+\)$")).Replace (metadata.localizedTitle, "") : string.Empty;
        }

    }

    /// <summary>productID基準でProductの在庫を表現する辞書</summary>
    public class Inventory : Dictionary<string, bool> {

        /// <summary>Productによるアクセス</summary>
        public bool this [Product product] {
            get { return base [product.definition.id]; }
            set { base [product.definition.id] = value; }
        }

    }

}
Sample.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Purchasing;
using UnityInAppPuchaser;

public class Sample : MonoBehaviour {

    [SerializeField] private Transform CatalogHolder = default;
    [SerializeField] private Button RestoreButton = default;

    /// <summary>製品目録</summary>
    private readonly ProductDefinition [] products = new [] {
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item1", ProductType.Consumable),
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item2", ProductType.NonConsumable),
            new ProductDefinition ("jp.nyanta.tetr4lab.unityiaptest.item3", ProductType.NonConsumable),
        };

    /// <summary>起動</summary>
    private void Start () {
        StartCoroutine (initPurchaser ());
    }

    /// <summary>開始処理</summary>
    private IEnumerator initPurchaser () {
        RestoreButton.interactable = false;
        Purchaser.Init (products);
        yield return new WaitUntil (() => Purchaser.Valid || Purchaser.Unavailable); // 初期化完了を待つ
        if (Purchaser.Valid) {
            Catalog.Create (CatalogHolder);
            foreach (var product in Purchaser.Products.all) {
                CatalogItem.Create (Catalog.ScrollRect.content, product);
            }
        }
        RestoreButton.interactable = true;
    }

    /// <summary>復元ボタン</summary>
    public void OnPushRestoreButton () {
        if (Purchaser.Unavailable) {
            StartCoroutine (initPurchaser ());
        } else if (Purchaser.Valid) {
            Purchaser.Restore (success => {
                if (!success) { ModalDialog.Create (transform.parent, "リストアに失敗しました。\nネットワーク接続を確認してください。"); }
            });
        }
    }

}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

外付けSSDをUnity Hubのデータ保存先にしてからの不便事項

Unity HubのUnityエディタのインストール先をSSDに変更し、プロジェクトなどもSSD側に保存するように運用し始めたのですが、 不便な事項 と 対応方法 をまとめました

スクリーンショット 2019-08-21 12.22.41.png

環境

PC: MacBook Air
購入したSSD: My Passport Go 500GB

関連記事

外付けSSDに圧迫データ(Unity Asset Storeフォルダ)をお引っ越し(Mac)
外付けSSDにUnity Hubのデータを保存したい(Mac) 【※未解決エラー有】


:ghost:以下、 不便事項 と 対応方法:ghost:

その1

SSDを保存先として新規作成したプロジェクトが開けない

Unity HubでSSDを保存先に指定してプロジェクトを新規作成すると、Unityで開く段階で、エラーが起こり開けません。。

:rolling_eyes: 対応方法

  1. プロジェクトを新規作成する際はいったんPC側を保存先として作成
  2. 作成後、SSDに丸っとコピー
  3. プロジェクトのリストから、PC側が保存先の方を削除、SSD側にコピーしたものを指定してリストに追加

その2

Unity バージョンを追加インストールしようとしてうまくいかない

Unity HubでSSDをインストール先にしている状態で、新しいUnity バージョンをインストールしようとした際、途中でエラー(「不完全または破損したダウンロードファイル」)が起きて失敗してしまう

:rolling_eyes: 対応方法

おそらく、インストール画面からマウスを離して違う画面を見たりしてしまうと、このエラーが発生してしまう
(バックグラウンド処理に移行するとうまくいかない。。?)
なので、

インストールが終わるまで、インストール画面を選択して最前面に表示させたまま、他の作業を何もしない:hugging:

その3

Unity バージョンにモジュールを追加しようとしても全然進まない

Unity HubでSSDをインストール先にしている状態で、Unity バージョンにモジュールを追加しようとした際、全然青いバーが進まない

:rolling_eyes: 対応方法

その2と同様の原因なので、

モジュール追加が終わるまで、インストール画面を選択して最前面に表示させたまま、他の作業を何もしない:hugging:

その4

iOS で Build And Run するとエラーになる

SSDに保存してあるプロジェクトを iOS に Switch Platform して、BuildのSave先をSSDにして、Build And Run をクリックすると、Buildの途中で
BadImageFormatException: Format of the executable (.exe) or library (.dll) is invalid.
というエラーが出てきてしまいBuildが完了できない

:rolling_eyes: 対応方法

  1. Unityプロジェクトを丸っとPC側にコピー、プロジェクトリストにPC側にコピーしたものを追加してUnityエディタで開く
  2. BuildのSave先もPC側を指定して、Build And Runして実機確認

まだ iOS しか Build していないので他Buildでも起こるかもしれないのですが、
その1と同じような原理が原因なのでしょうか。
BuildのSave先をPCに変えるだけだと同じくエラーになってしまうので、プロジェクトをPC側に移動する必要があります。


:alien:振り返り:alien:

比較的安めのSSDを買ったのが悪かったのでしょうか。。
それとも違う理由でしょうか。。

でもとりあえずはこのまま上手に付き合っていく方法を探していきたいと思います。

以上!prz

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityアセット ProTexで楽々テクスチャ作成!

はじめに

この記事はUnity アセット真夏のアドベントカレンダー 2018 Summer! の 24 日目の記事です。

昨日は ooee オーエー@個人ゲーム開発者さんの「Odin – Inspector and Serializerでマスターデータを作ろう!」でした。

Unityでゲームを作る際に大変な事の一つと言えば、テクスチャ作成だと思います。
ネットでフリーのテクスチャを探してもイメージに合ったものが見つからなかったり、自分でテクスチャを作ろうにも、それなりに技術はいるし、時間がかかる...
そんな悩みを解決してくれるのが、この記事で紹介するProTexという無料アセットです。

ProTex
image.png

ProTexとは

ProTexは、UnityのWindow上でノードベースでTextureを作るためのアセットです。
主な利点は二つあります。
・質の良いテクスチャをノードベースで、かなり簡単に作れる。
・Textureあらかじめ生成せずに、実行時に生成するので、プロジェクトの軽量化に繋がる。(NormalやEmitionのマップを保存する必要がない)

1. アセットのインポート

まず、アセットをインポートします。
全てインポートしてください。
無料アセットです。

2. ProTexアセットの作成

下の写真のように、右クリック→Create→ProTex→ProTexTextureをクリックしてください。
image.png

作ったアセットを開くと下のようなWindowが出てくるはずです。
Inspectorの説明はもう一つの写真の通りです。
image.png
20190819_145604271_iOS.png

3. ノードの説明

新しく作ったWindowの中で右クリックすると、ノードを選んで作ることが出来ます。
それぞれのノードが持つ効果は、言葉の意味の通りなのですが、分かりにくいノードの使い方は説明しておこうと思います。
また、ノードの効果はアセット付属のマニュアル(英語)の写真をみれば、だいたい分かると思います。
image.png
ノードにはそれぞれのinputoutがあり、それらをつないで、Outputノードにつなげる事でテクスチャを作っていきます。
また、それぞれノードは詳細が設定できます。
しかし、そこで赤いApply Changesボタンを押さないと設定の変更が反映されないので、注意してください。
image.png

Uniform Color    Colorize  

単色をoutします。     後ほど説明。

Blur      Grayscale

ぼかします。    カラーを白黒にします。

ProTex Texture 

Examplesのテクスチャをoutします。  

interpolate  

AとBの中間値をoutします。

High pass    Low pass

明るい部分以外黒に  暗い部分以外黒に 

offset

X,Y座標を移動させます。

4. 基本的なテクスチャの作り方

それでは、基本的なテクスチャの作り方を説明します。
流れとしては下のようになります。

白で形を作る

*AlphaをOperationにする事で透過テクスチャを作れます。
image.png

色を塗る

基本はColorizeノードを使います。
Colorizeノードの使い方をは下の通りです。
20190819_150224146_iOS.png

背景などを整える

ここでProTex Textureノードを使うと楽だと思います。
このアセットにはたくさんのデモテクスチャがあって、それらをこのノードで、使うことが出来ます。
最終的にノードをoutputノードにつなげることで、プレビューに反映されます。
適当に整えたのが下の写真です。
image.png

5. 作ったテクスチャの使用方法

では、最後に作ったテクスチャの使用方法を説明していきます。
2つの使い方があります。

一つはinspectorからボタンをおして、生成する物です。

もう一つは、Corponentで実行時に生成して使います。
MaterialをつけたオブジェクトにPro Tex Material Binderを入れるだけで、Materialにテクスチャが入っていきます。
image.png

最後に

長くなってすみませんでした。
ペイントソフトが使えなくても、簡単にテクスチャを作れるのは、本当に便利です。
少し慣れる必要があると思いますが、使いやすいので、ぜひ試してみてください。

明日はさとやんさんの「Animancerで楽々アニメーションプログラミング」です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[iOS13] ASWebAuthenticationSession用いたOAuth認証の変更点

はじめに

OAuth認証やASWebAuthenticationSession、などについてはここではふれません。
ASWebAuthenticationSession用いたOAuth認証をiOS12で実装しており、これからiOS13に対応する方法の一例を書きたいと思います。

変更点

  • ASWebAuthenticationSessionstart を呼び出す前に、 presentationContextProvider を設定しなければいけない

In macOS, and for iOS apps with a deployment target of iOS 13 or later, after you call start, the session instance stores a strong reference to itself. To avoid deallocation during the authentication process, the session keeps the reference until after it calls the completion handler.

For iOS apps with a deployment target earlier than iOS 13, your app must keep a strong reference to the session to prevent the system from deallocating the session while waiting for authentication to complete.

参照元

これにより、以下の書き換えが必要になります。

iOS12.m
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:[NSURL URLWithString:url] callbackURLScheme:nil completionHandler:^(NSURL *callbackURL, NSError *error) {
    // SessionToken の取得など
    self.webAuthenticationSession = nil;
}];
[self.webAuthenticationSession start];

iOS13.m
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:[NSURL URLWithString:url] callbackURLScheme:nil completionHandler:^(NSURL *callbackURL, NSError *error) {
    // SessionToken の取得など
    self.webAuthenticationSession = nil;
}];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000
if (@available(iOS 13.0, *)) {
    self.webAuthenticationSession.presentationContextProvider = uiViewController; // uiViewControllerについては後で記載
}
#endif
[self.webAuthenticationSession start];

以下のように presentationContextProviderASWebAuthenticationPresentationContextProviding Protocol を実装したインスタンスを設定すればいいだけです。

self.webAuthenticationSession.presentationContextProvider = uiViewController;

例えば

HogeLoginViewController.h
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

API_AVAILABLE(ios(13.0), macos(10.15))
@interface HogeLoginViewController : UIViewController <ASWebAuthenticationPresentationContextProviding>

@end
#endif
HogeLoginViewController.m
#import "HogeLoginViewController.h"

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

@implementation HogeLoginViewController {

}

...

- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
    return view.window;
}
...

@end

#endif

問題点

これだと、認証システム基盤を作成するには、問題があります。
APIの引数にUI要素のものを渡し他場合、以下のような問題が出てくるからです。

  • Androidと共通基盤の場合、引数が異なる
  • 引数にUI要素なものを渡さないといけない?
  • Unityなどで利用する場合、どうするの?

解決策は簡単で、 UIViewController を使わないだけです。

解決策

HogeBridgeAPI.h
#import <Foundation/Foundation.h>

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

API_AVAILABLE(ios(13.0), macos(10.15))
@interface HogeBridgeAPI : NSObject <ASWebAuthenticationPresentationContextProviding>

@end
#endif
HogeBridgeAPI.m
#import "HogeBridgeAPI.h"

#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 130000

@implementation HogeBridgeAPI {

}

- (ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
    return UIApplication.sharedApplication.keyWindow;
}

@end
#endif

HogeBridgeAPIなるものを作成します。

return UIApplication.sharedApplication.keyWindow;

presentationAnchorForWebAuthenticationSession の戻り値は、 UIApplication.sharedApplication.keyWindow とすることで UIViewController を使わくても大丈夫です。

あとは、 コンストラクタなどで HogeBridgeAPI を生成

self.webAuthenticationSession.presentationContextProvider = self.bridgeAPI;

などしてあげれば解決します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】MonoBehaviour.Reset()の使い方

始めに

自作スクリプトにインスペクタでコンポーネントをぽちぽち割り当てていくのが面倒くさい…。
そんなときに使えるのがMonoBehaviourにあるReset()というメッセージ。
意外と融通が利いて便利なのですが、あまり使われていない?気がしますので、使い方の例を含めて紹介させて頂きます。

Reset()とは

https://docs.unity3d.com/ja/current/ScriptReference/MonoBehaviour.Reset.html
要するにエディタでコンポーネント初期化のときに呼ばれるメソッドです。
具体的にはインスペクタで「Add Component」したときだったり、
右クリック or 歯車アイコンをクリックして出てくるメニューから「Reset」を選択したときに呼ばれます。
reset_addcomponent.png
reset_menu.png

使い方

主にSerializeFieldの値を初期化するのに使います。
といっても宣言で値を代入しておけばAdd Component/Resetしたときに勝手にその値にしてくれるので、
それで事足りるならReset()を用意する必要はありません。

    [SerializeField]
    private int _testValue = 100;   // 特に何もしなくてもResetしたら100に戻る

宣言で代入できない値での初期化に使う

宣言で代入できない値をデフォルトにしたい(そしてそれを任意でインスペクタから変更できるようにもしたい)、そんなときがReset()の出番です。

ResetTest.cs
using UnityEngine;

public class ResetTest : MonoBehaviour
{
    [SerializeField]
    private Rigidbody _rigidbody = null;

    [SerializeField]
    private Transform _child = null;

    [SerializeField]
    private Camera _mainCamera = null;

    [SerializeField]
    private Light _lightInScene = null;

    [SerializeField]
    private Texture _textureInResources = null;

    [SerializeField]
    private Texture _textureInAssets = null;

    [SerializeField]
    private float _rigidbodyMass = 0f;

    private void Reset()
    {
        // 一緒に付いているコンポーネントをセットする
        _rigidbody = GetComponent<Rigidbody>();

        // 子オブジェクト「Child」をセットする
        _child = transform.Find("Child");

        // メインカメラをセットする
        _mainCamera = Camera.main;

        // シーン中のLightコンポーネントをセットする
        _lightInScene = GameObject.FindObjectOfType<Light>();

        // Resources中のTextureアセットをセットする
        _textureInResources = Resources.Load<Texture>("texture");

#if UNITY_EDITOR
        // Resources外のTextureアセットをセットする
        _textureInAssets = UnityEditor.AssetDatabase.LoadAssetAtPath<Texture>("Assets/Textures/texture.png");
#endif

        // 参照取得済み他コンポーネントの値を使ってセットする
        if (_rigidbody != null)
        {
            _rigidbodyMass = _rigidbody.mass;
        }
    }
}

これをAdd Conponentしてやると、こんな感じで勝手に設定済みの状態にしてくれます。reset_test.png
もちろん、この後各フィールドを変更するのも自由ですし、また上記の状態にResetすることもできます。

他コンポーネントの設定値の変更に使う

さらには、他のコンポーネントの設定値を変更することもできます。

    private void Reset()
    {
        // Transformの位置を変更
        transform.position = new Vector3(100f, 100f, 100f);

        // Rigidbodyのconstraintsを変更
        var rigidbody = GetComponent<Rigidbody>();
        if (rigidbody != null)
        {
            rigidbody.constraints = RigidbodyConstraints.FreezeRotation;
        }
    }

ただこれは相手側のコンポーネントからすれば、非実行時なのに外部から突然設定を書き換えられてしまうということですから、意図しない挙動に繋がる可能性もあります。
使いどころは要検討と言えるでしょう。

それ以外に使う

そもそも、エディタモードで実行できることなら大体できるみたいです。

    private void Reset()
    {
        // Add Componentしたと思ったらエディタが終了するトラップみたいなコンポーネント
        // !!!絶対やめましょう!!!
#if UNITY_EDITOR
        UnityEditor.EditorApplication.Exit(0);
#endif
    }

注意点

と、便利なReset()ですが一つ注意点があります。
それは呼び出されるのはエディタモードのときのみということ。
つまり、ランタイムでスクリプトからAddComponent()した場合は呼び出されません。

        gameObject.AddComponent<ResetTest>();   // ResetTest.Reset()は呼び出されない

この場合、SerializeField各値は宣言での代入値(上のResetTest.csの例で言うとnull(None)とか0fとか)になります。

なお参考までに、やろうと思えばReset()を直接呼び出すことはできます。
(publicにすればクラス外からでも呼び出し可能)

        gameObject.AddComponent<ResetTest>().Reset();   // Reset()はpublicで定義

ただ、これはReset()をどこでも自由に呼び出せるということになりますから、
もし別の誰かが内部値のクリアメソッドか何かと勘違いして呼び出したりして、それ以降インスペクタでやった設定がリセットされた状態で動作していることに気づかなかったりすると、バグに繋がりそうな気がしないでもないです。
私見ではインスペクタ設定の補助程度の範囲で使った方が無難かなと思います。

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityでPostgreSQLを使う

環境

Unity 2017.1
Windows10
PostgreSQL 9.6.3
.Net 4.6

前提

PostgreSQLはインストール済み、データベースとその中身も既にある

やること

1, http://pgfoundry.org/frs/?group_id=1000140 からDLLのZipをダウンロード

基本的には最新版の内、自分の環境にあったものを持ってくる。今回は.Net4.6だったので、「○○-net40.zip」を使った。多分「○○-net45.zip」でも動く

2,zip中の「Npgsql.dll」を対象のプロジェクトのAssets以下のどこかに入れる

3,UnityEditorのdllファイルが置いてある場所から、「System.Data.dll」をコピーして、同様に対象のプロジェクトのAssets以下のどこかに入れる

2017以降だと、
"Unityのダウンロード先\Editor\Data\Mono\lib\mono\2.0"
にある。EditorフォルダにUnityEditorの実行ファイルがあるので、ショートカットから辿ってもいい。

UnityHubだったらHubのメイン画面から「インストール→対象バージョンのメニューバー→エクスプローラーで開く」からダウンロード先フォルダに飛べる

以上で環境構築が完了

ここまでで、対象のプロジェクトがこんな感じになっていればOK

キャプチャ.PNG

コード

using UnityEngine;
using Npgsql;

public class SQLExample: MonoBehaviour
{
    void Start()
    {
        //ここを実行環境に合わせて変える
        string connectionString =
          "Server=ServerName;" +
          "Database=DBName;" +
          "User ID=Id;" +
          "Password=Pass;";

        using (var dbcon = new NpgsqlConnection(connectionString))
        {

            dbcon.Open();
            NpgsqlCommand dbcmd = dbcon.CreateCommand();
            // ここら辺を欲しい内容に合わせて変える
            string sql =
          "SELECT *" +
          "FROM user_list";
            dbcmd.CommandText = sql;
            NpgsqlDataReader reader = dbcmd.ExecuteReader();
            while (reader.Read())
            {
                // ここら辺を欲しい内容に合わせて変える
                string FirstName = (string)reader["first_name"];
                string LastName = (string)reader["last_name"];
                Debug.Log(FirstName + " " + LastName);
            }
            // clean up
            reader.Close();
            reader = null;
            dbcmd.Dispose();
            dbcmd = null;
            dbcon.Close();
        }

    }
}

コードはテストコードをコピーしているので適当
usingが冗長かな…?
再生ボタンを押すとクエリが実行される

UnityでMySQLではなくPostgreSQL を使いたかった

MySQLなら記事がたくさんあったが、PostgreSQLでは一切Unityに関する記事が見つからない
けど、PostgreSQLで過去に作ったDBがあり、どうしてもそこを使う必要があった
実際にやることはそんなに多くないが2時間以上調べるのにかかったのでつらかった

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】【初中級者向け】単純な写経に疲れたあなたへ

写経とは?

プログラミング修行僧による瞑想のような行為である(ちがう)。

はじめに

本記事は、僕が Unity を始めるにあたり、 Unityの教科書 Unity2019完全対応版 2D&3Dスマートフォンゲーム入門講座 (Entertainment&IDEA) を扱った勉強法を紹介する記事となります。
ただ写すだけだとどうにも身にならないし退屈なんだよなぁという方におすすめです。
対象としては、ある程度プログラミングは書けるし、ゲーム作りにも興味あるけど、何から手を付けたらいいかよくわからんのよねー。という人。

環境

  • Windows 10
  • Unity Hub 2.1.0
  • Unity 2019.1.14f1
  • Visual Studio 2017
  • GitHub Desctop

今回の対象の教科書について

とりあえず Unity 始めたいなと思って、最初は何もなくてもいけるやろとタカをくくっていたわけですが、撃沈しました。
それで適当に検索したところ評判の良いこちらの教科書にいきついた流れです。

  • 章立てで別のゲームを作るようになってる
  • ゲーム作りに必要な概念が学べる
  • 挿絵があって読みやすい

評判が高いだけあってかなり良書です。

この教科書にマッチした勉強法

章立てで作ることになるテーマのゲームを 「教科書を見ずに」自分で作る 以上です。
最初の教科書のテーマは「占いルーレット」でした。
そのままゲームテーマを丸パクリというのもコンプライアンス的にまずいのかなぁという思いもあり、数字当てルーレットに変えました。

成果物

kurukuru3.gif

レポジトリ
https://github.com/mentol310/MyRouletteGame

手順

1. クラス設計する

まず何よりも設計が大事です。
設計の見通しが立っているだけで最後までやりきれるかがだいぶ変わってきます。

↓の記事を参考にしました。
グローバルゲームジャムでクラス設計をやった話2019

roulette_class.png

最終的に、 Input, Output namespace はクラス化しない方が今回はシンプルかなと思ってクラス化を避けたりしましたがおおよそ最初に立てたこの設計通りに出来上がりました。

クラス図には、web 版の plantuml かけるツール planttext を利用しました。
ユーザー登録もできて uml 管理機能もありとても便利です :hand_splayed_tone2:

2. git レポジトリを作成する

基本 git は cli 操作でやってたんですが、今回 GUI の GitHub Desktop を使ってみました。
GitHub のアカウントでログインしたら後は New Repository から生成先ディレクトリを選択して、レポジトリ名を入力します。
Unity テンプレートの .gitignroe も選択可能なので便利でした。

3. ゲームを作っていく

あとは作っていくのみです。
今回は以下の流れでようやっと出来上がりました。
(実装にあたり UniRX を使ったためアセットストアからDLしました。)

  1. 画面中央に円(ルーレットホイール相当)を置く(ここから詰みそうで泣きました)
  2. ホイールの中に円(ボール相当)を置く
  3. ボールがホイールの中で回るようにする
  4. ボールの状態(止まってる/回ってる)をホイールで購買できるように
  5. テキスト入力欄を置く
  6. テキスト入力状態が購買できるように
  7. テキスト入力状態によりボタン制御を行う
  8. ボタン制御によりボールが回るように
  9. 四角形の上に数字が表示されるように(出目相当)
  10. 出目をプレハブ化
  11. ホイールの子要素として、プレハブ化した出目を動的に生成する
  12. ボールに一番近い出目を取得できるように
  13. ボールに一番近い出目出力をボール位置の変動に伴い変動するように
  14. 止まった出目と予想値が当たってたら当たり、ハズレてたらハズレが表示されるように

todo として最初やろうかなと思ってた掛け金の処理とか回す対象を入れ替えれるようにする対応を行おうと思いましたが、一旦このテーマでこれ以上の知見を得るのは難しそうなので完としました。

ほぼほぼ 0 からのスタートだったとはいえだいぶかかりました。。。

詰まったところは以下です。

画面中央に円(ルーレットホイール相当)を置く

Asset > Sprites > Circle から作成できます。
Unity で何かしらのオブジェクト = 全て「GameObject」という先入観が何故かあり、GameObject メニューをずっといじってたんですが、「ないじゃん!」となって泣いてました。

ホイールの中に円(ボール相当)を置く

オブジェクトが重なっているため、Order in Layer で描画順を調整する必要があります。
さっき円の作り方はわかったしこれは楽勝やろ!と思ってたんですが、「ホイール相当の円しか表示されない」という状態になりました。
「手前に写したいオブジェクトの Order 数値を上げる。」で解決しました。

ボールの状態(止まってる/回ってる)をホイールで購買できるように

クラス設計にて 「ディーラーがホイールを監視してホイールの上で転がってるボールが止まったら~」と Dealer から Wheel 越しに Ball.State を Subscribe する必要がありましたが、なんか色々エラーが出て参ってました。
この辺の話になります。

四角形の上に数字が表示されるように

「壁に文字を表示」だったり「オブジェクトに文字を貼り付ける」だったり検索すると情報はたくさん出てくるんですが、どうもうまくいきいませんでした。
調べてみると、「World Space 設定にした Canvas 毎オブジェクトの子要素とする。その後、大きさと位置を調整する。」というのがありました。
この「大きさと位置」ではっきりした数値が書かれてるところが中々なくて認識不足が原因でした。
ひとまず位置は全部 0, scale は x, y を 0.02 へ width, height を 680 x 480 へとすれば見えるようになります。

その他得た知見

詰まったところも多々あれば素直に調べた記事通りやると実行できた!というのも多かったです。

  • [SerializeField] をつけるとクラスとしては private で閉じてるけど UI からは設定できるというのができて安全
  • ReactiveCommand への BindTo でボタンと紐付けできる
  • RotateAround 第二引数で回転方向を選べる
  • Time.deltaTime は前フレームから経った時間の取得
  • Visual Studio のインテリセンスはかなり高い(昔 2007 とか扱ってた時の重い、扱いづらいのイメージがあったので革新的でした)
  • 文字列変換埋め込み便利 $""
  • Linq, UniRX の基本的な知見

...... etc

最後に

何気なくやってみるかでやってみた割にはかなり多くの学びがありました。
「何か」を始めるスイッチを探してる人にホントおすすめです!

あとC#たのしい :raised_hand:

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UIElementsでもUniRxを使いたい

はじめに

これは『Unityゆるふわサマーアドベントカレンダー2019』の21日目の記事です。
前日は@kingyo222さんの『Unity:UI Elements でいくつかサンプル書いたよ!』でした。
翌日は @Sunmax0731 さんの『Unityとセンシングについて何か』です。

UnityにおけるUI実装パターンおさらい

IMGUI

おそらく、エディタ拡張でおなじみの文法ではないでしょうか。
これはエディタ拡張だけじゃなく、ゲーム中でも使用可能なのですが、

https://docs.unity3d.com/ja/current/Manual/GUIScriptingGuide.html

IMGUI システムは通常、プレイヤーが操作する普通のゲーム内ユーザーインターフェイスに使う事は意図されていません

ここにも書かれている通り、今はこれでゲームのUIを組んでいくことはほとんどないでしょう。
ゲーム中でボタンを表示するには以下のようにMonoBehaviourやらにOnGUIを定義します。

public class ImguiSampleScene : MonoBehaviour
{
    void OnGUI()
    {
        if (GUILayout.Button("Button"))
        {
            Debug.Log("にゃんぱすー");
        }
    }
}

イベントを登録できるわけではなく、ループでの管理になるので、ボタンのステートの管理が別途必要になって非常にスパゲティがゆでやすい!

あと、当然のように ゲームを再生しないとボタンが表示されない ので、デバッグ用のUIを作るにしてもIMGUIは厳しいかなぁ、という感覚です。
今のUnityは多重シーンロードも容易く管理できますので、DebugMenuのようなシーンを用意し、LoadSceneでAdditiveに読み込んであげるのがいいと思います。

NGUIの覇権時代(Unity 4.5以前)

Unity 4.5までは標準のUIシステムが上記のIMGUIしかなく、NGUIなどのプラグインをアセットストアで買って使っていました。
私も昔は使っていたのですが、NGUIのなにもかもを忘れているので、きっと今後も使わないでしょう。
なので割愛(ひどい)。

uGUIの誕生と発展(Unity 4.6以降)

Unity 4.6でようやくUnity標準のUI機能が追加されました。
上記のNGUIの作者がUnityに入り、NGUIから枝分かれするような形で作ったものらしいです。

何はともあれ、標準のものができて、めでたしめでたし、ということで使ってみましょう。

public class UguiSampleScene : MonoBehaviour
{
    public Button Button;

    void Awake()
    {
        Button.onClick.AddListener(() => Debug.Log("にゃんぱすー"));
    }

    private void OnDestroy()
    {
        Button.onClick.RemoveAllListeners();
    }
}

イベントを登録できるようになって非常に良くなりました!
しかもIMGUIと違って 再生しなくてもSceneビューにボタンが配置される! 素敵!

これができれば、オブザーバパターンでViewとModelを分離もできるので、してみます。

public interface IObserver
{
    void Notify();
}

public class UguiSampleScene : MonoBehaviour, IObserver
{
    public Text Text;
    public Button Button;

    private UguiSampleModel model;

    void Awake()
    {
        // Observerに登録する
        model = new UguiSampleModel(this);

        Button.onClick.AddListener(() => model.IncrementCount());
    }

    private void OnDestroy()
    {
        Button.onClick.RemoveAllListeners();
    }

    // モデルから更新依頼が呼ばれたらUIに反映させる
    void IObserver.Notify()
    {
        Text.text = model.Count.ToString();
    }
}

public class UguiampleModel
{
    public int Count { get; private set; }

    private IObserver observer;

    public UguiSampleModel(IObserver observer)
    {
        this.observer = observer;
    }

    public void IncrementCount()
    {
        Count++;

        // Countが増えたので、Viewを更新して、とお願いする
        observer.Notify();
    }
}

最低限のObserverパターンでの実装を上でやってみました。
キモとなるのはIObserverで、Modelは自分の状態が更新されたらとにかくNotifyを呼ぶ、MonoBehaviourはNotifyを受けたらとにかくUIを更新する、ということをすることにより、ViewとModelを分離しています。
今はこれがIncrementCountのみですが、Modelの機能が増えてくるとスパゲティがゆでやすくなりそうだな、というのは容易に想像できるかと思います。

UniRxの登場(Unity 5以降)

UniRxの登場はUnityにおけるUIの歴史において一つの大きなものだったと言って過言ではないと思います。
そもそもTaskの時代になっても一向に非同期処理ができなかったUnity上で非同期処理を行うものとして使っていた方もおられると思うのですが、RxはもともとLINQ to Eventsとか呼ばれたりとイベントの処理に長けています。
実際に、上のObserverパターンをUniRxで書き直すとこうなります。

public class UguiSampleScene : MonoBehaviour
{
    public Text Text;
    public Button Button;

    private UguiSampleModel model;

    void Awake()
    {
        model = new UguiSampleModel();

        Button.onClick.AddListener(() => model.IncrementCount());

        // モデルから更新依頼が呼ばれたらUIに反映させる
        model.OnUpdate.Subscribe(_ => Notify());
    }

    private void OnDestroy()
    {
        Button.onClick.RemoveAllListeners();
    }

    void Notify()
    {
        Text.text = model.Count.ToString();
    }
}

public class UguiSampleModel
{
    public int Count { get; private set; }
    public Subject<Unit> OnUpdate { get; } = new Subject<Unit>();

    public void IncrementCount()
    {
        Count++;

        // Countが増えたので、Viewを更新して、とお願いする
        OnUpdate.OnNext(Unit.Default);
    }
}

Subjectを使って書きなおすとこうなります。
本来のオブザーバパターンのObserverとは観察者という意味なのですが、UniRxでは IObvervable(観察できるもの)をSubscribe(購読)する ことによって、オブザーバパターンを実装します。

このIObservableが実は素晴らしく、以下のようにボタンのイベントも購読できます。

// Button.onClick.AddListener(() => model.IncrementCount());
Button.OnClickAsObservable()
    .Subscribe(_ => model.IncrementCount());

また、SubscribeはIDisposableを使った寿命の管理もできます。

// Disposableを貰う
var disposable = Button.OnClickAsObservable()
    .Subscribe(_ => model.IncrementCount());

// DisposeすることがRemoveListenerと同じ役目
disposable.Dispose();

Rxでは寿命の管理も大事なので、複数のIDisposableを管理する機能などが複数存在します。

https://blog.xin9le.net/entry/2014/02/10/120619

xin9leさんが各機能を細かくまとめてくれててめっちゃ助かるので、是非一度見てみてください。

さらにUniRxにはReactivePropertyやSubscribeTo、AddToなどの便利機能もあります。
これらを使って書きなおすとこうなります。

public class UguiSampleScene : MonoBehaviour
{
    public Text Text;
    public Button Button;

    private UguiSampleModel model;

    void Awake()
    {
        model = new UguiSampleModel();

        Button.OnClickAsObservable()
            .Subscribe(_ => model.IncrementCount())
            .AddTo(this); // 寿命はこのGameObjectが消えるまで

        model.Count
            .SubscribeToText(Text) // テキストにSubscribe
            .AddTo(this); // 寿命はこのGameObjectが消えるまで
    }
}

public class UguiSampleModel
{
    public ReactiveProperty<int> Count { get; } = new ReactiveProperty<int>();

    public void IncrementCount()
    {
        Count.Value++; // .Valueを書き換えると、OnNextが呼ばれる
    }
}

ここまでくると、UniRxを使うと、自前でNotifyしてたオブザーバパターンに比べてシンプルに書ける、というのが実感できるかと思います。

UIElementsへの移行(Unity 2018.3以降、Unity 2020.x以降予定)

さて、なぜここだけ以降が2種類あるかといいますと、UIElementsは既にEditor用としてはリリースされているのですが、ハイ、アプリ側でもUnity 2020で使えるようになるらしいです。
ただIMGUIとuGUIもメンテはされるようで廃止という話にはすぐならないそうで、当然2020年リリースですから、2021年ぐらいにリリースされるアプリまではuGUIバリバリの現役ではないでしょうか…?

しかし、だからと言ってUIElementsから逃げられるかというと、そんなことはないはずで、Prefab in Prefabが登場しようが再利用できるテンプレートには絶対勝てないのは目に見えているので、さっさと勉強を始めましょうと思い、自分も触ってみました。

どうも、UXMLという見た目の定義ファイルをまず用意しなきゃダメのようです。

<?xml version="1.0" encoding="utf-8"?>
<engine:UXML
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:engine="UnityEngine.UIElements"
    xmlns:editor="UnityEditor.UIElements"

xsi:noNamespaceSchemaLocation="../../../UIElementsSchema/UIElements.xsd"
xsi:schemaLocation="
                        UnityEngine.UIElements ../../../UIElementsSchema/UnityEngine.UIElements.xsd
                        UnityEditor.UIElements ../../../UIElementsSchema/UnityEditor.UIElements.xsd
                        UnityEditor.PackageManager.UI ../../../UIElementsSchema/UnityEditor.PackageManager.UI.xsd
"
>

<!-- ここから↑はおまじないみたいなもの、ここから下が配置されるパーツ -->

  <engine:Label name="label" text="0"/>
  <engine:Slider name="slider"/>
  <engine:Button name="button" text="Submit"/>

<!-- ここまでが配置されるパーツ -->

</engine:UXML>

image.png

なんかボタンとスライダーの位置が逆な気がしますが、今回は大目に見ましょう(雑)。
これをC#上で扱うにはUQueryなるものを使うといいそうです。

// 他のアセットと同じようにUXMLを読み込む
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Windows/SampleObjectWindow.uxml");
// CloneTreeでInstantiateみたいな感じらしい
var labelFromUXML = visualTree.CloneTree();

// ↑で作ったインスタンスから名前でパーツを引ける
var label = labelFromUXML.Q<Label>("label");
var button = labelFromUXML.Q<Button>("button");

ややこしいことに、このButtonクラスとかはuGUIのButtonクラスとかとは別物です。
なのでUniRxの便利な拡張が使えません…

ので、簡単に作ってみましょう。
まずはButtonから。

public static IObservable<Unit> OnClickAsObservable(this UnityEngine.UIElements.Button source)
{
    return Observable.FromEvent(
        h => h,
        h => source.clickable.clicked += h,
        h => source.clickable.clicked -= h);
}

Observable.FromEventを使うと、このような感じで任意のイベントをIObservableにできます。
上で話した通り、IObservableにしてしまえばあとはこちらのもので、Subscribeしましょう!

var button = labelFromUXML.Q<Button>("button");
button.OnClickAsObservable()
    .Subscribe(_ => Debug.Log("Nya"))
    .AddTo(this);

これでUIElementsをuGUIと似たような感じで取り扱うことができました。
IObservable最高!

次にSliderの方のイベントもIObservableにしてしまいましょう。
ただし、Sliderをそのままするのではなく、これらのものはINotifyValueChangedというインターフェイスを実装しているらしいので、これに対して拡張メソッドを定義します。

public static IObservable<ChangeEvent<T>> OnValueChange<T>(this INotifyValueChanged<T> source)
{
    return Observable.FromEvent<EventCallback<ChangeEvent<T>>, ChangeEvent<T>>(
        h => new EventCallback<ChangeEvent<T>>(h),
        h => source.RegisterValueChangedCallback(h),
        h => source.UnregisterValueChangedCallback(h));
}

public static IObservable<T> OnValueChanged<T>(this INotifyValueChanged<T> source)
{
    // ↑のChangeEvent<T>がpreviousValueとnewValueを持ってるけど、
    // 欲しいのは大体newValueだと思うので、ここで変換する
    return source.OnValueChange().Select(x => x.newValue);
}

// ついでにSubscribeToも定義しておく
public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextElement text)
{
    return source.SubscribeWithState(text, (x, t) => t.text = x.ToString());
}

できました!
これを使ってSubscribeしてみましょう。

var label = labelFromUXML.Q<Label>("label");
var slider = labelFromUXML.Q<Slider>("slider");
slider.OnValueChanged()
    .SubscribeToText(label)
    .AddTo(this);

sample.gif

IObservable最高!(大事なことなので…)

まとめ

IObservable最高!

あとこれ単体だと記事が長くなるかと思って、補足記事として『コチラ』もご覧ください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityの各種イベントをIObservable<T>に変換する

この記事は、これは『Unityゆるふわサマーアドベントカレンダー2019』の21日目の記事、『UIElementsでもUniRxを使いたい』の補足記事です。

Subjectを使ってIMGUIをIObservable化

単純にButtonのクリックイベントをIObservable化します。
する前はこんな感じ。

public class ImguiSampleScene : MonoBehaviour
{
    bool previousPressed;

    void OnGUI()
    {
        var currentPressed = GUILayout.Button(Text);
        if (!currentPressed && previousPressed)
        {
            Debug.Log("Clicked!!")
        }
        previousPressed = currentPressed;
    }
}

これだとボタンの数だけpreviousPressedとcurrentPressedが必要になってしまいます。
そこでラッピングします。

public class IMGUIButton : IDisposable
{
    private Subject<Unit> onClick = new Subject<Unit>();
    public IObservable<Unit> OnClickAsObservable() => onClick;

    private string Text { get; set; }
    private bool previousPressed;

    public IMGUIButton(string text)
    {
        Text = text;
    }

    public void Dispose()
    {
        onClick.Dispose();
    }

    public void Draw()
    {
        var currentPressed = GUILayout.Button(Text);
        if (!currentPressed && previousPressed)
        {
            onClick.OnNext(Unit.Default);
        }
        previousPressed = currentPressed;
    }
}

これを使うと

public class IMGUISampleScene : MonoBehaviour
{
    private IMGUIButton button;

    private void Start()
    {
        button = new IMGUIButton("Button");
        button.AddTo(this);

        button.OnClickAsObservable().Subscribe(_ => Debug.Log("Clicked!!")).AddTo(this);
    }

    void OnGUI()
    {
        button.Draw();
    }
}

こうできます。

uGUIのI***HandlerをIObservable化

通常だとこんな感じでインターフェイスを作ったものをuGUIに配置すると思います。
これもいいのですが、本来このInputというのはゲームのコントローラと一緒で1つでいいものです。
しかし、シチュエーションによって、例えばフィールドであればキャラを動かす、インベントリならアイテムを入れ替えたりする、など、同じイベントで異なる挙動をする場合、このクラス1つにそれらの処理が依存し、密になってしまう可能性があります。

public class InputPresenter : MonoBehaviour, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
    public void OnBeginDrag(PointerEventData eventData) { }
    public void OnDrag(PointerEventData eventData) { /* フィールドならキャラ移動、インベントリならアイテム移動の処理 */ }
    public void OnEndDrag(PointerEventData eventData) { }
    public void OnPointerDown(PointerEventData eventData) { }
    public void OnPointerUp(PointerEventData eventData) { }
    public void OnPointerClick(PointerEventData eventData) { }
}

そこをIObservable化することで疎にしましょう。

public interface IInputEvents
{
    IObservable<PointerEventData> OnDragBegin();
    IObservable<PointerEventData> OnDrag();
    IObservable<PointerEventData> OnDragEnd();
    IObservable<PointerEventData> OnPressPointer();
    IObservable<PointerEventData> OnReleasePointer();
    IObservable<PointerEventData> OnClick();
}

まず欲しいイベント類をinterfaceで定義し、それをuGUIの方で実装します。

[RequireComponent(typeof(RaycastTarget))]
public class InputPresenter : MonoBehaviour, IInputEvents, IDragHandler, IBeginDragHandler, IEndDragHandler, IPointerDownHandler, IPointerUpHandler, IPointerClickHandler
{
    public float ClickThrottleTime = 0.25f;
    private float lastPointerDownTime;
    private bool isDrag;

    private Subject<PointerEventData> beginDrag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDragBegin() => beginDrag;

    private Subject<PointerEventData> drag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDrag() => drag;

    private Subject<PointerEventData> endDrag = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnDragEnd() => endDrag;

    private Subject<PointerEventData> pointerDown = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnPressPointer() => pointerDown;

    private Subject<PointerEventData> pointerUp = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnReleasePointer() => pointerUp;

    private Subject<PointerEventData> pointerClick = new Subject<PointerEventData>();
    public IObservable<PointerEventData> OnClick() => pointerClick;

    public void OnDestroy()
    {
        this.beginDrag.Dispose();
        this.drag.Dispose();
        this.endDrag.Dispose();
        this.pointerDown.Dispose();
        this.pointerUp.Dispose();
        this.pointerClick.Dispose();
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        beginDrag.OnNext(eventData);
    }

    public void OnDrag(PointerEventData eventData)
    {
        isDrag = true;
        drag.OnNext(eventData);
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        endDrag.OnNext(eventData);
    }

    public void OnPointerDown(PointerEventData eventData)
    {
        isDrag = false;
        lastPointerDownTime = Time.realtimeSinceStartup;

        pointerDown.OnNext(eventData);
    }

    public void OnPointerUp(PointerEventData eventData)
    {
        pointerUp.OnNext(eventData);
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        // 長押しやドラッグがクリックと誤検知されるのを防ぐ
        if (isDrag || Time.realtimeSinceStartup - lastPointerDownTime > ClickThrottleTime)
        {
            return;
        }

        pointerClick.OnNext(eventData);
    }
}

これを使う場合は、

public class CharacterMovementModel
{
    private Vector2 startPosition;

    public CharacterMovementModel(IInputEvents input, CompositeDisposable gameObjectLifeTime)
    {
        var dragDirection = Vector2.zero;

        // ドラッグで移動
        input.OnDragBegin()
            .Do(x => startPosition = x.position)  // ドラッグ開始時に指の位置を始点として確保
            .Select(_ => input.OnDrag()) // ドラッグ中に繋げる
            .Switch()
            .Subscribe(x => dragDirection = (x.position - startPosition).normalized) //  ↑の始点との差で方向を取って、スティック入力風にする
            .AddTo(gameObjectLifeTime);
        input.OnDragEnd()
            .Subscribe(x => dragDirection = Vector2.zero) // ドラッグ終了時にお掃除
            .AddTo(gameObjectLifeTime);

        // クリックでジャンプ
        input.OnClick()
            .Subscribe(x => Jump()) // 割愛
            .AddTo(gameObjectLifeTime);
    }
}

という感じで実装することができます。

UIElementsのManipulatorについて

調べたところ、Unity 2019.1.14f1では以下のEventBaseが継承された各種イベントがあるようです。

UnityEngine.UIElements.AttachToPanelEvent
UnityEngine.UIElements.BlurEvent
UnityEngine.UIElements.ChangeEvent<T>
UnityEngine.UIElements.CommandEventBase<T>
UnityEngine.UIElements.ContextClickEvent
UnityEngine.UIElements.ContextualMenuPopulateEvent
UnityEngine.UIElements.CustomStyleResolvedEvent
UnityEngine.UIElements.DetachFromPanelEvent
UnityEngine.UIElements.DragAndDropEventBase<T>
UnityEngine.UIElements.DragEnterEvent
UnityEngine.UIElements.DragExitedEvent
UnityEngine.UIElements.DragLeaveEvent
UnityEngine.UIElements.DragPerformEvent
UnityEngine.UIElements.DragUpdatedEvent
UnityEngine.UIElements.EventBase<T>
UnityEngine.UIElements.ExecuteCommandEvent
UnityEngine.UIElements.FocusEvent
UnityEngine.UIElements.FocusEventBase<T>
UnityEngine.UIElements.FocusInEvent
UnityEngine.UIElements.FocusOutEvent
UnityEngine.UIElements.GeometryChangedEvent
UnityEngine.UIElements.IMGUIEvent
UnityEngine.UIElements.InputEvent
UnityEngine.UIElements.KeyboardEventBase<T>
UnityEngine.UIElements.KeyDownEvent
UnityEngine.UIElements.KeyUpEvent
UnityEngine.UIElements.MouseCaptureEvent
UnityEngine.UIElements.MouseCaptureEventBase<T>
UnityEngine.UIElements.MouseCaptureOutEvent
UnityEngine.UIElements.MouseDownEvent
UnityEngine.UIElements.MouseEnterEvent
UnityEngine.UIElements.MouseEnterWindowEvent
UnityEngine.UIElements.MouseEventBase<T>
UnityEngine.UIElements.MouseLeaveEvent
UnityEngine.UIElements.MouseLeaveWindowEvent
UnityEngine.UIElements.MouseMoveEvent
UnityEngine.UIElements.MouseOutEvent
UnityEngine.UIElements.MouseOverEvent
UnityEngine.UIElements.MouseUpEvent
UnityEngine.UIElements.PanelChangedEventBase<T>
UnityEngine.UIElements.TooltipEvent
UnityEngine.UIElements.ValidateCommandEvent
UnityEngine.UIElements.WheelEvent

ふむむ、IMGUIEventなるものまで、あるんですねぇ…

前日の@kingyo222さんの『Unity:UI Elements でいくつかサンプル書いたよ!』によりますと、Manipulatorなるものがあり、これを継承してVisualElementにAddManipulatorするといい感じにイベントを制御できるそうです。

class MyManipulator : Manipulator
{
    protected override void RegisterCallbacksOnTarget()
    {
        // EventBaseなものを登録するとそれが取れるらしい
        target.RegisterCallback<ContextClickEvent>(OnMouseUpEvent); 
    }

    protected override void UnregisterCallbacksFromTarget()
    {
        target.UnregisterCallback<ContextClickEvent>(OnMouseUpEvent);
    }

    void OnMouseUpEvent(EventBase<ContextClickEvent> evt)
    {
        UnityEngine.Debug.Log("Nya");
    }
}

// 上のManipulatorをこんな感じで追加するとイベントが取れる

var button = labelFromUXML.Q<Button>("button");
button.AddManipulator(new MyManipulator());

個人的には素直にObservable.FromEventで以下のようにする手段の方が好きなんですが、個別にこのOnHogeAsObservableみたいなものを用意しないといけないので、それはそれで面倒です。
そういう意味では標準で用意されているManipulatorは使い勝手がいいと思います。

public static IObservable<Unit> OnClickAsObservable(this Button source)
{
    return Observable.FromEvent(
        h => h,
        h => source.clickable.clicked += h,
        h => source.clickable.clicked -= h);
}

//==

var button = labelFromUXML.Q<Button>("button");
button.OnClickAsObservable()
    .Subscribe(_ => UnityEngine.Debug.Log("Nya"))
    .AddTo(LifeTimeDisposable);

IManipulatorについて

基本イベントを取るだけであればManipulatorを継承しておけばいいと思いますが、AddManipulatorはIManipulatorを引数に取るので、本来はManipulatorより自由度が高いです。

public interface IManipulator
{
    VisualElement target { get; set; }
}

というかんじで、Manipulator(操縦者)の通り、VisualElementをとりあえず操作するだけっぽいです。
UnityEditor上だとWindowが開かれたときにtargetのsetterが呼ばれるようになってます。
なので、UIアニメーションなど(がUIElements上でどう実装するのか、Animatorなのか、他なのかは置いといて)はこのManipulatorで操作してやるのかしら、という印象です。

class ScaleAnimation : IManipulator
{
    private VisualElement _target;
    public VisualElement target
    {
        get { return _target; } // getは素直に返しましょう
        set
        {
            // Manipulatorの方はここでRegister/Unregisterを実装
            this._target = value;
        }
    }

    private float start;
    private float end;
    public ScaleAnimation(float start = 1f, float end = 1f) { // 割愛 }

    public async UniTask PlayAsync() { // 割愛 }
}

// ==

var button = labelFromUXML.Q<Button>("button");
var scaleAnimation = new ScaleAnimation(1f, 2f);
button.AddManipulator(scaleAnimation);

await scaleAnimation.PlayAsync(); // 任意のタイミングでPlayできるし、終了も待てる

おそらく上記のようなやり方だとRxだとやりづらくて、イベントとは別に明確に棲み分けができるものだと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む