unity扩展编辑器_扩展Unity编辑器的技巧-Unity MARS的经验教训

unity扩展编辑器_扩展Unity编辑器的技巧-Unity MARS的经验教训

unity扩展编辑器

The Authoring Tools Group in Unity Labs is developing Unity MARS, a Unity extension that enables users to build and test robust AR experiences. This blog post captures some insights and tips for extending the Editor that have come out of the development of MARS. Keep in mind that some of these are generally applicable, but others depend on your specific use case. Many of these have corresponding examples in our SuperScience GitHub repository.

Unity Labs中的Authoring Tools Group正在开发 Unity MARS ,这是Unity扩展,使用户能够构建和测试强大的AR体验 。 这篇博客文章捕获了一些 扩展 MARS开发中产生的 见解和技巧,以 扩展编辑器 。 请记住,其中一些通常适用,但其他则取决于您的特定用例。 其中许多在我们的 SuperScience GitHub存储库中 都有相应的示例 。

在编辑模式下运行 (Running in Edit Mode)

A Unity MARS Scene generally consists of conditions that real-world data must meet, and digital content that appears when those conditions are met. To support this workflow, Unity MARS has the Simulation View, which lets a user test their setup against different environments and adjust their conditions and content in the context of these environments. Since adjustments made from the Simulation View need to be saved back to the Scene, the simulation must happen in Edit Mode.

Unity MARS场景通常由现实数据必须满足的条件以及满足这些条件时出现的数字内容组成。 为了支持此工作流程,Unity MARS具有模拟视图,该视图可让用户针对不同的环境测试其设置并在这些环境的上下文中调整其条件和内容。 由于需要将从“模拟视图”进行的调整保存回场景,因此模拟必须在“编辑模式”下进行。

There are a couple of different ways to make a MonoBehaviour run in Edit Mode. There are the attributes ExecuteInEditMode and ExecuteAlways, which make all instances of a MonoBehaviour run in Edit Mode. This isn’t ideal for our use case since we only want instances involved in a simulation to run, so instead, we use the property runInEditMode. (Note that ExecuteAlways supports Prefab Mode but ExecuteInEditMode does not. In 2018.3 runInEditMode also supports Prefab Mode.)

有两种不同的方法可以使MonoBehaviour在“编辑模式”下运行。 有 ExecuteInEditModeExecuteAlways 属性 ,这些 属性 使MonoBehaviour的所有实例都在“编辑模式”下运行。 这对于我们的用例而言并不理想,因为我们只希望运行模拟中涉及的实例,因此,我们使用属性 runInEditMode 。 (请注意, ExecuteAlways 支持Prefab模式,但 ExecuteInEditMode 不 支持 。在2018.3中, runInEditMode 也支持Prefab模式。)

The first time a MonoBehaviour’s runInEditMode property is set to true, it starts up the same lifecycle that happens in Play Mode (Awake, Start, etc.). Setting runInEditMode to false after this does stop the object from receiving updates, but it does not trigger OnDisable. It’s only the next time runInEditMode is set to true that the object if it was already enabled, is disabled and reenabled. In MARS we found it handy to make a MonoBehaviour extension method StopRunInEditMode, which disables the MonoBehaviour, sets its runInEditMode to false, and then re-enables it (only if the MonoBehaviour was already enabled). We also have a StartRunInEditMode for consistency, but this just sets runInEditMode to true, which already triggers OnEnable if the MonoBehaviour was enabled.

第一次将MonoBehaviour的 runInEditMode 属性设置为true时,它将启动与播放模式(唤醒,开始等)中相同的生命周期。 在此之后 将 runInEditMode 设置 为false确实会阻止对象接收更新,但是 不会 触发OnDisable。 只有在下一次将 runInEditMode 设置为true时,对象才会被启用,被禁用并重新启用。 在MARS中,我们发现方便地制作MonoBehaviour扩展方法 StopRunInEditMode ,该 方法 禁用MonoBehaviour,将其 runInEditMode 设置 为false,然后重新启用它(仅在MonoBehaviour已启用的情况下)。 我们也有一个 StartRunInEditMode 的一致性,但这只是设置 runInEditMode 为true,如果MonoBehaviour启用这已经触发OnEnable。

