当前位置: 首页 > 工具软件 > Urho3D > 使用案例 >

Urho3D场景

马琛
2023-12-01

Urho3D的场景模型可以描述为基于组件的场景图。“场景”由场景节点的层次结构组成,从根节点开始,根节点也代表整个场景。每个节点都有一个3D变换(位置、旋转和缩放)、一个名称和一个ID+可选标记,以及用户变量的自由形式VariantMap,但没有其他功能。

组件

通过调用CreateComponent()在节点中创建不同的组件,可以实现渲染3D对象、声音播放、物理和脚本逻辑更新。与事件一样,在C++中,组件由类型名称散列标识,组件创建和检索函数的模板形式是为了方便而存在的。例如:

Light* light = node->CreateComponent<Light>();

在脚本中,字符串用于标识组件类型,因此相同的代码如下所示:

Light@ light = node.CreateComponent("Light");

因为组件是使用对象工厂创建的,所以必须为每个组件类型注册工厂。

创建到场景中的组件本身具有特殊作用:实现场景范围的功能。它们应先于所有其他组件创建,并包括以下内容:

  • 八叉树Octree:实现空间分区和加速可见性查询。没有此选项,无法渲染三维对象。
  • PhysicsWorld:实现物理模拟。没有这一点,诸如RigidBody或CollisionShape之类的物理组件就无法正常工作。
  • DebugRenderer:实现调试几何体渲染。

“普通”组件(如灯光、摄影机或静态模型)不应直接创建到场景中,而应创建到子节点中

标识和查询

可以使用函数GetChild()从场景(或任何父节点)中按名称查询节点。查询可以选择性地递归,这意味着它遍历到子层次结构中。这是相对缓慢的,因为涉及字符串比较。

与节点不同,组件没有名称;同一节点内的组件仅通过其类型和节点组件列表中的索引来标识,该列表按创建顺序填写。有关详细信息,请参阅GetComponent()或GetComponents()的各种重载。

创建时,节点和组件都会获得场景全局整数ID。可以使用函数GetNode()和GetComponent()从场景中查询它们。这比 例如递归基于名称的场景节点查询快得多。

可以选择将字符串标记分配到场景节点中,以帮助识别。例如,请参见函数AddTag()、RemoveTag()和SetTags()。通过调用GetNodesWithTag()函数,可以从场景中查询具有特定标记的节点。

场景层次

没有实体或游戏对象的内置概念;而是由程序员决定节点层次结构,以及在哪些节点中放置任何逻辑。通常,三维世界中的自由移动对象将被创建为根节点的子对象。可以使用或不使用名称创建节点,请参见CreateChild()。未强制执行节点名称的唯一性。

每当有一些层次结构组合时,建议(实际上是必要的,因为组件没有自己的3D变换)创建子节点。例如,如果角色手中拿着一个对象,则该对象应该有自己的节点,该节点将成为角色手部骨骼的父节点(也是节点)。物理CollisionShape例外,它可以相对于节点单独偏移和旋转。有关详细信息,请参阅物理学。请注意,在计算子节点的世界派生变换时,场景自身的变换被有意忽略,作为一种优化,因此更改它不会产生任何影响,应该保持原样(位于原点,不旋转,不缩放).

场景节点可以自由重新设置。相反,组件始终创建到它们所属的节点,并且不能在节点之间移动。子节点和组件都使用SharedPtr容器存储;这意味着,如果不存在对子节点的其他引用,则从父节点分离子节点或删除组件也会破坏它。节点和组件都提供了Remove()函数来实现这一点,而无需通过父节点。请注意,在调用该函数后,在所讨论的节点或组件上的任何操作都不安全。

创建不属于场景的节点也是合法的。例如,当摄影机在可能加载或保存的场景中移动时,这很有用,因为这样,摄影机将不会与实际场景一起保存,并且在加载场景时不会被破坏。

