Unity開発で使える設計の話+Zenjectの紹介

139
Unity開発で使える設計の話 Zenjectの紹介 2017/11/26 @toRisouP

Transcript of Unity開発で使える設計の話+Zenjectの紹介

Unity開発で使える設計の話+

Zenjectの紹介

2017/11/26

@toRisouP

自己紹介

• 名前 とりすーぷ(@toRisouP)

• Qiitaとかよく書いてる

• 同人ゲーム作ってる

ハクレイフリーマーケット

• 2017年10月にリリースした東方二次創作ゲーム

• オンライン対戦対応のパーティゲーム

おしながき

• 設計とは

– 設計とは何か、設計の何がよいのか

– モデリングとクラス設計

• 覚えておくべき基礎知識

– クラス図の読み方

– SOLID原則

– デザインパターン

• Zenjectの紹介

– 依存性注入とは何か

• まとめ

設計とは何か

設計とは何か

• 何をどうやって作るのかを決定する作業

– そもそも何を作ろうとしているのか?

– 必要な機能はどうすれば作れるのか?

– 必要なリソースは何なのか?

– 作るのにどれくらい時間がかかるのか?

– どういう手順で作業をするのか?

だいたいの開発フロー

1. 企画を決める

2. 仕様を決める

3. システム設計する

4. コーディングする

5. テスト、デバッグする

6. リリースする

7. 運用、保守する

←ここ

設計すると何がよいのか?

設計のよさ

• 設計することで作るものの全体像が可視化される

– 規模感をチーム内で共有できる

– 何から手を付ければ効率よく進むかわかるようになる

– 今後問題が起きそうな部分を先に洗い出せる

本題

モデリングとクラス設計

モデリングとクラス設計

• モデリング

– 抽象的な考えを図や記号で可視化する作業

– 3Dモデリングの意味ではない

– UML(統一モデリング言語)を使って描くことがほとんどである

• クラス設計

– どういうクラスを作るのか

クラス同士がどういう関係であるのか、

といった具体的に何のクラスを作るのかを決める作業

モデリングの例

モデリングの例:シーケンス図

• オブジェクト同士のやりとりを時間軸に沿って表現する図

– 複雑な処理を可視化することができる

– 通信処理が挟まる部分などはシーケンス図を作っておくと混乱しない

モデリングの例:アクティビティ図

• 手続きの流れを表現する図

– フローチャートの上位互換みたいなもの

– 状態遷移図に似てる(似てるだけで別物である)

モデリングの例:クラス図

• 登場するオブジェクトとその関係性を表す図

– クラスやインターフェイスなどのオブジェクトを記載

– 関連・依存・集約・コンポジション・汎化・実現なのどの

関係を矢印で表す

モデリングをやるメリット

• これからの作業内容を可視化できる

– どこから作れば効率がよいかわかるようになる

– 実装時に迷うことがなくなってスムーズに開発できる

• 後から見返す時の資料として残すことができる

– どういう作りになってるか俯瞰的に見ることができる

– 不具合調査時のあたりがつけやすい

– 引き継ぎや新人教育に使える

クラス設計

クラス設計

• クラスの関係をモデリングすること

– どういうオブジェクトが必要なのか

– オブジェクト間がどういう関係にあるのか?

クラス設計は適当にやれば

OKといったものではない

クラス設計をやるためには

• クラス図が読み書きできる必要がある

– UMLのクラス図を覚えるのがよい

• 設計原則を覚える必要がある

– 設計する上で守ることが推奨されるルール

• 設計の定石も覚えておくべき

– デザインパターンやDIといった応用性の高い考え

こういった

クラス設計をする上で

覚えておくべき知識を解説します

おしながき

• 設計とは

– 設計とは何か、設計の何がよいのか

– モデリングとクラス設計

• 覚えておくべき基礎知識

– クラス図の読み方

– SOLID原則

– デザインパターン

• Zenjectの紹介

– 依存性注入とは何か

– かんたんな使い方

• まとめ

(クラス設計をやる上で)

覚えておくべき基礎知識

覚えておくべき知識

• クラス図の読み方

• SOLID原則

• デザインパターン

クラス図の読み方

クラス図の読み方 1/6

• オブジェクトの読み方

– 名前空間、各記号の意味

名前空間

abstract class interface

class struct

クラス図の読み方 2/6