By default, an object running in Edit Mode only gets an update whenever something in the Scene changes, but you can also use EditorApplication.QueuePlayerLoopUpdate to force an update. If you want objects to continuously update in Edit Mode, you can hook up QueuePlayerLoopUpdate to the delegate EditorApplication.update.

默认情况下,仅当场景中的某些内容发生更改时,在“编辑模式”下运行的对象才会获得更新,但是您也可以使用 EditorApplication.QueuePlayerLoopUpdate 强制进行更新。 如果希望对象在“编辑模式”下不断更新,则可以将 QueuePlayerLoopUpdate 连接到委托 EditorApplication.update

To see in action how runInEditMode works, you can use our RunInEditHelper. This Editor Window lets you modify which objects are running and toggle the Player Loop.

要实际了解runInEditMode的工作原理,可以使用我们的 RunInEditHelper 。 通过此编辑器窗口,您可以修改正在运行的对象并切换播放器循环。

响应场景中的更改 (Responding to changes in a Scene)

Part of the utility of the Simulation View is that it improves iteration times for setting up and testing an AR Scene — when a user modifies their Scene’s conditions, we restart the simulation process so that the user gets immediate feedback for how their new condition setup works. In Unity MARS the way we detect when a user makes a change to their Scene is by hooking into Undo.postprocessModifications and Undo.undoRedoPerformed.

Simulation View实用程序的一部分功能是它可以缩短用于设置和测试AR场景的迭代时间-当用户修改场景的条件时,我们将重新启动仿真过程,以便用户立即获得有关其新条件设置工作方式的反馈。 在Unity MARS中,我们检测用户何时对其场景进行更改的方法是通过钩住 Undo.postprocessModifications 和 Undo.undoRedoPerformed

The postprocessModifications callback takes an array of UndoPropertyModifications, so if you only want to detect a specific type of modification you can examine the currentValue field of each UndoPropertyModification. For example, in Unity MARS we check the Type of currentValue.target so that we only respond to changes involving Unity MARS-specific Components.

该 postprocessModifications 回调需要数组 UndoPropertyModification S,因此,如果您只想检测修改的具体类型,你可以检查 每一个 UndoPropertyModification 的 CurrentValue的 领域 。 例如,在Unity MARS中,我们检查 currentValue.target 的类型, 以便仅响应涉及Unity MARS特定组件的更改。

Undo.undoRedoPerformed, however, doesn’t take any parameters so it’s harder to know what exactly was changed. It’s likely that when this callback occurs, the modified object(s) will include Selection.activeGameObject or Selection.gameObjects, but this is only guaranteed to be the case when an object is modified through an Inspector that is not locked. It is also always possible for a modification to happen from some arbitrary user code, for example through SerializedProperty.

但是, Undo.undoRedoPerformed 不接受任何参数,因此很难确切地知道更改了什么。 可能发生此回调时,修改后的对象将包括 Selection.activeGameObjectSelection.gameObjects ,但是只有通过未锁定的Inspector修改对象时,才可以保证情况如此。 修改也总是可能发生在某些任意用户代码上,例如通过 SerializedProperty

延迟回应 (Delayed responses)

In many cases, you may have some response to a change that you don’t want to immediately happen from Undo callbacks. For example, a user dragging a slider in the Inspector will trigger a lot of calls to Undo.postprocessModifications, and in Unity MARS we don’t want to restart the simulation process for each of those calls. If you only want to trigger a response when a user is “done” making some continuous change, a reasonable solution is to have a short timer (say about 0.3 seconds) that is reset and started when a change is detected, and to only trigger the response when the timer finishes.

