【純粋なロジック設計】Unity C#で実践する副作用のないプログラミング

2025/03/22に公開
5

閲覧いただきありがとうございます。はじめまして、ゲーム開発所RYURYUの「りゅうや」と申します。

❏ ゲーム開発ランキング【 1位 】実績多数 (ココナラ)
❏ ココナラ総販売【 220件超 】
❏ GC甲子園2022・東京ゲームショウ2023など出展経験あり

■ Unityを使ったゲーム・VRの受託開発についてのお問い合わせは、Xからお気軽にどうぞ。
https://x.com/RYURYU_GAME_MFG

■ 記事に関するご質問やご意見は、Discordサーバーまでお寄せください。
https://discord.gg/5FwuKCacNy

【純粋なロジック設計】Unity C#で実践する副作用のないプログラミング

Unityを使用したゲーム開発において、コードの品質とメンテナンス性はプロジェクトの成功に直結します。特に、副作用のない純粋なロジック設計は、バグの発生を抑え、チーム開発の効率を高めるために重要です。本記事では、Unity C#で副作用のないプログラミングを実践する方法について詳しく解説します。具体的なコード例や設計パターンを交え、あなたのプロジェクトに即活用できる知識を提供します。

純粋関数と副作用の重要性

純粋関数とは?

純粋関数とは、同じ入力に対して常に同じ出力を返し、副作用がない関数を指します。副作用とは、関数が外部の状態を変更したり、外部からの入力を操作したりすることを指します。純粋関数を使用することで、コードの予測可能性が向上し、テストが容易になります。

副作用がもたらす問題点

副作用が多いコードは以下のような問題を引き起こします:

  • デバッグが困難:状態の変化が予測しにくくなり、バグの原因追及が難しくなります。
  • テストが複雑化:外部依存が増えるため、単体テストが困難になります。
  • 再利用性の低下:特定のコンテキストに依存するため、関数の再利用が難しくなります。

Unityにおける純粋なロジック設計のメリット

コードの予測可能性向上

純粋関数を使用することで、関数の出力が入力に完全に依存するため、コードの挙動が予測しやすくなります。これにより、バグの発見と修正が迅速に行えるようになります。

テストの容易さ

副作用がないため、関数を単体でテストすることが容易です。モックの作成やデータの初期化が不要となり、テストの効率が大幅に向上します。

並行処理の安全性

副作用がないため、複数のスレッドから同時に関数を呼び出しても状態の競合が発生しません。これにより、マルチスレッド環境での開発が容易になります。

Unityで純粋関数を実装する方法

基本的な純粋関数の例

以下は、純粋関数の基本的な例です。この関数は、渡された数値を2倍にして返しますが、外部の状態を変更することはありません。

DoubleFunction.cs
public static class MathUtils
{
    public static int Double(int x)
    {
        return x * 2;
    }
}

副作用を持つ関数との比較

副作用を持つ関数の例として、ゲームオブジェクトの位置を直接変更する関数を見てみましょう。

MoveObject.cs
public class ObjectMover : MonoBehaviour
{
    public void Move(Vector3 newPosition)
    {
        transform.position = newPosition;
    }
}

この関数は外部の状態(transform.position)を変更するため、副作用があります。これに対して純粋関数を使用する場合、状態の変更は関数外で行います。

状態の管理

状態を管理するために、ステートマシンパターンデータドリブンアプローチを採用することで、副作用を最小限に抑えることが可能です。以下は、ステートマシンを使用してキャラクターの状態を管理する例です。

StateMachine.cs
public interface IState
{
    void Enter();
    void Execute();
    void Exit();
}

public class IdleState : IState
{
    public void Enter() { }
    public void Execute() { /* Idle logic */ }
    public void Exit() { }
}

public class StateMachine
{
    private IState currentState;

    public void ChangeState(IState newState)
    {
        currentState?.Exit();
        currentState = newState;
        currentState.Enter();
    }

    public void Update()
    {
        currentState?.Execute();
    }
}

関数型プログラミングの導入

高階関数の活用

高階関数とは、他の関数を引数に取ったり、返り値として返したりする関数のことです。これにより、コードの再利用性が高まり、ロジックの分離が容易になります。

HigherOrderFunction.cs
public static class FunctionalUtils
{
    public static Func<T, TResult> Compose<T, TIntermediate, TResult>(
        Func<T, TIntermediate> first,
        Func<TIntermediate, TResult> second)
    {
        return x => second(first(x));
    }
}

リンケージの活用

リンケージ(関数の合成)は、複雑なロジックを単純な関数の組み合わせとして表現する手法です。これにより、コードの可読性と保守性が向上します。

副作用を管理する設計パターン

イミュータブルデータ構造

データ構造をイミュータブル(不変)にすることで、データの予期しない変更を防ぎます。以下は、イミュータブルなデータクラスの例です。

