# 2. Jobs and Burst for GameObject Projects Part of [[DOTS]]. Previous: [[1. DOTS Mental Model Packages and Data Locality]]. Next: [[3. Entities Authoring Baking and ISystem]]. You don't need Entities to get most of the speed-up for hot loops. Jobs and Burst work in any MonoBehaviour project. > [!note] Verification status > `IJobParallelFor`, `IJobParallelForTransform`/`TransformAccessArray`, `[BurstCompile]` and `noise.snoise` are long-standing APIs. The claim that `IJobParallelForTransform` parallelizes **per hierarchy root** comes from earlier Unity docs and community guidance. It was **not re-verified** on 6000.5. **Code not compiled.** ## 2.1 Rules of the Job System 1. Jobs are **structs** that hold only **blittable data and Native containers** (`NativeArray<T>` and so on). No managed objects, no `UnityEngine.Object`. 2. **Schedule** returns a `JobHandle`. **Complete()** before you read results on the main thread. 3. Native containers need explicit **Allocator** lifetimes: - `Temp`: within one frame, main thread. - `TempJob`: up to 4 frames, passed to jobs. - `Persistent`: until you `Dispose()`. 4. The **safety system** throws if two jobs write the same container without a dependency. Read-only fields get `[ReadOnly]`. 5. `[BurstCompile]` on the job struct compiles `Execute` to native SIMD code. Burst supports a subset of C# (no managed allocations or exceptions in hot paths), plus `Unity.Mathematics` types (`float3`, `math.*`). ## 2.2 Example A: Burst heightmap for Terrain [[1. Terrain Setup and Heightmaps]] generates a heightmap on the main thread. Here's the same idea as a parallel Burst job. ```csharp // Assets/Scripts/Terrain/HeightmapNoiseJob.cs using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; [BurstCompile] public struct HeightmapNoiseJob : IJobParallelFor { [WriteOnly] public NativeArray<float> Heights; // row-major: index = z * Resolution + x public int Resolution; public float2 WorldSize; // terrain size in metres (x, z) public float2 Offset; // seed offset public float Frequency; // e.g. 0.004 public int Octaves; // e.g. 5 public float Persistence; // e.g. 0.5 public float Lacunarity; // e.g. 2 public void Execute(int index) { int x = index % Resolution; int z = index / Resolution; float2 world = new float2(x, z) / (Resolution - 1) * WorldSize + Offset; float sum = 0f, amplitude = 1f, frequency = Frequency, norm = 0f; for (int o = 0; o < Octaves; o++) { sum += amplitude * (noise.snoise(world * frequency) * 0.5f + 0.5f); // 0..1 norm += amplitude; amplitude *= Persistence; frequency *= Lacunarity; } Heights[index] = sum / norm; } } ``` ```csharp // Assets/Scripts/Terrain/BurstTerrainShaper.cs using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; [RequireComponent(typeof(Terrain))] public class BurstTerrainShaper : MonoBehaviour { public int seed = 42; public float frequency = 0.004f; [Range(1, 8)] public int octaves = 5; public float persistence = 0.5f; public float lacunarity = 2f; [ContextMenu("Generate (Burst)")] public void Generate() { TerrainData data = GetComponent<Terrain>().terrainData; int res = data.heightmapResolution; var heights = new NativeArray<float>(res * res, Allocator.TempJob); var rng = new Unity.Mathematics.Random((uint)math.max(1, seed)); var job = new HeightmapNoiseJob { Heights = heights, Resolution = res, WorldSize = new float2(data.size.x, data.size.z), Offset = rng.NextFloat2(-10000f, 10000f), Frequency = frequency, Octaves = octaves, Persistence = persistence, Lacunarity = lacunarity, }; // Batch size 64: each worker grabs 64 indices at a time. job.Schedule(heights.Length, 64).Complete(); // TerrainData.SetHeights wants float[y, x] (y = z row). Copy out. var grid = new float[res, res]; for (int z = 0; z < res; z++) for (int x = 0; x < res; x++) grid[z, x] = heights[z * res + x]; heights.Dispose(); data.SetHeights(0, 0, grid); } } ``` On a 1025² heightmap with 5 octaves, the noise loop is about 5 million `snoise` calls. Spread across cores with Burst, it should be far faster than the managed loop. **Measure it in the Profiler** rather than trusting a number, because the copy-out is still single-threaded. ## 2.3 Example B: 2,000 schooling fish with IJobParallelForTransform ```csharp // Assets/Scripts/DOTS/FishSchoolJobs.cs using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; using UnityEngine.Jobs; public class FishSchoolJobs : MonoBehaviour { public GameObject fishPrefab; public int count = 2000; public float spawnRadius = 20f; public float speed = 3f; public float turnRate = 2f; public Transform attractor; TransformAccessArray transforms; NativeArray<float3> velocities; JobHandle handle; void Start() { transforms = new TransformAccessArray(count); velocities = new NativeArray<float3>(count, Allocator.Persistent); var rng = new Unity.Mathematics.Random(1234); for (int i = 0; i < count; i++) { float3 p = (float3)transform.position + rng.NextFloat3Direction() * spawnRadius * rng.NextFloat(); // No shared parent! Transform jobs parallelize across hierarchy roots. GameObject fish = Instantiate(fishPrefab, p, Quaternion.identity); transforms.Add(fish.transform); velocities[i] = rng.NextFloat3Direction() * speed; } } void Update() { handle.Complete(); // previous frame's job (safety) var job = new SwimJob { Velocities = velocities, Target = attractor ? (float3)attractor.position : (float3)transform.position, Speed = speed, TurnRate = turnRate, DeltaTime = Time.deltaTime, ElapsedTime = (float)Time.timeAsDouble, }; handle = job.Schedule(transforms); } void LateUpdate() => handle.Complete(); // finish before rendering void OnDestroy() { handle.Complete(); if (transforms.isCreated) transforms.Dispose(); if (velocities.IsCreated) velocities.Dispose(); } [BurstCompile] struct SwimJob : IJobParallelForTransform { public NativeArray<float3> Velocities; public float3 Target; public float Speed, TurnRate, DeltaTime, ElapsedTime; public void Execute(int i, TransformAccess t) { float3 pos = t.position; float3 v = Velocities[i]; float s = ElapsedTime * 0.5f; float3 wander = new float3( noise.snoise(new float2(i * 0.37f, s)), noise.snoise(new float2(i * 0.71f, s + 10f)) * 0.3f, noise.snoise(new float2(i * 0.13f, s + 20f))); float3 desired = math.normalizesafe((Target - pos) * 0.05f + wander) * Speed; v = math.lerp(v, desired, math.saturate(TurnRate * DeltaTime)); t.position = pos + v * DeltaTime; if (math.lengthsq(v) > 1e-4f) t.rotation = quaternion.LookRotationSafe(math.normalize(v), math.up()); Velocities[i] = v; } } } ``` ### Why `Complete()` in LateUpdate? Scheduling in Update and completing in LateUpdate gives the workers the rest of the frame (animation, physics, other scripts) to run alongside the main thread. Completing straight after `Schedule` would work, but the main thread would stand idle while it waits. ## 2.4 Burst Inspector and toggles - **Jobs > Burst > Open Inspector**: see the generated assembly and confirm vectorization. - **Jobs > Burst > Enable Compilation**: switch it off to compare performance or to debug with breakpoints. - **Jobs > Burst > Safety Checks**: on in the editor. Leave them on while you develop, and profile in a **development build**. ## 2.5 Patterns - **Data Locality** ([[Game Programming Patterns (Review)]]): `NativeArray<float3>` velocities instead of a `Fish` component on every object. - **Update Method**: one manager updates every fish, instead of 2,000 `Update()` calls. ## Exercise Add a "predator" transform: within 5 m, fish steer away at double speed. That's Level Up!'s **feedback** idea made visible. The school "flinches" as a group. ## Sources - Job System overview (6000.5): https://docs.unity3d.com/6000.5/Documentation/Manual/job-system-overview.html - `IJobParallelForTransform`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Jobs.IJobParallelForTransform.html - Burst on 6000.5 (1.8.31): https://docs.unity3d.com/6000.5/Documentation/Manual/com.unity.burst.html - Mathematics `noise`: https://docs.unity3d.com/Packages/[email protected]/api/Unity.Mathematics.noise.html - `TerrainData.SetHeights`: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/TerrainData.SetHeights.html