Unite2017 参加しました その4
バグを殲滅!Unityにおける実践テスト手法
www.slideshare.net
Unity のテストツール
Unity 標準テスト環境あり
- Unity5.6 play mode
- Untiy5.5以前 edit mode
単体テスト
NUnit
- C#向けのテストツール
- Unityの古めのバージョンから支えていたが、Unity5.6からNUnit3系となったっぽい
TIPS
- MonoBehaiviorのオブジェクトはTearDownでnew する
- GUIで指定する各Objectは引数の差し替えで追加する。
- なので、依存関係がやばいとテストが大変
単体テスト環境の欠点
- MonoBehaiviorのライフサイクルはテストできない
- もしテストをするなら、MVPのPresenterのような形でライフサイクルときちんと切り出さなければならない?
- SingletonやStaticなものは、テストを再実行してもメモリ上に残る。(明示的にイニシャライザをしてあげる)
- Unity5.6未満だと、テストで作成されたgameObjectはテスト後でも残るので、明示的にデストラクタを呼ぶ必要がある。
結合テスト
方法
- キャラクタなどをnewして、Positionなどを整えることで結合テストが可能となる。
- Object間の関係をテストするのに使える。接触した場合の動作など。
- PlayModeであれば、ライフサイクルのテストも可能
MonobehaviourTest
- Unity5.6から使えるようになったテスト
- ライフサイクルをスタブする場合に便利かもしれない
- いくつかバグがすでに見つかっているので注意
その他 テストツール詳解
- UnityTestTools
- Integration Test Framework
- Unity UI Test Automation
Unite2017 参加しました その3
「黒騎士と白の魔王」にみるC#で統一したサーバー/クライアント開発と現実的なUniRx使いこなし術
www.slideshare.net
開発環境
- Perticleの関係で、IL2CPPでビルドはしているが、unity5.3.7
- MMORPG すべてのバトルはC#サーバーにて処理
- AI・演算・結果処理はサーバー
- クライアントはコマンドを受けて、画面を更新
C#大統一理論
メリット
- コードの共有が可能
- サーバーとクライアントのエンジニア同士が互いのコードを理解できる。
- サーバーとクライアントでエンジニアを入れ替えられる。(エンジニアのレベル向上)
- DLLなどの中間プロジェクトを共有できる
- SharedLibraryという中間定義をする必要があった
- IDL(Interface Definition Language)経由の中間コード生成が不要
デメリット
- エンジニアがサーバーとクライアントを合わせたチームになるため、コミュニケーションが困難
- エンジニアには得手不得手がある。
HTTP/2
WebAPIを置き換えるために何か別のフレームワークを利用したい
gRPCとは
- Googoleの提唱するHTTP/2のフレームワーク
- HTTP/2は常時接続・バイナリ通信をサポート
- gRPCはストリーミング通信をサポート
- Unityにおいて、リアルタイム通信が可能になる!
gRPCの導入
Battle Engine
- WebAPI系とリアルタイム通信系の2つ
- リアルタイム通信にはgRPCを使ったStreamingServerを実装
- ちなみに、敵のAI部分はF#で日本語プログラミング言語を実装
UniRX
- UniRXを利用して、MVVMのようなアーキテクチャを使っているようだ。
メリット
- 通信のハンドリングとRXの親和性は高め(非同期処理向け)
- Unity2017でasync/await が実装されるためこっちの方がオススメ
デメリット
- リアクティブスパゲッティが完成
- Rxと使いまくるとChaosへ。
- 伝搬を短くしましょう。
- 循環したらやばいので、注意する。
Tips
ToYieldInstructionの場合IObservableでEmpty返しても思い通りに動かない。 Neverを返すのがオススメ
その他
JSON.NET(Unity)のシリアライズ遅い
マスタデータの生成が遅い
- MasterMemory
- In memory databaseを利用することで解決
参考URL
Unite2017 参加しました その2
パフォーマンス向上のためのスクリプトのベストプラクティス
www.slideshare.net
C# Compiler
- .NET Frameworkはスタック型のマシン
- ソースコードがコンパイルされると最終的に、中間言語(IL)が出力される。
.NET4.6
動画を見直して、再度学習します。 * Unity2017が.NET4.6に対応するので、C#6.0の機能を利用することができる * 非同期処理でAsyncやTasksに対応 * 今後.NET Standard2.0にも対応予定 * .NET4.6が安定したら、新たなGCにも対応よてい
Marshalling
Unmanaged Codeとは
- 開発者自身がオブジェクトやメモリの管理に責任を持つコード。
- GC(Garbage Collector)の対象から外れる。
- DLL/Librarieなどを経由してManaged Codeにアクセスすることができる。
Marshallingとは
- C# やC++ 間で、IntやStringを共有するとき、それぞれのオブジェクトに変換作業。
- Managed codeとUnmanaged Codeを行き来するためのオーバーヘッド作業。
- C#とC++間ではMarshalling asをつけることでClassやStructの対応がとれる。
- Memoaryのレイアウトを意識しないとパフォーマンスに大きな影響を与えてしまう。
- Marshallingするとメモリ内でコピーをしてしまう注意が必要
Unityにおける ManagedHeap
- Heap領域に必要に応じて、メモリを確保する。
- Heap領域が確保できないと、Heap領域を増やす。
- GC(Garbage Collector)は使用されなくなったオブジェクトを削除する。
- GCはHeap領域が足りなくなった場合に、走る。それでも足りないと増やす。
- CGによって削除された空間は、埋められることはないためFragmentのになる。
- そして、一度Heap領域として確保されたメモリは解放されることがない。
避けるためには
- ReuseCollectionを使う。
- 可能な限りStringを避ける。
- ラムダ関数やメソッドはHeapを使ってしまう。
Boxing
- 値型が参照型を引数として渡されるときに発生する。
- 値がheapにallocateされてしまう。(temporary heap managed allocation)
- Fragmentを引き起こす原因になりうる。
Foreachループ
- EnumeratorオブジェクトがHeap領域にAllocateされてしまう。すなわちHeap領域を圧迫してしまう。
- 簡単なForeachであればコンパイラが自動でForloopに置き換える。
- ただし、Collectionの場合はBoxingが発生してしまう。
自身Enumeratorを定義するなど工夫が必要
UnityのAPIでもArrayを返す場合、Heap領域にallocateしてしまう。 ForLoopの中でUnityのArrayを参照する場合は、Loopの前に値をポインタに保持して、そのポインタ経由でアクセスする必要がある。(mesh.verticesやmesh.triangles 遅いのもこれが原因か。)
? Operator
Swiftエンジニアなら慣れ親しんでいるOptional(null許容) C# 6で出てきていたのですね。
参考
[Unite 2016 Tokyo]モバイル端末向けのUnityアプリケーションの最適化実践テクニック - YouTube
Unite2017 参加しました
Unity最適化講座 ~スペシャリストが教えるメモリとCPU使用率の負担最小化テクニック~
www.slideshare.net
Transformsについて
- TransformsはすべてのGameObjectが持っている。
- Transformsが変更されると、イベントメッセージが送られる。
- イベントメッセージはPhysics, Render, Particleなど様々なComponentsに対して送られる。
- Transformを変更すると、処理が高くつく
これからのTransformの変更方法
- SetPosisionAndRotation(Unity5.6~)を利用する。 このAPIを利用することで、無駄なイベントメッセージを減らすことができる。
Transformsが多すぎる問題
- Unityちゃん単体では150ものTransofrmsが存在する。
- Unityちゃんを大量に設置した場合、制御するTransformsが多すぎるため、パフォーマンスは悪くなる
大量のTransformsの最適化
- [inspector]->[Optimize Game Object] にチェックを入れる GameObejctに対するアニメーションをサブスレッドに投げることができる。 それをしないと、すべてメインスレッドで動いてします。
- 一部のTransformsを処理から除外する。(間引く) 除外対象を[Extra Tranforms to Expose]に追加する。
Physics
Physicsの中でも、以下の2つに計算コストがとられる
- Rigidbodyの更新など物理演算
- Raycast
Physicsを重くするには
- 狭い空間にたくさんのCollider(できればMeshCollider)を設置
Physicsを軽くするために
- RaycastのMaxDistanceパラメータを設定する
- Physics Layersを適切に設定することで、不要な処理を省く
- 物理演算の間隔を間引く(パフォーマンスと精度のトレードオフ) [Inspector]->[TimeManager]を制御。Timestepsは代替0.04か0.08くらい。 ただし、シミュレーションゲームでは安易に下げるのは危険
Rigidbodyの付いたGameObjectを操作するときの注意
- SetPositionAndRotationを利用してはいけない。計算コストが高くつく
- RigidBodyのMovePositionとMoveRotationを利用する。
Memory time
IL2CPP Memory Profiler
- IL2CPPのツールでメモリの展開MAPとその詳細が視覚的に見られる。
- UnityEngine.ObjectsとC# Object のメモリをみられる。
- タイルのように使用されているメモリの大きさがわかる
- タイルをクリックすることで詳細がみられる。
- Textureのメモリ使用状況もわかるので、重要でないTextureの削減を考えることができる。
hideFlags
- hideanddontsaveがついていると、そのメモリはアンロードされなくなる。
- memoryleakを簡単に作り出せる便利なフラグ
Texutres non-readble
- Textureを読み込むときに注意が必要
- readbleで読み込んでしまうと、Memoryを2倍使ってしまう。
- Textureをコードなどで変更する必要がないときはnon-readbleにする。
- Texture2Dなどで変更した際もapply時に、non-readbleにするとよい
レガシーコード改善を読んで その2
本投稿の話題
既存コードを変更する際の技。今後、テストを導入できるようにするための布石となる方法。 Swiftに適応するならどうするか。
スプラウトメソッド
- 既存のメソッド内に、新たなメソッドを呼び出すように変更を加える方法。
- 新たなメソッド内だけでもテストが可能となる。
例えば、以下のようなコードがあったとする。
func example() { // doSomething 何かしらの処理を実行している }
doSomethingの後に、何かしらの新たな処理を実行したい場合
func example() { // doSomething // example()内で処理を書くことも可能だが、以下のようにdoSomething2を別に用意する。 doSomething2() } func doSomething2() { // 新たな処理をここに書く }
上記のようにすることで、なるべく既存のdoSomethingに変更を加えずに、修正をすることができる。
また、もしdoSomethingが所属するクラスの依存関係が複雑すぎる場合でも、doSomething2の処理に必要な情報を引数とすることで、doSomething2だけでもテストが可能となる。(新規追加処理がテストできる)
スプライトクラス
- 既存クラスに対して新たな責務を追加したい場合に有効
- 新たな責務を別のクラスとして切り分けて置く
- 変更が必要な既存処理で新たなクラスのインスタンスを生成/利用する。
コードが複雑になるため、使い過ぎには注意が必要となる。一方で別クラスに分けることで、新たな責務を担うクラスだけでもテストが可能となる。
ラップメソッド
- 既存のメソッドをラップするメソッドを用意し、既存処理→新規処理(新規処理→既存処理)と呼び出す
- 既存のメソッドの処理を一切触れることなく、新たな処理を追加することができる
- 既存のメソッドの前・後処理として、処理が独立しているときに有効
例えば、以下のようなコードがあったとする。
func exmaple() { // doSomething }
exmapleを呼び出している箇所すべてに対して、後処理(doSomething3)を追加したい場合
func exmaple(){ doSomething2() doSomething3() } func doSomething2() { // doSomething } func doSomething3() { // 新たな処理 }
既存の処理をdoSomething2として再定義し、exmaple()では既存の処理と新たな処理(doSomething3)を呼びだす。
上記のようにすることで、既存の処理を変更せずに新たな処理を追加することができる。
新たな処理を一部だけ追加したい場合は、exmapleはそのままで、exmaple2を新たに定義し、exampleとdoSomething3を呼び出す。新たな処理を追加したい箇所だけexample2に変更すればよい。
ラップクラス
既存のクラスのメソッドをProtocolで抽出し、新たに定義するクラスをProtocolに適合させる。 新たなクラスのインスタンス生成時に、既存クラスのインスタンスを渡す。変更が必要な個所だけ修正する。 変更が必要ない箇所は既存クラスのインスタンスに処理を委譲する。
Protocolに抽出しなくても、既存クラスが継承可能ならば継承でも実現できる。
protocol SomeProtocol { func something1() func something2() } class SomeClass: SomeProtocol { func something1() { } func something2() { } } class SomeClassWrap: SomeProtocol { let someProtocol: SomeProtocol init(with something: SomeProtocol) { self.someProtocol = something } func something1() { self.someProtocol.something1() } func something2() { self.someProtocol.something2() } }
レガシーコード改善ガイド (Object Oriented SELECTION)
- 作者: マイケル・C・フェザーズ,ウルシステムズ株式会社,平澤章,越智典子,稲葉信之,田村友彦,小堀真義
- 出版社/メーカー: 翔泳社
- 発売日: 2009/07/14
- メディア: 大型本
- 購入: 45人 クリック: 673回
- この商品を含むブログ (157件) を見る
UserDefaultsを初期化する
UserDefaults を初期化するためには
if let bundleId = Bundle.main.bundleIdentifier { UserDefaults.standard.removePersistentDomain(forName: bundleId) }
persistentDomain(forName:)で domainNameで指定した、KeyとValueのディクショナリーが受け取れる。
domainNameをBundleIdentiferに指定することで、アプリ内すべてのKeyとValueが得られる
iOSで使いにくいUserDefaultをなんとかする
はじめに
iOSアプリにおいて、アプリを消しても値を保存しておける、UserDefaultはとても便利だと思う。 しかしながら、どうしても使いにくい部分が存在する。 例えば、UserDefaultの値を取り出すKeyがStringなところだ。
よくある使い方
// 保存する UserDefaults.standard.set("sample", forKey: "sampleKey") UserDefaults.standard.synchronize() // とりだす var str = UserDefaults.standard.string(forKey: "sampleKey")
- Key値をタイポすると実行時まで築くことができない
- stored propertyのように使用したい
stored propertyのように振る舞うために
前準備
class DefaultsKeys { static let sampleKey = DefaultsKey<String>("sampleKey") private init() {} } class DefaultsKey<ValueType>: DefaultsKeys { let key: String init(_ key: String) { self.key = key } } extension UserDefaults { subscript(key: DefaultsKey<String>) -> String { get { if let value = string(forKey: key.key) { return value } else { return "" } } set { set(newValue,forkey: key.key) synchronize() } } }
使い方
UserDefaults.standard[.sampleKey] = "sample" let str = UserDefaults.standard[.sampleKey]
その他
- static let sampleKey のところを増やすことでKeyを増やせる
- subscript を増やせばString以外にも対応可能
- DefalutsKeyとDefaultsKeysを統合したかったのですが、staticプロパティはGenericタイプに対応していない。
おわりに
なんとかスッキリと利用することができるようになったので満足。