• オブジェクトの読み方

– アクセス修飾子、フィールド、メソッド定義

○ Public

◇ Protected

□ Private

フィールド

メソッド

クラス図の読み方 3/6

• 関連

– 実線で繋ぐとつながりを示す

– 実線矢印で繋ぐと一方通行の関連を示す

(「使う側 → 使われる側」という向きで引く)

PlayerとItemは相互に関係がある

WeaponはBulletを知ってるBulletはWeaponを知らない

クラス図の読み方 4/6

• 依存

– 破線矢印:片方が相手の状態に対して影響を受ける

• 例)麻痺状態になるとすばやさが1/4になる

クラス図の読み方 5/6

• 集約、コンポジション

– それぞれ「本体とパーツの関係」を表す

• ◇が集約:分解できる関係

• ◆がコンポジション:分解できない関係

– 数字を書くと個数を表す

集約 コンポジション

クラス図の読み方 6/6

• 汎化

– 白抜き実線矢印:クラスの継承関係を表す

• 実現

– 白抜き破線矢印:インターフェイスの実装を表す

BossはEnemyを継承している

BlockはIBreakableを実装している

読み方はわかった

じゃあ描く時は?

PlantUMLを使うことをおすすめ

• テキストベースでUML図が描けるスグレモノ

– クラス図の他にシーケンス図やアクティビティ図も描ける

– AtomやVS Codeにプラグインを入れればすぐ使える

覚えておくべき知識

• クラス図の読み方

• SOLID原則

• デザインパターン

SOLID原則

SOLID原則

• オブジェクト指向プログラミングにおける5つの原則

– 「原則」の名の通り、理由があって違反するのは問題はない

• SOLID原則を意識すれば設計はだいたいは上手くいく

– Unity開発でも当然SOLID原則は有効

• 普通はオブジェクト指向で開発するしね?

5つの原則

• 単一責任原則

– Single Responsibility Principle

• オープン・クローズド原則 (重要)

– Open-Closed Principle

• リスコフの置換原則

– Liskov Substitution Principle

• 依存性逆転の原則 (重要)

– Dependency Inversion Principle

• インターフェイス分離の原則

– Interface Segregation Principle

単一責任原則Single Responsibility Principle

単一責任原則

「1個のクラスに役割は1つ」

• 1つのクラスを変更する理由は1つでないといけない

– 1個のクラスに複数の仕事を持たせてはいけない

• クラスの名前を適切につけるのがコツ

例:単一責任原則に違反したクラス

• PlayerControllerクラス

– キー入力管理

– 移動

– アニメーション再生

– 体力管理

– エフェクト・効果音再生

1つのクラスに機能が詰まりすぎ!

機能追加や修正する時に、関係ない部分に影響が出る可能性が高い!

単一責任原則を守ったクラス

役割に応じてクラスを分割しよう!

名前を見たら何をするのかすぐわかるくらいの粒度が適切

• 移動管理:PlayerMover

• 入力管理:PlayerKeyInput

• アニメーション:PlayerAnimator

• エフェクト再生:PlayerEffectPlayer

オープン・クローズド原則Open-Closed Principle

重要

オープン・クローズド原則

モジュールは

拡張について開いていなければならず、

修正に対して閉じていなければならない

どういう意味?

• 機能の追加は簡単にできないといけない(オープン)

• ただしその時に修正が発生してはいけない(クローズ)

要するに?

• 基底クラスやインターフェイスを使って、

抽象的に操作できるようにしよう

よくない例

• Bulletクラスが相手に対してダメージを与えるコード

– Tagを見てswitch文で処理を分岐している

何がダメ?

• Bulletの操作対象が増えた時にコードに修正が必要

– 全てのSwitch文を漏れなく探して書き換える必要がある

– 変更の手間もかかるし、修正漏れがあっても気づけない

Bossを実装したのでSwitch文に追加もし追加を忘れてもエラーにならないし動いてしまう

どうすればいいのか?

• 処理を「抽象」に依存させればよい

– 基底クラスやインターフェイスを使って処理を呼び出す実装に変える

• こうするとSwtich文は不要になる

オープン・クローズド原則

• インターフェイスや基底クラスで抽象化しよう

– 使う側が相手の型を意識する実装は不健全である

– 機能を追加する時は「継承」や「実装」を行えばよい

