20191215のUnityに関する記事は11件です。

UnityとGo言語でAPI通信する

はじめに

ゲーム開発エンジン「Unity」と、Googleにより開発されたプログラミング言語「Go」を用いて、凄くシンプルなREST APIを実装したいと思います。

Unityについて:https://unity.com/ja
Goについて:https://golang.org/

開発環境

  • MacBook Pro (15-inch, 2018)
  • macOS Catalina
  • Unity 2019.1.1f1 Personal
  • Go 1.13.5 darwin/amd64

システム構造

Unityをクライアントサイド、APIサーバ(Goで実装)をサーバサイドとして作りました。

内容

サーバサイドで保存されているデータに対して、Unity側からデータのID(一意なもの)を指定して対象データを取得します。データに関しては、予めいくつか用意しておきます。

今回はDBを使わないので、オンメモリのデータストア(リストを使います)に格納しておこうと思います。なので、サーバを停止させるとデータは吹き飛びますw

実装

サーバサイド

まずサーバサイドから実装していきます。
といっても、Goには便利なパッケージがたくさんあり、かつネットには参考になる情報が大量にあるため、それらをめっちゃ活用しました。

コードは以下の通りです。

package main

import (
    "log"
    "net/http"
    "strconv"

    "github.com/ant0ine/go-json-rest/rest"
)

type Monster struct {
    ID   int
    Name string
}

// オンメモリのデータストア.
var dataStore = map[int]*Monster{}

func main() {
    api := rest.NewApi()
    api.Use(rest.DefaultDevStack...)

    router, err := rest.MakeRouter(
        rest.Get("/getAllData", GetAllData),
        rest.Post("/postData", PostData),
        rest.Get("/getData/:id", GetData),
    )
    if err != nil {
        log.Fatal(err)
    }
    api.SetApp(router)
    log.Printf("Server Started.")

    // APIサーバを起動.
    log.Fatal(http.ListenAndServe(":8080", api.MakeHandler()))
}

// データを新しく作成する.
func PostData(w rest.ResponseWriter, r *rest.Request) {
    monster := Monster{}
    err := r.DecodeJsonPayload(&monster)

    if err != nil {
        rest.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    dataStore[monster.ID] = &monster

    w.WriteJson(&monster)
}

// 指定IDのデータを取得する.
func GetData(w rest.ResponseWriter, r *rest.Request) {
    id, _ := strconv.Atoi(r.PathParam("id"))

    var monster *Monster
    if dataStore[id] != nil {
        monster = &Monster{}
        *monster = *dataStore[id]
    }

    if monster == nil {
        rest.NotFound(w, r)
        return
    }

    w.WriteJson(monster)
}

// 全データを取得する.
func GetAllData(w rest.ResponseWriter, r *rest.Request) {
    allData := make([]Monster, len(dataStore))

    i := 0
    for _, data := range dataStore {
        allData[i] = *data
        i++
    }
    w.WriteJson(&allData)
}

こちらのコードを作成するために、以下の資料を大変参考にさせていただきました!
golangでREST APIをやってみた①

今回は、簡単にRESTfulなAPIサーバを構築することができる以下のパッケージを使っています。
Go-Json-Rest

コードに関して大事なところは、以下の箇所かと思います。

    router, err := rest.MakeRouter(
        rest.Get("/getAllData", GetAllData),
        rest.Post("/postData", PostData),
        rest.Get("/getData/:id", GetData),
    )

ここでは、go-json-restのrestパッケージに実装されているMakeRouterを使って、ルーティングパスを3つ設定しています。それぞれ以下のハンドラと結びつきます。

  • /getAllData
    • GetAllData : データストアに格納されている全データを取得するハンドラ.
  • /postData
    • PostData : データストアに新しくデータを格納するハンドラ.
  • /getData/:id
    • GetData : 指定idを持つデータを取得するハンドラ.

今後新しくルーティングを作成していきたい場合は、MakeRouterにHTTPリクエストメソッドに対して、ルーティングパスとハンドラを結び付けて定義してあげればいいということですね。

クライアントサイド

サーバサイドに対して処理を要求するクライアントサイドの実装を行います。
クライアントはUnityを使うので、最初はEditorで以下の画面を作成しました。

unity_editor.png

Hierarchyビューを見ていただくと、InputAreaOutputAreaというオブジェクトがあります。これらはそれぞれ、Gameビューにおける下側の要素と上側の要素を指しています。

また、とても重要なのがHierarchyビューのClientオブジェクトです。これはEmpty Objectなのですが、以下のスクリプトがアタッチされています。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UnityEngine.UI;

public class ApiClient : MonoBehaviour
{
    public InputField inputField;
    public Text outputText;
    public Image monsterImage;

    private string baseURL = "http://127.0.0.1:8080";

    public void GetDataFromAPIServer()
    {
        StartCoroutine(GetData());
    }

    IEnumerator GetData()
    {
        string url = baseURL + "/getData/" + int.Parse(inputField.text);
        UnityWebRequest request = UnityWebRequest.Get(url);
        yield return request.SendWebRequest();

        if (request.isNetworkError)
        {
            Debug.Log(request.error);
        }
        else
        {
            if(request.responseCode == 200)
            {
                string rawData = request.downloadHandler.text;
                MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);

                string imgPath = "monster_" + monsterData.ID;

                outputText.text = monsterData.Name;
                monsterImage.sprite = Resources.Load<Sprite>(imgPath);
            }
        }
    }
}

上記コードは以下の資料を参考にさせていただきました!
UnityでHTTPに接続する

このスクリプトでは、GetDataFromAPIServer()メソッドが実行されるとGetData()が動きます。
GetData()内で、リクエスト対象のURLを作成後、以下の箇所でサーバ側への送信とレスポンス受付けを行います。

        yield return request.SendWebRequest();

レスポンスのステータスコードが200ならば、返ってきたJSONデータ(string型)をプログラム内の変数に格納します。
その後、JSONデータをパースして対応するクラスインスタンスに情報を入れていきます。以下の箇所です。

                MonsterData monsterData = JsonUtility.FromJson<MonsterData>(rawData);

このMonsterDataクラスですが、以下のようにしました。データを一意に保つためのIDと、データの名前Nameを持ちます。なので返却されるJSONは、この構造を持っている必要があります。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[System.Serializable]
public class MonsterData
{
    public int ID;
    public string Name;
}

上のソースコード(ApiClientクラスのほう)をClientオブジェクトにアタッチしたのが、以下の状態です。

client_set.png

いくつかInspectorビューから紐づけているコンポーネントがあります。

  • InputField
    • クライアント側から指定するIDを入力するフィールド.
  • OutputText
    • サーバ側から返ってきたデータのName変数を格納する.
  • MonsterImage
    • 取得データのIDを使ってクライアント側に置いてある対応する画像をセットする.

これらは以下のように紐づけています。

client_link.png

最後に、今回はGUIのボタンが押されたらサーバ側に処理を依頼するようにしようと思ったので、Hierarchyビュー上に置かれているボタンオブジェクトに対して、ClientオブジェクトにアタッチしたApiClientクラスが持つGetDataFromAPIServer()メソッドを割り当てています。

client_btn.png

これで準備OKです!

動作確認

まずAPIサーバを立てておきます。以下の状態で待機します。

$ go run server.go
2019/12/15 23:25:10 Server Started.

ターミナルで別タブを開いて、いくつかデータを格納(POST)しておきます。

$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 1, "Name": "スライム"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 39

{
  "ID": 1,
  "Name": "スライム"
}%

$ curl -i -H "Content-Type: application/json" \
-d '{"ID": 2, "Name": "ソルジャー"}' http://127.0.0.1:8080/postData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: yyyyy
Content-Length: 42

{
  "ID": 2,
  "Name": "ソルジャー"
}%