在许多情况下,您可能会对不想 立即 从 撤消 回调中 发生 的更改有所React 。 例如,用户在Inspector中拖动滑块将触发对 Undo.postprocessModifications 的大量调用 ,在Unity MARS中,我们不想为每个调用重新启动仿真过程。 如果您只想在“完成”用户进行一些连续更改时触发响应,则合理的解决方案是让一个短计时器(例如大约0.3秒)在检测到更改时重置并启动,并仅触发计时器结束时的响应。

Here is an example of this pattern in action. You can find the corresponding implementation here.

这是这种模式的一个示例。 您可以在 此处 找到相应的实现 。

存储场景元数据 (Storing Scene metadata)

For each Unity MARS Scene, we need to store certain information as metadata, such as the kinds of real-world data that the Scene requires. There are a couple of different ways you can store metadata for a Scene, each with their own pros and cons:

对于每个Unity MARS场景,我们需要将某些信息存储为元数据,例如场景所需的各种现实数据。 您可以通过几种不同的方式存储场景的元数据,每种方式各有利弊:

  1. A ScriptableObject Asset per-Scene

    一个 个ScriptableObject 资产每个场景

    • Pros

      优点

      • You can access metadata without opening the Scene.

        您无需打开场景即可访问元数据。

      • You can keep Editor-only metadata out of builds (if you have Editor metadata in a separate Asset from Runtime metadata).

        您可以将仅编辑器的元数据保留在构建之外(如果您将编辑器元数据与运行时元数据放在单独的资产中)。

      • It’s easy to find if you store it in the same directory as the Scene.

        很容易找到是否将其存储在与场景相同的目录中。

      Pros

      优点

    • Cons

      缺点

      • You have to make sure it is kept in-sync with the Scene.*

        您必须确保它与场景保持同步。*

      • You have to check for Scene duplication so that metadata also gets duplicated.

        您必须检查场景重复,以便元数据也被重复。

      • It makes the project bulkier (for each Scene you need a new Asset).

        这会使项目变得更大(对于每个场景,您都需要一个新资产)。

      Cons

      缺点

    A ScriptableObject Asset per-Scene

    一个 个ScriptableObject 资产每个场景

  2. One master ScriptableObject Asset that stores metadata for all Scenes

    一个主 ScriptableObject 资产,用于存储所有场景的元数据

    • Pros

      优点

      • You can access metadata without opening the Scene.

        您无需打开场景即可访问元数据。

      • You can keep Editor-only metadata out of builds (if you have Editor metadata in a separate Asset from Runtime metadata).

        您可以将仅编辑器的元数据保留在构建之外(如果您将编辑器元数据与运行时元数据放在单独的资产中)。

      • There’s one metadata Asset per-project rather than one Asset per-Scene.

        每个项目有一个元数据资产,而不是每个场景有一个资产。

      Pros

      优点

    • Cons

      缺点

      • You have to make sure it is kept in-sync with the Scene.*

        您必须确保它与场景保持同步。*

      • You have to check for Scene duplication so that metadata also gets duplicated.

        您必须检查场景重复,以便元数据也被重复。

      • Version control is more difficult — changes to one Scene affect the whole master Asset.

        版本控制更加困难-对一个场景的更改会影响整个主资产。

      • Scenes cannot be distributed among different packages since metadata for all Scenes is stored in one place.

        由于所有场景的元数据都存储在一个地方,因此无法在不同的程序包之间分配场景。

      Cons

      缺点

    One master ScriptableObject Asset that stores metadata for all Scenes

    一个主 ScriptableObject 资产,用于存储所有场景的元数据

  3. A MonoBehaviour in the Scene itself

    一个 MonoBehaviour 在场景本身

    • Pros

      优点

      • It’s easy to keep in-sync with the Scene — saving the Scene inherently saves the metadata.

        与场景保持同步很容易-保存场景会固有地保存元数据。

      • Duplicating the Scene inherently duplicates the metadata.

        复制场景本质上就是复制元数据。

      • It’s easy to find since it’s right there in the Scene.

        因为它就在场景中,所以很容易找到它。

      Pros

      优点

    • Cons

      缺点

      • You can only access metadata by opening the Scene.

        您只能通过打开场景来访问元数据。

      • It can be worse for user experience (“why is this object in my Scene?”)

        对于用户体验而言,情况可能更糟(“为什么此对象在我的场景中?”)

        • Depending on your use case, you could hide the metadata Game Object or Component in the hierarchy and have a custom window for viewing/editing it.

          根据您的用例,您可以 在层次结构中 隐藏 元数据“游戏对象”或“组件”,并具有用于查看/编辑它的自定义窗口。

        It can be worse for user experience (“why is this object in my Scene?”)

        对于用户体验而言,情况可能更糟(“为什么此对象在我的场景中?”)

      Cons

      缺点

    A MonoBehaviour in the Scene itself

    一个 MonoBehaviour 在场景本身

