ИÑпользование управлÑемых типов в Задачах
(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:
// Создаем world ManagedObjectWorld world = new ManagedObjectWorld(); // ДобавлÑем управлÑемый объект // Получаем обратно ÑÑылку ManagedObjectRef<string> message = world.Add("Hello!"); // Получаем управлÑемый объект по ÑÑылке string str = world.Get(message); print(str); // Hello! // УдалÑем объект world.Remove(message);
Ошибки обрабатываютÑÑ Ð´Ð¾Ð²Ð¾Ð»ÑŒÐ½Ð¾ логично:
// Передать null ManagedObjectRef<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 remove string 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.