以下のREST APIを叩くと、結果が全件返ってきてくれました!

$ curl -i http://127.0.0.1:8080/getAllData
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-Powered-By: go-json-rest
Date: xxxxx
Content-Length: 103

[
  {
    "ID": 1,
    "Name": "スライム"
  },
  {
    "ID": 2,
    "Name": "ソルジャー"
  }
]%

次にクライアントサイドを起動して動作をみてみます。
まず、IDが1のデータを取得してみます。(格納しておいたデータでいうと、スライムが該当します)

以下のように入力して「データを取得」を押すと、、、

execute_1.png

無事データを取得して、画面に反映されました!(画像データはあらかじめ自分で作ったものを参照してますw)

result_1.png

同じように、IDを2にしてやってみます。以下のようにして「データを取得」を押すと、、、

execute_2.png

IDが2のデータ(ソルジャー)が取得できました!

result_2.png

終わりに

今回、UnityとGoを用いて簡単なAPI通信を実装してみました。
今後は今回学んだことも踏まえながら、より発展的な内容にも取り組んでいけたらなと思っています!

ありがとうございました!

参考

golangでREST APIをやってみた①
UnityでHTTPに接続する
UnityWebRequest
UnityWebRequestの使い方【Unity】
Go 言語の値レシーバとポインタレシーバ

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

UnityでAndroid実機で実行したときに一部環境で画面が乱れる問題と対処法

発生した問題

UnityでAndroidスマートフォン向けの2Dゲームを開発していてゲーム画面のアスペクト比を統一させようとした。
方法はスクリプトでスマホ画面のアスペクト比が基準となるアスペクト比(今回は16:9)と異なる場合、ゲーム開始時にメインカメラのrectを変更してゲーム画面のアスペクト比を統一して余ったところは黒帯を表示させることで対応した。
エディタ上でゲームを実行したところ下の写真のようにカメラ外の左右の余った部分は黒帯が表示されている。
New Unity Project - test - Android - Unity 2019.2.13f1 Personal _DX11_ 2019_12_15 14_30_54.png
しかし、ビルドして手元のPixel 3aで実行したところ、下の写真のように左右のカメラの表示外の部分が黒帯にならず表示が乱れる現象が発生した。
Screenshot_20191215-142906.png
他の実機端末で実行するとエディタ上で実行したように、余った部分は黒く表示されて同じ現象は発生しなかった。

原因

調べたところ下の記事に書いてあるようにカメラの描写外の部分はメモリ上のゴミが残されているかららしい。
Android の特定機種で画面が乱れたら……

対処法

画面全体を映す二つ目のカメラを用意してメインカメラの描写外を表示させることによって一番下の画像のように問題が起こってた端末でもエディタ上と同じく表示されるようになった。全体を映す二つ目のカメラの描写の上にメインカメラを描写してるイメージ。二つ目のカメラはCulling Maskをすべてのレイヤーのオブジェクトを表示しない設定にして黒帯の部分にオブジェクトが映るのを回避している。二つ目のカメラのDepthをメインカメラより小さくしないと二つ目のカメラが上に表示されて全体真っ黒な画面になるので注意。(今回の場合だとBackgroundの色を変えることでほかの色の帯にすることが可能)
ゲームでどのようにカメラを使用しているかによって対処法も変わりそう。
New Unity Project - test - Android - Unity 2019.2.13f1 Personal _DX11_ 2019_12_15 14_32_33.png

Screenshot_20191215-143323.png

さいごに

もし間違えている箇所などがありましたらコメント等で教えてください

参考

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

【Unity】シーンをロードするとき、スマートにパラメーターを渡す方法

こんにちは!
ねこじょーかー(@nekojoker1234)と申します。

Unityで開発をしていると、シーンをロードする処理は必須ですよね。
そのときに、「パラメーターを渡してロードしたいな」と考えている人も少なくないはず。

しかし、調べてもなかなか方法が出てきません。

Qiitaで探してみると、[Unity] シーン切り替え時にパラメータを渡すという記事が出てきました。

こちらの内容でもできそうですが、少し難易度が高い印象。

もっと簡単にやる方法はないか、と調べてみたところ、いい方法があったので記事にしておきます。

シーン間でパラメータを渡す方法

結論から言うと、直接パラメータを渡すのではなく、「間に一つクラスを挟んであげて、お互いにそのクラスを参照する」という方法を取ります。

では、具体的に見ていきましょう。

1.中継役となるクラスを作成する

public static class SceneParameter {
    public static string CrossSceneInformation { get; set; }
}

MonoBehaviourを継承しないクラスなので、GameObjectにアタッチすることはできませんが、そのまま使うことができます。

GameObjectにアタッチしていないので、シーンが変わっても値が保持されている、という仕組みです。

2.ロード前にパラメータをセットする

今回はSceneBを読み込む例としました。

using UnityEngine;
UnityEngine.SceneManagement;

public class ScriptA : MonoBehaviour {
    void Start()
    {
        SceneParameter.CrossSceneInformation = "Hello World!";
        SceneManager.LoadScene("SceneB");
    }
}

3.ロード先でパラメータを取得する

ロード先(SceneB)で、セットしたパラメータを読み込みます。
staticなので、インスタンス化することなく、そのまま参照できます。

using UnityEngine;
public class ScriptB: MonoBehaviour {

    void Start () {
        Debug.Log(SceneParameter.CrossSceneInformation);
    }
}

実行したら、ちゃんと値を渡せていることが確認できます。

Console
Hello World!
UnityEngine.Debug:Log(Object)
ScriptB:Start() 

これだけです。
かなりスッキリしましたね!!

すてきなシーンロードライフ(?)を送りましょう!

参考

Load scene with param variable Unity

あわせて読みたい

筆者のブログ:https://nekojokerblog.com

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

顔を常に映すカメラ(相対位置を保ちたいけど子オブジェクトにしたくない場合)

概要

こういう「絶叫マシンに乗る芸人の表情を映すカメラ」みたいなのを作りたいとして、簡単にやるなら「カメラを頭部ボーンの子にする」だけで良いのですが、ヒエラルキーに配置したモデルのプレハブ中の深い階層に異物を置きたくない場合があります。

そこで、カメラをモデルとは無関係な上位の階層に置き、「指定オブジェクトとの相対位置を保ち続ける」スクリプトをくっつけることにしました。

スクリプト

IsolatedChild.cs
// Parent に指定したオブジェクトの子であるかのように相対座標を維持します
// (ヒエラルキー内のどこに配置してもそのように振る舞います)。
// 直接の子オブジェクトとして配置できない際に有用です。
// MIT License

using UnityEngine;

public class IsolatedChild : MonoBehaviour
{
    public Transform parent;
    private Transform chaser;

    void Start()
    {
        GameObject obj = new GameObject("IsolatedChild");
        chaser = obj.transform;
        chaser.parent = transform.parent;
        chaser.position = parent.position;
        chaser.forward = parent.forward;
        transform.parent = chaser;
    }

    void LateUpdate()
    {
        chaser.position = parent.position;
        chaser.rotation = parent.rotation;
    }
}

使い方

  1. カメラをヒエラルキー内の好きな場所に配置し、初期状態のモデルに対して位置と向きを合わせる
    image.png
  2. カメラに上記スクリプト(Isolated Child)をアタッチし、Parent に注視したいボーンを指定
    image.png

動作原理

スクリプトが Start すると、自分自身(=カメラ)と親との間に空オブジェクト(chaser)を挟み込み、自分はその空オブジェクトの子になります。この空オブジェクトは LateUpdate の処理で常に Parent と同一の座標と向きを保ち続けるので、カメラは擬似的に Parent の子オブジェクトのように振る舞うことになります。

Tips

絶叫マシンに乗ってる芸人の顔はだいたいカメラが寄りすぎて変な顔に映りがちなので、可愛く映したい場合はカメラの Field of View を小さくする(と同時に少し距離を取る)と良いです。

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

Unityのmeta漏れを探すCLI

GO言語の練習がてら、Unityのmetaファイルの整合性を雑に検証するCLIを書いてみた。

どう考えても必要な情報は Library/assetDatabase3 に入ってるが、形式は内緒っぽいので、指定ディレクトリ以下のファイルを見繕って guid 一覧を作ることにした。

UnityのYAMLはYAMLとしては非正規なのでまともにParseできないしやらない。正規表現で雑に引っこ抜いてる。

もうちょっと頑張ると、バージョン管理にファイル追加するときに meta を補完したり、guidを全部手繰って足りないファイルを勝手に add するとかも可能だと思われ。多分重たいけど。

package main

import (
    "path"
    "path/filepath"
    "os"
    "flag"
    "fmt"
    "bufio"
    //"strings"
    "regexp"
)

func readguid4meta(fpath string) string {
    var line []byte
    //var err error

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    line,_,_ = rdr.ReadLine()
    line,_,_ = rdr.ReadLine()
    defer fp.Close()
    //var s := string(line)
    return string(line)[6:]
}

func pick_guids_file(fpath string ) []string {
    var err error

    re,_ := regexp.Compile("guid: [0-9a-f]{32}")
    res := make([]string,3)

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    for {
        var buf []byte
        buf, _, err = rdr.ReadLine()
        if err != nil {
            break
        }
        ///fmt.Printf(string(buf))
        foundx := re.FindAllString(string(buf),9)

        for _, x := range foundx {
            if( x == "00000000000000000000000000000000" || x == "0000000000000000f000000000000000" || x == "0000000000000000e000000000000000" ){
                continue
            }
            res = append(res, x[6:])
        }
        ///fmt.Printf("%s\n",found)
        //fmt.Printf(found[0])
    }

    return res
}

var guid_map = make(map[string]string,3)

func register_guid(oname string, guid string ) {
    var ok bool
    if _,ok = guid_map[guid] ; ! ok { // unknown guid
        //fmt.Printf("# %s|.meta: %s\n",oname,guid)
        guid_map[guid] = oname
    } else {
        if guid_map[guid] == oname {

        } else if guid_map[guid][0] != '?' && oname[0] == '?' {

        } else if guid_map[guid][0] == '?' && oname[0] != '?' {
            ////fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        } else if guid_map[guid][0] != '?' {
            fmt.Printf("# ? %s!|.meta: %s != %s\n",guid_map[guid],guid,oname )
        } else {
            //fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        }
    }
}

func visit(fpath string, fi os.FileInfo, err error ) error {
    extn := path.Ext(fpath)
    oname := fpath[:len(fpath)-len(extn)]
    switch extn {
    case ".meta":
        guid := readguid4meta(fpath)
        register_guid(oname,guid)
    case ".unity",".mat","prefab","asset":
        ///fmt.Printf("? %s\n",fpath )
        ll := pick_guids_file(fpath)
        for _, gs := range ll {
            register_guid("?" + fpath, gs )
        }
        //fmt.Printf("%s\n",ll)
    }
    return nil
}

func main () {
    flag.Parse()
    root := flag.Arg(0)
    filepath.Walk(root,visit)

    for k,v := range guid_map {
        if v[0] == '?' {
            fmt.Printf("%s %s\n",v[1:],k )
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityのmetaのcommit漏れを探すCLI

GO言語の練習がてら、Unityのmetaファイルの整合性を雑に検証するCLIを書いてみた。

guidから実ファイルへの検索が必要で、どう考えても必要な情報は Library/assetDatabase3 に入ってるが、形式は内緒っぽいので、指定ディレクトリ以下のファイルを見繕って guid 一覧を作ることにした。

UnityのYAMLはYAMLとしては非正規なのでまともにParseできないしやらない。正規表現で雑に引っこ抜いてる。

もうちょっと頑張ると、バージョン管理にファイル追加するときに meta を補完したり、guidを全部手繰って足りないファイルを勝手に add するとかも可能だと思われ。多分重たいけど。

package main

import (
    "path"
    "path/filepath"
    "os"
    "flag"
    "fmt"
    "bufio"
    //"strings"
    "regexp"
)

func readguid4meta(fpath string) string {
    var line []byte
    //var err error

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    line,_,_ = rdr.ReadLine()
    line,_,_ = rdr.ReadLine()
    defer fp.Close()
    //var s := string(line)
    return string(line)[6:]
}

func pick_guids_file(fpath string ) []string {
    var err error

    re,_ := regexp.Compile("guid: [0-9a-f]{32}")
    res := make([]string,3)

    fp,_ := os.Open(fpath)
    rdr := bufio.NewReaderSize(fp,100)
    for {
        var buf []byte
        buf, _, err = rdr.ReadLine()
        if err != nil {
            break
        }
        ///fmt.Printf(string(buf))
        foundx := re.FindAllString(string(buf),9)

        for _, x := range foundx {
            if( x == "00000000000000000000000000000000" || x == "0000000000000000f000000000000000" || x == "0000000000000000e000000000000000" ){
                continue
            }
            res = append(res, x[6:])
        }
        ///fmt.Printf("%s\n",found)
        //fmt.Printf(found[0])
    }

    return res
}

var guid_map = make(map[string]string,3)

func register_guid(oname string, guid string ) {
    var ok bool
    if _,ok = guid_map[guid] ; ! ok { // unknown guid
        //fmt.Printf("# %s|.meta: %s\n",oname,guid)
        guid_map[guid] = oname
    } else {
        if guid_map[guid] == oname {

        } else if guid_map[guid][0] != '?' && oname[0] == '?' {

        } else if guid_map[guid][0] == '?' && oname[0] != '?' {
            ////fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        } else if guid_map[guid][0] != '?' {
            fmt.Printf("# ? %s!|.meta: %s != %s\n",guid_map[guid],guid,oname )
        } else {
            //fmt.Printf("# %s|.meta: %s\n",oname,guid)
            guid_map[guid] = oname
        }
    }
}

func visit(fpath string, fi os.FileInfo, err error ) error {
    extn := path.Ext(fpath)
    oname := fpath[:len(fpath)-len(extn)]
    switch extn {
    case ".meta":
        guid := readguid4meta(fpath)
        register_guid(oname,guid)
    case ".unity",".mat","prefab","asset":
        ///fmt.Printf("? %s\n",fpath )
        ll := pick_guids_file(fpath)
        for _, gs := range ll {
            register_guid("?" + fpath, gs )
        }
        //fmt.Printf("%s\n",ll)
    }
    return nil
}

func main () {
    flag.Parse()
    root := flag.Arg(0)
    filepath.Walk(root,visit)

    for k,v := range guid_map {
        if v[0] == '?' {
            fmt.Printf("%s %s\n",v[1:],k )
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】Profile Analyzerが便利という話

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

今年の春ぐらいにProfile Analyzerと言うツールのPreview版が公開されました。
こちらは最適化を行う上では結構便利な機能であり、Previewと言えども個人的には結構使っていけそうな雰囲気があります。

ただ、このツールに関する日本語の情報をあまり見受けない?印象があったので1、今回は紹介序に各種機能などを簡単に解説して行ければと思います。

検証環境

Unity version

  • Unity 2018.4.13f1+
    • Documents曰く「Unity5.6以降」 までは互換性があるとのこと。(PackageManagerの管理下よりコピーしてくれば動作するらしい?)

Packages version

  • Profile Analyzer 0.5.0-preview.1
    • ※執筆時点での最新バージョン

※注意点

便利と言えどもパッケージ自体はまだpreviewです。
将来的な変更で記事中の内容と合わなくなる点が出てくるかもしれないので、その点のみご了承ください。

後は内容的に「Unityの既存のProfiler(以降、Unity Profilerと表記)」に関する前知識がある程度必要となってきますが、記事中ではUnity Profilerに関する基礎的なところからは触れないのでご了承ください。
(一応参考資料だけ載せておきます。)

TL;DR

このツールは何?

  • Unity Profilerの拡張機能(と呼べるかもしれない)
    • なので立ち位置的にはUnity Profilerを置き換える「全く新しいProfiler」とかでは無い
  • 出来ることからして「Profilerの結果を分析(Analyze)する為のツール」と言えそう

ツールで出来ること

一言で言うと「Unity Profilerで計測したプロファイリング結果の分析/比較」が可能。
後は分析結果をCSVとして出力可能。

★ 単一データの分析

  • プロファイリング結果から「任意のフレーム範囲」を指定して分析
    • e.g. 300フレーム中の「120~149フレーム → 計30フレーム」を指定して分析
  • 分析した複数フレームにまたがる処理時間(ms)の「平均値・中央値・最大値・最小値」などを視覚化

「単一データの分析」では1つのプロファイリング結果を分析して、複数フレームにまたがる処理時間の中央値や平均値などを簡単に算出/視覚化することが出来ます。

★ データの比較

  • 2つのプロファイリング結果を比較
    • 分析結果(平均値/中央値/etc..)の差分(diff)を簡単にチェック可能
  • 比較することで「組み込んだ最適化処理」の効果などが確認しやすくなる
    • 最適化を組み込んだ影響(他の処理負荷など)の再発見に繋がったりも
  • Unity Profilerと合わせて使うことでネックを特定しやすくなったり

「データの比較」は特に便利な機能であり、例えば「最適化前の結果」と「最適化後の結果」の2点を渡すことで適用した処理の効果を簡単に比較することが出来ます。

→ e.g. とあるロジックを軽量化するとした際に、対応前のデータを事前に取っておくことで、軽量化対応後と比較してどれくらい効果があったのかを簡単にチェックする事が可能。

分析結果のフィルタリング

  • 結果を「メソッド名」や「スレッド」などでフィルタリング
    • スレッド → MainThread, RenderThread, JobWorker, etc..

例えば「レンダリング周り」で調整を入れた際には、RenderThreadでフィルタリングすることで見通しが良くなったりします。

使い方

ツールの基本的な概要及び具体的な使い方については以下の公式のブログ/ドキュメントに纏められています。

とは言え...リンクを貼って「後は読んでね」だけだと記事として微妙なので...簡単な使い方及び計測結果などを踏まえつつ解説していきます。
詳細についてはドキュメントなども合わせてご覧ください。

用語統一

Unity Profilerでは「.data」と言う拡張子でプロファイリング結果を保存できますが、これとは別にProfile Analyzer側でも分析結果を「.pdata」と言う拡張子で保存することが出来ます。(詳細については後述)

少し用語が入り混じってくるので...以下の呼び方で統一します。

記事中での呼び方 該当箇所 拡張子
プロファイリング結果 Unity Profilerのプロファイリング結果 .data
分析結果 Profile Analyzerの分析結果 .pdata

★ 単一データの分析

サンプルとして以下のプロファイリング結果を対象に解説を進めていきます。
(物としてはNew Sceneで作ったばかりの何もないシーンで記録した300フレーム分の記録)

※ドキュメント : Single View

sample.png

Profile Analyzer側でプロファイリング結果を読み込む

次にProfile Analyzer側で結果を読み込むために先ずは画面を開きます。
Profile Analyzerはメニューバーにある「Window -> Analysis -> Profile Analyzer」から開けます。

menu.png

開くと以下の様な画面が表示されるかと思います。
後は注釈の通りに【Pull Dataボタン】を押下して先程記録(若しくはロード)したプロファイリング結果を読み込みます。

single_sample.png

※分析結果の保存について

【Pull Dataボタン】で読み込んだプロファイリング結果はProfile Analyzer側で分析された上で結果が画面に表示されます。(画面の詳細は後述)

この時の「分析結果」は【Saveボタン】から.pdataとしてファイルに保存することが出来ます。
予め分析結果を保存している場合には【Pull Dataボタン】から読み込まずとも、Profile Analyzer上の【Loadボタン】から読み込むことが出来ます。

※こちらは既知の制限としてドキュメントにも記載されており、「.dataと.pdataの両方を保持するのがオススメ」とも説明されています。

Profile Analyzerの見かた/使い方

プロファイリング結果の分析が完了すると以下の様な画面に表示が変わるかと思います。

後はこちらの画面を操作して各種分析を行う形となりますが、機能については全て解説していくと数が多いので...今回はその中にある幾つかの機能を紹介していきます。

Art004.png

複数フレームにまたがる分析

今回のプロファイリング結果は「96フレーム~396フレーム」の計300フレーム分が読み込まれてますが、この中から「110フレから140フレまでの計31フレーム分を分析したい」と言った場合には以下の注釈にある領域を操作して「複数のフレーム」を指定します。

profile_1.png

分析結果は赤枠内に表示

指定範囲の分析結果は主に赤枠内に表示されます。
例えば「Marker Details for currently selected range」の項目を見ると、マーカー名に応じた中央値(Median)平均(Mean)と言った分析結果を確認することが出来ます。

上記の31フレームの分析結果を見ると、例えば以下の要素などが確認出来ます。

Maeker Name 中央値(ms) 平均値(ms)
PlayerLoop 12.20 11.36
Camera.Render 0.15 0.17
WaitForTargetFPS 11.83 10.94

その上にある「Top 10 markers on median frame」の項目には名前の通りMedian(中央値)を基準としたTop10が表示されてます。

もちろん全範囲指定も可能

全範囲指定すれば「全フレーム分」をそのまま分析することが出来ます。
→ 選択範囲は以下の赤枠内で確認可能。

独自でツールを作ったりせずともシュッと平均値などを分析できるのは便利ですね。

profile_2.png

その他、範囲指定に関する情報はドキュメントの「Frame Control and Range Selection」を御覧ください。

項目のフィルタリング

Profilerの処理内容に該当するMakerは文字列指定やスレッド指定などでフィルタリングすることが出来ます。
指定箇所としては以下の赤枠内となります。

filter.png

名称でフィルタリング

例えば「MonoBehaviour.Update全体の負荷」を見たい場合には以下のようにUpdate.ScriptRunBehaviourUpdateを【Name Filter : All】で指定することでフィルタリング出来ます。
※今回はUpdateを呼び出す物が1つも存在しないので結果は0msとなっている。

横にある【Exclude Names】を指定すればその名称を除外することも可能です。

ScriptRunBehaviourUpdate.png

スレッド単位でフィルタリング

【Name Filter】の下にある【Thread】の項目ではThread単位でフィルタリングすることが出来ます。

デフォルトではMainThreadのみが選択されている状態となりますが、以下のようにRenderThreadのみを表示する形にして【Apply】するとRenderThreadに関する情報のみが表示されるようになります。

Art003.png

その他

詳細は割愛しますが、他にある機能として【Depth Slice】でスタックレベルでフィルタリング出来たり、【Analysis Type】で表示結果をTotal or Selfに切り替えたり出来ます。

詳細はドキュメントの「Filtering System」を御覧ください。

★ データの比較

ここからはデータの比較解説用にサンプルを変更します。

対象としては以前自分が作った「VRMSpringBoneのJobSystem対応」をベースにして、MonoBehaviourベースの実装からJobSystemベースの実装2に切り替えた際の差分に注目して解説を進めていきます。

※その他、サンプルの詳細についてはこちらを参照 (クリックで展開)
  • 検証内容
  • 実行環境
    • Standalone(Windows) + IL2CPP
    • CPU : Intel Core i7-8700K (Worker Threadは11本)

VRMSpringBoneのJobSystem対応の詳細については「こちらのスライド」を御覧ください。
ちなみに動作画面は↓になります。

aaa.png

※ドキュメント : Compare View

プロファイリング結果の読み込み

注釈の通り、読み込む必要のあるプロファイリング結果は2つ必要になります。
データが用意できたらProfile AnalyzerのModeを【Compare】に切り替えます。

データを読み込む方法については「単一データの分析」と同じです。
→ 分析データ(.pdata)が有るなら【Loadボタン】から読み込み、無ければUnity Profiler側でプロファイリング結果(.data)をロードして【Pull Dataボタン】で読み込み。

compare2.png

読み込むデータは2つ必要となるので、解説中では以下の前提で進めていきます。

  • 最適化前のデータ (MonoBehaviourベースの実装)
    • 上の青いボタンでロード
      • → 以降、画面中の青色表記は最適化前に該当
  • 最適化後のデータ (Jobsystemベースの実装)
    • 下のオレンジ色のボタンでロード
      • → 同様にオレンジ色表記は最適化後に該当

分析結果

以下に全フレーム分を対象とした分析結果を貼ります。
→ 分析方法については「単一データの分析」と変わりません。

注目できるポイントとしては赤線を引いているLateUpdateの負荷です。
VRMSpringBoneは数が多い分だけLateUpdateのMainThread占有率が目立ってしまう傾向があり、最適化後の方と比べると中央値が13.45ms削減出来ていることが分かります。

ただ、もう一点気になるポイントもあります。
箇所としては赤破線を引いているFinishFrameRenderingであり、最適化前と比べて処理が伸びていることが伺えます。
※同様に下にあるGfx.WaitForPresentなども伸びている。

springbone2.png

何故伸びた?

折角なので処理負荷の原因を特定する際の一例として伸びた原因についても簡単に追ってみたいと思います。3

今回問題となっているPostLateUpdate.FinishFrameRenderingは呼び出し階層としては結構上の方に位置しており、これだけだと具体的に「どこの処理が重いのか」が分かりづらいです。4

なので、処理を追う際にはUnity Profilerも合わせて活用する形で追っていきたいと思います。

ポイント: Unity Profilerも合わせて活用

Profile Analyzerで見れるのは「分析結果」であり、「時系列で何があったか?」と言った情報についてはUnity ProfilerのTimelineの方が確認しやすいと思います。

と言うことで2点のプロファイリング結果のTimelineを見比べてみましょう。

最適化前

(画像だけだと分かり辛いところもあるかもしれませんが...)
Profile Analyzerの分析結果と合わせてみることで以下の要点が見えてきます。

  • PostLateUpdate.FinishFrameRenderingの負荷はほぼ一律 (と言うよか目立ったスパイクとかは無い)
    • → その上でGfx.WaitForPresentは発生していない
  • Gfx.ProcessCommandsがフレーム中に完結している

def.png

最適化後

こちらもProfile Analyzerの分析結果と合わせて見ることで以下の要点が見えてきます。

  • 最適化後の方はPostLateUpdate.FinishFrameRenderingで定期的に負荷が発生
    • Timeline上のコールスタックを見るとGtx.WaitForPresentが伸びていることが見えてきた
      • ※ 同様にProfile Analyzer側でも最適化後のみGtx.WaitForPresentが伸びているのが伺える
  • 前フレームのGfx.ProcessCommandsがはみ出ている

話を纏めてしまうと最適化前と最適化後でMainThreadの処理時間が大分変わってしまっており、それが影響してGPUの実行タイミングにズレが生じてはみ出ている事が分かりました。

opt3.png

CSVへのエクスポート

分析結果はCSV形式で出力することが出来ます。

単一データの分析」又は「データの比較」で分析結果を読み込んだ状態で、メニューに有る【Exportボタン】を押下する事でメニューが表示されます。

※ドキュメント : Export Dialog

csv_ex.png

以下のCSVは、上述の「データの比較」の章にて検証した内容をそのまま出力したものです。

※画像省略 (クリックで展開)

Art001.png

まだ具体的な利用方法までは思いついていない段階ですが...自作のツールなり仕組みなりに組み込むと言ったことが可能かもしれません。

その他Tips

分析結果の表示項目の変更

以下の分析結果の表示項目について、こちらは右クリックから変更することが可能です。

  • 「単一データの分析」 → Marker Details for currently selected range
  • 「データの比較」 → Marker Comparation for currently selected range

Art001.png

項目は「単一データの分析」か「データの比較」で変わってくるので、詳細についてはドキュメントをご覧ください。

コンテキストメニューからFiltersにMarker名を追加

【Filters】への追加は直接の入力以外にも、右クリックで表示されるコンテキストメニューからも設定することが出来ます。

やり方としては「追加したいMarker名」を選択した後に右クリックで以下のようなメニューが表示されます。
→ 例えばここから「Add to Include Filter」を実行すると【Name Filter :】に選択したMarker名が追加されます。

他にも「Set as Parent Marker Filte」を実行すると【Parent Maker :】に選択したMarker名が追加され、表示内容を「指定したMarker以下のコールスタック」に限定することが出来ます。

Art002.png

その他、コンテキストメニューの内容はドキュメントを御覧ください。

最後に

まだpreview packageではありますが、触れてみて普通に使っていけそうな印象はありました。
※後は実態がEditor拡張であり、ランタイムに含まれないという点も導入しやすい感も。

解説は以上となりますが、今回話した内容以外にも色々と使い方は有るかと思います。
他に便利な使用例と言ったものが出てきたら随時アウトプットしていければと思います。
(※私以外にも「こう使ってる」「この用途だと便利」的な情報があれば、どんどんシェア/アウトプットして頂けると幸いです! :bow:)

検討事項

色々記載しましたが...言うてUnity Profilerには300フレームしか保持できない制限があります...。故に「暫く動かした結果を分析」と言った対応は難しいかもしれません。。

Unity 2019.3からは表示フレームを最大2000フレームまで引き伸ばすことが出来るので、仮にProfile Analyzerが対応されていたら活用の幅が広がるかもしれません。(まだ未検証なので要調査...)

関連リンク

Profiler基礎


  1. 実際にどれくらい使われているのだろうか..? :thinking:  

  2. 記事中ではCentralizedBufferと言う実装をベースに解説 

  3. やり方については色々とアプローチが有るかと思われます。今回は説明用の手順として考えてみた物で解説。(これが必ずしも「正しい追い方」では無いと思うのでご了承を...) 

  4. FinishFrameRenderingの下でGfx.WaitForPresentが伸びているからコレじゃね?」とパッと見て判断/自己解決できるゴリラさんなら読み飛ばしても良いかも... 

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

UnityプロジェクトをGitLab-CIを使って自動ビルド環境を構築しよう

GitLabで自動ビルド環境を作りたい!

こう思った理由

ゲーム制作にはいろいろな人が参加します。
プログラマー・ゲームデザイナー・グラフィックデザイナー・サウンドクリエイター...
チームメンバー全員がUnityを扱えれば最高ですが、必ずしも全員がUnityを扱うスキルが必要でしょうか。
これは議論の余地がありますが、僕は「全員がUnityを扱えなくても良い」と考えます。
そのため、Unityを扱えずとも、進捗状況を確認できる環境の構築が必須になりました。

なぜ「全員がUnityを扱えなくても良い」と考えるか

  • デザイナー・サウンドクリエイター陣にUnityを教えるコストが高い。
  • 自動ビルド環境が整っていないと、必然的にUnityのバージョンを全員で統一する必要がある。
  • 全員にGitの使い方を教えるのもコストが高い。
  • 現在の進捗状況を確認する方法が別にあれば、全員がUnityを使わなくても良くなる。

準備項目

  • GitLabアカウントを作成する
  • 自身のローカル環境にあるUnityプロジェクトをGit管理下に置く
  • GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする
  • Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する
  • Unityプロジェクトの中に「ci」フォルダをコピーする
  • 「Assets/Scripts/Editor」フォルダを自身のUnityプロジェクトの同じ階層にコピーする
  • GitLabの「Setting」欄の「CI/CD」メニューの中の「Variables」の「Key」欄に「UNITY_USERNAME」と「UNITY_PASSWORD」を追加し、「Value」欄にそれに対応した値を追加する
  • GitLabにUnityプロジェクトをpushする
  • 生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする
  • GitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする

以上の作業を行います。画像を使って解説します。

GitLabアカウントの作成

キャプチャ.PNG
GitLab.comのSign upページに行き、アカウントを作成します。
GitLab.com

自身のローカル環境にあるUnityプロジェクトをGit管理下に置く

Gitの使い方は僕なんかよりよっぽど詳しい人達がいるので、その人達を参考にしてください。

GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする

キャプチャ1.PNG
unity3d-gitlab-ci-exampleのclone httpをコピーし、cloneコマンドでローカルに落とします。

Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する

キャプチャ2.PNG
「unity3d-gitlab-ci-example」の中の「.gitlab-ci.yml」ファイルをコピーして、Unityプロジェクトの直下にペーストします。
「.gitlab-ci.yml」の中の「Build」項目内のビルドするファイルの種類は、必要なものを残して、あとは削除しても問題ありません。
ついでに、「Build」項目内でWebGLでのビルドを記入しない場合は、最後の「Pages」の項目も必要ありません。

Unityプロジェクトの中に「ci」フォルダをコピーする

「unity3d-gitlab-ci-example」内の「ci」フォルダはそのまま、Unityプロジェクトの直下にコピーします。

「Assets/Scripts/Editor」フォルダを自身のUnityプロジェクトの同じ階層にコピーする

「unity3d-gitlab-ci-example」内の「Scripts/Editor」フォルダは、Unityプロジェクトの「Assets」フォルダー内にコピーすれば問題ないと思われます。「unity3d-gitlab-ci-example」の構成と同じにしてください。

GitLabの「Setting」欄の「CI/CD」メニューの中の「Variables」の「Key」欄に「UNITY_USERNAME」と「UNITY_PASSWORD」を追加し、「Value」欄にそれに対応した値を追加する

キャプチャ3.PNG
黄色くマーカーしたところです。ここには、いつも使用している自分のユーザーネーム(メールアドレス)とパスワードを「Value」に入れています。一番上の「UNITY_LICENSE_CONTENT」はまだ入力する必要がありません。

GitLabにUnityプロジェクトをpushする

これでようやくプッシュまでの準備は完了です。一回目のプッシュが終わったあとも、もう少し作業が残っています。もう少しの辛抱です。

生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする

キャプチャ4.PNG
「Job」の中の「get-activation-file」を走らせて、この画面が出たら成功です。「Artifacts」をダウンロードし、「Unity3d.alf」ファイルを取得したら、Unity公式サイトのライセンス確認ページで「Unity3d.alf」ファイルをアップロードします。
Unityライセンス確認ページ

GitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする

キャプチャ5.PNG
黄色くマーカーした場所です。Valueの値は、テキストエディタで「Unity_v2019.x.ulf」を開いて見ることができる文章です。まるっとコピーして、Valueの中に入れます。

これでGitLab-CIで自動ビルドができるようになると思います。以降は、Pushするたびに自動で最新の進捗を実行ファイルにしてくれます。

Unity内でエラーが出る場合

「Assets/Scrpts/Editor」にコピーしたファイルがUnityでエラーを出している場合があります。その時は、エラーを吐いている行をコメントアウトすれば大丈夫です。

さいごに

Unityアドベントカレンダーパート3の15日目の記事はこんな感じです。「Unityが使えないメンバーにも今の開発状況を遊ばせたいなぁ...」という方々への参考になれば幸いです。閲覧いただきありがとうございました。

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

GitLab-CIを使ってUnityプロジェクトに自動ビルド環境を構築しよう

GitLabで自動ビルド環境を作りたい!

こう思った理由

ゲーム制作にはいろいろな人が参加します。
プログラマー・ゲームデザイナー・グラフィックデザイナー・サウンドクリエイター...
チームメンバー全員がUnityを扱えれば最高ですが、必ずしも全員がUnityを扱うスキルが必要でしょうか。
これは議論の余地がありますが、僕は「全員がUnityを扱えなくても良い」と考えます。
そのため、Unityを扱えずとも、進捗状況を確認できる環境の構築が必須になりました。

なぜ「全員がUnityを扱えなくても良い」と考えるか

  • デザイナー・サウンドクリエイター陣にUnityを教えるコストが高い。
  • 自動ビルド環境が整っていないと、必然的にUnityのバージョンを全員で統一する必要がある。
  • 全員にGitの使い方を教えるのもコストが高い。
  • 現在の進捗状況を確認する方法が別にあれば、全員がUnityを使わなくても良くなる。

準備項目

  • GitLabアカウントを作成する
  • 自身のローカル環境にあるUnityプロジェクトをGit管理下に置く
  • GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする
  • Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する
  • Unityプロジェクトの中に「ci」フォルダをコピーする
  • 「Assets/Scripts/Editor」フォルダを自身のUnityプロジェクトの同じ階層にコピーする
  • GitLabの「Setting」欄の「CI/CD」メニューの中の「Variables」の「Key」欄に「UNITY_USERNAME」と「UNITY_PASSWORD」を追加し、「Value」欄にそれに対応した値を追加する
  • GitLabにUnityプロジェクトをpushする
  • 生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする
  • GitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする

以上の作業を行います。画像を使って解説します。

GitLabアカウントの作成

キャプチャ.PNG
GitLab.comのSign upページに行き、アカウントを作成します。
GitLab.com

自身のローカル環境にあるUnityプロジェクトをGit管理下に置く

Gitの使い方は僕なんかよりよっぽど詳しい人達がいるので、その人達を参考にしてください。

GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする

キャプチャ1.PNG
unity3d-gitlab-ci-exampleのclone httpをコピーし、cloneコマンドでローカルに落とします。

Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する

キャプチャ2.PNG
「unity3d-gitlab-ci-example」の中の「.gitlab-ci.yml」ファイルをコピーして、Unityプロジェクトの直下にペーストします。
「.gitlab-ci.yml」の中の「Build」項目内のビルドするファイルの種類は、必要なものを残して、あとは削除しても問題ありません。
ついでに、「Build」項目内でWebGLでのビルドを記入しない場合は、最後の「Pages」の項目も必要ありません。

Unityプロジェクトの中に「ci」フォルダをコピーする

「unity3d-gitlab-ci-example」内の「ci」フォルダはそのまま、Unityプロジェクトの直下にコピーします。

「Assets/Scripts/Editor」フォルダを自身のUnityプロジェクトの同じ階層にコピーする

「unity3d-gitlab-ci-example」内の「Scripts/Editor」フォルダは、Unityプロジェクトの「Assets」フォルダー内にコピーすれば問題ないと思われます。「unity3d-gitlab-ci-example」の構成と同じにしてください。

GitLabの「Setting」欄の「CI/CD」メニューの中の「Variables」の「Key」欄に「UNITY_USERNAME」と「UNITY_PASSWORD」を追加し、「Value」欄にそれに対応した値を追加する

キャプチャ3.PNG
黄色くマーカーしたところです。ここには、いつも使用している自分のユーザーネーム(メールアドレス)とパスワードを「Value」に入れています。一番上の「UNITY_LICENSE_CONTENT」はまだ入力する必要がありません。

GitLabにUnityプロジェクトをpushする

これでようやくプッシュまでの準備は完了です。一回目のプッシュが終わったあとも、もう少し作業が残っています。もう少しの辛抱です。

生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする

キャプチャ4.PNG
「Job」の中の「get-activation-file」を走らせて、この画面が出たら成功です。「Artifacts」をダウンロードし、「Unity3d.alf」ファイルを取得したら、Unity公式サイトのライセンス確認ページで「Unity3d.alf」ファイルをアップロードします。
Unityライセンス確認ページ

GitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする

キャプチャ5.PNG
黄色くマーカーした場所です。Valueの値は、テキストエディタで「Unity_v2019.x.ulf」を開いて見ることができる文章です。まるっとコピーして、Valueの中に入れます。

これでGitLab-CIで自動ビルドができるようになると思います。以降は、Pushするたびに自動で最新の進捗を実行ファイルにしてくれます。

Unity内でエラーが出る場合

「Assets/Scrpts/Editor」にコピーしたファイルがUnityでエラーを出している場合があります。その時は、エラーを吐いている行をコメントアウトすれば大丈夫です。

さいごに

Unityアドベントカレンダーパート3の15日目の記事はこんな感じです。「Unityが使えないメンバーにも今の開発状況を遊ばせたいなぁ...」という方々への参考になれば幸いです。閲覧いただきありがとうございました。

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

【Unity+ARKit3(+PeopleOcclution)】カラスがゴミ袋を回収するクソアプリを作る

この記事はクソアプリ Advent Calendar 2019 の15日目の記事です。

クソアプリクリエイターの皆さんが知見や学びの深い全然クソじゃないアプリを皆さん生み出していく中、
自分のためだけに勉強がてら新しいiPhone使って作りたいものを作りました:innocent:

ARで認識した床面をタップすると、ゴミ袋が出てきてカラスがせっせと回収に行くアプリです。
ゴミ袋を回収すると、ポイントカウントしていきます。
子供には好評でした。

コンポ 1.gif

今回制作したものは時短でやろうと思い、Asset Storeの使用が多いのですが、
Asset Store関連のものを覗いたプロジェクトデータをGithubにアップしたので、合わせて見てみてください。
https://github.com/sadakitchen/ARFoundationTest/tree/master

環境

  • macOS Catalina 10.15.2
  • Xcode 11.3
  • Unity 2019.2.2f1
  • iPhone 11 Pro(iOS 13.2.3)

使用アセット

参考

経緯とアプローチ

手持ちのiPhone Xがグリーンスクリーンになってしまい、iPhone 11 Proを購入したのですが、
前々から気になっていた ARKit3 を試したかったためなにか作ろうと思い立ちました。

まず、 ARKit について体系的に知りたかったので、
@shu223 さんの「実践ARKit」でサンプル動かしてざっと眺めることに。
どんな仕組みで動いているのかも詳細に書かれているのでオススメです。
スクリーンショット 2019-12-14 15.09.16.png

次に Unity で Unity-Technologies/arfoundation-samples のサンプルシーンをビルドすることに。

が、ここでmacOS Catalinaユーザーへの罠があり・・・
CatalinaとUnityの最新バージョン(2019/12/15現在 2019.2.15f1)で
ビルドがうまく行かないエラーに見舞われてしまうことに・・・

しばらく情報を探し回ってたところ、ここのスレッドで Unity 2019.2.2f1 だとうまくビルドできたという記事を発見。
https://forum.unity.com/threads/unknown-shader-compiler-error-using-unity-2019-2-8f1-when-building-ios.758339/

Unity 2019.2.2f1にて、
Player Settings の Other Settings の Color Space を Linearに変更、
Auto Graphics API の チェックをOFFにし、Graphics APIs にて Metal を最上位に設定。

スクリーンショット 2019-12-14 15.32.51.png

ちなみになぜかBuild And Runが動作しないので、
一度BuildでXcodeプロジェクトファイルを作り、Unity-iPhone.xcodeproj を起動するフローでした。

ひとしきり触った後は、技術選定を考えました。

ARKit3 の特徴は以下とのことで、

  • People Occlution(人物の奥行きを加味したAR合成)
  • Motion Capture(人物のスケルトンを取得)
  • Simultaneous Front and Back Camera(フロントカメラとバックカメラ)
  • Multiple Face Tracking(複数顔認識)
  • Collaborative Sessions(ARWorldMapの共有機能強化)

今回はググるとサンプルが沢山あって困りにくそうなPeopleOcclutionを使ってみようと思いました:innocent:

アプリ概要は以下です。
名称未設定のアートワーク.png

制作のポイント

前述のUnityのAR FundationでPeopleOcclusionしてみたが非常に詳しく解説しているので、
ポイントを要所要所解説します。

1 . Package Managerから必要なものをDL
- ARFoundation 3.0.0 preview.6
- ARKit XR Plugin 3.0.0 preview.4
※ 2019.12.14現在 ARFoundation 3.0.1 で ARHumanBodyManager (人体検出関連クラス)が存在しないようです。

2 . シーン上にGameObject > XR から ARSession、ARSessionOrigin、ARDefaultPlane を追加。
ARDefaultPlaneはPrefab化し、シーンから削除しておきます。

3 . ARSessionOrigin に ARPlaneManager、ARHumanBodyManagerコンポーネントを追加。
ARPlaneManagerコンポーネントのPlane PrefabにPrefab化しておいたARDefaultPlaneを設定し、
ARHumanBodyManagerコンポーネントの設定を、
Human Segmented Stencil:Full Screen Resolution、
Human Segmented Depth:Standard Resolution
に設定します。

4 . PeopleOcclusionPostEffect.csとPeopleOcclusion.shaderを作成します。
UnityのAR FundationでPeopleOcclusionしてみたのコードをベースに以下のように変更しました。

PeopleOcclusion.shader
            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                if(_ONWIDE == 1)
                {
                    o.uv1 = float2(v.uv.x, (1.0 - (_UVMultiplierLandScape * 0.5f)) + (v.uv.y / _UVMultiplierLandScape));
                    o.uv2 = float2(lerp(1.0 - o.uv1.x, o.uv1.x, _UVFlip), lerp(o.uv1.y, 1.0 - o.uv1.y, _UVFlip));
                }
                else
                {
                    o.uv1 = float2(1.0 - v.uv.y, 1.0 - _UVMultiplierPortrait * 0.5f + v.uv.x / _UVMultiplierPortrait);
                    float2 oUV1_f = float2((1.0 - (_UVMultiplierPortrait * 0.5f)) + (v.uv.x / _UVMultiplierPortrait), v.uv.y);
                    o.uv2 = float2(lerp(1.0 - oUV1_f.y, oUV1_f.y, 0), lerp(oUV1_f.x, 1.0 - oUV1_f.x, 1));
                }
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);
                fixed4 cameraFeedCol = tex2D(_CameraFeed, i.uv1);
                float sceneDepth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv));
                float4 stencilCol = tex2D(_OcclusionStencil, i.uv2);
                float occlusionDepth = tex2D(_OcclusionDepth, i.uv2) * 0.625; //0.625 hack occlusion depth based on real world observation

                float showOccluder = step(occlusionDepth, sceneDepth) * stencilCol.r; // 1 if (depth >= ocluderDepth && stencil)

                return lerp(col, cameraFeedCol, showOccluder);
            }
PeopleOcclusionPostEffect.cs
    // ...中略
    [SerializeField] private Shader m_peopleOcclusionShader = null;
    //[SerializeField] Texture2D testTexture;  // 削除
    // ...中略

    private void RefreshCameraFeedTexture()
    {
        // ...中略
        m_cameraFeedTexture.Apply();
        m_material.SetTexture("_CameraFeed", m_cameraFeedTexture);  // testTextureをm_cameraFeedTextureに変更
    }

※Player Settings から Other Settings の Allow 'Unsafe' Code のチェックをONにしておきます。

シーン上のARCameraにPeopleOcclusionPostEffectをアタッチし以下のように設定します。
スクリーンショット 2019-12-14 21.08.37.png

5 . 続いて、カメラで表示されている空間にオブジェクトを表示することができるようにSpawn.csを作成します。
Spawn.csとARRaycastManagerコンポーネントをAR Session Originへアタッチ。
Spawnのm_SpawnPrefabには表示したいオブジェクトを適当に用意してアタッチできます。

Spawn.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

[RequireComponent(typeof(ARRaycastHit))]
public class Spawn : MonoBehaviour
{
    [SerializeField] GameObject m_SpawnPrefab;

    private readonly List<ARRaycastHit> _hitResults = new List<ARRaycastHit>();
    private ARRaycastManager _rayManage;

    private void Awake()
    {
        _rayManage = this.GetComponent<ARRaycastManager>();
    }

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_rayManage.Raycast(Input.GetTouch(0).position, _hitResults, TrackableType.PlaneWithinPolygon))
            {
                Instantiate(m_SpawnPrefab, _hitResults[0].pose.position, Quaternion.identity);
            }
        }
    }
}

