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 | 備考 |
---|---|---|---|
プロパティパターン | 8 | 2020.3 LTS 以降 | .NET Standard 2.1 対応 |
リレーショナルパターン (<=, >, など) | 9 | 2022.2 以降 (Roslyn 4.x) | |
スイッチ式 | 8 | 2020.3 LTS 以降 |
6. ベストプラクティス & 注意点
- 網羅性を保つ: 最後に _ => を書いて デフォルト動作 を保証するか、コンパイラ警告 (CS8509) を解消しましょう。
- 副作用の重複を避ける: 複数ケースで同じ処理を呼ぶときは、メソッド抽出で共通化すると可読性が上がります。
- テスト容易性: 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 以降なら 追加設定なし で利用可能
ぜひ既存コードを見直し、パターンマッチングを武器 にクリーンなステート管理を取り入れてみてください。
ディスカッション
コメント一覧
まだ、コメントがありません