Введение в Job System
(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 valuesIJobParallelForTransform
: 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. ЕÑли еÑÑ‚ÑŒ какие-либо вопроÑÑ‹, не ÑтеÑнÑйтеÑÑŒ задавать их в комментариÑÑ…!