但是,根据使用的组件,将组件创建到场景外的节点,然后稍后将节点移动到场景,可能无法完全按预期工作。例如,如果RigidBody组件无法访问场景的物理世界组件来实际创建子弹刚体对象,则该组件无法存储其速度。

场景更新

启用更新的场景(默认)将在每次主循环迭代时自动更新。请参阅SetUpdateEnabled()。

通过禁用节点和组件,可以将其从场景更新中排除,请参见SetEnabled()。例如,禁用可绘制组件也会使其不可见,声源组件变得听不见。如果节点被禁用,则无论其自身的启用/禁用状态如何,其所有组件都被视为禁用

创建逻辑功能

要实现游戏逻辑,通常需要创建脚本对象(使用脚本时)或新组件(使用C++时)。脚本对象存在于C++占位符组件中,但基本上可以认为是组件本身。对于一个简单的示例,请检查05_AnimatingScene示例,该示例将创建一个到场景节点的Rotator对象,以在每个帧更新时执行旋转。

除非你有非常严肃的理由这样做,否则你不应该在C++中为实现你自己的逻辑子类Node类。这样做理论上可行,但有以下缺点:

  • 如果没有更改,加载和保存将无法正常工作。它假设根节点是场景,所有子节点都属于节点类。它将不知道如何实例化自定义子类。
  • 编辑器不知道如何编辑子类。

加载和保存场景

场景可以以二进制、JSON或XML格式加载和保存;请参见函数Load()、LoadXML()、Load JSON、Save()和SaveXML()以及SaveJSON()。有关如何工作的技术详细信息,请参阅序列化。加载场景后,首先删除其中的所有现有内容(子节点和组件)。

标记为临时的节点和组件将不会被保存。请参见SetTemporary()。

为了能够跟踪加载(大型)场景的进度,而不会在加载过程中程序停止,还可以异步加载场景。这意味着在每一帧上,场景都会加载资源和子节点,直到超过一定的毫秒数。请参见LoadAsync()和LoadAsyncXML()。使用函数IsAsyncLoading()和GetAsyncProgress()跟踪加载进度;后者返回一个介于0和1之间的浮点值,其中1是完全加载的。场景在完全加载之前不会更新或渲染。

对象预制件

对于需要动态创建新对象的游戏来说,仅加载或保存整个场景不够灵活。另一方面,创建复杂对象并在代码中设置它们的财产也很繁琐。因此,还可以将场景节点(及其子节点、组件和属性)保存为二进制、JSON或XML,以便稍后将其实例化为场景。这种保存的对象通常被称为预制件。有三种方法可以做到这一点:

  • 在代码中,对所讨论的节点调用Save()、SaveJSON()或SaveXML()。
  • 在编辑器中,选择层次结构窗口中的节点,然后从“文件”菜单中选择“将节点另存为”。
  • 使用AssetImporter中的“node”命令,该命令将保存场景节点层次和输入资源中包含的任何模型(例如Collada文件)

要将保存的节点实例化到场景中,请根据格式调用instantiate()、scene::InstantiateJSON()或InstantiateXML()。节点将被创建为场景的子节点,但之后可以自由地重新编程。需要指定放置节点的位置和旋转。NinjaSnowWar示例使用XML格式作为其对象预置;这些文件位于bin/Data/Objects目录中。

场景图形事件

场景对象在场景图形修改时发送事件,例如添加或删除节点或组件、更改节点或组件的启用状态或更改名称或标记。这些在编辑器中用于实现使场景层次窗口保持最新。请参见包含文件SceneEvents.h。请注意,当节点从场景中移除时,不会发出单个组件移除的信号。

进一步资料

有关基于组件的场景模型的详细信息,请参见例如http://cowboyprogramming.com/2007/01/05/evolve-your-heirachy/.请注意,Urho3D场景模型并不是一个纯粹的实体组件系统设计,它将组件作为裸数据容器,只有系统对其进行操作。相反,Urho3D组件包含自己的逻辑,并与它们所依赖的系统(如渲染、物理或脚本引擎)进行主动通信。

 类似资料: