← SEARCHABLE ARCHIVE
C# · 2026-08-12

C# from Basics to Mastery — Part 10

Organizing C# into a testable game structure

INTERACTIVE READING MODE →

The articles written on this blog were created with the goal of teaching. Also, the information in the article may be incorrect, so please be careful.

There is a difference between knowing a lot of grammar and creating a program that is easy to change. Finally, let's separate the game rules, save, and screen to create a structure that can be verified in small units.

Place rules in pure objects

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);
    }
}

If your combat rules don't directly reference UI or engine objects, you can use the same code in console and tests. The screen is only responsible for displaying results and transmitting input.

External functions are placed behind the interface

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;
    }
}

By putting your file or server implementation behind an interface, you can put a fake implementation for testing instead of a real repository. This is not a layer to hide functionality, but rather a boundary separating code that may change differently.

Test the operation

[Fact]
public void DamageNeverDropsBelowOne()
{
    var combat = new CombatService();

    int damage = combat.CalculateDamage(5, 100);

    Assert.Equal(1, damage);
}

The test checks externally visible rules rather than simply following the implementation of the private method. Then, you can see whether the same operation is maintained even if the internal structure is organized.

Setting engine boundaries

In Unity, a MonoBehaviour can receive input and life cycle, and a regular C# object can be divided into responsible for the rules. Making all classes a MonoBehaviour makes it easier to connect to the editor, but creation and testing are tied to the engine.

A good structure is not one with a large number of files. Owners of state, lifetime of asynchronous operations, event unsubscription, and external resource boundaries are clearly structured in the code.

C#'s types, interfaces, generics, events, LINQ, and async are not separate technologies. It is a tool used together to divide changeable parts and reduce incorrect states.