Maemaemae

Unity C#で構造体を使いこなすベストプラクティス

Unityで開発する際、構造体(struct)の適切な使用はパフォーマンスとコード品質に大きな影響を与えます。この記事では、Unity C#での構造体活用に関する実践的なアプローチを紹介します。

構造体とクラスの選択基準

構造体を選ぶべき場面

// 例: 3次元の座標を表す小さな構造体
public struct Position
{
    public float x;
    public float y;
    public float z;
}

クラスを選ぶべき場面

// 例: 複雑なゲームオブジェクトデータ
public class EnemyData
{
    public string name;
    public List<Ability> abilities;
    public Dictionary<string, float> stats;
}

効率的な構造体設計

サイズを最小限に保つ

// 良い例: 必要最小限のフィールドを持つ構造体
public struct PlayerInput
{
    public float horizontal;
    public float vertical;
    public bool jumpPressed;
}

// 避けるべき例: 大きすぎる構造体
public struct GameState
{
    public string levelName;  // 文字列は参照型
    public List<Enemy> enemies;  // コレクションは参照型
    public Dictionary<int, PlayerData> players;
    // 他多数のデータ...
}

イミュータブルデザイン

// イミュータブルな構造体の例
public readonly struct GameTime
{
    private readonly float elapsedTime;

    public GameTime(float time)
    {
        elapsedTime = time;
    }

    public float ElapsedTime => elapsedTime;

    // 変更する代わりに新しいインスタンスを返す
    public GameTime AddTime(float deltaTime) => new GameTime(elapsedTime + deltaTime);
}

メソッド引数での利用

// 小さな構造体は値渡し
public void MoveCharacter(Vector3 direction)
{
    transform.position += direction * speed * Time.deltaTime;
}

// 大きめの構造体は参照渡し
public void ProcessPhysics(ref PhysicsData physicsData)
{
    // データを処理・更新
}

// 読み取り専用の場合はinキーワード
public float CalculateTrajectory(in ProjectileData data)
{
    // データを読み取るだけの処理
    return /* 計算結果 */;
}

Unityでの構造体活用パターン

ECS/DOTS との連携

using Unity.Entities;
using Unity.Mathematics;

// DOTSでのコンポーネントとして使用する構造体
public struct TransformComponent : IComponentData
{
    public float3 position;
    public quaternion rotation;
    public float3 scale;
}

Jobs Systemでの活用

using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;

[BurstCompile]
public struct ParticleUpdateJob : IJobParallelFor
{
    public NativeArray<float3> positions;
    public NativeArray<float3> velocities;
    public float deltaTime;

    public void Execute(int index)
    {
        positions[index] += velocities[index] * deltaTime;
    }
}

マスデータ構造

// ゲーム内のタイルデータを表す構造体
public struct TileData
{
    public byte type;
    public byte elevation;
    public ushort flags;

    // 32ビットに収まるようパッキング
}

// 使用例
public class ChunkManager : MonoBehaviour
{
    private NativeArray<TileData> tiles;
    private const int ChunkSize = 16 * 16;

    void Awake()
    {
        tiles = new NativeArray<TileData>(ChunkSize, Allocator.Persistent);
    }

    void OnDestroy()
    {
        if (tiles.IsCreated)
            tiles.Dispose();
    }
}

よくある問題と解決策

ボックス化の回避

// 問題: ボックス化が発生するコード
private List<Vector3> positions = new List<Vector3>();
Dictionary<Vector3, int> positionMap = new Dictionary<Vector3, int>();

// 解決策: ジェネリック型やNativeCollectionの活用
private NativeList<Vector3> positions;
private NativeParallelHashMap<Vector3, int> positionMap;

誤ったコピーセマンティクス

// 問題: 構造体のフィールドを直接変更できない
public struct Enemy
{
    public Vector3 position;
    public int health;
}

void UpdateEnemy()
{
    Enemy enemy = enemies[0];
    enemy.position += Vector3.forward; // これだけではenemies[0]は更新されない
    enemies[0] = enemy; // 正しい方法: 変更後に再代入する
}

過度なメモリ割り当て

// 問題: 大きな構造体の過剰コピー
public struct LargeData
{
    public Vector3[] points; // 配列は参照なのでOK
    public float[,] heightMap; // 2次元配列も参照
}

// メソッド呼び出しでのコピー問題を解決
public void ProcessData(ref LargeData data)
{
    // refで参照渡しすることでコピーを防止
}

パフォーマンス計測

Profilerでの比較

// 構造体とクラスの性能比較例
public class PerformanceTest : MonoBehaviour
{
    private const int Count = 100000;

    void Start()
    {
        TestStructs();
        TestClasses();
    }

    void TestStructs()
    {
        Profiler.BeginSample("Struct Creation");
        var positions = new Vector3[Count];
        for (int i = 0; i < Count; i++)
            positions[i] = new Vector3(i, i, i);
        Profiler.EndSample();
    }

    void TestClasses()
    {
        Profiler.BeginSample("Class Creation");
        var positions = new Vector3Wrapper[Count];
        for (int i = 0; i < Count; i++)
            positions[i] = new Vector3Wrapper(i, i, i);
        Profiler.EndSample();
    }
}

public class Vector3Wrapper
{
    public float x, y, z;
    public Vector3Wrapper(float x, float y, float z)
    {
        this.x = x; this.y = y; this.z = z;
    }
}

実装ガイドライン

  1. 構造体サイズを小さく保つ: 16バイト以下が理想的
  2. 参照型フィールドを最小限に: 参照型が多いとボックス化が増える
  3. イミュータブルな設計を検討: 変更操作には新しいインスタンスを返す
  4. 大きな構造体はref/in修飾子で渡す: コピーのオーバーヘッドを軽減
  5. Unity固有のコレクションを活用: NativeArrayやNativeList等
  6. 構造体のコレクションで連続アクセスを意識: キャッシュ効率の向上

参考資料


Unity 2022.3LTS以降およびC# 9.0以上を想定しています。バージョンによって一部挙動が異なる場合があります。

UnityC#構造体パフォーマンス最適化