(Russian translation from English by Maxim Voloshin)

Новая Job System, недавно дебютировавшая в Unity 2018.1, начала процесс изменения того, как действительно должны быть написаны все скрипты. В сочетании с грядущей ECS и Burst компилятором, старая парадигма, основанная на использовании MonoBehaviour, в конце концов будет упразднена. Эта статья является руководством к тому, как начать изучать новый путь написания Unity скриптов.

Используемые типы

Каждый blittable значимый тип может быть использован в любой задаче(job). Чтобы тип был одновременно и blittable, и значимый, он должен быть примитивом(int и т.д.), перечислением, указателем или структурой, содержащей только примитивы, перечисления, указатели и blittable структуры.

Это определение исключает все ссылочные типы. Это означает, что классы, интерфейсы, делегаты, динамические типы, строки и (управляемые) массивы не могут быть использованы в задачах.

Нативные коллекции, такие как NativeArray и NativeSlice могут быть использованы в задачах. Пользовательские нативные коллекции также могут быть использованы. Гораздо больше типов, таких как NativeHashMap станут доступны в будущих версиях Unity, особенно с добавлением ECS (Entity-Component-System), которая сейчас находится в “превью.”

Используемое API

С того момента как задачи запущены в главном потоке, они не могут получить доступ к большинству стандартного API движка. Только особые части API могут быть использованы. На сколько я знаю, единственное документированное и не экспериментальное API, которое безопасно использовать в 2018.1 это Transform и доступ к нему должен осуществляться через TransformAccess или TransformAccessArray. Ниже мы посмотрим как это делается.

Несмотря на наличие только одного документированного API, мы можем найти больше функционала, который может быть использован вне главного потока, чтением C# исходинков Unity. Чтобы найти именно то, что нам нужно, необходимо искать функции с атрибутами [ThreadSafe или [FreeFunction] совместно со свойством IsThreadSafe, возвращающим true.

Для версии 2018.1.0f2 можно найти следующее:

