C# パターンマッチング超活用 — 

switch

 式でゲームのステート管理を綺麗に

対象読者

  • C# 8 以降の新機能を取り入れたいプログラマ
  • Unity(2022.2 以降)でゲームの状態遷移を実装している開発者
  • 既存の enum + if スパゲッティを卒業したい方

この記事で扱うもの

  • switch  と パターンマッチング の基本
  • PlayerState { HP: <= 0 } => Die() 形式の読み解き方
  • 優先順位 と 網羅性(exhaustiveness)の設計ポイント
  • Unity プロジェクトへの導入例(MonoBehaviour & StateMachineBehaviour)
  • C# バージョンおよび Unity の対応状況

1. イントロダクション — “宣言的” な状態分岐へ

ゲーム開発では「HP が 0 以下なら死亡」「スタン中なら操作不能」といった 状態遷移 が至る所に現れます。従来の if/else if/else は簡単ですが、条件が増えるほど可読性と保守性が下がります。

if (currentState.HP <= 0)
{
    Die();
}
else if (currentState.IsStunned)
{
    Stun();
}
else
{
    Idle();
}

C# 8 以降で導入された パターンマッチング は、これらの条件を 宣言的 に書ける強力な機能です。以下の switch 式を見てください。

_ = currentState switch
{
    PlayerState { HP: <= 0 }        => Die(),
    PlayerState { IsStunned: true } => Stun(),
    _                               => Idle(),
};
  • プロパティパターン(Type { Prop: pattern })
  • リレーショナルパターン(<= 0 など)

を組み合わせることで、複雑な条件をシンプルに表現できます。

以下は そのままビルドして動かせる最小構成 のコンソール アプリ版サンプルです。

Die / Stun / Idle は「何か処理をして ダミーの戻り値 を返す」ように実装し、switch  が値を返すという C# の要件を満たしています。


Program.cs

using System;

namespace SwitchPatternDemo
{
    // プレイヤー状態を表すレコード型
    public record PlayerState(int HP, bool IsStunned);

    class Program
    {
        static void Main()
        {
            // テスト用に 3 つの状態を用意
            var states = new[]
            {
                new PlayerState(HP: 10, IsStunned: false), // 通常
                new PlayerState(HP:  0, IsStunned: false), // HP 0 → 死亡
                new PlayerState(HP:  5, IsStunned: true)   // スタン
            };

            foreach (var currentState in states)
            {
                // 戻り値 (int) を _ に捨てることで副作用だけを実行
                _ = currentState switch
                {
                    PlayerState { HP: <= 0 }        => Die(),
                    PlayerState { IsStunned: true } => Stun(),
                    _                               => Idle(),
                };
            }
        }

        // 以降 3 メソッドは何らかの処理後に int を返す
        private static int Die()
        {
            Console.WriteLine("Die() called   → プレイヤー死亡");
            return 0;
        }

        private static int Stun()
        {
            Console.WriteLine("Stun() called  → スタン処理");
            return 0;
        }

        private static int Idle()
        {
            Console.WriteLine("Idle() called  → 何もしない");
            return 0;
        }
    }
}

実行結果例

Idle() called  → 何もしない
Die() called   → プレイヤー死亡
Stun() called  → スタン処理

Unity で使う場合(MonoBehaviour)

using UnityEngine;

public record PlayerState(int HP, bool IsStunned);

public class PlayerController : MonoBehaviour
{
    [SerializeField] int hp = 10;
    [SerializeField] bool isStunned;

    void Update()
    {
        var currentState = new PlayerState(hp, isStunned);

        _ = currentState switch
        {
            PlayerState { HP: <= 0 }        => Die(),
            PlayerState { IsStunned: true } => Stun(),
            _                               => Idle(),
        };
    }

    // ここでは int を返して switch 式の要件を満たしている
    private int Die()
    {
        Debug.Log("Die() called");
        // アニメ・エフェクト・シーン遷移など
        return 0;
    }

    private int Stun()
    {
        Debug.Log("Stun() called");
        // 入力無効処理など
        return 0;
    }

    private int Idle()
    {
        // とくに何もせず 0 を返す
        return 0;
    }
}

ポイント

  • void メソッドは式の戻り値に使えないため、上記のように ダミーの戻り値 (ここでは int) を返すメソッド にしておくと switch 式で利用できます。
  • 副作用(アニメ再生・状態変更など)だけが目的なら、switch  に書き換えるのも選択肢です。