– 既存コードの変更は一切不要になる

• GetComponet<T>はインターフェイスも指定できる

– 指定インターフェイスを実装したコンポーネントを取得できる

– 相手が何のオブジェクトか意識せずに処理を書くことができる

リスコフの置換原則Liskov Substitution Principle

リスコフの置換原則(LSP)

派生型はその基底型と

置換可能でなければいけない

どういう意味?

• 派生型は基底型で決めたルールを変更してはいけない

– アクセス修飾子を派生で勝手に上書きしてはいけない

– メソッドを実行するのに必要な判定を基底より強化してはいけない

– メソッドの実行結果を基底より緩くしてはいけない

LSP違反すると何が起きるのか?

• 型安全性がこわれる

– 間違えた使い方をしてもコンパイルエラーにならない

– 実際に動かすまで正しく動作するかわからない

• 「型」を意識してコードを書く必要が出て来る

– ある型の時のみ処理を分岐する、みたいな処理が出て来る

– オープン・クローズド原則に違反する

LSPを違反させる例

• IDをもつ「Enemyクラス」

– IDが同じならば同一の敵であると判定する

同一性のチェック

• IDが同じならtrue

• IDが違ってたらfalse

• この場合は正しく動く!

LSP違反クラス

• 「EnemyWithTeamIdクラス」

– IDとTeamIDの2つを使って同一性チェック

– 基底のIsSameEnemyをnewで隠蔽している

同じクラス同士で比較すると…

• ちゃんと判定できる

– Idが同じでもTeamIdが違うのでfalse判定になる

これをEnemyにアップキャストすると…

• 判定が壊れる

– 両者でIsSameEnemyの挙動が違うのだからあたりまえ

– “EnemyWithTeamId”を”Enemy”として扱うと危険になってしまった

リスコフの置換原則

• 基底クラスの定めたルールには従うこと

– 派生クラスで勝手にルールを書き換えると動作保証できなくなる

– is-aの関係を破壊するような継承を行うと違反しやすい

– Equalsのoverrideも違反しやすいので注意

• C#のnew修飾子はLSP違反を引き起こす可能性が高い

– newでメソッドを隠蔽すると基底と派生でふるまいが変わってしまう

– メソッドを上書きする場合はできるだけvirtual・override修飾子を使おう

依存性逆転の原則Dependency Inversion Principle

重要

依存性逆転の原則(DIP)

上位モジュールが下位モジュールに依存してはいけない

どちらも抽象に依存するべきである

つまり?

• 上位モジュールが仕様を決め、

下位モジュールがその仕様に従うのが正しい姿である

– 上位モジュール:相手を使う側のクラス

– 下位モジュール:上位から使われる側のクラス

下位

上位

だめな例

• 上位モジュール:Player

• 下位モジュール:Enemy

• PlayerがEnemyに対してApplyDamageを実行する

下位モジュールを変更する

「Enemyの種類が追加された」

「ApplyDamage()の仕様を一部勝手に書き換えた」

↑増えた

↑引数が増えた

上位に変更が必要になる

• PlayerはApplyDamage()の呼び出しを全部書き換える必要がある

– シグネチャが変わったことに対応しないといけない

– Enemy2が増えたことで処理の分岐も必要になる

– オープン・クローズド原則にも違反している

修正:依存関係を逆転させる

• インターフェイスをPlayer側に定義してそれを使う

– インターフェイスの管轄をPlayer側にするのがキモ

– EnemyにPlayerの仕様を押し付けることができるようになる

依存関係の「逆転」

• 矢印の向きがちゃんと逆になってる

適用できる場所の例

• クラス内で他のクラスを直接newしている

→ 他のClientに差し替えようとするとコードの修正が必要になる

依存性逆転をすると

• 依存するインスタンスを切り替えられるようになる

– 用途に合わせてClientを差し替えられる

他にも…

• Singletonへの依存を弱めることができる

– UnityだとManagerクラスをSingletonで作ることが多い

– 利用するManagerを外から渡してあげるようにすれば、

Singletonへの依存が消せるようになる

依存性逆転の原則

• 依存関係をインターフェイスを使って整理しよう

– 上位が仕様を決め、下位がそれに従うのが正しい設計

• インターフェイスの使い所はここ!

– インターフェイスは依存性逆転を行うためにある(と思う)