For Unity MARS we originally went with the approach of having one master ScriptableObject. We later switched to using MonoBehaviours since the cons of having a master Asset were more trouble than they were worth (especially merge conflicts and keeping metadata in-sync), and also because at that point, we already needed a Unity MARS-specific Component in the Scene for other reasons.

对于Unity MARS,我们最初采用的是拥有一个主 ScriptableObject的方法 。 后来我们改用 MonoBehaviour, 因为拥有主资产的弊端比其 应有 的麻烦(特别是合并冲突和使元数据保持同步),并且因为那时,我们已经需要特定于Unity MARS的组件由于其他原因在现场。

*If you go with a ScriptableObject approach, be careful about when you sync your metadata with the Scene. If you dirty the metadata Asset while the Scene is unsaved, there is potential for the metadata to be saved before the Scene since saving the project doesn’t save the open Scene. If you delay dirtying the metadata Asset until the Scene is saving, then you need to make sure the metadata gets saved as well at that time. If you use OnWillSaveAssets, you can accomplish this by checking if the given paths include the Scene path and, if so, dirtying the metadata Asset and including its path in the string array you return. Here is an example of how to implement this.

*如果使用 ScriptableObject 方法,请注意在将元数据与场景同步时。 如果在未保存场景的情况下弄脏了元数据资产,则有可能在场景之前保存元数据,因为保存项目不会保存打开的场景。 如果在保存场景之前将元数据资产弄脏,则需要确保同时保存元数据。 如果使用 OnWillSaveAssets ,则可以通过检查给定的路径是否包括Scene路径,以及是否弄脏元数据Asset并将其路径包含在返回的字符串数组中来完成此操作。 是如何实现此示例。

装配体定义 (Assembly Definitions)

It’s important to keep in mind that any extension you write has to play nicely with other extensions and user code. Using assembly definitions means that users don’t have to recompile your extension every time Unity compiles. They also make it easier for users to define their dependencies on your code.

重要的是要记住,您编写的任何扩展都必须与其他扩展和用户代码很好地配合使用。 使用程序集定义 意味着用户不必在每次Unity编译时都重新编译您的扩展。 它们还使用户可以更轻松地定义他们对代码的依赖关系。

There are three standard assemblies for a package or editor extension.

