| name | ecs-architecture |
| description | Architecture Entity Component System (ECS/DOTS) — Unity Entities 1.0, Flecs, EnTT, Bevy ECS, data-oriented design, Job System, Burst Compiler, archetypes, systèmes de requête et optimisation cache CPU. |
| tags | ["ecs","dots","unity","flecs","entt","bevy","data-oriented-design","burst","job-system"] |
ECS Architecture — Guide Complet
Ce skill couvre l'architecture Entity Component System (ECS) et le Data-Oriented Design (DOD) pour le développement de jeux. À charger pour toute tâche impliquant Unity DOTS, Flecs, EnTT, Bevy ECS, ou la conception orientée données.
1. Principes Fondamentaux du Data-Oriented Design
Problème de l'OOP classique
OOP (Objet): Player { Transform, Rigidbody, Health, MeshRenderer, Collider }
→ Mémoire dispersée (cache misses)
→ Virtual table overhead
→ Difficulté de parallélisation
ECS (Données): Position[] (tableau contigu)
Velocity[] (tableau contigu)
Health[] (tableau contigu)
→ Cache CPU friendly (SoA)
→ Pas de vtable
→ Parallélisable trivialement
AoS vs SoA
AoS (Array of Structs) | SoA (Struct of Arrays)
struct Particle { | struct ParticleSystem {
float3 pos; | float3* positions;
float3 vel; | float3* velocities;
float life; | float* lifetimes;
float mass; | float* masses;
}; | };
|
Cache miss élevé si on ne | Cache friendly: on lit/écrit
lit que la position | SEULEMENT ce dont on a besoin
Itérration chaude vs froide
Séparer les données chaudes des données froides = principe clé du DOD.
2. Unity DOTS (Entities 1.0)
Architecture DOTS
World
├── EntityManager # Crée/détruit des entités, ajoute/retire des components
├── EntityCommandBuffer # File les changements structuraux (playback différé)
├── SystemState # Lifecycle des systèmes
└── EntityQuery # Requêtes sur les composants
└── Archetype # Groupe d'entités avec les MÊMES types de composants
├── Chunk # Bloc mémoire contigu (16KB = ArchetypeChunk)
│ ├── EntityA → Position, Velocity, Health
│ ├── EntityB → Position, Velocity, Health
│ └── ... (max 128 entités par chunk)
└── ...
Component Data (IComponentData)
using Unity.Entities;
public struct Position : IComponentData
{
public float3 Value;
}
public struct Velocity : IComponentData
{
public float3 Value;
}
public struct JoueurTag : IComponentData { }
public struct Equipe : ISharedComponentData
{
public int Valeur;
}
[InternalBufferCapacity(8)]
public struct WaypointBuffer : IBufferElementData
{
public float3 Position;
}
public struct EnableableTag : IComponentData, IEnableableComponent { }
public struct CleanupTag : ICleanupComponentData { }
Création d'Entité et Archétypes
using Unity.Entities;
using Unity.Transforms;
public partial class GameInitializationSystem : SystemBase
{
protected override void OnUpdate()
{
EntityManager entityManager = World.EntityManager;
Entity player = entityManager.CreateEntity(
typeof(LocalTransform),
typeof(Velocity),
typeof(JoueurTag)
);
entityManager.SetComponentData(player, new LocalTransform
{
Position = float3.zero,
Rotation = quaternion.identity,
Scale = 1f
});
entityManager.SetComponentData(player, new Velocity
{
Value = new float3(0, 0, 5)
});
var archetype = entityManager.CreateArchetype(
typeof(LocalTransform),
typeof(Velocity)
);
NativeArray<Entity> entities = entityManager.CreateEntity(archetype, 10000,
Allocator.Temp);
}
}
EntityCommandBuffer (ECB) — Le Pattern Standard
public partial struct WeaponSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
EntityCommandBuffer ecb = new EntityCommandBuffer(Allocator.Temp);
foreach (var (bullet, entity) in SystemAPI.Query<BulletComponent>().WithEntityAccess())
{
if (bullet.Destroyed)
{
ecb.DestroyEntity(entity);
}
}
state.EntityManager.DestroyEntity(ecb);
}
}
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(WeaponSystem))]
public partial struct WeaponEcbSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var ecbSingleton = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
var ecb = ecbSingleton.CreateCommandBuffer(state.WorldUnmanaged);
}
}
System Types and Lifecycle
using Unity.Entities;
using Unity.Burst;
[BurstCompile]
public partial struct MovementSystem : ISystem
{
private EntityQuery _query;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
_query = state.GetEntityQuery(
ComponentType.ReadWrite<LocalTransform>(),
ComponentType.ReadOnly<Velocity>()
);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
float dt = SystemAPI.Time.DeltaTime;
foreach (var (transform, velocity) in
SystemAPI.Query<RefRW<LocalTransform>, RefRO<Velocity>>())
{
transform.ValueRW.Position += velocity.ValueRO.Value * dt;
}
}
[BurstCompile]
public void OnDestroy(ref SystemState state) { }
}
public partial class LegacyMovementSystem : SystemBase
{
protected override ()
{
dt = SystemAPI.Time.DeltaTime;
Entities.ForEach(( LocalTransform transform, Velocity velocity) =>
{
transform.Position += velocity.Value * dt;
}).Schedule();
}
}
MovementAspect : IAspect
{
Entity Self;
RefRW<LocalTransform> Transform;
RefRO<Velocity> Velocity;
RefRO<JoueurTag> Tag;
{
Transform.ValueRW.Position += Velocity.ValueRO.Value * dt;
}
}
[]
MovementAspectSystem : ISystem
{
[]
{
dt = SystemAPI.Time.DeltaTime;
( aspect SystemAPI.Query<MovementAspect>())
{
aspect.Move(dt);
}
}
}
EntityQuery — Requêtes Avancées
[BurstCompile]
public partial struct QueryExampleSystem : ISystem
{
private EntityQuery _query;
public void OnCreate(ref SystemState state)
{
_query = new EntityQueryBuilder(Allocator.Temp)
.WithAll<LocalTransform, Velocity>()
.WithAny<JoueurTag, EnnemiTag>()
.WithNone<DeadTag>()
.WithOptions(EntityQueryOptions.IncludePrefab
| EntityQueryOptions.IncludeDisabledEntities)
.Build(ref state);
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var entities = _query.ToEntityArray(Allocator.TempJob);
int count = _query.CalculateEntityCount();
var chunks = _query.ToArchetypeChunkArray(Allocator.TempJob);
foreach (var chunk in chunks)
{
var transforms = chunk.GetNativeArray<LocalTransform>(ref state
.GetArchetypeChunkComponentType<LocalTransform>());
}
}
}
Chunk Iteration — Bas Niveau
[BurstCompile]
public struct ChunkIterationJob : IJobChunk
{
[ReadOnly] public ComponentTypeHandle<Velocity> VelocityHandle;
public ComponentTypeHandle<LocalTransform> TransformHandle;
public float DeltaTime;
public void Execute(in ArchetypeChunk chunk, int unfilteredIndex, bool useEnabledMask,
in Unity.Entities.CodeGenerated.JobChunkJobSafetyCache safety)
{
var transforms = chunk.GetNativeArray(ref TransformHandle);
var velocities = chunk.GetNativeArray(ref VelocityHandle);
for (int i = 0; i < chunk.Count; i++)
{
transforms[i] = new LocalTransform
{
Position = transforms[i].Position + velocities[i].Value * DeltaTime,
Rotation = transforms[i].Rotation,
Scale = transforms[i].Scale
};
}
}
}
Bake et SubScenes
using Unity.Entities;
using Unity.Baking;
public class CubeAuthoring : MonoBehaviour
{
public float Vitesse = 5f;
public float Direction = 1f;
}
public class CubeBaker : Baker<CubeAuthoring>
{
public override void Bake(CubeAuthoring authoring)
{
DependsOn(authoring);
var entity = GetEntity(TransformUsageFlags.Dynamic);
AddComponent(entity, new Velocity
{
Value = new float3(authoring.Direction * authoring.Vitesse, 0, 0)
});
AddComponent<JoueurTag>(entity);
}
}
Blob Assets — Données Immuables Partagées
using Unity.Entities;
public struct ArmeStatBlob
{
public float Degats;
public float Portee;
public float CadenceTir;
public int MunitionsMax;
public BlobArray<float> DegatsParDistance;
}
public struct ArmeComponent : IComponentData
{
public BlobAssetReference<ArmeStatBlob> Stats;
}
public static BlobAssetReference<ArmeStatBlob> CreerStatsArme()
{
var builder = new BlobBuilder(Allocator.Temp);
ref var root = ref builder.ConstructRoot<ArmeStatBlob>();
root.Degats = 50f;
root.Portee = 100f;
root.CadenceTir = 0.5f;
root.MunitionsMax = 30;
var array = builder.Allocate(ref root.DegatsParDistance, 5);
array[0] = 50f; array[1] = 40f; array[2] = 30f;
array[3] = 20f; array[4] = ;
result = builder.CreateBlobAssetReference<ArmeStatBlob>(Allocator.Persistent);
builder.Dispose();
result;
}
stats = armeComponent.Stats.Value;
damage = stats.DegatsParDistance[()(distance / )];
System Groups et Ordre d'Exécution
using Unity.Entities;
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateBefore(typeof(MovementSystem))]
[BurstCompile]
public partial struct InputSystem : ISystem { }
[UpdateInGroup(typeof(SimulationSystemGroup))]
[UpdateAfter(typeof(InputSystem))]
[BurstCompile]
public partial struct MovementSystem : ISystem { }
[UpdateInGroup(typeof(SimulationSystemGroup))]
public partial struct CombatSystemGroup : ISystemGroup { }
[UpdateInGroup(typeof(CombatSystemGroup))]
public partial struct AttackSystem : ISystem { }
[UpdateInGroup(typeof(CombatSystemGroup))]
[UpdateAfter(typeof(AttackSystem))]
public partial struct DamageSystem : ISystem { }
3. Flecs (C/C++) — ECS Multiplateforme
Flecs est un ECS en C, avec bindings C++, Rust, Python, Lua, TypeScript. Très performant et riche.
typedef struct {
float x, y, z;
} Position;
typedef struct {
float x, y, z;
} Velocity;
ECS_COMPONENT(world, Position);
ECS_COMPONENT(world, Velocity);
ECS_SYSTEM(world, MoveSystem, EcsOnUpdate, Position, Velocity);
void MoveSystem(ecs_iter_t *it) {
Position *p = ecs_field(it, Position, 1);
Velocity *v = ecs_field(it, Velocity, 2);
for (int i = 0; i < it->count; i++) {
p[i].x += v[i].x * it->delta_time;
p[i].y += v[i].y * it->delta_time;
}
}
ecs_entity_t e = ecs_new(world, Position);
ecs_set(world, e, Velocity, {5, 0, 0});
ecs_query_t *q = ecs_query(world, {
.filter.terms = {
{ .id = ecs_id(Position) },
{ .id = ecs_id(Velocity), .inout = EcsIn },
}
});
ECS_TAG(world, AmiDe);
ECS_TAG(world, Equipe);
ECS_TAG(world, PortePar);
ecs_add_pair(world, alice, AmiDe, bob);
ecs_add_pair(world, epee, PortePar, alice);
ecs_query(world, {
.filter.terms = {
{ .id = ecs_pair(PortePar, EcsWildcard) }
}
});
4. EnTT (C++17) — ECS Header-Only
#include <entt/entt.hpp>
struct Position {
float x, y, z;
};
struct Velocity {
float dx, dy, dz;
};
struct Health {
int current, max;
};
struct DeadTag {};
int main() {
entt::registry registry;
auto entity = registry.create();
registry.emplace<Position>(entity, 0.f, 0.f, 0.f);
registry.emplace<Velocity>(entity, 1.f, 0.f, 0.f);
registry.emplace<Health>(entity, 100, 100);
auto view = registry.view<Position, Velocity>(entt::exclude<DeadTag>);
view.each([](auto &pos, auto &vel) {
pos.x += vel.dx;
pos.y += vel.dy;
});
auto group = registry.group<Position>(entt::get<Velocity>);
registry.on_construct<Health>().connect<&on_health_created>();
registry.on_update<Position>().connect<&on_position_changed>();
type = registry.(_hs);
entt::snapshot{registry}
.<entt::entity>([](, &entity) { })
.<Position>([]( &pos) { });
}
5. Bevy ECS (Rust)
use bevy::prelude::*;
#[derive(Component)]
struct Position(Vec3);
#[derive(Component)]
struct Velocity(Vec3);
#[derive(Component)]
struct Health {
current: f32,
max: f32,
}
#[derive(Component)]
struct Player;
fn movement_system(mut query: Query<(&mut Position, &Velocity)>, time: Res<Time>) {
for (mut pos, vel) in query.iter_mut() {
pos.0 += vel.0 * time.delta_seconds();
}
}
fn damage_system(
mut commands: Commands,
mut query: Query<(Entity, &mut Health)>,
time: Res<Time>,
) {
for (entity, mut health) in query.iter_mut() {
health.current -= 10.0 * time.delta_seconds();
if health.current <= 0.0 {
commands.entity(entity).despawn();
}
}
}
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)]
enum {
Input,
Movement,
Combat,
Render,
}
() {
App::()
.(DefaultPlugins)
.(Update,
input_system.(GameSystem::Input),
movement_system.(GameSystem::Movement).(GameSystem::Input),
damage_system.(GameSystem::Combat).(GameSystem::Movement),
)
.();
}
(
enemies: Query<(& Position, &Velocity), (
With<Enemy>,
Without<Dead>,
)>,
) {}
6. Data-Oriented Design Patterns
Component Design Patterns
1. CHAUD (hot) — dans le path critique, itéré chaque frame
LocalTransform, Velocity, AABB, AnimationBone
2. TIÈDE (warm) — itéré quelques fois par frame
Health, Mana, ColliderRadius, Team
3. FROID (cold) — rarement accédé, souvent via lookup
Name, Description, QuestFlags, DialogState
→ Mettre dans des Blob Assets ou des singletons
Singleton Pattern (ECS)
[BurstCompile]
public partial struct GameStateSystem : ISystem
{
public void OnUpdate(ref SystemState state)
{
var gameState = SystemAPI.GetSingleton<GameState>();
var timeOfDay = SystemAPI.GetSingleton<TimeOfDay>();
SystemAPI.SetSingleton(new TimeOfDay { Value = timeOfDay.Value + 0.016f });
}
}
var singletonEntity = entityManager.CreateEntity(typeof(GameState));
entityManager.SetComponentData(singletonEntity, new GameState { ... });
EntityCommandBuffer — Bonnes Pratiques
var ecb = new EntityCommandBuffer(Allocator.Temp);
var ecb = new EntityCommandBuffer(Allocator.TempJob);
var ecbSystem = World.GetExistingSystemManaged<EndSimulationEntityCommandBufferSystem>();
var ecb = ecbSystem.CreateCommandBuffer();
var ecbs = new NativeArray<EntityCommandBuffer>(threadCount, Allocator.TempJob);
foreach (var e in ecbs)
{
state.EntityManager.DestroyEntity(e);
}
7. Performance — Cache CPU et Optimisation
Cache Line (64 bytes)
Cache L1: 32KB → ~512 positions (float3 × 4 = 16 bytes)
Cache L2: 256KB → ~16000 positions
Cache L3: 8-32MB → ~1M positions
SoA Position[]: accès séquentiel → préfetcher CPU heureux → bande passante saturée
AoS Particle[]: accès dispersé → cache misses → starvation ALU
Branch Prediction
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
foreach (var (transform, health) in SystemAPI.Query<RefRW<LocalTransform>, Health>())
{
if (health.Value < 0.5f)
{
transform.ValueRW.Scale *= 0.95f;
}
}
}
foreach (var transform in SystemAPI.Query<RefRW<LocalTransform>>()
.With<DamagedTag>())
{
transform.ValueRW.Scale *= 0.95f;
}
Structural Changes — Coût
entityManager.SetComponentEnabled<DisabledTag>(entity, false);
entityManager.RemoveComponent<DisabledTag>(entity);
8. Pièges Courants
- Structural changes dans Update() → plantage (thread safety). Toujours utiliser ECB.
- Lookup périmé → invalide après structural change. Recréer après chaque ECB Playback.
- Trop de composants par entité → archétype fragmenté (peu d'entités par chunk = gaspillage mémoire). Maximum ~20 composants.
- Shared Components avec valeurs trop diverses → fragmentation des chunks.
- Burst pas activé →
[BurstCompile] oublié sur ISystem → perte de perf × 10.
- Allocation mémoire dans les jobs → utiliser
Allocator.TempJob et disposer.
- Changement de world → ne pas mélanger des Entity de Worlds différents.
- Blob Assets oubliés → memory leak si
Dispose() pas appelé.
- Hybrid Mono en production → un composant managed = pas de Burst = goulot.