• 「依存性注入」への話にもつながる

– 詳しくは後述

インターフェイス分離の原則Interface Segregation Principle

インターフェイス分離の原則

クライアントが利用しないメソッドへの依存を

強制してはいけない

意味

• インターフェイスは適切な粒度で定義しよう

– 不必要なメソッドがインターフェイスに紛れていると

使う側で混乱する

– インターフェイスを適切に分離し、

必要なインターフェイスにだけアクセスできるようにしよう

SOLID原則のまとめ

• SOLID原則はクラス設計の中核

– これをちゃんと守っていれば設計はだいたいなんとかなる

• SOLID原則は絶対ではない

– あくまで「原則」

– あえて違反したほうが逆にキレイにまとまるんであれば、

違反してもよい(柔軟に対応しよう)

特に

• オープン・クローズド原則

• 依存性逆転の原則

• この2つは特に重要で、ここをしっかり意識すると

かなりまともな設計ができる

覚えておくべき知識

• クラス図の読み方

• 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

個人的によく使うGoFのパターン

• GoFのデザインパターン

– Observerパターン

– Mementoパターン

– Facadeパターン

Observerパターン

• イベントの通知と購読を実現するパターン

• 観察者(Observer)と観察対象(Subject)が登場する

• 「Rx」はObserverパターンをベースに作られている

– UniRxを使っている人は自然にこのパターンを使っているはず

Mementoパターン

• Undo機能を実現するパターン

– インスタンスの状態を別の場所に保存しておくことで、

特定のタイミングでインスタンスの状態を巻き戻せるようになるパターン

• パズルゲームで1手前の状態に戻す

• プレイヤが死んだらプレイヤのステータスを開始時点に戻す

• などといった場面に使える

Facadeパターン

• 処理を簡単化する窓口を提供するパターン

– GameObjectにたくさんのコンポーネントが張り付いていて、

どこから欲しい情報を集めたら良いのかわからない!

みたいな時に、処理を一括して受け付ける窓口を提供する

• Playerから情報を取得したい

– 今ダッシュしているのか?

– 今空中にいるのか?

– 気絶しているのか?

– 何のアニメーションを再生しているのか?

• どのコンポーネントから情報を取り出せば

いいんだ?

Facadeオブジェクトを作る

• PlayerDataProvider

– プレイヤの情報を収集して外に返す窓口クラス

外からの情報取得が簡単になる

• PlayerDataProviderさえ取得すれば情報が取れる

– PlayerDataProviderが代表してデータの収集をする

– 内部の構造がどうなっているか外から気にしなくて済む

– 複雑なシステムを外からは簡単に触れる用にしたい時に

Facadeパターンは有効

デザインパターンのまとめ

• 設計作業の指針にできる

– 何かのデザインパターンに当てはめると簡単に実装できたってこともある

– チームでコミュニケーションを取る時に、デザインパターンの名前がスッと

出てくると話が早い

• ただしデザインパターンに振り回されてはいけない

– メリットもあるが、デメリットもあるパターンが存在する

– デザインパターンを妄信せず、自分で考えてアレンジする必要がある

覚えておくべき知識・まとめ

• クラス図の読み方、描き方

– クラス図を読めるようになるとよい

– 描く時はPlantUMLを使うのがおすすめ

• SOLID原則

– 特にオープン・クローズド原則と依存性逆転の原則が

個人的に重要だと思う

• デザインパターン

– 覚えておくとよい

以上が設計のお話

おしながき

• 設計とは

– 設計とは何か、設計の何がよいのか

– モデリングとクラス設計

• 覚えておくべき基礎知識

– クラス図の読み方

– SOLID原則

– デザインパターン

• Zenjectの紹介

– 依存性注入とは何か

• まとめ

Zenject

Zenjectとは

• 依存性注入を管理してくれるフレームワーク

– AssetStoreから無料でDLできる(MITライセンス)

– 最近は採用事例が増えてきた

Zenjectを知るにはまず

依存性注入の理解が必要

依存性注入Dependency Injection

依存性注入

• オブジェクトを外から「注入」すること

– Dependency Injection、略してDIと呼ぶことが多い

• Dependencyに「依存性」という訳が当てられているが、

本来の意味としては「オブジェクトの注入」である

– 依存性逆転の原則(DIP)を適用した場合はこのDIが

必要になる