程序包或编辑器扩展有三个标准程序集。

  1. Runtime

    运行

    1. This contains any code that needs to get included in a player build, if any.  An Editor-only extension may not need a Runtime assembly.

      它包含任何需要包含在播放器版本中的代码(如果有)。 仅限编辑器的扩展可能不需要运行时程序集。

    2. Any Component that needs to go on Scene objects, whether it gets included in the build or not, must go in Runtime.

      任何需要在Scene对象上运行的组件,无论是否包含在构建中,都必须在运行时运行。

    3. The runtime assembly cannot reference the Editor assembly, just like scripts in an Editor folder with no assembly definition

      运行时程序集无法引用编辑器程序集,就像没有程序集定义的“编辑器”文件夹中的脚本一样

    Runtime

    运行

  2. Editor

    编辑

    1. This should contain all custom Inspector code as well as any non-Component C# code that is only needed in the Editor.

      它应该包含所有自定义的Inspector代码以及仅在编辑器中需要的任何非组件C#代码。

    2. The Editor assembly definition should only include the Editor as the target platform.

      编辑器程序集定义应仅包括编辑器作为目标平台。

    3. The Editor assembly will almost certainly reference the Runtime assembly

      编辑器程序集几乎肯定会引用运行时程序集

    Editor

    编辑

  3. Tests

    测验

    1. This should contain all code that is only for testing the extension.  It is not necessary to distribute this folder with extension unless you want others to be able to change it, but some packages include it.

      它应该包含仅用于测试扩展的所有代码。 除非您希望其他人可以更改带有扩展名的文件夹,否则不必分发此文件夹,但是某些软件包中包括它。

    2. If you have both Edit & Play Mode tests, you need two different assemblies.

      如果同时具有“编辑和播放模式”测试,则需要两个不同的程序集。

      1. Extension.Tests.Editor (Edit Mode)

        Extension.Tests.Editor (编辑模式)

      2. Extension.Tests.Runtime (Play Mode)

        Extension.Tests.Runtime (播放模式)

      If you have both Edit & Play Mode tests, you need two different assemblies.

      如果同时具有“编辑和播放模式”测试,则需要两个不同的程序集。

    Tests

    测验

The runtime assembly’s name is the name of the package. Each of the other assembly definitions should be named like PackageName.{suffix}.

运行时程序集的名称是程序包的名称。 每个其他程序集定义都应像 PackageName。{suffix} 这样命名

In Unity MARS, this is MARS, MARS.Editor, and MARS.Tests. The top-level namespace of your extension’s code should match the prefix of your assembly definitions.

在Unity MARS中,这是 MARS,MARS.Editor和MARS.Tests。 扩展代码的*名称空间应与程序集定义的前缀匹配。

在运行时使用编辑器汇编代码 (Using Editor assembly code in runtime)

It’s sometimes necessary to reference Editor code in your runtime assembly.  For example, a MonoBehaviour may exist only for the purpose of edit-time functionality, but it must live in a runtime assembly due to the rule against MonoBehaviours in Editor assemblies. In this case, it is often useful to define some static delegate fields inside of an #if UNTY_EDITOR directive. Your Editor class can then assign its own methods to those delegates, providing access to itself in the runtime assembly.   You can find an example of this pattern in our SuperScience repo. There is an EditorWindow, a class in the runtime assembly with Editor delegates, and a MonoBehaviour that uses these delegates.

有时有必要在运行时程序集中引用编辑器代码。 例如,MonoBehaviour可能仅出于编辑时功能的目的而存在,但是由于在Editor程序集中存在反对MonoBehaviours的规则,因此它必须存在于运行时程序集中。 在这种情况下,在#if UNTY_EDITOR指令中定义一些静态委托字段通常很有用。 然后,您的Editor类可以将自己的方法分配给那些委托,从而在运行时程序集中提供对其自身的访问。 您可以在我们的SuperScience存储库中找到这种模式的示例。 有 一个EditorWindow , 运行时程序 集中的 一个类 ,其中包含Editor委托,还有 一个 使用这些委托 的MonoBehaviour

生命周期事件和可扩展性 (Lifecycle Events & Extensibility)

One of the key patterns we follow in order to allow integrating other aspects of a user’s project with an extension is to provide events to hook into for important state changes in each smaller system.  We often refer to these as lifecycle events when they are provided for the important changes in a system or object. It’s important to remember that it’s not possible to account for everything a user might want to do to integrate with your tool, so ideally your event signatures should not be too restrictive — usually returning void. For instance, when we open or close a scene for simulation in Unity MARS, we have an event to allow for any custom setup or teardown a user’s project requires.  You would use this to add your own features to the Unity MARS simulation. This setup means that we don’t have to account for everything a user might do for their specific use case — something which is impossible for us to do — but the user is still allowed a high degree of flexibility to implement whatever they want. In this case, we have the simplest version of a lifecycle: just creation and destruction events, with no arguments, passed to the event functions.

