(Russian translation from English by Maxim Voloshin)
Структуры, иÑпользуемые Job System не могут Ñодержать управлÑемые типы вроде string
, class
, или делегатов. Ðа данный момент Ñто Ð±Ð¾Ð»ÑŒÑˆÐ°Ñ Ð¿Ñ€Ð¾Ð±Ð»ÐµÐ¼Ð° Ñ‚.к. их повÑемеÑтно иÑпользует Unity API и мы вынуждены Ñ Ð½Ð¸Ð¼Ð¸ работать. Ð¡ÐµÐ³Ð¾Ð´Ð½Ñ Ð¼Ñ‹ поговорим о том, как мы можем преодолеть Ñти Ð¾Ð³Ñ€Ð°Ð½Ð¸Ñ‡ÐµÐ½Ð¸Ñ Ð¸ иÑпользовать управлÑемые типы в задачах.
УправлÑемый подход
Дабы продемонÑтрировать, чего мы хотим добитьÑÑ, начнем Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸ ÐºÐ¾Ñ‚Ð¾Ñ€Ð°Ñ Ð¸Ñпользует уйму управлÑемых типов. Ее цель выбрать текÑÑ‚ Ñ Ñ€ÐµÐ·ÑƒÐ»ÑŒÑ‚Ð°Ñ‚Ð°Ð¼Ð¸, который будет показан в конце игры.
struct Player{ public int Id; public int Points; public int Health;}struct ChooseTextJobManaged : IJob{ public Player Player; public Player[] AllPlayers; public string WinText; public string LoseText; public string DrawText; public string[] ChosenText; public void Execute() { // ЕÑли мы умерли, то мы проиграли if (Player.Health <= 0) { ChosenText[0] = LoseText; return; } // Выбрать живого игрока Ñ Ð¼Ð°ÐºÑимальным количеÑтвом очков, кроме Ð½Ð°Ñ Player mostPointsPlayer = new Player { Id = 0, Points = int.MinValue }; foreach (Player player in AllPlayers) { // Мертвый if (player.Health <= 0) { continue; } // Мы if (player.Id == Player.Id) { continue; } // МакÑимум очков if (player.Points > mostPointsPlayer.Points) { mostPointsPlayer = player; } } // У Ð½Ð°Ñ Ð±Ð¾Ð»ÑŒÑˆÐµ очков чем у кого либо... выиграли if (Player.Points > mostPointsPlayer.Points) { ChosenText[0] = WinText; } // У Ð½Ð°Ñ Ð¼ÐµÐ½ÑŒÑˆÐµ очков чем у топового игрока… проиграли else if (Player.Points < mostPointsPlayer.Points) { ChosenText[0] = LoseText; } // У Ð½Ð°Ñ Ñтолько же очков как и у топового игрока... Ð½Ð¸Ñ‡ÑŒÑ else { ChosenText[0] = DrawText; } }}
Ðа Ñамом деле, Ñама логика не имеет Ð·Ð½Ð°Ñ‡ÐµÐ½Ð¸Ñ Ð² данном Ñлучае. Важно то, что задача пытаетÑÑ Ð²Ð·ÑÑ‚ÑŒ одно из полей типа string
: (WinText
, LoseText
, DrawText
) и его значение уÑтановить в ChosenText[0]
который, между прочим, Ñлемент управлÑемого маÑÑива Ñтрок.
Ðтот код нарушает требование, что задачи, даже не компилируемые Burst, не должны иÑпользовать управлÑемые типы, такие как string
или управлÑемые маÑÑивы, наподобие string[]
. Ðо вÑе равно давайте попробуем запуÑтить его:
class TestScript : MonoBehaviour{ void Start() { Player player = new Player { Id = 1, Health = 10, Points = 10 }; Player[] allPlayers = { player, new Player { Id = 2, Health = 10, Points = 5 }, new Player { Id = 3, Health = 0, Points = 5 } }; string winText = "You win!"; string loseText = "You lose!"; string drawText = "You tied!"; string[] chosenText = new string[1]; new ChooseTextJobManaged { Player = player, AllPlayers = allPlayers, WinText = winText, LoseText = loseText, DrawText = drawText, ChosenText = chosenText }.Run(); print(chosenText[0]); }}
Вызов ChooseTextJobManaged.Run
приводит к тому, что Unity броÑает иÑключение:
InvalidOperationException: ChooseTextJobManaged.AllPlayers is not a value type. Job structs may not contain any reference types.Unity.Jobs.LowLevel.Unsafe.JobsUtility.CreateJobReflectionData (System.Type type, Unity.Jobs.LowLevel.Unsafe.JobType jobType, System.Object managedJobFunction0, System.Object managedJobFunction1, System.Object managedJobFunction2) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/ScriptBindings/Jobs.bindings.cs:96)Unity.Jobs.IJobExtensions+JobStruct`1[T].Initialize () (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:23)Unity.Jobs.IJobExtensions.Run[T] (T jobData) (at /Users/builduser/buildslave/unity/build/Runtime/Jobs/Managed/IJob.cs:42)TestScript.Start () (at Assets/TestScript.cs:75)
Unity жалуетÑÑ, что AllPlayers
ÑвлÑетÑÑ ÑƒÐ¿Ñ€Ð°Ð²Ð»Ñемым (“ÑÑылочнымâ€) типом поÑкольку Ñто управлÑемый маÑÑив. ЕÑли бы мы Ñделали его NativeArray
, мы бы получили другое иÑключение об оÑтальных полÑÑ…, навроде WinText
.
УправлÑемые ÑÑылки
Чтобы обойти Ñто ограничение, нам надо будет чем-то заменить объекты и управлÑемый маÑÑив. Мы можем легко заменить управлÑемый маÑÑив на NativeArray
, но Ñами объекты не имеют замены из коробки.
ФактичеÑки, мы не можем иÑпользовать управлÑемые объекты изнутри задачи, но ключевой момент в том, что нам доÑтаточно проÑто ÑоÑлатьÑÑ Ð½Ð° них. То еÑÑ‚ÑŒ ChooseTextJob
проÑто выбирает Ñтроку, не работает Ñ Ñимволами, не ÑоединÑет неÑколько Ñтрок и не Ñоздает новую.
ПолучаетÑÑ, что вÑе, что нам на Ñамом деле нужно, Ñто нечто, что может поÑлужить ÑÑылкой на управлÑемый объект, а не Ñам управлÑемый объект. ПроÑтой int
Ñделает Ñто, при уÑловии, что у Ð½Ð°Ñ ÐµÑÑ‚ÑŒ отображение Ñтого int
на управлÑемый объект когда нам нужно его иÑпользовать.
Ð’Ñпомним подход из Ñтрого типизированные int и обернем int
в Ñтруктуру. Мы не будем перегружать никаких операторов, потому что в данном Ñлучае Ð´Ð»Ñ int
Ñто излишне, но Ñто добавит ÑтрогоÑти, по Ñравнению Ñ Ð¸Ñпользованием неименованного int
.
public struct ManagedObjectRef<T> where T : class{ public readonly int Id; public ManagedObjectRef(int id) { Id = id; }}
Теперь вмеÑто string
, мы можем иÑпользовать ManagedObjectRef
. Само по Ñебе наличие имени типа не даÑÑ‚ Unity выброÑить иÑключение. Ð’Ñе что мы здеÑÑŒ имеем Ñто int
и он подходит как Ð½ÐµÐ»ÑŒÐ·Ñ ÐºÑтати Ð´Ð»Ñ Ð¸ÑÐ¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ð½Ð¸Ñ Ñ Ð·Ð°Ð´Ð°Ñ‡Ð°Ð¼Ð¸.
Далее, нам нужно найти ÑпоÑоб Ñоздать ÑÑылку и обратитьÑÑ Ðº ней позже. Давайте обернем проÑтой Ñловарь Dictionary
чтобы Ñделать вот Ñто:
using System.Collections.Generic;public class ManagedObjectWorld{ private int m_NextId; private readonly Dictionary<int, object> m_Objects; public ManagedObjectWorld(int initialCapacity = 1000) { m_NextId = 1; m_Objects = new Dictionary<int, object>(initialCapacity); } public ManagedObjectRef<T> Add<T>(T obj) where T : class { int id = m_NextId; m_NextId++; m_Objects[id] = obj; return new ManagedObjectRef<T>(id); } public T Get<T>(ManagedObjectRef<T> objRef) where T : class { return (T)m_Objects[objRef.Id]; } public void Remove<T>(ManagedObjectRef<T> objRef) where T : class { m_Objects.Remove(objRef.Id); }}
Ð’Ñе отлично, Ñто клаÑÑ, он иÑпользует Dictionary
который, в Ñвою очередь, иÑпользует управлÑемые объекты, потому что только он предназначен Ð´Ð»Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ вне Job System.
Вот как мы будем иÑпользовать ManagedObjectWorld
:
// Создаем worldManagedObjectWorld world = new ManagedObjectWorld();// ДобавлÑем управлÑемый объект// Получаем обратно ÑÑылкуManagedObjectRef<string> message = world.Add("Hello!");// Получаем управлÑемый объект по ÑÑылкеstring str = world.Get(message);print(str); // Hello!// УдалÑем объектworld.Remove(message);
Ошибки обрабатываютÑÑ Ð´Ð¾Ð²Ð¾Ð»ÑŒÐ½Ð¾ логично:
// Передать nullManagedObjectRef<string> nullRef = default(ManagedObjectRef<string>);string str = world.Get(nullRef); // Exception: ID 0 не найден// Ðеверный типManagedObjectRef<string> hi = world.Add("Hello!");ManagedObjectRef<int[]> wrongTypeRef = new ManagedObjectRef<int[]>(hi.Id);int[] arr = world.Get(wrongTypeRef); // Exception: приведение string в int[] не удалоÑÑŒ// Двойное удалениеworld.Remove(hi);world.Remove(hi); // ПуÑтой вызов// Get after removestring hiStr = message.Get(hi); // Exception: ID isn't found (it was removed)
New Job
Теперь, когда ManagedObjectRef
и ManagedObjectWorld
в нашем раÑпорÑжении, мы можем преобразовать ChooseTextJobManaged
в ChooseTextJobRef
Ñделав Ñледующие изменениÑ:
- Заменить вÑе управлÑемые маÑÑивы на
NativeArray
(т.е.string[]
наNativeArray
) - Заменить вÑе управлÑемые объекты на
ManagedObjectRef
(т.е.string
наManagedObjectRef
) - БонуÑ: Заменить
foreach
наfor
(Ð´Ð»Ñ ÑовмеÑтимоÑти Ñ Burst)
Обратите внимание, что логика, Ñама по Ñебе, не изменилаÑÑŒ.
Ð¤Ð¸Ð½Ð°Ð»ÑŒÐ½Ð°Ñ Ð²ÐµÑ€ÑÐ¸Ñ Ð·Ð°Ð´Ð°Ñ‡Ð¸:
[BurstCompile]struct ChooseTextJobRef : IJob{ public Player Player; public NativeArray<Player> AllPlayers; public ManagedObjectRef<string> WinText; public ManagedObjectRef<string> LoseText; public ManagedObjectRef<string> DrawText; public NativeArray<ManagedObjectRef<string>> ChosenText; public void Execute() { // ЕÑли мы умерли, то мы проиграли if (Player.Health <= 0) { ChosenText[0] = LoseText; return; } // Выбрать живого игрока Ñ Ð¼Ð°ÐºÑимальным количеÑтвом очков, кроме Ð½Ð°Ñ Player mostPointsPlayer = new Player { Id = 0, Points = int.MinValue }; for (int i = 0; i < AllPlayers.Length; i++) { Player player = AllPlayers[i]; // Мертвый if (player.Health <= 0) { continue; } // Мы if (player.Id == Player.Id) { continue; } // МакÑимум очков if (player.Points > mostPointsPlayer.Points) { mostPointsPlayer = player; } } // У Ð½Ð°Ñ Ð±Ð¾Ð»ÑŒÑˆÐµ очков чем у кого либо... выиграли if (Player.Points > mostPointsPlayer.Points) { ChosenText[0] = WinText; } // У Ð½Ð°Ñ Ð¼ÐµÐ½ÑŒÑˆÐµ очков чем у топового игрока… проиграли else if (Player.Points < mostPointsPlayer.Points) { ChosenText[0] = LoseText; } // У Ð½Ð°Ñ Ñтолько же очков как и у топового игрока... Ð½Ð¸Ñ‡ÑŒÑ else { ChosenText[0] = DrawText; } }}
Ðаконец, доработаем код запуÑка задачи чтобы передать NativeArray
и ManagedObjectRef
:
class TestScript : MonoBehaviour{ void Start() { Player player = new Player { Id = 1, Health = 10, Points = 10 }; NativeArray<Player> allPlayers = new NativeArray<Player>(3, Allocator.TempJob); allPlayers[0] = player; allPlayers[1] = new Player { Id = 2, Health = 10, Points = 5 }; allPlayers[2] = new Player { Id = 3, Health = 0, Points = 5 }; string winText = "You win!"; string loseText = "You lose!"; string drawText = "You tied!"; ManagedObjectWorld world = new ManagedObjectWorld(); ManagedObjectRef<string> winTextRef = world.Add(winText); ManagedObjectRef<string> loseTextRef = world.Add(loseText); ManagedObjectRef<string> drawTextRef = world.Add(drawText); NativeArray<ManagedObjectRef<string>> chosenText = new NativeArray<ManagedObjectRef<string>>(1, Allocator.TempJob); new ChooseTextJobRef { Player = player, AllPlayers = allPlayers, WinText = winTextRef, LoseText = loseTextRef, DrawText = drawTextRef, ChosenText = chosenText }.Run(); print(world.Get(chosenText[0])); allPlayers.Dispose(); chosenText.Dispose(); }}
При запуÑке программа выведет You win!
как и ожидалоÑÑŒ.
Заключение
ЕÑли Вам надо только ÑоÑлатьÑÑ Ð½Ð° управлÑемые объекты внутри задачи и не нужно их иÑпользовать, Ñто отноÑительно легко решить их заменой на ManagedObjectRef
и ManagedObjectWorld
. Мы можем Ñделать Ñто даже при компилÑции Ñ Burst и мы можем поддерживать безопаÑноÑÑ‚ÑŒ типов, иÑÐ¿Ð¾Ð»ÑŒÐ·ÑƒÑ Ð¿Ð¾Ð´Ñ…Ð¾Ð´ Ñтрогой целочиÑленной типизации. Ðто может помочь преодолеть отÑтавание API пока Unity уходит от управлÑемых типов в рамках их инициативы DOTS.