dots-spawning-patterns
Senior-level entity spawning — batched Instantiate from prefab Entity, ECB.Instantiate from jobs, deterministic Random with CreateFromIndex, RequireForUpdate gating, and Allocator.Temp lifecycle. Use when implementing spawners, enemy waves, projectile fire, particle-like ECS entities, or any "create N entities per frame" code.
- License 许可
- MIT
- 最近更新
- 2026-07-25
- Stars
- ★ 8
- Forks
- 0
01 前言 Front Matter
- name
- dots-spawning-patterns
- metadata
- { "tier": 3, "internal-only": true }
- description
- Senior-level entity spawning — batched Instantiate from prefab Entity, ECB.Instantiate from jobs, deterministic Random with CreateFromIndex, RequireForUpdate gating, and Allocator.Temp lifecycle. Use when implementing spawners, enemy waves, projectile fire, particle-like ECS entities, or any "create N entities per frame" code.
03 SKILL.md 正文
Spawning Patterns — Senior Patterns
Spawning N entities efficiently is the difference between a 60fps shooter and a 6fps slideshow. The correct pattern is batched Instantiate from a baked prefab Entity, recorded into an ECB when called from a job, with a deterministic seed.
Intent
Create entities in bulk, deterministically, without main-thread cost in hot paths.
Senior pattern — main-thread spawner (one batch per trigger)
public partial struct SpawnSystem : ISystem
{
uint m_UpdateCounter;
[BurstCompile]
public void OnCreate(ref SystemState state)
{
// Don't run until a Spawner singleton exists (set by baking).
state.RequireForUpdate<Spawner>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var spinningCubesQuery = SystemAPI.QueryBuilder().WithAll<RotationSpeed>().Build();
// Only spawn when none exist — bursty, not per-frame.
if (!spinningCubesQuery.IsEmpty) return;
var prefab = SystemAPI.GetSingleton<Spawner>().Prefab;
// Batched: instantiate 500 in one call, return a temp NativeArray of entities.
var instances = state.EntityManager.Instantiate(prefab, 500, Allocator.Temp);
// Deterministic random: CreateFromIndex hashes the seed so consecutive
// seeds don't produce visually-similar results.
var rng = Random.CreateFromIndex(m_UpdateCounter++);
foreach (var entity in instances)
{
var transform = SystemAPI.GetComponentRW<LocalTransform>(entity);
transform.ValueRW.Position = (rng.NextFloat3() - new float3(0.5f, 0, 0.5f)) * 20f;
}
// Allocator.Temp auto-frees at end of frame. No manual Dispose.
}
}
Senior pattern — spawn from inside a parallel job
When spawn cadence depends on per-entity state (e.g. each turret fires when its cooldown expires):
public partial struct TurretFireSystem : ISystem
{
[BurstCompile]
public void OnCreate(ref SystemState state) {
state.RequireForUpdate<Spawner>();
state.RequireForUpdate<BeginSimulationEntityCommandBufferSystem.Singleton>();
}
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
var prefab = SystemAPI.GetSingleton<Spawner>().BulletPrefab;
var ecb = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>()
.CreateCommandBuffer(state.WorldUnmanaged)
.AsParallelWriter();
new FireJob {
Ecb = ecb,
Prefab = prefab,
Dt = SystemAPI.Time.DeltaTime,
}.ScheduleParallel();
}
}
[BurstCompile]
partial struct FireJob : IJobEntity
{
public EntityCommandBuffer.ParallelWriter Ecb;
public Entity Prefab;
public float Dt;
void Execute([ChunkIndexInQuery] int chunkIndex,
ref TurretCooldown cd,
in LocalTransform t)
{
cd.TimeLeft -= Dt;
if (cd.TimeLeft > 0) return;
cd.TimeLeft = cd.Interval;
// Record the spawn; ECB plays back next frame.
var bullet = Ecb.Instantiate(chunkIndex, Prefab);
Ecb.SetComponent(chunkIndex, bullet, LocalTransform.FromPosition(t.Position));
}
}
Deterministic randomness
// Per-frame seed; the same frame number gives the same sequence — replay-safe.
var rng = Random.CreateFromIndex(updateCounter);
// Per-entity seed (so two entities created on the same frame diverge):
var rng = Random.CreateFromIndex((uint)entity.Index ^ updateCounter);
Banned:
UnityEngine.Random— managed, not Burst-compatible, not deterministic across runsnew Random()without seed — non-deterministicRandom.CreateFromIndex(0)—CreateFromIndexrejects 0; will throw
Anti-patterns
- ❌
state.EntityManager.Instantiate(prefab)called in a loop. Always use the batched overload that takes a count or a destination array. - ❌ Spawning from a job using
state.EntityManager.Instantiatedirectly. Must go throughEcb.Instantiatewith[ChunkIndexInQuery]. - ❌ Loading the prefab via
Resources.LoadorAddressablesinOnUpdate. The prefab Entity is baked at conversion — fetch it once from a singleton. - ❌ Spawning N entities per frame "just in case". Gate with
RequireForUpdate<T>and intent components; spawn bursty, not continuous. - ❌ Using
Allocator.Persistentfor the temp entity array fromInstantiate.Allocator.Tempis right — it auto-frees at frame end. - ❌ Forgetting
RequireForUpdate<Spawner>()inOnCreate. Without it,GetSingleton<Spawner>()throws if no spawner exists.
Failure modes
| Symptom | Likely cause |
|---|---|
| Spawn cost spikes in the profiler | Per-entity Instantiate instead of batched |
| "InvalidOperationException: Singleton does not exist" | Missing RequireForUpdate<Spawner> |
| Spawn position is always (0,0,0) | Forgot to set LocalTransform after Instantiate; prefab's transform applies but per-entity position not written |
| Replays diverge | UnityEngine.Random or unseeded Random |
Crash on Random.CreateFromIndex(0) |
Seed must be non-zero; use `(updateCounter |
| Bullets spawn one frame late | Expected — ECB playback. If you need same-frame visibility, the reader must run after the ECB phase you recorded into |
Runtime verification
- Static: every
Instantiate(prefab)call should use the batched overload or be wrapped inEcb.Instantiate. Grep forEntityManager.Instantiate(with no second argument insideOnUpdate. - Runtime: spawn N entities, assert query count rises by exactly N within the expected number of frames (1 for main-thread, 2 for ECB-deferred). Run twice with the same seed; assert positions are bit-identical.
Performance notes
- Batched
Instantiate(prefab, 500, Allocator.Temp)is roughly the cost of one structural change, not 500. Always batch. - The baked prefab Entity has all components fully constructed; spawn copies them. Minimize prefab archetype size if spawning thousands per frame — every byte multiplies.
- Spawning a million entities at once is feasible; spawning ten per frame for a hundred thousand frames is not. Pick burst spawn over sustained drip.
Compile / editor safety
- The prefab Entity reference is produced by a Baker (see
dots-baking-patterns). Don't try to load it at runtime. Randomhere isUnity.Mathematics.Random(using Unity.Mathematics;) — Burst-friendly value type.UnityEngine.Randomlooks similar but is managed and banned in hot paths.
Entities version notes (1.4.x)
state.EntityManager.Instantiate(prefab, count, allocator)is the current batched overload.Random.CreateFromIndex(seed)is current. Old code usednew Random((uint)Time.frameCount)— non-deterministic across builds, refuse it.Allocator.Tempfor per-system, single-frame arrays.Allocator.TempJobonly when an array is passed into a job that may outlive the calling method.
See also
dots-baking-patterns— how the prefab Entity ref gets madedots-ecb-orchestration— phase selection for in-job spawndots-entity-lifecycle— destruction is the matching half
04 同仓库更多 Skills
dots-update-groups
How to choose the right ECS system group and ordering so per-frame and per-fixed-step work is deterministic and never races its own readers. Covers InitializationSystemGroup / SimulationSystemGroup / PresentationSystemGroup / FixedStepSimulationSystemGroup / VariableRateSimulationSystemGroup, UpdateBefore / UpdateAfter peer ordering, OrderFirst / OrderLast group-boundary placement, RateManager, and the cross-group ordering trap. Use when adding an ISystem, fixing "value visible one frame late" bugs, or coupling simulation to a fixed timestep.
dots-transform-patterns
The Entities 1.x transform model — LocalTransform (writable input) vs LocalToWorld (system-owned matrix), Parent/Child hierarchy, reparenting cost, uniform vs non-uniform scale (PostTransformMatrix), and when to opt out with custom transform systems. Use when designing parent/child hierarchies, runtime reparenting (mount/dismount/socket), or choosing transform components at the Baker boundary.
dots-singleton-patterns
Model cross-system shared configuration and game state as a single-entity component with a strict single-writer contract. Covers RequireForUpdate gating, SystemAPI.GetSingleton / GetSingletonRW / HasSingleton / TryGetSingleton, bake-time vs runtime-created vs ECB-system singletons, the "exactly one entity" runtime invariant, and the single-writer rule. Use when designing frame-stable global config, shared mutable state, or any component intended to exist on exactly one entity.
dots-hybrid-bridge
The one-way bridge between DOTS-owned runtime state and Unity-owned presentation. Covers UnityObjectRef for managed asset refs in components, the DOTS-writes / Unity-reads ownership rule, when to use managed IComponentData vs UnityObjectRef vs CompanionComponent, baker-bridge vs runtime-bridge, and SubScene boundary rules. Use when an entity must reference Unity assets (Mesh, Material, AudioClip, ScriptableObject), a MonoBehaviour view must read entity state, or input/UI must trigger ECS work.
dots-event-driven-ecs
Model one-frame events and external requests in ECS without dual ownership, polling, or persistent state leaks. Covers request-tag pattern with ECB, enableable command components (no structural change), event buffers (IBufferElementData), explicit consumer ownership, one-frame latency rules, and the banned C# event / UnityEvent / static bus anti-patterns. Use when input/UI/network must trigger ECS work, when a one-frame signal fans out to N consumers (damage, hit, collision), or when state changes must be processed exactly once.
valid_skill
A complete, well-formed test fixture skill for SkillHub validation testing. Covers basic task routing and code review for Unity projects.