6 . ゴミ袋に向かっていくためのSpawnTrace.csを作成します。
指定したTagNameからオブジェクトを検索し、その中から最も近いオブジェクトに向かって行く処理にしています。
TagNameはTrashBagとしましたが、任意の名前で大丈夫です。

SpawnTrace.cs
using UnityEngine;

public class SpawnTrace : MonoBehaviour
{
    [SerializeField] private GameObject m_Tracer;
    [SerializeField] private string m_TagName;
    [SerializeField] private float speed = 0.1f;

    private GameObject _targetObject;

    void Update()
    {
        _targetObject = GetTargetObject(m_TagName);
        if (_targetObject == null) return;

        Vector3 relativePos = _targetObject.transform.position - m_Tracer.transform.position;
        Quaternion rotation = Quaternion.LookRotation(relativePos);
        m_Tracer.transform.rotation = Quaternion.Slerp(m_Tracer.transform.rotation, rotation, speed);

        m_Tracer.transform.position = Vector3.MoveTowards(m_Tracer.transform.position, _targetObject.transform.position,
            speed * Time.deltaTime);
    }

    private GameObject GetTargetObject(string tagName)
    {
        float nearDis = 0;
        GameObject targetObj = null;

        foreach (GameObject obs in GameObject.FindGameObjectsWithTag(tagName))
        {
            var tmpDis = Vector3.Distance(obs.transform.position, m_Tracer.transform.position);

            if (nearDis == 0 || nearDis > tmpDis)
            {
                nearDis = tmpDis;
                targetObj = obs;
            }
        }

        return targetObj;
    }
}

