Open24

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 }は人間にもマシンにも可読性悪い
けめるけめる

なるほど参考になります...!

自分が知っているのはこっちでしたね、大体使用感は同じはず…

https://neue.cc/2020/12/15_597.html

  • .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は不安定かつ最重要ということになり、内部の実装もごちゃごちゃになりがちである