20191125のUnityに関する記事は14件です。

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つずつして、最後にスクリプトにまとめます

やったこと

  1. docker pull gableroux/unity3d
  2. Dockerコンテナに入りライセンスファイルを用意
  3. ライセンスファイルをコピー
  4. ビルド用クラスを作成
  5. Unityプロジェクトをビルド
  6. build.gradleとAndroidManifest.xmlを編集
  7. GradleでAndroidプロジェクトをビルド
  8. 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イメージをpull
docker pull gableroux/unity3d

しかし、このイメージではAndroidプロジェクトを生成/ビルドができないので、UnityバージョンとUnityプロジェクトのビルドターゲットを指定しなければなりません。

この記事では、 Unity 2018.4.5f1 でビルドターゲットをAndroid とするので、Dockerイメージのtagに2018.4.5f1-androidを指定します
※目的のバージョンのイメージが無い場合があるので、 DockerHub のページで確認しておきましょう

目的のDockerイメージをpull
docker 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内のターミナルに接続できる
なので、接続してUnityAndroid 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.ulfUnity_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.ulf

4. ビルド用クラスを作成してAssets/Scripts/Editorに配置

Unityプロジェクトをコマンドラインからビルドできるように次のC#ソースをAssets/Scripts/Editorに設置します

ApplicationBuild.cs
using 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.log
Exiting 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.gradleAndroidManifest.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#のでのUTF8EncodingUTF-8 with BOMであるので注意が必要。 UTF-8 with BOMbuild.gradlegradleに渡すとエラーになるので注意
※UnityのPostProcessBuildにはこの記事ではふれません
build.gradlebundleのコメントアウトだが このページ によれば次に示す置換でも問題ない

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 bundleDebugAar
AARファイルを生成
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.ulf

8. 作業をシェルスクリプトにまとめる

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 bundleDebugAar

Unityプロジェクトのルートディレクトリに移動して、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 PipelineCircleCIGitHub 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

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

UnityからkintoneのREST APIを叩く方法

はじめに

Unityからウェブデータベースにアクセスしたい!というシチュエーション、ありますよね。

UIが整っているウェブデータベースは非エンジニアでもデータの管理が可能なため、エンジニアと非エンジニアが同じプロジェクトに関わっている場合はとても便利です。

この記事では非エンジニアでもデータの入力や編集がしやすかったり、データベースの構造が容易に変更できるkintoneにUnity内からアクセスする方法を紹介します。

example.png

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内のデータにアクセスすることができます。

設定方法

設定方法は簡単です:

  1. SimpleJSONのコードをCommunity Wikiからコピーし、SimpleJSON.cs という名前で保存します
  2. Unityプロジェクト内の『assets』フォルダ内の『plugins』フォルダ(無かったら作る)にSimpleJSON.csを配置します

simplejsoninfolder.png
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.cs
using 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);
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

自動で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);
            }
        }
    }    
}

実行時の結果

playmode.gif

この機能を利用すれば他でも応用できそうです。

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

Unityのプロジェクトをバックアップする方法

目的

Unityでプロジェクトをバックアップする方法を以下の3つにまとめてみました。

  1. Collaborateを使う方法
  2. GitHubを使う方法
  3. フォルダごとバックアップする方法

環境

Unity Hub 2.1.3
Unity2018.4.11f1
Mac 10.15.1

1. Collaborateを使う方法

Unityの機能にCollaborateという機能があります。
手軽にプロジェクトをクラウドに保存できるという優れものです。
Collaborateの設定方法と使い方を説明します。

Collaborateでできること

  • バージョン管理
  • プロジェクトの共有

GitHubのUnity版だと思ってください。

設定方法

1. 対象プロジェクトとUnityアカウントを紐づける

プロジェクトを開いてServicesタブにて自分のアカウント名を選択してCreateボタンを押します。
スクリーンショット 2019-11-25 16.56.49.png

Servicesタブが表示されていない場合
ツールバーのWindow>General>Services

2. Collaborateを有効化

CollaborateのOFFONに切り替えます
スクリーンショット 2019-11-25 09.29.32.png

Create together seamlessly を有効にします
スクリーンショット 2019-11-25 09.29.53.png

Collaborateのメニューが表示されれば完了です。
左上のCollabアイコンも有効化さたのでクリックします。
スクリーンショット 2019-11-25 09.30.24.png

3. プロジェクトをアップロードする

アップロードする際のコメントを入力してPublish now!をクリックします。