今回はCrowというGameObjectを作成し、それにSpawnTrace.csをアタッチしています。
スクリーンショット 2019-12-14 22.35.54.png

7 . ゴミ袋と衝突したときゴミ袋を消去し、数えるCounter.csを作成します。
作成後、前述のCrowにCounter.cs、Rigidbodyコンポーネントをアタッチします。

Counter.cs
using UnityEngine;
using UnityEngine.UI;

public class Counter : MonoBehaviour
{
    [SerializeField] private string m_TagName;
    [SerializeField] private Text m_ScoreText;
    private int _score = 0;

    void OnTriggerEnter(Collider collision)
    {
        if (collision.gameObject.CompareTag(m_TagName))
        {
            Destroy(collision.gameObject);
            _score++;
            m_ScoreText.text = _score.ToString();
        }
    }
}

スクリーンショット 2019-12-14 22.56.14.png

10 . Buildして確かめてみます。
Player Settings の Other Settings より以下設定を行います。
Camera Usage Description : (任意の文字列)
Target minimum iOS Version : 13.0
Architecture : ARM64

まとめ

ネイティブでの制作も検討したのですが、
Unityを利用することでAsset Storeを使うことができるメリットが大きかったため、
目的を最も素早く達成できる Unity を選択しました。

クソアプリ制作は、どんなクオリティでも許される免罪符的なイベントだと思います。
腰の重い人にピッタリなイベントです。

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

