C# 基礎からマスターまで — 第10回
テスト可能なゲーム構造でC#を整理する
本ブログ作成文は、講義を目指して製作されました。 また、作成された文章の情報も間違っている可能性がありますので、ご注意ください。
文法を多く知っているものと変更しやすいプログラムを作るのは違う。 最後に、ゲームルール、保存、画面を分離して小さな単位で検証できる構造を作ろう。
ルールを純粋なオブジェクトに置く
public sealed class CombatService
{
public int CalculateDamage(int attack, int defense)
{
return Math.Max(1, attack - defense);
}
public void Attack(Character attacker, Character target)
{
int damage = CalculateDamage(attacker.Attack, target.Defense);
target.TakeDamage(damage);
}
}
戦闘ルールがUIやエンジンオブジェクトを直接参照しない場合は、コンソールとテストでも同じコードを使用できます。 画面は結果を表示し、入力を渡す役割だけを引き受けます。
外部機能はインターフェイスの後ろに置く
public interface ISaveStore
{
Task SaveAsync(SaveData data, CancellationToken token);
Task<SaveData?> LoadAsync(CancellationToken token);
}
public sealed class GameSession
{
private readonly ISaveStore saveStore;
public GameSession(ISaveStore saveStore)
{
this.saveStore = saveStore;
}
}
ファイルやサーバーの実装をインターフェイスの後ろに置くことで、実際のリポジトリの代わりにテスト用の偽の実装を置くことができます。 これは機能を隠すための階層ではなく、変更可能性が異なるコードを分離する境界だ。
動作をテストする
[Fact]
public void DamageNeverDropsBelowOne()
{
var combat = new CombatService();
int damage = combat.CalculateDamage(5, 100);
Assert.Equal(1, damage);
}
テストは、privateメソッドの実装をそのまま従うよりも、外部から見える規則をチェックします。 これにより、内部構造を整理しても同じ動作が維持されるかどうかがわかります。
エンジンの境界を定める
Unityでは、MonoBehaviourが入力とライフサイクルを受け取り、通常のC#オブジェクトがルールを担当するように分けることができます。 すべてのクラスをMonoBehaviourにすると、エディタの接続は楽ですが、生成とテストはエンジンに結び付けられます。
良い構造はファイル数が多い構造ではありません。 状態の所有者、非同期作業の寿命、イベントの購読解除、外部リソース境界がコードで明らかな構造だ。
C#のタイプ、インタフェース、ジェネリック、イベント、LINQ、asyncはそれぞれ別々に書く技術ではない。 変更可能な部分を分けて間違った状態を減らすために一緒に使うツールだ。