英文原文:
https://www.jacksondunstan.com/articles/5397
我们永远无法在Job中实际使用托管对象
Job 结构不能包含托管类型,如字符串、类实例或委托。目前这是一个痛苦,因为很多 Unity API 依赖于这些,所以我们不得不处理它们。今天我们将讨论如何在我们的Job中使用托管类型来弥补这一点。
管理方法
为了说明我们想要做什么,让我们从使用大量托管类型的工作开始。其目的是选择要为游戏结果显示的文本。
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 we died, we lose
if (Player.Health <= 0)
{
ChosenText[0] = LoseText;
return;
}
// Get the highest points of any alive player except us
Player mostPointsPlayer = new Player { Id = 0, Points = int.MinValue };
foreach (Player player in AllPlayers)
{
// Dead
if (player.Health <= 0)
{
continue;
}
// Us
if (player.Id == Player.Id)
{
continue;
}
// High score
if (player.Points > mostPointsPlayer.Points)
{
mostPointsPlayer = player;
}
}
// We have more points than the player with the most points... win
if (Player.Points > mostPointsPlayer.Points)
{
ChosenText[0] = WinText;
}
// We have less points than the player with the most points... lose
else if (Player.Points < mostPointsPlayer.Points)
{
ChosenText[0] = LoseText;
}
// We have the same points than the player with the most points... draw
else
{
ChosenText[0] = DrawText;
}
}
}
逻辑在这里并不重要。重要的部分是Job想要选择一个字符串字段(WinText、LoseText、DrawText)并将其设置为 ChosenText[0],这是一个托管的字符串数组。
此代码违反了Job的要求,即使是不由 Burst 编译的Job,也不得访问诸如字符串之类的托管类型和诸如 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 轻松替换托管数组,但托管对象没有直接替换。
我们永远无法在Job中实际使用托管对象,但这里的关键实现是我们只需要引用它们。也就是说,ChooseTextJob 只是选择一个字符串,它不会查看它的字符、连接它或构建字符串。
所以我们真正需要的是可以作为对托管对象的引用的东西,而不是托管对象本身。一个简单的 int 就可以了,前提是当我们需要使用对象时,我们有一个将该 int 映射到我们在工作之外可用的托管对象。
让我们从强类型整数方法中获取一个页面,并将该 int 包装在一个结构中。我们不会重载任何运算符,因为 int 不打算以这种方式使用,但这将添加强大的命名类型而不是使用原始 int。
public struct ManagedObjectRef<T>
where T : class
{
public readonly int Id;
public ManagedObjectRef(int id)
{
Id = id;
}
}
现在,我们可以使用 ManagedObjectRef 代替字符串。仅仅存在类型名称不会导致 Unity 抛出异常。我们这里真正拥有的只是一个 int,它非常适合在Job中使用。
接下来,我们需要一种方法来创建这些引用并在以后查找它们。让我们包装一个简单的 Dictionary<int, object> 来做到这一点:
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);
}
}
这是一个类,它使用一个字典,并且它使用托管对象是可以的,因为它只打算在Job之外使用。
以下是我们如何使用 ManagedObjectWorld:
// Create the world
ManagedObjectWorld world = new ManagedObjectWorld();
// Add a managed object to the world
// Get a reference back
ManagedObjectRef<string> message = world.Add("Hello!");
// Get a managed object using a reference
string str = world.Get(message);
print(str); // Hello!
// Remove a managed object from the world
world.Remove(message);
错误情况的处理相当合理:
// Get null
ManagedObjectRef<string> nullRef = default(ManagedObjectRef<string>);
string str = world.Get(nullRef); // Exception: ID 0 isn't found
// Wrong type
ManagedObjectRef<string> hi = world.Add("Hello!");
ManagedObjectRef<int[]> wrongTypeRef = new ManagedObjectRef<int[]>(hi.Id);
int[] arr = world.Get(wrongTypeRef); // Exception: cast string to int[] fails
// Double remove
world.Remove(hi);
world.Remove(hi); // No-op
// 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 替换所有托管对象(例如字符串到 ManagedObjectRef)
- 额外:用 for 替换 foreach(为了突发兼容性)
请注意,逻辑本身没有改变。
这是最终的Job:
[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 we died, we lose
if (Player.Health <= 0)
{
ChosenText[0] = LoseText;
return;
}
// Get the highest points of any alive player except us
Player mostPointsPlayer = new Player { Id = 0, Points = int.MinValue };
for (int i = 0; i < AllPlayers.Length; i++)
{
Player player = AllPlayers[i];
// Dead
if (player.Health <= 0)
{
continue;
}
// Us
if (player.Id == Player.Id)
{
continue;
}
// High score
if (player.Points > mostPointsPlayer.Points)
{
mostPointsPlayer = player;
}
}
// We have more points than the player with the most points... win
if (Player.Points > mostPointsPlayer.Points)
{
ChosenText[0] = WinText;
}
// We have less points than the player with the most points... lose
else if (Player.Points < mostPointsPlayer.Points)
{
ChosenText[0] = LoseText;
}
// We have the same points than the player with the most points... draw
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! "正如预期的那样。
总结
如果您只需要引用Job内部的托管对象而不实际使用它们,则将它们替换为 ManagedObjectRef 和 ManagedObjectWorld 相对容易。即使在使用 Burst 编译时我们也可以这样做,并且我们可以在使用强类型整数方法的同时保持类型安全。这可以帮助弥补一些差距,因为 Unity 作为其 DOTS 计划的一部分从托管类型过渡。