- 投稿日:2019-11-25T23:12:22+09:00
Unity + DockerでUnityプロジェクトからAARファイルを自動生成
はじめに
UnityのSceneをAndroidのSubView(FrameLayout)として埋め込むまで
https://qiita.com/_nonono/items/253aa15d6027ecc8ad66
このような記事があり、AndroidにUnityのシーンをFrameLayoutに埋め込むことができます(便利Unityで開発せずビルド成果物(AARファイル)のみ必要な場合
1.指定のバージョンのUnityをインストール
2.Unityを立ち上げAndroidプロジェクトをExport
3.Android Studioでビルド
上の3つの作業を手作業で行うので一苦労だと思います
Unity、Android Studioのバージョンを意識したりしなければならいので良い方法が必要です「なんとか良い方法を!」と思って
Unityの入ったDockerイメージを使ってAARの生成を自動化
してみたので、作業を下に記していきますTL;DR
いきなり一連の作業をスクリプトにできないので、dockerの中で作業を1つずつして、最後にスクリプトにまとめます
やったこと
docker pull gableroux/unity3d
- Dockerコンテナに入りライセンスファイルを用意
- ライセンスファイルをコピー
- ビルド用クラスを作成
- Unityプロジェクトをビルド
- build.gradleとAndroidManifest.xmlを編集
- GradleでAndroidプロジェクトをビルド
- build.shを書いて完全自動化
実際の環境
・MacBook Pro (15-inch, 2016) macOS 10.13.6(17G10007)
・docker desktop ver. 2.1.0.5(40693)1. Unityの入ったDockerイメージをpullする
まずは、Unityの入っているDockerイメージをインターネットから落としてきます
次のコマンドを実行すると最新版のコンテナが自分のローカルに保存されます
Dockerイメージをpulldocker pull gableroux/unity3dしかし、このイメージではAndroidプロジェクトを生成/ビルドができないので、
Unityバージョン
とUnityプロジェクトのビルドターゲット
を指定しなければなりません。この記事では、
Unity 2018.4.5f1
でビルドターゲットをAndroid
とするので、Dockerイメージのtagに2018.4.5f1-android
を指定します
※目的のバージョンのイメージが無い場合があるので、 DockerHub のページで確認しておきましょう目的のDockerイメージをpulldocker pull gableroux/unity3d:2018.4.5f1-android
docker images
を実行するとpullしてきたイメージが確認できますDockerImageの確認cha84rakanal$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE gableroux/unity3d 2018.4.5f1-android 3cc6afa5c2bd 3 months ago 6.7GB
docker run -it {image name} bash
を実行すればイメージからコンテナが作成され、Dokcer内のターミナルに接続できる
なので、接続してUnity
とAndroid SDK
があるかを確認しておきますUnityとAndroidSDKの確認cha84rakanal$ docker run -it --rm gableroux/unity3d:2018.4.5f1-android bash root@7462f939f4d0:/# echo $ANDROID_HOME /opt/android-sdk-linux root@7462f939f4d0:/# ls /opt/ android-sdk-linux Unity Unity-2018.4.5f1 root@7462f939f4d0:/#2. Dockerコンテナに入ったUnity用のライセンスを生成する(1回のみ・手作業)
普段使うmacOSやWindowsでUnityを一番最初に起動すると次の画面が出てアクティベーションをする必要があります
次の作業は、DockerコンテナにインストールされているUnityのアクティベーションを行います
まず、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます
※ パスワードを平打ちするので気をつけましょうDockerコンテナの立ち上げcd /path/to/project docker run -it --rm \ -e "UNITY_USERNAME=username@example.com" \ -e "UNITY_PASSWORD=example_password" \ -e "TEST_PLATFORM=linux" \ -e "WORKDIR=/root/project" \ -v "$(pwd):/root/project" \ gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \ bash次に、Dockerコンテナ内で以下のコマンドを実行します
アクティベーションをするxvfb-run --auto-servernum --server-args='-screen 0 640x480x24' \ /opt/Unity/Editor/Unity \ -logFile /dev/stdout \ -batchmode \ -username "$UNITY_USERNAME" -password "$UNITY_PASSWORD"すると、出力に次のようなXMLが表示されるので、表示されたXMLを
unity3d.alf
というファイル名で保存しますターミナルでの出力LICENSE SYSTEM [2017723 8:6:38] Posting <?xml version="1.0" encoding="UTF-8"?><root><SystemInfo><IsoCode>en</IsoCode><UserName>[...]もし、XMLではなく
401
エラーが出た場合は、2段階認証を切りましょう
Googleでサインイン
やFaceBookでサインイン
でUnity IDを作成している場合でも401
エラーがでるので、メールアドレスから作成したUnityIDを使いましょう401エラーCan't activate unity: No sufficient permissions while processing request HTTP error code 401
unity3d.alf
が用意できたら https://license.unity3d.com/manual にアクセスします。アクセスしたらサイトの指示に従ってunity3d.alf
をアップロード、質問に答えていきましょう。すべて終わるとUnity_v2018.x.ulf
かUnity_v2019.x.ulf
がダウンロードできるので保存しておきます。
このUnity_v*.x.ulfファイル
は今後必要になるので大切に保管しておきますプロジェクトチームの誰かがこの作業をやれば、Unityのバージョンを変えない限り、
3以降の作業でUnity_v2018.x.ulf
を使いまわすことができます(多分※アクティベーションの詳しい手順は このページ に書いてありますが、まとめとしてこの記事にも記しています
3. Dockerコンテナに入ったUnityにライセンスを当てる
2の手順でダウンロードしてきたファイル
Unity_v2018.x.ulf
をUnityプロジェクトのルートディレクトにおきますUnity_v2018.x.ulfの設置/path/to/project ├── Assets ├── Library ├── Logs ├── Packages ├── ProjectSettings ├── README.md └── Unity_v2018.x.ulf次に、Unityプロジェクトジェクトのディレクトリに移動してdockerコンテナを立ち上げます
Dockerコンテナの立ち上げcd /path/to/project docker run -it --rm \ -v "$(pwd):/root/project" \ gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \ bashここから先の作業7.が終わるまでは、dockerコンテナ内のターミナルで
exit
をしてはいけませんライセンスファイルをUnityプロジェクトのルートディレクトに設置できたら、dockerコンテナ内のターミナルで次のコマンドを実行します。これでライセンスファイルの設置は完了です
Unityのライセンスファイルを設置set -e set -x mkdir -p /root/.cache/unity3d mkdir -p /root/.local/share/unity3d/Unity/ set +x cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf4. ビルド用クラスを作成してAssets/Scripts/Editorに配置
Unityプロジェクトをコマンドラインからビルドできるように次のC#ソースを
Assets/Scripts/Editor
に設置しますApplicationBuild.csusing UnityEngine; using System; using System.Linq; public static class ApplicationBuild { private static string[] GetAllScenePaths() { return EditorBuildSettings.scenes .Where(scene => scene.enabled) .Select(scene => scene.path) .ToArray(); } public static void AndroidBuild() { string[] scenes = GetAllScenePaths(); BuildPipeline.BuildPlayer(scenes, "./Build/", BuildTarget.Android, BuildOptions.AcceptExternalModificationsToPlayer); } }公式ドキュメント を確認すると、 ビルドのパラメータを色々設定できます。
今回は、プロジェクトのエクポートパスは./Build
、ビルドターゲットはAndroidなのでBuildTarget.Android
、AndroidのビルドはExternalで行うのでオプションにBuildOptions.AcceptExternalModificationsToPlayer
を指定していますBuildPipeline.BuildPlayer
BuildPlayer(EditorBuildSettingsScene[] levels, string locationPathName, BuildTarget target, BuildOptions options)
パラメータ levels The Scenes to be included in the build. If empty, the currently open Scene will be built. Paths are relative to the project folder (Assets/MyLevels/MyScene.unity). locationPathName 成果物の保存先のパス target ビルドする BuildTarget options ビルドしたプレイヤーを実行するか、などの追加の BuildOptions 5. コマンドラインからUnityプロジェクトをビルド
次のコマンドでUnityプロジェクトをコマンドラインからビルドできます
コマンドラインからUnityプロジェクトをビルド/opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuildログは
./build.log
に保存されるので、エディタ等で確認するとビルドが進んでるのがわかります
ビルド中は、ターミナルに何かこれといって表示されるものは無いです(無視できるエラーとかは出るビルドが完了するとログに次の行が記録されます
build.logExiting batchmode successfully now!※成果物の保存先のパス
./Build
ディレクトリがすでにあるとビルドが停止してしまうので消しておきましょう6. APKではなくAARを作成するようにbuild.gradleとAndroidManifest.xmlを編集
5の作業が終わり、ビルドが成功すると、
./Build
ディレクトリにAndroidプロジェクトが生成されますコマンドラインでのビルド後/path/to/project ├── Assets ├── Build │ └── {Project Name} │ ├── build.gradle │ ├── gradle.properties │ ├── libs │ ├── local.properties │ ├── proguard-unity.txt │ └── src ├── Library ├── Logs ├── Packages ├── ProjectSettings ├── README.md └── Unity_v2018.x.ulfデバイスにインストールする
.apk
の作成であれば、gradle
コマンドでビルドして終了です
今回は、.aar
の作成なので、build.gradle
とAndroidManifest.xml
を編集する必要があります
build.gradle
apply plugin: 'com.android.application'
をapply plugin: 'com.android.library'
に変更applicationId 'com.project.unitytest'
を削除bundle
をコメントアウト(Unity2018.3.x以降??)build.gradle--- apply plugin: 'com.android.application' +++ apply plugin: 'com.android.library' --- applicationId 'com.project.unitytest' --- bundle { +++ /*bundle { language { enableSplit = false } density { enableSplit = false } abi { enableSplit = true } --- } +++ }*/
AndroidManifest.xml
- 該当のintent-filterタグとその子をすべてコメントアウト
AndroidManifest.xml<!--<intent-filter>--> <!--<action android:name="android.intent.action.MAIN" />--> <!--<category android:name="android.intent.category.LAUNCHER" />--> <!--<category android:name="android.intent.category.LEANBACK_LAUNCHER" />--> <!--</intent-filter>-->UnityプロジェクトからAARファイルの生成を行うには、この作業も自動化しておく必要があるので、
UnityのPostProcessの機構を使って自動化しておきます[Unity] PostProcessでビルド後に処理を差し込む
https://qiita.com/edo_m18/items/346439f7678218e85e69※
build.gradle
の文字コードはUTF-8
で書き出すこと、 C#のでのUTF8Encoding
はUTF-8 with BOM
であるので注意が必要。UTF-8 with BOM
のbuild.gradle
をgradle
に渡すとエラーになるので注意
※UnityのPostProcessBuildにはこの記事ではふれません
※build.gradle
のbundle
のコメントアウトだが このページ によれば次に示す置換でも問題ないbuild.gradle--- bundle { +++ splits { language { --- enableSplit = false +++ enable false } density { --- enableSplit = false +++ enable false } abi { --- enableSplit = true +++ enable true } }bundle部分を処理するC#の例build_text = build_text.Replace("bundle {", "splits {"); build_text = build_text.Replace("enableSplit = false", "enable false"); build_text = build_text.Replace("enableSplit = true", "enable true");7. コマンドラインでAndroidプロジェクトをビルド
残る作業は
./Build
ディレクトリに生成されたAndroidプロジェクトをビルドしてAARファイルを生成するだけです
生成されたAndroidプロジェクトのディレクトリ(build.gradle
があるディレクトリ)に移動して次のコマンドを実行するだけですAARファイルを生成gradle bundleDebugAarAARファイルを生成gradle bundleReleaseAarデバッグとリリースがあるので必要に応じて使い分けましょう
ビルドが成功するとプロジェクトのディレクトリに
build
ディレクトリが生成されて、
その中に{PROJECT_NAME}-debug.aar
{PROJECT_NAME}-release.aar
が生成されますビルド後/path/to/project ├── Assets ├── Build │ └── {Project Name} │ ├── build │ │ └── outputs │ │ └── aar │ │ ├── {PROJECT_NAME}-debug.aar │ │ └── {PROJECT_NAME}-release.aar │ ├── build.gradle │ ├── gradle.properties │ ├── libs │ ├── local.properties │ ├── proguard-unity.txt │ └── src ├── Library ├── Logs ├── Packages ├── ProjectSettings ├── README.md └── Unity_v2018.x.ulf8. 作業をシェルスクリプトにまとめる
4から7の作業をまとめて、コマンド一行でAARファイルの生成できるようにします
ライセンスファイルはUnityプロジェクトのルートディレクトリに設置して、6の作業はUnityのPostProcessBuildで自動化しておきましょうそうして作業をまとめたスクリプトが次になります
build.sh#!/usr/bin/env bash set -e set -x mkdir -p /root/.cache/unity3d mkdir -p /root/.local/share/unity3d/Unity/ set +x cp ~/project/Unity_v2018.x.ulf /root/.local/share/unity3d/Unity/Unity_lic.ulf cd ~/project/ && rm -r ./Build && /opt/Unity/Editor/Unity -batchmode -quit -nographics -logFile ./build.log -projectPath . -executeMethod ApplicationBuild.AndroidBuild cd ~/project/Build/{Project Name} && gradle bundleDebugAarUnityプロジェクトのルートディレクトリに移動して、dockerコンテナ内で
build.sh
を実行しますAARの自動ビルドcd /path/to/project chmod 777 build.sh docker run -it --rm \ -v "$(pwd):/root/project" \ gableroux/unity3d:gableroux/unity3d:2018.4.5f1-android \ /bin/bash -c "/root/project/build.sh"さいごに
これでコマンドを実行してビルドされるのを待つだけになりました!
これをBitbucket Pipeline
やCircleCI
、GitHub Actions
とかのCIツールに載せていけるといいですね
まとめている間に、既にmacOSやWindowsにUnityがインストールされていて、かつ、Android SDKがインストールされているなら、Docker上じゃなくていいかなと思いました余談
実際の出力cha84rakanal$ time docker run -it --rm -e "WORKDIR=/root/project" -v "$(pwd):/root/project" gableroux/unity3d:2018.4.5f1-android /bin/bash -c "/root/project/build.sh" + mkdir -p /root/.cache/unity3d + mkdir -p /root/.local/share/unity3d/Unity/ + set +x ALSA lib confmisc.c:767:(parse_card) cannot find card '0' ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default ALSA lib confmisc.c:767:(parse_card) cannot find card '0' ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_card_driver returned error: No such file or directory ALSA lib confmisc.c:392:(snd_func_concat) error evaluating strings ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_concat returned error: No such file or directory ALSA lib confmisc.c:1246:(snd_func_refer) error evaluating name ALSA lib conf.c:4528:(_snd_config_evaluate) function snd_func_refer returned error: No such file or directory ALSA lib conf.c:5007:(snd_config_expand) Evaluate error: No such file or directory ALSA lib pcm.c:2495:(snd_pcm_open_noupdate) Unknown PCM default /home/builduser/buildslave/unity/build/Editor/Platform/Linux/UsbDevices.cpp:UsbDevicesQuery Welcome to Gradle 5.1.1! Here are the highlights of this release: - Control which dependencies can be retrieved from which repositories - Production-ready configuration avoidance APIs For more details see https://docs.gradle.org/5.1.1/release-notes.html Starting a Gradle Daemon (subsequent builds will be faster) > Task :help Welcome to Gradle 5.1.1. To run a build, run gradle <task> ... To see a list of available tasks, run gradle tasks To see a list of command-line options, run gradle --help To see more detail about a task, run gradle help --task <task> For troubleshooting, visit https://help.gradle.org BUILD SUCCESSFUL in 5s 1 actionable task: 1 executed debugger-agent: Unable to listen on 28 Starting a Gradle Daemon (subsequent builds will be faster) Deprecated Gradle features were used in this build, making it incompatible with Gradle 6.0. Use '--warning-mode all' to show the individual deprecation warnings. See https://docs.gradle.org/5.5.1/userguide/command_line_interface.html#sec:command_line_warnings BUILD SUCCESSFUL in 2m 7s 21 actionable tasks: 21 executed real 33m9.065s user 0m0.162s sys 0m0.178s cha84rakanal$
time
コマンドで実行時間を図ったら30分もビルドしてるので、dockerコンテナ内よりホスト側でビルドした方がはやいやんけw
- 投稿日:2019-11-25T22:40:14+09:00
UnityからkintoneのREST APIを叩く方法
はじめに
Unityからウェブデータベースにアクセスしたい!というシチュエーション、ありますよね。
UIが整っているウェブデータベースは非エンジニアでもデータの管理が可能なため、エンジニアと非エンジニアが同じプロジェクトに関わっている場合はとても便利です。
この記事では非エンジニアでもデータの入力や編集がしやすかったり、データベースの構造が容易に変更できるkintoneにUnity内からアクセスする方法を紹介します。
Unity初心者なので、何かツッコミがありましたら遠慮なくお願いします。
kintone環境
無料の開発者ライセンスをこちらのページから入手することができます。
すぐに手に入るので、この環境をもとに記事のサンプルコードを試してみてください。
kintone内のアプリの作り方やアプリIDの確認方法については色々と記事は出ているので、ググってください。認証
Unityからkintoneにアクセスする際は:
- パスワード認証
- APIトークン認証
のいずれかの認証方法が簡単なので、お勧めします。認証についてはこちらを参照してください:
https://developer.cybozu.io/hc/ja/articles/201941754#step7使用するフレームワーク
SimpleJSON
kintoneアプリ(ウェブデータベース)内のレコード情報を取得するためにREST APIを叩くと、ネストが深くて複雑なJSONが返ってきます。このJSONの形はkintoneアプリの作り方によっては全然違います。C#ではあらかじめどんな形のレスポンスが返ってくるのかをスクリプト内のクラスで定義する必要がありますが、kintoneのフォームを変更するとこのクラスの内容も変更する必要があるので、とても面倒くさいです。
この問題を解決するためにSimple JSONというフレームワークを使います。
SimpleJSONは非常に使いやすいフレームワークで、REST APIを叩くのに必要なUnityWebRequestで返ってきたJSONをJSONNodeという型に入れることで容易にJSON内のデータにアクセスすることができます。
設定方法
設定方法は簡単です:
- SimpleJSONのコードをCommunity Wikiからコピーし、SimpleJSON.cs という名前で保存します
- Unityプロジェクト内の『assets』フォルダ内の『plugins』フォルダ(無かったら作る)にSimpleJSON.csを配置します
3. スクリプト内に using SimpleJSON; を定義しますこれだけです。
使い方
kintoneのREST APIを叩いて、下記のようなJSONレスポンスが返ってきたとします:
{ "records": [ { "recordID": { "type": "RECORD_NUMBER", "value": "1" }, "mychoice": { "type": "DROP_DOWN", "value": "おめがレイ" } }, { "recordID": { "type": "RECORD_NUMBER", "value": "2" }, "mychoice": { "type": "DROP_DOWN", "value": "おめがリオ" } } ], "totalCount": "2" }このJSONの中の『おめがレイ』と『おめがリオ』という値は下記のようにstring型として取得ができます:
string JSONstring = request.downloadHandler.text; JSONNode KintoneResponse = JSON.Parse(JSONstring); var FirstAnswer = KintoneResponse["records"][0]["mychoice"]["value"].Value; var SecondAnswer = KintoneResponse["records"][1]["mychoice"]["value"].Value;REST APIの叩き方
StartCoroutineメソッドを呼び、yield return文を使ってREST APIを叩き、callbackでJSONレスポンスを用いた処理をします。
下記のサンプルコードでは、kintone内のアプリに対してAPIを叩き、全てのレコードの情報を取得しています(100件以上レコードがある場合は100件まで取得します)。
レコードの取得後、1件目のレコードの中の『mychoice』というフィールドコードを設定したフィールドに保存された値をコンソールに出力しています。
{サブドメイン名}、{アプリID}、{APIトークン}は自分の環境に合わせて正しい値を設定してください。testAPI.csusing System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; using SimpleJSON; public class testAPI : MonoBehaviour { void Start() { StartCoroutine( getKintoneRecords( (JSONNode JSONresponse) => { HandleAPIresponse(JSONresponse); } ) ); } private IEnumerator getKintoneRecords(System.Action<JSONNode> callBack) { //APIのURI、HTTPメソッド、ヘッダーの設定 string RequestURL = "https://{サブドメイン名}.cybozu.com/k/v1/records.json?app={アプリID}"; UnityWebRequest request = UnityWebRequest.Get(RequestURL); request.SetRequestHeader("X-Cybozu-API-Token", "{APIトークン}"); //APIのリクエストを送信 yield return request.SendWebRequest(); //レスポンスをJSONNode型に格納 string JSONstring = request.downloadHandler.text; JSONNode JSONnode = JSON.Parse(JSONstring); //JSONNode型のレスポンスをcallBackに渡す callBack(JSONnode); } private void HandleAPIresponse(JSONNode APIresponse) { //コールバック関数内でAPIリクエストで返されたレスポンスの処理を行う string KintoneData = APIresponse["records"][0]["mychoice"]["value"].Value; Debug.Log("Data from kintone is:" + KintoneData); } }コード解説
まず、StartCoroutineメソッドを実行します。
getKintoneRecords関数を呼び、この関数の中のyield return文でREST APIを叩きます。REST APIのレスポンスはJSONNode型のJSONresponseという変数に格納され、この変数をそのままHandleAPIresponse関数で処理をします。void Start() { StartCoroutine( getKintoneRecords( (JSONNode JSONresponse) => { HandleAPIresponse(JSONresponse); } ) ); }getKintoneRecords関数の中でREST APIを叩く処理を書きます。
REST APIのレスポンスで続けて処理を行いたい場合はコールバックを使用するので、このように宣言します。private IEnumerator getKintoneRecords(System.Action<JSONNode> callBack) { //REST APIを叩く処理 }kintoneのREST APIを叩く際に、ヘッダーに認証情報が必要です。
APIトークン認証を使用する際はアプリからAPIトークンを生成し、下記のようにヘッダー情報を付与します。request.SetRequestHeader("X-Cybozu-API-Token", "{APIトークン}");APIトークン認証ではなくユーザ認証を使用したい場合、下記のように宣言します
request.SetRequestHeader("X-Cybozu-Authorization", "{『ユーザ名:パスワード』をBASE64エンコードした文字列}");yield return文でREST APIを叩きます。
レスポンスが返ってくるまで、次の行には進まないようになっています。yield return request.SendWebRequest();レスポンスをJSONNode型の変数に格納し、次の関数で使えるようにします。
JSONNode型はSimpleJSONによって作られた型です。//レスポンスをJSONNode型に格納 string JSONstring = request.downloadHandler.text; JSONNode JSONnode = JSON.Parse(JSONstring); //JSONNode型のレスポンスをcallBackに渡す callBack(JSONnode);最後に、コールバック関数内でREST APIのレスポンスを活用した処理を行います。
private void HandleAPIresponse(JSONNode APIresponse) { //コールバック関数内でAPIリクエストで返されたレスポンスの処理を行う string KintoneData = APIresponse["records"][0]["mychoice"]["value"].Value; Debug.Log("Data from kintone is:" + KintoneData); }
- 投稿日:2019-11-25T22:24:55+09:00
自動でBootシーンに切り替える仕組みを作ると人生が楽になる 【Unity】
ゲームを作る時に、初期化用のシーンを経由してから指定したシーンに移行し動作させることってあると思うのですが
そういう設計にすると制作中テストする度に初期化用のシーンに戻る必要が出てきて少し大変ですよねこの問題はUnityのEditor拡張を利用することで解決できます。
実装は簡単。再生時にシーンを切り替える命令を登録するだけ
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; using UnityEngine.SceneManagement; [InitializeOnLoad] public static class PlayModeEditor { static string m_bootSceneName ="Boot";// ←移動したいシーンの名前を記入する static PlayModeEditor() { EditorApplication.playModeStateChanged += ChangeBootScene; } static void ChangeBootScene(PlayModeStateChange state) { // 実行状態になったら if (state == PlayModeStateChange.EnteredPlayMode) { // 別シーンで起動していた場合切り替える Scene scene = SceneManager.GetActiveScene(); if (!scene.Equals(m_bootSceneName)) { SceneManager.LoadScene(m_bootSceneName); } } } }実行時の結果
この機能を利用すれば他でも応用できそうです。
- 投稿日:2019-11-25T20:41:00+09:00
Unityのプロジェクトをバックアップする方法
目的
Unityでプロジェクトをバックアップする方法を以下の3つにまとめてみました。
- Collaborateを使う方法
- GitHubを使う方法
- フォルダごとバックアップする方法
環境
Unity Hub 2.1.3
Unity2018.4.11f1
Mac 10.15.11. Collaborateを使う方法
Unityの機能にCollaborateという機能があります。
手軽にプロジェクトをクラウドに保存できるという優れものです。
Collaborateの設定方法と使い方を説明します。Collaborateでできること
- バージョン管理
- プロジェクトの共有
GitHubのUnity版だと思ってください。
設定方法
1. 対象プロジェクトとUnityアカウントを紐づける
プロジェクトを開いてServicesタブにて自分のアカウント名を選択して
Create
ボタンを押します。
Servicesタブが表示されていない場合
ツールバーのWindow
>General
>Services
2. Collaborateを有効化
Create together seamlessly を有効にします
Collaborateのメニューが表示されれば完了です。
左上のCollabアイコンも有効化さたのでクリックします。
3. プロジェクトをアップロードする
アップロードする際のコメントを入力して
Publish now!
をクリックします。一番下の履歴はプロジェクトを作成した最初の状態なので誤って
Go back to...
をクリックしてしまうと、プロジェクトを作成した初期状態に戻ってしまうので注意が必要です。使い方
バージョンの戻し方
今後はプロジェクトに変更を加える度にCollaborateにアップロードすることで、新しいバージョン履歴が増えます。
1つ前のバージョンに戻したい場合は、Restore
ボタンを押すことでそのバージョンまで戻すことができます。プロジェクトの共有方法
人型のアイコンをクリックするとCollaborateのダッシュボードへ移動します。
Add a person or group 欄へ共有したUnityID(メールアドレス)を入力して
Add
ボタンでプロジェクトのチームメンバーに招待することができます。
作ったプロジェクトを見てもらいたい場合や、共同で開発する際に便利ですね。
Collaborateまとめ
CollaborateはUnityに付属しているだけあって、煩わしい設定がなく初心者でも簡単に設定できるところがメリットですね。
ただし、無料版(デフォルト状態)だと容量が1GBまでと少々容量不足がデメリットです。
勉強がてらUnityを使う分には問題ありませんが、本格的なアプリ作成となると有料も視野に入れた方がよさそうですね。2. GitHubを使う方法
バージョン管理といえばGitHubですが、UnityのアセットストアにUnity版GitHubがあります。
GitHubをUnityだけで使いたい場合は、手頃にGitHubが使えて便利なツールだと思います。設定方法
アセットストアからインポートする
Assets Store にて「GitHub for Unity」をダウンロードしてプロジェクトにインポートします。
GitHubウインドウを表示
ツールバーから
Window
>GitHub
でGitHubウインドウが表示されます。
Initialize a git repository for this project
ボタンをクリックすると自動的に初回分のコミットがされます。
Gitへコミットする
Changesタブを選択するとコミットするフォルダを選択する画面になります。
対象フォルダにチェックを入れて、「Commit summary」と「Commit description」にそれぞれコミットの概要と詳細説明を入力後にCommit to [master]
ボタンをクリックします。GitHubへPushする
GitHubへサインインする
GitHubウインドウの右上にある
Signin
をクリックするとサインイン画面が表示されますので、アカウントを入力してSign in
ボタンをクリックします。
GitHubのアカウントはあらかじめ作成する必要があります
https://github.com/GitHubでリポジトリ作成
GitHubの管理画面の右上プラスアイコンから
New repository
でリポジトリ名を決めてリポジトリを作成します。
Unity側のSettingへ情報をセット
Git ConfigurationへGitHubアカウント情報をセットします。
Repository Configurationへ先ほどのSSHをセットします。
Pushする
PushボタンをクリックしてGitHubリポジトリへプロジェクトをPushします。
Pushされたことを確認
BranchesタブにてRemoteBranchesにmasterブランチがPushされてます。
GitHubのリポジトリページにもデータがアップロードされていることが確認できます。
GitHubまとめ
GitHubをまったく使ったことない場合は少々敷居が高いかもしれませんが、GitHubを使いこなせるようになると、他言語の開発でも便利です。
本来GitHubはCLIですが、GUIベースで操作できるところもUnityらしく、初心者でも扱いやすいですね。3. フォルダごとバックアップする方法
最後にアナログ的な方法になりますが、プロジェクトフォルダごと外付けHDDなどにバックアップする方法です。
Unityはプロジェクトごとに1つのフォルダでまとめられ独立しているのでデータ移行は比較的簡単ですね。総合まとめ
Unityでプロジェクトをバックアップできる方法をまとめてみました。
最後にそれぞれのメリット、デメリットをまとめておきますので参考になれば幸いです。1. Collaborate
メリット
- Unity付属機能で設定が楽
- バージョン管理ができる
デメリット
- 無料で使える容量が1GBと少々物足りない
2. GitHub for Unity
メリット
- GitHubという最強ツールを使えること
- バージョン管理ができる
デメリット
- GitHubについての知識がないと設定がやや煩雑に感じる
3. フォルダごとバックアップする方法
メリット
- 特別な知識は必要としない
デメリット
- バージョン管理できない
- バックアップ作業は手作業
- 投稿日:2019-11-25T17:39:46+09:00
UnityTestRunner向けTest属性を自作してみる
前書き
アドカレ4日目です。
UnityTestRunnerについてちょっと書いてみます。テスト、いいですよね。たくさんテストを書くと仕事が進んでいるような気がしてきます。
僕はそんなにテスト書かない人間ですけど、テストを書くのが嫌いでは無いです。
最近、UnityTestRunnerとその裏側であるNUnitに触れていたら、TestAttribute
の仕組みが気になって、自作のAttributeとかを噛ませてみたくなったので、試してみました。シンプルな自作TestAttribute
まずはじめに
[SimpleTest] public void テストメソッド() { }
こんな感じでTestRunnerに認識されるAttributeを自作してみます。実装と使用方法
TestAttribute定義
SimpleTestAttribute.csusing System; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class SimpleTestAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture { public TestMethod BuildFrom(IMethodInfo method, Test suite) => new TestMethod(method, suite); }使用方法
SimpleTestSample.csusing NUnit.Framework; public class SimpleTestSample { [SimpleTest] public void Test() { } [SimpleTest] public void TestFail() => Assert.Fail(); }上記のテストケースであれば、UnityTestRunner上では次のスクリーンショットのように表示されます。
仕組み
UnityTestRunnerに認識させるためにやったことはAttributeの定義(とテストメソッドへの付与)だけです。
継承元について
NUnit.Framework.NUnitAttribute
を継承していますが、これはSystem.Attribute
を直接継承してもUnityTestRunner上では動作に違いはありませんでしたが、その場の雰囲気でNUnitAttributeを採用しました。
他にもNUnit標準のAttributeはいくつかあるのでNUnit内のAttributeを眺めるのもいいかもしれません。
例えば、UnityTestAttribute
なんかはCombiningStrategyAttribute
を継承していたりします。ISimpleTestBuilderについて
NUnit.Framework.Interfaces.ISimpleTestBuilder
は次のように定義されています。ISimpleTestBuilder.cspublic interface ISimpleTestBuilder { TestMethod BuildFrom(IMethodInfo method, Test suite); }今回はTestMethodのコンストラクタ
TestMethod(IMethodInfo method, Test suite)
にそのまま流し込んで完了としました。
もう少しイロイロしてくれる便利クラスとしてNUnit.Framework.Internal.Builders.NUnitTestCaseBuilder
というものも存在していて、標準のTestAttribute
などはこのビルダーを使っているようです。IImplyFixtureについて
NUnit.Framework.Interfaces.IImplyFixture
自体は何のメソッドも持たないマーカーインターフェースですが、最低限これだけ付いていればテストとして認識させることができます。ただし前述のISimpleTestBuilder.BuildFrom
のようなTestMethodを提供するインターフェースが存在しないと、何のテストも実行されません。
シーンを指定してTestを実行する属性の実装(その1)
テストメソッドを実行する直前に任意の処理を挟み込むAttributeを実装してみます。
今回の例ではテスト実行前にEditorSceneManager.OpenScene("hoge.unity");
を叩いてみます。実装と使用方法
TestAttribute定義
UnityTestSceneAttribute.csusing System; using System.Reflection; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; using UnityEditor.SceneManagement; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class UnityTestSceneAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture { public readonly string scenePath; public UnityTestSceneAttribute(string scenePath) => this.scenePath = scenePath; public TestMethod BuildFrom(IMethodInfo method, Test suite) => new TestMethod(new MethodProxy(method, () => EditorSceneManager.OpenScene(scenePath)), suite); } // NUnit.Framework.Internal.MethodWrapperをさらに包むもの class MethodProxy : IMethodInfo { private readonly IMethodInfo _methodInfo; private readonly Action _beforeTest; public MethodProxy(IMethodInfo methodInfo, Action beforeTest) { _methodInfo = methodInfo; _beforeTest = beforeTest; } public object Invoke(object fixture, params object[] args) { _beforeTest.Invoke(); return _methodInfo.Invoke(fixture, args); } public ITypeInfo TypeInfo => _methodInfo.TypeInfo; public MethodInfo MethodInfo => _methodInfo.MethodInfo; public string Name => _methodInfo.Name; public bool IsAbstract => _methodInfo.IsAbstract; public bool IsPublic => _methodInfo.IsPublic; public bool ContainsGenericParameters => _methodInfo.ContainsGenericParameters; public bool IsGenericMethod => _methodInfo.IsGenericMethod; public bool IsGenericMethodDefinition => _methodInfo.IsGenericMethodDefinition; public ITypeInfo ReturnType => _methodInfo.ReturnType; public T[] GetCustomAttributes<T>(bool inherit) where T : class => _methodInfo.GetCustomAttributes<T>(inherit); public bool IsDefined<T>(bool inherit) => _methodInfo.IsDefined<T>(inherit); public IParameterInfo[] GetParameters() => _methodInfo.GetParameters(); public Type[] GetGenericArguments() => _methodInfo.GetGenericArguments(); public IMethodInfo MakeGenericMethod(params Type[] typeArguments) => _methodInfo.MakeGenericMethod(); }使用方法
using NUnit.Framework; using UnityEngine; public class UnityTestSceneTest { [UnityTestScene("Assets/Scenes/CameraAru.unity")] public void AruScene() => Assert.IsNotNull(Object.FindObjectOfType<Camera>()); [UnityTestScene("Assets/Scenes/CameraNai.unity")] public void NaiScene() => Assert.IsNull(Object.FindObjectOfType<Camera>()); }次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
仕組み
NUnitのIMethodInfoを実装してMethodProxyクラスを定義し、BuildFromに渡ってくるIMethodInfoのプロキシとしてInvokeに割り込んでいます。
なんとなくActionを渡していますが、直接Invokeメソッド内に書いちゃっても問題ないですね。なお、今回のBuildFromメソッド実装では、色々実装が足りないのでValuesやRange属性と組み合わせることができませんが、そこはご愛嬌ということで。
シーンを指定してTestを実行する属性の実装(その2)
前述した「シーンを指定してTestを実行する属性の実装(その1)」の場合、色々な属性と組み合わせるとボロが出始めました。
そこで、もう少しボロが出にくいUnityTestRunnerライクな実装をしてみます。
使い方は一緒なので割愛します。実装
UnityTestSceneAttribute.csusing NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; using UnityEditor.SceneManagement; using UnityEngine.TestTools; [System.AttributeUsage(System.AttributeTargets.Method, AllowMultiple = true)] public class UnityTestSceneAttribute : NUnitAttribute, ISimpleTestBuilder, IImplyFixture, IOuterUnityTestAction { public readonly string scenePath; public UnityTestSceneAttribute(string scenePath) => this.scenePath = scenePath; public TestMethod BuildFrom(IMethodInfo method, Test suite) => new TestMethod(method, suite); public System.Collections.IEnumerator BeforeTest(ITest test) { EditorSceneManager.OpenScene(scenePath); // 1frameくらい間を置いた方が安心 yield return null; } public System.Collections.IEnumerator AfterTest(ITest test) { yield break; } }仕組み
都合よくUnityTestRunnerのテストの前後に処理を挟み込める
IOuterUnityTestAction
というインターフェースが用意されているので、これを自作Attributeに実装してあげるだけです。
テスト属性に密接に関わる処理はUnitySetUp
やUnityTearDown
よりこちらのインターフェースを使った方が簡単なケースもあるかもしれません。
TestCaseSourceのUnityTest版
NUnitには
TestCaseSourceAttribute
というものがいて、このAttributeにイテレータを返すメンバー名を渡してあげると1つのテストから複数のテストケースを量産することができます。
TestAttribute
に対するUnityTestRunnerのUnityTestAttribute
のように、TestCaseSourceAttribute
に対してUnityTestCaseAttribute
があるかなーっと思ったのですが、現状見つからなかったので自作してみました。
以下に実装を残します。実装と使用方法
TestAttribute定義
UnityTestCaseSourceAttribute.csusing System; using System.Collections.Generic; using System.Linq; using System.Reflection; using NUnit.Framework; using NUnit.Framework.Interfaces; using NUnit.Framework.Internal; using NUnit.Framework.Internal.Builders; [AttributeUsage(AttributeTargets.Method, AllowMultiple = true)] public class UnityTestCaseSourceAttribute : TestCaseSourceAttribute, ITestBuilder { private readonly NUnitTestCaseBuilder _builder = new NUnitTestCaseBuilder(); // コンストラクタ public UnityTestCaseSourceAttribute(string sourceName) : base(sourceName) { } public UnityTestCaseSourceAttribute(Type sourceType, string sourceName) : base(sourceType, sourceName) { } public UnityTestCaseSourceAttribute(Type sourceType) : base(sourceType) { } public UnityTestCaseSourceAttribute(Type sourceType, string sourceName, object[] methodParams) : base(sourceType, sourceName, methodParams) { } // ITestBuilder public new IEnumerable<TestMethod> BuildFrom(IMethodInfo method, Test suite) { var cases = (IEnumerable<ITestCaseData>) typeof(TestCaseSourceAttribute).InvokeMember("GetTestCasesFor", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod, null, this, new object[] {method}); return cases.OfType<TestCaseParameters>().Select(p => BuildFromImpl(method, suite, p)); } private TestMethod BuildFromImpl(IMethodInfo method, Test suite, TestCaseParameters caseParam) { caseParam.ExpectedResult = new object(); caseParam.HasExpectedResult = true; var t = _builder.BuildTestMethod(method, suite, caseParam); if (t.parms != null) t.parms.HasExpectedResult = false; return t; } }使用法
SimpleTestEx.csusing System.Collections; using NUnit.Framework; using UnityEngine.Networking; public class SimpleTestEx { public static object[][] urls = { new object[] {"https://google.co.jp", 200}, new object[] {"https://yahoo.co.jp", 200}, }; [UnityTestCaseSource("urls")] public IEnumerator UnityTestExのテスト(string url, int responseCode) { using (var req = UnityWebRequest.Get(url)) { var ope = req.SendWebRequest(); while (ope.isDone == false) yield return null; Assert.AreEqual(responseCode, req.responseCode); } } }次のスクリーンショットは、上記のテストコードをUnityTestRunnerで実行実行した結果です。
仕組み
TestCaseSourceAttribute
を継承して、ITestBuilder
を実装し直してみました。
TestCaseSourceAttribute
にはvirtualなメソッドなんて存在しないので、BuildFromメソッドをnewして隠蔽し、GetTestCasesFor
メソッドをリフレクションでこじ開けるという、気合いと根性に満ちた実装になっています。
TestCaseSourceAttribute
を写経するのも選択肢としてはありですが、少々複雑だったので横着してみました。
ぱっと見はそこそこ素直な実装に見えるんじゃないでしょうか。
アスキーアートだって
結構なんでもできるので、
[AATest] public void AA表示したい() { }こんな感じでアスキーアートを出すことだって自由です。
それではみなさん楽しいテストライフを!
- 投稿日:2019-11-25T15:09:07+09:00
Editor拡張でbuiltするシーンを変える
Scenes in Bultをカスタマイズしたい時、以下のように書く
EditorBuildSettings.scenes = new EditorBuildSettingsScene[]{ new EditorBuildSettingsScene("Assets/_Kitasenju/Scenes/VJ.unity", false),//disable new EditorBuildSettingsScene("Assets/_Kitasenju/Scenes/main.unity", true)//enable };
- 投稿日:2019-11-25T14:14:13+09:00
ScriptingDefineSymbolsを設定するついでにProjectSettingsの値も書き換える
Unityでアプリを作っていて、場合によって構成を変えたい場合があります。
たとえば展示するアプリ・配布するアプリなどです。その際はScriptingDefineSymbolsに値を突っ込み、プログラム上で以下のように条件分岐することができます。こういうやつです。
#if HOGE #else #endif便利なエディタ拡張
こちらの記事で簡単に設定できるエディタ拡張が公開されてました。ありがとうございます
http://baba-s.hatenablog.com/entry/2015/02/10/102941ついでにProjectSettingの値も変更
ScriptingDefineSymbolsの変更に加え、ProjectSettingも書き加えたかったの追記しました。
using System.Collections.Generic; using System.Linq; using System.Xml; using UnityEditor; using UnityEngine; /// <summary> /// シンボルを設定するウィンドウを管理するクラス /// </summary> public class SymbolWindow : EditorWindow { //=================================================================================================== // クラス //=================================================================================================== /// <summary> /// シンボルのデータを管理するクラス /// </summary> private class SymbolData { public string Name { get; private set; } // 定義名を返します public string Comment { get; private set; } // コメントを返します public bool IsEnable { get; set; } // 有効かどうかを取得または設定します /// <summary> /// コンストラクタ /// </summary> public SymbolData( XmlNode node ) { Name = node.Attributes[ "name" ].Value; Comment = node.Attributes[ "comment" ].Value; } } //=================================================================================================== // 定数 //=================================================================================================== private const string ITEM_NAME = "Tools/Symbols"; // コマンド名 private const string WINDOW_TITLE = "Symbols"; // ウィンドウのタイトル private const string XML_PATH = "Assets/Plugins/Editor/defineSymbols/symbols.xml"; // 読み込む .xml のファイルパス //=================================================================================================== // 変数 //=================================================================================================== private static Vector2 mScrollPos; // スクロール座標 private static SymbolData[] mSymbolList; // シンボルのリスト //=================================================================================================== // 静的関数 //=================================================================================================== /// <summary> /// ウィンドウを開きます /// </summary> [MenuItem( ITEM_NAME )] private static void Open() { var window = GetWindow<SymbolWindow>( true, WINDOW_TITLE ); window.Init(); } //=================================================================================================== // 関数 //=================================================================================================== /// <summary> /// 初期化する時に呼び出します /// </summary> private void Init() { var document = new XmlDocument(); document.Load( XML_PATH ); var root = document.GetElementsByTagName( "root" )[ 0 ]; var symbolList = new List<XmlNode>(); foreach ( XmlNode n in root.ChildNodes ) { if ( n.Name == "symbol" ) { symbolList.Add( n ); } } mSymbolList = symbolList .Select( c => new SymbolData( c ) ) .ToArray(); var defineSymbols = PlayerSettings .GetScriptingDefineSymbolsForGroup( EditorUserBuildSettings.selectedBuildTargetGroup ) .Split( ';' ); foreach ( var n in mSymbolList ) { n.IsEnable = defineSymbols.Any( c => c == n.Name ); } } /// <summary> /// GUI を表示する時に呼び出されます /// </summary> private void OnGUI() { EditorGUILayout.BeginVertical(); mScrollPos = EditorGUILayout.BeginScrollView( mScrollPos, GUILayout.Height( position.height ) ); foreach ( var n in mSymbolList ) { EditorGUILayout.BeginHorizontal( GUILayout.ExpandWidth( true ) ); n.IsEnable = EditorGUILayout.Toggle( n.IsEnable, GUILayout.Width( 16 ) ); if ( GUILayout.Button( "Copy" ) ) { EditorGUIUtility.systemCopyBuffer = n.Name; } EditorGUILayout.LabelField( n.Name, GUILayout.ExpandWidth( true ), GUILayout.MinWidth( 0 ) ); EditorGUILayout.LabelField( n.Comment, GUILayout.ExpandWidth( true ), GUILayout.MinWidth( 0 ) ); EditorGUILayout.EndHorizontal(); } if ( GUILayout.Button( "Save" ) ) { var defineSymbols = mSymbolList .Where( c => c.IsEnable ) .Select( c => c.Name ) .ToArray(); var str = string.Join( ";", defineSymbols ); PlayerSettings.SetScriptingDefineSymbolsForGroup( EditorUserBuildSettings.selectedBuildTargetGroup, str ); //追記 if(str.IndexOf("HOGE")>=0){ PlayerSettings.productName = "HOGE"; PlayerSettings.allowedAutorotateToLandscapeLeft=true; PlayerSettings.allowedAutorotateToPortrait=false; PlayerSettings.allowedAutorotateToPortraitUpsideDown=false; PlayerSettings.allowedAutorotateToLandscapeRight=false; PlayerSettings.applicationIdentifier = "com.hoge.hogehoge"; PlayerSettings.iOS.targetDevice = iOSTargetDevice.iPhoneAndiPad; }else{ PlayerSettings.productName = "FUGA"; PlayerSettings.allowedAutorotateToPortrait=true; PlayerSettings.allowedAutorotateToPortraitUpsideDown=false; PlayerSettings.allowedAutorotateToLandscapeLeft=false; PlayerSettings.allowedAutorotateToLandscapeRight=false; PlayerSettings.applicationIdentifier = "com.hoge.fugafuga"; PlayerSettings.iOS.targetDevice = iOSTargetDevice.iPhoneOnly; //PlayerSettings.SplashScreen.show = true; } Close(); } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } }
- 投稿日:2019-11-25T11:57:22+09:00
【Unity】円形状の移動制限のかけ方
Script
testpublic GameObject centerObj; //中心となるオブジェクト(ゲームオブジェクトじゃなくても中心点がわかればOK) [SerializeField, Header("半径")] float radius; [SerializeField, Header("速さ")] float speed; // Update is called once per frame void Update () { Move(); Restriction(); } void Move() { //手動で動かせるように(円形の移動制限には関係ありません) transform.position += new Vector3(Input.GetAxis("Horizontal") *speed*Time.deltaTime, Input.GetAxis("Vertical") * speed * Time.deltaTime, 0); } /// <summary> /// 円形の移動制限 /// </summary> void Restriction() { //自身と円形に移動制限させたい位置の中心点との距離を測り半径以上になっていれば処理 if (Vector3.Distance(transform.position,centerObj.transform.position)>radius) { //中心点から自身までの方向ベクトルを作る Vector3 nor=transform.position-centerObj.transform.position; //作った方向ベクトルを正規化する nor.Normalize(); //方向ベクトル分半径に移動させる transform.position = nor * radius; } }どんな場合に使うの?
自分は自作バーチャルパッドを作った時に使いました
- 投稿日:2019-11-25T11:54:57+09:00
【Unity】事前に決めた座標を行ったり来たりする方法
行ったり来たりする処理
身近で行ったり来たりする処理どうする?的な話題になったので自分なりに考えた処理
サンプルコード
[SerializeField, Header("移動速度")] float speed = 5; [SerializeField, Header("感知できる距離")] float range = 0.5f; [SerializeField,Header("移動したい座標")] Vector3[] movePath = new Vector3[3]; Vector3 direction; int pathValue = 0; //現在どのパスなのか int nextPathValue; //次の移動したいパス enum MoveType { MoveOn, Return } MoveType moveType; private void Start() { //初期位置を記憶しておく(配列の0番目) movePath[0] = transform.position; //次の座標のパス nextPathValue = 1; moveType = MoveType.MoveOn; } private void Update() { Move(); } /// <summary> /// 動くよ /// </summary> void Move() { //移動する向きベクトルを作成 direction = movePath[nextPathValue] - movePath[pathValue]; //方向ベクトルを作成 direction.Normalize(); Debug.Log(pathValue +" "+nextPathValue); //移動 transform.position += direction * Time.deltaTime * speed; //目標地点に近づいたか評価 if (CheckDistance(transform.position, movePath[nextPathValue], range)) { //次のパスに移動先を変更 NextDesignatedLocation(); } } /// <summary> /// 範囲を指定してその範囲ないか調べる関数 /// </summary> /// <param name="_myPos">自身の位置</param> /// <param name="_nextPos">次の移動位置</param> /// <param name="_range">半径</param> /// <returns></returns> bool CheckDistance(Vector3 _myPos, Vector3 _nextPos, float _range) { bool inRange = false; //targetの位置に近づいたらtrue if (Vector3.Distance(_myPos, _nextPos) <= _range) { inRange = true; } return inRange; } /// <summary> /// 次のパスに指定する /// </summary> void NextDesignatedLocation() { switch (moveType) { case MoveType.MoveOn: pathValue = nextPathValue; nextPathValue++; if (nextPathValue == movePath.Length-1) { moveType = MoveType.Return; } break; case MoveType.Return: pathValue = nextPathValue; nextPathValue--; if (nextPathValue == 0) { moveType = MoveType.MoveOn; } break; default: pathValue = nextPathValue; nextPathValue++; if (nextPathValue == movePath.Length - 1) { moveType = MoveType.Return; } break; } }まとめ
急いで書いたとはいえ汎用性がなさそうなものができてしまった…
- 投稿日:2019-11-25T11:48:24+09:00
【Unity】Gameビューにグリッドを表示
やりたいこと
サンプルコード
testusing UnityEngine; [RequireComponent(typeof(MeshFilter)),RequireComponent(typeof(MeshRenderer))] public class LineGrid : MonoBehaviour { Vector3[] verts; //ポリゴンの頂点を入れる int[] triangles; //三角形を描く際に、頂点の描画順を指定する GameObject camera; //カメラ [SerializeField,Header("使用するMaterial")] Material material; [SerializeField,Header("大きさ")] Vector2Int size; [SerializeField,Header("線の太さ")] float lineSize; void Start() { //カメラを取得 camera = GameObject.FindGameObjectWithTag("MainCamera"); } // Update is called once per frame void Update() { CreateGlid(); //カメラをグリッドの中心へ(必要ない場合はコメントアウトしてください) camera.transform.position = new Vector3((float)size.x / 2, ((float)size.y / 2) - 0.1f, -10); } void CreateGlid() { //新しいMeshを作成 Mesh mesh = new Mesh(); //頂点の番号をsize分確保、縦横の線が一本ずつなくなるので+2を入れる、一本の線は頂点6つで表示させるので*6 triangles = new int[(size.x + size.y + 2) * 6]; //頂点の座標をsize分確保 verts = new Vector3[(size.x + size.y + 2) * 6]; //頂点番号を割り当て for (int i = 0; i < triangles.Length; i++) { triangles[i] = i; } //何回for分が回ったかをカウントさせる int x = 0, y = 0; //縦線 for (int i = 0; i < (size.x + 1) * 6; i += 6) { verts[i] = new Vector3(x, 0, 0); verts[i + 1] = new Vector3(x, size.y, 0); verts[i + 2] = new Vector3(lineSize + x, size.y, 0); verts[i + 3] = new Vector3(lineSize + x, size.y, 0); verts[i + 4] = new Vector3(lineSize + x, 0, 0); verts[i + 5] = new Vector3(x, 0, 0); x++; } //横線 for (int i = (size.x+1) * 6; i < (size.x + size.y + 2) * 6; i+=6) { verts[i] = new Vector3(0,y,0); verts[i+1] = new Vector3(size.x+lineSize, y, 0); verts[i+2] = new Vector3(0, y - lineSize, 0); verts[i+3] = new Vector3(size.x + lineSize, y, 0); verts[i+4] = new Vector3(size.x + lineSize, y - lineSize, 0); verts[i+5] = new Vector3(0, y - lineSize, 0); y++; } //作った頂点番号、座標データを作成したmeshに追加 mesh.vertices = verts; mesh.triangles = triangles; //再計算() mesh.RecalculateBounds(); mesh.RecalculateNormals(); //再計算後に完成したMeshを追加 GetComponent<MeshFilter>().mesh = mesh; //設定したMaterialを反映 GetComponent<MeshRenderer>().material = material; } }コード解説1
testtriangles = new int[(size.x + size.y + 2) * 6]; verts = new Vector3[(size.x + size.y + 2) * 6];なぜここで(+2)や(6)をしているかの解説です。
サンプルコードで*Vector2Intが宣言されていますがこれはグリッドのマス目数を表いしています。
例えば(+2)をせずに処理をしたとするとVector2Intの中身がx=2,y=2の場合。
この赤の線のところが描画されなくなってしまいます。
コード解説2
test//何回for分が回ったかをカウントさせる int x = 0, y = 0; //縦線 for (int i = 0; i < (size.x + 1) * 6; i += 6) { verts[i] = new Vector3(x, 0, 0); verts[i + 1] = new Vector3(x, size.y, 0); verts[i + 2] = new Vector3(lineSize + x, size.y, 0); verts[i + 3] = new Vector3(lineSize + x, size.y, 0); verts[i + 4] = new Vector3(lineSize + x, 0, 0); verts[i + 5] = new Vector3(x, 0, 0); x++; } //横線 for (int i = (size.x+1) * 6; i < (size.x + size.y + 2) * 6; i+=6) { verts[i] = new Vector3(0,y,0); verts[i+1] = new Vector3(size.x+lineSize, y, 0); verts[i+2] = new Vector3(0, y - lineSize, 0); verts[i+3] = new Vector3(size.x + lineSize, y, 0); verts[i+4] = new Vector3(size.x + lineSize, y - lineSize, 0); verts[i+5] = new Vector3(0, y - lineSize, 0); y++; }ここでは頂点座標を決めています。
頂点を下の図のように配置すると四角形が形成されます。あとはこの四角形のx,yの長さを決めてあげれば線のように描画できます。
完成
- 投稿日:2019-11-25T11:43:26+09:00
【Unity】なぜGetComponentを毎フレーム取得することが推奨されていないのか
毎フレームGetComponent<>()
よくGetComponentは毎フレームやることはよろしくないという話を聞きますがなぜダメなのでしょうか?
試してみよう!
今回は二つのScriptを用意しました
片方は先にComponent取得してから10万回Transform.positionにvector(0,0,0)を入れる
C#test.csprivate void Start() { Transform t=GetComponent<Transform>(); for (int i = 0; i < 100000; i++) { t.position = new Vector3(0,0,0); } }もう片方は10万回GetComponetして10万回Transform.positionにvector(0,0,0)を入れる
C#test2.csprivate void Start() { for (int i = 0; i < 100000; i++) { GetComponent<Transform>().position = new Vector3(0,0,0); } }結果
2倍近く差が出てるんですけど…
今回は10万回で検証しましたがこれが毎フレーム実行されたとしても2倍近く処理が無駄ってことですね。
仕様上仕方がない理由以外ではUpdateでGetComponentをするこはよろしくなさそうですね。
- 投稿日:2019-11-25T11:31:05+09:00
【Unity】3D座標を2D座標に直す方法
3D座標を2D座標に変換
オブジェクトのxとyの値だけを使えばそれは2Dの座標なのでは?と最初自分は思っていましたがCameraの位置や角度を変えるとうまくいかなくなるのでそれらに対応したものを作っていきましょう。
Script公開
test.cspublic GameObject obj; //3D座標から2D座標に変換したいオブジェクト public GameObject confirmation; //座標がちゃんと変換されているか確認用 void Start() { //カメラを平行投影にする ここ一番重要!透視投影のままだとうまく座標変換できません Camera.main.orthographic = true; } // Update is called once per frame void Update() { confirmation.transform.position = Change2DPos(obj); } Vector2 Change2DPos(GameObject obj3D) { //Camera.mainになっていますが設定したいCameraがあれば変更してくさい。 Vector2 pos2D=Camera.main.ViewportToWorldPoint(Camera.main.WorldToViewportPoint(obj3D.transform.position)); return pos2D; } }Script的にはこうなります。
変換部分の解説
test.cs//一度3Dのオブジェクト座標をビューポート座標に変換。 var pos1= Camera.main.WorldToViewportPoint(obj3D.transform.position); //ここのZの値はカメラのZ座標が入るので一度0にします(ここはご自由に変えてください) pos1.z=0; //ビューポート変換した座標をワールド座標に直します。 var pos2 =Camera.main.ViewportToWorldPoint(pos1);これらのことを一行で書くと最初に書いたコードになります(Z座標とくに考慮してませんが)
終わり
なにか質問やここ間違ってるなどありましたらコメントください。
- 投稿日:2019-11-25T10:07:55+09:00
Unityエディタでテストプレイ中にバグ報告する機能を作る
この記事は【unityプロ技】 Advent Calendar 2019の11日目の記事です。
はじめに
こんにちは、天神いな(@ina_amagami)です。スマートフォンゲーム会社でUnityを使用したゲーム開発をしつつ、2019年現在は開発チームのリードエンジニアとしてプロジェクト効率化業務なども行っています。
個人でアマガミナブログという技術ブログを書いていますが、Qiita初投稿です。よろしくお願いします!
チーム開発をしていると、Unityエディタでテストプレイ中にバグを見つけることが多々ありますよね。
バグだ!と思った時にすぐ担当者に修正依頼を投げられると便利です。そこで今回はUnityエディタからBacklogにバグ報告チケットを追加する機能を作ったので、実装方法含めて紹介します。
Redmineなど他のバグ管理ツールを使用する場合でも、考え方はそのまま使えるはずです。
1. Backlogとは
nurab社によって運営されているタスク管理サービスです。Backlogではタスクの単位を「課題」と呼び、プロダクトのバグ管理にも利用することが可能です。(プログラム上はTicketでややこしいので課題のことを記事内ではチケットと呼びます)
今回は「1プロジェクト10人まで」といった制約が付いているフリープランで利用できる機能の範囲内を想定しています。
制限例として、チケットに添付できるファイル数は1つまでです。そのため今回はGameビューのスクリーンショット添付のみ作成しますが、有料プランをお使いの方は他のファイルを追加で添付するのも良いでしょう。
例えば僕が開発チームに導入しているものはログファイルを添付しています。
2. 作ったもの
こんな感じのウィンドウを開いてバグ報告します。
チケットの作成が完了したら、すぐにブラウザで確認できます。
Backlog上にはこんな感じで登録されます。
ツール化しておくことで、報告時の詳細文フォーマットを固定する効果もあります。
BacklogにはSlackやChatworkへの通知設定もあるので、併せて設定しておくと捗ります。
3. ダウンロード
個人開発用にフリープランで使うため作り直したものをGitHubにて公開中です。あくまで作例なので、ご利用する環境に応じてカスタマイズして頂ければと思います。
Assets/Editor/BugReporter
以下にバグ報告機能の作例が入っています。作例と記事内で公開しているコードは全てパブリックドメインです(ライセンスを主張しません)
作例内で使用している各種プラグインやパッケージについては、各々のライセンスに従って下さい。
この記事では実装方法の解説がメインなので、導入方法だけ知りたいという方に向けて個人ブログに導入方法だけの解説を用意する予定です。
4. 確認環境
- macOS High Sierra
- Unity2018.3.12f1 / 2019.1.8f1 / 2019.2.13f1(Unity2018.3以上必須)
- Google Chrome 78.0
5. 今回やること
- BacklogAPI認証
- SettingsProviderでツールの設定をProjectSettingsから行う
- ScriptableWizardで報告ウィンドウを作成
- 直近のエラーログを本文に追加する
- スクリーンショット添付機能の作成
など
6. スタンス
このような効率化ツールを作成する上で重要なのは、いかにローコストで最良の結果を得るかです。
- 使えるライブラリがあるならそれを使って自作はしない
- プロダクト本体ほど設計にこだわらない
- 実装コストが高い機能を無理に導入しない
勉強目的であれば話は変わりますが、今回はプロジェクト効率化業務としての観点を重視していきます。
入門記事ではないため、前提知識の解説はある程度スキップして進めていきます。ご了承下さい。
実装準備
作例プロジェクトでは
5. Backlogアプリケーション登録
以外は済ませてある状態なので作例からコピーでもOKです。
Plugins/
内のNBacklog
とunity-backlog
にはAssembly Definitionを設定しています。これらが原因でうまく動作しない場合は.asmdef
ファイルを削除するか、以下を参考にして下さい。参考:Unity Assembly Definition 完全に理解した
1. NBacklogの導入
BacklogAPIを叩くためのライブラリはBacklogAPIのマニュアルにまとまっています。
C#向けの公式ライブラリはありませんが、hal1932氏が作成したNBacklogがGitHubで公開されているので、こちらをプロジェクト内に導入します。
2. NuGet / Newtonsoft.Jsonの導入
NBacklogを導入するとNewtonsoft.Jsonが無いためエラーが出ます。
今回はNuGetForUnityを使ってインストールします。
NuGetForUnityをプロジェクトに追加したら、メニューから
NuGet > Manage NuGet Packages
を開きます。リストからJson.NETをインストールします。
NuGetを導入することで
Assets/
直下にファイルが色々と増えてしまうので、これが気になる方はNewtonsoft.Jsonのdllを直接プロジェクトに導入して下さい。3. NBacklogのコードを一部修正
NBacklog/Rest/RestClient.cs
を修正します。Unityに入っているバージョンの
HttpClient
ではMaxConnectionsPerServer
に対応していないため、こちらを以下のように書き換えますRestClient.cspublic RestClient(string baseUri, int maxConnectionPerServer = 256, JsonSerializer serializer = null) { // _client = new HttpClient(new HttpClientHandler() // { // MaxConnectionsPerServer = maxConnectionPerServer, // }); _client = new HttpClient(new HttpClientHandler());4. ビルドに含まれないようにする
NBacklog、NuGetでインポートしたパッケージをバグ報告機能以外で利用しない場合は、アプリサイズを肥大化させないためにビルドに含まれないようにします。
4-1. NBacklogをEditorフォルダ以下に移動
NBacklogを
Editor
フォルダ以下に移動させます。その際、
System.Drawing
とSystem.Web
がうまく読み込めずにエラーが出ます。また、Unity2019.2以降ではEditorフォルダ以下に入れなくてもアセンブリの仕様が変わったためか同様のエラーが出ます。これらを解消するには
csc.rsp
というファイルを作成します。参考:Unity - Manual: Referencing additional class library assemblies
csc.rsp
を既に他の用途で作成している場合は以下を追記、ない場合はAssets/
直下に作成して下さい。csc.rsp-r:System.Drawing.dll -r:System.Web.dll対応としてこれが最適なのかは分からないですが、Android/iOSでNBacklogをアプリに含めるよりはビルド後のサイズが削減できていることは確認済みです。
4-2. NuGet / packagesのImport設定
Assets/
直下にNuGet.config
とpackages.config
というファイルが作成されているので、これらのInspectorから設定を変更します。Include PlatformsのEditorだけチェックを入れてApplyして下さい。
5. Backlogアプリケーション登録
NBacklogを利用したBacklogAPIの認証にはOAuth2を利用します。
通信処理を用意すればAPIKeyを利用することも可能ですが、チケットの登録者がAPIKeyを発行した人になってしまうことで誰からのバグ報告なのか分からなくなる問題もあり、OAuth2を推奨します。
認証に利用する
client_id
とclient_secret
を取得するにはBacklogアプリケーション登録のページからログインしてアプリケーション登録を行います。新規登録ボタンをクリックして登録画面に入ります。
5-1. RedirectURL
今回はWebサイトからの認証ではないので特に使いません。指定は必要なので
http://localhost:12345
とかhttp://localhost:54321
とか適当なURIでOKです。5-2. 認証画面に表示される情報を設定
デフォルトは
English
になっています。そのままでもいいですが、今回は日本語に変更しておきます。下までスクロールすると「この言語を削除する」というボタンがあるのでこれを押して削除します。
消えたら日本語で新しく追加します。追加できたら「この言語をデフォルトの表示にする」にチェックを入れて下さい。このチェックを付けないと登録に失敗します。
アプリケーション名とアプリケーションの説明を入力します。今回はバグ報告にしか利用しないので説明は「バグ報告する機能です」としていますが、同一の認証で他の機能も使えるようにする場合はそれも書き加えておきます。
サイトURLは不要です。これで登録すると
client_id
とclient_secret
が発行されます。以上で準備は完了です。
BacklogAPI認証
NBacklogをエディタ拡張で扱いやすいようにラップしたものを用意しました。
※今回のバグ報告ツールに必要な機能以外は特に用意していませんのでご了承下さい。こちらはMITライセンスで公開していますが、記事内に抜粋したコードはパブリックドメインです。
重要なポイントを解説していきます。
1. 設定ファイルの用意
プロジェクト毎に変更しやすいように設定ファイルをScriptableObjectで作成します。
BacklogAPIData.csusing UnityEngine; using UnityEditor; using System.IO; /// <summary> /// Backlog API関連のプロジェクト依存データ /// </summary> public class BacklogAPIData : ScriptableObject { /// <summary> /// アセットパス /// </summary> private const string AssetPath = "Backlog/BacklogAPI.asset"; [Header("Backlog Developerサイトで登録して下さい")] public string RedirectURI; public string ClientId; public string ClientSecretId; [Header("対象プロジェクト")] public string SpaceKey; public string Domain; public string ProjectKey; [Header("認証情報のキャッシュファイル名(gitignoreで除外して下さい)")] public string CacheFileName = "backlog_oauth2cache.json"; /// <summary> /// APIデータをロード /// </summary> public static BacklogAPIData Load() { var asset = EditorGUIUtility.Load(AssetPath); if (!asset) { // 無かったら作成 CreateAsset(); asset = EditorGUIUtility.Load(AssetPath); } return asset as BacklogAPIData; } /// <summary> /// アセット作成 /// </summary> public static void CreateAsset() { var outputPath = "Assets/Editor Default Resources/" + AssetPath; var fullDirPath = Path.GetDirectoryName(Application.dataPath.Replace("Assets", outputPath)); if (!Directory.Exists(fullDirPath)) { Directory.CreateDirectory(fullDirPath); } AssetDatabase.CreateAsset(CreateInstance<BacklogAPIData>(), outputPath); AssetDatabase.Refresh(); } /// <summary> /// SerializedObjectで取得 /// </summary> public static SerializedObject GetSerializedObject() { return new SerializedObject(Load()); } }設定項目については後ほど説明します。
1-1. データが無ければ作成する
複数作るScriptableObjectであれば右クリックメニューから作成できるようにしておいても良いと思いますが、今回はプロジェクトに1つだけあれば大丈夫な設定ファイルなので最初にロードしようとした時に作成するようにしました。
ロードには
EditorGUIUtility.Load()
を使用しています。このメソッドではロード先をAssets/Editor Default Resources/
内で指定するので、ファイルがなかった時にはこの中に作成しています。他の保存先にしたい場合は書き換えて使って下さい。
1-2. SerializedObjectでの取得
エディタ拡張ではSerializedObjectで取り回すことが多いので取得メソッドを用意しています。
2. ProjectSettingsから設定可能にする
Unity2018.3から、UIElementsの機能で
SettingsProvider
を継承したクラスを作成することでProjectSettingsに設定項目を追加できるようになったのでこちらを利用します。参考:【Unity】Unity 2018.3 で Project Settings ウィンドウに独自のメニューを追加する方法
ちなみに以前は
PreferenceItem
属性を使ってPreferencesに設定項目を追加できましたが、こちらはUnity2019.1から非推奨になっています。BacklogAPISettings.csusing UnityEditor; using UnityEngine; #if UNITY_2019_1_OR_NEWER using UnityEngine.UIElements; #else using UnityEngine.Experimental.UIElements; #endif /// <summary> /// BacklogAPIの設定ファイルをProjectSettingsから編集できるようにする /// </summary> public class BacklogAPISettings : SettingsProvider { private const string Path = "Project/BacklogAPI"; public BacklogAPISettings(string path, SettingsScope scope) : base(path, scope) { } /// <summary> /// ProjectSettingsに項目追加 /// </summary> [SettingsProvider] private static SettingsProvider Create() { var provider = new BacklogAPISettings(Path, SettingsScope.Project) { // 検索対象のキーワード登録(SerializedObjectから自動で取得) keywords = GetSearchKeywordsFromSerializedObject(BacklogAPIData.GetSerializedObject()) }; return provider; } private static SerializedObject so; public override void OnActivate(string searchContext, VisualElement rootElement) { // 設定ファイル取得 so = BacklogAPIData.GetSerializedObject(); } public override void OnGUI(string searchContext) { // プロパティの表示 var iterator = so.GetIterator(); EditorGUI.BeginChangeCheck(); while (iterator.NextVisible(true)) { bool isScript = iterator.name.Equals("m_Script"); if (isScript) { GUI.enabled = false; } EditorGUILayout.PropertyField(iterator); if (isScript) { GUI.enabled = true; } } if (EditorGUI.EndChangeCheck()) { so.ApplyModifiedProperties(); } EditorGUILayout.Space(); // データ検証用ボタン using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("スペースを開く")) { var data = BacklogAPIData.Load(); Application.OpenURL($"https://{data.SpaceKey}.{data.Domain}/projects/{data.ProjectKey}"); } if (GUILayout.Button("認証テスト")) { var backlogAPI = new BacklogAPI(); try { backlogAPI.LoadProjectInfo(() => { EditorUtility.DisplayDialog("認証成功", "BacklogAPIの認証に成功しました。", "OK"); }); } catch (System.Exception e) { Debug.LogException(e); } } } } }ポイントをいくつか解説します。
2-1. SettingsProviderの作成
SettingsProvider
属性を付けたstaticメソッドでインスタンスを返すことで設定項目として追加されます。private const string Path = "Project/BacklogAPI"; [SettingsProvider] private static SettingsProvider Create() { var provider = new BacklogAPISettings(Path, SettingsScope.Project) { // 検索対象のキーワード登録(SerializedObjectから自動で取得) keywords = GetSearchKeywordsFromSerializedObject(BacklogAPIData.GetSerializedObject()) }; return provider; }
GetSearchKeywordsFromSerializedObject
でSerializedObjectに含まれるプロパティからキーワードを抽出して設定しておきます。これだけでProjectSettingsの右上にある検索窓から項目を検索できるようになります。SettingsProvider最高!
2-2. プロパティの表示
// プロパティの表示 var iterator = so.GetIterator(); EditorGUI.BeginChangeCheck(); while (iterator.NextVisible(true)) { bool isScript = iterator.name.Equals("m_Script"); if (isScript) { GUI.enabled = false; } EditorGUILayout.PropertyField(iterator); if (isScript) { GUI.enabled = true; } } if (EditorGUI.EndChangeCheck()) { so.ApplyModifiedProperties(); }今回は特に表示のカスタマイズは行わないので、イテレータでぶん回してPropertyFieldを表示していきます。
Scriptは変更できてしまうと困るので、Inspectorと同じように編集不可で表示します。
編集したのに保存されない問題に遭遇しないよう、最後に
ApplyModifiedProperties()
を呼ぶのを忘れずに。2-3. テストボタン
入力したデータが正しいかどうか検証するためのボタンを用意します。
// データ検証用ボタン using (new EditorGUILayout.HorizontalScope()) { if (GUILayout.Button("スペースを開く")) { var data = BacklogAPIData.Load(); Application.OpenURL($"https://{data.SpaceKey}.{data.Domain}/projects/{data.ProjectKey}"); } if (GUILayout.Button("認証テスト")) { var backlogAPI = new BacklogAPI(); try { backlogAPI.LoadProjectInfo(() => { EditorUtility.DisplayDialog("認証成功", "BacklogAPIの認証に成功しました。", "OK"); }); } catch (System.Exception e) { Debug.LogException(e); } } }スペースを開く
Backlog対象プロジェクトのホーム画面をブラウザで開きます。
認証テスト
API認証が通って、プロジェクト情報が正しくロードできるかテストします。バグ報告を実行する時も同じですが、認証時はブラウザが起動します。
「許可する」を押したらリダイレクト先のページ(今回の例では
http://localhost:12345
)が開きますが、必要ないので閉じます。Unity側に戻ると認証成功のダイアログが出ます。
一度認証に成功するとキャッシュファイルが作られるので、以降ブラウザは起動しません。
2-4. 設定項目
ProjectSettingsではこのように表示されます。
Backlog Developerサイトで発行した情報
Redirect URI
Client Id
Client Secret
は、Backlog Developerサイトで発行した情報を入力して下さい。対象プロジェクトの情報
Backlogプロジェクトのホーム画面URLから各項目が分かります。
https://{Space Key}.{Domain}/projects/{Project Key}
例)
https://amagamina.backlog.com/projects/BUG_REP
この例であれば
Space Key
がamagamina
、Domain
がbacklog.com
、Project Key
がBUG_REP
です。プロジェクト情報を間違えてしまうと全然関係ないプロジェクトに対してバグ報告してしまうことになるので注意して下さい。
また、認証でログインしたアカウントが対象プロジェクトに参加していない人であればバグ報告に失敗します。認証情報のキャッシュファイル名(パス)
このキャッシュファイルはプロジェクトのルート(Assetsフォルダと同じ階層)を基準にしたパスで作成されます。
git管理している場合であれば共有されないようignoreに追加するか、既にignore対象になっているパスに書き換えて下さい。
2-Ex. レイアウト調整
このままでも問題ないですが几帳面な僕はBacklogAPIの設定項目の中身が他の設定に比べて左側に寄っているのが気になるので、直しておきます。
他の設定がやっていることと同じことをやればいいよね。と思ってUnityCsReferenceを漁ってみたらSettingsWindow.csが機能を持っていることが分かったのですが、internal…。
仕様変更になる可能性もあるので、やり方だけ参考にして自前で用意することにしました。
HorizontalScope
やVerticalScope
と同じようなGUIScopeを作成します。BacklogAPISettings.cs/// <summary> /// 他の設定項目と比べて左側の余白が無いので、GUIScopeを作って付ける /// </summary> internal class GUIScope : GUI.Scope { public GUIScope() { GUILayout.BeginHorizontal(); GUILayout.Space(6f); GUILayout.BeginVertical(); } protected override void CloseScope() { GUILayout.EndVertical(); GUILayout.EndHorizontal(); } }中に書かれてるものを
OnGUI()
の中にそのまま書いても同じですが、usingを使えばコードがスッキリするので、これを使います。BacklogAPISettings.csusing (new GUIScope()) { // プロパティの表示 ... }これで余白ができました。
3. BacklogAPI認証処理
BacklogAPI.csusing System.Linq; using UnityEngine; using NBacklog; using NBacklog.DataTypes; using NBacklog.OAuth2; /// <summary> /// BacklogAPI /// </summary> public class BacklogAPI { /// <summary> /// APIデータ /// </summary> public BacklogAPIData APIData { get; private set; } /// <summary> /// スペース /// </summary> public NBacklog.DataTypes.Space Space { get; private set; } /// <summary> /// プロジェクト /// </summary> public Project Project { get; private set; } /// <summary> /// プロジェクト情報 /// </summary> public class ProjectData { /// <summary> /// チケットタイプ /// </summary> public TicketType[] TicketTypes; /// <summary> /// 優先度 /// </summary> public Priority[] Priorities; /// <summary> /// カテゴリ /// </summary> public Category[] Categories; /// <summary> /// マイルストーン /// </summary> public Milestone[] Milestones; /// <summary> /// ユーザー /// </summary> public User[] Users; } public ProjectData Data { get; } = new ProjectData(); /// <summary> /// プロジェクト情報の取得 /// </summary> public async void LoadProjectInfo(System.Action onSuccess = null) { APIData = BacklogAPIData.Load(); // 認証 var client = new BacklogClient(APIData.SpaceKey, APIData.Domain); await client.AuthorizeAsync(new OAuth2App() { ClientId = APIData.ClientId, ClientSecret = APIData.ClientSecretId, RedirectUri = APIData.RedirectURI, CredentialsCachePath = APIData.CacheFileName, }); // 各種データ取得 Space = client.GetSpaceAsync().Result.Content; Project = client.GetProjectAsync(APIData.ProjectKey).Result.Content; Data.TicketTypes = Project.GetTicketTypesAsync().Result.Content; Data.Priorities = client.GetPriorityTypesAsync().Result.Content; Data.Categories = Project.GetCategoriesAsync().Result.Content; Data.Milestones = Project.GetMilestonesAsync().Result.Content; Data.Users = Project.GetUsersAsync().Result.Content; onSuccess?.Invoke(); } }良い作りとは言えませんが呼び元がasyncでなくとも問題ないように戻り値をvoidにして成功コールバックを返す形にしています。気になる方は
async Task
に書き換えて下さい。
AuthorizeAsync()
はawaitが必須です。同期処理で書いてしまうとブラウザを起動して認証が完了するまで拘束されるので、認証が終わってもUnityエディタに戻ってこれなくなります。認証するついでにプロジェクトの各種情報も取得しておきます。例えば
GetUsersAsync()
はプロジェクトメンバーの一覧が取得できるので、バグ報告UIから担当者を選ぶドロップダウンリストに使用します。バグ報告用の設定ファイルを作成
バグ報告する時にカテゴリや担当者をドロップダウンから選ぶようにしますが、デフォルト値を設定しておきたいのでこちらも設定ファイルを作ります。
作成方法はBacklogAPIの設定ファイルと同じなので省略します。
先述した左側の余白だけはBacklogAPIでは6pxだったのに対してここでは10pxにしないと同じになりませんでした。原因はよくわからないです…。
発生バージョンやカテゴリはBacklog上のプロジェクト設定で作成しておきます。
発生バージョンについてはバージョンが上がる度にこの設定も更新しないと報告者が古いバージョンを指定したまま報告するミスが発生する可能性もあります。
そのため、もしBacklog上で設定している発生バージョンとUnityのPlayerSettingsで設定しているバージョンが同じで問題ないなら同期させておくのがいいでしょう。
バグ報告ウィザードの作成
それではバグ報告機能の作成に入ります。今回はScriptableWizardを使用します。
1. ScriptableWizardって何?
ちょっとした設定を入れてアセットを作成したりとか複雑なウィンドウが必要ない時にサクっと使えるウィンドウです。個人的には気に入っていてよく使っています。
設定機能はSettingsProviderを使用しましたが、直近だとUnity2018.4でバージョンを止めるプロジェクトも多いと思うのでここにはUIElementsは使っていません。
2. ウィザードの生成
まずはUnityのメニューに
Backlog/バグ報告
の項目を追加して、クリックしたらウィザードが生成されるようにします。ポイントは以下の4つです。
- プレイ中のみ開けるようにする
- BacklogAPIの認証&プロジェクト情報のロードが終わってからウィザードを開く
- ヘッダでスペース名とプロジェクト名が分かるようにする(違うプロジェクトに対してバグ報告しようとしてないか確認する意味も込めて)
- 項目がちゃんと収まるようにウィンドウの最小サイズを設定する
BugReportWizard.cs/// <summary> /// Backlogにバグ報告チケットを追加する /// </summary> public class BugReportWizard : ScriptableWizard { private const string MenuPath = "Backlog/バグ報告"; /// <summary> /// BacklogAPI /// </summary> private static readonly BacklogAPI m_BacklogAPI = new BacklogAPI(); private static BacklogAPI.ProjectData ProjectData => m_BacklogAPI.Data; // 1. プレイ中のみ開けるようにする [MenuItem(MenuPath, validate = true)] static bool OpenValidate() { return EditorApplication.isPlaying; } [MenuItem(MenuPath)] static void Open() { try { EditorUtility.DisplayProgressBar("Backlog", "プロジェクト情報をロード中です...", 0f); // 2. BacklogAPIの認証&プロジェクト情報のロードが終わってからウィザードを開く m_BacklogAPI.LoadProjectInfo(OpenWizard); } catch (Exception e) { Debug.LogException(e); } finally { EditorUtility.ClearProgressBar(); } } private static void OpenWizard() { // 3. ヘッダでスペース名とプロジェクト名が分かるようにする string header = $"{m_BacklogAPI.Space.Name}:{m_BacklogAPI.Project.Name}:バグ報告"; var wizard = DisplayWizard<BugReportWizard>(header, "バグ報告する"); // 4. 項目がちゃんと収まるようにウィンドウの最小サイズを設定する wizard.minSize = new Vector2(640f, 440f); } ... }ウィザードの表示は
DisplayWizard
を使用します。第2引数に渡しているのは作成ボタン(今回はバグ報告ボタン)に表示する文字です。3. デフォルト値の取得
ロードしたプロジェクト情報から各項目のプルダウンリストの元になるstringのリストを作成しつつ、ProjectSettingsで設定したデフォルト値のインデックスを取得しておきます。
BugReportWizard.cs// 種別は固定 private TicketType ticketType; // プルダウンで選択するもの private int priorityIndex; private List<string> priorityDropDown; private int versionIndex; private List<string> versionDropDown; private int categoryIndex; private List<string> categoryPullDown; private int assigneeIndex; private List<string> assigneePullDown; // ウィザードが有効になる時に自動で呼ばれる private void OnEnable() { var defaultValues = BugReportData.Load(); // 種別(固定) ticketType = ProjectData.TicketTypes.FirstOrDefault(x => x.Name == defaultValues.TicketType); // 優先度 priorityDropDown = ProjectData.Priorities.Select(x => x.Name).ToList(); priorityIndex = priorityDropDown.FindIndex(x => x.Equals(defaultValues.Priority)); // 発生バージョン versionDropDown = ProjectData.Milestones.Select(x => x.Name).ToList(); versionIndex = versionDropDown.FindIndex(x => x.Equals(defaultValues.CurrentVersion)); // カテゴリ categoryPullDown = ProjectData.Categories.Select(x => x.Name).ToList(); categoryIndex = categoryPullDown.FindIndex(x => x.Equals(defaultValues.Category)); // 担当者 assigneePullDown = ProjectData.Users.Select(x => x.Name).ToList(); assigneeIndex = assigneePullDown.FindIndex(x => x.Equals(defaultValues.Assignee)); }4. ウィンドウの描画
描画される時は
DrawWizardGUI()
がコールされます。この中で項目の配置を書いていきます。BugReportWizard.cs// 設定項目 private string ticketTitle; private string content; private string howTo; private bool isCaptureScreenShot = true; private bool isSendLog = true; private string searchText; protected override bool DrawWizardGUI() { if (m_BacklogAPI.Project == null || ticketType == null) { // 初期化失敗 Close(); return false; } EditorGUILayout.LabelField("タイトル(必須)"); ticketTitle = EditorGUILayout.TextField(ticketTitle); if (string.IsNullOrEmpty(ticketTitle)) { Color cCache = GUI.contentColor; GUI.contentColor = Color.red; EditorGUILayout.LabelField("タイトルを入力して下さい"); GUI.contentColor = cCache; } EditorGUILayout.Space(); const float minHeight = 42f; EditorGUILayout.LabelField("バグ内容"); content = EditorGUILayout.TextArea(content, GUILayout.MinHeight(minHeight)); EditorGUILayout.Space(); EditorGUILayout.LabelField("再現方法"); howTo = EditorGUILayout.TextArea(howTo, GUILayout.MinHeight(minHeight)); EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { priorityIndex = EditorGUILayout.Popup("優先度", priorityIndex, priorityDropDown.ToArray()); versionIndex = EditorGUILayout.Popup("発生バージョン", versionIndex, versionDropDown.ToArray()); } EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { categoryIndex = EditorGUILayout.Popup("カテゴリ", categoryIndex, categoryPullDown.ToArray()); assigneeIndex = EditorGUILayout.Popup("担当者", assigneeIndex, assigneePullDown.ToArray()); } EditorGUILayout.Space(); using (new EditorGUILayout.HorizontalScope()) { isCaptureScreenShot = EditorGUILayout.Toggle("スクリーンショットを送る", isCaptureScreenShot); isSendLog = EditorGUILayout.Toggle("ログを送る", isSendLog); } EditorGUILayout.Space(); // キーワードバグ検索機能 EditorGUILayout.Space(); EditorGUILayout.LabelField("キーワードバグ検索(ブラウザで開きます)"); using (new EditorGUILayout.HorizontalScope()) { searchText = GUILayout.TextField(searchText, "SearchTextField", GUILayout.Width(200)); GUI.enabled = !string.IsNullOrEmpty(searchText); if (GUILayout.Button("Clear", "SearchCancelButton")) { searchText = string.Empty; } GUI.enabled = true; if (GUILayout.Button("検索", GUILayout.Width(60f))) { string searchURL = "https://{0}.{1}/find/{2}?condition.projectId={3}&condition.issueTypeId={4}&condition.statusId=1&condition.statusId=2&condition.statusId=3&condition.limit=20&condition.offset=0&condition.query={5}&condition.sort=UPDATED&condition.order=false&condition.simpleSearch=false&condition.allOver=false"; var uri = new Uri(string.Format( searchURL, m_BacklogAPI.Space.Key, m_BacklogAPI.APIData.Domain, m_BacklogAPI.Project.Key, m_BacklogAPI.Project.Id, ticketType.Id.ToString(), searchText)); Application.OpenURL(uri.AbsoluteUri); } } EditorGUILayout.Space(); return true; }キーワードバグ検索機能
これはBacklog上に登録されているバグをキーワード検索する機能です。ブラウザで開くだけですが…。
今から報告しようとしているバグが既に登録されてるかも!と思った時に調べることができます。
検索画面のURLにパラメータをゴリっと埋め込んでいるので、URL仕様が変わることがあれば修正が必要になりそうです(ないことを祈ります)。
TextFieldのスタイルに
SearchTextField
、ButtonにSearchCancelButton
を使えばよくある検索窓を作れるので、覚えておくと何かと便利かと思います。5. タイトルを入力するまで報告ボタンを押せないようにする
BacklogAPIの仕様でチケットのタイトルは必須項目です。
ScriptableWizardに
isValid
というプロパティが定義されていて、これが作成ボタン(報告ボタン)がアクティブかどうか判定するために使われています。テキストが入力されたりなど、ウィザードに何かしら更新があった時は
OnWizardUpdate()
がコールされるので、この中でタイトルが入力されているかどうかチェックします。BugReportWizard.csprivate void OnWizardUpdate() { isValid = !string.IsNullOrEmpty(ticketTitle); }チケットの登録
ウィザードの準備は終わりました。最後にバグ報告ボタンが押された後のチケット登録処理を見ていきます。
BugReportWizard.csprivate async void OnWizardCreate() { EditorUtility.DisplayProgressBar("Backlog", "準備中です...", 0f); ...ボタンが押されると、
OnWizardCreate()
がコールされます。スクリーンショットを撮影する際に非同期待ちできると楽だったので
async
を付けて非同期で呼ばれるようにしておきます。手順を見ていきます。
1. 詳細文の作成
入力された情報を基に詳細文を作成します。バグ内容、再現方法は入力された時のみ見出しを追加するようにしておきます。
BugReportWizard.cs//-- 詳細作成 string desc = ""; // バグ内容 if (!string.IsNullOrEmpty(content)) { desc += $"【バグ内容】\n{content}\n\n"; } // 再現方法 if (!string.IsNullOrEmpty(howTo)) { desc += $"【再現方法】\n{howTo}\n\n"; } // 発生OS desc += "【環境】\n"; #if UNITY_EDITOR_OSX desc += "発生OS:Mac\n"; #elif UNITY_EDITOR_WIN desc += "発生OS:Windows\n"; #endifついでに発生OSを知りたいこともあると思うので簡単にくっつけておきます。
Unity2019.3からLinux版のエディターも提供されているので必要であれば対応しておくと良いでしょう。
もっと細かい情報を取得したい場合はSystemInfoクラスを使用して下さい。
2. ログを詳細文に入れる
フリープランは1つしかファイルを添付できず、その枠はスクリーンショットに利用したいのでログは詳細文の中に詰め込みます。
BugReportWizard.csif (isSendLog) { desc += "\n【ログ】\n"; desc += LogRecorder.GetBacklogLogText(); }直近で発生したログを記録しておくための
LogRecorder
クラスを作成します。LogRecorder.csusing System.Collections.Generic; using UnityEngine; using System; public static class LogRecorder { /// <summary> /// ログデータ /// </summary> private struct LogData { /// <summary> /// メッセージ /// </summary> public string Message; /// <summary> /// スタックトレース /// </summary> public string StackTrace; /// <summary> /// ログタイプ /// </summary> public LogType Type; /// <summary> /// 最終発生日時 /// </summary> public DateTime LastDate; } // 最大保持件数(重複は排除した後) private const int maxLog = 32; // ログリスト private static List<LogData> logList = new List<LogData>(maxLog); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void RegisterReceiveLog() { Application.logMessageReceived += ReceiveLog; } private static void ReceiveLog(string logMessage, string logStackTrace, LogType logType) { // 通常のログは無視 if (logType == LogType.Log) { return; } // 既に追加されているものと同じなら最終発生時間だけ更新 for (int i = 0; i < logList.Count; ++i) { var log = logList[i]; if (log.Type == logType && log.Message.Equals(logMessage) && log.StackTrace.Equals(logStackTrace)) { log.LastDate = DateTime.Now; logList[i] = log; return; } } if (logList.Count >= maxLog) { // 発生時間が最も古いものを取り除く SortListByLastDate(); logList.RemoveAt(logList.Count - 1); } logList.Add(new LogData { Message = logMessage, StackTrace = logStackTrace, Type = logType, LastDate = DateTime.Now }); } // 最終発生時間でソート private static void SortListByLastDate() { logList.Sort((x, y) => DateTime.Compare(y.LastDate, x.LastDate)); } ... }
Application.logMessageReceived
にコールバックを設定することでログが記録されたタイミングで取得できます。このコールバック登録タイミングですが、エディタ停止時なども受け取ってしまうと困るのでプレイ開始直後に登録します。
MonoBehaviour継承のスクリプトを用意しておいてAwakeで…というのはイマイチなので、
RuntimeInitializeOnLoadMethod
属性を付けたメソッドを作ります。
RuntimeInitializeLoadType.BeforeSceneLoad
を指定することで、シーンがロードされる前のタイミングで登録可能です。[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] private static void RegisterReceiveLog() { Application.logMessageReceived += ReceiveLog; }ログファイルではなく詳細文に差し込む都合上、ログの量が多いとチケットが見づらくなってしまいます。
そのためここでは重複は排除した上で32件まで、デバッグ用ログは含めないようにしています。
作成したログリストは、Backlogの詳細文に適した形でフォーマットして取得できるようにしておきます。
LogRecorder.cspublic static string GetBacklogLogText() { SortListByLastDate(); string text = ""; foreach (var log in logList) { switch (log.Type) { case LogType.Error: case LogType.Exception: case LogType.Assert: text += "''&color(#ff0000) { "; break; case LogType.Warning: text += "''&color(#bbbb00) { "; break; default: continue; } text += $"{log.Type.ToString()}''"; text += " }\n"; text += $"''LastDate'' {log.LastDate}\n"; text += $"''Message'' {log.Message}\n"; text += $"''StackTrace'' {log.StackTrace}\n"; } return text; }例えば
&color(カラーコード) {文字}
で文字色を変更できるので、エラー系統は赤、Warningは黄色などにしています。なおフォーマットはMarkdownかBacklog独自フォーマットかをプロジェクトの基本設定で選択できるようになっています。
ここがMarkdownになっている場合は上記の例ではうまくフォーマットできないので、Markdownに書き直すか設定を変更する必要があります。
3. スクリーンショットの添付
続いてスクリーンショットの添付です。
3-1. ファイル添付の方法
添付というとチケット登録の通信と同時に送るイメージを持つかもしれませんが、これは違います。
Backlogの添付ファイルは、まず最初に仮アップロードします。
するとレスポンスで添付ファイルに振られたIDなどの情報を取得できます(NBacklog上はAttachmentクラス)。
この情報をチケット登録する際に指定することで、仮アップロードしたファイルは削除され、チケット情報と一緒に添付ファイルとして登録されます。
3-2. スクリーンショット添付の手順
今回は以下のような手順でスクリーンショットを添付しています。
- Gameビューのキャプチャをjpgで保存
- Backlog上に仮アップロード
- jpgファイルを削除
BugReportWizard.cs//-- 添付ファイルの作成 var attachments = new List<Attachment>(); // 画面スクショ string screenShotName = string.Empty; if (isCaptureScreenShot) { EditorUtility.DisplayProgressBar("Backlog", "スクリーンショットを添付しています...", 0.25f); // 1. Gameビューのキャプチャをjpgで保存 screenShotName = $"capture_{DateTime.Now.ToString("yyyy_MM_dd_H-mm-ss")}.jpg"; ScreenCapture.CaptureScreenshot(screenShotName); string path = $"{Application.dataPath}/../{screenShotName}"; // 撮影完了待ち bool isPaused = EditorApplication.isPaused; EditorApplication.isPaused = false; var startTime = DateTime.Now; var timeLimit = new TimeSpan(0,0,5); while (!File.Exists(path)) { await Task.Delay(500); if (DateTime.Now - startTime > timeLimit) { // タイムアウト EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("エラー", "スクリーンショットの撮影に失敗しました", "OK"); return; } } EditorApplication.isPaused = isPaused; try { // 2. Backlog上に仮アップロード var attachment = m_BacklogAPI.AddAttachment(path); if (attachment == null) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("エラー", "スクリーンショットの添付に失敗しました", "OK"); return; } attachments.Add(attachment); } catch (Exception e) { EditorUtility.ClearProgressBar(); Debug.LogException(e); } finally { // 3. jpgファイルを削除 File.Delete(path); } }ファイル名は後ほど使うので、キャッシュしておきます。保存先は
Assets
と同じ階層です。Gameビューをキャプチャする
ScreenCapture.CaptureScreenshot()
の注意点として、保存したファイルを利用する際は撮影完了待ちをしないといけません。ここでは簡単な対応法として0.5秒ごとにファイルの存在をチェックして完了するまで待機しています。永久に終わらないことも考えられるのでタイムアウトも付けておきます。
この際の注意点として、エディタが再生中かつ一時停止しているとawaitのところから先に進まなくなってしまいます。
そのため一時停止されている場合は解除して、完了待ちが終わったら元に戻します。
bool isPaused = EditorApplication.isPaused; EditorApplication.isPaused = false; ... EditorApplication.isPaused = isPaused;スクリーンショットを送る時はその場面を逃さないために一時停止してからバグ報告を起動することが多いはずなので、この対応は必須です。
BacklogAPIの認証処理もawaitで止まる
エディタが一時停止しているとawaitから進まなくなる問題はAPIの認証処理でも起こります(という事に記事を書いている途中で気づきました…)。
ProjectSettingsの認証テストから認証する場合は再生停止中でも認証ができるので問題ないですが、初めてバグ報告を利用する人が一時停止した状態でウィザードを開く可能性もあるので、対応を入れておきます。
BacklogAPI.cs// エディタが再生中かつ一時停止中だと認証時にawaitで止まってしまうので、キャッシュがない時は一時停止を解除する bool isPaused = EditorApplication.isPaused; if (EditorApplication.isPlaying) { bool isCached = File.Exists($"{Application.dataPath}/../{APIData.CacheFileName}"); if (!isCached) { EditorApplication.isPaused = false; } } // 認証 var client = new BacklogClient(APIData.SpaceKey, APIData.Domain); await client.AuthorizeAsync(new OAuth2App() {...}); EditorApplication.isPaused = isPaused;初回だけ一時停止で残した場面を逃してしまうことになりますが、ウィザードが開かず混乱するよりは良いでしょうという事で今回はこれくらいの対応に留めておきます。
3-3. ファイルの仮アップロード処理
BacklogAPI.csに実装しておきました。こちらを使用します。
なお、ファイルはプロジェクトをまとめた組織単位にあたるスペースに対して追加します。
BacklogAPI.cs/// <summary> /// スペースに添付ファイルを追加 /// </summary> public Attachment AddAttachment(string filePath) { var fileInfo = new System.IO.FileInfo(filePath); var res = Space.AddAttachment(fileInfo).Result; if (CheckIsRetry(res)) { // トランザクション系のエラーだったらリトライ res = Space.AddAttachment(fileInfo).Result; } return GetResult(res); } /// <summary> /// トランザクション系のエラーで失敗しているかチェック /// </summary> public bool CheckIsRetry<T>(BacklogResponse<T> res) { return !res.IsSuccess && res.Errors.Any(x => x.Message.StartsWith("Deadlock")); } /// <summary> /// レスポンスの結果を取得 /// </summary> public T GetResult<T>(BacklogResponse<T> res) where T : BacklogItem { if (res.IsSuccess) { return res.Content; } Debug.LogError(string.Join(", ", res.Errors.Select(x => x.Message))); return null; }NBacklogのテストコードに書かれていたものを参考に、トランザクション系のエラーで失敗した時は1度だけリトライするようにしておきます。
レスポンスのエラーチェック周りはチケット登録処理でも利用するので汎用的に使えるよう、メソッドに切り出してあります。
4. チケット登録処理
ここまで準備した内容を基にチケットを登録して完了です。
BugReportWizard.cs//-- チケット作成 EditorUtility.DisplayProgressBar("Backlog", "バグ報告チケットを追加しています...", 0.75f); // 優先度 string priorityName = priorityDropDown[priorityIndex]; var priority = ProjectData.Priorities.FirstOrDefault((x) => x.Name == priorityName); // バージョン string versionName = versionDropDown[versionIndex]; var version = ProjectData.Milestones.FirstOrDefault((x) => x.Name == versionName); // カテゴリ string categoryName = categoryPullDown[categoryIndex]; var category = ProjectData.Categories.FirstOrDefault((x) => x.Name == categoryName); // 担当者 string assigneeName = assigneePullDown[assigneeIndex]; var assignee = ProjectData.Users.FirstOrDefault((x) => x.Name == assigneeName); var ticket = new Ticket(ticketTitle, ticketType, priority); ticket.Description = desc; ticket.Versions = new[] { version }; ticket.Categories = new[] { category }; ticket.Assignee = assignee; if (attachments.Count > 0) { ticket.Attachments = attachments.ToArray(); } try { var result = m_BacklogAPI.AddTicket(ticket); if (result == null) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("エラー", "チケット作成に失敗しました", "OK"); return; } // サムネをを加える(後述) ... EditorUtility.ClearProgressBar(); if (EditorUtility.DisplayDialog("Backlog", " バグ報告が完了しました", "チケットを開く", "閉じる")) { m_BacklogAPI.OpenBacklogTicket(result); } } catch (Exception e) { EditorUtility.ClearProgressBar(); Debug.LogException(e); }プルダウンで選択したものはそのままだと文字列なので、適切なデータ型に直します(プロジェクト情報から取得し直す)。
配列で渡す必要があるもの(Backlog上で複数登録できる仕様になっているもの)については配列に直しておきます。
AddTicket()
の処理内容です。流れ自体は添付ファイルの仮アップロードと同じです。BacklogAPI.cs/// <summary> /// チケットを追加 /// </summary> public Ticket AddTicket(Ticket ticket) { var res = Project.AddTicketAsync(ticket).Result; if (CheckIsRetry(res)) { // トランザクション系のエラーだったらリトライ res = Project.AddTicketAsync(ticket).Result; } return GetResult(res); }最後に完了を知らせるダイアログでチケットページを開けるようにしておきます。
OpenBacklogTicket()
の中身は以下の通りです。BacklogAPI.cs/// <summary> /// Backlogのチケットページを開く /// </summary> public void OpenBacklogTicket(Ticket ticket) { Application.OpenURL($"https://{Space.Key}.{APIData.Domain}/view/{ticket.Key}"); }チケット登録のレスポンスで取得したTicketクラスにURLで使用されるキーが入っているので、これを使えばブラウザで開くためのURLが作成できます。
Ex. サムネイルの追加
ここまでの内容で最低限必要そうな要件は満たしていますが、添付されたスクリーンショットを見るのにチケットページの下までスクロールして添付ファイルの一覧から開く必要があってちょっと面倒です。
そこでチケットページの冒頭にスクリーンショットのサムネイルが表示されるようにしてみます。
チケット登録が完了した直後に以下の処理を追加します。
BugReportWizard.cs// サムネをを加える // [注意] カスタム属性があるプロジェクトの場合、カスタム属性の対応を入れないと更新に失敗する if (isCaptureScreenShot) { EditorUtility.DisplayProgressBar("Backlog", "チケットに情報を追加しています...", 0.9f); // 一度スペースに追加した添付ファイルは、実際にチケットが発行されると削除され新しくIDが振られるので、 // このタイミングでないと付け加えられない int thumbnailId = result.Attachments.First(x => x.Name.Equals(screenShotName)).Id; result.Description = $"#thumbnail({thumbnailId.ToString()})\n\n" + desc; result = m_BacklogAPI.UpdateTicket(result); if (result == null) { EditorUtility.ClearProgressBar(); EditorUtility.DisplayDialog("エラー", "情報の追加に失敗しました", "OK"); return; } }詳細文でサムネイルを表示するには、文中に
#thumbnail(添付ファイルID)
を加えます。先に詳細文に加えておかない理由として、先述した通りスペースに対して仮アップロードされた添付ファイルは削除されてしまうからです。
IDも新しいものが振られるので、チケットを登録したあと、新しく発行されたIDを使ってチケットを更新するという流れでしか実現できません。
添付ファイルが複数ある場合はどれがスクリーンショットなのか調べないといけないので、ファイルを保存する際に利用したファイル名をキャッシュしていたわけです。
チケットの更新処理は以下の通りです。これも流れは登録と同じです。
BacklogAPI.cs/// <summary> /// チケットの更新 /// </summary> public Ticket UpdateTicket(Ticket ticket) { var res = Project.UpdateTicketAsync(ticket).Result; if (CheckIsRetry(res)) { // トランザクション系のエラーだったらリトライ res = Project.UpdateTicketAsync(ticket).Result; } return GetResult(res); }更新を行う都合上、Backlogホーム画面に表示される更新一覧にも表示されてしまうというデメリットもあるので、ホーム画面をすっきりさせたい場合は導入するかどうか要検討です。
こちらを見れば分かる通り、記事中にサムネを加えなくてもホーム画面ではスクリーンショットのサムネが表示されます。
カスタム属性に注意
フリープランでは利用できませんが、Backlogでは既に用意されている項目に加えて独自に項目を追加できるカスタム属性という仕様があります。
カスタム属性の対応を入れないと更新に失敗してしまうので、使っている場合は注意して下さい。
僕は対応する気になれなかったのでBacklog側に設定していたカスタム属性を削除してしまいました(元々あまり使ってなかったのもあり)。
まとめ
1. 拡張案
今回は最低限これくらい入っていれば活用してもらえるかなというラインに留めたので、もっと使いやすくしたいならこんなのはどうでしょうというのを書いておきます。
バグ内容、再現方法にBacklogフォーマットを適用するボタン
ブラウザでチケットを編集する時はこういうボタンがあるので、これもあったら嬉しいですよね。太字とか箇条書きは使いたいこともありそうです。
スクリーンショットのペイント機能
枠で囲んだり、矢印を付けたいことってありますよね。それくらいの簡単な加工ができると便利そうです。
そこまでしたい場合は「OSのペイントツールで加工してBacklog上で直接アップロードしてね」でも問題ないかと思っているので今後も特に入れる予定はないです…。
動画撮影と添付
動画でないと状況が伝わりにくいバグもありますよね。実装するならNintendoSwitchについてる機能みたいに、直近の数十秒を動画ファイルにするとかでしょうか。
常に記録し続けるとエディタが重くなりそうなのでメニューから動画撮影のON/OFFが必要そうですね。
任意ファイル添付
Unityエディタ自体のバグを報告する機能には任意ファイルを添付する機能が付いていますね。
任意ファイルはBacklog上に直接アップロードで問題ないかとは思っていますが、実装は簡単なので入れても良いでしょう。(ただしフリープランの場合はスクリーンショットと合わせて1つまでなので注意)
2. 参考
- BacklogAPIとは
- NBacklog
- 【Unity】Unity 2018.3 で Project Settings ウィンドウに独自のメニューを追加する方法
- Editor拡張 - ScriptableWizardについて
3. 最後に
ツール製作コスト > ツールで削減できるコスト
になってしまってはダメですが、このバグ報告機能は比較的手軽に作れて(と言って良いのかどうかは怪しいですが)、報告する側のコストも、バグ調査/修正する側のコストも削減できるので効果が大きいのではないでしょうか。専属デバッガーがいない状況なら特に、Unityでプロダクト開発を行っているメンバーからも気軽にバグ報告できるようになるメリットは大きいでしょう。
ぜひ皆さんのプロジェクトに応じた形でカスタマイズしてご利用頂ければと思います!
- 投稿日:2019-11-25T00:59:37+09:00
【Unity】これ以上セーフエリア対応で消耗しないためのアセットを作った【AutoScreen】
AutoScreen(GitHub)
はじめに
Unityにおけるセーフエリアの対応に関しては
Screen.safeArea
というAPIがあるのですが、ビルドして実機で呼ばないと端末ごとの値が取れない=エディタ上ではレイアウトの確認ができないという辛さがあります。最近だとUnite Tokyo 2019でも紹介されたDevice Simulatorが記憶に新しいところですが、このパッケージはUnity2019.3からでないと使用できず、まだプレビュー版となっています。
- 新機能「Device Simulator」でモバイル開発のイテレーションを加速させよう(Unity公式ブログ)
- 【Unity】Device Simulatorでノッチとセーフエリアの対策(テラシュールブログ)
個人的な感覚ではUnityはたとえLTS版であってもある程度時間が経つまでは本番で使用するのが怖いので、自分が実際にDevice Simulatorを使えるようになるのは2020年の後半になる気がしています。
そんなに待ってられないよ!ということでUniSafeAreaAdjusterをありがたく使わせてもらってたのですが
- 解像度に合わせて端末を選択するのが面倒
- 特に複数の
GameObject
にSafeAreaAdjuster
コンポーネントをつけてるとき- デバイスのフレーム表示機能がほしい
- 手軽に対応端末を追加・拡張できるようにしたい
という気持ちがピークに達して自分でアセットを作ってしまったのでご紹介します。
便宜上「アセット」と書いてますがアセットストアに公開するのはけっこう面倒なのでGitHubでだけ公開しています。
↓↓↓↓↓↓↓
AutoScreen(GitHub)READMEがまだないのですが、そのうち本記事を元に英語で書くと思います。
機能
- モバイル実機上でセーフエリアに応じて
RectTransform
のアンカーを自動調整するコンポーネント
SafeArea
:セーフエリア用(適用したい方向を上下左右自由に組み合わせ可)UnsafeArea
:非セーフエリア用(上下左右どれか選択)RuntimeSafeAreaUpdater
:画面の回転を自動検知してSafeArea
,UnsafeArea
を更新- エディタ上でセーフエリアをリアルタイムプレビュー
- 既存のGameウィンドウでセーフエリアあり端末の解像度を選択するだけ(追加作業なし)
- 再生/非再生の状態に関係なく即時反映
- on/off可能なオプション
- デバイスのフレームを表示
- セーフエリアの境界線表示
対応状況
- Unity2018.3以降対応(Device Simulatorは未対応)
- iOS:実機・エディタともに対応
- iPhone X/XS/11 Pro
- iPhone XS Max/11 Pro Max
- iPhone XR
- iPad Pro (第3世代, 11インチ)
- iPad Pro (第3世代, 12.9インチ)
- Android:実機のみ対応
Android端末のエディタプレビューは以下の理由でオミットしています。
- 個人的にAndroid端末にビルドする必要性がない
- フレーム画像と解像度・セーフエリアデータの収集に手間がかかる
- 対応が必要な端末数が多そう
端末のマスターデータの追加自体は簡単なので、必要に応じて後述の手順を参考に自分で足してください。
インストール方法
リポジトリ内に
*.unitypackage
ファイルがあるのでこちらを使用してください。使い方
とりあえず動作を確認してみたい場合は
Demo
シーンを用意してあるのでいじってみたりビルドしてみてください。セーフエリアの自動調整機能
Canvas
直下のGameObject
にSafeArea
/UnsafeArea
コンポーネントをAddComponent
するとRectTransform
のAnchor
が自動で調整されます。直下じゃなくてもCanvas
に至るまでの親GameObject
のRectTransform
がすべて縦横に完全にストレッチするようにしてあれば正常に動作します。
親GameObject
が存在する場合のRectTransform
の設定デバイスのフレーム表示/セーフエリアの範囲表示
セーフエリアありの端末解像度を選択するとGameウィンドウの左上に歯車アイコンが表示されるので、そこから表示・非表示を切り替え可能です。
こだわった点
基本的には以下の3点を追求しました。
- uGUIのオートレイアウトのような簡単で自然な使い心地
- 使ってて細かい挙動を含めイライラしない
- シンプルかつ高い拡張性
リアルタイムプレビュー
- エディタ上で
- 既存のGameウィンドウの解像度選択を変更するだけで
- リアルタイムに
- 再生/非再生の状態を問わず
- セーフエリアあり端末での見た目が自動で調整
されるので、ビルドしなくてもセーフエリアありの場合のレイアウトを手軽に確認できます。
設定変更がGameウィンドウから可能
通常アセットやエディタの拡張の設定は
[MenuItem]
を使用してグローバルメニュー(+ショートカットキー)から行えるようにするのですが、Gameウィンドウ上に設定のUIを配置することでon/off切り替えの煩雑さを軽減しました。ちなみにUIの配置にはUIElementsを使用しています。
高い拡張性
端末解像度とセーフエリアの情報は
ScriptableObject
を継承したアセットとして保持しているので、追加・拡張が簡単にできます。
SafeArea
,UnsafeArea
コンポーネントで満たせない複雑な要件の場合も、SafeAreaBase
クラスを継承することでエディタ上でのリアルタイムプレビューの機能が簡単に実装できます。Gitフレンドリー
セーフエリアの対応は
RectTransform
のアンカーを自動調整することで実現していますが、シーンやプレハブの保存前に必ずアンカーをリセットしています。これにより「Gameウィンドウで異なる解像度を選択して保存 → Gitでdiffが出る」ということが起きません。
実装の詳細
Gameウィンドウの情報を利用するにあたり、Gameウィンドウの実態である
GameView
クラスが公開されていないため、リフレクションを用いてそのデータにアクセスしています。実際にリフレクションを用いているのは
GameViewProxy
クラスのみで、値の変更についてはGameViewEvent
クラスが監視・イベント化しています。実機のフレーム表示は前もって用意したフレーム画像を描画していて、セーフエリアの大きさは事前にシミュレータビルドで収集した値を
ScriptableObject
を継承したGameViewScreen
アセットに保存・使用しています。デバイスのフレーム表示とセーフエリアの境界線表示はそれぞれ
DeviceFrameDrawer
コンポーネントとSafeAreaDrawer
コンポーネントが行っていて、それらのコンポーネントがアタッチされたAutoScreenManager
プレハブが自動的にHierarchyに配置されるようになっています。このプレハブインスタンスはシーンやビルドには含まれず、Hierarchyにも表示されません。対応端末追加の手順
エディタ上でのプレビューはマスターデータを追加するだけで簡単に対応機種を増やすことが可能です。
- Gameウィンドウから手作業で解像度を追加します。自動化したい場合はGameViewSizeHelperを使うと良いです。
- Projectウィンドウで右クリックし、
Create
->ScriptableObjects
->GameViewScreen
からマスターデータを保持するためのアセットを作成します。- 2.で作成したアセットの各値をInspectorウィンドウから設定します。
- 解像度やセーフエリアの値はDemoシーンをシミュレータや実機にビルドして確認すると楽です。
Base Text
は1.で追加した解像度のLabel
と同じ文字列にしてください。- 必要であればデバイスのフレーム画像を追加し、アセットの
Frame
にセットしてください。- 解像度を適当に変更すると反映されます。Unityを再起動する必要はありません。
Tips
- 自動で追加される
RuntimeSafeAreaUpdater
コンポーネントは画面の回転を許可していないアプリの場合は不要- 11インチのiPadはGameウィンドウから解像度を登録するとプレビュー可能
- デフォルトだとUnityに解像度が登録されてない
- ラベル名は「iPadPro 2388x1668 Landscape」「iPadPro 2388x1668 Portrait」で登録する
- 12インチのiPadの解像度を選択するとセーフエリアあり(第3世代)としてプレビュー表示される
- 第1〜2世代のセーフエリアなしレイアウトを確認したい場合は適当なラベル名で別途解像度を追加すればOK
- セーフエリア境界線の太さ・色は
AutoScreenManager
プレハブから調整可能SafeAreaDrawer
コンポーネントは単体で実機でも動作可能ライセンス
MIT
参考
- GameView.cs(UnityCsReference)
- GameViewSizes.cs(UnityCsReference)
- UniSafeAreaAdjuster(GitHub)
- NotchSolution(GitHub)
最後に
やりたいことや細かい挙動の調整を全部やろうとしたら結果的に
- 新しいプレハブAPIの使い方
- UIElements
[ExecuteAlways]
HideFlags
等々Unityのよく知らなかった様々な機能について詳しく知る良い機会になりました。個別の知見についてはアドベントカレンダーのときにでもまとめたいと思います。
コードにコメント書かないマンなのですがコード自体はシンプルで読みやすいと思うので、わからないことがあったらとりあえずコードを読んでみてください。
実は実践未投入なので、何か不具合があればPRかTwitterへ → @su10_dev