Unity開発で使える設計の話+Zenjectの紹介
-
Upload
torisoup -
Category
Technology
-
view
16.589 -
download
0
Transcript of Unity開発で使える設計の話+Zenjectの紹介
おしながき
• 設計とは
– 設計とは何か、設計の何がよいのか
– モデリングとクラス設計
• 覚えておくべき基礎知識
– クラス図の読み方
– SOLID原則
– デザインパターン
• Zenjectの紹介
– 依存性注入とは何か
• まとめ
設計とは何か
• 何をどうやって作るのかを決定する作業
– そもそも何を作ろうとしているのか?
– 必要な機能はどうすれば作れるのか?
– 必要なリソースは何なのか?
– 作るのにどれくらい時間がかかるのか?
– どういう手順で作業をするのか?
モデリングとクラス設計
• モデリング
– 抽象的な考えを図や記号で可視化する作業
– 3Dモデリングの意味ではない
– UML(統一モデリング言語)を使って描くことがほとんどである
• クラス設計
– どういうクラスを作るのか
クラス同士がどういう関係であるのか、
といった具体的に何のクラスを作るのかを決める作業
モデリングの例:クラス図
• 登場するオブジェクトとその関係性を表す図
– クラスやインターフェイスなどのオブジェクトを記載
– 関連・依存・集約・コンポジション・汎化・実現なのどの
関係を矢印で表す
モデリングをやるメリット
• これからの作業内容を可視化できる
– どこから作れば効率がよいかわかるようになる
– 実装時に迷うことがなくなってスムーズに開発できる
• 後から見返す時の資料として残すことができる
– どういう作りになってるか俯瞰的に見ることができる
– 不具合調査時のあたりがつけやすい
– 引き継ぎや新人教育に使える
クラス設計をやるためには
• クラス図が読み書きできる必要がある
– UMLのクラス図を覚えるのがよい
• 設計原則を覚える必要がある
– 設計する上で守ることが推奨されるルール
• 設計の定石も覚えておくべき
– デザインパターンやDIといった応用性の高い考え
おしながき
• 設計とは
– 設計とは何か、設計の何がよいのか
– モデリングとクラス設計
• 覚えておくべき基礎知識
– クラス図の読み方
– SOLID原則
– デザインパターン
• Zenjectの紹介
– 依存性注入とは何か
– かんたんな使い方
• まとめ
クラス図の読み方 3/6
• 関連
– 実線で繋ぐとつながりを示す
– 実線矢印で繋ぐと一方通行の関連を示す
(「使う側 → 使われる側」という向きで引く)
PlayerとItemは相互に関係がある
WeaponはBulletを知ってるBulletはWeaponを知らない
クラス図の読み方 5/6
• 集約、コンポジション
– それぞれ「本体とパーツの関係」を表す
• ◇が集約:分解できる関係
• ◆がコンポジション:分解できない関係
– 数字を書くと個数を表す
集約 コンポジション
クラス図の読み方 6/6
• 汎化
– 白抜き実線矢印:クラスの継承関係を表す
• 実現
– 白抜き破線矢印:インターフェイスの実装を表す
BossはEnemyを継承している
BlockはIBreakableを実装している
PlantUMLを使うことをおすすめ
• テキストベースでUML図が描けるスグレモノ
– クラス図の他にシーケンス図やアクティビティ図も描ける
– AtomやVS Codeにプラグインを入れればすぐ使える
SOLID原則
• オブジェクト指向プログラミングにおける5つの原則
– 「原則」の名の通り、理由があって違反するのは問題はない
• SOLID原則を意識すれば設計はだいたいは上手くいく
– Unity開発でも当然SOLID原則は有効
• 普通はオブジェクト指向で開発するしね?
5つの原則
• 単一責任原則
– Single Responsibility Principle
• オープン・クローズド原則 (重要)
– Open-Closed Principle
• リスコフの置換原則
– Liskov Substitution Principle
• 依存性逆転の原則 (重要)
– Dependency Inversion Principle
• インターフェイス分離の原則
– Interface Segregation Principle
例:単一責任原則に違反したクラス
• PlayerControllerクラス
– キー入力管理
– 移動
– アニメーション再生
– 体力管理
– エフェクト・効果音再生
1つのクラスに機能が詰まりすぎ!
機能追加や修正する時に、関係ない部分に影響が出る可能性が高い!
単一責任原則を守ったクラス
役割に応じてクラスを分割しよう!
名前を見たら何をするのかすぐわかるくらいの粒度が適切
• 移動管理:PlayerMover
• 入力管理:PlayerKeyInput
• アニメーション:PlayerAnimator
• エフェクト再生:PlayerEffectPlayer
何がダメ?
• Bulletの操作対象が増えた時にコードに修正が必要
– 全てのSwitch文を漏れなく探して書き換える必要がある
– 変更の手間もかかるし、修正漏れがあっても気づけない
Bossを実装したのでSwitch文に追加もし追加を忘れてもエラーにならないし動いてしまう
オープン・クローズド原則
• インターフェイスや基底クラスで抽象化しよう
– 使う側が相手の型を意識する実装は不健全である
– 機能を追加する時は「継承」や「実装」を行えばよい
– 既存コードの変更は一切不要になる
• GetComponet<T>はインターフェイスも指定できる
– 指定インターフェイスを実装したコンポーネントを取得できる
– 相手が何のオブジェクトか意識せずに処理を書くことができる
どういう意味?
• 派生型は基底型で決めたルールを変更してはいけない
– アクセス修飾子を派生で勝手に上書きしてはいけない
– メソッドを実行するのに必要な判定を基底より強化してはいけない
– メソッドの実行結果を基底より緩くしてはいけない
LSP違反すると何が起きるのか?
• 型安全性がこわれる
– 間違えた使い方をしてもコンパイルエラーにならない
– 実際に動かすまで正しく動作するかわからない
• 「型」を意識してコードを書く必要が出て来る
– ある型の時のみ処理を分岐する、みたいな処理が出て来る
– オープン・クローズド原則に違反する
これをEnemyにアップキャストすると…
• 判定が壊れる
– 両者でIsSameEnemyの挙動が違うのだからあたりまえ
– “EnemyWithTeamId”を”Enemy”として扱うと危険になってしまった
リスコフの置換原則
• 基底クラスの定めたルールには従うこと
– 派生クラスで勝手にルールを書き換えると動作保証できなくなる
– is-aの関係を破壊するような継承を行うと違反しやすい
– Equalsのoverrideも違反しやすいので注意
• C#のnew修飾子はLSP違反を引き起こす可能性が高い
– newでメソッドを隠蔽すると基底と派生でふるまいが変わってしまう
– メソッドを上書きする場合はできるだけvirtual・override修飾子を使おう
上位に変更が必要になる
• PlayerはApplyDamage()の呼び出しを全部書き換える必要がある
– シグネチャが変わったことに対応しないといけない
– Enemy2が増えたことで処理の分岐も必要になる
– オープン・クローズド原則にも違反している
修正:依存関係を逆転させる
• インターフェイスをPlayer側に定義してそれを使う
– インターフェイスの管轄をPlayer側にするのがキモ
– EnemyにPlayerの仕様を押し付けることができるようになる
他にも…
• Singletonへの依存を弱めることができる
– UnityだとManagerクラスをSingletonで作ることが多い
– 利用するManagerを外から渡してあげるようにすれば、
Singletonへの依存が消せるようになる
依存性逆転の原則
• 依存関係をインターフェイスを使って整理しよう
– 上位が仕様を決め、下位がそれに従うのが正しい設計
• インターフェイスの使い所はここ!
– インターフェイスは依存性逆転を行うためにある(と思う)
• 「依存性注入」への話にもつながる
– 詳しくは後述
意味
• インターフェイスは適切な粒度で定義しよう
– 不必要なメソッドがインターフェイスに紛れていると
使う側で混乱する
– インターフェイスを適切に分離し、
必要なインターフェイスにだけアクセスできるようにしよう
SOLID原則のまとめ
• SOLID原則はクラス設計の中核
– これをちゃんと守っていれば設計はだいたいなんとかなる
• SOLID原則は絶対ではない
– あくまで「原則」
– あえて違反したほうが逆にキレイにまとまるんであれば、
違反してもよい(柔軟に対応しよう)
デザインパターン
• プログラムにおける設計の定石
– こういう時はこの設計にするといいよ、というパターン集
• GoFデザインパターンが特に有名
– 23個の汎用パターン
– 他にもMVCやMVVMやMVPといったアーキテクチャに関する
デザインパターンもある
GoFデザインパターン一覧
• Iterator
• Adapter
• TemplateMethod
• FactoryMethod
• Singleton
• Prototype
• Builder
• AbstractFactory
• Bridge
• Strategy
• Composite
• Decorator
• Visitor
• ChainOfResponsibility
• Facade
• Mediator
• Observer
• Memento
• State
• Flyweight
• Proxy
• Command
• Iterpreter
Observerパターン
• イベントの通知と購読を実現するパターン
• 観察者(Observer)と観察対象(Subject)が登場する
• 「Rx」はObserverパターンをベースに作られている
– UniRxを使っている人は自然にこのパターンを使っているはず
Mementoパターン
• Undo機能を実現するパターン
– インスタンスの状態を別の場所に保存しておくことで、
特定のタイミングでインスタンスの状態を巻き戻せるようになるパターン
• パズルゲームで1手前の状態に戻す
• プレイヤが死んだらプレイヤのステータスを開始時点に戻す
• などといった場面に使える
Facadeパターン
• 処理を簡単化する窓口を提供するパターン
– GameObjectにたくさんのコンポーネントが張り付いていて、
どこから欲しい情報を集めたら良いのかわからない!
みたいな時に、処理を一括して受け付ける窓口を提供する
例
• Playerから情報を取得したい
– 今ダッシュしているのか?
– 今空中にいるのか?
– 気絶しているのか?
– 何のアニメーションを再生しているのか?
• どのコンポーネントから情報を取り出せば
いいんだ?
外からの情報取得が簡単になる
• PlayerDataProviderさえ取得すれば情報が取れる
– PlayerDataProviderが代表してデータの収集をする
– 内部の構造がどうなっているか外から気にしなくて済む
– 複雑なシステムを外からは簡単に触れる用にしたい時に
Facadeパターンは有効
デザインパターンのまとめ
• 設計作業の指針にできる
– 何かのデザインパターンに当てはめると簡単に実装できたってこともある
– チームでコミュニケーションを取る時に、デザインパターンの名前がスッと
出てくると話が早い
• ただしデザインパターンに振り回されてはいけない
– メリットもあるが、デメリットもあるパターンが存在する
– デザインパターンを妄信せず、自分で考えてアレンジする必要がある
覚えておくべき知識・まとめ
• クラス図の読み方、描き方
– クラス図を読めるようになるとよい
– 描く時はPlantUMLを使うのがおすすめ
• SOLID原則
– 特にオープン・クローズド原則と依存性逆転の原則が
個人的に重要だと思う
• デザインパターン
– 覚えておくとよい
おしながき
• 設計とは
– 設計とは何か、設計の何がよいのか
– モデリングとクラス設計
• 覚えておくべき基礎知識
– クラス図の読み方
– SOLID原則
– デザインパターン
• Zenjectの紹介
– 依存性注入とは何か
• まとめ
依存性注入
• オブジェクトを外から「注入」すること
– Dependency Injection、略してDIと呼ぶことが多い
• Dependencyに「依存性」という訳が当てられているが、
本来の意味としては「オブジェクトの注入」である
– 依存性逆転の原則(DIP)を適用した場合はこのDIが
必要になる
ResourceProviderの例を使ってDI
• 「ResourceProvider」というデータを提供してくれるクラスがある
• 「ResourceProvider」は「Client」を使ってデータを取得してくる
• 「Client」は抽象化されインターフェイスが定義されている
• 「Client」の実装はプロトコルごとに複数用意されている
ResoureProviderを誰が作るのか?
• ResourceProviderの生成にClietも同時に必要になる
• 2つまとめて生成する「Factory」を作ったほうが良さそう
– デザインパターンのFactoryMethodパターンが近い
依存性注入の問題
• 依存性の解決を誰が行うのか?
– 依存関係が多段になるほど解決が難しくなっていく
– 依存関係を解決する順番も考える必要が出て来ることがある
• オブジェクト数が増えると人間の手では
管理できないほど複雑なDIが必要になってくる
3. Injectアトリビュートをつける
• 注入して欲しいところに[Inject]アトリビュートをつける
– フィールド、またはメソッドに[Inject]をつけるとそこに注入してくれる
– コンストラクタで注入している場合は何もしなくてOK
5.実行する
• 後はZenjectがオブジェクトを注入してくれる
– さっき作ってたFactoryみたいなのはZenjectがやってくれる
– もちろん自分でFactoryを作ってInstallerに登録も可能
SceneBinding
• Scene上にあるオブジェクトを登録して、
実行時にInjectしてくれる機能
– Unityのコンポーネント限定の機能
– 「Inspector ViewでコンポーネントをD&Dで紐付ける作業」
をZenjectに任せることができるようになる
Zenject使わないいつもの
• 例:MainGameManager が GameTimerを 使う
– [SerializeField]をつけてUnityEditorから設定する
– いつもやるやつ
Inspector Viewで設定
これをZenjectにやらせる
2.Zenject Bindng コンポーネントを用意
– 貼り付ける場所はどこでもいい
– ここに注入するオブジェクトを登録しておく
(Installerに登録する作業を裏で自動でやってくれる)
Scene Bindingsの設定おわり
• これでZenjectが実行時にコンポーネントを注入してくれる
– 設定が外れてMissing 状態になるリスクを減らすことができる
– 扱うコンポーネントの数が増えた時にも簡単に対応できる
– シーンをまたいで注入もできる
ハクレイフリーマーケットでの設定例
Scene Bindings のよさ
• Singleton Mono Behaviourを消し去れる
– 「インタンスの参照を簡単に取得できる」って理由で多用された
SingletonMonoBehaviourの代替として使うことが出来る
– Zenjectを導入しているならScene Bindings で
コンポーネントを解決するようにしよう
依存性注入とZenjectのまとめ
• 依存性注入は必須テクニック
– モジュールを疎結合にするとどうしても必要になる
– DIしやすい形に設計していけるとよい
• Zenjectはすごい便利
– 複雑なDIをする必要があるなら使うことをオススメ
– Scene Bindingsを使えばEditor上のぽちぽち作業を減らせられる
– Singleton Mono Behaviour の代わりに使うこともできる
ただし…
• Zenjectの動作パフォーマンスはそれほど良くはない
– 注入する時に内部でリフレクションが走る
– 大量のオブジェクトを生成したりすると顕著に遅い
– 導入するメリット・デメリットの見極めは必要
おしながき
• 設計とは
– 設計とは何か、設計の何がよいのか
– モデリングとクラス設計
• 覚えておくべき基礎知識
– クラス図の読み方
– SOLID原則
– デザインパターン
• Zenjectの紹介
– 依存性注入とは何か
• まとめ
最後のまとめ
• 設計作業はプログラミングにおいて普遍的なもの
– 設計について勉強するなら、Unityの参考書よりは
プログラミングそのものについて扱っている本の方が
詳しく書いてあると思う
• Unityのサンプルプログラムは設計原則守ってないヤツ多い
• 設計の学習はトライ&エラーで経験を積むのが一番早い
– 実際に経験しないと設計の話はピンとこない
– 手を動かしていろいろ設計を試し、
失敗を繰り返して学ぶのがやっぱ一番はやい
昔書いたQiita記事
• グローバルゲームジャムでクラス設計をやった話2017
– https://qiita.com/toRisouP/items/5b7814fda00cab120e39
• この講演を聞いた後に↑の記事を見返してもらえると
新しい発見があるかと思います
おすすめの本
• C#実践開発手法
デザインパターンとSOLID原則によるアジャイルなコーディング
– Gary McLean Hall(著),クイープ 訳 / 5,400円
– デザインパターン、SOLID原則、依存性注入について詳しく解説している