渋谷ほととぎす通信

新しいこと、枯れたこと問わず大庭が興味を持ったものを調査、生活の効率を求める完全趣味の技術ブログ。基礎を大事にしています。

DOTween実行時警告 : An error inside a tween callback was silently taken care of ~~► Index was outside the bounds of the array.の対処法

f:id:esakun:20150825162207p:plain


DOTWEEN ► An error inside a tween callback was silently taken care of (Void \<HogehogeMethod>b__21_0(Int32)) ► Index was outside the bounds of the array.

DOTweenでこういう警告が出て困っていたのですが、DOTweenが悪いわけでありませんでした。(大抵悪いのはコードを書いてる自分です)

DOTweenを実行する直前に配列の例外がスローしてた

transform.DOLocalMoveX(1, 1).OnComplete(()=>_array[範囲外Index]);

このような感じでOnComplete関数の中で例外がスローしているとこのような警告が発生するようです。

注意:実機ではクラッシュする

UnityEditorで実行する際は警告のみ出力されるだけなのですが、実機で実行すると問答無用でクラッシュいたします(iPhoneXで確認済み)。ご注意を。


はい、スッキリ。

環境

  • DOTween 1.2.160
  • Unity2019.2.8f1

開発初期に暇を持て余しているUnityエンジニアができる42のTips前編

本記事は、サムザップ Advent Calendar 2019 #2の12/11の記事です。


株式会社サムザップでUnityエンジニアをしている大庭です。
グラフィックデザイナー、Flashデベロッパーを経て、現在はUnityエンジニアでスマホゲームを作っています。Unity歴は6年程です。


これから紹介していく内容は、開発初期に後回しにされがちな、でも初期にやっておいた方が良い事ばかりです。
僕はUnityエンジニアとしてプロジェクトを進める上で以下の事を重要視しています。

  • 動くものをいち早く作ってメンバーに共有
  • ワークフロー構築、開発手法を整える事が最優先
  • いきなり正解は出ないので、不都合が出てきたら修正していくスタイル
  • 修正見直しの数を繰り返して精度を上げていく

この思想も取り入れた形でリストアップしました。

本記事の導入

新規ゲームプロジェクトがスタートし、あなたはUnityエンジニアとして参画する事になったと想定します。しかし、ゲーム仕様はまだ真っ白な状態で、計画しているモック開発の要件も決まっていません。


「あ〜仕様も決まってないし、やる事がなくて暇だな〜」


って、


んな事ぁない!!


仕様が無くてもやる事はいくらでもあります。


今回のタイトルを見て「42Tips!?多すぎじゃない・・・!?」と思ったあなた!ぜひ読んでみてください。
今まで新規開発に携わる事の多かった僕が思いつく「仕様がなくてもUnityエンジニアが開発序盤にやれる事」をツラツラ紹介していきます。

開発思想・設計系

1.開発環境・開発思想を決める

本記事の読者はUnityエンジニアが多いと思いますので、開発環境はUnityをベースに考えていきます。

開発環境Unityについて

Unityのアップデートに関して、随時追いかけていくのが良いと考えています。バージョンが離れてしまうと、APIのインターフェースに変更が入っていたりして、思わぬ修正が発生する場合があります。ただし、メジャーアップデート直後はバグが多い可能性もあるので、アップデートの通知が来てもすぐにバージョンアップせず少し様子を見る事にしています。


20XX.5、20XX.6辺りまでマイナーバージョンが上がってくると、比較的安心してバージョンを上げる気持ちにさせてくれます。

ソースコードのエディタについて

  • コードフォーマットの共通化に時間をかけたくない
    • 自動でフォーマットする機能がある
  • Unity開発をする上で機能が充実している

ソースコードを書くメンバーは、以上の点を満たすRiderを使うようにしています。 コードフォーマットの設定はカスタマイズする時間がもったいないのでデフォルト状態で使用しています。
ただ、Riderのバージョンによって微妙にデフォルト状態が違うので揃えた方が安全です。

開発思想について

ここはプロジェクト次第で、一概には言えないため、僕がよく採用する開発思想を紹介します。

a.基本はUnityの機能を素直に使うようにする

Unityエンジンの設計思想に反しないように機能を使うようにします。 これはUnityがアップデートした時にトラブルが起き辛いようにする他、後から参画したメンバーが理解しやすくするためでもあります。
※ピーキーにチューニングしたい場合は、置き換え可能な部分に切り出して実装するなど工夫をするのが良いと考えています。

b.巨大なサードパーティフレームワークに依存しないようにする

各メンバーの学習コストが増えて即戦力にならない問題や、採用フレームワークがアップデートされなくなるなどの運用後のリスクを回避するためです。
新しい技術を使った方が便利だったり、一時的な開発効率が上がる場合もあるのですが、開発から運用後の事を想定して採用技術は選定していきたいところです。


新しいものは、その分検証期間が必要ですし、見えていないトラブルもありえます。また、世の中に情報が溜まっていないので余計に時間がかかります。
開発時間に余裕のあるプロジェクトであれば新しい技術の採用を検討する価値はあり、逆に短納期の場合は速度を優先し、枯れた安定した技術を採用するという事も重要だと思っています。

2.Unityプロジェクトディレクトリ構成を決める

何をどこに格納するのが良いかは最初に方針だけでも決めておいた方が良いです。


僕が最近採用する構成は以下です。
※Assetsフォルダ配下の構成です。

┌ AssetBundleModule/ ・・・サブモジュール
    ├ AssetBundles/・・・AssetBundleにビルドされるものを格納
    └ Tools/・・・AssetBundleサブモジュール用のツール(主にアセットのチェッカー、ビューワー、非エンジニア向け機能)
├ Project/・・・アプリに含まれるアセット
    ├ Title/ 
        └ (タイトルシーンのコンテンツが格納される)
    ├ OutGame/
        ├ Common/・・・OutGameの共通アセット
        └ Display/・・・各画面
            ├ Home/ ・・・ホーム画面ソースコード、prefab
            ├ Quest/ ・・・Quest画面ソースコード、prefab
            └ Menu/
        └ Dialog/・・・ダイアログ(ポップアップ)
            ├ Alert・・・アラートダイアログソースコード、prefab
            └ Shop・・・ショップダイアログソースコード、prefab
    ├ Battle/
        └ (バトルシーンのコンテンツが格納される)
    └ Resources/ ・・・ Projectで唯一のResourcesフォルダ。
├ ExternalAssets/ ・・・外部アセット(アセットストアから落としたものとか)
├ Modules/・・・サブモジュール群
├ Preset/・・・プリセットデータ
├ ScriptTemplates/・・・スクリプトテンプレート
├ StreamingAssets/
├ Tools/・・・開発ツール
└ Sandbox/ ・・・テストコード格納
    └ ohba_shunsuke/ ・・・各開発者ごとにディレクトリを切ったテストコード

Resourcesフォルダは無闇に増やさない

どのようなリソースがアプリに含まれるか分からなくなるため、無闇にResourcesフォルダは作らない事にしています。

アセットストアからダウンロードしたものをまとめる

アセットストアからダウンロードしたものは、基本的にAssets配下に散らばってしまいフォルダ構成を汚し、視認性を悪くしてしまうため、ExternalAssetsにまとめています。

ソースコードとprefabを無理やり分けない

よく見かける以下のような区分けはしません。

フォルダ名 内容
Scripts ソースコードを格納
Prefabs Prefabを格納

視認性が悪くなるため、スクリプトとPrefabを別々に分けず、同じディレクトリに入れる事が多いです。機能単位でディレクトリを切ってそこにまとめるのが良いのではないかと思っています。

3..gitignoreの設定

本記事は細かい粒度で書いてしまっているので、当たり前やん!!みたいな事も書いていますが、.gitignoreを設定していなかったプロジェクトにぶち当たった事もあるので一応言及しておきます。
最初から.gitignoreを作成しておきましょう。

4.AndroidManifest.xmlの設定

初期状態ではファイル自体存在しないので、とりあえず用意はしておきます。
ゲーム開発でAndroidのマルチウィンドウに対応する事は少ないと思うので、マルチウィンドウをオフにする処理だけ追加しておきます。

5.コーディング規約を決める

コーディング規約は、メンバー間で宗教戦争みたいなものに発展する場合もあり、地味に時間を取られがちです。
ただコーディング規約決定に時間を割くのはナンセンスなので、Riderデフォルト設定のコーディング規約に準ずるという事で、今のプロジェクトは進めています。

