A large-scale Particle Life simulation built with Unity DOTS (ECS + Jobs + Burst), focusing on emergent behavior through simple force rules. Thousands to particles interact in real time, forming complex, self-organizing patterns.
protected override void OnUpdate() { var query = World.EntityManager.CreateEntityQuery(typeof(ParticleCreateRequestComponent)); if (query.CalculateEntityCount() == 0) return;
var requestEntity = query.GetSingletonEntity(); var request = SystemAPI.GetComponent<ParticleCreateRequestComponent>(requestEntity);
var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; var archetype = entityManager.CreateArchetype(typeof(Particle), typeof(LocalToWorld));
var colorCount = SystemAPI.GetSingleton<ColorConfigComponent>().ColorCount; for (var i = 0; i < request.Count; i++) { var e = entityManager.CreateEntity(archetype);
var pos = _rand.NextFloat2(request.MinPosition, request.MaxPosition); entityManager.SetComponentData(e, new Particle { ColorIndex = _rand.NextInt(0, colorCount), Position = pos, Velocity = float2.zero }); entityManager.SetComponentData(e, new LocalToWorld { Value = float4x4.Translate(new float3(pos, 0)) }); }
entityManager.DestroyEntity(requestEntity); }
1 2 3 4 5 6
publicstruct Particle : IComponentData { publicint ColorIndex; public float2 Position; public float2 Velocity; }
Movement System
Read the global config component to update particle positions based on their velocities.
[BurstCompile] publicvoidOnUpdate(ref SystemState state) { var simulationComponent = SystemAPI.GetSingleton<ParticleSimulationConfigComponent>(); if (!simulationComponent.SimulationEnabled) return;
var colorComponent = SystemAPI.GetSingleton<ColorConfigComponent>(); var boundaryComponent = SystemAPI.GetSingleton<BoundaryConfigComponent>();
var particles = _particleQuery.ToComponentDataArray<Particle>(Allocator.TempJob); var colorIndices = new NativeArray<int>(particles.Length, Allocator.TempJob); var positions = new NativeArray<float2>(particles.Length, Allocator.TempJob); var velocities = new NativeArray<float2>(particles.Length, Allocator.TempJob); for (var i = 0; i < particles.Length; i++) { colorIndices[i] = particles[i].ColorIndex; positions[i] = particles[i].Position; velocities[i] = particles[i].Velocity; }
var deltaTime = SystemAPI.Time.DeltaTime; var frictionFactor = CalFrictionFactor(deltaTime, simulationComponent.FrictionHalfLife, simulationComponent.FrictionFactor); var job = new ForceJob { // Single ColorIndices = colorIndices, Positions = positions, Velocities = velocities,
var index = 0; foreach (var particle in SystemAPI.Query<RefRW<Particle>>()) { var p = particle.ValueRO; p.Velocity = velocities[index]; particle.ValueRW = p; index++; }
[BurstCompile] privatestruct ForceJob : IJobFor { // Single [ReadOnly] public NativeArray<int> ColorIndices; [ReadOnly] public NativeArray<float2> Positions; public NativeArray<float2> Velocities;
// Global [ReadOnly] public ColorConfigComponent ColorComponent; [ReadOnly] publicfloat DeltaTime; [ReadOnly] publicfloat FrictionFactor; [ReadOnly] public ParticleSimulationConfigComponent SimulationComponent; [ReadOnly] public BoundaryConfigComponent BoundaryComponent;
publicvoidExecute(int i) { var colorIndexA = ColorIndices[i]; var posA = Positions[i];
var totalForceDir = float2.zero; for (var j = 0; j < Positions.Length; j++) { if (i == j) continue;
float2 dir; if (BoundaryComponent.BoundaryEnabled) { GetWrappedDelta(posA, Positions[j], BoundaryComponent.MinPosition, BoundaryComponent.MaxPosition, out dir); } else { dir = Positions[j] - posA; }
var dist = math.length(dir); if (!(dist < SimulationComponent.MaxAttractionDistance)) continue;
var f = CalForce(dist / SimulationComponent.MaxAttractionDistance, ColorComponent.AttractionMatrix.Value.Matrix[ colorIndexA * ColorComponent.ColorCount + ColorIndices[j]], 0.3f); totalForceDir += math.normalizesafe(dir) * f;
[BurstCompile] privatestaticfloatCalForce(float r, float a, float beta) { if (r < beta) { return r / beta - 1; }
if (r > beta && r < 1) { return a * (1 - math.abs(2 * r - 1 - beta) / (1 - beta)); }
return0.0f; }
[BurstCompile] privatestaticvoidGetWrappedDelta(in float2 a, in float2 b, in float2 min, in float2 max, out float2 result) { var size = max - min; var delta = b - a;