スクリーンショット 2019-11-25 09.34.33.png

アップロードされるまでしばらく待ちます。
スクリーンショット 2019-11-25 09.35.18.png

アップロード完了したら、左下のアイコンをクリックします。
スクリーンショット 2019-11-25 09.37.58.png

アップロードされたバージョン履歴が確認できます。
スクリーンショット 2019-11-25 09.55.46.png

一番下の履歴はプロジェクトを作成した最初の状態なので誤ってGo back to...をクリックしてしまうと、プロジェクトを作成した初期状態に戻ってしまうので注意が必要です。

使い方

バージョンの戻し方

今後はプロジェクトに変更を加える度にCollaborateにアップロードすることで、新しいバージョン履歴が増えます。
1つ前のバージョンに戻したい場合は、Restoreボタンを押すことでそのバージョンまで戻すことができます。

プロジェクトの共有方法

人型のアイコンをクリックするとCollaborateのダッシュボードへ移動します。
スクリーンショット 2019-11-25 17.24.52.png

Add a person or group 欄へ共有したUnityID(メールアドレス)を入力してAddボタンでプロジェクトのチームメンバーに招待することができます。
作ったプロジェクトを見てもらいたい場合や、共同で開発する際に便利ですね。
スクリーンショット 2019-11-25 17.27.44.png

Collaborateまとめ

CollaborateはUnityに付属しているだけあって、煩わしい設定がなく初心者でも簡単に設定できるところがメリットですね。
ただし、無料版(デフォルト状態)だと容量が1GBまでと少々容量不足がデメリットです。
勉強がてらUnityを使う分には問題ありませんが、本格的なアプリ作成となると有料も視野に入れた方がよさそうですね。

2. GitHubを使う方法

バージョン管理といえばGitHubですが、UnityのアセットストアにUnity版GitHubがあります。
GitHubをUnityだけで使いたい場合は、手頃にGitHubが使えて便利なツールだと思います。

設定方法

アセットストアからインポートする

Assets Store にて「GitHub for Unity」をダウンロードしてプロジェクトにインポートします。
スクリーンショット 2019-11-25 10.21.12.png

スクリーンショット 2019-11-25 10.21.51.png

スクリーンショット 2019-11-25 10.22.06.png

GitHubウインドウを表示

ツールバーからWindow>GitHubでGitHubウインドウが表示されます。
Initialize a git repository for this projectボタンをクリックすると自動的に初回分のコミットがされます。
スクリーンショット 2019-11-25 10.28.10.png
スクリーンショット 2019-11-25 10.28.42.png
スクリーンショット 2019-11-25 10.29.37.png

Gitへコミットする

Changesタブを選択するとコミットするフォルダを選択する画面になります。
対象フォルダにチェックを入れて、「Commit summary」と「Commit description」にそれぞれコミットの概要と詳細説明を入力後にCommit to [master]ボタンをクリックします。

スクリーンショット 2019-11-25 10.36.34.png
スクリーンショット 2019-11-25 10.38.25.png

GitHubへPushする

GitHubへサインインする

GitHubウインドウの右上にあるSigninをクリックするとサインイン画面が表示されますので、アカウントを入力してSign inボタンをクリックします。
スクリーンショット 2019-11-25 10.39.43.png

GitHubのアカウントはあらかじめ作成する必要があります
https://github.com/

GitHubでリポジトリ作成

GitHubの管理画面の右上プラスアイコンからNew repositoryでリポジトリ名を決めてリポジトリを作成します。
スクリーンショット 2019-11-25 10.45.24.png

リポジトリのSSHをコピーします。
スクリーンショット 2019-11-25 10.56.26.png

Unity側のSettingへ情報をセット

Git ConfigurationへGitHubアカウント情報をセットします。
Repository Configurationへ先ほどのSSHをセットします。
スクリーンショット 2019-11-25 10.57.14.png

Pushする

PushボタンをクリックしてGitHubリポジトリへプロジェクトをPushします。
スクリーンショット 2019-11-25 10.55.14.png

Pushされたことを確認

BranchesタブにてRemoteBranchesにmasterブランチがPushされてます。
スクリーンショット 2019-11-25 10.59.21.png

GitHubのリポジトリページにもデータがアップロードされていることが確認できます。
スクリーンショット 2019-11-25 10.58.41.png

GitHubまとめ

