软件开发指南
本指南说明如何扩展 Unity MARS 或开发特定于应用程序的自定义行为。内容涵盖不同 MARS 子系统的运行方式,并提供一些示例来帮助您进行扩展,从而更好地满足您的需求。
包内容
Unity MARS 是包含以下文件夹的包:
位置 | 描述 |
---|---|
Editor |
可添加或修改 2D Editor UI 的 Unity Editor 脚本。 |
Interfaces |
编写 Unity MARS 扩展所需的接口脚本。 |
Runtime |
核心 Unity MARS 运行时系统。 |
Tests |
单元测试。 |
Videos |
用于测试的示例视频文件。 |
Unity MARS 具有以下包依赖项:
com.unity.xrtools.module-loader
com.unity.xrtools.utils
com.unity.prefab-handles
com.unity.textmeshpro
com.unity.timeline
com.unity.legacyinputhelpers
com.unity.postprocessing
软件开发者主题
本节涵盖一些高级主题。目标受众是在特殊情况下需要直接访问 Unity MARS 的某些部分(例如默认提供程序)的开发者。
生成所有 .csproj 文件
编写引用 Unity.MARS
程序集的代码时,需要在 Preferences > External Tools 下启用 Generate all .csproj files 选项。Unity MARS 中包含的生成代码将使用 Assets/MARS/Generated
中的程序集定义引用来编译为 Unity.MARS
程序集。默认情况下,Unity 不会为包程序集生成 .csproj
文件,但是由于资源中存在此 .asmref
,因此会为 Unity.MARS
生成一个这样的文件。可是,该项目不会引用 MARS 包中的任何代码,所以该程序集内的大多数类型似乎都会缺失。通过 Generate all .csproj files 选项,Unity 将为 Unity.MARS
主程序集生成一个项目,并且您的 IDE 在处理 MARS 代码时会按预期运行。
请注意,此设置会影响当前用户的所有 Unity 项目。
Unity 建议用户在使用 Unity MARS 时将 IDE 更新到最新版本的集成包(例如 com.unity.ide.rider
)。在更高版本中,用于生成其他项目文件的选项会按包类型细分。在这种情况下,确保已启用 Registry packages
。
MARS 会话
MARS 会话组件在 MARS 场景中有多种用途。Unity MARS 主要使用 MARS 会话组件本身来存储场景元数据(例如所需特征的列表),并根据场景中实体所需的特征来自动填充此组件。在您进行更改时,MARS Editor 扩展会分析您的场景,并在 MARS 会话的 Requirements 列表中添加或移除特征。您无法编辑此列表。
MARS Editor 扩展可确保只要在激活的场景中存在 MARS 实体,MARS 会话就会存在并且是主摄像机的父级。将第一个 MARS 实体添加到场景时,也会自动添加一个 MARS 会话。
如果您的场景中已经有摄像机,这个摄像机将成为 MARS 会话 (MARS Session) 游戏对象的子项,并会向摄像机添加 MARSCamera 脚本。如果场景中没有摄像机,MARS 会为您创建一个摄像机。如果您尝试删除 MARS 会话或摄像机,MARS 会自动重新创建它。自动生成的 MARS 会话还添加一个用户游戏对象,这个游戏对象将合成摄像机位置的姿态;这会使场景中的查询与用户关联。如果您不需要用户游戏对象,您可以放心地将其删除。
为了防止此行为,请从 Unity 的主菜单中选择 Project Settings > MARS > Scene Module,然后启用 Block Ensure Session 选项。MARS 将此设置保存到项目。此设置还会影响运行时的“确保会话”行为。此选项通过公共属性 MARSSceneModule.BlockEnsureSession
向脚本公开。可以临时启用这个选项以修改会话层级视图或在运行时创建代理,而无需自动启动 MARS。
为了支持 MARS 世界比例功能,MARS 将摄像机游戏对象变为 MARS 会话 (MARS Session) 游戏对象的子项。MARS 中的系统始终认定摄像机具有父级,这将在您需要获取/设置世界比例或摄像机偏移量的情况下简化代码复杂性。
可以在 MARS 会话组件的 Inspector 中设置每个场景功能岛台覆盖。这表明场景具有来自全局默认功能岛台的备选提供程序集。该字段是可选字段,在大多数情况下,应将其留空。
功能注入
功能注入是一种集成模式,可提供对各种数据提供程序包的一般访问权。一对 C# 接口定义给定功能的 API。一个接口的名称以 IProvides
开头,并定义数据提供程序类必须实现的方法或事件,以便 Unity MARS 使用这些方法或事件。另一个接口(名称始终以 IUses
开头)由需要访问提供程序方法的类实现。目标是尽可能简化向用户脚本添加额外功能的过程。您可以添加/删除订阅者接口,而无需更改类中的其他任何内容。
有关更多信息,请参阅 Google Docs 中的功能注入说明 (Functionality Injection Explained)。
功能岛台
功能注入模块(FI 模块)可管理功能岛台。为确保可以将提供功能的所有模块添加到所有岛台,必须在模块完成加载之前将所有岛台添加到 FI 模块。Unity MARS 假定模块始终为可访问状态。如果您使用的岛台尚未经过设置,或者您在工作流程中过晚尝试设置岛台,MARS 会生成警告。这样可确保您以后不会收到任何令人困惑的错误。在大多数情况下,您无需编写操纵岛台的代码;而是在场景中的 MARS 会话游戏对象上设置岛台引用,或使用激活的岛台来将功能注入游戏对象。
根据 MARSSession 中设置的覆盖,场景启动时会在 MARSSceneModule 中设置激活的岛台。如果未设置任何覆盖,则激活的岛台始终是默认岛台。这些岛台保留对默认提供程序列表的序列化引用。这些序列化设置定义了您设置场景时特定于配置的默认值。FunctionalityIsland.SetupDefaultProviders()
方法负责执行此步骤。场景启动时,MARSSceneModule 调用此方法。
功能岛台中包含的默认值会影响提供程序选择。通过创建和修改功能岛台资源可以控制在哪些场景中和平台上使用哪些提供程序。如果您的项目中给定的提供程序类型只有一个提供程序,则无需创建默认值(除非该提供程序需要预制件)。
要设置默认值,请导航到功能岛台的 Inspector 窗口,然后按照以下步骤操作:
- 在 Default Providers 列表中添加一个新行。
- 选择需要默认值的提供程序接口。
- 选择默认提供程序类型,或者为该提供程序设置预制件引用。
添加预制件时,如果需要,将使用该预制件中的所有提供程序组件。您无需为想要加载的每种提供程序类型指定预制件。如果确实需要两次指定预制件(在某些重叠情况下可能会发生),仅会实例化一次,并且 MARS 处理包含该预制件的下一行时,将复用同一实例。
Module Loader
Module Loader 包 (com.unity.xrtools.module-loader
) 使多个系统可以在同一个项目中共存并相互交互。依赖这个包的包将实现一些类来扩展 IModule
或某种变体,这使得这些包可以在 Editor 中以及在运行时按可配置的顺序加载和卸载,并以可预测的系统性方式进行连接。这个包中包括功能注入模块,该模块使包可以向彼此公开功能或用户代码,而无需相互直接引用。
可通过两种方式访问 Module Loader:
- 从 Unity 主菜单中,选择 Edit > Project Settings,然后选择 Module Loader 选项卡。
- 选择您项目中的 ModuleLoaderCore 资源。
在此处可以打开或关闭 Unity MARS 的某些部分。某些开关由于其他启用的模块依赖于它们,因此已被禁用。由于循环依赖性,您无法一次性关闭所有 MARS。禁用某个模块时,也可以禁用依赖于该模块的模块。请参阅依赖关系模型和相关模型以获取有关哪些模块相互依赖的更多信息。默认情况下,示例模块被禁用。如果启用示例模块,可能会导致问题。无法禁用功能注入模块。
Enabled Modules UI 下方是一系列只读列表,其中显示了各个模块的顺序。Load 和 Unload 应包含所有模块,但回调顺序仅适用于实现那些特定接口的模块。
有关更多信息,请参阅 Module Loader 包文档。
重新加载模块
如果您需要重置 Unity MARS 系统的状态,无需重新启动 Unity Editor。可以使用重新加载模块 (Reload Modules) 功能仅重新启动 MARS Editor 扩展。为此,请找到 Module Loader,然后单击 Reload Modules。
MARS 数据库
Unity MARS 构建于一个灵活的数据库层之上。该层是在运行时构建的,包含 MARS 支持的原生内置类型以及由提供程序或用户脚本添加的任何新增自定义类型。
在数据库中,关于世界的观察结果被分解为数据。此数据由任何类型的单个变量组成,并通过特征(即数据的字符串标识符)对此数据进行描述。一些数据和特征是被动的(例如,语义标签,比如将平面标记为“墙”或“地板”)。其他数据和特征是主动的(例如,面部姿态是主动的特征,其值会不断变化)。
以下三个类可以访问 MARS 数据库:
MARSBackend(读取)
这些查询用于查找匹配的数据段(对于代理组,则查找数据集),然后将该数据返回给提出相关要求的类或函数。
人工合成对象(写入)
在包含人工合成对象和人工合成特征的游戏对象上放置的组件。这些游戏对象激活后,它们会将数据写回到 MARS 数据库。这是用于使模拟环境就像现实环境一样与 Unity MARS 进行交互的机制。
人工合成数据让许多不同类型的条件汇集到一个适合设计并作为创作依据的语义数据段中。复制的代理或代理组中的人工合成数据本质上是以可视化方式创作的推断 API。也可以将人工合成数据用于在用户创作的内容中标记关键位置或数据,例如生成点的良好位置、数字环境中的界标点或边,或者用于连接桥的空间。然后,可以在更多的代理或代理组中使用/保留此数据,从而允许大量不同的内容或环境类型通过简单平坦的层级视图来进行交互。
推断 API(读取和写入)
推断 API 是一组高级脚本和系统,用于检查数据库状态以新建语义接口或创建其他数据。有关更多信息,请参阅本页面的推断 API 部分。
查询
这是 MARS 数据库中的数据寻址方法。查询并非是直接进行的,而是要构造可执行此操作的对象。查询包含必须匹配的条件以及找到匹配数据时执行的操作。
除条件外,查询还有两个额外的配置选项:通用查询数据和排他性。
通用查询数据
以下参数关系到 MARS 后端如何管理查询。常规查询和代理组查询会使用这些参数。
参数 | 定义 |
---|---|
Time Out | 指定查询在停止尝试之前必须查找匹配项的时间。 |
Update Match Interval | 指定对查询匹配项更新的检查频率。 |
Reacquire on Loss | 指定在某个条件不再匹配时查询是否尝试查找另一个匹配项。 |
排他性
这个选项控制其他查询是否可以读取已经匹配的数据。排他性也可以为只读,这种情况下允许与所有数据匹配,但不应干扰应用程序的视觉流。例如,如果您有两个看起来像桌子的代理,您的应用程序会检测到第一个代理并在其上放置内容,然后忽略第二个代理。
代理组查询
术语“查询”通常指的是针对一个代理(例如一张桌子或一个地板平面)查询一组数据。代理组查询会引用多个常规查询,以及每个代理之间必须匹配的一系列关系。
必需子代理
您需要将 ProxyGroup 中的每个子代理标记为必需或非必需。所有子代理(无论是否必需)都必须匹配,才能让代理组初步匹配。如果某个必需子代理的条件或涉及该子代理的关系不再匹配,则整个代理组都会失去匹配。但是,如果非必需子代理已丢失,则代理组将继续使用其他子代理的匹配数据。
编写条件
代理上的条件组件将定义特征(以及可选的值范围),通过测试该特征可确定是否与 MARS 数据库中的数据匹配。Unity MARS 包含适合常见用例的若干此类脚本,并提供 API 来供您编写自己的脚本。
所有条件都作用于通过特征名称在数据库中筛选的单一数据类型。
要编写条件,请按以下步骤操作:
使用以下模板来创建新脚本:
public class _ConditionName_Condition : Condition<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _TraitDefinition_ }; public override TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public override float RateDataMatch(ref _dataType_ data) }
填写
k_RequiredTraits
字段和RateDataMatch()
方法。k_RequiredTraits
数组必须恰好包含一个TraitRequirement
(用于定义此条件所作用于的特征)。RateDataMatch()
方法采用您指定的 dataType 的单个元素,并返回 0-1 范围内的数字以指示其与条件的匹配程度(其中 0 为不匹配,而 1 为完美匹配)。该方法一次对应于 MARS 数据库中测试的一个条目。
条件一次仅作用于一个特征和设置。可以针对单个对象设置多个条件。这是建议的工作流程。如果您的条件需要一次性了解更多数据,请使用 MultiCondition。
编写 MultiCondition
代理上的 MultiCondition 组件将定义多个特征,通过测试这些特征可确定是否与 MARS 数据库中的数据匹配。Unity MARS 包含适合常见用例的若干此类脚本,并提供 API 来供您编写自己的脚本。
要编写 MultiCondition,请按以下步骤操作:
使用以下模板来创建新脚本:
public class _MultiConditionName_Condition : MultiCondition<_conditionType1_,_conditionType2_> { [System.Serializable] public class _conditionType1_ : SubCondition, ICondition<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _TraitDefinition_ }; public string traitName { get { return k_RequiredTraits[0].TraitName; } } public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public float RateDataMatch(ref _dataType_ data) { } } [System.Serializable] public class _conditionType2_ : SubCondition, ICondition<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _TraitDefinition_ }; public string traitName { get { return k_RequiredTraits[0].TraitName; } } public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public float RateDataMatch(ref _dataType_ data) { } } }
针对每个 SubCondition,填写
k_RequiredTraits
字段和RateDataMatch
方法。如果您需要将 OnValidate 方法用于这两个 SubCondition,应该在外部类中为两者实现该方法。
编写关系
ProxyGroup 上的关系组件将定义特征(以及可选的值范围),通过测试该特征可确定是否与 MARS 数据库中的数据匹配。Unity MARS 包含适合常见用例的若干此类脚本,并提供 API 来供您编写自己的脚本。
要编写关系,请按以下步骤操作:
使用以下模板来创建新脚本:
public class _RelationName_Relation : Relation<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _child1TraitDefinition_, _child2TraitDefinition_ }; public override TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public override float RateDataMatch(ref _dataType_ child1Data, ref _dataType_ child2Data) { } }
填写
k_RequiredTraits
字段和RateDataMatch()
方法。k_RequiredTraits
数组必须恰好包含两个TraitRequirement
。第一个条目应该用于定义第一个子项的特征,第二个条目应该用于第二个子项的特征。RateDataMatch
方法采用您指定的 dataType 的两个元素。该方法对应于 MARS 数据库中同时测试的一对上下文。通常,关系一次仅检查一个属性和设置。
编写 MultiRelation
ProxyGroup 上的 MultiRelation 组件将定义组中两个代理之间的多个关系,通过测试每个关系可确定是否有匹配项。Unity MARS 包含其中一个脚本作为示例,并提供 API 来供您编写自己的脚本。
要编写 MultiRelation,请按以下步骤操作:
使用以下模板来创建新脚本:
public class _MultiRelationName_ : MultiRelation<_relationType1_,_relationType2_> { [System.Serializable] public class _relationType1_ : SubRelation, IRelation<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _child1TraitDefinition_, _child2TraitDefinition_ }; public string child1TraitName { get { return k_RequiredTraits[0].TraitName; } } public string child2TraitName { get { return k_RequiredTraits[1].TraitName; } } public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public float RateDataMatch(ref _dataType_ child1Data, ref _dataType_ child2Data) { } } [System.Serializable] public class _relationType2_ : SubRelation, IRelation<_dataType_> { static readonly TraitRequirement[] k_RequiredTraits = { _child1TraitDefinition_, _child2TraitDefinition_ }; public string child1TraitName { get { return k_RequiredTraits[0].TraitName; } } public string child2TraitName { get { return k_RequiredTraits[1].TraitName; } } public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } public float RateDataMatch(ref _dataType_ child1Data, ref _dataType_ child2Data) { } } }
针对每个 SubRelation,填写
k_RequiredTraits
字段和RateDataMatch
方法。如果您需要将 OnValidate 方法用于这两个 SubRelation,应该在外部类中为两者实现该方法。
编写操作
操作将数据库中的更改转换为 Unity 场景中的更改(作为 AR 生命周期事件)。为了创建这些组件,您需要编写代码来实现用于操作或代理组操作的接口。您可以将操作组件应用于代理,或者将代理组操作组件应用于代理组。
适用的 AR 生命周期事件为:
- 获取:在首次找到并应用匹配的现实数据时触发。
- 更新:匹配的现实数据更改时,按指定的时间间隔触发。
- 超时:在给定时间范围内未找到一组特定条件的情况下触发。
- 丢失:在一组匹配的实际数据发生太大变化以致不再满足给定条件的情况下触发。
要编写操作,请按以下步骤操作:
使用以下模板来创建新脚本:
public class _ActionName_Action : MonoBehaviour, IMatchAcquireHandler, IMatchUpdateHandler, IMatchLossHandler, IMatchTimeoutHandler, IRequiresTraits { static readonly TraitRequirement[] k_RequiredTraits; public void OnMatchAcquire(QueryResult queryResult) {} public void OnMatchUpdate(QueryResult queryResult) {} public void OnMatchLoss(QueryResult queryResult) {} public void OnMatchTimeout(QueryArgs queryArgs) {} public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; } }
对于您要响应的每个 AR 生命周期事件(获取、更新、超时和丢失),添加相应接口并填写相应函数。
QueryResult 至少包含对象条件所需的数据。还可以包含其他数据,并且您可以检查此类数据是否存在。可以选择某个操作是否必须在匹配选项上发生,或者该操作是否为可选。
k_RequiredTraits
应该包含该操作需要处理的所有特征。请参阅SetAlignedPoseAction
类以了解示例。如需进一步了解特征以及如何获取特征,请参阅特征的文档。
在 MARSEntity Inspector 中填充您的类
您编写的新操作和条件显示在 MARS Entity Inspector UI 中的 Add MARS Component > Action/Condition > Other 之下,然后自动采用堆叠式 Inspector 样式。您可以为它们提供自定义菜单路径。为此,请向您的类添加 MonoBehaviorComponentMenu
属性,如下所示:
[MonoBehaviourComponentMenu(typeof(_YourAction_), "Action/_YourActionPath_")]
人工合成数据
引用 MARS 代理的方式是构建一组必需数据及条件。人工合成数据系统可以反映这一点。该系统使用合成组件来修饰游戏对象和传统内容,而人工合成组件可以收集这些属性并将它们添加到 MARS 数据库中。人工合成数据甚至可以充当许多类似推断 API 活动的可视化接口。大多数修饰世界的高级工作流程都依赖此系统。
要使用人工合成数据,请按以下步骤操作:
创建或找到您要反映到 MARS 数据库中的游戏对象。如果您使用更多语义信息来修饰现有 AR 数据,该游戏对象可以是代理游戏对象的子游戏对象,或者如果您使用纯模拟数据,则该游戏对象可以是根级别的游戏对象。
添加“SynthesizedObject”组件。
添加任意数量的 Synthesized Traits 或 Synthesized Data 组件。其中的每个组件将游戏对象的不同方面反映到 MARS 数据库中。
在运行时或模拟时,如果启用或禁用了人工合成对象,MARS 会在根级别添加或删除这些对象。如果获取或丢失了父级,则会在数据库中添加或删除作为 MARSEntity 子项的人工合成对象。
- SynthesizedObject 的变换组件更改时,某些特征(例如基于位置的特征)会更新其值。更新 AR 生命周期方法还会触发 SynthesizedObject 值的更新。
编写 SynthesizedTrait
要编写 SynthesizedTrait,请按以下步骤操作:
使用以下模板来创建脚本:
public class Synthesized_TraitName_ : SynthesizedTrait<_DataType_> { public override string TraitName { get; } public override bool UpdateWithTransform { get; } public override _DataType _GetTraitData() }
填写上文的方法。
就像对其他任何 SynthesizedTrait 一样,将其添加到所需的 SynthesizedObject。
推断 API
Unity MARS 基于包含大量修饰元素的 AR 场景的概念;例如,包含以下元素的场景:标记为“地板”的平面、标记为家具的边界体积、附加的光源值以及许多其他标记的元素。目前,大多数提供程序一次只能提供一种类型的数据(例如平面或面部),而没有这种丰富数据。
推断 API 填补了此空缺,并充分利用 AR 硬件/软件堆栈。为此,它们在 MARS 数据库中创建或变异数据。它们可以一次查看数据库中所有不同的数据段,并将不同数据集关联在一起,或者根据现有数据做出额外推断。
该包附带一个推断 API 示例,其中标记了地板平面。这为基于手机的 AR 释放了强大的功能。
推断 API 的选择标准
推断 API 是根据以下条件来选择和运行的:
它需要运行的数据是否存在?例如,地板推断 API 需要数据库中已经存在平面,因此该 API 可以检查这些平面并推断哪个平面可能与地板平面相关。
该应用程序是否需要由推断 API 提供的数据?例如,如果在 Unity MARS 中设计的应用程序未使用需要地板特征的任何条件,则地板推断 API 将不会运行(即使可以运行),因为它没有理由运行。
此推断 API 提供的数据是否来自其他来源?例如,用户可能在提供语义标记平面的 AR 硬件上运行 MARS 应用程序。这些平面已经使用“地板”标签添加到数据库中。在此情况下,地板推断 API 不会运行,因为它无法添加任何新内容。
编写推断 API
推断 API 通过以下模板以脚本形式启动:
[CreateAssetMenu(menuName = “Mars/_Name_ ReasoningAPI”)]
public class _Name_ReasoningAPI : ScriptableObject, IReasoningAPI
{
static readonly TraitDefinition[] k_ProvidedTraits;
static readonly TraitRequirement[] k_RequiredTraits;
public float processSceneInterval { get; }
public TraitDefinition[] GetProvidedTraits() { return k_ProvidedTraits; }
public TraitRequirement[] GetRequiredTraits() { return k_RequiredTraits; }
public void Setup()
public void TearDown()
public void ProcessScene()
public void UpdateData()
}
此模板包含以下部分:
k_ProvidedTraits
是该推断 API 添加到激活 MARS 场景的特征数组。k_RequiredTraits
是必须存在于数据库中才能让推断 API 运行的特征数组。Setup()
执行一次性设置以获取对数据库及其内容的引用。TearDown()
释放在Setup()
方法中设置的资源。ProcessScene()
是一种频繁(但不是每帧)调用的方法,用于资源密集型操作。UpdateData()
是一种每帧调用的方法,调用该方法的目的是确保由推断 API 创建或更改的数据与场景保持最新状态。
创建推断 API 后,就可以开始使用它。请遵循以下步骤:
将数据添加到推断 API。为此,请添加
IProvidesTraits<T>
、IRequiresTraits<T>
或IUsesMARSData<T>
接口。这些接口使您可以访问 MARS 数据库中的数据,还可以在这些数据库条目中添加、编辑和删除数据。IUsesMARSData
提供的GetCollection<T>
方法对于获取特定类型的数据库中的所有数据特别有用。在您的项目中创建推断 API 的实例。在 Project 视图中单击右键,然后选择 Create > MARS > Name Reasoning API。
在您的项目中找到 ReasoningModule 游戏对象,然后展开 Reasoning APIs 部分。
将您的推断 API 添加到此数组。
提供程序
如何选择提供程序
Unity MARS 应用程序应该实现多种集成。功能注入 (FI) 模块和功能岛台可应对许多不同情况,包括:
- 为特定类型的功能提供显而易见的单一选项。
- 在您必须为某些情况指定默认值时,提供多种选项。
- 明确指定在哪些情况下使用哪些提供程序以及用于哪些游戏对象。
场景模块和功能注入模块会自动加载符合以下条件的提供程序:
- 匹配该场景中游戏对象上的订阅者接口。
- 提供该场景中条件所要求的特征。
任何实现提供程序接口的场景游戏对象都优先于激活功能岛台的默认设置中的提供程序。这样,您可以确保所需的提供程序存在并在场景中运行。脚本可以在运行时为激活岛台设置相同类型的其他提供程序,但是标准行为是在场景加载时设置提供程序,并在整个场景运行期间提供程序保持不变。
为对象列表调用 InjectFunctionality
时(例如,在运行时设置模拟或设置 MARS 时),系统会根据场景中的功能订阅者来选择并初始化提供程序。ProviderSelectionOptions
属性可让您使用额外信息来注释提供程序类型。第一个参数是 Priority
,您可以在提供程序选择过程中将此参数用于“推荐”或“降级”某个提供程序。如果有多种类型实现了某个提供程序接口,则 MARS 会选择 Priority
值最高的提供程序。如果多个提供程序类型具有相同的 Priority
值,则 MARS 会在列表中任意选择第一个类型,并记录一条警告。在这种情况下,行为不确定,并且可能会发生意外更改。
在根据特征要求来选择提供程序时(通常与订阅者要求同时执行),系统会首先收集所有实现了 IProvidesTraits
并提供至少一个必需特征的提供程序。这是通过调用 RequireProvidersWithDefaultProviders
来实现的。如果列表中只有一个提供程序,MARS 会选择它。否则,然后会按照提供程序包含的特征数量和场景需要的特征数量(“分数”),按照降序对提供程序列表进行排序。如果两个提供程序的分数相等,则 MARS 会检查其中一个是否为功能岛台中的默认提供程序,如果是,则优先选择该提供程序。如果两个提供程序都在默认列表中或都不在默认列表中,则 MARS 会比较它们的优先级并按降序对类型进行排序。如果列表中的前两个提供程序的“分数”和优先级不相等,则 MARS 选择第一个提供程序。否则,MARS 仍会选择第一个提供程序,但还会记录一条警告。在这种情况下,行为不确定,并且可能会发生意外更改。
您可以使用功能岛台来手动覆盖提供程序选择。将提供程序类或预制件添加到功能岛台并不能保证 MARS 会在运行时加载它。默认设置仅用于消除 SetupDefaultProviders 中提供程序选择方面的不确定性。根据订阅者要求而选择提供程序时,功能岛台默认值优先于 Priority
。如上所述,根据特征要求进行选择时,默认值高于 Priority
,但仅在提供程序具有相等“分数”时才考虑默认值。在给定提供程序类型存在多个提供程序的所有情况下,应确保在激活的岛台上设置了默认值。
通常,提供程序包应提供示例功能岛台资源来作为起点。在大多数情况下,应用程序从诸如 XRSDK 提供程序包或面部跟踪器之类的基础提供程序开始,然后根据需要向已包含的功能岛台添加额外的默认提供程序。
选择性扩展
选择性扩展 (Elective Extensions) 在为配置的构建目标(当前为 Android、iOS 和 Lumin)进行构建时运行,并根据已知良好的设置和包版本列表进行检查。
将项目构建到不同的构建目标时,可能会出现构建目标所需的包丢失或与另一个构建目标不兼容的情况。首次构建到新的构建目标时,您可能还会忽略 Player Settings 和其他位置的特定设置。为简化此过程,选择性扩展检测到不匹配情况时,将向控制台输出警告,其中指出潜在的问题以及如何解决该问题的建议。
可以调整配置文件以适合您的项目要求。这些文件位于 com.unity.mars\Editor\Scripts\Build\ElectiveExtensionsConfiguration<platform name>.cs
编写与模拟兼容的行为
Unity MARS 中的模拟将激活的场景中的所有游戏对象复制到模拟场景,并在复制的 MonoBehaviour
(用于实现 ISimulatable
接口)上设置 runInEditMode
属性。
如果要编写在模拟中运行的自定义行为,请注意以下几点:
- 该行为必须实现
ISimulatable
才能在模拟期间接收MonoBehaviour
回调。 - 如果该行为实例化游戏对象,它应使用
GameObjectUtils.Create
或GameObjectUtils.Instantiate
。这些实用程序分别是GameObject
构造函数和Object.Instantiate
的封装器。在模拟期间被调用时,实例化的游戏对象将添加到模拟场景中,并且其ISimulatable
行为会在编辑模式下运行。 - 如果该行为销毁游戏对象,它应使用
UnityObjectUtils.Destroy
。在运行模式下调用时,此实用程序调用Object.Destroy
,而在编辑模式下调用时,它调用Object.DestroyImmediate
。 - 在某些情况下,模拟的游戏对象在不同模拟之间持续存在(而不是被销毁和替换为新副本)。这意味着,如果自定义的
ISimulatable
行为更改了自身或另一个游戏对象的任何状态,则它必须在OnDisable
中重置该状态。另外,如果该行为需要执行在模拟启动时应进行的任何设置,则必须在OnEnable
中执行此操作。仅在某个行为第一次将runInEditMode
设置为true
时才调用Awake
。
MARS 时间
MarsTime.Time
是自 Unity MARS 生命周期开始(激活的 MARS 会话接收到 OnEnable
时)以来经过的时间。它按固定的时间间隔 (MarsTime.TimeStep
) 走时,并在每一次走时之后触发 MarsTime.MarsUpdate
回调。
MarsTime.TimeScale
会影响 MarsTime.Time
,但是不会影响 MarsTime.TimeStep
。这实际上意味着随着 MarsTime.TimeScale
的增加,每次播放器循环更新的 MarsUpdate
数量也会增加。
由于模拟能够以各种时间标度运行,因此核心 MARS 系统和模拟数据提供程序必须使用 MarsTime
属性和 MarsUpdate
回调来确保确定性行为。对于某些自定义的可模拟行为,重要的可能是无论时间标度如何,它们也必须以确定性方式执行。
此类自定义行为应使用 MARS 时间 API 并遵循以下准则:
- 使用
MarsTime.MarsUpdate
而不是MonoBehaviour.Update
- 使用
MarsTime.Time
而不是Time.time
- 使用
MarsTime.TimeStep
而不是Time.deltaTime
- 使用
MarsTime.FrameCount
而不是Time.frameCount
- 使用
MarsTime.TimeScale
而不是Time.timeScale
如果您希望某个行为使用 MonoBehaviour.Update
并仍与具有时间标度的模拟兼容,则必须按以下准则,将 MarsTime.TimeScale
纳入基于时间的计算:
- 使用
MarsTime.Time
而不是Time.time
- 使用
MarsTime.ScaledDeltaTime
而不是Time.deltaTime
查找摄像机
如果要在 ISimulatable
中获取摄像机,应使用 MarsRuntimeUtils.GetActiveCamera()
(除非需要序列化对场景摄像机的直接引用)。在特定情况下应使用以下方法:
GetActiveCamera()
在模拟时会从模拟中获取激活的摄像机,而在运行模式下时会获取 MARS 会话摄像机。MarsRuntimeUtils.GetSessionAssociatedCamera()
返回激活的 (MARSSession.Instance
) MARSSession 摄像机。
两种方法都可以选择回退到 Camera.main
;如果为 null,则查找任何摄像机。
MARS Editor 系统
数据可视化对象
数据可视化对象是在模拟中由隐藏的复制器创建的代理。这些对象由编辑器工具(例如创建工具和比较工具)使用。
可以通过启用 Project Settings > MARS > Visual Settings 中的 Show Data Visuals In Hierarchy 选项,在 Content Scene Hierarchy 面板中查看数据可视化对象。
在 Project Settings 中,还可以切换 Disable Simulation Data Visuals 以在模拟开始时停止生成数据可视化对象。请注意,禁用数据可视化对象后,依赖它的工具将无法运行。