C#ゲーム設計メモ2(思想強め)
※全部あくまで個人の考えです
※意見を表明するものというよりは自分の中で整理したり理解を定着させるためのものなので間違ったことも結構書いてるはず
わかりやすいコード
- ルールがクラス、関数に落とし込めてる
- 自分ルールが少ない
- 参照方向の意図がなんとなくわかる
クラス分けの仕方
「分けたほうが分かり辛い」はそんなないのではと思っている
分けたほうが分かり辛いのは、クラス自体がでかすぎて中途半端に分けると分かり辛い、の意なのかも
もちろんValueObjectDomainPrimitiveレベルで分ける必要はないものとかもあるけど、でも基本はルールをクラス自体に落とし込むためにValueObjectDomainPrimitiveを使いたい場面も多いし、そういう形にうまくパースしてバクの温床である「関数内バリデーション」を避けるのは有効だと思う
1クラスにprivateな関数が何個もあって、それを自分がいっぱい使ってる、みたいなのは自分ルールが多くなりがち
なぜならそのメソッドには暗黙に正しい/間違った実行順があったりするから
そして間違った実行を回避するために大量のバリデーションが書かれ、それが仕様変更や追加実装で大量のバクになるという寸法
クラスの依存関係はいわば処理の流れであり、こういう大量のカプセル化できてるようでできてないprivate関数を減らす効果があると思う
private関数と、それのpublicな公開用関数があって、privateにもpublicにも使われることを想定する、とかは結構慎重にやったほうがいいと思う
同じような関数がいっぱい用意されたクラスがあったとして、それがpublicでも使う気になりづらい
クラス内に変な自分ルールがあると、他者の利用すら困難になる
クラスを分けるデメリットがあるとしたら、下方向にどんどん伸びていくような階層構造のクラスの底を参照するために、各階層のクラスにただ下のクラスに移譲するだけの関数を書きまくる、みたいなムーブは嫌にはなる
あと、クラスの参照方向まではどうしてもほとんどC#のコードレベルでルール化できない
のでこの辺はアーキテクチャとか設計思想とかでなんとかすることになる
↑について
たとえば(名前テキトーだけど)
class PlayerMovementComponent : MonoBehaviour
{
private Rigidbody _rigidbody;
public void Move()
{
// 実装
}
}
class PlayerMovement
{
private PlayerMovementComponent _movement;
ctor();
public void Move() => _movement.Move();
}
みたいなMonoBehaviourのAPIのカプセル化を目的としたクラスを作ったとして(それの是非は関係なく)、利用者がこの関係性に目を配らせずにPlayerMovementComponentを直接使うというミスは簡単に起こり得るということを言っている
一応internalを使えばレイヤーを分けてインターフェースを切りながら作ることは出来るんだけど、まあゲーム開発だとやりすぎな場合も十分あると思う(この辺は分からん)
HP自体をクラスにするか問題
HPは普通にプリミティブとして作る派とクラスとして作る派がいると思う
class Player
{
public int Hp
{
get => _hp;
set
{
if (value < 0) throw new ArgumentException("HPは0未満にできません。");
_hp = value;
}
}
int _hp;
}
or
readonly record struct Hp
{
public int Value { get; }
public Hp(int value)
{
if (value < 0) throw new ArgumentException("HPは0未満にできません。");
Value = value;
}
public static Hp operator +(Hp hp1, Hp hp2) => new(hp1.Value + hp2.Value);
public static Hp operator -(Hp hp1, Hp hp2) => new(hp1.Value - hp2.Value);
}
class Player
{
public Hp Hp { get; private set; }
public Player(Hp hp)
{
Hp = hp;
}
}
個人的にはプロパティ内にいろいろ書くくらいだったらクラス化した方が好きではある(この意見は変わる可能性高い)、たとえHPを使うクラスがPlayerだけだったとしても
理由としては
- ファイルを見る姿勢として、Player.csファイルを見るときにHPのロジックまで毎回見たいわけじゃない
- Player外からはHpはカプセル化に成功しているけど、Playerクラス内では厳密には_hpを直接いじれば悪さが出来るので、重箱の隅をつつくとカプセル化出来てない
しんどくなる部分としては、
- .Valueがだるい
- loggerに出すときに{ "Value" = 10 }は人間にもマシンにも可読性悪い
ValueObjectGenerator の出番……?
explicit で operator 実装されているので明示的な cast使えば .Value しなくてもよさそうですね。
勿論、 loggin 用の ToString の定義はあるので直接食わせて良さそうですね。
なるほど参考になります...!
自分が知っているのはこっちでしたね、大体使用感は同じはず…
- .Valueがだるい
- loggerに出すときに{ "Value" = 10 }は人間にもマシンにも可読性悪い
こちらもこの辺に対する解決目的をもって作られたようですが、こういうライブラリをなんかちょっと食わず嫌いしていたきらいがあったかもしれないですね
というか一回触ってやめたんだけど、なんでだったかちょっと忘れてしまった
レイヤーについて(まじで知らん)
よくゲーム開発で用いられるアーキテクチャとか設計パターンレベルの依存関係は
- クリーンアーキテクチャ
- 3層
- 4層
- 拡大解釈オリジナル
- 名前
- Domain
- Infrastructure
- Store
- Application
- Presentation
- View
- MVP
- 依存関係ガン無視Singletonだらけ設計
どの設計パターンも目的としては
- 制御フローの整理
- レイヤーレベルでのカプセル化
- SOLIDのSを守り切る
あたりが挙げられるとは感じる
特に制御フローは重要視される
DDDとか含むアーキテクチャをそのまま採用するのはゲームでは難しい(らしい)
理由としては
- DomainよりもViewのがゲームだと圧倒的に製品の根幹に近い
- ドメインエキスパートがViewの話ばっかするの想像したらまあそうなるかとは思う
- つまり見た目とか細かいフレームでの挙動とかそういう部分がゲームとして最重要レベルになることが往々にしてあるということ
- そういうわけでView層が非常にでかい
- UnityでもViewを部品として使いやすいインターフェースを持った状態で組み上げることは大事
Viewをとにかく自作しないといけないゲーム開発において、クリーンアーキテクチャとかで全部のViewのインターフェースを切って依存関係を逆転!みたいな行為はかなりしんどいのであまりやりたくないという派閥もいる(自分も今はこっち側)
ちなみに依存関係の逆転(制御フローの整理法)についてはインターフェース以外にも
- Observerパターン
- ひいてはMVPパターン
- イベント
などがある
Observerは双方向を単方向にする威力がある
eventはさらに依存関係を逆転させる力がある
こう考えるとViewは不安定かつ最重要ということになり、内部の実装もごちゃごちゃになりがちである