「Riderさんがそう言ってるから仕方ないよね♥」

※ただしRiderのバージョンごとに微妙に設定内容が異なる可能性があるので注意です

6.MonoBehaviourクラスのAwake、Startメソッドは極力使わない

Unity特有のコード規約みたいな話です。

Awake、Startを無闇に使用してしまうと、、、

  • 実行順がわからない(保証されない)※Script Execution Orderを無闇に変更したくない
  • 知らない所で処理が走られてしまうのが嫌だ(管理できない)

以上の理由からMonoBehaviourが提供するAwakeやStartなどのイベント関数は、極力使わないようにしています。
後から入ったメンバーも処理の流れを理解しやすいというメリットにも繋がります。

7.大人数で開発できるような設計

人数が集まれば開発速度、単純に上がるという事はありません。

  • 開発のフェーズ依存
  • 参画するメンバーのスキル依存

以上の理由の他にもう一つ大きな要因として、大人数で開発できる設計になっているか?です。

  • 疎結合な実装になっているか
  • Viewとロジックが別れているか

ソースコードだと上記のような話になってきます。
Unityの場合は、GameObjectの設計も重要です。


例えばアウトゲームであれば、合計で100画面あるUIが一つのPrefabになっていたりすると、20画面同時に修正箇所が発生しても同時に1人しか作業ができません。
※Prefabは競合するとうまくマージされないため


当たり前かもしれませんが、各画面を1Prefabずつ分けておくと良いです。
また、その画面を構成するパーツ要素も細かくprefab化できていると大人数で開発をしやすくなります。


できる限りシーンを触らなくても開発できるようにしておくのも重要です。

8.文字列をソースコードやPrefabへの直書き回避とローカライズ対応

任意の文字列をソースコードやPrefab(Textコンポーネント等)に直書きすると、以下の状況で困ります。

  • 文字列を一括で置き換える必要が出た時大変
  • ローカライズする時にどこで使用しているか探すのが大変

そんな時は、キーと値をセットにしたデータを用意し、そこから文字列を取得するようなヘルパークラスを用意する手が考えられます。

 lang,JP,EN
 yes,はい,YES
 no,いいえ,NO
 cancel,キャンセル,CANCEL
 hp,HP,HP

上記のようなcsvを実行時にロードできるようにしておきます。

 // 日本語をセット
 Localization.SetLanguage("JP");
 var str = Localization.Get("yes");
 Debug.Log(str); // output : はい

このような感じでキーを引数にして文字列を取得するように実装しておくと、文字列がソースコードに散らばらなくなって良いです。


Localization.SetLanguage("EN");とすれば、機械的に英語に文字列が置き換わるようになるので、ローカライズ対応の手助けになるかもしれません。

基盤開発系

9.Unityのシーン遷移基盤開発

ある程度大きなUnityプロジェクトになってくると、シーンを分ける事があります。
分けた際に、そのシーン間を遷移させる仕組みを設計、実装しておくと良いです。

SceneManager.LoadSceneAsync("シーン名", LoadSceneMode.Single);

上記のUnity標準機能を個々に書いていっても実現できますが、シーン間に演出、表現をはさみたい場合、このAPIだけでは不足しています。こういう場合はSceneManagerのラッパークラスを作って対応します。

どんな演出が考えられるか?

前述した通り、演出が挟まるのであれば、その分の余白を残して実装する事が重要になります。

  • Tips表示
  • アセットローディングバーの表示
  • 暗転させる

などなど。


それらを仕様が無いながらも、他ゲーム、今までの経験を参考に拡張できるようなシーン遷移管理クラスを作っておくと後々楽になります。

10.画面遷移システムの作成

先程はシーン to シーンの話でしたが、今回は1つのシーン内での画面遷移についてです。


画面遷移は必ず必要になります。デザイン次第で大きく変わりますが、とりあえず動くものを作っておきます。

 // 全画面をEnumで定義する
 public enum DisplayType
 {
     None = 0,
     Home = 1,
     Quest = 2,
     Gacha = 3,
     Menu = 4
 }

画面をEnumで定義します。

https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/328771/d586912a-db7d-a3ce-411c-4c33b0444013.png

上の画像のように、Enum値と画面prefabを紐付けておきます。

// 画面遷移開始
DisplayManger.Instance.Goto(DisplayType.Home);

上記のようなコードで画面が遷移するように実装しています。
すごくざっくりな話になっていますが、表示中の画面Prefabを指定したPrefabに入れ替える実装をしています。

11.どのシーンからでも開発できるような仕組みの設計

「9.Unityのシーン遷移基盤開発」の内容に関連しますが、どのシーンからでも起動できるようにしておくと、開発の効率は上がります。

それを実現するために、各シーンのエントリーポイントとなる共通クラスを継承したクラスをシーンのルートに配置しておくというルールを設けて実装しています。

例えば

  • タイトルシーン
  • アウトゲームシーン
  • バトルシーン

という3シーン構成であれば、それぞれにSceneEntryクラスを継承したのコンポーネント(TitleSceneEntry、OutGameSceneEntry、BattleSceneEntry)を各シーンのルートに配置します。

シーンエントリーポイントクラスの仕事とは?

  • 通常起動とそれ以外の起動の処理における初期化処理の共通化
  • クラス名の通り、そのシーンのエントリーポイントとして動くようにする(そのために、Awake、Startをあえて使わないようにしてます)
    ※参考 : 「6.MonoBehaviourクラスのAwake、Startメソッドは極力使わない」

以下各シーン毎の起動例を紹介します。

タイトルシーンから起動した場合(通常起動)
  • ユーザーが存在しなければ利用規約のダイアログを出す(ユーザーが存在すれば省略してログイン)
    • 利用規約を許可したらユーザーを作成
  • マスターデータの取得
  • アセットのダウンロード
タイトルシーン以外から起動した場合(非通常起動)
  • ユーザーが存在しなければ、利用規約など出さずにユーザー作成。存在すればログインする。
  • マスターデータの取得
  • アセットのダウンロード

という処理フローになり、通常起動と違って色々省略されています。


これらの処理の分岐をシーンエントリーポイントクラスが担っており、このクラスを使う事でどのシーンからでも起動できるようにしています。

開発効率がとても良くなるのでオススメで、

保守していきたいところです。

12.サウンド再生基盤

ゲーム開発する上ではサウンド再生は必ず必要になります。
開発序盤ではどんな技術を採用するかは決まらない可能性が高いです。

  • Unity標準サウンド
  • CRI

など。


そこで何を採用しても大丈夫なように再生部分を以下のようなインターフェースにしておきます。

 public interface ISoundPlayer
 {
     int Play(string soundName);
     
     void Stop(int id);
     
     void SetVolume(float volume);
     
     void SetMute(bool isMute);
 }

開発初期はUnity標準サウンドで実装しておいて、仕様が決まってきたら必要な技術に内部実装を置き換えていく想定です。

13.動画再生基盤

動画を再生させる可能性がある場合は、事前に再生検証をしておきたいです。
12.サウンド再生基盤の動画版なので内容は割愛します(考え方は同じです)。

一点、ゲーム内設定のBGMボリュームを動画再生時に反映するというのを忘れやすいので注意です。


実機で再生できるかどうかの検証はもちろんですが、動画は比較的ファイル容量が大きくなるため、ファイルをサーバーに置いて、ダウンロード時間の確認(実機で)を早めにしておくとよいでしょう。

14.ゲームエフェクトの再生と再利用機構

バトル中に大量のゲームエフェクトが表示される事が想定される場合、エフェクトオブジェクトを再利用できる機構を事前に作っておくと良いです。

  • 大量のエフェクトを毎度生成するとGCが発生しやすい
  • Instantiateがそもそも処理的に重いため、スパイクの原因になる

ユーザー体感をより良くするためにできる限りエフェクトを再利用できるように設計しておきます。
またバトル開始前のローディング中にあらかじめ必要なエフェクトをロードする機能を追加しておくと良いかもしれません。

15.ローカルファイル保存の仕組み

ローカルに保存するのはよくあるので、事前に仕組みを用意しておく事ができます。 内部の実装は置き換えられるように一旦ラッパークラスを定義します。よく使いそうなメソッドを用意しておきます。

  • SetInt
  • SetFloat
  • SetString
  • SetBool
  • Set<T>
  • GetInt
  • GetFloat
  • GetString
  • GetBool
  • Get<T>