GitHubをまったく使ったことない場合は少々敷居が高いかもしれませんが、GitHubを使いこなせるようになると、他言語の開発でも便利です。
本来GitHubはCLIですが、GUIベースで操作できるところもUnityらしく、初心者でも扱いやすいですね。

3. フォルダごとバックアップする方法

最後にアナログ的な方法になりますが、プロジェクトフォルダごと外付けHDDなどにバックアップする方法です。
Unityはプロジェクトごとに1つのフォルダでまとめられ独立しているのでデータ移行は比較的簡単ですね。

総合まとめ

Unityでプロジェクトをバックアップできる方法をまとめてみました。
最後にそれぞれのメリット、デメリットをまとめておきますので参考になれば幸いです。

1. Collaborate

メリット

  • Unity付属機能で設定が楽
  • バージョン管理ができる

デメリット

  • 無料で使える容量が1GBと少々物足りない

2. GitHub for Unity

メリット

  • GitHubという最強ツールを使えること
  • バージョン管理ができる

デメリット

  • GitHubについての知識がないと設定がやや煩雑に感じる

3. フォルダごとバックアップする方法

メリット

  • 特別な知識は必要としない

デメリット

  • バージョン管理できない
  • バックアップ作業は手作業
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityTestRunner向けTest属性を自作してみる

前書き

アドカレ4日目です。
UnityTestRunnerについてちょっと書いてみます。

テスト、いいですよね。たくさんテストを書くと仕事が進んでいるような気がしてきます。
僕はそんなにテスト書かない人間ですけど、テストを書くのが嫌いでは無いです。
最近、UnityTestRunnerとその裏側であるNUnitに触れていたら、TestAttributeの仕組みが気になって、自作のAttributeとかを噛ませてみたくなったので、試してみました。

シンプルな自作TestAttribute

まずはじめに [SimpleTest] public void テストメソッド() { } こんな感じでTestRunnerに認識されるAttributeを自作してみます。

実装と使用方法

TestAttribute定義

SimpleTestAttribute.cs
using 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.cs
using NUnit.Framework;

public class SimpleTestSample
{
    [SimpleTest] public void Test() { }
    [SimpleTest] public void TestFail() => Assert.Fail();
}

上記のテストケースであれば、UnityTestRunner上では次のスクリーンショットのように表示されます。
スクリーンショット 2019-11-25 午後4.49.33.png

仕組み

UnityTestRunnerに認識させるためにやったことはAttributeの定義(とテストメソッドへの付与)だけです。

継承元について

NUnit.Framework.NUnitAttributeを継承していますが、これは System.Attribute を直接継承してもUnityTestRunner上では動作に違いはありませんでしたが、その場の雰囲気でNUnitAttributeを採用しました。
他にもNUnit標準のAttributeはいくつかあるのでNUnit内のAttributeを眺めるのもいいかもしれません。
例えば、UnityTestAttributeなんかはCombiningStrategyAttributeを継承していたりします。

ISimpleTestBuilderについて

NUnit.Framework.Interfaces.ISimpleTestBuilder は次のように定義されています。

ISimpleTestBuilder.cs
public 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.cs
using 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で実行実行した結果です。
スクリーンショット 2019-11-25 午後4.56.49.png

仕組み

NUnitのIMethodInfoを実装してMethodProxyクラスを定義し、BuildFromに渡ってくるIMethodInfoのプロキシとしてInvokeに割り込んでいます。
なんとなくActionを渡していますが、直接Invokeメソッド内に書いちゃっても問題ないですね。

なお、今回のBuildFromメソッド実装では、色々実装が足りないのでValuesやRange属性と組み合わせることができませんが、そこはご愛嬌ということで。


シーンを指定してTestを実行する属性の実装(その2)

前述した「シーンを指定してTestを実行する属性の実装(その1)」の場合、色々な属性と組み合わせるとボロが出始めました。
そこで、もう少しボロが出にくいUnityTestRunnerライクな実装をしてみます。
使い方は一緒なので割愛します。

実装

UnityTestSceneAttribute.cs
using 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に実装してあげるだけです。
テスト属性に密接に関わる処理は UnitySetUpUnityTearDown よりこちらのインターフェースを使った方が簡単なケースもあるかもしれません。


TestCaseSourceのUnityTest版

NUnitには TestCaseSourceAttribute というものがいて、このAttributeにイテレータを返すメンバー名を渡してあげると1つのテストから複数のテストケースを量産することができます。
TestAttribute に対するUnityTestRunnerの UnityTestAttribute のように、 TestCaseSourceAttribute に対して UnityTestCaseAttribute があるかなーっと思ったのですが、現状見つからなかったので自作してみました。
以下に実装を残します。

