DOTS 정리 (2) – Unity Job 시스템 시작하기

https://learn.unity.com/tutorial/660ffc54edbc2a1b0887d446?uv=6&projectId=660ffcd3edbc2a162b7baa27#66ceb506edbc2a03a1163ee9

학습 참고 문서

공식 Sample Project 링크

https://github.com/Unity-Technologies/EntityComponentSystemSamples/tree/master


멀티코어 CPU를 활용하기 위해 Job을 사용하는 기본적인 방법

STEP

– 싱글 스레드 잡을 생성, 예약, 완료

– 병렬 잡을 생성, 예약, 완료

– 다른 잡에 종속되는 잡을 예약

– NativeArrays를 사용

파란 큐브와 빨간 큐브를 각각 Seeker와 Target으로 정하고 각Seeker은 가장 가까운 Target으로 이어지는 디버그 라인을 추가합니다.

각 Step은 서로 다른 방식으로 문제를 해결하여 Step마다 성능 비교를 할 수 있습니다.

SeekerPrefab의 인스턴스를 생성 -> 2D 평면을 따라 무작위 방향과 무작위 위치를 설정 ->

Target도 동일한 방식으로 생성 -> 모든 Seeker와 Target의 트랜스폼을 정적 배열에 할당하여

FindNearest 스크립트에서 액세스하여 Update()

Sample 위치

Step 1. Scene – No Job

일반적인 브루트 포스 순회 방법, 각 Seeker 마다 모든 Target과의 거리를 확인

Spawner.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using UnityEngine;
namespace Tutorials.Jobs.Step1
{
public class Spawner : MonoBehaviour
{
// The set of targets is fixed, so rather than
// retrieve the targets every frame, we'll cache
// their transforms in this field.
public static Transform[] TargetTransforms;
public GameObject SeekerPrefab;
public GameObject TargetPrefab;
public int NumSeekers;
public int NumTargets;
public Vector2 Bounds;
public void Start()
{
Random.InitState(123);
for (int i = 0; i < NumSeekers; i++)
{
GameObject go = GameObject.Instantiate(SeekerPrefab);
Seeker seeker = go.GetComponent<Seeker>();
Vector2 dir = Random.insideUnitCircle;
seeker.Direction = new Vector3(dir.x, 0, dir.y);
go.transform.localPosition = new Vector3(
Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
}
TargetTransforms = new Transform[NumTargets];
for (int i = 0; i < NumTargets; i++)
{
GameObject go = GameObject.Instantiate(TargetPrefab);
Target target = go.GetComponent<Target>();
Vector2 dir = Random.insideUnitCircle;
target.Direction = new Vector3(dir.x, 0, dir.y);
TargetTransforms[i] = go.transform;
go.transform.localPosition = new Vector3(
Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
}
}
}
}
using UnityEngine; namespace Tutorials.Jobs.Step1 { public class Spawner : MonoBehaviour { // The set of targets is fixed, so rather than // retrieve the targets every frame, we'll cache // their transforms in this field. public static Transform[] TargetTransforms; public GameObject SeekerPrefab; public GameObject TargetPrefab; public int NumSeekers; public int NumTargets; public Vector2 Bounds; public void Start() { Random.InitState(123); for (int i = 0; i < NumSeekers; i++) { GameObject go = GameObject.Instantiate(SeekerPrefab); Seeker seeker = go.GetComponent<Seeker>(); Vector2 dir = Random.insideUnitCircle; seeker.Direction = new Vector3(dir.x, 0, dir.y); go.transform.localPosition = new Vector3( Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y)); } TargetTransforms = new Transform[NumTargets]; for (int i = 0; i < NumTargets; i++) { GameObject go = GameObject.Instantiate(TargetPrefab); Target target = go.GetComponent<Target>(); Vector2 dir = Random.insideUnitCircle; target.Direction = new Vector3(dir.x, 0, dir.y); TargetTransforms[i] = go.transform; go.transform.localPosition = new Vector3( Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y)); } } } }
using UnityEngine;

namespace Tutorials.Jobs.Step1
{
    public class Spawner : MonoBehaviour
    {
        // The set of targets is fixed, so rather than 
        // retrieve the targets every frame, we'll cache 
        // their transforms in this field.
        public static Transform[] TargetTransforms;

        public GameObject SeekerPrefab;
        public GameObject TargetPrefab;
        public int NumSeekers;
        public int NumTargets;
        public Vector2 Bounds;

        public void Start()
        {
            Random.InitState(123);

            for (int i = 0; i < NumSeekers; i++)
            {
                GameObject go = GameObject.Instantiate(SeekerPrefab);
                Seeker seeker = go.GetComponent<Seeker>();
                Vector2 dir = Random.insideUnitCircle;
                seeker.Direction = new Vector3(dir.x, 0, dir.y);
                go.transform.localPosition = new Vector3(
                    Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
            }

            TargetTransforms = new Transform[NumTargets];
            for (int i = 0; i < NumTargets; i++)
            {
                GameObject go = GameObject.Instantiate(TargetPrefab);
                Target target = go.GetComponent<Target>();
                Vector2 dir = Random.insideUnitCircle;
                target.Direction = new Vector3(dir.x, 0, dir.y);
                TargetTransforms[i] = go.transform;
                go.transform.localPosition = new Vector3(
                    Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
            }
        }
    }
}

Seeker.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using UnityEngine;
namespace Tutorials.Jobs.Step1
{
public class Seeker : MonoBehaviour
{
public Vector3 Direction;
public void Update()
{
transform.localPosition += Direction * Time.deltaTime;
}
}
}
using UnityEngine; namespace Tutorials.Jobs.Step1 { public class Seeker : MonoBehaviour { public Vector3 Direction; public void Update() { transform.localPosition += Direction * Time.deltaTime; } } }
using UnityEngine;

namespace Tutorials.Jobs.Step1
{
    public class Seeker : MonoBehaviour
    {
        public Vector3 Direction;

        public void Update()
        {
            transform.localPosition += Direction * Time.deltaTime;
        }
    }
}

Target.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using UnityEngine;
namespace Tutorials.Jobs.Step1
{
public class Target : MonoBehaviour
{
public Vector3 Direction;
public void Update()
{
transform.localPosition += Direction * Time.deltaTime;
}
}
}
using UnityEngine; namespace Tutorials.Jobs.Step1 { public class Target : MonoBehaviour { public Vector3 Direction; public void Update() { transform.localPosition += Direction * Time.deltaTime; } } }
using UnityEngine;

namespace Tutorials.Jobs.Step1
{
    public class Target : MonoBehaviour
    {
        public Vector3 Direction;

        public void Update()
        {
            transform.localPosition += Direction * Time.deltaTime;
        }
    }
}

FindNearest.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using UnityEngine;
namespace Tutorials.Jobs.Step1
{
public class FindNearest : MonoBehaviour
{
public void Update()
{
// Find nearest Target.
// When comparing distances, it's cheaper to compare
// the squares of the distances because doing so
// avoids computing square roots.
Vector3 nearestTargetPosition = default;
float nearestDistSq = float.MaxValue;
foreach (var targetTransform in Spawner.TargetTransforms)
{
Vector3 offset = targetTransform.localPosition - transform.localPosition;
float distSq = offset.sqrMagnitude;
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
nearestTargetPosition = targetTransform.localPosition;
}
}
Debug.DrawLine(transform.localPosition, nearestTargetPosition);
}
}
}
using UnityEngine; namespace Tutorials.Jobs.Step1 { public class FindNearest : MonoBehaviour { public void Update() { // Find nearest Target. // When comparing distances, it's cheaper to compare // the squares of the distances because doing so // avoids computing square roots. Vector3 nearestTargetPosition = default; float nearestDistSq = float.MaxValue; foreach (var targetTransform in Spawner.TargetTransforms) { Vector3 offset = targetTransform.localPosition - transform.localPosition; float distSq = offset.sqrMagnitude; if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestTargetPosition = targetTransform.localPosition; } } Debug.DrawLine(transform.localPosition, nearestTargetPosition); } } }
using UnityEngine;

namespace Tutorials.Jobs.Step1
{
    public class FindNearest : MonoBehaviour
    {
        public void Update()
        {
            // Find nearest Target.
            // When comparing distances, it's cheaper to compare
            // the squares of the distances because doing so
            // avoids computing square roots.
            Vector3 nearestTargetPosition = default;
            float nearestDistSq = float.MaxValue;
            foreach (var targetTransform in Spawner.TargetTransforms)
            {
                Vector3 offset = targetTransform.localPosition - transform.localPosition;
                float distSq = offset.sqrMagnitude;
                if (distSq < nearestDistSq)
                {
                    nearestDistSq = distSq;
                    nearestTargetPosition = targetTransform.localPosition;
                }
            }

            Debug.DrawLine(transform.localPosition, nearestTargetPosition);
        }
    }
}
탐색시마다 FindNearest의 컴포넌트
약 10 FPS의 성능을 보여준다.

Seeker의 1000개의 FindNearest가 Update()를 수행하면서 약 82ms를 프레임마다 실행 (개당 0.082ms)

Step 2. Scene – SingleThreadedJob

일반적인 브루트 포스 순회 방법, 하나의 FindNearest에서 Target과의 거리를 확인

Step2 위치

Spawner.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using UnityEngine;
using Seeker = Tutorials.Jobs.Step1.Seeker;
using Target = Tutorials.Jobs.Step1.Target;
namespace Tutorials.Jobs.Step2
{
public class Spawner : MonoBehaviour
{
public static Transform[] TargetTransforms;
// Cache the seeker transforms.
public static Transform[] SeekerTransforms;
public GameObject SeekerPrefab;
public GameObject TargetPrefab;
public int NumSeekers;
public int NumTargets;
public Vector2 Bounds;
public void Start()
{
Random.InitState(123);
SeekerTransforms = new Transform[NumSeekers];
for (int i = 0; i < NumSeekers; i++)
{
GameObject go = GameObject.Instantiate(SeekerPrefab);
Seeker seeker = go.GetComponent<Seeker>();
Vector2 dir = Random.insideUnitCircle;
seeker.Direction = new Vector3(dir.x, 0, dir.y);
SeekerTransforms[i] = go.transform;
go.transform.localPosition = new Vector3(
Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
}
TargetTransforms = new Transform[NumTargets];
for (int i = 0; i < NumTargets; i++)
{
GameObject go = GameObject.Instantiate(TargetPrefab);
Target target = go.GetComponent<Target>();
Vector2 dir = Random.insideUnitCircle;
target.Direction = new Vector3(dir.x, 0, dir.y);
TargetTransforms[i] = go.transform;
go.transform.localPosition = new Vector3(
Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
}
}
}
}
using UnityEngine; using Seeker = Tutorials.Jobs.Step1.Seeker; using Target = Tutorials.Jobs.Step1.Target; namespace Tutorials.Jobs.Step2 { public class Spawner : MonoBehaviour { public static Transform[] TargetTransforms; // Cache the seeker transforms. public static Transform[] SeekerTransforms; public GameObject SeekerPrefab; public GameObject TargetPrefab; public int NumSeekers; public int NumTargets; public Vector2 Bounds; public void Start() { Random.InitState(123); SeekerTransforms = new Transform[NumSeekers]; for (int i = 0; i < NumSeekers; i++) { GameObject go = GameObject.Instantiate(SeekerPrefab); Seeker seeker = go.GetComponent<Seeker>(); Vector2 dir = Random.insideUnitCircle; seeker.Direction = new Vector3(dir.x, 0, dir.y); SeekerTransforms[i] = go.transform; go.transform.localPosition = new Vector3( Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y)); } TargetTransforms = new Transform[NumTargets]; for (int i = 0; i < NumTargets; i++) { GameObject go = GameObject.Instantiate(TargetPrefab); Target target = go.GetComponent<Target>(); Vector2 dir = Random.insideUnitCircle; target.Direction = new Vector3(dir.x, 0, dir.y); TargetTransforms[i] = go.transform; go.transform.localPosition = new Vector3( Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y)); } } } }
using UnityEngine;
using Seeker = Tutorials.Jobs.Step1.Seeker;
using Target = Tutorials.Jobs.Step1.Target;

namespace Tutorials.Jobs.Step2
{
    public class Spawner : MonoBehaviour
    {
        public static Transform[] TargetTransforms;

        // Cache the seeker transforms.
        public static Transform[] SeekerTransforms;

        public GameObject SeekerPrefab;
        public GameObject TargetPrefab;
        public int NumSeekers;
        public int NumTargets;
        public Vector2 Bounds;

        public void Start()
        {
            Random.InitState(123);

            SeekerTransforms = new Transform[NumSeekers];
            for (int i = 0; i < NumSeekers; i++)
            {
                GameObject go = GameObject.Instantiate(SeekerPrefab);
                Seeker seeker = go.GetComponent<Seeker>();
                Vector2 dir = Random.insideUnitCircle;
                seeker.Direction = new Vector3(dir.x, 0, dir.y);
                SeekerTransforms[i] = go.transform;
                go.transform.localPosition = new Vector3(
                    Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
            }

            TargetTransforms = new Transform[NumTargets];
            for (int i = 0; i < NumTargets; i++)
            {
                GameObject go = GameObject.Instantiate(TargetPrefab);
                Target target = go.GetComponent<Target>();
                Vector2 dir = Random.insideUnitCircle;
                target.Direction = new Vector3(dir.x, 0, dir.y);
                TargetTransforms[i] = go.transform;
                go.transform.localPosition = new Vector3(
                    Random.Range(0, Bounds.x), 0, Random.Range(0, Bounds.y));
            }
        }
    }
}

FindNearest.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Tutorials.Jobs.Step2
{
public class FindNearest : MonoBehaviour
{
// The size of our arrays does not need to vary, so rather than create
// new arrays every field, we'll create the arrays in Awake() and store them
// in these fields.
NativeArray<float3> TargetPositions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
public void Start()
{
Spawner spawner = Object.FindFirstObjectByType<Spawner>();
// We use the Persistent allocator because these arrays must
// exist for the run of the program.
TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
}
// We are responsible for disposing of our allocations
// when we no longer need them.
public void OnDestroy()
{
TargetPositions.Dispose();
SeekerPositions.Dispose();
NearestTargetPositions.Dispose();
}
public void Update()
{
// Copy every target transform to a NativeArray.
for (int i = 0; i < TargetPositions.Length; i++)
{
// Vector3 is implicitly converted to float3
TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
}
// Copy every seeker transform to a NativeArray.
for (int i = 0; i < SeekerPositions.Length; i++)
{
// Vector3 is implicitly converted to float3
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
}
// To schedule a job, we first need to create an instance and populate its fields.
FindNearestJob findJob = new FindNearestJob
{
TargetPositions = TargetPositions,
SeekerPositions = SeekerPositions,
NearestTargetPositions = NearestTargetPositions,
};
// Schedule() puts the job instance on the job queue.
JobHandle findHandle = findJob.Schedule();
// The Complete method will not return until the job represented by
// the handle finishes execution. Effectively, the main thread waits
// here until the job is done.
findHandle.Complete();
// Draw a debug line from each seeker to its nearest target.
for (int i = 0; i < SeekerPositions.Length; i++)
{
// float3 is implicitly converted to Vector3
Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
}
}
}
}
using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; namespace Tutorials.Jobs.Step2 { public class FindNearest : MonoBehaviour { // The size of our arrays does not need to vary, so rather than create // new arrays every field, we'll create the arrays in Awake() and store them // in these fields. NativeArray<float3> TargetPositions; NativeArray<float3> SeekerPositions; NativeArray<float3> NearestTargetPositions; public void Start() { Spawner spawner = Object.FindFirstObjectByType<Spawner>(); // We use the Persistent allocator because these arrays must // exist for the run of the program. TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent); SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); } // We are responsible for disposing of our allocations // when we no longer need them. public void OnDestroy() { TargetPositions.Dispose(); SeekerPositions.Dispose(); NearestTargetPositions.Dispose(); } public void Update() { // Copy every target transform to a NativeArray. for (int i = 0; i < TargetPositions.Length; i++) { // Vector3 is implicitly converted to float3 TargetPositions[i] = Spawner.TargetTransforms[i].localPosition; } // Copy every seeker transform to a NativeArray. for (int i = 0; i < SeekerPositions.Length; i++) { // Vector3 is implicitly converted to float3 SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition; } // To schedule a job, we first need to create an instance and populate its fields. FindNearestJob findJob = new FindNearestJob { TargetPositions = TargetPositions, SeekerPositions = SeekerPositions, NearestTargetPositions = NearestTargetPositions, }; // Schedule() puts the job instance on the job queue. JobHandle findHandle = findJob.Schedule(); // The Complete method will not return until the job represented by // the handle finishes execution. Effectively, the main thread waits // here until the job is done. findHandle.Complete(); // Draw a debug line from each seeker to its nearest target. for (int i = 0; i < SeekerPositions.Length; i++) { // float3 is implicitly converted to Vector3 Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]); } } } }
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

namespace Tutorials.Jobs.Step2
{
    public class FindNearest : MonoBehaviour
    {
        // The size of our arrays does not need to vary, so rather than create
        // new arrays every field, we'll create the arrays in Awake() and store them
        // in these fields.
        NativeArray<float3> TargetPositions;
        NativeArray<float3> SeekerPositions;
        NativeArray<float3> NearestTargetPositions;

        public void Start()
        {
            Spawner spawner = Object.FindFirstObjectByType<Spawner>();
            // We use the Persistent allocator because these arrays must
            // exist for the run of the program.
            TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
            SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
            NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
        }

        // We are responsible for disposing of our allocations
        // when we no longer need them.
        public void OnDestroy()
        {
            TargetPositions.Dispose();
            SeekerPositions.Dispose();
            NearestTargetPositions.Dispose();
        }

        public void Update()
        {
            // Copy every target transform to a NativeArray.
            for (int i = 0; i < TargetPositions.Length; i++)
            {
                // Vector3 is implicitly converted to float3
                TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
            }

            // Copy every seeker transform to a NativeArray.
            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                // Vector3 is implicitly converted to float3
                SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
            }

            // To schedule a job, we first need to create an instance and populate its fields.
            FindNearestJob findJob = new FindNearestJob
            {
                TargetPositions = TargetPositions,
                SeekerPositions = SeekerPositions,
                NearestTargetPositions = NearestTargetPositions,
            };

            // Schedule() puts the job instance on the job queue.
            JobHandle findHandle = findJob.Schedule();

            // The Complete method will not return until the job represented by
            // the handle finishes execution. Effectively, the main thread waits
            // here until the job is done.
            findHandle.Complete();

            // Draw a debug line from each seeker to its nearest target.
            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                // float3 is implicitly converted to Vector3
                Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
            }
        }
    }
}

FindNearestJob.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
// We'll use Unity.Mathematics.float3 instead of Vector3,
// and we'll use Unity.Mathematics.math.distancesq instead of Vector3.sqrMagnitude.
using Unity.Mathematics;
namespace Tutorials.Jobs.Step2
{
// Include the BurstCompile attribute to Burst compile the job.
[BurstCompile]
public struct FindNearestJob : IJob
{
// All of the data which a job will access should
// be included in its fields. In this case, the job needs
// three arrays of float3.
// Array and collection fields that are only read in
// the job should be marked with the ReadOnly attribute.
// Although not strictly necessary in this case, marking data
// as ReadOnly may allow the job scheduler to safely run
// more jobs concurrently with each other.
// (See the "Intro to jobs" for more detail.)
[ReadOnly] public NativeArray<float3> TargetPositions;
[ReadOnly] public NativeArray<float3> SeekerPositions;
// For SeekerPositions[i], we will assign the nearest
// target position to NearestTargetPositions[i].
public NativeArray<float3> NearestTargetPositions;
// 'Execute' is the only method of the IJob interface.
// When a worker thread executes the job, it calls this method.
public void Execute()
{
// Compute the square distance from each seeker to every target.
for (int i = 0; i < SeekerPositions.Length; i++)
{
float3 seekerPos = SeekerPositions[i];
float nearestDistSq = float.MaxValue;
for (int j = 0; j < TargetPositions.Length; j++)
{
float3 targetPos = TargetPositions[j];
float distSq = math.distancesq(seekerPos, targetPos);
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
NearestTargetPositions[i] = targetPos;
}
}
}
}
}
}
using Unity.Burst; using Unity.Collections; using Unity.Jobs; // We'll use Unity.Mathematics.float3 instead of Vector3, // and we'll use Unity.Mathematics.math.distancesq instead of Vector3.sqrMagnitude. using Unity.Mathematics; namespace Tutorials.Jobs.Step2 { // Include the BurstCompile attribute to Burst compile the job. [BurstCompile] public struct FindNearestJob : IJob { // All of the data which a job will access should // be included in its fields. In this case, the job needs // three arrays of float3. // Array and collection fields that are only read in // the job should be marked with the ReadOnly attribute. // Although not strictly necessary in this case, marking data // as ReadOnly may allow the job scheduler to safely run // more jobs concurrently with each other. // (See the "Intro to jobs" for more detail.) [ReadOnly] public NativeArray<float3> TargetPositions; [ReadOnly] public NativeArray<float3> SeekerPositions; // For SeekerPositions[i], we will assign the nearest // target position to NearestTargetPositions[i]. public NativeArray<float3> NearestTargetPositions; // 'Execute' is the only method of the IJob interface. // When a worker thread executes the job, it calls this method. public void Execute() { // Compute the square distance from each seeker to every target. for (int i = 0; i < SeekerPositions.Length; i++) { float3 seekerPos = SeekerPositions[i]; float nearestDistSq = float.MaxValue; for (int j = 0; j < TargetPositions.Length; j++) { float3 targetPos = TargetPositions[j]; float distSq = math.distancesq(seekerPos, targetPos); if (distSq < nearestDistSq) { nearestDistSq = distSq; NearestTargetPositions[i] = targetPos; } } } } } }
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;

// We'll use Unity.Mathematics.float3 instead of Vector3,
// and we'll use Unity.Mathematics.math.distancesq instead of Vector3.sqrMagnitude.
using Unity.Mathematics;

namespace Tutorials.Jobs.Step2
{
    // Include the BurstCompile attribute to Burst compile the job.
    [BurstCompile]
    public struct FindNearestJob : IJob
    {
        // All of the data which a job will access should 
        // be included in its fields. In this case, the job needs
        // three arrays of float3.

        // Array and collection fields that are only read in
        // the job should be marked with the ReadOnly attribute.
        // Although not strictly necessary in this case, marking data  
        // as ReadOnly may allow the job scheduler to safely run 
        // more jobs concurrently with each other.
        // (See the "Intro to jobs" for more detail.)

        [ReadOnly] public NativeArray<float3> TargetPositions;
        [ReadOnly] public NativeArray<float3> SeekerPositions;

        // For SeekerPositions[i], we will assign the nearest 
        // target position to NearestTargetPositions[i].
        public NativeArray<float3> NearestTargetPositions;

        // 'Execute' is the only method of the IJob interface.
        // When a worker thread executes the job, it calls this method.
        public void Execute()
        {
            // Compute the square distance from each seeker to every target.
            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                float3 seekerPos = SeekerPositions[i];
                float nearestDistSq = float.MaxValue;
                for (int j = 0; j < TargetPositions.Length; j++)
                {
                    float3 targetPos = TargetPositions[j];
                    float distSq = math.distancesq(seekerPos, targetPos);
                    if (distSq < nearestDistSq)
                    {
                        nearestDistSq = distSq;
                        NearestTargetPositions[i] = targetPos;
                    }
                }
            }
        }
    }
}
약 30FPS 성능
하나의 FindNearest 객체에서 관리, 프레임마다 22.77ms, Worker Therad는 Idle
JobHandle.Complete가 21.45ms인 이유는 Complete 호출이 반환되려면 실제 잡이 실행 및 완료될 것을 기다려야 하기 때문임

FindNearest 업데이트 자체는 아무것도 하지 않고 잡이 실행 및 완료될 것을 기다리고 있음

Burst는 아직 미사용이라서 Job이 메인 스레드에서 실행 중

Step 1 보다 성능이 향상된 원인은 Step 1의 Seeker의 Update() 1000개에 대한 오버헤드가 발생되지 않음 (오직 1개)

트랜스폼을 순회할 때 메모리를 건너 뛰면서 트랜스폼에 액세스했는데 이는 캐시에 부하주는 작업을 배열로 변경하여 성능을 향상시킴

Job에 BurstCompile 속성이 추가되었는데 BurstCompile될 수 있음을 뜻함
활성화
30-> 102 FPS로 엄청난 성능 향상을 할 수 있음
JobHandle.Complete가 21.45ms -> 1.48ms로 단축됨 (유니티에서는 약 20배의 성능 향상이 있다고함)

Step 3. Scene – ParallelJob

일반적인 브루트 포스 순회 방법, 하나의 FindNearest에서 Target과의 거리를 확인

Step 1, 2는 각 1,000개 Step 3는 각 5000개의 인스턴스를 사용

FindNearest.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Spawner = Tutorials.Jobs.Step2.Spawner;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
namespace Tutorials.Jobs.Step3
{
// Exactly the same as in prior step except this schedules a parallel job instead of a single-threaded job.
public class FindNearest : MonoBehaviour
{
NativeArray<float3> TargetPositions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
public void Start()
{
Spawner spawner = GetComponent<Spawner>();
TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
}
public void OnDestroy()
{
TargetPositions.Dispose();
SeekerPositions.Dispose();
NearestTargetPositions.Dispose();
}
public void Update()
{
for (int i = 0; i < TargetPositions.Length; i++)
{
TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
}
for (int i = 0; i < SeekerPositions.Length; i++)
{
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
}
FindNearestJob findJob = new FindNearestJob
{
TargetPositions = TargetPositions,
SeekerPositions = SeekerPositions,
NearestTargetPositions = NearestTargetPositions,
};
// Execute will be called once for every element of the SeekerPositions array,
// with every index from 0 up to (but not including) the length of the array.
// The Execute calls will be split into batches of 100.
JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100);
findHandle.Complete();
for (int i = 0; i < SeekerPositions.Length; i++)
{
Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
}
}
}
}
using Spawner = Tutorials.Jobs.Step2.Spawner; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; namespace Tutorials.Jobs.Step3 { // Exactly the same as in prior step except this schedules a parallel job instead of a single-threaded job. public class FindNearest : MonoBehaviour { NativeArray<float3> TargetPositions; NativeArray<float3> SeekerPositions; NativeArray<float3> NearestTargetPositions; public void Start() { Spawner spawner = GetComponent<Spawner>(); TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent); SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); } public void OnDestroy() { TargetPositions.Dispose(); SeekerPositions.Dispose(); NearestTargetPositions.Dispose(); } public void Update() { for (int i = 0; i < TargetPositions.Length; i++) { TargetPositions[i] = Spawner.TargetTransforms[i].localPosition; } for (int i = 0; i < SeekerPositions.Length; i++) { SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition; } FindNearestJob findJob = new FindNearestJob { TargetPositions = TargetPositions, SeekerPositions = SeekerPositions, NearestTargetPositions = NearestTargetPositions, }; // Execute will be called once for every element of the SeekerPositions array, // with every index from 0 up to (but not including) the length of the array. // The Execute calls will be split into batches of 100. JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100); findHandle.Complete(); for (int i = 0; i < SeekerPositions.Length; i++) { Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]); } } } }
using Spawner = Tutorials.Jobs.Step2.Spawner;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;

namespace Tutorials.Jobs.Step3
{
    // Exactly the same as in prior step except this schedules a parallel job instead of a single-threaded job.
    public class FindNearest : MonoBehaviour
    {
        NativeArray<float3> TargetPositions;
        NativeArray<float3> SeekerPositions;
        NativeArray<float3> NearestTargetPositions;

        public void Start()
        {
            Spawner spawner = GetComponent<Spawner>();
            TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
            SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
            NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
        }

        public void OnDestroy()
        {
            TargetPositions.Dispose();
            SeekerPositions.Dispose();
            NearestTargetPositions.Dispose();
        }

        public void Update()
        {
            for (int i = 0; i < TargetPositions.Length; i++)
            {
                TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
            }

            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
            }

            FindNearestJob findJob = new FindNearestJob
            {
                TargetPositions = TargetPositions,
                SeekerPositions = SeekerPositions,
                NearestTargetPositions = NearestTargetPositions,
            };

            // Execute will be called once for every element of the SeekerPositions array,
            // with every index from 0 up to (but not including) the length of the array.
            // The Execute calls will be split into batches of 100.
            JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100);

            findHandle.Complete();

            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
            }
        }
    }
}

FindNearestJob.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Tutorials.Jobs.Step3
{
[BurstCompile]
public struct FindNearestJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> TargetPositions;
[ReadOnly] public NativeArray<float3> SeekerPositions;
public NativeArray<float3> NearestTargetPositions;
public void Execute(int index)
{
float3 seekerPos = SeekerPositions[index];
float nearestDistSq = float.MaxValue;
for (int i = 0; i < TargetPositions.Length; i++)
{
float3 targetPos = TargetPositions[i];
float distSq = math.distancesq(seekerPos, targetPos);
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
NearestTargetPositions[index] = targetPos;
}
}
}
}
}
using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; namespace Tutorials.Jobs.Step3 { [BurstCompile] public struct FindNearestJob : IJobParallelFor { [ReadOnly] public NativeArray<float3> TargetPositions; [ReadOnly] public NativeArray<float3> SeekerPositions; public NativeArray<float3> NearestTargetPositions; public void Execute(int index) { float3 seekerPos = SeekerPositions[index]; float nearestDistSq = float.MaxValue; for (int i = 0; i < TargetPositions.Length; i++) { float3 targetPos = TargetPositions[i]; float distSq = math.distancesq(seekerPos, targetPos); if (distSq < nearestDistSq) { nearestDistSq = distSq; NearestTargetPositions[index] = targetPos; } } } } }
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

namespace Tutorials.Jobs.Step3
{
    [BurstCompile]
    public struct FindNearestJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<float3> TargetPositions;
        [ReadOnly] public NativeArray<float3> SeekerPositions;

        public NativeArray<float3> NearestTargetPositions;

        public void Execute(int index)
        {
            float3 seekerPos = SeekerPositions[index];
            float nearestDistSq = float.MaxValue;
            for (int i = 0; i < TargetPositions.Length; i++)
            {
                float3 targetPos = TargetPositions[i];
                float distSq = math.distancesq(seekerPos, targetPos);
                if (distSq < nearestDistSq)
                {
                    nearestDistSq = distSq;
                    NearestTargetPositions[index] = targetPos;
                }
            }
        }
    }
}
약 31.8FPS
ParallelJob을 이용하여 Job이 다른 모든 워커 스레드에 병렬적으로 분할되도록 함
100개의 인덱스 단위로 병렬 작업

Step 4. Scene – ParallelJob_Sorting

정렬을 이용한 병렬 작업

각 10,000개의 인스턴스 생성

FindNearest.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Spawner = Tutorials.Jobs.Step2.Spawner;
namespace Tutorials.Jobs.Step4
{
public class FindNearest : MonoBehaviour
{
NativeArray<float3> TargetPositions;
NativeArray<float3> SeekerPositions;
NativeArray<float3> NearestTargetPositions;
public void Start()
{
Spawner spawner = GetComponent<Spawner>();
TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
}
public void OnDestroy()
{
TargetPositions.Dispose();
SeekerPositions.Dispose();
NearestTargetPositions.Dispose();
}
public void Update()
{
for (int i = 0; i < TargetPositions.Length; i++)
{
TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
}
for (int i = 0; i < SeekerPositions.Length; i++)
{
SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
}
SortJob<float3, AxisXComparer> sortJob = TargetPositions.SortJob(new AxisXComparer { });
FindNearestJob findJob = new FindNearestJob
{
TargetPositions = TargetPositions,
SeekerPositions = SeekerPositions,
NearestTargetPositions = NearestTargetPositions,
};
JobHandle sortHandle = sortJob.Schedule();
JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100, sortHandle);
findHandle.Complete();
for (int i = 0; i < SeekerPositions.Length; i++)
{
Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
}
}
}
}
using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; using UnityEngine; using Spawner = Tutorials.Jobs.Step2.Spawner; namespace Tutorials.Jobs.Step4 { public class FindNearest : MonoBehaviour { NativeArray<float3> TargetPositions; NativeArray<float3> SeekerPositions; NativeArray<float3> NearestTargetPositions; public void Start() { Spawner spawner = GetComponent<Spawner>(); TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent); SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent); } public void OnDestroy() { TargetPositions.Dispose(); SeekerPositions.Dispose(); NearestTargetPositions.Dispose(); } public void Update() { for (int i = 0; i < TargetPositions.Length; i++) { TargetPositions[i] = Spawner.TargetTransforms[i].localPosition; } for (int i = 0; i < SeekerPositions.Length; i++) { SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition; } SortJob<float3, AxisXComparer> sortJob = TargetPositions.SortJob(new AxisXComparer { }); FindNearestJob findJob = new FindNearestJob { TargetPositions = TargetPositions, SeekerPositions = SeekerPositions, NearestTargetPositions = NearestTargetPositions, }; JobHandle sortHandle = sortJob.Schedule(); JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100, sortHandle); findHandle.Complete(); for (int i = 0; i < SeekerPositions.Length; i++) { Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]); } } } }
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
using UnityEngine;
using Spawner = Tutorials.Jobs.Step2.Spawner;

namespace Tutorials.Jobs.Step4
{
    public class FindNearest : MonoBehaviour
    {
        NativeArray<float3> TargetPositions;
        NativeArray<float3> SeekerPositions;
        NativeArray<float3> NearestTargetPositions;

        public void Start()
        {
            Spawner spawner = GetComponent<Spawner>();
            TargetPositions = new NativeArray<float3>(spawner.NumTargets, Allocator.Persistent);
            SeekerPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
            NearestTargetPositions = new NativeArray<float3>(spawner.NumSeekers, Allocator.Persistent);
        }

        public void OnDestroy()
        {
            TargetPositions.Dispose();
            SeekerPositions.Dispose();
            NearestTargetPositions.Dispose();
        }

        public void Update()
        {
            for (int i = 0; i < TargetPositions.Length; i++)
            {
                TargetPositions[i] = Spawner.TargetTransforms[i].localPosition;
            }

            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                SeekerPositions[i] = Spawner.SeekerTransforms[i].localPosition;
            }

            SortJob<float3, AxisXComparer> sortJob = TargetPositions.SortJob(new AxisXComparer { });

            FindNearestJob findJob = new FindNearestJob
            {
                TargetPositions = TargetPositions,
                SeekerPositions = SeekerPositions,
                NearestTargetPositions = NearestTargetPositions,
            };