これで提示された switch パターン マッチングがそのまま動作します。必要に応じて返り値の型や内部処理を差し替えてください。


2. パターンごとの詳解

パターンマッチ条件使用構文
PlayerState { HP: <= 0 }currentState が PlayerState かつ HP が 0 以下プロパティパターン + リレーショナルパターン
PlayerState { IsStunned: true }IsStunned プロパティが trueプロパティパターン + 定数パターン

2.1 順序が意味するもの

switch は 上から順に 評価されます。同時に複数の条件を満たす場合は先頭のケースが勝ちます。例えば「HP 0、かつスタン中」のときに死亡処理を優先したいのか、スタン処理を優先したいのかで並び順を調整しましょう。

2.2 when 句で複合条件

特定の武器を装備している場合のみ特殊ダウン演出を挟みたい、といった場合は when 句を追加できます。

_ = currentState switch
{
    PlayerState ps when ps.HP <= 0 && ps.HasSpecialWeapon => SpecialDie(),
    PlayerState { HP: <= 0 }                             => Die(),
    PlayerState { IsStunned: true }                      => Stun(),
    _                                                    => Idle(),
};

3. “式” としての switch — 戻り値 vs 副作用

switch  は必ず値を返します。とはいえゲームロジックでは 処理を実行するだけ の場合も多いでしょう。そのときは戻り値を _ に捨てるか、switch  に書き換える方法があります。

3.1 文形式

switch (currentState)
{
    case PlayerState { HP: <= 0 }:
        Die();
        break;
    case PlayerState { IsStunned: true }:
        Stun();
        break;
    default:
        Idle();
        break;
}

文形式なら戻り値を意識する必要がありません。一方で ラムダ → 処理 の簡潔さは失われます。コード規約と開発チームの好みで使い分けましょう。


4. Unity への組み込み例

4.1 MonoBehaviour + Update ループ

public class PlayerController : MonoBehaviour
{
    PlayerState currentState;

    void Update()
    {
        _ = currentState switch
        {
            PlayerState { HP: <= 0 }        => Die(),
            PlayerState { IsStunned: true } => Stun(),
            _                               => Idle(),
        };
    }

    private void Die()  { /* アニメ・SE 再生 */ }
    private void Stun() { /* 一定時間入力無効 */ }
    private void Idle() { /* 通常行動 */ }
}

4.2 StateMachineBehaviour との相性

Animator ステートマシンを拡張する StateMachineBehaviour でも同じく活用できます。ステートごとの 条件ガード を分けたいときに便利です。


5. バージョン互換性

機能C#Unity備考
プロパティパターン82020.3 LTS 以降.NET Standard 2.1 対応
リレーショナルパターン (<=, >, など)92022.2 以降 (Roslyn 4.x)
スイッチ式82020.3 LTS 以降

6. ベストプラクティス & 注意点

  1. 網羅性を保つ: 最後に _ => を書いて デフォルト動作 を保証するか、コンパイラ警告 (CS8509) を解消しましょう。
  2. 副作用の重複を避ける: 複数ケースで同じ処理を呼ぶときは、メソッド抽出で共通化すると可読性が上がります。
  3. テスト容易性: switch 式は戻り値を返せるため、ステート名 を返す形にして Unit Test で分岐ロジックだけを確認するのも一手です。
PlayerAction action = currentState switch
{
    PlayerState { HP: <= 0 }        => PlayerAction.Die,
    PlayerState { IsStunned: true } => PlayerAction.Stun,
    _                               => PlayerAction.Idle,
};
Assert.AreEqual(PlayerAction.Die, action); // NUnit

7. まとめ

パターンマッチングは「条件分岐=命令的」という従来の発想を覆し、宣言的 で読みやすいコードを実現します。Unity においても MonoBehaviour.Update, StateMachineBehaviour, さらに DOTween のアニメーション完了コールバック処理など、多くの場所で恩恵があります。

  • 条件増加による メンテナンスコスト を劇的に削減
  • リレーショナル & プロパティパターンで 複雑条件を一行表現
  • Unity 2022.2 以降なら 追加設定なし で利用可能

ぜひ既存コードを見直し、パターンマッチングを武器 にクリーンなステート管理を取り入れてみてください。

訪問数 19 回, 今日の訪問数 1回

C#,Unity

Posted by hidepon