実装と使用方法

TestAttribute定義

UnityTestCaseSourceAttribute.cs
using 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.cs
using 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で実行実行した結果です。
UnityTestCaseSourceAttribute

仕組み

TestCaseSourceAttributeを継承して、ITestBuilderを実装し直してみました。
TestCaseSourceAttributeにはvirtualなメソッドなんて存在しないので、BuildFromメソッドをnewして隠蔽し、GetTestCasesForメソッドをリフレクションでこじ開けるという、気合いと根性に満ちた実装になっています。
TestCaseSourceAttributeを写経するのも選択肢としてはありですが、少々複雑だったので横着してみました。
ぱっと見はそこそこ素直な実装に見えるんじゃないでしょうか。


アスキーアートだって

結構なんでもできるので、

[AATest]
public void AA表示したい() { }

たったこれだけのテストから
スクリーンショット 2019-12-02 午後6.32.54.png

こんな感じでアスキーアートを出すことだって自由です。

それではみなさん楽しいテストライフを!

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

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
};

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

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();
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】円形状の移動制限のかけ方

Script

test
    public 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;
        }
    }

どんな場合に使うの?

自分は自作バーチャルパッドを作った時に使いました

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

【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;
        }
    }

まとめ

急いで書いたとはいえ汎用性がなさそうなものができてしまった…

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

【Unity】Gameビューにグリッドを表示

やりたいこと

こんな感じにGameビューにグリッドを表示したい。
e0e9c35659dcfca165114835e3ab45c7.png

サンプルコード

test
using 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

test
        triangles = 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の場合。
アートボード 1.png

この赤の線のところが描画されなくなってしまいます。

コード解説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++;
        }

ここでは頂点座標を決めています。

頂点を下の図のように配置すると四角形が形成されます。

Mesh線.png

あとはこの四角形のx,yの長さを決めてあげれば線のように描画できます。

完成

0c1ee38dc580dd930f19c6b853c75840.gif

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

【Unity】なぜGetComponentを毎フレーム取得することが推奨されていないのか

毎フレームGetComponent<>()

よくGetComponentは毎フレームやることはよろしくないという話を聞きますがなぜダメなのでしょうか?

試してみよう!

今回は二つのScriptを用意しました

片方は先にComponent取得してから10万回Transform.positionにvector(0,0,0)を入れる

C#test.cs
  private 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.cs
    private void Start()
    {
        for (int i = 0; i < 100000; i++)
        {
            GetComponent<Transform>().position = new Vector3(0,0,0);
        }
    }

結果

8f265ca3adca739ea0423092d4e3da10.png
2倍近く差が出てるんですけど…
今回は10万回で検証しましたがこれが毎フレーム実行されたとしても2倍近く処理が無駄ってことですね。
仕様上仕方がない理由以外ではUpdateでGetComponentをするこはよろしくなさそうですね。

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

【Unity】3D座標を2D座標に直す方法

3D座標を2D座標に変換

オブジェクトのxとyの値だけを使えばそれは2Dの座標なのでは?と最初自分は思っていましたがCameraの位置や角度を変えるとうまくいかなくなるのでそれらに対応したものを作っていきましょう。

Script公開

test.cs
    public 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座標とくに考慮してませんが)

終わり

なにか質問やここ間違ってるなどありましたらコメントください。

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

Unityエディタでテストプレイ中にバグ報告する機能を作る

この記事は【unityプロ技】 Advent Calendar 2019の11日目の記事です。

はじめに

こんにちは、天神いな(@ina_amagami)です。スマートフォンゲーム会社でUnityを使用したゲーム開発をしつつ、2019年現在は開発チームのリードエンジニアとしてプロジェクト効率化業務なども行っています。

個人でアマガミナブログという技術ブログを書いていますが、Qiita初投稿です。よろしくお願いします!

チーム開発をしていると、Unityエディタでテストプレイ中にバグを見つけることが多々ありますよね。
バグだ!と思った時にすぐ担当者に修正依頼を投げられると便利です。

そこで今回はUnityエディタからBacklogにバグ報告チケットを追加する機能を作ったので、実装方法含めて紹介します。

Redmineなど他のバグ管理ツールを使用する場合でも、考え方はそのまま使えるはずです。

1. Backlogとは

https://backlog.com/ja/