            JobHandle sortHandle = sortJob.Schedule();
            JobHandle findHandle = findJob.Schedule(SeekerPositions.Length, 100, sortHandle);
            findHandle.Complete();

            for (int i = 0; i < SeekerPositions.Length; i++)
            {
                Debug.DrawLine(SeekerPositions[i], NearestTargetPositions[i]);
            }
        }
    }
}

FindNearestJob.cs

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;
namespace Tutorials.Jobs.Step4
{
[BurstCompile]
public struct FindNearestJob : IJobParallelFor
{
[ReadOnly] public NativeArray<float3> TargetPositions;
[ReadOnly] public NativeArray<float3> SeekerPositions;
public NativeArray<float3> NearestTargetPositions;
public void Execute(int index)
{
float3 seekerPos = SeekerPositions[index];
// Find the target with the closest X coord.
int startIdx = TargetPositions.BinarySearch(seekerPos, new AxisXComparer { });
// When no precise match is found, BinarySearch returns the bitwise negation of the last-searched offset.
// So when startIdx is negative, we flip the bits again, but we then must ensure the index is within bounds.
if (startIdx < 0) startIdx = ~startIdx;
if (startIdx >= TargetPositions.Length) startIdx = TargetPositions.Length - 1;
// The position of the target with the closest X coord.
float3 nearestTargetPos = TargetPositions[startIdx];
float nearestDistSq = math.distancesq(seekerPos, nearestTargetPos);
// Searching upwards through the array for a closer target.
Search(seekerPos, startIdx + 1, TargetPositions.Length, +1, ref nearestTargetPos, ref nearestDistSq);
// Search downwards through the array for a closer target.
Search(seekerPos, startIdx - 1, -1, -1, ref nearestTargetPos, ref nearestDistSq);
NearestTargetPositions[index] = nearestTargetPos;
}
void Search(float3 seekerPos, int startIdx, int endIdx, int step,
ref float3 nearestTargetPos, ref float nearestDistSq)
{
for (int i = startIdx; i != endIdx; i += step)
{
float3 targetPos = TargetPositions[i];
float xdiff = seekerPos.x - targetPos.x;
// If the square of the x distance is greater than the current nearest, we can stop searching.
if ((xdiff * xdiff) > nearestDistSq) break;
float distSq = math.distancesq(targetPos, seekerPos);
if (distSq < nearestDistSq)
{
nearestDistSq = distSq;
nearestTargetPos = targetPos;
}
}
}
}
public struct AxisXComparer : IComparer<float3>
{
public int Compare(float3 a, float3 b)
{
return a.x.CompareTo(b.x);
}
}
}
using System.Collections.Generic; using Unity.Burst; using Unity.Collections; using Unity.Jobs; using Unity.Mathematics; namespace Tutorials.Jobs.Step4 { [BurstCompile] public struct FindNearestJob : IJobParallelFor { [ReadOnly] public NativeArray<float3> TargetPositions; [ReadOnly] public NativeArray<float3> SeekerPositions; public NativeArray<float3> NearestTargetPositions; public void Execute(int index) { float3 seekerPos = SeekerPositions[index]; // Find the target with the closest X coord. int startIdx = TargetPositions.BinarySearch(seekerPos, new AxisXComparer { }); // When no precise match is found, BinarySearch returns the bitwise negation of the last-searched offset. // So when startIdx is negative, we flip the bits again, but we then must ensure the index is within bounds. if (startIdx < 0) startIdx = ~startIdx; if (startIdx >= TargetPositions.Length) startIdx = TargetPositions.Length - 1; // The position of the target with the closest X coord. float3 nearestTargetPos = TargetPositions[startIdx]; float nearestDistSq = math.distancesq(seekerPos, nearestTargetPos); // Searching upwards through the array for a closer target. Search(seekerPos, startIdx + 1, TargetPositions.Length, +1, ref nearestTargetPos, ref nearestDistSq); // Search downwards through the array for a closer target. Search(seekerPos, startIdx - 1, -1, -1, ref nearestTargetPos, ref nearestDistSq); NearestTargetPositions[index] = nearestTargetPos; } void Search(float3 seekerPos, int startIdx, int endIdx, int step, ref float3 nearestTargetPos, ref float nearestDistSq) { for (int i = startIdx; i != endIdx; i += step) { float3 targetPos = TargetPositions[i]; float xdiff = seekerPos.x - targetPos.x; // If the square of the x distance is greater than the current nearest, we can stop searching. if ((xdiff * xdiff) > nearestDistSq) break; float distSq = math.distancesq(targetPos, seekerPos); if (distSq < nearestDistSq) { nearestDistSq = distSq; nearestTargetPos = targetPos; } } } } public struct AxisXComparer : IComparer<float3> { public int Compare(float3 a, float3 b) { return a.x.CompareTo(b.x); } } }
using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Jobs;
using Unity.Mathematics;

namespace Tutorials.Jobs.Step4
{
    [BurstCompile]
    public struct FindNearestJob : IJobParallelFor
    {
        [ReadOnly] public NativeArray<float3> TargetPositions;
        [ReadOnly] public NativeArray<float3> SeekerPositions;

        public NativeArray<float3> NearestTargetPositions;

        public void Execute(int index)
        {
            float3 seekerPos = SeekerPositions[index];

            // Find the target with the closest X coord.
            int startIdx = TargetPositions.BinarySearch(seekerPos, new AxisXComparer { });

            // When no precise match is found, BinarySearch returns the bitwise negation of the last-searched offset.
            // So when startIdx is negative, we flip the bits again, but we then must ensure the index is within bounds.
            if (startIdx < 0) startIdx = ~startIdx;
            if (startIdx >= TargetPositions.Length) startIdx = TargetPositions.Length - 1;

            // The position of the target with the closest X coord.
            float3 nearestTargetPos = TargetPositions[startIdx];
            float nearestDistSq = math.distancesq(seekerPos, nearestTargetPos);

            // Searching upwards through the array for a closer target.
            Search(seekerPos, startIdx + 1, TargetPositions.Length, +1, ref nearestTargetPos, ref nearestDistSq);

            // Search downwards through the array for a closer target.
            Search(seekerPos, startIdx - 1, -1, -1, ref nearestTargetPos, ref nearestDistSq);

            NearestTargetPositions[index] = nearestTargetPos;
        }

        void Search(float3 seekerPos, int startIdx, int endIdx, int step,
                    ref float3 nearestTargetPos, ref float nearestDistSq)
        {
            for (int i = startIdx; i != endIdx; i += step)
            {
                float3 targetPos = TargetPositions[i];
                float xdiff = seekerPos.x - targetPos.x;

                // If the square of the x distance is greater than the current nearest, we can stop searching.
                if ((xdiff * xdiff) > nearestDistSq) break;

                float distSq = math.distancesq(targetPos, seekerPos);

                if (distSq < nearestDistSq)
                {
                    nearestDistSq = distSq;
                    nearestTargetPos = targetPos;
                }
            }
        }
    }

    public struct AxisXComparer : IComparer<float3>
    {
        public int Compare(float3 a, float3 b)
        {
            return a.x.CompareTo(b.x);
        }
    }
}

x축을 따라 TargetPositions를 정렬합니다

이어서 FindNearestJob에서 바이너리 검색을 통해 탐색기의 x 좌표와 가장 가까운 타겟의 위치를 찾고

그 인덱스 주위를 탐색해 가장 가까운 타겟을 찾습니다.

그러면 대부분의 경우 탐색기를 모든 타겟과 비교하는 대신 몇몇 타겟과만 비교하는 작업이 되어 더 간단합니다.

먼저 SortJob은 Collections 패키지의 네이티브 배열 확장 메서드로서 SortJob 구조체를 반환합니다.

그 구조체 자체는 잡이 아니지만 Schedule 메서드를 호출할 경우 하나가 아닌 두 개의 잡이 예약됩니다.

이 메서드는 먼저 segmentSortJob을 생성하고 예약하는데 이는 배열의 서로 다른 세그먼트를 독립적으로 정렬하는 병렬 잡입니다.

이후 두 번째 작업이자 싱글 스레드 작업이며 같은 배열 내 세그먼트들을 병합하는 SegmentSortMerge가 예약됩니다

이 두 잡은 기존 배열 내에서 MergeSort를 수행합니다.

결정적으로 두 번째의 병합 잡은 세그먼트 정렬이 완료될 때까지 기다려야 합니다.

그래서 두 번째 잡을 예약할 때 첫 번째 잡의 잡 핸들을 전달하면 첫 번째 잡은 두 번째 잡의 종속 관계가 됩니다.

잡이 종속 관계를 가질 경우 워커 스레드들은 그 종속 관계의 실행이 모두 완료될 때까지 해당 잡을 실행하지 않습니다.

둘 이상의 잡을 예약해야 하며 해당 잡들의 실행 순서를 지정해야 할 때는 종속 관계를 통해 그 순서를 정할 수 있습니다.

이 코드에서 FindNearestJob을 예약할 때 sortJob 핸들을 종속성으로 전달하는데 이는 MergeSort가 완료될 때까지

이 잡의 실행이 시작되어서는 안 되기 때문입니다.

이후 잡이 완료되면 잡의 모든 종속성도 묵시적으로 완료되므로 SegmentSort, SegmentMerge, FindNearestJob의 세 잡이 모두 완료됩니다.

FindNearestJob을 보면 Collections 패키지의 네이티브 배열 확장 메서드인 BinarySearch를 호출하고 있습니다.

이는 단지 X 좌표와 비교해서 SeekerPos와 일치하는 TargetPositions 내 인덱스를 반환합니다.

대부분은 정확히 일치하는 결과를 찾을 수 없을 것이므로 반환되는 인덱스 값은 대부분 음수가 될 겁니다.

하지만 이 메서드에 숨겨진 요령이 있다면 반환된 인덱스 값의 비트를 뒤집어서 배열에서 가장 가까운 일치 값의 인덱스를 얻을 수 있다는 겁니다.

해당 값을 고정해서 범위 내에 있게 하면 됩니다.

이제 x 좌표가 탐색기의 x 좌표와 가장 가까운 타겟의 인덱스를 알아냈지만 타겟을 위아래로 검색하여 탐색기와 더욱 가까운 타겟이 존재하지 않는지 확인합니다.

이 검색에서 중요한 요령은 x 좌표값의 차이가 지금까지 찾아낸 최단 거리보다 더 클 경우 검색을 중단할 수 있다는 겁니다.

TargetPositions가 x 좌표에 따라 정렬되니까요

이 임곗값을 넘고 나면 해당 임곗값 반대편에 존재하는 모든 후보들은 우리가 이미 찾아낸 후보보다 더 가까이에 있을 수 없습니다.

31 FPS의 성능
SegmentSortMerge는 하나의 스레드에서만 실행
트랜스폼을복사하는 데 시간이 더 오래 걸림

“DOTS 정리 (2) – Unity Job 시스템 시작하기”에 대한 1개의 생각

  1. 핑백: DOTS 정리 (3) – HelloCube로 엔티티 알아보기 - 어제와 내일의 나 그 사이의 이야기

댓글 달기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다