[Unity ECS] 在Job中使用托管类型

63 篇文章 11 订阅

英文原文:

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 计划的一部分从托管类型过渡。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值