コード例

LocalStorage.SetString("key", "Sumzap");

var value = LocalStorage.GeString("key");
Debug.Log(value);// output : Sumzap

16.外部アセットを想定したロード処理

アプリ外部からアセットをダウンロード、ロードして使用する場合は、アセットバンドルや、今だとアドレッサブルアセットシステム(以下:AAS)を使用すると思います。
開発序盤ではそのシステムが無い、または何を使用するかを決めていない場合もあると思います。
ただ、決まっていないからといって、実装は後回しにすると手戻りが多く発生する可能性があるため、そうならないような実装を最初から心がけたいところです。

インターフェースを利用して抽象化

 public interface IAssetBundleManager
 {
     void Load<T>(string assetBundleName, System.Action<T> onComplete, System.Action onError = null);
     
     void Release(string assetBundleName);
 }

このような外部アセットをロードと解放を実装したインターフェースを定義しておきます。 開発序盤はResources.Loadで実装しておき、後から実際のAssetBundleまたはAASに置き換えていくと良いかもしれません。


後に出てくる32.アセットバンドル名を自動で設定されるようにするであえてアセットバンドルのパスをAssetBundles/Resources/のようにResouresをわざわざ挟んでいるのは、アセットバンドル名とResources.Loadのパスを合わせる事で、本実装時の手戻りを最小限にするためです。

17.Androidバックキー設計

後に回すと面倒なのがAndroidバックキー対応です。


バトル中やバトルのリザルトでは不要な場合がほとんどなので、Androidバックキーが必要になるのは主にアウトゲーム側です。

Androidバックキーを実行した時は以下の処理を想定して実装しておく

  • ダイアログ表示中だったらダイアログを閉じる(閉じると不都合なダイアログが存在するので、分岐できるように設計しておく)
  • 画面遷移システム側で履歴を保持して前の画面に戻る
  • ホーム画面でバックキーが押されたらタイトルへ戻す(遷移履歴を消す)
  • タイトルではアプリを終了する

Androidバックキーの連打対応

画面遷移演出中、API実行中などAndroidバックキーが動いては不都合な状態が存在するので、演出中、API実行中などAndroidバックキーが動いてはいけない状態をフラグとして取得できるようにして、trueだったらAndroidバックキーを押しても動かないようにしておきます。

18.ローカルPush通知の実装

ローカルPush通知もスマホゲーム開発なら必ず実装する機能なので、先に手を付けておきます。 以下のような要件が想定されます。

  • アプリを終了した時、サスペンドした時にローカルPushを指定の時間後に予約する
  • アプリを起動した時、ローカルPushの予約を全て消す
  • 設定からON/OFFできる事が多いので、設定値を取得できるようにする
  • Push通知の許可タイミングを任意のタイミングにできるようにしておく

サクッと無料で実装しておきたい場合は、Unity公式のMobile NotificationsというパッケージがPackage Managerからインストールできます。

続きは後編へ

今回は42のTipsのうち18までを紹介しました。
まだ半分ですが「うへぇ〜〜」と思ったあなた!これでも暇しなくて済みますね。
(さらに僕の個人ブログにて後編へと続きます笑)

そして今回紹介した内容を実装し終えたとしても、それでFIXではありません。
プロジェクトが進む中で見直し、修正して適正な姿に変化させていく必要があります。

まずは手を動かし形にしてみてください。これらのTipsが皆様の開発のお役に立てたなら幸いです。

明日は@kazuhiro1128さんの記事です。

Unity PolygonCollider2DのEditColliderができなくなる条件

UnityのPolygonCollider2Dはインスペクタからドラッグで感覚的にコライダーのメッシュを作ることができます。

f:id:esakun:20191204005621p:plain
ピンクの枠部分をクリックすると、シーンビューにコライダーのハンドルが表示されます。

f:id:esakun:20191204010009g:plain
このようにSpriteRendererで表示している2DオブジェクトのPolygonColliderを感覚的に編集することができます。


ここから本題ですが、とある条件でコライダーのハンドルが表示されなくなるトラブルに見舞われます。

インスペクタを複数立ち上げるとダメ

複数のインスペクタウィンドウをUnityEditor上に作成してしまうと、Unityのバグなのか、うまく動かなくなります。

個人的によくあるケースとしては、インスペクタを複数立ち上げないといけない作業をした後、そのまま放置してしまい、「EditColliderのハンドルが出てこない!!!!」ってなります。

インスペクタ、シーンビュー共に1つずつにしてみる

おそらくEditColliderしたときのイベントがシーンビューにうまく通知していないのかなという予測をたてています。
インスペクタ、シーンビュー共に1つずつにしてあげると、本件のトラブルは避けられるのではないかなと思います。


以上です。

環境

  • Unity2019.2.8f1 (Unity2018.3でも発生していた)

DOTweenをasync/await化して可読性の高いコードにしてみる件(キャンセルにも対応)

この辺りの記事からの続きですが、個人的な開発環境をコルーチンからasync/awaitに移行中です。
移行するにあたって、僕が最もよく使うDOTweenのasync/await化は必須で、特にキャンセル周りは演出を作る上では最重要と考えています。


本記事では安全に使いやすくキャンセルができるかを調査します。

async/awaitを使用したDOTweenサンプルコード

f:id:esakun:20191029135232g:plain

このように2秒動いた後、2秒動きます。
今までSequenceで書いていたようなことがawaitだけでかけてとても可読性が高いコードになります。

キャンセル処理を挟んだasync/awaitのDOTweenサンプルコード

f:id:esakun:20191029135217g:plain
内容としては1つ目のTweenを開始1秒後にキャンセルし、次のTweenを実行させます。

以下のサンプルコードのように、CancellationTokenをTweenにToAwaiterメソッドで渡し、キャンセルしたいときはトークンを発行したCancellationTokenSourceのCancelメソッドを実行します。

DOTweenのasync/await化は拡張メソッドで対応

DOTweenAwaiterExtensionという拡張メソッドを作成しDOTweenのasync/awaitを対応しています。
ToAwaiterメソッドにCancellationToken、キャンセル時の振る舞いEnumを渡すようにしています。

キャンセル時の振る舞いの種類

  • Kill・・・Kill()する
  • KillWithCompleteCallback・・・KillしてOnCompleteコールバックを呼ぶ(要はKill(true)する)
  • Complete・・・Complete()する

参考 : DOTweenのコールバック関数の実行順 - 渋谷ほととぎす通信


経験上この3種類で、大抵まかなえているためこのような実装になっています。

また、キャンセル処理を実行すると例外伝播するため、タスクの実行処理をtry-catchで囲まないと処理が止まりますのでご注意を。


以下DOTweenAwaiterExtensionの全文です。

最後に

コールバックの連鎖になりがちなTweenですが明るい兆しが今回のasync/await化で見えてきました。 CancellationTokenを毎回作らないといけない、try-catchで囲まないといけないというのは、若干面倒臭いですが、そのデメリットを超えたメリットを感じています。

参考

Task.Delayの待機時間をUnityEditor上で計測をしたら少しずれる件

Task.Delayの待機時間が本当に数値通りかを以下のサンプルコードで確認しました。

async void Start()
{
    var t = Time.time;
    var st = Time.realtimeSinceStartup;
    
    await Task.Delay(5000);
    
    // ゲーム時間
    Debug.Log($"Time.time :{Time.time - t}");

    // リアルタイムの時間
    Debug.Log($"RealtimeSinceStartup : {Time.realtimeSinceStartup - st}");
}

期待する結果は、5秒きっかり経過することです。

結果

f:id:esakun:20191028224740p:plain

このようにTime.timeは0.5秒早く、Time.realtimeSinceStartupは約5秒経過をマークしました。 事実ベースではTask.Delayで5秒待機するとゲーム時間は0.5秒ほど早く完了するようです。
※理由が知りたい

待機時間を長くしてみる

Task.Delay(10000);として10秒待機してみます。このときズレは大きくなるのか、一定なのか。

大きくズレていく

f:id:esakun:20191028231510p:plain

約1.5秒ズレました。
この結果から待機時間が長ければ長いほど指定した時間より早い時間で待機時間は終了してしまうことがわかりました。