FirebaseとUnityの連携 入門(Cloud Storage編)

この記事は、Firebase Advent Calendar 2019の15日目の記事です。

概要

以前、『FirebaseとUnityでアプリ開発(ハンズオンみたいなやつ)』という記事をアップし、そこでFirebaseとUnityの連携方法を簡単に解説しました。

今回は特定のFirebaseのサービスを扱う際、まずは0から作るのではなく、サンプルを活用してFirebaseとUnityの連携をいち早く体験できる方法のご紹介です。

Unityの場合、AssetBundleをサーバに配置し、UnityWebRequest.Getなどを使ってAssetBundleデータをダウンロードしますよね。
そのため今回は、Cloud Storageを触ってみることで、ゆくゆくAssetBundleデータをFirebaseで管理する設計イメージができないか、まずはやってみましょう。

環境

  • MacBookPro Mojave 10.14
  • Unity 2019.2.9f1
  • Firebase for Unity 6.3.0

セットアップ

まずは、導入方法を参考にFirebaseとUnityの連携準備は済ませておきましょう。

そしてサンプルコードとして公式が用意しているfirebase/quickstart-unityCloud Storage for Firebase Quickstartを活用します。

一見、FirebaseもUnityも古いバージョンで作られていますが、大幅な変更がない最低限の機能はちゃんと動くので、本格的なアプリへの導入の際の設計・開発で参考にしていきましょう。