ImmutableData.cs
public class PlayerState
{
    public readonly int Health;
    public readonly Vector3 Position;

    public PlayerState(int health, Vector3 position)
    {
        Health = health;
        Position = position;
    }

    public PlayerState WithHealth(int newHealth)
    {
        return new PlayerState(newHealth, Position);
    }

    public PlayerState WithPosition(Vector3 newPosition)
    {
        return new PlayerState(Health, newPosition);
    }
}

DI(依存性注入)パターン

依存性注入を用いることで、クラス間の依存関係を明確にし、副作用を最小限に抑えることができます。

DependencyInjection.cs
public interface ILogger
{
    void Log(string message);
}

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Debug.Log(message);
    }
}

public class GameService
{
    private readonly ILogger logger;

    public GameService(ILogger logger)
    {
        this.logger = logger;
    }

    public void StartGame()
    {
        logger.Log("Game Started");
    }
}

テストの実践

単体テストの重要性

副作用のない純粋関数は、単体テストの効率を大幅に向上させます。以下は、先ほどのMathUtils.Double関数の単体テスト例です。

MathUtilsTests.cs
using NUnit.Framework;

public class MathUtilsTests
{
    [Test]
    public void Double_ReturnsCorrectValue()
    {
        int input = 5;
        int expected = 10;
        int result = MathUtils.Double(input);
        Assert.AreEqual(expected, result);
    }
}

テスト駆動開発(TDD)の導入

テスト駆動開発を採用することで、コードの品質を高めつつ、設計時点で副作用を意識したアーキテクチャを構築できます。

副作用のないプログラミングの実践例

ゲームロジックの設計

以下は、ゲーム内のスコア管理を純粋関数で実装する例です。

ScoreManager.cs
public static class ScoreManager
{
    public static int AddScore(int currentScore, int points)
    {
        return currentScore + points;
    }

    public static int SubtractScore(int currentScore, int points)
    {
        return currentScore - points;
    }
}

データフローの最適化

データフローを明確にすることで、副作用を管理しやすくなります。以下は、データフローを管理するための例です。

DataFlow.cs
public class GameController
{
    private int playerScore = 0;

    public void CollectItem(int points)
    {
        playerScore = ScoreManager.AddScore(playerScore, points);
        UpdateUI(playerScore);
    }

    private void UpdateUI(int score)
    {
        // UIの更新ロジック
    }
}

デザインパターンの活用

クリーンアーキテクチャの採用

クリーンアーキテクチャを採用することで、依存関係を明確にし、副作用を最小限に抑えることができます。以下は、クリーンアーキテクチャの基本構造です。

レイヤー 説明
エンティティ ビジネスロジックを担う。純粋関数が多く含まれる。
ユースケース アプリケーションの具体的な動作を管理する。
インターフェース ユーザーインターフェースやデータベースとのやり取りを管理する。
フレームワーク 外部のライブラリやフレームワークに依存する部分を管理する。

リポジトリパターンの導入

データアクセスをリポジトリパターンで抽象化することで、副作用を管理しやすくなります。

RepositoryPattern.cs
public interface IPlayerRepository
{
    PlayerState GetPlayerState();
    void SavePlayerState(PlayerState state);
}

public class PlayerRepository : IPlayerRepository
{
    public PlayerState GetPlayerState()
    {
        // データ取得ロジック
    }

    public void SavePlayerState(PlayerState state)
    {
        // データ保存ロジック
    }
}

コードのリファクタリング

不純な関数の純粋化

既存の副作用を持つ関数を純粋関数にリファクタリングする方法を紹介します。例えば、以下の関数は副作用を持つため、純粋化が必要です。

OriginalFunction.cs
public class Enemy
{
    public int Health { get; private set; }

    public void TakeDamage(int damage)
    {
        Health -= damage;
        if (Health <= 0)
        {
            Die();
        }
    }

    private void Die()
    {
        // 死亡処理
    }
}

この関数を純粋関数にリファクタリングすると以下のようになります。

RefactoredFunction.cs
public static class EnemyUtils
{
    public static EnemyState TakeDamage(EnemyState state, int damage)
    {
        int newHealth = state.Health - damage;
        if (newHealth <= 0)
        {
            // 死亡処理を外部で行う
            return new EnemyState(newHealth, true);
        }
        return new EnemyState(newHealth, state.IsDead);
    }
}

public class EnemyState
{
    public int Health { get; }
    public bool IsDead { get; }

    public EnemyState(int health, bool isDead)
    {
        Health = health;
        IsDead = isDead;
    }
}

実践的な設計パターン

MVC(Model-View-Controller)と関数型アプローチの融合

MVCパターンと関数型アプローチを組み合わせることで、拡張性と保守性を高めることができます。

MVCExample.cs
public class PlayerModel
{
    public int Health { get; private set; }

    public void ApplyDamage(int damage)
    {
        Health = MathUtils.Double(damage); // 純粋関数を利用
    }
}

