- 投稿日:2019-12-15T23:53:51+09:00
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で以下の画面を作成しました。
Hierarchy
ビューを見ていただくと、InputArea
とOutputArea
というオブジェクトがあります。これらはそれぞれ、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
オブジェクトにアタッチしたのが、以下の状態です。いくつか
Inspector
ビューから紐づけているコンポーネントがあります。
InputField
- クライアント側から指定するIDを入力するフィールド.
OutputText
- サーバ側から返ってきたデータの
Name
変数を格納する.MonsterImage
- 取得データのIDを使ってクライアント側に置いてある対応する画像をセットする.
これらは以下のように紐づけています。
最後に、今回はGUIのボタンが押されたらサーバ側に処理を依頼するようにしようと思ったので、
Hierarchy
ビュー上に置かれているボタンオブジェクトに対して、ClientオブジェクトにアタッチしたApiClientクラスが持つGetDataFromAPIServer()
メソッドを割り当てています。これで準備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
のデータを取得してみます。(格納しておいたデータでいうと、スライムが該当します)以下のように入力して「データを取得」を押すと、、、
無事データを取得して、画面に反映されました!(画像データはあらかじめ自分で作ったものを参照してますw)
同じように、IDを
2
にしてやってみます。以下のようにして「データを取得」を押すと、、、IDが2のデータ(ソルジャー)が取得できました!
終わりに
今回、UnityとGoを用いて簡単なAPI通信を実装してみました。
今後は今回学んだことも踏まえながら、より発展的な内容にも取り組んでいけたらなと思っています!ありがとうございました!
参考
golangでREST APIをやってみた①
UnityでHTTPに接続する
UnityWebRequest
UnityWebRequestの使い方【Unity】
Go 言語の値レシーバとポインタレシーバ
- 投稿日:2019-12-15T17:21:46+09:00
UnityでAndroid実機で実行したときに一部環境で画面が乱れる問題と対処法
発生した問題
UnityでAndroidスマートフォン向けの2Dゲームを開発していてゲーム画面のアスペクト比を統一させようとした。
方法はスクリプトでスマホ画面のアスペクト比が基準となるアスペクト比(今回は16:9)と異なる場合、ゲーム開始時にメインカメラのrectを変更してゲーム画面のアスペクト比を統一して余ったところは黒帯を表示させることで対応した。
エディタ上でゲームを実行したところ下の写真のようにカメラ外の左右の余った部分は黒帯が表示されている。
しかし、ビルドして手元のPixel 3aで実行したところ、下の写真のように左右のカメラの表示外の部分が黒帯にならず表示が乱れる現象が発生した。
他の実機端末で実行するとエディタ上で実行したように、余った部分は黒く表示されて同じ現象は発生しなかった。原因
調べたところ下の記事に書いてあるようにカメラの描写外の部分はメモリ上のゴミが残されているかららしい。
Android の特定機種で画面が乱れたら……対処法
画面全体を映す二つ目のカメラを用意してメインカメラの描写外を表示させることによって一番下の画像のように問題が起こってた端末でもエディタ上と同じく表示されるようになった。全体を映す二つ目のカメラの描写の上にメインカメラを描写してるイメージ。二つ目のカメラはCulling Maskをすべてのレイヤーのオブジェクトを表示しない設定にして黒帯の部分にオブジェクトが映るのを回避している。二つ目のカメラのDepthをメインカメラより小さくしないと二つ目のカメラが上に表示されて全体真っ黒な画面になるので注意。(今回の場合だとBackgroundの色を変えることでほかの色の帯にすることが可能)
ゲームでどのようにカメラを使用しているかによって対処法も変わりそう。
さいごに
もし間違えている箇所などがありましたらコメント等で教えてください
参考
- 投稿日:2019-12-15T16:35:14+09:00
【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); } }実行したら、ちゃんと値を渡せていることが確認できます。
ConsoleHello World! UnityEngine.Debug:Log(Object) ScriptB:Start()これだけです。
かなりスッキリしましたね!!すてきなシーンロードライフ(?)を送りましょう!
参考
Load scene with param variable Unity
あわせて読みたい
筆者のブログ:https://nekojokerblog.com
- 投稿日:2019-12-15T11:55:32+09:00
顔を常に映すカメラ(相対位置を保ちたいけど子オブジェクトにしたくない場合)
概要
頭ボーンにカメラ固定しておけば表情の編集しやすかったなぁ pic.twitter.com/tI7TQ5Xxjw
— Dr.千霧?ぶっちぎりP (@bucchigiri) December 15, 2019こういう「絶叫マシンに乗る芸人の表情を映すカメラ」みたいなのを作りたいとして、簡単にやるなら「カメラを頭部ボーンの子にする」だけで良いのですが、ヒエラルキーに配置したモデルのプレハブ中の深い階層に異物を置きたくない場合があります。
そこで、カメラをモデルとは無関係な上位の階層に置き、「指定オブジェクトとの相対位置を保ち続ける」スクリプトをくっつけることにしました。
スクリプト
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; } }使い方
動作原理
スクリプトが
Start
すると、自分自身(=カメラ)と親との間に空オブジェクト(chaser
)を挟み込み、自分はその空オブジェクトの子になります。この空オブジェクトはLateUpdate
の処理で常にParent
と同一の座標と向きを保ち続けるので、カメラは擬似的にParent
の子オブジェクトのように振る舞うことになります。Tips
絶叫マシンに乗ってる芸人の顔はだいたいカメラが寄りすぎて変な顔に映りがちなので、可愛く映したい場合はカメラの
Field of View
を小さくする(と同時に少し距離を取る)と良いです。
- 投稿日:2019-12-15T07:17:01+09:00
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 ) } } }
- 投稿日:2019-12-15T07:17:01+09:00
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 ) } } }
- 投稿日:2019-12-15T01:48:25+09:00
【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
に関する基礎的なところからは触れないのでご了承ください。
(一応参考資料だけ載せておきます。)
- 一歩先のUnityでのパフォーマンス/メモリ計測、デバッグ術
- Unity2018/2019における最適化事情
- 比較的新しい資料。Profilerに関する最新機能などについても言及されている
- 【Unity道場スペシャル 2017京都】最適化をする前に覚えておきたい技術
- 講演動画はこちら
Unity Profiler
に関して凄く分かりやすく解説されている- 数年前の資料故に最新機能などは言及されていないので、上記2点と合わせてみることで補完できるかも
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
Profile Analyzer側でプロファイリング結果を読み込む
次に
Profile Analyzer
側で結果を読み込むために先ずは画面を開きます。
Profile Analyzer
はメニューバーにある「Window -> Analysis -> Profile Analyzer」から開けます。開くと以下の様な画面が表示されるかと思います。
後は注釈の通りに【Pull Dataボタン】を押下して先程記録(若しくはロード)したプロファイリング結果を読み込みます。※分析結果の保存について
【Pull Dataボタン】で読み込んだプロファイリング結果は
Profile Analyzer
側で分析された上で結果が画面に表示されます。(画面の詳細は後述)この時の「分析結果」は【Saveボタン】から
.pdata
としてファイルに保存することが出来ます。
予め分析結果を保存している場合には【Pull Dataボタン】から読み込まずとも、Profile Analyzer
上の【Loadボタン】から読み込むことが出来ます。※こちらは既知の制限としてドキュメントにも記載されており、「.dataと.pdataの両方を保持するのがオススメ」とも説明されています。
Profile Analyzer
の見かた/使い方プロファイリング結果の分析が完了すると以下の様な画面に表示が変わるかと思います。
後はこちらの画面を操作して各種分析を行う形となりますが、機能については全て解説していくと数が多いので...今回はその中にある幾つかの機能を紹介していきます。
複数フレームにまたがる分析
今回のプロファイリング結果は「96フレーム~396フレーム」の計300フレーム分が読み込まれてますが、この中から「110フレから140フレまでの
計31フレーム分
を分析したい」と言った場合には以下の注釈にある領域を操作して「複数のフレーム」を指定します。分析結果は赤枠内に表示
指定範囲の分析結果は主に赤枠内に表示されます。
例えば「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が表示されてます。もちろん全範囲指定も可能
全範囲指定すれば「全フレーム分」をそのまま分析することが出来ます。
→ 選択範囲は以下の赤枠内で確認可能。独自でツールを作ったりせずともシュッと平均値などを分析できるのは便利ですね。
その他、範囲指定に関する情報はドキュメントの「Frame Control and Range Selection」を御覧ください。
項目のフィルタリング
Profilerの処理内容に該当するMakerは文字列指定やスレッド指定などでフィルタリングすることが出来ます。
指定箇所としては以下の赤枠内となります。名称でフィルタリング
例えば「MonoBehaviour.Update全体の負荷」を見たい場合には以下のように
Update.ScriptRunBehaviourUpdate
を【Name Filter : All】で指定することでフィルタリング出来ます。
※今回はUpdateを呼び出す物が1つも存在しないので結果は0msとなっている。横にある【Exclude Names】を指定すればその名称を除外することも可能です。
スレッド単位でフィルタリング
【Name Filter】の下にある【Thread】の項目ではThread単位でフィルタリングすることが出来ます。
デフォルトでは
MainThread
のみが選択されている状態となりますが、以下のようにRenderThread
のみを表示する形にして【Apply】するとRenderThreadに関する情報のみが表示されるようになります。その他
詳細は割愛しますが、他にある機能として【Depth Slice】でスタックレベルでフィルタリング出来たり、【Analysis Type】で表示結果を
Total
orSelf
に切り替えたり出来ます。詳細はドキュメントの「Filtering System」を御覧ください。
★ データの比較
ここからはデータの比較解説用にサンプルを変更します。
対象としては以前自分が作った「VRMSpringBoneのJobSystem対応」をベースにして、
MonoBehaviourベースの実装
からJobSystemベースの実装
2に切り替えた際の差分に注目して解説を進めていきます。
※その他、サンプルの詳細についてはこちらを参照 (クリックで展開)
- 検証内容
- 「ニコニ立体ちゃん VRMモデル」256体を同時に動かした際の
VRMSpringBone
の処理負荷軽減- 実行環境
- Standalone(Windows) + IL2CPP
- CPU : Intel Core i7-8700K (Worker Threadは11本)
VRMSpringBone
のJobSystem対応の詳細については「こちらのスライド」を御覧ください。
ちなみに動作画面は↓になります。※ドキュメント : Compare View
プロファイリング結果の読み込み
注釈の通り、読み込む必要のあるプロファイリング結果は2つ必要になります。
データが用意できたらProfile Analyzer
のModeを【Compare】に切り替えます。データを読み込む方法については「単一データの分析」と同じです。
→ 分析データ(.pdata
)が有るなら【Loadボタン】から読み込み、無ければUnity Profiler
側でプロファイリング結果(.data
)をロードして【Pull Dataボタン】で読み込み。読み込むデータは2つ必要となるので、解説中では以下の前提で進めていきます。
- 最適化前のデータ (MonoBehaviourベースの実装)
- 上の青いボタンでロード
- → 以降、画面中の青色表記は最適化前に該当
- 最適化後のデータ (Jobsystemベースの実装)
- 下のオレンジ色のボタンでロード
- → 同様にオレンジ色表記は最適化後に該当
分析結果
以下に全フレーム分を対象とした分析結果を貼ります。
→ 分析方法については「単一データの分析」と変わりません。注目できるポイントとしては赤線を引いている
LateUpdate
の負荷です。
VRMSpringBone
は数が多い分だけLateUpdate
のMainThread占有率が目立ってしまう傾向があり、最適化後の方と比べると中央値が13.45ms
削減出来ていることが分かります。ただ、もう一点気になるポイントもあります。
箇所としては赤破線を引いているFinishFrameRendering
であり、最適化前と比べて処理が伸びていることが伺えます。
※同様に下にあるGfx.WaitForPresent
なども伸びている。何故伸びた?
折角なので処理負荷の原因を特定する際の一例として伸びた原因についても簡単に追ってみたいと思います。3
今回問題となっている
PostLateUpdate.FinishFrameRendering
は呼び出し階層としては結構上の方に位置しており、これだけだと具体的に「どこの処理が重いのか」が分かりづらいです。4なので、処理を追う際には
Unity Profiler
も合わせて活用する形で追っていきたいと思います。ポイント:
Unity Profiler
も合わせて活用
Profile Analyzer
で見れるのは「分析結果」であり、「時系列で何があったか?」と言った情報についてはUnity Profiler
のTimelineの方が確認しやすいと思います。と言うことで2点のプロファイリング結果のTimelineを見比べてみましょう。
最適化前
(画像だけだと分かり辛いところもあるかもしれませんが...)
Profile Analyzer
の分析結果と合わせてみることで以下の要点が見えてきます。
PostLateUpdate.FinishFrameRendering
の負荷はほぼ一律 (と言うよか目立ったスパイクとかは無い)
- → その上で
Gfx.WaitForPresent
は発生していないGfx.ProcessCommands
がフレーム中に完結している最適化後
こちらも
Profile Analyzer
の分析結果と合わせて見ることで以下の要点が見えてきます。
- 最適化後の方は
PostLateUpdate.FinishFrameRendering
で定期的に負荷が発生
- Timeline上のコールスタックを見ると
Gtx.WaitForPresent
が伸びていることが見えてきた
- ※ 同様に
Profile Analyzer
側でも最適化後のみGtx.WaitForPresent
が伸びているのが伺える- 前フレームの
Gfx.ProcessCommands
がはみ出ている話を纏めてしまうと最適化前と最適化後でMainThreadの処理時間が大分変わってしまっており、それが影響してGPUの実行タイミングにズレが生じてはみ出ている事が分かりました。
CSVへのエクスポート
分析結果は
CSV形式
で出力することが出来ます。「単一データの分析」又は「データの比較」で分析結果を読み込んだ状態で、メニューに有る【Exportボタン】を押下する事でメニューが表示されます。
※ドキュメント : Export Dialog
以下の
CSV
は、上述の「データの比較」の章にて検証した内容をそのまま出力したものです。
まだ具体的な利用方法までは思いついていない段階ですが...自作のツールなり仕組みなりに組み込むと言ったことが可能かもしれません。
その他Tips
分析結果の表示項目の変更
以下の分析結果の表示項目について、こちらは右クリックから変更することが可能です。
- 「単一データの分析」 →
Marker Details for currently selected range
- 「データの比較」 →
Marker Comparation for currently selected range
項目は「単一データの分析」か「データの比較」で変わってくるので、詳細についてはドキュメントをご覧ください。
コンテキストメニューからFiltersにMarker名を追加
【Filters】への追加は直接の入力以外にも、右クリックで表示されるコンテキストメニューからも設定することが出来ます。
やり方としては「追加したい
Marker名
」を選択した後に右クリックで以下のようなメニューが表示されます。
→ 例えばここから「Add to Include Filter
」を実行すると【Name Filter :】に選択したMarker名が追加されます。他にも「
Set as Parent Marker Filte
」を実行すると【Parent Maker :】に選択したMarker名が追加され、表示内容を「指定したMarker以下のコールスタック」に限定することが出来ます。その他、コンテキストメニューの内容はドキュメントを御覧ください。
最後に
まだ
preview package
ではありますが、触れてみて普通に使っていけそうな印象はありました。
※後は実態がEditor拡張であり、ランタイムに含まれないという点も導入しやすい感も。解説は以上となりますが、今回話した内容以外にも色々と使い方は有るかと思います。
他に便利な使用例と言ったものが出てきたら随時アウトプットしていければと思います。
(※私以外にも「こう使ってる」「この用途だと便利」的な情報があれば、どんどんシェア/アウトプットして頂けると幸いです!)
検討事項
色々記載しましたが...言うて
Unity Profiler
には300フレームしか保持できない制限があります...。故に「暫く動かした結果を分析」と言った対応は難しいかもしれません。。
Unity 2019.3
からは表示フレームを最大2000フレームまで引き伸ばすことが出来るので、仮にProfile Analyzer
が対応されていたら活用の幅が広がるかもしれません。(まだ未検証なので要調査...)関連リンク
- About Profile Analyzer(Documents)
- Profile Analyzer のご紹介
- 公式ブログ記事。和訳されている
Profiler基礎
- 投稿日:2019-12-15T01:40:38+09:00
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アカウントの作成
GitLab.comのSign upページに行き、アカウントを作成します。
GitLab.com自身のローカル環境にあるUnityプロジェクトをGit管理下に置く
Gitの使い方は僕なんかよりよっぽど詳しい人達がいるので、その人達を参考にしてください。
GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする
unity3d-gitlab-ci-exampleのclone httpをコピーし、cloneコマンドでローカルに落とします。Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する
「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」欄にそれに対応した値を追加する
黄色くマーカーしたところです。ここには、いつも使用している自分のユーザーネーム(メールアドレス)とパスワードを「Value」に入れています。一番上の「UNITY_LICENSE_CONTENT」はまだ入力する必要がありません。GitLabにUnityプロジェクトをpushする
これでようやくプッシュまでの準備は完了です。一回目のプッシュが終わったあとも、もう少し作業が残っています。もう少しの辛抱です。
生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする
「Job」の中の「get-activation-file」を走らせて、この画面が出たら成功です。「Artifacts」をダウンロードし、「Unity3d.alf」ファイルを取得したら、Unity公式サイトのライセンス確認ページで「Unity3d.alf」ファイルをアップロードします。
Unityライセンス確認ページGitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする
黄色くマーカーした場所です。Valueの値は、テキストエディタで「Unity_v2019.x.ulf」を開いて見ることができる文章です。まるっとコピーして、Valueの中に入れます。これでGitLab-CIで自動ビルドができるようになると思います。以降は、Pushするたびに自動で最新の進捗を実行ファイルにしてくれます。
Unity内でエラーが出る場合
「Assets/Scrpts/Editor」にコピーしたファイルがUnityでエラーを出している場合があります。その時は、エラーを吐いている行をコメントアウトすれば大丈夫です。
さいごに
Unityアドベントカレンダーパート3の15日目の記事はこんな感じです。「Unityが使えないメンバーにも今の開発状況を遊ばせたいなぁ...」という方々への参考になれば幸いです。閲覧いただきありがとうございました。
- 投稿日:2019-12-15T01:40:38+09:00
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アカウントの作成
GitLab.comのSign upページに行き、アカウントを作成します。
GitLab.com自身のローカル環境にあるUnityプロジェクトをGit管理下に置く
Gitの使い方は僕なんかよりよっぽど詳しい人達がいるので、その人達を参考にしてください。
GitLabで公開されている「unity3d-gitlab-ci-example」をローカルにcloneする
unity3d-gitlab-ci-exampleのclone httpをコピーし、cloneコマンドでローカルに落とします。Unityプロジェクトの中に「.gitlab-ci.yml」ファイルをコピーして、不要な項目を削除する
「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」欄にそれに対応した値を追加する
黄色くマーカーしたところです。ここには、いつも使用している自分のユーザーネーム(メールアドレス)とパスワードを「Value」に入れています。一番上の「UNITY_LICENSE_CONTENT」はまだ入力する必要がありません。GitLabにUnityプロジェクトをpushする
これでようやくプッシュまでの準備は完了です。一回目のプッシュが終わったあとも、もう少し作業が残っています。もう少しの辛抱です。
生成されたArtifactsをダウンロードし、その中の「Unity3d.alf」ファイルをUnity公式サイトのライセンス確認ページへアップロードし、「Unity_v2019.x.ulf」ファイルをダウンロードする
「Job」の中の「get-activation-file」を走らせて、この画面が出たら成功です。「Artifacts」をダウンロードし、「Unity3d.alf」ファイルを取得したら、Unity公式サイトのライセンス確認ページで「Unity3d.alf」ファイルをアップロードします。
Unityライセンス確認ページGitlabのSettings→CI/CD→Variablesに「UNITY_LICENSE_CONTENT」を追加し、Unity_v2019.x.ulfの中身をコピーする
黄色くマーカーした場所です。Valueの値は、テキストエディタで「Unity_v2019.x.ulf」を開いて見ることができる文章です。まるっとコピーして、Valueの中に入れます。これでGitLab-CIで自動ビルドができるようになると思います。以降は、Pushするたびに自動で最新の進捗を実行ファイルにしてくれます。
Unity内でエラーが出る場合
「Assets/Scrpts/Editor」にコピーしたファイルがUnityでエラーを出している場合があります。その時は、エラーを吐いている行をコメントアウトすれば大丈夫です。
さいごに
Unityアドベントカレンダーパート3の15日目の記事はこんな感じです。「Unityが使えないメンバーにも今の開発状況を遊ばせたいなぁ...」という方々への参考になれば幸いです。閲覧いただきありがとうございました。
- 投稿日:2019-12-15T00:23:46+09:00
【Unity+ARKit3(+PeopleOcclution)】カラスがゴミ袋を回収するクソアプリを作る
この記事はクソアプリ Advent Calendar 2019 の15日目の記事です。
クソアプリクリエイターの皆さんが知見や学びの深い全然クソじゃないアプリを皆さん生み出していく中、
自分のためだけに勉強がてら新しいiPhone使って作りたいものを作りましたARで認識した床面をタップすると、ゴミ袋が出てきてカラスがせっせと回収に行くアプリです。
ゴミ袋を回収すると、ポイントカウントしていきます。
子供には好評でした。今回制作したものは時短でやろうと思い、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)
使用アセット
- KUBIKOS - Animated Cube Mini BIRDS
キューブ状がかわいい鳥の3Dモデル。1mベースのキューブなのでARでも扱いやすいです(?)- DoozyUI: Complete UI Management System
UI周りのスクリプトを簡単にかつGUIで色々できる。今回はSoundyを使いました。- Puzzle Audio Kit (Music + FX)
サウンドアセット。アイテム取ったときのサウンドに使いました。参考
- 実践ARKit
https://booth.pm/ja/items/1038241- Github:Unity-Technologies/arfoundation-samples
https://github.com/Unity-Technologies/arfoundation-samples- UnityのAR FundationでPeopleOcclusionしてみた
https://qiita.com/Tanktop_in_Feb/items/55201612a8b449800100経緯とアプローチ
手持ちのiPhone Xがグリーンスクリーンになってしまい、iPhone 11 Proを購入したのですが、
前々から気になっていた ARKit3 を試したかったためなにか作ろうと思い立ちました。まず、 ARKit について体系的に知りたかったので、
@shu223 さんの「実践ARKit」でサンプル動かしてざっと眺めることに。
どんな仕組みで動いているのかも詳細に書かれているのでオススメです。
次に 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 を最上位に設定。ちなみになぜか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を使ってみようと思いました
制作のポイント
前述の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.shaderv2f 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をアタッチし以下のように設定します。
5 . 続いて、カメラで表示されている空間にオブジェクトを表示することができるようにSpawn.csを作成します。
Spawn.csとARRaycastManagerコンポーネントをAR Session Originへアタッチ。
Spawnのm_SpawnPrefabには表示したいオブジェクトを適当に用意してアタッチできます。Spawn.csusing 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.csusing 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をアタッチしています。
7 . ゴミ袋と衝突したときゴミ袋を消去し、数えるCounter.csを作成します。
作成後、前述のCrowにCounter.cs、Rigidbodyコンポーネントをアタッチします。Counter.csusing 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(); } } }10 . Buildして確かめてみます。
Player Settings の Other Settings より以下設定を行います。
Camera Usage Description : (任意の文字列)
Target minimum iOS Version : 13.0
Architecture : ARM64まとめ
ネイティブでの制作も検討したのですが、
Unityを利用することでAsset Storeを使うことができるメリットが大きかったため、
目的を最も素早く達成できる Unity を選択しました。クソアプリ制作は、どんなクオリティでも許される免罪符的なイベントだと思います。
腰の重い人にピッタリなイベントです。
- 投稿日:2019-12-15T00:09:07+09:00
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-unityのCloud Storage for Firebase Quickstartを活用します。
一見、FirebaseもUnityも古いバージョンで作られていますが、大幅な変更がない最低限の機能はちゃんと動くので、本格的なアプリへの導入の際の設計・開発で参考にしていきましょう。
Unity側の調整
ですが、活用すると言ってもquickstart-unity/storage/testapp/Assets/Firebase/Sample/Storage/UIHandler.cs
だけ扱うので、クラス名だけ変えてC#スクリプトを作成し、コピペしましょう。その後、コピペして作ったC#を空のGameObjectにアタッチし、そのInspector上に表示されるGUISkin変数にGUISkinを作成してアタッチしましょう。
また、カメラの調整を
Skybox
からSolid Color
に変更して、サンプルが分かりやすいように調整しておきましょう。Cloud Storageの設定
次にFirebaseコンソール側の作業になります。
Storageのメニューを開き、事前に画像などをアップし、詳細上から画像のリンクをコピーなどしてメモしておきましょう。
次にルールの設定です。
公式の『Storage セキュリティ ルールを使ってみる』に各ルールの設定サンプルがあり、Authを扱わないので今回は公開のルールを扱います。
この公開のルールは、誰でも読み込みと書き込みが可能な設定なので作業終了後に設定を戻しておくようにしておきましょう(自己責任でお願いします?)。実行
Local File Path
とStorage Location
の設定を先ほどメモした内容に書き換えてDownload Bytes
/Download Stream
/Download to File
の各ボタンを押してみると以下のようになります。成功せず、もしパーミッションエラーで403がある場合は、おそらくルールの設定変更忘れだと思います。
また、Local File Path
の変更を忘れているとデフォルトで設定されているdownloaded_file.txt
の名前でファイルが生成されてしまいます。さいごに
あとはAssetBundleさえ準備できればいつでもFirebaseで管理できるようになりそうですね。
ちなみにCloud Storageのファイルサイズには上りと下りで制限があるのか気になりましたでしょうか。
『Storage セキュリティ ルールを使ってみる』を読んでいるとルール側でデータサイズを指定して上り下りの制御ができるようです。ちょっと調べてみたところデータサイズの制限は特にないようなので、普通にAssetBundleのサーバとして扱えそうですよね^^