Unity側の調整

ですが、活用すると言ってもquickstart-unity/storage/testapp/Assets/Firebase/Sample/Storage/UIHandler.cs
だけ扱うので、クラス名だけ変えてC#スクリプトを作成し、コピペしましょう。

その後、コピペして作ったC#を空のGameObjectにアタッチし、そのInspector上に表示されるGUISkin変数にGUISkinを作成してアタッチしましょう。

スクリーンショット 2019-12-14 23.03.40.png

また、カメラの調整をSkyboxからSolid Colorに変更して、サンプルが分かりやすいように調整しておきましょう。

スクリーンショット 2019-12-14 23.14.04.png

Cloud Storageの設定

次にFirebaseコンソール側の作業になります。

Storageのメニューを開き、事前に画像などをアップし、詳細上から画像のリンクをコピーなどしてメモしておきましょう。

次にルールの設定です。
公式の『Storage セキュリティ ルールを使ってみる』に各ルールの設定サンプルがあり、Authを扱わないので今回は公開のルールを扱います。
この公開のルールは、誰でも読み込みと書き込みが可能な設定なので作業終了後に設定を戻しておくようにしておきましょう(自己責任でお願いします?)。

スクリーンショット 2019-12-14 23.04.33.png
スクリーンショット 2019-12-14 23.08.35.png
スクリーンショット 2019-12-14 23.08.52.png

実行

Local File PathStorage Locationの設定を先ほどメモした内容に書き換えてDownload Bytes/Download Stream/Download to Fileの各ボタンを押してみると以下のようになります。

成功せず、もしパーミッションエラーで403がある場合は、おそらくルールの設定変更忘れだと思います。
また、Local File Pathの変更を忘れているとデフォルトで設定されているdownloaded_file.txtの名前でファイルが生成されてしまいます。

スクリーンショット 2019-12-14 23.15.11.png
スクリーンショット 2019-12-14 23.15.18.png
スクリーンショット 2019-12-14 23.15.51.png

さいごに

あとはAssetBundleさえ準備できればいつでもFirebaseで管理できるようになりそうですね。

ちなみにCloud Storageのファイルサイズには上りと下りで制限があるのか気になりましたでしょうか。
Storage セキュリティ ルールを使ってみる』を読んでいるとルール側でデータサイズを指定して上り下りの制御ができるようです。

ちょっと調べてみたところデータサイズの制限は特にないようなので、普通にAssetBundleのサーバとして扱えそうですよね^^

 

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