どの場面でDIは使うのか?

• 依存性逆転の原則を適用した時

– 実際に使うクラスをインタンス化し、上位モジュールに

渡してあげる作業が「DI」

(Zenjectの話は一旦忘れて)

手作業でDIしてみる

ResourceProviderの例を使ってDI

• 「ResourceProvider」というデータを提供してくれるクラスがある

• 「ResourceProvider」は「Client」を使ってデータを取得してくる

• 「Client」は抽象化されインターフェイスが定義されている

• 「Client」の実装はプロトコルごとに複数用意されている

ResourceProviderの中身

• コンストラクタで外からインタンスを受け取る

– クラス内ではインターフェイスしか触らない

ResoureProviderを誰が作るのか?

• ResourceProviderの生成にClietも同時に必要になる

• 2つまとめて生成する「Factory」を作ったほうが良さそう

– デザインパターンのFactoryMethodパターンが近い

Factoryを作る

• HttpResourceProviderFactory

– HttpClientを使うResourceProviderを提供する

Factoryの実装

• ResourceProviderの生成時に使うClientを注入する

手動でDIおわり

• あとはFactoryを経由で

ResourceProviderを作ればOK

単純なDIだったら簡単

実際はもっと複雑になる

例えば

• ResourceManagerクラス

– ここにも依存性逆転の原則を適用した方が良さそう

こうなる

• つまりFactoryのDIも必要になる

– ResouceProviderの生成にFactoryが必要だった

– じゃあFactoryのFactoryを作るのか…?

他にも

• HttpClientをConfigに依存させる変更を入れる、とか

– どのApiサーバに通信するかをConfigで切り替える

– ConfigをDIすることで挙動を変える

依存性注入の問題

• 依存性の解決を誰が行うのか?

– 依存関係が多段になるほど解決が難しくなっていく

– 依存関係を解決する順番も考える必要が出て来ることがある

• オブジェクト数が増えると人間の手では

管理できないほど複雑なDIが必要になってくる

手動でDIには限界が出て来る

そこで使われるのが

DIフレームワーク

つまりそれが

Zenject

あらためてZenjectとは

• DIを管理してくれるフレームワーク

– オブジェクトの依存関係を簡単に定義できる

– 自動的にDIを実行してくれる

– Prefabの生成などにも対応

さっきの例

• Zenjectを使うとFactoryを使わずに書ける

– 代わりに「DIコンテナ」という概念が出て来る

(オブジェクトの関係性を定義するモジュール)

1.Zenjectを導入

• AssetStoreから入れて終わり

2. Installerを定義

• MonoInstallerを継承したInstallerを作る

– ここにDIコンテナにオブジェクトの関係性を記述していく

3. Injectアトリビュートをつける

• 注入して欲しいところに[Inject]アトリビュートをつける

– フィールド、またはメソッドに[Inject]をつけるとそこに注入してくれる

– コンストラクタで注入している場合は何もしなくてOK

4.SceneContextを配置

• シーン単位でDIを管理するマネージャオブジェクト

– 1シーンに1個必要

– ここに先程のInstallerを登録

Installerを貼り付けて登録

5.実行する

• 後はZenjectがオブジェクトを注入してくれる

– さっき作ってたFactoryみたいなのはZenjectがやってくれる

– もちろん自分でFactoryを作ってInstallerに登録も可能

6. 注入したいオブジェクトを差し替える時

• Installerを複数個定義して差し替えればOK

以上がZenjectを使ったDIの例

• 依存性逆転の原則をちゃんと守って作ってあれば、

「Installer」に記述するだけで使えるはず

• あとZenjectのDIコンテナを応用した機能もある

ついでに

Zenjectの便利な機能

Scene Bindings

SceneBinding

• Scene上にあるオブジェクトを登録して、

実行時にInjectしてくれる機能

– Unityのコンポーネント限定の機能

– 「Inspector ViewでコンポーネントをD&Dで紐付ける作業」

をZenjectに任せることができるようになる

Zenject使わないいつもの

• 例:MainGameManager が GameTimerを 使う

– [SerializeField]をつけてUnityEditorから設定する

– いつもやるやつ

Inspector Viewで設定

これをZenjectにやらせる

1.[SerializeField] を [Inject] に変更

これを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原則、依存性注入について詳しく解説している

以上

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

• @toRisouP