为了使用户项目的其他方面与扩展集成在一起,我们遵循的关键模式之一是提供 事件, 以吸引每个较小系统中重要状态的改变。 当为系统或对象的重要更改提供它们时, 我们通常将它们称为 生命周期事件 。 重要的是要记住,不可能考虑到用户可能想要与工具集成的所有内容,因此理想情况下,事件签名不应过于严格 - 通常返回 void 。 例如,当我们打开或关闭一个场景以在Unity MARS中进行仿真时,我们将有一个事件允许用户项目需要的任何自定义设置或拆卸。 您将使用此功能将自己的功能添加到Unity MARS仿真中。 这种设置意味着我们不必考虑用户针对其特定用例可能做的所有事情 - 对于我们来说这是不可能的 - 但是仍然允许用户高度灵活地实现他们想要的任何东西。 在这种情况下,我们有生命周期的最简单版本:只是创建和销毁事件(不带参数)传递给事件函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SimulationSceneModule
{
    public static event Action simulationSceneCreated;
    public static event Action simulationSceneDestroyed;
}
class SimulationLifecycleEventUser : MonoBehaviour
{
   public void OnEnable()
   {
       SimulationSceneModule.simulationSceneCreated += Setup;
       SimulationSceneModule.simulationSceneDestroyed += TearDown;
   }
   public void OnDisable()
   {
       SimulationSceneModule.simulationSceneCreated -= Setup;
       SimulationSceneModule.simulationSceneDestroyed -= TearDown;
   }
   void Setup() { /* project-specific setup work here */ }
   void TearDown() { /* project-specific teardown work here */ }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class SimulationSceneModule
{
     public static event Action simulationSceneCreated ;
     public static event Action simulationSceneDestroyed ;
}
class SimulationLifecycleEventUser : MonoBehaviour
{
   public void OnEnable ( )
   {
       SimulationSceneModule . simulationSceneCreated += Setup ;
       SimulationSceneModule . simulationSceneDestroyed += TearDown ;
   }
   public void OnDisable ( )
   {
       SimulationSceneModule . simulationSceneCreated -= Setup ;
       SimulationSceneModule . simulationSceneDestroyed -= TearDown ;
   }
   void Setup ( ) { /* project-specific setup work here */ }
   void TearDown ( ) { /* project-specific teardown work here */ }
}

It’s important to unsubscribe from the event once you don’t want to respond to it anymore.  If you don’t, you can get events firing after the object that wanted to use them has been destroyed. Lifecycle events often need to pass some state change data to the end user. If you wanted to communicate that a set of Components has been destroyed or changed, you would have an event like this.

重要的是,一旦您不想再对此活动进行退订。 如果不这样做,则可以破坏要使用的对象后触发事件。 生命周期事件通常需要将一些状态更改数据传递给最终用户。 如果您想传达一组组件已被破坏或更改的信息,则将发生这样的事件。

1
2
3
4
class ComponentEvents
{
    public event Action<List<Component>> componentsChanged;
}
1
2
3
4
class ComponentEvents
{
     public event Action < List < Component >> componentsChanged ;
}

The above examples use an Action, but if you want to be able to hook up events in the inspector, you can replace Action with UnityEvent.

上面的示例使用 Action ,但是如果您希望能够在检查器中挂接事件,则可以将Action替换为 UnityEvent

结语 (Wrapping up)

We love seeing all the cool and unique ways that the community adds to the Editor. It’s inspiring to see developers enabling success for developers. Our mission as the Authoring Tools Group is to better empower creators to shape the future of 3D, and we are happy to share what we learn along the way. If you have questions or feedback for us, feel free to reach out to [email protected]!

我们喜欢看到社区为编辑器添加的所有酷炫独特的方式。 看到开发人员为开发人员带来成功令人鼓舞。 我们作为创作工具小组的使命是更好地授权创作者塑造3D的未来,我们很乐意分享我们在此过程中学到的知识。 如果您对我们有任何疑问或反馈,请随时联系 [email protected]

翻译自: https://blogs.unity3d.com/2019/04/03/tips-for-extending-the-unity-editor-lessons-from-project-mars/

unity扩展编辑器