public class PlayerView
{
    public void UpdateHealthDisplay(int health)
    {
        // UI更新ロジック
    }
}

public class PlayerController
{
    private PlayerModel model;
    private PlayerView view;

    public PlayerController(PlayerModel m, PlayerView v)
    {
        model = m;
        view = v;
    }

    public void OnDamageReceived(int damage)
    {
        model.ApplyDamage(damage);
        view.UpdateHealthDisplay(model.Health);
    }
}

開発のベストプラクティス

コードレビューの重要性

コードレビューを通じて、副作用を持つコードが混入していないか確認することが重要です。チーム全体で純粋関数の重要性を共有し、ベストプラクティスを遵守しましょう。

ドキュメンテーション

純粋関数や副作用のない設計に関するドキュメンテーションを整備することで、新しいチームメンバーの理解を助け、プロジェクト全体の品質を向上させます。

副作用のないプログラミングの課題と解決策

学習コスト

関数型プログラミングの概念に慣れるまでには時間がかかることがあります。しかし、以下の方法でスムーズに導入できます:

  • チームでの勉強会:定期的な勉強会を開催し、知識を共有する。
  • 小さなプロジェクトから始める:小規模なプロジェクトで試行錯誤し、理解を深める。

パフォーマンス

純粋関数の多用は、場合によってはパフォーマンスに影響を与えることがあります。しかし、適切な最適化を行うことで、この問題を軽減できます。

まとめ

純粋なロジック設計をUnity C#で実践することは、コードの品質向上とメンテナンス性の向上に直結します。副作用のないプログラミングを採用することで、バグの発生を抑え、テストの効率を高めることが可能です。関数型プログラミングの概念や設計パターンを活用し、プロジェクトの成功に繋げましょう。

純粋関数のメリット

純粋関数を導入することで、コードの可読性と再利用性が向上し、バグの発生を抑えることができます。特に、テストが容易になるため、品質保証が容易になります。

Unityをもっと極めたい"あなた"へ ― 今すぐスキルアップのチャンス!

1. どこでもUnity教室「無料プラン」

❏ 毎日の質問で即解決|Unityに関する疑問や悩みは、専用Discordでプロの仲間とシェア!

  • 月額0円 で、テキストで気軽に質問・進捗共有が可能
  • 実績多数のコミュニティで、参加するだけで具体的な課題解決のヒントが手に入る

まずは無料で参加して、あなたのUnity学習を加速させましょう! 無料でDiscordに参加する]
https://discord.gg/5FwuKCacNy

2. Unity超入門書【1,000円】

Unityスキルを5日間でマスター|「実践×即戦力」を手に入れる!

  • 130,000文字超の詳細な解説と実例で、初心者でもすぐにUnityの基礎が身につく
  • 実際の成果例:5日間でシンプルな3D FPSゲームを完成
  • 専属講師サポートのオプション付きで、疑問を即解消しながら学習を進められる

まずはこの教材でUnity開発の第一歩を体験してください! 教材を今すぐ購入する
https://zenn.dev/ryuryu_game/books/fd28de9d8e963a/viewer/0570af

3. Unity超入門完全支援プラン【単発24,800円】

Unityの全てをプロがバックアップ|教材で学んだ内容を実践サポート!

  • 専属講師による24時間テキスト質問サポート(毎日17:00~21:00の回答)
  • 月2回×60分 または 月1回×120分のビデオチャットで、学習進捗やプロジェクトの具体的な課題を徹底サポート
  • 教材と連携し、実践の現場での疑問や課題をそのまま解決!
  • 限定:1度に最大10名様のみ受付!早期申込で安心のサポート体制を

教材で学んだ知識をさらに深め、実践に活かすならこのプランがおすすめです! 今すぐ詳細を確認する
https://ryuryu.memberpay.jp/service/item/yjo1sst

4. Unityプロジェクト完全支援プラン【月額48,000円】

Unityプロジェクトを本格サポート|個人の趣味からプロの現場まで幅広く対応!

  • 専属講師による24時間テキスト質問サポート(毎日17:00~21:00の回答)
  • 月2回×60分のビデオチャットで、プロジェクトの進行状況を細かくサポート
  • Unity開発の一部を代行するサービスが常に20%割引で利用可能
  • 基本操作からエラー対応、プロジェクト設計のアドバイスまで幅広くサポート
  • 専用Discordサーバーでのサポート体制(ご購入後に招待リンクを送付)

個人プロジェクトを着実に進め、より高い成果を求めるあなたに最適なプランです! 今すぐプロジェクト支援プランを確認する
https://ryuryu.memberpay.jp/plan/item/epeiywt

※ Unity超入門書で学んだ内容を、さらに深く実践に活かしたい方は、完全支援プランとプロジェクト支援プランの併用がおすすめです!

5

Discussion