ノートの端の書き残し

UnityやらC#やら。設計が得意かもしれない。

プログラマとして勉強したこと雑に思い返す

自分は、未経験からプログラマとして、ゲームアプリ開発に携わり、数年仕事をしながらゲームプログラムのスキルを磨いてきました。
今ではある程度技術も身につけ(まだ全然知らないことの方が遙かに多いですが)、まぁ中堅として仕事はできてるのかなという状態に落ち着いていますが、こうなるまでにどういう勉強をしてきたのかというのを振り返ってみたいと思います。
自分は完全な未経験の状態からまだ数年しか経っていないので、これが、ゲーム開発初めてみたいけどできるのかわからん、何したらいいのかわからん、みたいな人の参考になればいいなと思います。
(書いてる時点で4年ちょうどくらいでした)

0年目

就職する前のスキルは、完全な初心者そのものです。関数、変数って何みたいな状態から、とりあえずわからんわからん言ってても始まらないので、ネットで検索したり古本屋で適当に本を選んで勉強しました。 読んだ本は、以下のようなものです。

最初は、ゲームって言ったらC++らしいぞ、なんか評判良さそうな本があるぞとDirectXで描画やらをやり、一通りやってみて、これでゲーム作れるかというと時間的に厳しすぎたので、ゲームエンジンに頼って、Unityに手を出した感じですね。
C++やってたのにUEじゃないのかとは思うんですが、検索して出てくる資料の数が遙かにUnityの方が多かったためにUnityを選びました。
そして、Unityで何か作ってみるためにまずC#の勉強をし、簡単なゲーム(確か、ペルソナのコードブレイカーを真似てパズルゲームを作った覚えがあります)を作るまででおよそ2ヶ月かかりました。

1年目

この時点では、C#の本当に基本的な文法はとりあえず頭に入っていますが、ジェネリックもよくわからんしインターフェースも何のためにあるのかわからん、みたいな状態でした。(設計に関する知識がゼロだったので、ポリモーフィズムの何が嬉しいのかピンと来なかったんですね。) この1年目で、自分があまりにも何も知らないことを痛感しながら、仕事をしつつ学ぶというのを続けていました。
ちょうど時期的に、Unity2018や、UniTaskというのが登場し始めまして、アーキテクチャを意識した方がいいのではないかという雰囲気がUnityエンジニア界隈に流れ始めたものこのあたりだったのではないかと思います。
その流れと、自分の基礎を学んでいた時期が合致した結果、設計に比較的強い関心を持った成長をしたのではないかなと思います。
また、「Gotanda.Unity」や、「完全に理解した」のような勉強会にも参加してみたりして、自分が何を知らないのかという情報を得ていったりもしていました。

2年目

1年目に、見境なく勉強し、特に設計面についての知識を深めた結果、参加しているプロジェクトの設計がヤバいということに気がつきました。
ここで設計についてのちゃんとした知識を身につけなければならないと思い、1年目はネットや勉強会といったものから情報を得ていたのですが、基礎固めとして書籍を読みました。

  • Adaptive Code ~ C#実践開発手法 第2版

これはアーキテクチャとは別レイヤの話で、例えば継承を気軽に使いまくらずに委譲せよとか、そういうテクニックの本なのですが、後から変更を加えやすいコードが求められていたので、適切な書籍だったと思います。 また、シェーダやパーティクルなど見た目の勉強もこの辺りで行っていました。
シェーダはUnity独自の要素は少ないので、一般的なHLSLの書籍で学んで、ちょっとUnity対応するという感じでしたね。
基本的には、業務でしなければならないことには余裕が出てきたので、関係ないところ、覚えたら関われる幅が広がりそうなところを勉強していた感じです。
また、SharpLabやBenchmarkDotNetなどを使って、C#への理解を深めていったのもこの頃です。

3年目

設計周りは先輩含め、チームメンバーみんなで勉強していった感じで、その結果自分は設計面に関してリーダー的なポジションになりました。 ただ、正直理解がまだまだ足りていなかったので、ネット上のクリーンアーキテクチャの話だけでなく、ちゃんと書籍で勉強しました。

  • Clean Architecture 達人に学ぶソフトウェアの構造と設計