念のためにコルーチンで確認

IEnumerator Start()
{
    var t = Time.time;
    var st = Time.realtimeSinceStartup;
    yield return new WaitForSeconds(5f);
    Debug.Log($"Time.time :{Time.time - t}");
    Debug.Log($"RealtimeSinceStartup : {Time.realtimeSinceStartup - st}");
}

f:id:esakun:20191028225212p:plain

コルーチンは、ほぼほぼ時間通り待機してくれました。

最後に

特にオチはありませんが、Task.Delayを使用する際は、低負荷でも少しずれる、待機時間が長ければ長いほどズレは大きくなるということを頭に入れておくと良いのかなと思いました。

環境

  • Unity2019.1.10f1

※本記事の計測はUnityEditor上で行いました

C#コンストラクタパフォーマンス比較検証

余程のパフォーマンスチューニングが必要なときしか不要なパフォーマンス検証結果を残しておきます。


インスタンスをnewキーワードで生成する時、コンストラクタに引数を入れるパターンと入れ無いパターンがあります。

// コンストラクタに引数を持たないパターン
new Hoge {x = 24, foo = "shibuya24"};

// コンストラクタに引数を渡すパターン
new Piyo (24, "shibuya24");

これらは、一体どっちが早いの?という検証をします。

準備

以下のようなサンプルコードを用意します。

// 引数付きコンストラクタ
public class Piyo
{
    public int x;
    public string foo;

    public Piyo(int x, string foo)
    {
        this.x = x;
        this.foo = foo;
    }
}

// 引数付きコンストラクタなし
public class Hoge
{
    public int x;
    public string foo;
}

以下のStart関数内にUnity Profilerで計測してみます。
おそらく微々たる差だと思うので、1千万回実行した結果を比較します。

void Start()
{
    int ii = 10000000;
    Profiler.BeginSample("#### a ####");

    for (int i = 0; i < ii; i++)
        new Piyo {x = 0, foo = "foo"};

    Profiler.EndSample();


    Profiler.BeginSample("#### b ####");
    for (int i = 0; i < ii; i++)
        new Hoge {x = 24, foo = "shibuya24"};

    Profiler.EndSample();
}

結果

コンストラクタに引数を持たないパターン

f:id:esakun:20191024133728p:plain

コンストラクタに引数を渡すパターン

f:id:esakun:20191024133813p:plain

引数なしコンストラクタの方が、約5%高速ということがわかりました。

なぜ速度に違いがあるのか?

こういうときはhttps://sharplab.ioでILにしてみます。
IL全文はコチラ

呼び出しの処理

f:id:esakun:20191024134932p:plain

そこまで大きな差はない。

コンストラクタ引数なしのコンストラクタ処理

f:id:esakun:20191024135042p:plain

コンストラクタ引数ありのコンストラクタ処理

f:id:esakun:20191024135114p:plain

明らかにコンストラクタ引数ありのコンストラクタの行数が多くなっています。
この辺りが影響しているのではないかと勝手に思っています。

また、このILコードから引数が多ければ多いほど負荷が上がるのではないかという推測が出来ますが未検証です。

最後に

大量にインスタンスを生成するところで、使用可否のジャッジをすれば良いますし、微々たる差なのでそもそも考慮しなくても良いかもしれません。個人的にはILの違いを知るきっかけになったので良かったです。

async/awaitを使う上でタスクのキャンセルは例外処理であるという考え方に移行する必要がある件

この辺りの経緯から基礎的なところからUnityを使う上でのasync/awaitを学んでみます。

コルーチンと違って、戻り値を取得できる、コールバックが不要になるなど、便利だ便利だと周りが誘惑してきますが、個人的にはキャンセル処理が面倒臭いという点が一番気になっています。


複雑なシステムほどキャンセル処理は重要ですし、バグの温床になります。本記事だけでは終わらないと思いますが、キャンセル周りはしっかりと検証していこうと思っています。


本記事ではシンプルなタスクの実行とそのキャンセルについて調査しています。

Task.DelayでN秒待機する

async/awaitを学ぶ上でのテストコードとして、Task.Delayを使ったタスクを使っていきます。

実行すると5秒待機して Complete というログが出力されるショートコードです。

Task.Delayをキャンセルする

ボタンが押されたらタスクがキャンセルするという仕様で先ほどのTask.Delayをキャンセルさせます。

キャンセル実装するタスクに対して、 CancellationTokenSource を生成しTokenプロパティでトークンを発行し、そのトークンをDelayメソッドの第2引数に渡します。

実際にキャンセルを実行させるときは、 CancellationToken.Cancel() を実行することでキャンセルされます。

自前のタスクをキャンセルする

Task.DelayはCancellationTokenを引数に渡すだけでしたが、自前のカスタムタスクをキャンセルさせるためには別の処理が必要になります。

キャンセルをチェックするタイミングで CancellationToken.ThrowIfCancellationRequested() を実装します。

キャンセル処理は例外扱いなので注意

CancellationToken.Cancel()OperationCanceledException がスローするので、try-catchで例外をキャッチしておかないと以下のようなエラーが出力してプログラムが止まります。

