unity-ecs-patterns
Unity ECSをDOTS・Jobs・Burstとともに活用し、ハイパフォーマンスなゲーム開発をマスターするスキルです。データ指向のゲーム設計、パフォーマンス最適化、または大量のエンティティを扱う場面で使用してください。
description の原文を見る
Master Unity ECS (Entity Component System) with DOTS, Jobs, and Burst for high-performance game development. Use when building data-oriented games, optimizing performance, or working with large entity counts.
SKILL.md 本文
Unity ECS パターン
Entity Component System、Job System、Burst Compiler を含む Unity の Data-Oriented Technology Stack (DOTS) の本番環境向けパターン。
このスキルを使用する場合
- 高性能な Unity ゲームの構築
- 数千のエンティティを効率的に管理
- データ指向のゲームシステムの実装
- CPU バウンドなゲームロジックの最適化
- OOP ゲームコードの ECS への変換
- 並列化のための Jobs と Burst の使用
コアコンセプト
1. ECS vs OOP
| 側面 | 従来の OOP | ECS/DOTS |
|---|---|---|
| データレイアウト | オブジェクト指向 | データ指向 |
| メモリ | 散在している | 連続した |
| 処理 | オブジェクト単位 | バッチ処理 |
| スケーラビリティ | 数が多いと悪化 | 線形スケーリング |
| 最適な用途 | 複雑な動作 | 大規模シミュレーション |
2. DOTS コンポーネント
Entity: 軽量な ID (データなし)
Component: 純粋なデータ (動作なし)
System: コンポーネントを処理するロジック
World: エンティティのコンテナ
Archetype: コンポーネントの一意な組み合わせ
Chunk: 同じアーキタイプのエンティティ向けメモリブロック
パターン
パターン 1: 基本的な ECS セットアップ
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Burst;
using Unity.Collections;
// コンポーネント: 純粋なデータ、メソッドなし
public struct Speed : IComponentData
{
public float Value;
}
public struct Health : IComponentData
{
public float Current;
public float Max;
}
public struct Target : IComponentData
{
public Entity Value;
}
// タグコンポーネント (ゼロサイズマーカー)
public struct EnemyTag : IComponentData { }
public struct PlayerTag : IComponentData { }
// バッファコンポーネント (可変サイズ配列)
[InternalBufferCapacity(8)]
public struct InventoryItem : IBufferElementData
{
public int ItemId;
public int Quantity;
}
// 共有コンポーネント (エンティティをグループ化)
public struct TeamId : ISharedComponentData
{
public int Value;
}
パターン 2: ISystem を使用したシステム (推奨)
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
using Unity.Burst;
// ISystem: アンマネージド、Burst 互換、最高のパフォーマンス
[BurstCompile]
public partial struct MovementSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// システム実行前にコンポーネントを要求
state.RequireForUpdate<Speed>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float deltaTime = SystemAPI.Time.DeltaTime;
// シンプルな foreach - ジョブ自動生成
foreach (var (transform, speed) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Speed>>())
{
transform.ValueRW.Position +=
new float3(0, 0, speed.ValueRO.Value * deltaTime);
}
}
[BurstCompile]
public void OnDestroy(ref SystemState state) { }
}
// より詳細な制御用の明示的ジョブ
[BurstCompile]
public partial struct MovementJobSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var job = new MoveJob
{
DeltaTime = SystemAPI.Time.DeltaTime
};
state.Dependency = job.ScheduleParallel(state.Dependency);
}
}
[BurstCompile]
public partial struct MoveJob : IJobEntity
{
public float DeltaTime;
void Execute(ref LocalTransform transform, in Speed speed)
{
transform.Position += new float3(0, 0, speed.Value * DeltaTime);
}
}
パターン 3: エンティティクエリ
[BurstCompile]
public partial struct QueryExamplesSystem : ISystem
{
private EntityQuery _enemyQuery;
public void OnCreate(ref SystemState state)
{
// 複雑なケース用に手動でクエリを構築
_enemyQuery = new EntityQueryBuilder(Allocator.Temp)
.WithAll<EnemyTag, Health, LocalTransform>()
.WithNone<Dead>()
.WithOptions(EntityQueryOptions.FilterWriteGroup)
.Build(ref state);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// SystemAPI.Query - 最もシンプルなアプローチ
foreach (var (health, entity) in
SystemAPI.Query<RefRW<Health>>()
.WithAll<EnemyTag>()
.WithEntityAccess())
{
if (health.ValueRO.Current <= 0)
{
// 破棄用にマーク
SystemAPI.GetSingleton<EndSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged)
.DestroyEntity(entity);
}
}
// カウント取得
int enemyCount = _enemyQuery.CalculateEntityCount();
// すべてのエンティティ取得
var enemies = _enemyQuery.ToEntityArray(Allocator.Temp);
// コンポーネント配列取得
var healths = _enemyQuery.ToComponentDataArray<Health>(Allocator.Temp);
}
}
パターン 4: エンティティコマンドバッファ (構造的変更)
// 構造的変更 (作成/破棄/追加/削除) にはコマンドバッファが必要
[BurstCompile]
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct SpawnSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
foreach (var (spawner, transform) in
SystemAPI.Query<RefRW<Spawner>, RefRO<LocalTransform>>())
{
spawner.ValueRW.Timer -= SystemAPI.Time.DeltaTime;
if (spawner.ValueRO.Timer <= 0)
{
spawner.ValueRW.Timer = spawner.ValueRO.Interval;
// エンティティ作成 (同期ポイントまで遅延)
Entity newEntity = ecb.Instantiate(spawner.ValueRO.Prefab);
// コンポーネント値を設定
ecb.SetComponent(newEntity, new LocalTransform
{
Position = transform.ValueRO.Position,
Rotation = quaternion.identity,
Scale = 1f
});
// コンポーネント追加
ecb.AddComponent(newEntity, new Speed { Value = 5f });
}
}
}
}
// 並列 ECB 使用法
[BurstCompile]
public partial struct ParallelSpawnJob : IJobEntity
{
public EntityCommandBuffer.ParallelWriter ECB;
void Execute([EntityIndexInQuery] int index, in Spawner spawner)
{
Entity e = ECB.Instantiate(index, spawner.Prefab);
ECB.AddComponent(index, e, new Speed { Value = 5f });
}
}
パターン 5: Aspect (コンポーネントのグループ化)
using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;
// Aspect: 関連するコンポーネントをグループ化してコードをクリーンに
public readonly partial struct CharacterAspect : IAspect
{
public readonly Entity Entity;
private readonly RefRW<LocalTransform> _transform;
private readonly RefRO<Speed> _speed;
private readonly RefRW<Health> _health;
// オプショナルコンポーネント
[Optional]
private readonly RefRO<Shield> _shield;
// バッファ
private readonly DynamicBuffer<InventoryItem> _inventory;
public float3 Position
{
get => _transform.ValueRO.Position;
set => _transform.ValueRW.Position = value;
}
public float CurrentHealth => _health.ValueRO.Current;
public float MaxHealth => _health.ValueRO.Max;
public float MoveSpeed => _speed.ValueRO.Value;
public bool HasShield => _shield.IsValid;
public float ShieldAmount => HasShield ? _shield.ValueRO.Amount : 0f;
public void TakeDamage(float amount)
{
float remaining = amount;
if (HasShield && _shield.ValueRO.Amount > 0)
{
// シールドがダメージを最初に吸収
remaining = math.max(0, amount - _shield.ValueRO.Amount);
}
_health.ValueRW.Current = math.max(0, _health.ValueRO.Current - remaining);
}
public void Move(float3 direction, float deltaTime)
{
_transform.ValueRW.Position += direction * _speed.ValueRO.Value * deltaTime;
}
public void AddItem(int itemId, int quantity)
{
_inventory.Add(new InventoryItem { ItemId = itemId, Quantity = quantity });
}
}
// システムで Aspect を使用
[BurstCompile]
public partial struct CharacterSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var character in SystemAPI.Query<CharacterAspect>())
{
character.Move(new float3(1, 0, 0), dt);
if (character.CurrentHealth < character.MaxHealth * 0.5f)
{
// 低 HP ロジック
}
}
}
}
パターン 6: シングルトンコンポーネント
// シングルトン: このコンポーネントを持つエンティティは正確に 1 つ
public struct GameConfig : IComponentData
{
public float DifficultyMultiplier;
public int MaxEnemies;
public float SpawnRate;
}
public struct GameState : IComponentData
{
public int Score;
public int Wave;
public float TimeRemaining;
}
// ワールド作成時にシングルトンを作成
public partial struct GameInitSystem : ISystem
{
public void OnCreate(ref SystemState state)
{
var entity = state.EntityManager.CreateEntity();
state.EntityManager.AddComponentData(entity, new GameConfig
{
DifficultyMultiplier = 1.0f,
MaxEnemies = 100,
SpawnRate = 2.0f
});
state.EntityManager.AddComponentData(entity, new GameState
{
Score = 0,
Wave = 1,
TimeRemaining = 120f
});
}
}
// システムでシングルトンにアクセス
[BurstCompile]
public partial struct ScoreSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
// シングルトン読み取り
var config = SystemAPI.GetSingleton<GameConfig>();
// シングルトン書き込み
ref var gameState = ref SystemAPI.GetSingletonRW<GameState>().ValueRW;
gameState.TimeRemaining -= SystemAPI.Time.DeltaTime;
// 存在確認
if (SystemAPI.HasSingleton<GameConfig>())
{
// ...
}
}
}
パターン 7: Baking (GameObjects の変換)
using Unity.Entities;
using UnityEngine;
// オーサリングコンポーネント (エディタの MonoBehaviour)
public class EnemyAuthoring : MonoBehaviour
{
public float Speed = 5f;
public float Health = 100f;
public GameObject ProjectilePrefab;
class Baker : Baker<EnemyAuthoring>
{
public override void Bake(EnemyAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Speed { Value = authoring.Speed });
AddComponent(entity, new Health
{
Current = authoring.Health,
Max = authoring.Health
});
AddComponent(entity, new EnemyTag());
if (authoring.ProjectilePrefab != null)
{
AddComponent(entity, new ProjectilePrefab
{
Value = GetEntity(authoring.ProjectilePrefab, TransformUsageFlags.Dynamic)
});
}
}
}
}
// 依存関係を持つ複雑な Baking
public class SpawnerAuthoring : MonoBehaviour
{
public GameObject[] Prefabs;
public float Interval = 1f;
class Baker : Baker<SpawnerAuthoring>
{
public override void Bake(SpawnerAuthoring authoring)
{
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Spawner
{
Interval = authoring.Interval,
Timer = 0f
});
// プリファブのバッファを Bake
var buffer = AddBuffer<SpawnPrefabElement>(entity);
foreach (var prefab in authoring.Prefabs)
{
buffer.Add(new SpawnPrefabElement
{
Prefab = GetEntity(prefab, TransformUsageFlags.Dynamic)
});
}
// 依存関係を宣言
DependsOn(authoring.Prefabs);
}
}
}
パターン 8: Native Collections を使用したジョブ
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
using Unity.Mathematics;
[BurstCompile]
public struct SpatialHashJob : IJobParallelFor
{
[ReadOnly]
public NativeArray<float3> Positions;
// ハッシュマップへのスレッドセーフな書き込み
public NativeParallelMultiHashMap<int, int>.ParallelWriter HashMap;
public float CellSize;
public void Execute(int index)
{
float3 pos = Positions[index];
int hash = GetHash(pos);
HashMap.Add(hash, index);
}
int GetHash(float3 pos)
{
int x = (int)math.floor(pos.x / CellSize);
int y = (int)math.floor(pos.y / CellSize);
int z = (int)math.floor(pos.z / CellSize);
return x * 73856093 ^ y * 19349663 ^ z * 83492791;
}
}
[BurstCompile]
public partial struct SpatialHashSystem : ISystem
{
private NativeParallelMultiHashMap<int, int> _hashMap;
public void OnCreate(ref SystemState state)
{
_hashMap = new NativeParallelMultiHashMap<int, int>(10000, Allocator.Persistent);
}
public void OnDestroy(ref SystemState state)
{
_hashMap.Dispose();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var query = SystemAPI.QueryBuilder()
.WithAll<LocalTransform>()
.Build();
int count = query.CalculateEntityCount();
// 必要に応じてリサイズ
if (_hashMap.Capacity < count)
{
_hashMap.Capacity = count * 2;
}
_hashMap.Clear();
// ポジション取得
var positions = query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
var posFloat3 = new NativeArray<float3>(count, Allocator.TempJob);
for (int i = 0; i < count; i++)
{
posFloat3[i] = positions[i].Position;
}
// ハッシュマップを構築
var hashJob = new SpatialHashJob
{
Positions = posFloat3,
HashMap = _hashMap.AsParallelWriter(),
CellSize = 10f
};
state.Dependency = hashJob.Schedule(count, 64, state.Dependency);
// クリーンアップ
positions.Dispose(state.Dependency);
posFloat3.Dispose(state.Dependency);
}
}
パフォーマンステクニック
// 1. どこでも Burst を使用
[BurstCompile]
public partial struct MySystem : ISystem { }
// 2. 手動イテレーションより IJobEntity を推奨
[BurstCompile]
partial struct OptimizedJob : IJobEntity
{
void Execute(ref LocalTransform transform) { }
}
// 3. 可能な限り並列スケジュール
state.Dependency = job.ScheduleParallel(state.Dependency);
// 4. チャンク反復で ScheduleParallel を使用
[BurstCompile]
partial struct ChunkJob : IJobChunk
{
public ComponentTypeHandle<Health> HealthHandle;
public void Execute(in ArchetypeChunk chunk, int unfilteredChunkIndex,
bool useEnabledMask, in v128 chunkEnabledMask)
{
var healths = chunk.GetNativeArray(ref HealthHandle);
for (int i = 0; i < chunk.Count; i++)
{
// 処理
}
}
}
// 5. ホットパスでの構造的変更を回避
// 追加/削除の代わりに有効化可能なコンポーネントを使用
public struct Disabled : IComponentData, IEnableableComponent { }
ベストプラクティス
すべきこと
- SystemBase より ISystem を使用 - パフォーマンスが良い
- すべてを Burst コンパイル - 大幅な高速化
- 構造的変更をバッチ処理 - ECB を使用
- Profiler でプロファイル - ボトルネックを特定
- Aspects を使用 - クリーンなコンポーネントグループ化
すべきでないこと
- マネージド型を使用しない - Burst が動作しない
- ジョブ内での構造的変更をしない - ECB を使用
- 過度に設計しない - シンプルに開始
- チャンク利用率を無視しない - 類似したエンティティをグループ化
- 破棄を忘れない - Native Collections がメモリリークする
ライセンス: MIT(寛容ライセンスのため全文を引用しています) · 原本リポジトリ
詳細情報
- 作者
- wshobson
- リポジトリ
- wshobson/agents
- ライセンス
- MIT
- 最終更新
- 不明
Source: https://github.com/wshobson/agents / ライセンス: MIT
関連スキル
hugging-face-trackio
Trackioを使用してMLトレーニング実験を追跡・可視化できます。トレーニング中のメトリクスログ記録(Python API)、トレーニング診断のアラート発火、ログされたメトリクスの取得・分析(CLI)が必要な場合に活用してください。リアルタイムダッシュボード表示、Webhookを使用したアラート、HF Space同期、自動化向けのJSON出力に対応しています。
btc-bottom-model
ビットコインのサイクルタイミングモデルで、加重スコアリングシステムを搭載しています。日次パルス(4指標、32ポイント)とウィークリー構造(9指標、68ポイント)の2カテゴリーにわたる13の指標を追跡し、0~100のマーケットヒートスコアを算出します。ETFフロー、ファンディングレート、ロング/ショート比率、恐怖・貪欲指数、LTH-MVRV、NUPL、SOPR(LTH+STH)、LTH供給率、移動平均倍率(365日MA、200週MA)、週次RSI、出来高トレンドに対応します。市場サイクル全体を通じて買いと売りの両方の推奨を提供します。ビットコインの底値拾い、BTCサイクルポジション、買い時・売り時、オンチェーン指標、MVRV、NUPL、SOPR、LTH動向、ETFの流出入、ファンディングレート、恐怖指数、ビットコインが過熱状態か、マイナーコスト、暗号資産市場のセンチメント、BTCのポジションサイジング、「今ビットコインを買うべきか」「BTCが天井をつけているか」「オンチェーン指標は何を示しているか」といった質問の際にこのスキルを活用します。
protein_solubility_optimization
タンパク質の溶解性最適化 - タンパク質の溶解性を最適化します。タンパク質の特性を計算し、溶解性と親水性を予測し、有効な変異を提案します。タンパク質配列の特性計算、タンパク質機能の予測、親水性計算、ゼロショット配列予測を含むタンパク質エンジニアリング業務に使用できます。3つのSCPサーバーから4つのツールを統合しています。
research-lookup
Parallel Chat APIまたはPerplexity sonar-pro-searchを使用して、最新の研究情報を検索できます。学術論文の検索にも対応しています。クエリは自動的に最適なバックエンドにルーティングされるため、論文の検索、研究データの収集、科学情報の検証に活用できます。
tree-formatting
ggtree(R)またはiTOL(ウェブ)を使用して、系統樹の可視化とフォーマットを行います。系統樹を図として描画する際、ツリーレイアウトの選択、分類学に基づく枝やラベルの色付け、クレードの折りたたみ、サポート値の表示、またはツリーへのオーバーレイ追加が必要な場合に使用してください。系統推定(protein-phylogenyスキルを使用)やドメイン注釈(今後の独立したスキル)には使用しないでください。
querying-indonesian-gov-data
インドネシア政府の50以上のAPIとデータソースに接続できます。BPJPH(ハラール認証)、BOM(食品安全)、OJK(金融適正性)、BPS(統計)、BMKG(気象・地震)、インドネシア中央銀行(為替レート)、IDX(株式)、CKAN公開データポータル、pasal.id(第三者法MCP)に対応しています。インドネシア政府データを活用したアプリ開発、.go.idウェブサイトのスクレイピング、ハラール認証の確認、企業の法的適正性の検証、金融機関ステータスの照会、またはインドネシアMCPサーバーへの接続時に使用できます。CSRF処理、CKAN API使用方法、IP制限回避など、すぐに実行可能なPythonパターンを含んでいます。