これを読んで、基本的な考えは身に付いていたことが確認できたのですが、「詳細を後回しにする」など、何となくこうするといいよねくらいの理解だったものが、言語化できるようになりました。
言語化できるとさらに理解を深められるので、大事です。

偉そうなことを言わせてもらえば、最低限まともに仕事ができる基礎はできたかなと思っています。 (驚いたのですが、C# -> ILへのコンパイルや、利用しているライブラリの内部実装などちゃんと見ない人というのは非常に多く、そういう基礎をしっかり勉強しているだけで、中央値よりもかなりまともなコードが書けるようになっていると思います。)
なので、情報発信してみたり(Qiitaにも投稿始めてみたりしたけど、Zennも検討すればよかったかも)して、もっと広くスキルを磨いていけたらなと思ったりしています。

.NETのクラスライブラリ設計で言及された「シナリオ駆動開発」

◯◯駆動開発というのは多くあり、その多くは設計やアーキテクチャに関する概念です。

qiita.com

以前読みました「.NETのクラスライブラリ設計 改訂新版」

www.amazon.co.jp

全てのルールが、一般的なアプリ開発などで適応できるとは思えませんが、良い教えと悪い失敗が多くあり、ためになる一冊でした。

その中で重要度が大きいなと思った、「シナリオ駆動開発」というワードについて少しお話しします。

シナリオ駆動開発

「シナリオ駆動開発」とは、どこからでも使われうるようなクラスはどう作るのがいいのか、というお話です。

ユーザーは、そのクラスをどう使いたいの?

というお話。

例えば、
「アプリ内であるデータを使用するが、アプリには最初から入っておらず、サーバから取ってきて使用する、一度取ってきたら何度も取りに行かないようにストレージに保存して、次からはそこから使用する。」
という機能を作りたいケースを考えてみましょう。
要するにAssetBundleの話だと思っていただければ大丈夫です。

ドメイン駆動的考え方

必要な具体的機能は例えば以下のようなものになるでしょうか。

  • データを読み込む
  • ストレージに既にあるかチェックする
  • ストレージから読み込む
  • サーバから取ってくる
  • ストレージに保存する

また、実際にはメモリ管理も行うでしょうが、一旦別の話なので無視します。

さて、この状況でクラスの責務をどう分けるのが正解でしょう。

答えを言ってしまうと、単一の正解はありません。場合によります。

上記5つ全部分けりゃいいじゃん! と思うかもしれませんが、実際のところ、別に分けなくていい場合もあります。

機能としては上記5つは異なりますが、実際に使うときには将来的にも、絶対に、例外無く一緒に使われるならば、クラスを分ける意味は無いです。
変更容易性を高めるならば、将来の変更可能性を考慮すべきですが、そこを考えても変更はあり得ないなら、別にいいでしょう。
そして、その将来の変更可能性をどうやって判断するのかというのが、ドメインへの理解です。
ドメインへの理解こそが、そのプロジェクトで本当に適切な機能分解には不可欠ということです。

5つの機能と言いましたが、プロジェクトによってはもっと細かく分ける必要があるかもしれません。 例えば、ストレージに保存すると言っても、データの種類によって場所や、保存期間が違うかもしれないし、サーバから取ってくると言っても、全てのデータが同じサーバには無いかもしれません。 そういう場合は、データの所在を教えてくれるクラスを切り分けた方が良いでしょう。
そういう要件が無い場合でもデータの所在を取得する手段は用意するでしょうが、クラスを分けたりはしないですよね。
こんな感じで、クラスが持つべき責務というのに最適解はありません。

こういう話はやはりミノ駆動さんの話が良いですね。

qiita.com

シナリオ駆動的考え方

さて、ではシナリオ駆動ではどう考えるかという話です。
まず、シナリオ駆動という考え方は、特にドメイン駆動とは競合しません。
そもそも、アーキテクチャの考え方ではないんですね。

シナリオ駆動開発とは
使用者の目線で開発すること
です。

つまり、前述のドメイン駆動でも挙げたデータダウンロードの例でいうと、
データをメモリに展開し、使用したい人(クラス/メソッド)は、どう使いたいと思うか
というところから開発を始める手法となります。

C#で言えば、おそらく

var data = await AssetLoader.LoadAsync("名前とか、アセットを特定できる何か");

みたいにサッと書けたら使用者は嬉しいでしょうね。
また、不要になったら意識しなくてもメモリから解放されてくれれば尚良いでしょう。
その場合staticメソッドだと不都合があるかな。
事前に読み込むこともできてほしいですね。
ダウンロードする必要があるかのチェックも単体で使うかな。
などなど……

これを実現できるには何を公開すればいいかなぁと考えるのがシナリオ駆動開発です。
ここで必要なのは使用者の目線。
ドメイン駆動開発と同様、プロジェクトへの理解は欠かせませんね。

終わり

というわけで、シナリオ駆動開発っていい考え方だねというお話でした。
結局のところ、自分が作ったものがどのように扱われるのか、という、ユーザー目線
ここで言う「ユーザー」は、アプリのユーザーかもしれないし、クラスやメソッドのユーザーかもしれない
要するに将来の自分かもしれない

そして、こう言う、単一の正解が無いという難しさがあるため、ドメイン駆動開発や単一責任原則の大事さをプログラマに教えるには、自分で考える力を身につけさせるためにも 歴史ある地獄の炎上プロジェクトに叩き込むのが一番なのかもしれません。

UnityのAnimationの管理はビヘイビアツリーが便利かもしれない

UnityのAnimatorは使いづらい

Unityでアニメーションの遷移とかの管理、何を使っていますか?
公式はAnimatorControllerを使うように言ってきますし、Unity初心者講座とか見てもそう言ってます。

が、アニメーションが4つ5つ出てきたあたりから管理が怪しくなり、ステートの数が増えるにつれて、遷移の可能性は級数で増えていくのでいつしか地獄の様相を呈してきます。

zenn.dev

これはUnityのAnimatorControllerがダメというより、
そもそもステートマシンというのが、ステートの数が多いと破綻するものなのです。
個人的な意見だと、ステートが3つまでなら問題無くて、5つを超えたら別の方法を検討した方がいい、と思っています。
アニメーションの種類は、多くのケースで5つは超えるでしょうね。

ビヘイビアツリー

というわけで、ステートマシンの替わりとしてビヘイビアツリーを検討してみます。
ビヘイビアツリーは、一般的には敵キャラクターのAIなどで使用されるものですが、本質的にはただの「現在の状態を決定する仕組み」ですから、AI以外に適応できてもおかしくないでしょう。

ちなみに、ビヘイビアツリーはUnreal Engine4にはデフォルトで入っていまして、UEの解説リンクを貼っておきます。

historia.co.jp

Unityでビヘイビアツリーを使うには?

自作するのは、できなくはないですがとても大変なので、Asset Storeから入手しましょう。

おそらくUnityのビヘイビアツリーのアセットで一番有名なのはBehaviour Designerでしょう。

assetstore.unity.com

が、今回はこちらのアセット

assetstore.unity.com

Arbor3を使用してみます。

Arbor3について

Arbor3は、Behaviour Designer同様ビヘイビアツリーを扱えるアセットですが、ステートマシンも扱えます。
また、日本語使用者としてはうれしいことに、製作者の方が日本語使用者で、公式のマニュアルが日本語で書かれています。

arbor.caitsithware.com

サンプルが充実しているわけではないので、これ読めば完璧!というほどではないのですが、自分で調べながら動ける人なら十分有難いです。

ちなみに、Unity Japan公式がArbor3の紹介と使い方の説明をしています。

www.youtube.com

丁寧な解説

qiita.com

Arbor3でアニメーションの遷移を管理してみる

こちらのアセットを使用し、ユニティちゃんのアニメーションをArbor3のビヘイビアツリーで作ってみましょう。

unity-chan.com

ParameterContainerを作る

Arbor3で使用するパラメータを外部から操作するときは、ParameterContainerを使います。
ParameterContainerは、Arbor3のエディタを開くと出てるパラメータ一覧です。
f:id:u_osusi:20211218235131p:plain

BehaviourTree _tree;
_tree.parameterContainer.SetFloat("パラメータ名", value);

のようにスクリプトからアクセスできます。

また、コンポーネントとしても追加でき、こちらを参照することもできます。
好みで使用してください。

f:id:u_osusi:20211218234935p:plain

ツリーを作る

全体像はこんな感じになっています。
f:id:u_osusi:20211218234854p:plain ビヘイビアツリーについて全く知らないとさすがに説明しづらいのですが、簡単に言うと、
左から順番に見て行って、条件に合ってる最初の振る舞いノードを採用する。
という感じです。
今回は全ての振る舞いノードが、ただのアニメーション再生になっています。

ノードを作る

一番左のノードを見てみましょう。
f:id:u_osusi:20211218235607p:plain

ここでは、水平方向の速度が4を超えていたら、Unitychan_Runを再生することになっています。
さらに、このノードの前に、Selectorノードを挟んでおり、ここでは接地しているかを確認しています。
f:id:u_osusi:20211218235659p:plain

Unitychan_Runのノードは一番左ですから、

  • 接地している
  • 水平方向の速度が4を超えている

の条件を両方満たせば、Unitychan_Runが再生されるはずです。

こんな感じで、左から順に

  • 接地かつ水平の速さが4以上ならUnitychan_Run
  • 接地かつ水平の速さが0.1以上ならUnitychan_Walk
  • 接地ならUnitychan_Idle
  • 垂直の速度が+0.1より大きいならUnitychan_Jump_Up
  • 垂直の速度が-0.1より小さいならUnitychan_Jump_Fall
  • 無条件にUnitychan_Jump_MidAir

となるようノードを並べました。
左側から判定されるので、一番右は無条件で大丈夫なんですね。

あと必要なこと

パラメータは、前述のParameterContainerを通して渡しましょう。
方法はなんでもいいですが、自分はReactivePropertyで過不足なく反映させるようにしました。

IReadOnlyReactiveProperty<bool> _isGround;
IReadOnlyReactiveProperty<Vector2> _velocity;
_isGround.Subscribe(v => paramContainer.SetBool("IsGround", v));
_velocity.Subscribe(v => paramContainer.SetFloat("HorizontalSpeedAbs", Mathf.Abs(v.x)));
_velocity.Subscribe(v => paramContainer.SetFloat("VerticalSpeed", v.y));

また、反転も実装しましょう。反転はアニメーションとは無関係に実装すると良いでしょう。

結果

f:id:u_osusi:20211219002148g:plain

ビヘイビアツリーでアニメーションの設定ができました。
ステートマシンに比べて、6つの状態を管理しているにしてはシンプルなツリーではないかと思います。
ステートマシンだと、ジャンプで上昇中に坂に接地したらとか、2ステート間の関係を考える必要がありましたが、
ビヘイビアツリーならばその状態の条件だけを考えればよく(まぁ本当に考えないわけではないですが、少なくとも記述の上では現れないので)、スッキリした構造を作ることができます。
(正直思ったよりもビヘイビアツリーでやってみるのがいい感じでした。)

改善点

状態管理用のパラメータは文字列で渡しているので、ここはもうちょっと何とかしたいですね。
パラメータ管理だけに専念するクラスを分離すればそこまで問題ではないのですが、やっぱりちょっと抵抗感があります。
理想としては、ビヘイビアツリーそれぞれに対応したパラメータコンテナクラスを生成し、文字列ではなくフィールドを直接参照して値を設定できるようにできれば最高ですね。

AssetBundleを読み込む手段 メモリの使用量について

Addressable Asset Systemをそろそろ理解したいのだが、その前にAssetBundleのベストプラクティスを考える前提知識を整える必要がある。

AssetBundleにはいくつか利用方法(読み込み方法)があるようだが、それぞれ特徴があるようなので確かめてみる。

AssetBundleについての基本知識

qiita.com

Assetを読み込む方法

LoadFromFile

AssetBundleファイルのパスを指定して読み込む。

AssetBundle assetBundle = AssetBundle.LoadFromFile(assetBundlePath);

LoadFromStream

AssetBundleファイルのパスを指定して、Streamで読み込む。

AssetBundle assetBundle;
using (FileStream fileStream = new FileStream(assetBundlePath, FileMode.Open, FileAccess.Read))
{
    assetBundle = AssetBundle.LoadFromStream(fileStream);
}

暗号化されている場合、

FileStream fileStream = new FileStream(assetBundlePath);
// なんらかの復号Stream
DecryptStream decryptStream = new DecryptStream(fileStream, password, salt);
AssetBundle assetBundle = AssetBundle.LoadFromStream(decryptStream);

LoadFromStreamの復号について

暗号化アルゴリズムによって中のメモリ確保が変わる。 一般的な暗号化Stream(CryptStreamなど)を用いた場合は、上記の処理でAssetBundleファイルそのままをアンマネージドメモリへ読み込む。と思われる。(未検証) CanSeekがtrueな暗号化Streamを用いた場合は、上記処理ではメタ情報のみがMonoメモリに読み込まれる。 また、LoadAssetする時点で復号に用いたStreamが破棄されている場合、Loadに失敗する。 詳細な実装はC++領域なため公開されていないが、この動作から、LoadAssetの段階でも、目的のAssetの部分までSeekし、目的のAssetだけを復号して読み込んでいると推測できる。

LoadFromMemory

Monoメモリ上のバイト列からAssetBundleを生成する。

byte[] bytes = File.ReadAllBytes(assetBundlePath);
AssetBundle assetBundle = AssetBundle.LoadFromMemory(bytes);

のように使用する。 AssetBundleが暗号化されていれば復号処理を挟む。

// ファイルから直接バイナリとして全て読み込む。
byte[] encryptedBytes = File.ReadAllBytes(assetBundlePath);
byte[] decryptedBytes = Decryptor.Decrypt(encryptedBytes)
AssetBundle assetBundle = AssetBundle.LoadFromMemory(decryptedBytes);

検証内容

準備

適当な音楽ファイルを20個用意した。全てmp3で総容量は98.6MB、平均して1ファイル5MBだった。 これをLZ4圧縮で1つのAssetBundleにビルドした。(平文AssetBundle) さらに、その上でCanSeekな暗号化Streamで暗号化したものを用意した。(暗号文AssetBundle) 具体的には、こちらの記事をそのまま流用させてもらった。 tsubakit1.hateblo.jp 平文AssetBundle、暗号文AssetBundle共にサイズは85.2MBだった。

検証

LoadFromFile, LoadFromStream, LoadFromMemoryを用いた3つの検証を行った。 それぞれ、AssetBundleを読み込む段階と、その後LoadAssetでAudioClipを1つ読み込む段階でメモリの使用状況を確認し、UsedMemoryの変化を記録した。 LoadFromStreamのみ暗号文AssetBundleを使用し、他は平文AssetBundleを使用している。 LoadFromMemoryはReadAllBytesが事前に必要なため、そこも確認している。

結果

LoadFromFile LoadFromStream LoadFromMemory
ReadAllBytes Mono: 7MB -> 88MB
Unity: 110MB -> 110MB
AssetBundle Mono: 7MB -> 7MB
Unity: 110MB -> 110MB
Mono: 7MB -> 7MB
Unity: 110MB -> 110MB
Mono: 88MB -> 88MB
Unity: 110MB -> 191MB
AudioClip Mono: 7MB -> 7MB
Unity: 110MB -> 116MB
Mono: 7MB -> 7MB
Unity: 110MB -> 116MB
Mono: 88MB -> 88MB
Unity: 191MB -> 197MB

ここから、以下の特徴が推測できる。

LoadFromFile

  • これで取得するのはAssetBundleのメタ情報のみ。
    • ここからAssetBundle.LoadAsset<T>で中のAssetを取得する際も、目的以外のAssetは無視され、不要なメモリは確保せず、メモリに優しい。
  • 暗号化されたAssetBundleファイルに使用できない。

LoadFromStream(CanSeekな暗号化を行った暗号文AssetBundle)

  • これで取得するのはAssetBundleのメタ情報のみ。
    • ここからAssetBundle.LoadAsset<T>で中のAssetを取得する際も、目的以外のAssetは無視され、不要なメモリは確保せず、メモリに優しい。
  • 暗号化されたAssetBundleファイルに使用できるが、そのための暗号化アルゴリズムに制約がある。

LoadFromMemory

  • AssetBundleファイルを一括でMonoメモリに読み込む必要があり、その上でUnityメモリにコピーする必要もある。
    • メモリ効率が非常に悪い。AssetBundle内の1つのAssetを使用したい場合でもAssetBundle全体を読み込む必要がある上に、適切に処理しても一時的にAssetBundle1つ分のメモリがMonoメモリとUnityメモリに同時に存在する状態を避けられない。
  • 暗号化されたAssetBundleファイルに使用できる。

結論

平文のAssetBundleを使用する場合はLoadFromFileが適切。 暗号化されたAssetBundleは基本的にはLoadFromMemoryを使用するが、CanSeekな暗号化アルゴリズムを使用すればLoadFromStreamでメモリに優しいAsset読み込みが可能になる。

命名の話

与太話

.NETのクラスライブラリ設計を読みました。

www.amazon.co.jp

これは改訂版で、僕は改訂版で初めて読みました。 改訂前の本は10年ほど前に出ているようで、若干古い言及もあるのですが、良い本でした。

すごいテクニックが沢山! というのではなく、むしろ、膨大なライブラリを作るにあたっての苦労や失敗、後悔といった、しくじり先生的な本になっています。 マイクロソフトのエンジニアも間違えるんだという謎の安心感を得られますね。

命名の話

前にも書いたのですが、チームで開発するときに1番重要なルールは単一責任原則だと思います。 色々大切なことはありますが、1つ選べと言われたら多分これ。

nigiri.hatenablog.com

そして、これと付随するのが命名だと思います。 チーム開発の場合、他人の書いたコードを読むほうが、自分がコードを書くよりも断然多くなります。 その際に苦労するのが、名前から中身が推測できないコード。 ◯◯Managerとかですね。

こういう名前をつけてしまう理由はいくつかあるでしょうが大きく2つでしょうか。

1つは、中身が多く、バシッと決まった名前がつけられないから。 中身がフワフワしてるなら名前もフワフワしちゃうのは当然です。単一責任原則を守ればこれは回避できます。

もう1つは、読み手の気持ちになっていないから。 書いてる人は、「クラスの役割」->「クラスの名前」と名付けているかもしれませんが、読み手は「クラスの名前」->「クラスの役割」の順に読むのだから、それは改めるべきです。 これは、クラス設計を先に行い、詳細を後回しにすることで自然と実現できます。 必要な役割を洗い出す過程で適切にクラス分けを行い命名することで、神クラスの誕生を防ぎ、読み手に優しいコーディングにならざるを得ない状況を作りましょう。

もし、これらの理由ではなく単に命名に関心が無い故なのだとしたら、チーム開発においては大きな問題であると思います。 単一責任原則が守られているかチェックするのは結構難しいことだとは思いますが、例えば、 クラスの名前から、そのクラスが公開しているメンバを過不足無く推測できるか考えてみましょう。 「過不足無く」というのは、本当に適切なクラス分けと命名がなされているなら決して不可能なことではないです。

終わり

SOLID原則だとか、命名だとか、プログラミングを始めたばかりの頃は意識できなくて当然なんですが、これらがしっかりできている人、これらがいかに恩恵をもたらすかを理解している人は安心感が違います。ゲームエンジンを使ってとにかく動くものを作る、というのもモチベーション維持になりますし非常に価値あることだと思いますが、こういった基礎も同時に進めていくと、信頼されるプログラマにより近付けるのではないかなと、それを目指している身として感じます。

SOLID原則の話

SOLID原則

ja.wikipedia.org

オブジェクト指向プログラミングにおいて重要な5つの原則をまとめて言ってみたもの。

S = 単一責任の原則

O = 開放閉鎖の原則

L = リスコフの置換原則

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

D = 依存性逆転の原則

そんなの知ってるよ、という人だったり、ゲームでそんなうまくいかないよ、という人だったり、意見あるとは思いますが、最近特にこの大切さを感じています。

SOLID原則の序列

5つにまとめられて語られるSOLID原則ですが、単一責任原則がブッチ切りで重要度は高いと思います。

これは個人の印象ですが、SOLID原則の中でも重要度の序列(守らなくてもいいケースの少なさ、みたいな)はあって、

S > O >= L > I >= D

の順で重要かなと思っています。

Sは最重要。クラスの名前は、可能な限り厳密に。名前の厳密さはそのままそのクラスの凝集度になります。ただ、「単一」の解釈を共有しづらいのと、依存の方向性が一方向に保たれつつある程度クラスの責任が明確であれば問題は無かったりするので、これに関する議論は非常に難しい。個人的には、妥協したとしても、ドメインから最も遠いレイヤー以外は徹底して守るべきだと思っています。

Oも守るべきですが、守らなかったときの被害はSほどではないでしょう。

Lも基本守るべきですが、MonoBehaviourを継承したクラスを作っている身としては、使われ方次第かなと言いたくなります。ちょっと屁理屈っぽいですが。 ところでこいつだけリスコフとか人名が入っているのが気持ち悪いです。

Iはプロジェクトの状況に応じて適切な粒度を見極めたいところです。

Dは選択肢として持っておくべきだとは思いますけど、実際に全部抽象に依存されたら不便極まりないので、「原則」とまで言うのは過言じゃないか?と思ってしまいます。

愚痴

自分の経験ですが、これらをちゃんと理解して実践できている人は多くはないです。これが実践できていると、保守コストがグッと下がり、運用に5人エンジニアが必要だったところが、4人、3人で済む、みたいなケースも普通にあり得ると思うので、その分別の機能追加に回せたり、全体に開発速度バフをかけられたりと、非常に強いスキルだと思います。

また、自分は、これらを守ってコーディングすれば、守らない場合よりも、短期的にも長期的にも開発速度は上がると思っているので、時間がないからできない、みたいなことを言う人は要注意だな〜とも思っています。こういう開発手法ってどうしても例がWEBサービスとか、アーキテクチャのレイヤーがわかりやすいものになっちゃうんですが、3Dのアクションゲーム、そのアクション部分であっても同様に、これらを守って開発した方が良いと思いますね。

全人類が待ってたUnity公式多言語対応パッケージ

Unity公式ローカライズ用パッケージが

出たぁ~~~!!(結構前)

www.youtube.com

あんまり個人製作でローカライズはしないと思うんですが、仕事だとかなりありがたい。

(ボタンのラベルとか、ロジックにな~んにも関係無いテキスト表示をコードで制御したくないし、真面目にローカライズの仕組み作ると地獄みがあるので本当に素晴らしいです。)

使い方とかは以下のブログ記事がとても親切で、おススメです。

yotiky.hatenablog.com

解説の動画だと、日本語と英語で縦書き時の文字の向きを切り替える、というのをやっていましたね。

自分でもちょっといじったりしたところ、

みたいですね。なので、言語ごとにフォントを切り替える、というのももちろん可能。(これは試してみた) Unity上で考えられる一番柔軟なつくりって感じで素敵ですね。

また、デフォルトではTexture、String、AudioClip等のローカライズが可能ですが、MonoBehaviourのローカライズのための基底クラスが公開されてるっぽいので、それを継承すれば任意のMonoBehaviourのローカライズをカスタマイズできそうですね。必要かどうかはわかりませんが……

作り自体は言語ごとのDictionaryを作って、同じKeyから、それぞれのDictionaryを引くというわかりやすいものなので、使う分には理解しやすいパッケージだとも思います。

これでまた、ロジックに関係のないコードを抹殺することができてうれしい限りです。