nurab社によって運営されているタスク管理サービスです。Backlogではタスクの単位を「課題」と呼び、プロダクトのバグ管理にも利用することが可能です。(プログラム上はTicketでややこしいので課題のことを記事内ではチケットと呼びます)

今回は「1プロジェクト10人まで」といった制約が付いているフリープランで利用できる機能の範囲内を想定しています。

制限例として、チケットに添付できるファイル数は1つまでです。そのため今回はGameビューのスクリーンショット添付のみ作成しますが、有料プランをお使いの方は他のファイルを追加で添付するのも良いでしょう。

例えば僕が開発チームに導入しているものはログファイルを添付しています。

2. 作ったもの

こんな感じのウィンドウを開いてバグ報告します。

スクリーンショット 2019-12-06 13.27.27.png

チケットの作成が完了したら、すぐにブラウザで確認できます。

スクリーンショット 2019-12-04 10.16.09.png

Backlog上にはこんな感じで登録されます。

スクリーンショット 2019-12-04 10.21.08.png

スクリーンショット 2019-12-04 10.23.16.png

ツール化しておくことで、報告時の詳細文フォーマットを固定する効果もあります。

BacklogにはSlackやChatworkへの通知設定もあるので、併せて設定しておくと捗ります。

3. ダウンロード

個人開発用にフリープランで使うため作り直したものをGitHubにて公開中です。あくまで作例なので、ご利用する環境に応じてカスタマイズして頂ければと思います。

unity-bugreporter-example

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/内のNBacklogunity-backlogにはAssembly Definitionを設定しています。これらが原因でうまく動作しない場合は.asmdefファイルを削除するか、以下を参考にして下さい。

参考:Unity Assembly Definition 完全に理解した

1. NBacklogの導入

BacklogAPIを叩くためのライブラリはBacklogAPIのマニュアルにまとまっています。

C#向けの公式ライブラリはありませんが、hal1932氏が作成したNBacklogがGitHubで公開されているので、こちらをプロジェクト内に導入します。

NBacklog

2. NuGet / Newtonsoft.Jsonの導入

NBacklogを導入するとNewtonsoft.Jsonが無いためエラーが出ます。

今回はNuGetForUnityを使ってインストールします。

NuGetForUnityをプロジェクトに追加したら、メニューから

NuGet > Manage NuGet Packages を開きます。

json-net.png

リストからJson.NETをインストールします。

NuGetを導入することでAssets/直下にファイルが色々と増えてしまうので、これが気になる方はNewtonsoft.Jsonのdllを直接プロジェクトに導入して下さい。

3. NBacklogのコードを一部修正

NBacklog/Rest/RestClient.csを修正します。

Unityに入っているバージョンのHttpClientではMaxConnectionsPerServerに対応していないため、こちらを以下のように書き換えます

RestClient.cs
public 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.DrawingSystem.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.configpackages.configというファイルが作成されているので、これらのInspectorから設定を変更します。

スクリーンショット 2019-12-09 13.51.00.png

Include PlatformsのEditorだけチェックを入れてApplyして下さい。

5. Backlogアプリケーション登録

NBacklogを利用したBacklogAPIの認証にはOAuth2を利用します。

通信処理を用意すればAPIKeyを利用することも可能ですが、チケットの登録者がAPIKeyを発行した人になってしまうことで誰からのバグ報告なのか分からなくなる問題もあり、OAuth2を推奨します。

認証に利用するclient_idclient_secretを取得するにはBacklogアプリケーション登録のページからログインしてアプリケーション登録を行います。

新規登録ボタンをクリックして登録画面に入ります。

スクリーンショット 2019-11-29 9.14.34.png

5-1. RedirectURL

スクリーンショット 2019-11-29 10.23.45.png

今回はWebサイトからの認証ではないので特に使いません。指定は必要なのでhttp://localhost:12345とかhttp://localhost:54321とか適当なURIでOKです。

5-2. 認証画面に表示される情報を設定

デフォルトはEnglishになっています。そのままでもいいですが、今回は日本語に変更しておきます。

スクリーンショット 2019-11-29 10.20.40.png

下までスクロールすると「この言語を削除する」というボタンがあるのでこれを押して削除します。

消えたら日本語で新しく追加します。追加できたら「この言語をデフォルトの表示にする」にチェックを入れて下さい。このチェックを付けないと登録に失敗します。

スクリーンショット 2019-11-29 10.21.03.png