UnityEngine.CoreModule/UnityEngine.iOS/Device.cs-        public static extern void SetNoBackupFlag(string path);
UnityEngine.CoreModule/UnityEngine.iOS/Device.cs-        public static extern void ResetNoBackupFlag(string path);
UnityEngine.CoreModule/UnityEngine.iOS/Device.cs-        public static extern bool RequestStoreReview();
UnityEngine.CoreModule/UnityEngine.iOS/NotificationHelper.cs-        internal static extern void DestroyLocal(IntPtr target);
UnityEngine.CoreModule/UnityEngine.iOS/NotificationHelper.cs-        internal static extern void DestroyRemote(IntPtr target);
UnityEngine.CoreModule/UnityEngine.iOS/OnDemandResourcesRequest.cs-        private static extern void DestroyFromScript(IntPtr ptr);
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        private Quaternion GetRotation()
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        private Vector3 GetLossyScale()
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        private bool IsIdentity()
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        private float GetDeterminant()
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        private FrustumPlanes DecomposeProjection()
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public bool ValidTRS()
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static AtomicSafetyHandle Create()
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static AtomicSafetyHandle GetTempUnsafePtrSliceHandle()
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static void Release(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static extern void UseSecondaryVersion(ref AtomicSafetyHandle handle);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static void SetAllowSecondaryVersionWriting(AtomicSafetyHandle handle, bool allowWriting)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static EnforceJobResult EnforceAllBufferJobsHaveCompleted(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static EnforceJobResult EnforceAllBufferJobsHaveCompletedAndRelease(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static EnforceJobResult EnforceAllBufferJobsHaveCompletedAndDisableReadWrite(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static int GetReaderArray(AtomicSafetyHandle handle, int maxCount, IntPtr output)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static JobHandle GetWriter(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static string GetReaderName(AtomicSafetyHandle handle, int readerIndex)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static string GetWriterName(AtomicSafetyHandle handle)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        private static extern int GetFieldOffsetInStruct(FieldInfo field);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        private static extern int GetFieldOffsetInClass(FieldInfo field);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void* PinGCObjectAndGetAddress(object target, out ulong gcHandle);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public static extern void ReleaseGCObject(ulong gcHandle);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void CopyObjectAddressToPtr(object target, void* dstPtr);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void* Malloc(long size, int alignment, Allocator allocator);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void Free(void* memory, Allocator allocator);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void MemCpy(void* destination, void* source, long size);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void MemCpyReplicate(void* destination, void* source, int size, int count);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void MemCpyStride(void* destination, int destinationStride, void* source, int sourceStride, int elementSize, int count);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void MemMove(void* destination, void* source, long size);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public unsafe static extern void MemClear(void* destination, long size);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public static extern int SizeOf(Type type);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        public static extern bool IsBlittable(Type type);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/UnsafeUtility.cs-        internal static extern void LogError(string msg, string filename, int linenumber);
UnityEngine.CoreModule/UnityEngine/Bounds.cs-        public bool Contains(Vector3 point)
UnityEngine.CoreModule/UnityEngine/Bounds.cs-        public float SqrDistance(Vector3 point)
UnityEngine.CoreModule/UnityEngine/Bounds.cs-        private static bool IntersectRayAABB(Ray ray, Bounds bounds, out float dist)
UnityEngine.CoreModule/UnityEngine/Bounds.cs-        public Vector3 ClosestPoint(Vector3 point)
UnityEngine.CoreModule/UnityEngine/MaterialPropertyBlock.cs-        private static extern void DestroyImpl(IntPtr mpb);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern int ClosestPowerOfTwo(int value);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern bool IsPowerOfTwo(int value);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern int NextPowerOfTwo(int value);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern float GammaToLinearSpace(float value);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern float LinearToGammaSpace(float value);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static Color CorrelatedColorTemperatureToRGB(float kelvin)
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern ushort FloatToHalf(float val);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern float HalfToFloat(ushort val);
UnityEngine.CoreModule/UnityEngine/Mathf.cs-        public static extern float PerlinNoise(float x, float y);
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 TRS(Vector3 pos, Quaternion q, Vector3 s)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 Inverse(Matrix4x4 m)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 Transpose(Matrix4x4 m)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 Ortho(float left, float right, float bottom, float top, float zNear, float zFar)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 Perspective(float fov, float aspect, float zNear, float zFar)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 LookAt(Vector3 from, Vector3 to, Vector3 up)
UnityEngine.CoreModule/UnityEngine/Matrix4x4.cs-        public static Matrix4x4 Frustum(float left, float right, float bottom, float top, float zNear, float zFar)
UnityEngine.CoreModule/UnityEngine/MonoBehaviour.cs-        private static extern void ConstructorCheck([Writable] Object self);
UnityEngine.CoreModule/UnityEngine/Object.cs-        private static extern int GetOffsetOfInstanceIDInCPlusPlusObject();
UnityEngine.CoreModule/UnityEngine/Object.cs-        private static extern bool CurrentThreadIsMainThread();
UnityEngine.CoreModule/UnityEngine/Object.cs-        internal static extern bool DoesObjectWithInstanceIDExist(int instanceID);
UnityEngine.CoreModule/UnityEngine/Ping.cs-        private static extern void Internal_Destroy(IntPtr ptr);
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion FromToRotation(Vector3 fromDirection, Vector3 toDirection)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion Inverse(Quaternion rotation)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion Slerp(Quaternion a, Quaternion b, float t)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion SlerpUnclamped(Quaternion a, Quaternion b, float t)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion Lerp(Quaternion a, Quaternion b, float t)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion LerpUnclamped(Quaternion a, Quaternion b, float t)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        private static Quaternion Internal_FromEulerRad(Vector3 euler)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        private static Vector3 Internal_ToEulerRad(Quaternion rotation)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        private static void Internal_ToAxisAngleRad(Quaternion q, out Vector3 axis, out float angle)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion AngleAxis(float angle, Vector3 axis)
UnityEngine.CoreModule/UnityEngine/Quaternion.cs-        public static Quaternion LookRotation(Vector3 forward, [DefaultValue("Vector3.up")] Vector3 upwards)
UnityEngine.CoreModule/UnityEngine/Screen.cs-            Width { get; }
UnityEngine.CoreModule/UnityEngine/Screen.cs-            Height { get; }
UnityEngine.CoreModule/UnityEngine/ScriptableObject.cs-        private static extern void CreateScriptableObject([Writable] ScriptableObject self);
UnityEngine.CoreModule/UnityEngine/UnityLogWriter.cs-        private static extern void WriteStringToUnityLogImpl(string s);
UnityEngine.CoreModule/UnityEngine/Vector3.cs-        public static Vector3 Slerp(Vector3 a, Vector3 b, float t)
UnityEngine.CoreModule/UnityEngine/Vector3.cs-        public static Vector3 SlerpUnclamped(Vector3 a, Vector3 b, float t)
UnityEngine.CoreModule/UnityEngine/Vector3.cs-        private static extern void OrthoNormalize2(ref Vector3 a, ref Vector3 b);
UnityEngine.CoreModule/UnityEngine/Vector3.cs-        private static extern void OrthoNormalize3(ref Vector3 a, ref Vector3 b, ref Vector3 c);
UnityEngine.CoreModule/UnityEngine/Vector3.cs-        public static Vector3 RotateTowards(Vector3 current, Vector3 target, float maxRadiansDelta, float maxMagnitudeDelta)
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccessArray.cs-        internal static extern IntPtr GetSortedTransformAccess(IntPtr transformArrayIntPtr);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccessArray.cs-        internal static extern IntPtr GetSortedToUserIndex(IntPtr transformArrayIntPtr);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void GetPosition(ref TransformAccess access, out Vector3 p);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void SetPosition(ref TransformAccess access, ref Vector3 p);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void GetRotation(ref TransformAccess access, out Quaternion r);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void SetRotation(ref TransformAccess access, ref Quaternion r);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void GetLocalPosition(ref TransformAccess access, out Vector3 p);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void SetLocalPosition(ref TransformAccess access, ref Vector3 p);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void GetLocalRotation(ref TransformAccess access, out Quaternion r);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void SetLocalRotation(ref TransformAccess access, ref Quaternion r);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void GetLocalScale(ref TransformAccess access, out Vector3 r);
UnityEngine.CoreModule/UnityEngine.Jobs/TransformAccess.cs-        private static extern void SetLocalScale(ref TransformAccess access, ref Vector3 r);
UnityEngine.CoreModule/Unity.Jobs.LowLevel.Unsafe/JobsUtility.cs-        public static extern bool GetWorkStealingRange(ref JobRanges ranges, int jobIndex, out int beginIndex, out int endIndex);
UnityEngine.CoreModule/Unity.Jobs.LowLevel.Unsafe/JobsUtility.cs-        public unsafe static extern void PatchBufferMinMaxRanges(IntPtr bufferRangePatchData, void* jobdata, int startIndex, int rangeSize);
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static void SetAllowReadOrWriteAccess(AtomicSafetyHandle handle, bool allowReadWriteAccess)
UnityEngine.CoreModule/Unity.Collections.LowLevel.Unsafe/AtomicSafetyHandle.cs-        public static bool GetAllowReadOrWriteAccess(AtomicSafetyHandle handle)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static extern bool HasNodePool(IntPtr navMeshQuery);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private unsafe static PathQueryStatus BeginFindPath(IntPtr navMeshQuery, NavMeshLocation start, NavMeshLocation end, int areaMask, void* costs)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static extern PathQueryStatus UpdateFindPath(IntPtr navMeshQuery, int iterations, out int iterationsPerformed);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static extern PathQueryStatus EndFindPath(IntPtr navMeshQuery, out int pathSize);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private unsafe static extern int GetPathResult(IntPtr navMeshQuery, void* path, int maxPath);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static bool IsValidPolygon(IntPtr navMeshQuery, PolygonId polygon)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static int GetAgentTypeIdForPolygon(IntPtr navMeshQuery, PolygonId polygon)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static bool IsPositionInPolygon(IntPtr navMeshQuery, Vector3 position, PolygonId polygon)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static PathQueryStatus GetClosestPointOnPoly(IntPtr navMeshQuery, PolygonId polygon, Vector3 position, out Vector3 nearest)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static NavMeshLocation MapLocation(IntPtr navMeshQuery, Vector3 position, Vector3 extents, int agentTypeID, int areaMask = -1)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private unsafe static extern void MoveLocations(IntPtr navMeshQuery, void* locations, void* targets, void* areaMasks, int count);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private unsafe static extern void MoveLocationsInSameAreas(IntPtr navMeshQuery, void* locations, void* targets, int count, int areaMask);
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static NavMeshLocation MoveLocation(IntPtr navMeshQuery, NavMeshLocation location, Vector3 target, int areaMask)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static bool GetPortalPoints(IntPtr navMeshQuery, PolygonId polygon, PolygonId neighbourPolygon, out Vector3 left, out Vector3 right)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static Matrix4x4 PolygonLocalToWorldMatrix(IntPtr navMeshQuery, PolygonId polygon)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static Matrix4x4 PolygonWorldToLocalMatrix(IntPtr navMeshQuery, PolygonId polygon)
UnityEngine.AIModule/UnityEngine.Experimental.AI/NavMeshQuery.cs-        private static NavMeshPolyTypes GetPolygonType(IntPtr navMeshQuery, PolygonId polygon)
UnityEngine.UnityAnalyticsModule/UnityEngine.Analytics/UnityAnalyticsHandler.cs-        internal static extern void Internal_Destroy(IntPtr ptr);
UnityEngine.UnityAnalyticsModule/UnityEngine.Analytics/CustomEventData.cs-        internal static extern void Internal_Destroy(IntPtr ptr);
UnityEngine.AnimationModule/UnityEngine.Animations/AnimatorControllerPlayable.cs-        private static extern int StringToHash(string name);
UnityEngine.UnityConnectModule/UnityEngine/RemoteConfigSettings.cs-        internal static extern void Internal_Destroy(IntPtr ptr);
UnityEngine.GameCenterModule/UnityEngine.SocialPlatforms.GameCenter/GcLeaderboard.cs-        private static extern void GcLeaderboard_Dispose(IntPtr leaderboard);
UnityEngine.IMGUIModule/UnityEngine/ObjectGUIState.cs-        private static extern void Internal_Destroy(IntPtr ptr);
UnityEngine.UIElementsModule/UnityEngine.CSSLayout/Native.cs-        private static extern void CSSNodeFreeInternal(IntPtr cssNode);
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/DownloadHandler.cs-        private extern void Release();
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/CertificateHandler.cs-        private extern void Release();
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UploadHandler.cs-        private extern void Release();
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs-        private static extern string GetWebErrorString(UnityWebRequest.UnityWebRequestError err);
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs-        private extern void Release();
UnityEngine.UnityWebRequestModule/UnityEngine.Networking/UnityWebRequest.cs-        public extern void Abort();
UnityEngine.ARModule/UnityEngine.XR.Tango/MeshReconstructionServer.cs-        internal static extern void DestroyThreaded(IntPtr server);
UnityEngine.AnimationModule/UnityEngine/Animator.cs-        public static extern int StringToHash(string name);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern void InternalSetScriptingPtr(uint providerId, AudioSampleProvider provider);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern bool InternalIsValid(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern uint InternalGetMaxSampleFrameCount(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern uint InternalGetAvailableSampleFrameCount(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern uint InternalGetFreeSampleFrameCount(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern uint InternalGetFreeSampleFrameCountLowThreshold(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern void InternalSetFreeSampleFrameCountLowThreshold(uint providerId, uint sampleFrameCount);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern bool InternalGetEnableSampleFramesAvailableEvents(uint providerId);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern void InternalSetEnableSampleFramesAvailableEvents(uint providerId, bool enable);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern bool InternalGetEnableSilencePadding(uint id);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern void InternalSetEnableSilencePadding(uint id, bool enabled);
UnityEngine.AudioModule/UnityEngine.Experimental.Audio/AudioSampleProvider.cs-        private static extern IntPtr InternalGetConsumeSampleFramesNativeFunctionPtr();

Заметьте, что некоторые из них приватные, но они могут быть использованы косвенно через публичное API. Также, вызов Debug.Log косвенно вызовет приватный UnityLogWriter.WriteStringToUnityLogImpl.

Есть немного функций, написанных на чистом C#. Например, Vector3.Lerp и Color.Lerp не делают никаких вызовов в нативную C++ часть движка. Любая из этих функций может быть использована совместно с задачами. Также хорошо использовать API, вызывающее только потокобезопасные функции в нативном коде, описанные и перечисленные выше.

По мере выхода новых версий Unity, все больше и больше API становится доступно для JobSystem. Например, дорожная карта Unity говорит, что Playable API будет доступно для задач в версии 2018.2.

Помимо Unity API, любое другое API, которое является потокобезопасным, может быть использовано. Может быть тяжело найти какие-либо функции, подпадающие под ограничения используемых типов в JobSystem, но это вполне возможно. Также есть возможность написать новое API.

Определение работы

Самый первый шаг – это определить задачу. Задача должна выполнять небольшой объем четко определенной работы, почти как функция. Для начала, давайте опишем структуру:

public struct ApplyVelocityJob
{
}

Далее, решите какого типа будет эта задача. На данный момент есть три типа задач из которых и нужно сделать выбор:

  • IJob: простая задача, запускаемая единожды
  • IJobParallelFor: a job that’s executed for a range of values
  • IJobParallelForTransform: a job that accesses transforms

Давайте начнем с самого простого IJob и его реализацией:

public struct ApplyVelocityJob : IJob
{
    public void Execute()
    {
    }
}

Теперь мы должны определить исходные и выходные данные задачи. Чтобы сделать это мы добавим поля в структуру вместо добавления аргументов в параметры метода Execute. Эта задача будет менять позицию объекта через соответствующую скорость с учетом промежутка прошедшего времени. Теперь давайте добавим поля:

public struct ApplyVelocityJob : IJob
{
    public float ElapsedTime;
    public NativeArray<Vector3> Positions;
    public NativeArray<Vector3> Velocities;
 
    public void Execute()
    {
    }
}

Для лучшей оптимизации и обработки ошибок, мы дополнительно добавим атрибут [ReadOnly] и [WriteOnly] к полям-параметрам:

public struct ApplyVelocityJob : IJob
{
    [ReadOnly]
    public float ElapsedTime;
 
    public NativeArray<Vector3> Positions;
 
    [ReadOnly]
    public NativeArray<Vector3> Velocities;
 
    public void Execute()
    {
    }
}

Последний шаг заключается в реализации всей логики задачи путем заполнения метода Execute:

public struct ApplyVelocityJob : IJob
{
    [ReadOnly]
    public float ElapsedTime;
 
    public NativeArray<Vector3> Positions;
 
    [ReadOnly]
    public NativeArray<Vector3> Velocities;
 
    public void Execute()
    {
        for (int i = 0; i < Positions.Length; ++i)
        {
            Positions[i] += Velocities[i] * ElapsedTime;
        }
    }
}

Вот и все, что нужно сделать! Однако, есть возможность легко распараллелить эту задачу, унаследовав ее от IJobParallelFor. Вместо запуска Execute единожды для всего массива, система задач запустит Execute один раз для каждого элемента массива. Каждый раз, когда вызывается Execute, он может выполняться на разных ядрах процессора. Это означает, что задача сможет использовать все ядра в системе после нескольких простых изменений! Давайте посмотрим как это сделать:

public struct ApplyVelocityParallelJob : IJobParallelFor
{
    [ReadOnly]
    public float ElapsedTime;
 
    public NativeArray<Vector3> Positions;
 
    [ReadOnly]
    public NativeArray<Vector3> Velocities;
 
    public void Execute(int index)
    {
        Positions[index] += Velocities[index] * ElapsedTime;
    }
}

После того, как цикл for, по сути, был перемещен в систему задач, возможно, параллельная версия стала даже проще, т.к. мы избавились от цикла.

И наконец, если мы хотим изменить трансформации объекта внутри задачи, нам нужно наследоваться от IJobParallelForTransform. Давайте изменим эту задачу так, чтобы она не просто использовала Vector3 в NativeArray, а напрямую меняла позицию объекта:

public struct ApplyVelocityTransformJob : IJobParallelForTransform
{
    [ReadOnly]
    public float ElapsedTime;
 
    [ReadOnly]
    public NativeArray<Vector3> Velocities;
 
    public void Execute(int index, TransformAccess transform)
    {
        transform.localPosition += Velocities[index] * ElapsedTime;
    }
}
Создание задачи

Создание задачи так же просто, как и создание любой другой структуры. Просто вызовите new и заполните все поля. Синтаксис инициализатора может в этом хорошо помочь:

ApplyVelocityJob job = new ApplyVelocityJob
{
    ElapsedTime = Time.time,
    Positions = positions,
    Velocities = velocities
};
 
ApplyVelocityParallelJob parallelJob = new ApplyVelocityParallelJob
{
    ElapsedTime = Time.time,
    Positions = positions,
    Velocities = velocities
};
 
ApplyVelocityTransformJob transformJob = new ApplyVelocityTransformJob
{
    ElapsedTime = Time.time,
    Velocities = velocities
};
Старт задач

Задача может быть запущена несколькими способами. Давайте запустим пример с IJob:

JobHandle jobHandle = job.Schedule();

Планирование задачи скажет системе задач, что она готова для выполнения. В результате мы получаем обработчик JobHandle, который мы можем использовать в будущем. Например, если мы хотим выполнить задачу, сразу после другой задачи, мы можем передать обработчик в Schedule. Таким образом, чтобы запустить одну и ту же задачу дважды мы можем запустить ее второй раз, после того как выполнение первого раза завершится:

// Запланировать без зависимостей
JobHandle jobHandle1 = job.Schedule();
 
// Запланировать с зависимостью
JobHandle jobHandle2 = job.Schedule(jobHandle1);

Теперь давайте запланируем IJobParallelFor. Чтобы сделать это, нам надо передать пару дополнительных параметров. Первый – число всех итераций цикла, которые надо выполнить. Второй – число итераций на одном ядре процессора. Вместо одного универсального значения, это число должно быть больше для маленьких задач и меньше для больших задач. Вот как работает вызов планировщика:

// Запланировать без зависимостей
JobHandle jobHandle1 = parallelJob.Schedule(positions.Length, 32);
 
// Запланировать с зависимостью
JobHandle jobHandle2 = parallelJob.Schedule(positions.Length, 32, jobHandle1);

И последний IJobParallelForTransform, который принимает параметр TransformAccessArray. Этот параметр оборачивает управляемый массив Transform и должен быть вручную освобожден вызовом Dispose, так же как NativeArray. Вот как создать и запланировать задачу:

// Получаем или создаем управляемый массив Transform
Transform[] transforms = new [] { transform };
 
// Оборачиваем управляемый массив с помощью TransformAccessArray
TransformAccessArray accessArray = new TransformAccessArray(transforms);
 
// Запланировать без зависимостей
JobHandle jobHandle1 = transformJob.Schedule(accessArray);
 
// Запланировать с зависимостью
JobHandle jobHandle2 = transformJob.Schedule(accessArray, jobHandle1);

Есть еще один последний шаг после того как все задачи были запланированы или есть значительное количество работы, которую нужно выполнить в главном потоке перед планированием следующих задач. Мы должны сказать системе задач, что готовы начать выполнение всего, что запланировали:

JobHandle.ScheduleBatchedJobs();

Нормальная ситуация планировать задачи через Schedule и JobHandle.ScheduleBatchedJobs, но IJob и IJobParallelFor также могут быть запущены мгновенно и синхронно - выполнение продолжится только после завершения задачи. Такую возможность поддерживает и IJobParallelForTransform. Мы вызываем Run с разными параметрами, но без каких-либо зависимостей. Теперь запустим задачу:

job.Run();
 
parallelJob.Run(positions.Length);
Complex Dependencies

Ранее, все наши Schedule методы принимали только один JobHandle параметр с зависимостью. Это хорошо, когда каждая задача напрямую зависит только от одной другой задачи и косвенно от “связного списка” задач:

// a -> b -> c
JobHandle a = job.Schedule();
JobHandle b = job.Schedule(a);
JobHandle c = job.Schedule(b);

Иногда нам нужна задача, зависимая от результата двух или более задач. JobHandle.CombineDependencies предназначен как раз для таких ситуаций и соединяет несколько обработчиков в один. Существует три перегрузки, зависящих от количества объединяемых обработчиков:

// Соединение двух обработчиков
JobHandle ab = JobHandle.CombineDependencies(a, b);
 
// Соединение трех
JobHandle abc = JobHandle.CombineDependencies(a, b, c);
 
// Объединяет произвольное количество обработчиков
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(4, Allocator.Persistent);
handles[0] = a;
handles[1] = b;
handles[2] = c;
handles[3] = d;
JobHandle abcd = JobHandle.CombineDependencies(handles);

Теперь мы можем строить более сложные “деревья” зависимостей, нежели просто “связный список”:

// a & b -> c -> d & e
//       -> f
JobHandle a = job.Schedule();
JobHandle b = job.Schedule();
JobHandle ab = JobHandle.CombineDependencies(a, b);
JobHandle c = job.Schedule(ab);
JobHandle f = job.Schedule(ab);
JobHandle d = job.Schedule(c);
JobHandle e = job.Schedule(c);
Finishing the Job

Будучи запланированными, задачи будут запущены системой автоматически. Мы можем, однако, узнать статус этих задач. Для начала, есть свойство, которое мы можем проверить, чтобы узнать закончено ли выполнение задачи:

if (jobHandle1.IsCompleted)
{
    Debug.Log("Job done");
}

Еще есть функция для проверки зависит ли задача от другой. Она вернет true только между временем вызова Schedule и временем окончания выполнения зависимой задачи, т.к. иначе она уже не будет иметь зависимости.

if (JobHandle.CheckFenceIsDependencyOrDidSyncFence(jobHandle, parallelJobHandle))
{
    Debug.Log("Job still waiting on Parallel Job");
}

Наконец, если в главном потоке мы дошли до того момента, когда у нас должен быть результат выполнения задачи, можно остановить главный поток, пока задача не завершится. Один из способов сделать это - вызвать Complete:

jobHandle.Complete();

Если у нас много “листьев” в “дереве” зависимостей задач, CompleteAll может быть более подходящим, поскольку это позволяет нам ждать завершения нескольких задач. Как и в случае с CombineDependencies, есть три перегрузки:

// Ждать завершения двух задач
JobHandle.CompleteAll(a, b);
 
// Ждать завершения трех задач
JobHandle.CompleteAll(a, b, c);
 
// Ждать завершения произвольного количества задач
NativeArray<JobHandle> handles = new NativeArray<JobHandle>(4, Allocator.Temp);
handles[0] = a;
handles[1] = b;
handles[2] = c;
handles[3] = d;
JobHandle.CompleteAll(handles);
К чему все идет

Использование Job System это новый способ писать код в Unity, и часто не сразу становится очевидно куда поместить каждую из этих частей. Хотя самого правильного способа не существует, типичный шаблон можно описать так:

public class TestScript : MonoBehaviour
{
    // Данные этих задач помещаются в поля и должны быть освобождены позже
    // Они включают доступ к массиву Transform
    private NativeArray<Vector3> positions;
    private NativeArray<Vector3> velocities;
    private TransformAccessArray transformAccessArray;
 
    // Обработчики задач помещаются в поля, чтобы проверить статусы и дождаться их позже
    private JobHandle jobHandle;
    private JobHandle parallelJobHandle;
    private JobHandle transformJobHandle;
 
    // Как альтернатива: Awake() и Start()
    void OnEnable()
    {
        // Создаем данные для их обработки в задачах
        // Включая доступ к Transform
        positions = new NativeArray<Vector3>(100, Allocator.Persistent);
        velocities = new NativeArray<Vector3>(100, Allocator.Persistent);
        for (int i = 0; i < velocities.Length; ++i)
        {
            velocities[i] = Vector3.one;
        }
        Transform[] transforms = { transform };
        transformAccessArray = new TransformAccessArray(transforms);
    }
 
    // Альтернативы: OnDestroy() и OnApplicationQuit() 
    void OnDisable()
    {
        // Освобождаем данные 
        // Включая массив Transform
        positions.Dispose();
        velocities.Dispose();
        transformAccessArray.Dispose();
    }
 
    // Или FixedUpdate(), но будьте осторожны с несколькими вызовами за один кадр
    void Update()
    {
        // Создаем задачи и передаем параметры через поля
        ApplyVelocityJob job = new ApplyVelocityJob
        {
            ElapsedTime = Time.time,
            Positions = positions,
            Velocities = velocities
        };
        ApplyVelocityParallelJob parallelJob = new ApplyVelocityParallelJob
        {
            ElapsedTime = Time.time,
            Positions = positions,
            Velocities = velocities
        };
        ApplyVelocityTransformJob transformJob = new ApplyVelocityTransformJob
        {
            ElapsedTime = Time.time,
            Velocities = velocities
        };
 
        // Планирование задач, включая зависимости
        // Сохраняем обработчики в поля
        jobHandle = job.Schedule();
        parallelJobHandle = parallelJob.Schedule(
            positions.Length,
            32,
            jobHandle);
        transformJobHandle = transformJob.Schedule(
            transformAccessArray,
            parallelJobHandle);
 
        // Стартуем задачи
        JobHandle.ScheduleBatchedJobs();
    }
 
    // Как альтернатива: Update() или FixedUpdate() в том же или в следующем кадре
    private void LateUpdate()
    {
        // Подождать пока листья дерева зависимостей задач завершатся
        transformJobHandle.Complete();
    }
}

Как написано в комментариях, есть возможность для переноса частей этого шаблона в альтернативные методы. Самое главное то, что главный поток ответственен за создание, планирование и ожидание задачи, включая создание и освобождение параметров задачи. Для каждой конкретной игры это будут разные места, особенно в отношении того, когда ждать завершения. Это может произойти позже в том же кадре, в котором запущены задачи или в следующем кадре, если выполнение занимает больше времени.

Заключение

Job System удивительно легко использовать, учитывая, насколько она мощная. Пока Unity планируют внедрять новое API в долгосрочной перспективе, часть самого важного функционала (например, перемещение персонажей) уже доступна. Примеры, как это может быть использовано в 2018.1, можете найти в проекте на GitHub. Если есть какие-либо вопросы, не стесняйтесь задавать их в комментариях!