TaskCanceledException: A task was canceled.
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.GetResult () (at <7d97106330684add86d080ecf65bfe69>:0)
AsyncAwaitTest+<ExecuteWait>d__3.MoveNext () (at Assets/AsyncAwaitTest.cs:37)
--- End of stack trace from previous location where exception was thrown ---
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd (System.Threading.Tasks.Task task) (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.TaskAwaiter.GetResult () (at <7d97106330684add86d080ecf65bfe69>:0)
AsyncAwaitTest+<Start>d__2.MoveNext () (at Assets/AsyncAwaitTest.cs:21)
--- End of stack trace from previous location where exception was thrown ---
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw () (at <7d97106330684add86d080ecf65bfe69>:0)
System.Runtime.CompilerServices.AsyncMethodBuilderCore+<>c.<ThrowAsync>b__6_0 (System.Object state) (at <7d97106330684add86d080ecf65bfe69>:0)
UnityEngine.UnitySynchronizationContext+WorkRequest.Invoke () (at /Users/builduser/buildslave/unity/build/Runtime/Export/Scripting/UnitySynchronizationContext.cs:115)
UnityEngine.UnitySynchronizationContext:ExecuteTasks()

タスク実行時には必ずtry-catchを使う

try
{
   // tryで囲ってタスクを実行する
    await ExecuteWait(_cts.Token);
}
catch (OperationCanceledException e)
{
    // タスクがキャンセルされた時の処理を書く
}

キャンセルするタスクは、上のような感じでtry-catchを使うことになり、OperationCanceledExceptionをキャッチしたらキャンセル処理を実装するということになります。

コルーチンとは180度考え方を変える必要あり

今までコルーチンでキャンセル処理を実装するときは、StopCoroutineを使う、または紐づくGameObjectを非アクティブする、または破棄するといった複数の手段が存在しました。


しかしasync/awaitは、CancellationTokenSourceのCancelメソッドを呼ぶ1択です。

今までUnityで体に染み付くほどコルーチンを使っていた身としては、


「タスクのキャンセルは例外処理である」


と、考え方を大きく変える必要があるなと思いました。

初心者向けUnity Shader Graphの始め方

f:id:esakun:20150730215258g:plain:w450

ノードを繋げてシェーダを作成することができる、Unity純正のShader Graphを始めてみました。

環境セットアップ(Unity2019.1.10f1の場合)

f:id:esakun:20190918020203p:plain:w450 新規プロジェクトで始める場合、Unity HubからLightWeight RPを選んだ状態でプロジェクトを作成するとスムーズにShader Graphを始められます。


参考 : UnityHubが過去バージョンのUnityをインストールできるようになっていた - 渋谷ほととぎす通信


Shader Graphを利用するためにはScriptable Render Pipeline(以下:SRP)を利用する必要があります。
そこでLightWeigh Render Pipeline(以下:LWRP)を使用します。

LWRPはUnityが提供する汎用性の高いSRPです。基本はこれを使ってカスタマイズしていくのが良いでしょう。LightWeightという名前とは裏腹にそこそこの絵作りが可能で高機能です。


ちなみに、先日のCEDEC2019のセッションでも発表されていましたが、Unity2019.3ではLightWeightというキーワードはUniversalという名前にリライトされUniversal Render Pipeline (以下:URP)になる予定です。

シェーダファイルの作成 CreateからUnlit Graphを選択

f:id:esakun:20190918021515p:plain 今回はUnlitなシェーダを作成するので Create > Shader > Unlit Graph からシェーダを作成します。

f:id:esakun:20190918021745p:plain

作成したシェーダファイルをダブルクリックするとShader Graphが起動します。

Timeでアニメーション

f:id:esakun:20190918022216g:plain Create Node > Input > Basic > TimeからTimeノードを追加します。
こんな感じでTimeのサイン波プロパティをカラーに適用するとこんな感じで白と黒を行き来するシェーダができます。


f:id:esakun:20190918024135g:plain

シェーダにテクスチャを追加する方法

シェーダのインプット情報として任意のテクスチャを追加する方法の説明です。

f:id:esakun:20190918032531p:plain

外部からテクスチャをセットする場合はShaderGraph左上のBlackboardから追加します。

f:id:esakun:20190918024313p:plain

Blackboardが表示されていない場合

f:id:esakun:20190918024332p:plain 表示されていない場合はShader Graph右上のBlackboardをクリックすると表示されます。

Texture2Dプロパティの追加

f:id:esakun:20190918024522p:plain プラスボタンをクリックすると、追加可能なプロパティが表示されるのでテクスチャを追加する場合はTexture2Dを選択します。

f:id:esakun:20190918024720p:plain

D&Dでプロパティノードを追加

f:id:esakun:20190918024854g:plain

BlackboardからD&DするとTexture2Dのノードが追加されます。

Texture2Dをシェーダに適用する

f:id:esakun:20190918030344j:plain:w120

こちらのテクスチャをフェッチして表示させてみます。

f:id:esakun:20190918025656p:plain まずはサンプルとなるテクスチャをBlackboardにセットしておきます。ここでセットしたテクスチャはあくまでシェーダの確認用になります。Materialにセットする際は再度テクスチャをセットする必要があります。

f:id:esakun:20190918025753g:plain

このようにTexture2Dノードから何もないところへD&Dするとノード作成メニューが出てくるので、Input > Texture > Sample Texture2Dを選択します。

f:id:esakun:20190918030308p:plain

Sample Texture2DノードのRGBAプロパティをUnlit Masterのカラーへ繋げればテクスチャのカラーをそのまま反映することができます。

いろいろ遊んでみる

f:id:esakun:20190918031228g:plain

Shader Graphには様々なノードが実装されています。最後に何も考えずに遊んでみました。

ノードはこのような感じでSimple Noiseでノイズを作っています。 f:id:esakun:20190918032735p:plain

最後に

基本的なShader Graphの使い方はわかりました。ノードを繋げるだけでシェーダが作成できるのは思った以上に簡単でした。 ただし、簡単だと思うのはシェーダを書いたことがあるエンジニア側の意見であって、ノンプログラマーにとってはとても難しいことだと認識しています(コードを直書きするよりは楽だと思うけど)。


今後の方針としては、数あるノードの把握、使い方を理解していこうと思います。ネットには作例がゴロゴロ転がっているので、それらを参考に作っていく、または今までShader Labで作ってきたシェーダをShader Graphで置き換えてみるというのも良い勉強になりそうです。


ただし置き換えが完了したとしても、僕の主戦場であるスマホ環境でどのくらいパフォーマンスがでるシェーダがジェネレートされるのかが気になりポイントです。

実戦投入する上で、この辺りは慎重に計測していく必要があるかなと思います。


とはいえLWRP(URP)の時代は来そう(現にポストモーテムがCEDEC2019で発表されている)ですし、Shader Graphを使いこなすことでクリエーターとの協業ができるようになり、プロダクトのクオリティを上げられるかもしれないので頑張って勉強していこうと思います。

環境

  • Unity2019.1.10f1
  • macOS Mojave 10.14.5

CEDEC2019気になったUnity情報、直接中の人に質問しにいったこと編

f:id:esakun:20190902025818p:plain

タイムシフト、スライド、動画が公開されるのがほぼ当たり前になりつつある、CEDECやUniteにおいて、現地に行って参加するメリットの1つは、発表者や中の人と直接話しができることです。Unityのロードマップの中で気になっている以下の2点についてUnityの中の複数人にヒアリングした結果を残しておきます。

  1. Addressable Asset System(以下:AAS)の登場でAssetBundleは消え去ってしまうのか?
  2. Entity Component System(以下:ECS)の登場でGameObjectは消え去ってしまうのか?

Addressable Asset System(以下:AAS)の登場でAssetBundleは消え去ってしまうのか?

結論から言うとAssetBundleはなくならないです。
理由はAAS自体がAssetBundleの上に乗っかるいわばラッパー的な存在だからです。


ただし、今後のアップデートによってAssetBundleをECSに対応させたりする可能性があるので、ファイルフォーマット自体に変更が入ると思われますので、その際は全ビルドが必要になるかもしれません。全ビルドしなくてもよいのが理想です...


Scriptable Build Pipelineでビルドした方が、無駄なものがくっついてこないとの事で、旧来のAssetBundleのビルドパイプラインより性能が良いようです。現状のAssetBundleシステムを使う上でも、ビルドフローをScriptable Build Pipelineにした方が良いかもしれないとのことでした。


またUnity社としてもAASに力を入れていくため、新しいフィーチャーはAASに入っていくため、安定してきたらAASを使うべきなのだろうと思いました。

Entity Component System(以下:ECS)の登場でGameObjectは消え去ってしまうのか?

個人的に一番気になっている案件ですが「消え去りますん」という曖昧な答えです。

GameObjectがECSに全て置き換わるには、かなりの段階を踏む必要がありそうです。

  1. ECSが現状のGameObjectベース開発と同等の機能要件を満たす
  2. ECSの開発が現状のGameObjectベース開発級に簡単なオペレーションになる
  3. ECSの開発事例が増えていく
  4. Unity界隈的にECSだけでもうイイんじゃない?っていう雰囲気になる
  5. 少しずつGameObject系のAPIが非推奨になっていく

Unityは古い機能をかなりの期間残しておくので、当分の間GameObjectが消えることは無いと思われます。しかも現状作られているプロダクトのほぼ全てがGameObjectで作られているため影響範囲もかなり大きいです。


AASと同様ですが、UnityはECSに力を入れているため新しいフィーチャーはECSに追加されていくと思われます。 いつ頃「Unity界隈的にECSだけでもうイイんじゃない?っていう雰囲気になる」という状態になるかは分かりませんが、頃合いを見計らってECSに移っていく必要があるのだろうなと思います。


とりあえずここ1〜2年で何かが起きることはなさそうです。

最後に

気になった情報を直接ブースで聞けるのはありがたかったです。これ以外にもUnityのセッションで理解できなかったことも聞かせて頂きました。
繰り返しになりますが、こういう事ができるのが現地に行くメリットだなと思います。

参考サイト様

CEDEC2019 Unityミニセッション気になった事まとめ

f:id:esakun:20190902025818p:plain
CEDEC2019では休憩時間中にUnityブースでミニセッションが行われていました。計6回行われた内容の中で特に気になったものをまとめておきます。

ちなみにミニセッションはTwitterで告知されていました。

通称、裏CEDECです。

UnityのMultiPlayサービスの得意なこと

話を聞く限り、Photon使う必要ないじゃん?って思う内容だったけど、世界規模でスケールするような案件でないと金額的に高くついちゃうのかなという予想。


マルチプレイゲームを作り際の手段の1つとしてチェックしておきたいです。

動画でみせます!2019年の大人気アセットを徹底紹介!

こちらはメモしきれなかったので、資料公開された際に確認します。

Unity Distribution Potal(略してUDP)をつかってAndroidアプリを世界の市場へ

Unity Distribution Portal (UDP)|プラットフォームを越えてゲームを配布。

あまりUDPについて知らなかったのですが、Androidアプリを世界のプラットフォームで展開するサポートをしてくれる無料サービスです。対応プラットフォームは増えていくので、一度試してみたいところです。


1点問題点として、UnityAdsなどの広告は動きはするが、広告のリンク先はGooglePlayになるので、配信プラットフォーム側が同判断するか、ここは開発者の判断になりそうです。


このセッションの中で、Unity公式ローカライゼーションツールの存在を知りました。

その名もLocalization Toolsです。 上のキャプチャの通り、Manifestを手書きで変更する必要ありますが、触れはします。Unity2019以降対応です。

多機能ボイチャをお手軽に導入する方法

UnityのグループになったVIVOXというサービスの紹介でした。

f:id:esakun:20190908233030p:plain:w450

f:id:esakun:20190908233153p:plain:w450

ボイチャを使ってくれた人は、平均ゲームプレイ時間 2倍、継続率5倍になるという結果が出ているようです。

音声からテキストへの変換機能もあるようです。

とりあえず始めてみるには、以下のページから。

参考リンク

セルシェーダを使った、高品位な「セルルック」の作り方

スライドの写真で残しておきます。

f:id:esakun:20190908233324p:plain:w450 全部モデルは同じで、シェーダ違いです。左上がつにティちゃんシェーダで、右下がユニティちゃんトゥーンシェーダー2.0です。

f:id:esakun:20190908233335p:plain:w450 f:id:esakun:20190908233400p:plain:w450 f:id:esakun:20190908233411p:plain:w450 f:id:esakun:20190908233423p:plain:w450 f:id:esakun:20190908233437p:plain:w450 f:id:esakun:20190908233514p:plain:w450 f:id:esakun:20190908233523p:plain:w450 f:id:esakun:20190908233544p:plain:w450 f:id:esakun:20190908233614p:plain:w450 f:id:esakun:20190908233635p:plain:w450

f:id:esakun:20190908233648p:plain:w450 f:id:esakun:20190908233718p:plain:w450 f:id:esakun:20190908233732p:plain:w450 f:id:esakun:20190908233751p:plain:w450 f:id:esakun:20190908233802p:plain:w450 f:id:esakun:20190908233821p:plain:w450 f:id:esakun:20190908233844p:plain:w450 f:id:esakun:20190908233855p:plain:w450 f:id:esakun:20190908233909p:plain:w450 f:id:esakun:20190908233947p:plain:w450 f:id:esakun:20190908234005p:plain:w450 f:id:esakun:20190908234014p:plain:w450 f:id:esakun:20190908234038p:plain:w450 f:id:esakun:20190908234055p:plain:w450 TimelineとCinemachine、PostProcessingStack v2で調整。

f:id:esakun:20190908234108p:plain:w450 f:id:esakun:20190908234322p:plain:w450 f:id:esakun:20190908234333p:plain:w450 f:id:esakun:20190908234343p:plain:w450

続きはUniteTokyo2019とのことです。

堅実なUnityバージョンの遊び方

とりあえず、プロジェクトに人が増えそうになったタイミングでLTSにしておく、以上。

最後に

f:id:esakun:20190908234806p:plain:w450

Unityブースで毎日ホットドッグ、おにぎりの配給ありがとうございました。美味しかったです。

CEDEC2019気になったUnity情報主に2019.3以降の新機能編

f:id:esakun:20190902025818p:plain

CEDEC2019に参加してきて自分なりのレポートを残している最中です。 本記事は「Unity2019年注目機能まとめ」を元にUnity2019の新機能について気になったことをまとめています。ただし、基本的にUnity2019.3以降の機能です。

Universal Render Pipeline(以下:URP)

  • ポストエフェクト対応
  • UTS2も対応(UnityChanShader V2)

旧LightWeightRenderPipelineの事です(名前が変わっただけ)。 基本的な描画についてはカバーし、レンダリングパイプラインをカスタマイズできるというメリットもあり、今後の開発ではURPを使う場面が多く出てくる気がしました。

ShaderGraphのサブグラフ

ShaderGraphとは別アセットのサブグラフの軽い紹介がありました。
便利そうなスニペット的なノードを追加することができるようなアセットでした(多分)。
とても良さそうな印象は受けております。

VisualEffectGraph

Unityの大前さん的に使った方が良い機能として上がっていたので取り上げています。

Houdiniと組み合わせるとなお面白いという話を聞き、早速CEDECの本屋で衝動買しました。

keijiroさんのリポジトリを見るのが手っ取り早いかもしれません。

Profile Analyzer

複数フレームまたがる分析、2つのキャプチャデータを比較することができるすぐれものです。どんどん活用すべきツールでしょう。

Runtime Animation Rigging

既存のアニメーションにUnity上で新たにリグを追加して、アニメーションを分岐させたりできるリグ機能です。ワークフロー構築の手段の1つとして一度触ってみておきたい機能です。

AssetDatabaseV2

AssetDatabaseが大幅に改善されるらしいです。

アセットインポートのプラットフォーム切り替えが高速化

今までキャッシュサーバで頑張っていたアセットインポートですが、プラットフォームごとのインポート情報が保持される?だったような、とにかくSwitch Platformが高速になるようです。

普通にありがたい。

オンデマンドインポートが実装され、再生時に必要なインポートのみ走る機構

こちらは個人的に特に注目している機能です。
よくあるUnityで困るパターンとして、長い間開発しているUnityプロジェクトを新規で開こうとすると、アセットインポートで一日潰れることがあります。


しかし、このオンデマンドインポートは、そのシーン、再生時に必要なアセットだけをインポートする機能らしいです。これにより、長時間待機の苦痛から開放される可能性があります。期待大です。

2Dワークフロー

  • Photoshopワークフロー(レイヤーを読み込める)
  • 2D描画のShaderGraph対応
  • 2D描画のURP対応

Terrain Tools

  • Terrainに足りない機能を補ってくれるツール
  • Terrain Colliderに穴を開けることができるようになった
  • 複数のTerrainをなめらかに繋げられるようになった

イマイチだったTerrainがパワーアップします。
CEDEC2019: Unityではじめるオープンワールド入門 アーティスト編 こちらのセッションで、 Terrain、Houdini、Maya、Substance Designerを使ったワークフローが紹介されており、とても参考になりました。

Burst Compilderで浮動小数点のゆらぎをなくす

f:id:esakun:20190908211110p:plain:w450

BurstCompilerを使うと、プラットフォームごとの浮動小数点のゆらぎ、物理演算によるゆらぎがなくなります。覚えておきたいテクニックです。

ARFoundation

  • AR開発をUnityのシーンビューを活用
  • フェイストラッキング
  • 2Dイメージトラッキング
  • ARKitオブジェクトの認識

Unity公式ブログにも紹介されています。

最後に

僕が気になった機能を取り上げていますので、資料公開の際にはその他の機能のチェックをしていただければと思います。今使うなら覚悟を持つ必要があり、痛みを生じる機能群の紹介もされていて面白かったです。

  • UIElements
  • New InputSystem
  • UIBuilder

この辺りです。


Unity2019.3からはUnityEditor自体のデザインもフラットになり、新機能も多いので非常に楽しみです。

CEDEC2019気になったUnity情報DOTS編

f:id:esakun:20190902025818p:plain CEDEC2019に聞き手として参加させて頂き「Unityではじめるオープンワールド入門 エンジニア編」セッションを元にDOTS関連についてまとめます。

そもそもDOTSとは?

簡単に紹介すると、DOTSとは将来のスタンダードなUnity開発手法になると思われるフレームワークです。

  • NativeContainer
  • Entitiy Compmnent System(以下:ECS)
  • C#JobSystem
  • Burst Compiler

これらの技術を使い、実行時のロード高速化、大量のオブジェクト、GC軽減などの恩恵にあずかることが出来ます。

DOTSと既存システムは組み合わせて使用

セッションではオープンワールド風のステージ + キャラ1体という構成のDEMOをベースに技術紹介されています。

オープンワールド風DEMOのざっくり仕様

  • 1km四方のステージ
  • 主にTerrainで作成(その他Houdini、Maya、SubStance Designerを使用)
  • 4万オブジェクト(LOD含めると21万オブジェクト)
  • 全体で1200万ポリゴン

f:id:esakun:20190908032218p:plain:w450

オープンワールドなので、生い茂った木や草が大量に配置されていて、草は風に揺れている風に動いています。 21万個のGameObjectを動かすというのはまず無理なので、それをDOTSだったらグリグリ動かせる、またモバイルでも動きますよというDEMO担っています。実機のiPhoneXでも30FPS出ていました。

DOTSは草や木のみ使用

オブジェクト タイプ
草・木 エンティティ
キャラ GameObject

ということで、全てのオブジェクトがECSということではなく、適材適所で使用されていました。
エンティティとは、ECSの世界で言うGameObject的なものでGameObjectから機能をそぎ取ったものです。

ECSを少しでも触ったことがある人ならわかると思いますが、現状のUnityエディタにおけるECSの扱い方はとてもハードモードでして、現状のGameObjectベースの開発と比べると、Unity社が掲げている思想「ゲームを民主化する」には程遠い状態です。

ということで、たくさん配置するものはECS、それ以外はGameObjectで作るというのが、現状の選択肢になるのではないかと思います。

シーンビューを活用したECSワークフロー

Unityの良いところの1つはエディタが強力な点です。
ECSで開発するときもエディタのシーンビューを使って、画面を見ながら開発したいため(その方が作りやすい)、SubSceneを使用したワークフローの紹介がされていました。

GameObjectをエンティティに変換したSubScene

f:id:esakun:20190908021244p:plain:w200

Hierarchyから任意のGameObjectを選んでコンテキストメニューからNew SubScene From SelectionをクリックするだけでGameObjectはエンティティに変換されます。


4万個のオブジェクトを一つ一つSubSceneにするわけにはいかないので、ステージを4分割し、4つのSubSceneを作成しています。
4分割の理由はTerrainを4つ使っているので、その単位でSubSceneにしているようでした。

ちなみにSubSceneに変換すると変換前のGameObjectは、変換したSubSceneの子階層に入ります。

SubSceneに変換したGameObjectに更新が入った場合のワークフロー

SubSceneに変換したGameObjectに更新が入った場合は、元のGameObjectを修正してSubSceneのInspectorのRebuild Entity Cacheボタンを押すだけで更新が入ります。
f:id:esakun:20190908021121p:plain:w450

これで、アーティストとの作業分担はできると思います。

【Unity】Scene上に構築したステージを、Entity群に変換してECSで利用可能にする「SubScene」 - テラシュールブログ
SubSceneについてはこちらの記事をご確認ください。

SubSceneになったことによる良い副作用

4万個のオブジェクト(LOD含めると21万個)をECSを使わなかった場合、実行するだけでもとても時間がかかることが想定されます。理由としては21万個のGameObjectのデシリアライズが入るためです。
エンティティ(SubScene)に事前に変換しておくことで、最適化されたメモリ配列になった状態でオブジェクトがロードされるため高速になります。
比較DEMOでもゲーム実行するまでの時間はECSの方が圧倒的に早かったです。


こういうところがECSの良いところなのでしょう。

現状SubSceneの外部リソース化には難あり

SubSceneに変換したオブジェクトは、以下のディレクトリに格納されます。

  • Assets/EntityCache/Resources/
  • Assets/StreamingAssets/EntityCache/

それらを外部リソースとして扱うためには、色々と頑張らないといけないようです。 そもそもStreamingAssetsに入っているバイナリはAssetBundleには出来ないので、そのままリモートにアップロードし、Resources配下のファイルはAssetBundleにしてアップロードします。

問題はパスで以下のEntityScenesPathsクラスにベタベタとStreamingAssetsResourcesと書かれているので、この辺を修正してパスを修正する必要がありそうです(未検証)


※確認バージョン Entity v0.1.1 preview

public static string GetLoadPath(Hash128 sceneGUID, PathType type, int sectionIndex)
{
    if (type == PathType.EntitiesSharedComponents)
        return $"{sceneGUID}_{sectionIndex}_shared";
    else if (type == PathType.EntitiesHeader)
        return GetPath(sceneGUID, type, "");

    var path = GetPath(sceneGUID, type, sectionIndex.ToString());

    if (type == PathType.EntitiesBinary)
        return Application.streamingAssetsPath + "/EntityCache/" + Path.GetFileName(path);
    else if (type == PathType.EntitiesSharedComponents)
        return Path.GetFileNameWithoutExtension(path);
    else
        return path;
}

まだまだPreviewなので、変更されるかもしれません。

端末が熱くならないようにGPU負荷を下げるTips

経緯としてはiPhoneXで動かしたら実機が異常に熱くなり、CEDEC会場で展示するということも踏まえ対策をされたようです。

プロファイルするとGPUに異常な負荷がかかっていたとのことで、以下それを解消するためのTipsです。

VertexShaderがクソ重い問題

VertexShaderが72ms使用していたようで、原因は草などに使用されているカットアウトでした。 ということで、カットアウトをやめるという修正を入れました。モバイルとは相性が悪いカットアウトです。

OnDemandRendering.renderFrameIntervalで描画処理をスキップ

ここでも取り上げたOnDemandRendering.renderFrameIntervalが活躍しています。

実機は展示されるため、触っていないときには負荷を下げて端末の熱が上がらないようにしたいところ。そこでOnDemandRendering.renderFrameIntervalを使い、画面に指がタッチしていないときには描画処理をスキップさせて負荷を下げ、無駄な発熱を抑えることが出来たようです。

Graphics Jobsを使用

f:id:esakun:20190906021623p:plain:w450

PlayerSettingsのGraphicsJobsにチェックを入れます。 するとGPUの処理をRenderThreadに移すことができ、GPU負荷を下げることができます。

CanvasのScreenSpaceOverlay使用しない

CanvasのScreenSpaceOverlayを使用すると毎フレームGPUが動いてしまうため、使用しないことでGPU負荷を下げる、というかGPU負荷を上げないようにしたとのこと。

解像度を下げた

今回実機がiPhoneXなので、DynamicResolutionが対応していたため(Metalは対応している)、解像度を下げてGPU負荷を下げたようです。

その他のトピックス

LightWeight Render Pipelineの名称が変更

Unity2019.3からUniversal Render Pipelineに変更になります。LightWeightという名前がしょぼそうという印象を与えてしまうかららしいです。

f:id:esakun:20190908032133p:plain:w450

最後に

DOTSを実践的に使用したポストモーテム的なセッションで、多くの知見が得られました。
しかし、ECSはこれからもAPIが変わっていきそうな雰囲気がするので、実戦投入はまだまだ先かなという印象です。個人的に遊んでみるのには良いかなと。
また、このオープンワールドのUnityプロジェクトは公開したいと言っていたので、いつかアップされるかもしれません。

CEDEC2019気になったUnity情報パフォーマンス編

f:id:esakun:20190902025818p:plain CEDEC2019に聞き手として参加させて頂き「Unity2018/2019における最適化事情」セッションを元にパフォーマンス関連についてまとめます。

既存機能のアップデート

f:id:esakun:20190907223335p:plain:w450 既存機能アップデートに関するスライドですが、気になったものだけ取り上げて説明します。

AnimatorのGameObject非アクティブ化時のリセット対策

※Unity2018.1から対応

Animator.keepAnimatorControllerStateOnDisable APIのことでこれをtrueにすると非アクティブにしてもステートが残ります。デフォルトfalseで、非アクティブ時にステートのバッファをクリアしているようです。

Animator-keepAnimatorControllerStateOnDisable - Unity スクリプトリファレンス

ParticleSystemのGPU Instancing対応

※Unity2018.1から対応

通常WorkerThreadのParticleSystem.GeometryJobでParticleSystemのメッシュ結合処理をしています。CPU側でメッシュの結合をしてGPUに送り、パーティクルを表示させています。 f:id:esakun:20190907231343p:plain:w200

GPU Instancingを有効にすると、CPUで処理しているメッシュ結合がなくなり、GPU側でコピーされるようになり、処理の高速化が期待されます。
※GPU Instancingを有効化するとWorderThreadのGeometryJobはいなくなります。


RenderModeがMeshの時かつ、Enable Mesh GPU Instancingにチェックが入っていないとGPU Instancingは有効になりません。

f:id:esakun:20190907234958p:plain:w450

またUnityが用意しているシェーダ(Particle/Standard Unlitなど)なら対応していますが、自作で書いているものはそれに倣って修正する必要があります。

【Unity】パーティクルをGPU Instancingで描画してみる & 対応シェーダーを自作してみる - テラシュールブログ
詳しくはこちらの記事で。


PhysXのアップデート

f:id:esakun:20190907223350p:plain:w450

f:id:esakun:20190907214902p:plain:w450

スライド画像の通りです。

Profilerのアップデート

f:id:esakun:20190907215014p:plain:w450

上図のProfilerアップデート概要の通りProfilerが大規模にアップデートされています。
分かりやすいものは以下箇条書きしています。

  • Profilerのフレームが300から2000までアップ可能。Preferencesから設定する (Unity2019.3から対応)
  • IL2CPPビルドした実機でもDeepProfilingが可能 (Unity2019.3から対応)

特にIL2CPPビルド後のDeepProfilingは待ち望んでいました。(まだベータで使えないけど)


ここからは軽い説明付きです。

Hierarchyでメインスレッド以外が閲覧可

※Unity2019.3から対応

f:id:esakun:20190907225701p:plain:w450 このようにメインスレッド以外の状態をHierarchyから見れるようになりました。
(今までTimelineでしか確認することはできなかった)
ただし僕の環境(Mac & Unity2019.3.0b1)では、プルダウンを切り替えようとすると以下のヌルポが出て確認できませんでした。

NullReferenceException: Object reference not set to an instance of an object
UnityEditor.EditorWindow.Close () (at /Users/builduser/buildslave/unity/build/Editor/Mono/EditorWindow.cs:919)
UnityEditor.StatelessAdvancedDropdown.ResetAndCreateWindow () (at /Users/builduser/buildslave/unity/build/Editor/Mono/Inspector/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs:21)
UnityEditor.StatelessAdvancedDropdown.DoLazySearchablePopup (UnityEngine.Rect rect, System.String selectedOption, System.Int32 selectedIndex, System.Func`1[TResult] displayedOptionsFunc, UnityEngine.GUIStyle style) (at /Users/builduser/buildslave/unity/build/Editor/Mono/Inspector/AdvancedDropdown/EditorGUI/StatelessAdvancedDropdown.cs:125)

ProfilerReader

Unity黒河さん作のアセット。
ProfilerのログをCSV書き出しをしてくれます。これを使ってテストの自動化を行うといったことをklabのセッションでやっていたようです(セッションは見ていないですが、公開されているスライドはとても有益でした)
Android向けUnity製ゲーム最適化のためのCI/CDと連携した自動プロファイリングシステム

CreateGPUProgramの詳細が表示されるようになった

※Unity2019.3から対応

f:id:esakun:20190907214413p:plain:w450 ShaderVariantが切り替わる際の詳細が表示されるようになりました。

Profilerに任意のデータを埋め込めるFrameMetaData

※Unity2019.1から対応

Profilerに任意のデータを埋め込めるようになりました。

黒河さんのTweetで紹介されていました。

公式リファレンス

OnDemandRendering.renderFrameIntervalの追加

※Unity2019.3から対応

f:id:esakun:20190907214803p:plain:w450 描画処理のみスキップさせることができるようになりました。

OnDemandRendering.renderFrameInterval = 1;

例えば上のようにOnDemandRendering.renderFrameIntervalに1を代入すると、1フレームずつスキップされます。ターゲットフレームが60だった場合30FPSになるようなイメージです。2を代入したら15FPS。

元に戻したいときは0を代入します。


画面更新が不要な場合の処理負荷軽減をさせることができますし、描画だけを止めたい時に1行書くだけで実装できるのはとても楽です。

少し余談

OnDemandRendering.renderFrameIntervalはUnity2019.3から対応とのことですが、PlayerLoopをカスタマイズすればUnity2018でも対応は可能です。ショートカット的なAPIが追加されたということになります。

MeshAPI V2

NativeContainerを利用した動的メッシュ生成APIの追加が予定されているとのことです。これにより、GCAllocしない、また高速化が期待できます。実装タイミングはUnity2020辺りの予定らしいです。

最後に

今回パフォーマンス編としましたが、LWRP、DOTSについては外しています。DOTSについては別記事で執筆予定です。
聞き漏らした内容もあると思うので資料公開された際には、誤記がないかチェックしようと思います。また、早くUnity2019.3が安定化してもらいたいと思いました。

UIElementsのStyleSheetインスタンスは保持してはいけない

開発環境をUnity2019にアップグレードしたし、新機能のUIElementsを始めました。

公式のブログを読みながら進めています。

本記事はUIElementsに関する落とし穴共有です。

StyleSheetのフィールド保持

何も気にせず、StyleSheetインスタンスをフィールドに保持してしまいました。
以下のような感じで。

StyleSheet _stylesheet;

void OnEnable()
{
    _stylesheet = AssetDatabase.LoadAssetPath<StyleSheet>("Assets/Editor/Hoge/Hoge.uss");

    var label = new Label("test");
    label.styleSheets.Add(stylesheet);
}

ussファイル更新ができなくなる

ussファイルを変更するとこの通りエラーが起きてしまう。

MissingReferenceException: The object of type 'StyleSheet' has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
UnityEngine.Object.get_name () (at /Users/builduser/buildslave/unity/build/Runtime/Export/Scripting/UnityEngineObject.bindings.cs:189)
UnityEngine.UIElements.StyleSheets.StyleSheetCache.GetPropertyID (UnityEngine.UIElements.StyleSheet sheet, UnityEngine.UIElements.StyleRule rule, System.Int32 index) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/StyleSheets/StyleSheetCache.cs:333)
UnityEngine.UIElements.StyleSheets.StyleSheetCache.GetPropertyIDs (UnityEngine.UIElements.StyleSheet sheet, System.Int32 ruleIndex) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/StyleSheets/StyleSheetCache.cs:253)
UnityEngine.UIElements.VisualTreeStyleUpdaterTraversal.ProcessMatchedRules (UnityEngine.UIElements.VisualElement element, System.Collections.Generic.List`1[T] matchingSelectors) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/VisualTreeStyleUpdater.cs:313)
UnityEngine.UIElements.VisualTreeStyleUpdaterTraversal.TraverseRecursive (UnityEngine.UIElements.VisualElement element, System.Int32 depth) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/VisualTreeStyleUpdater.cs:248)
UnityEngine.UIElements.StyleSheets.HierarchyTraversal.Recurse (UnityEngine.UIElements.VisualElement element, System.Int32 depth) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/HierarchyTraversal.cs:24)
UnityEngine.UIElements.VisualTreeStyleUpdaterTraversal.TraverseRecursive (UnityEngine.UIElements.VisualElement element, System.Int32 depth) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/VisualTreeStyleUpdater.cs:271)
UnityEngine.UIElements.StyleSheets.HierarchyTraversal.Recurse (UnityEngine.UIElements.VisualElement element, System.Int32 depth) (at 

StyleSheetはローカル変数で使用

このようにローカル変数に保持しておけば問題は起きません。
ussファイルを変更するとシームレスに見た目の更新が入ります。

void OnEnable()
{
    var stylesheet = AssetDatabase.LoadAssetPath<StyleSheet>("Assets/Editor/Hoge/Hoge.uss");
    var label = new Label("test");
    label.styleSheets.Add(stylesheet);
}

最後に

まだ始めたばかりでよくわからないUIElementsですが、uxml、ussで構造と見た目を作っていく感じは、今までのIMGUIとは全く違うアプローチです。

またHTML/CSSのような馴染みもあるため、自分の武器にすべく完全理解しようと思います。

以上

子階層のSpriteRendererのSpriteが1つでも未設定だったらリストを赤くするUnityのEditor拡張

オブジェクトの設定が正しいかどうかをチェックする方法を考えていて、見た目から分かるアプローチをやってみます。

お題は「SpriteRendererのSprite設定がnullのオブジェクトを分かりやすくする」です。

Hierarchy、Projectそれぞれ処理を書く

  • EditorApplication.hierarchyWindowItemOnGUI
  • EditorApplication.projectWindowItemOnGUI

EditorApplicationの hierarchyWindowItemOnGUIprojectWindowItemOnGUI メソッドでそれぞれEditor拡張を書いていきます。

Hierarchyビューでの見え方

f:id:esakun:20190805035011p:plain:w450

Projectビューでの見え方

f:id:esakun:20190805035039p:plain:w450

Hierarchyビュー、ProjectビューそれぞれSpriteが外れていたら、うっすらですが背景が赤くなるようにしています。

全ソースはこちら

最後に

static bool IsValid(GameObject go)
{
    var arr = go.GetComponentsInChildren<SpriteRenderer>();
    return arr.Any(x => x.sprite == null);
}

IsValidの部分の条件を書き換えれば、使い回せます。
プロダクトの規模が大きくなればなるほどバリデーションやチェックツールは必要になってきます。
そんなときの手段の一つとして手元においておこうと思います。

環境

  • Unity2019.1.9f1