アプリケーション名とアプリケーションの説明を入力します。今回はバグ報告にしか利用しないので説明は「バグ報告する機能です」としていますが、同一の認証で他の機能も使えるようにする場合はそれも書き加えておきます。

スクリーンショット 2019-11-29 10.21.51.png

サイトURLは不要です。これで登録するとclient_idclient_secretが発行されます。

スクリーンショット 2019-12-01 16.58.21.png

以上で準備は完了です。

BacklogAPI認証

NBacklogをエディタ拡張で扱いやすいようにラップしたものを用意しました。
※今回のバグ報告ツールに必要な機能以外は特に用意していませんのでご了承下さい。

unity-backlog

こちらはMITライセンスで公開していますが、記事内に抜粋したコードはパブリックドメインです。

重要なポイントを解説していきます。

1. 設定ファイルの用意

プロジェクト毎に変更しやすいように設定ファイルをScriptableObjectで作成します。

BacklogAPIData.cs
using 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.cs
using 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最高!

スクリーンショット 2019-12-02 9.16.40.png

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認証が通って、プロジェクト情報が正しくロードできるかテストします。バグ報告を実行する時も同じですが、認証時はブラウザが起動します。

スクリーンショット 2019-12-02 8.30.38.png

「許可する」を押したらリダイレクト先のページ(今回の例ではhttp://localhost:12345)が開きますが、必要ないので閉じます。

Unity側に戻ると認証成功のダイアログが出ます。

一度認証に成功するとキャッシュファイルが作られるので、以降ブラウザは起動しません。

2-4. 設定項目

ProjectSettingsではこのように表示されます。

スクリーンショット 2019-12-02 9.09.48.png

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 KeyamagaminaDomainbacklog.comProject KeyBUG_REP です。

プロジェクト情報を間違えてしまうと全然関係ないプロジェクトに対してバグ報告してしまうことになるので注意して下さい。
また、認証でログインしたアカウントが対象プロジェクトに参加していない人であればバグ報告に失敗します。

認証情報のキャッシュファイル名(パス)

このキャッシュファイルはプロジェクトのルート(Assetsフォルダと同じ階層)を基準にしたパスで作成されます。

git管理している場合であれば共有されないようignoreに追加するか、既にignore対象になっているパスに書き換えて下さい。

2-Ex. レイアウト調整

このままでも問題ないですが几帳面な僕はBacklogAPIの設定項目の中身が他の設定に比べて左側に寄っているのが気になるので、直しておきます。

Editor設定
スクリーンショット 2019-12-07 12.19.30.png

BacklogAPI設定
スクリーンショット 2019-12-07 20.45.46.png

他の設定がやっていることと同じことをやればいいよね。と思ってUnityCsReferenceを漁ってみたらSettingsWindow.csが機能を持っていることが分かったのですが、internal…。

仕様変更になる可能性もあるので、やり方だけ参考にして自前で用意することにしました。

HorizontalScopeVerticalScopeと同じような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.cs
using (new GUIScope())
{
    // プロパティの表示
    ...
}

これで余白ができました。

スクリーンショット 2019-12-07 21.04.15.png

3. BacklogAPI認証処理

BacklogAPI.cs
using 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にしないと同じになりませんでした。原因はよくわからないです…。

スクリーンショット 2019-12-07 21.15.23.png

発生バージョンやカテゴリはBacklog上のプロジェクト設定で作成しておきます。

発生バージョンについてはバージョンが上がる度にこの設定も更新しないと報告者が古いバージョンを指定したまま報告するミスが発生する可能性もあります。

そのため、もしBacklog上で設定している発生バージョンとUnityのPlayerSettingsで設定しているバージョンが同じで問題ないなら同期させておくのがいいでしょう。

バグ報告ウィザードの作成

それではバグ報告機能の作成に入ります。今回はScriptableWizardを使用します。

1. ScriptableWizardって何?

ちょっとした設定を入れてアセットを作成したりとか複雑なウィンドウが必要ない時にサクっと使えるウィンドウです。個人的には気に入っていてよく使っています。

設定機能はSettingsProviderを使用しましたが、直近だとUnity2018.4でバージョンを止めるプロジェクトも多いと思うのでここにはUIElementsは使っていません。

2. ウィザードの生成

まずはUnityのメニューにBacklog/バグ報告の項目を追加して、クリックしたらウィザードが生成されるようにします。

ポイントは以下の4つです。

  1. プレイ中のみ開けるようにする
  2. BacklogAPIの認証&プロジェクト情報のロードが終わってからウィザードを開く
  3. ヘッダでスペース名とプロジェクト名が分かるようにする(違うプロジェクトに対してバグ報告しようとしてないか確認する意味も込めて)
  4. 項目がちゃんと収まるようにウィンドウの最小サイズを設定する
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.cs
private void OnWizardUpdate()
{
    isValid = !string.IsNullOrEmpty(ticketTitle);
}

チケットの登録

ウィザードの準備は終わりました。最後にバグ報告ボタンが押された後のチケット登録処理を見ていきます。

BugReportWizard.cs
private 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.cs
if (isSendLog)
{
    desc += "\n【ログ】\n";
    desc += LogRecorder.GetBacklogLogText();
}

直近で発生したログを記録しておくためのLogRecorderクラスを作成します。

LogRecorder.cs
using 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.cs
public 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独自フォーマットかをプロジェクトの基本設定で選択できるようになっています。

スクリーンショット 2019-12-06 8.39.28.png

ここがMarkdownになっている場合は上記の例ではうまくフォーマットできないので、Markdownに書き直すか設定を変更する必要があります。

3. スクリーンショットの添付

続いてスクリーンショットの添付です。

3-1. ファイル添付の方法

添付というとチケット登録の通信と同時に送るイメージを持つかもしれませんが、これは違います。

Backlogの添付ファイルは、まず最初に仮アップロードします。

するとレスポンスで添付ファイルに振られたIDなどの情報を取得できます(NBacklog上はAttachmentクラス)。

この情報をチケット登録する際に指定することで、仮アップロードしたファイルは削除され、チケット情報と一緒に添付ファイルとして登録されます。

3-2. スクリーンショット添付の手順

今回は以下のような手順でスクリーンショットを添付しています。

  1. Gameビューのキャプチャをjpgで保存
  2. Backlog上に仮アップロード
  3. 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ホーム画面に表示される更新一覧にも表示されてしまうというデメリットもあるので、ホーム画面をすっきりさせたい場合は導入するかどうか要検討です。

スクリーンショット 2019-12-06 10.23.11.png

こちらを見れば分かる通り、記事中にサムネを加えなくてもホーム画面ではスクリーンショットのサムネが表示されます。

カスタム属性に注意

フリープランでは利用できませんが、Backlogでは既に用意されている項目に加えて独自に項目を追加できるカスタム属性という仕様があります。

カスタム属性の対応を入れないと更新に失敗してしまうので、使っている場合は注意して下さい。

僕は対応する気になれなかったのでBacklog側に設定していたカスタム属性を削除してしまいました(元々あまり使ってなかったのもあり)。

参考:Backlogの課題追加のAPIで分かりにくい点

まとめ

1. 拡張案

今回は最低限これくらい入っていれば活用してもらえるかなというラインに留めたので、もっと使いやすくしたいならこんなのはどうでしょうというのを書いておきます。

バグ内容、再現方法にBacklogフォーマットを適用するボタン

ブラウザでチケットを編集する時はこういうボタンがあるので、これもあったら嬉しいですよね。太字とか箇条書きは使いたいこともありそうです。

スクリーンショット 2019-12-06 13.55.40.png

スクリーンショットのペイント機能

枠で囲んだり、矢印を付けたいことってありますよね。それくらいの簡単な加工ができると便利そうです。

そこまでしたい場合は「OSのペイントツールで加工してBacklog上で直接アップロードしてね」でも問題ないかと思っているので今後も特に入れる予定はないです…。

動画撮影と添付

動画でないと状況が伝わりにくいバグもありますよね。実装するならNintendoSwitchについてる機能みたいに、直近の数十秒を動画ファイルにするとかでしょうか。

常に記録し続けるとエディタが重くなりそうなのでメニューから動画撮影のON/OFFが必要そうですね。

任意ファイル添付

Unityエディタ自体のバグを報告する機能には任意ファイルを添付する機能が付いていますね。

任意ファイルはBacklog上に直接アップロードで問題ないかとは思っていますが、実装は簡単なので入れても良いでしょう。(ただしフリープランの場合はスクリーンショットと合わせて1つまでなので注意)

2. 参考

3. 最後に

ツール製作コスト > ツールで削減できるコストになってしまってはダメですが、このバグ報告機能は比較的手軽に作れて(と言って良いのかどうかは怪しいですが)、報告する側のコストも、バグ調査/修正する側のコストも削減できるので効果が大きいのではないでしょうか。

専属デバッガーがいない状況なら特に、Unityでプロダクト開発を行っているメンバーからも気軽にバグ報告できるようになるメリットは大きいでしょう。

ぜひ皆さんのプロジェクトに応じた形でカスタマイズしてご利用頂ければと思います!

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

【Unity】これ以上セーフエリア対応で消耗しないためのアセットを作った【AutoScreen】

AutoScreen(GitHub)

AutoScreen_01.gif
AutoScreen_02.gif

はじめに

Unityにおけるセーフエリアの対応に関してはScreen.safeAreaというAPIがあるのですが、ビルドして実機で呼ばないと端末ごとの値が取れない=エディタ上ではレイアウトの確認ができないという辛さがあります。

最近だとUnite Tokyo 2019でも紹介されたDevice Simulatorが記憶に新しいところですが、このパッケージはUnity2019.3からでないと使用できず、まだプレビュー版となっています。

個人的な感覚ではUnityはたとえLTS版であってもある程度時間が経つまでは本番で使用するのが怖いので、自分が実際にDevice Simulatorを使えるようになるのは2020年の後半になる気がしています。

そんなに待ってられないよ!ということでUniSafeAreaAdjusterをありがたく使わせてもらってたのですが

  • 解像度に合わせて端末を選択するのが面倒
    • 特に複数のGameObjectSafeAreaAdjusterコンポーネントをつけてるとき
  • デバイスのフレーム表示機能がほしい
  • 手軽に対応端末を追加・拡張できるようにしたい

という気持ちがピークに達して自分でアセットを作ってしまったのでご紹介します。

便宜上「アセット」と書いてますがアセットストアに公開するのはけっこう面倒なので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直下のGameObjectSafeArea/UnsafeAreaコンポーネントをAddComponentするとRectTransformAnchorが自動で調整されます。直下じゃなくてもCanvasに至るまでの親GameObjectRectTransformがすべて縦横に完全にストレッチするようにしてあれば正常に動作します。

スクリーンショット 2019-11-24 11.27.57.png
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にも表示されません。

対応端末追加の手順

エディタ上でのプレビューはマスターデータを追加するだけで簡単に対応機種を増やすことが可能です。

  1. Gameウィンドウから手作業で解像度を追加します。自動化したい場合はGameViewSizeHelperを使うと良いです。
  2. Projectウィンドウで右クリックし、Create -> ScriptableObjects -> GameViewScreenからマスターデータを保持するためのアセットを作成します。
  3. 2.で作成したアセットの各値をInspectorウィンドウから設定します。
    • 解像度やセーフエリアの値はDemoシーンをシミュレータや実機にビルドして確認すると楽です。
    • Base Textは1.で追加した解像度のLabelと同じ文字列にしてください。
    • 必要であればデバイスのフレーム画像を追加し、アセットのFrameにセットしてください。
  4. 解像度を適当に変更すると反映されます。Unityを再起動する必要はありません。

Tips

  • 自動で追加されるRuntimeSafeAreaUpdaterコンポーネントは画面の回転を許可していないアプリの場合は不要
  • 11インチのiPadはGameウィンドウから解像度を登録するとプレビュー可能
    • デフォルトだとUnityに解像度が登録されてない
    • ラベル名は「iPadPro 2388x1668 Landscape」「iPadPro 2388x1668 Portrait」で登録する
  • 12インチのiPadの解像度を選択するとセーフエリアあり(第3世代)としてプレビュー表示される
    • 第1〜2世代のセーフエリアなしレイアウトを確認したい場合は適当なラベル名で別途解像度を追加すればOK
  • セーフエリア境界線の太さ・色はAutoScreenManagerプレハブから調整可能
  • SafeAreaDrawerコンポーネントは単体で実機でも動作可能

ライセンス

MIT

参考

最後に

やりたいことや細かい挙動の調整を全部やろうとしたら結果的に

  • 新しいプレハブAPIの使い方
  • UIElements
  • [ExecuteAlways]
  • HideFlags

等々Unityのよく知らなかった様々な機能について詳しく知る良い機会になりました。個別の知見についてはアドベントカレンダーのときにでもまとめたいと思います。

コードにコメント書かないマンなのですがコード自体はシンプルで読みやすいと思うので、わからないことがあったらとりあえずコードを読んでみてください。

実は実践未投入なので、何か不具合があればPRかTwitterへ → @su10_dev

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