WWDC Quick Look 💓 By SwiftGGTeam
Extend Reality Composer Pro 3 functionality with Xcode

Extend Reality Composer Pro 3 functionality with Xcode

观看原视频

Highlight

Reality Composer Pro 3 支持通过 Xcode 编写插件,将自定义的 Component、System、动画 Action 和 Script Graph 节点暴露给编辑器,让美术和设计师在编辑器内直接调整参数、预览效果,无需反复编译部署 App。

核心内容

团队协作的痛点

做空间计算项目时,开发者和美术通常各自为战。工程师在 Xcode 里写代码控制 3D 场景逻辑,美术在 Reality Composer Pro 里摆放模型、调材质。双方的工作流是割裂的:美术想调一个参数看看效果,必须等工程师改完代码、编译、打包、部署到设备上才能验证。这个循环动辄几分钟,一天下来大量时间耗在等待上。

更大的问题是,工程师写的自定义 Component 和 System 对编辑器来说是黑盒。美术看不到这些组件的属性,也无法在编辑器里直接修改。所有调整都要通过工程师中转,沟通成本高,迭代速度慢。

Reality Composer Pro 3 的插件机制

Apple 今年的解决方案是插件化。工程师在 Xcode 里写一个动态库(Dynamic Library),把项目特有的 Component、System、动画 Action 注册到 Reality Composer Pro 3。编辑器加载这个插件后,这些自定义能力就会出现在编辑器的 UI 中,美术可以直接使用。

02:07)整个工作流是这样的:Xcode 项目里有两个 Scheme,一个构建最终的游戏 App,另一个构建 RCPCustomComponents.framework 插件。Reality Composer Pro 3 项目与 Xcode 项目通过 simulation bar 关联,可以直接从编辑器启动 App。两个项目放在同一个 Git 仓库里,团队成员各自修改后提交合并。编辑器还自带了自定义的合并工具,处理 JSON 格式的内部数据时冲突更少。

03:34)数据流向也很清晰:Xcode 构建 App 和插件,Reality Composer Pro 3 用插件加载自定义组件和系统,美术在编辑器里调整场景,最后导出为 Reality File 被 App 加载运行。

从 Script Graph 切换到代码的时机

05:20)Reality Composer Pro 3 内置了 Script Graph 可视化脚本工具,不需要写代码就能实现很多逻辑。但 Script Graph 也有局限:图太大以后难以维护,而且无法调用 Script Graph 之外的其他 Apple API(比如 SwiftUI)。

演讲者举了一个例子:一口大锅(cauldron)里的水需要随材料加入而升降,还要有漩涡效果。这个逻辑如果完全用 Script Graph 做,图会很复杂。而且后续还要和食材漂浮系统联动,用代码实现更合理。

详细内容

自定义 Component 和 System

06:07)第一步,定义一个 Component 来存储大锅的水位:

import RealityKit

public struct Cauldron: Component, Codable {
    public var waterLevel: Float

    enum CodingKeys: CodingKey {
        case waterLevel
    }
}

关键点:

  • Component 协议让 Cauldron 可以被附加到 Entity 上
  • Codable 是必须的,这样 Reality Composer Pro 3 才能在编辑器中序列化这个组件,并保存到 Reality File 中
  • CodingKeys 用于控制哪些属性参与序列化;简单场景可以省略

06:42)第二步,写 System 来响应水位变化:

import RealityKit

public struct CauldronSystem: System {
    let query = EntityComponentQuery(Cauldron.self)
    public init(scene: Scene) {}

    public func update(context: SceneUpdateContext) {
        for (entity, cauldron) in context.entities(matching: query) {
            guard let water = entity.findEntity(named: "Cauldron_Water_mesh")
                else { continue }
            water.setPosition(SIMD3<Float>(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)
        }
    }
}

关键点:

  • EntityComponentQuery(Cauldron.self) 定义了系统关心的实体集合
  • update(context:) 每帧执行,遍历所有带有 Cauldron 组件的实体
  • findEntity(named:) 查找名为 "Cauldron_Water_mesh" 的子实体,即水面网格
  • setPosition(_:relativeTo:) 根据水位值调整水面高度

07:00)第三步,创建插件让编辑器识别这些组件和系统:

import RealityComposerPro

final class RCPCustomComponentsPlugin: RealityComposerProPlugin {
    public func setup(context: any RealityComposerProContext) {
        context.registerComponent(Cauldron.self)
        context.registerSystem(CauldronSystem.self)
    }
}

@_cdecl("createRealityComposerProPlugin")
public func createRealityComposerProPlugin() -> UnsafeMutableRawPointer {
    return RCPCustomComponentsPlugin().passRetained()
}

关键点:

  • RealityComposerProPlugin 协议来自 RealityComposerPro Swift Package,在编辑器里通过 “Run With Xcode” 链接项目时会自动添加
  • setup(context:) 中注册组件和系统,编辑器加载插件后即可识别
  • @_cdecl("createRealityComposerProPlugin") 将函数导出为 C 符号,让插件加载器能找到入口
  • passRetained() 返回原始指针,因为插件加载器通过 DLL 接口调用

编译插件 Scheme 后,在 Reality Composer Pro 3 中打开项目,编辑器会提示是否信任插件。信任后,Cauldron 组件就会出现在 Custom Components 文件夹中。把它附加到大锅实体上,调整 waterLevel 属性,水面会实时响应。

10:12)更棒的是,你可以在 Xcode 里给 System 的 update 方法打断点,attach 到编辑器进程上调试。代码在编辑器内部运行,调试体验和调试普通 App 一样。

与 Shader Graph 联动

10:35)基础水位调好后,下一步是给水面加漩涡效果。技术美术已经在 Shader Graph 里做好了漩涡材质,有 rotationSpeedvortexCoeff 等参数。我们需要从代码里驱动这些参数。

先扩展 Cauldron 组件,增加漩涡相关属性:

public struct Cauldron: Component, Codable {
    public var waterLevel: Float
    public var rotationSpeed: Float
    public var minWaterLevel: Float
    public var maxWaterLevel: Float
    public var vortexCoeff: Float
}

11:05)然后修改 CauldronSystem,把组件属性同步到 Shader Graph 材质参数:

public func update(context: SceneUpdateContext) {
    for (entity, cauldron) in context.entities(matching: query) {
        guard let water = entity.findEntity(named: "Cauldron_Water_mesh") else { continue }
        water.setPosition(SIMD3<Float>(0, 1, 0) * cauldron.waterLevel, relativeTo: entity)

        guard var model = water.components[ModelComponent.self] else { continue }
        guard var mat = model.materials.first as? ShaderGraphMaterial else { continue }
        let surface = computeSurface(cauldron: cauldron)
        try? mat.setParameter(name: "Level Radius", value: .float(surface.levelRadius))
        try? mat.setParameter(name: "Lowest Point",
            value: .float(cauldron.waterLevel - surface.lowestPoint))
        try? mat.setParameter(name: "Height Change", value: .float(surface.heightChange))
        try? mat.setParameter(name: "Level Coeff", value: .float(surface.levelCoeff))
        try? mat.setParameter(name: "Is Level", value: .bool(surface.isLevel))
        model.materials[0] = mat
        water.components.set(model)
    }
}

关键点:

  • water.components[ModelComponent.self] 取出实体的模型组件
  • model.materials.first as? ShaderGraphMaterial 获取 Shader Graph 材质
  • setParameter(name:value:) 设置材质参数,支持 .float.bool 等类型
  • 修改后的材质需要重新赋值回 model.materials[0],再调用 water.components.set(model) 写回实体

每次重建插件后需要重启 Reality Composer Pro 3。重启时会显示组件变更的 diff 对话框,确认后新属性就会出现在编辑器中。调整 rotationSpeed,漩涡深度会实时变化。

自定义动画 Action

13:25)Reality Composer Pro 3 的动画序列器(sequencer)支持自定义动画 Action。演讲者演示了一个让水位从 0.3 渐变到 0.5 的动画。

先定义 Action:

import RealityKit

public struct SetWaterLevelAction: EntityAction, Codable {
    public let startWaterLevel: Float
    public let endWaterLevel: Float

    public var animatedValueType: (any AnimatableData.Type)? { Transform.self }
}

关键点:

  • EntityAction 协议让 Action 可以被放入 sequencer timeline
  • Codable 保证 Action 能序列化到 Reality File
  • animatedValueType 返回 Transform.self,这是 EntityAction 协议的要求,用于在动画执行器中访问目标实体

14:05)再写执行逻辑:

extension SetWaterLevelAction {
    static func subscribe() {
        Task { @MainActor in
            SetWaterLevelAction.subscribe(to: .updated) { event in
                let normalizedTime = (event.playbackController.time - event.startTime) /
                    event.duration
                let action = event.action
                let currentLevel = action.startWaterLevel +
                    Float(normalizedTime) * (action.endWaterLevel - action.startWaterLevel)
                guard let entity = event.targetEntity else { return }
                guard var cauldron = entity.components[Cauldron.self] else { return }
                cauldron.waterLevel = currentLevel
                entity.components.set(cauldron)
            }
        }
    }
}

关键点:

  • subscribe(to: .updated) 监听动画更新事件
  • normalizedTime 计算当前动画进度(0 到 1)
  • 用线性插值计算当前水位:start + progress * (end - start)
  • event.targetEntity 获取目标实体,更新其 Cauldron 组件

14:56)最后在插件中注册 Action 并调用 subscribe()

final class RCPCustomComponentsPlugin: RealityComposerProPlugin {
    public func setup(context: any RealityComposerProContext) {
        context.registerComponent(Cauldron.self)
        context.registerSystem(CauldronSystem.self)

        context.registerAction(SetWaterLevelAction.self)
        SetWaterLevelAction.subscribe()
    }
}

注册后,在编辑器中创建序列(Sequence),添加动画轨道,把 SetWaterLevelAction 拖到 timeline 上,设置起始和结束水位,播放即可看到水位渐变动画。

自定义 Script Graph 节点

17:32)Script Graph 是 Reality Composer Pro 3 的可视化脚本系统,让设计师不写代码就能做交互。通过插件,可以把自定义组件暴露为 Script Graph 节点。

最简单的方式是用 @Scriptable 宏:

import RealityKit
import RealityKitScripting
import RealityKitScriptingMacros

@Scriptable
public struct Cauldron: Component, Codable {
    public var waterLevel: Float
    public var rotationSpeed: Float
    public var minWaterLevel: Float
    public var maxWaterLevel: Float
    public var vortexCoeff: Float
}

关键点:

  • @Scriptable 宏会自动生成 SchemaProvider.schema,描述组件的属性结构
  • 需要导入 RealityKitScriptingRealityKitScriptingMacros
  • 这两个模块和 RealityComposerPro Swift Package 一样,在通过编辑器创建 Xcode 项目时自动配置

18:08)然后在插件中注册脚本模块:

public func setup(context: any RealityComposerProContext) {
    context.registerComponent(Cauldron.self)
    context.registerSystem(CauldronSystem.self)

    context.registerAction(SetWaterLevelAction.self)
    SetWaterLevelAction.subscribe()

    Task { @MainActor in
        let config = RKS.Configuration(id: "ChaparralVillage")
            .onInitialize { _ in
            [
                Module("ChaparralVillage") {
                    Cauldron.SchemaProvider.schema
                }
            ]
        }
        try! RKS.addConfiguration(config)
    }
}

关键点:

  • 脚本模块注册必须在主线程执行,所以包在 Task { @MainActor in ... }
  • RKS.Configuration(id:) 创建项目专属的配置
  • onInitialize 闭包返回模块列表,每个模块包含组件的 schema
  • RKS.addConfiguration(config) 将配置添加到 RealityKitScripting 系统

注册后,给大锅实体添加 Scripting 组件,打开 Script Graph 编辑器。添加 Update 节点、If 节点检测按键,把 If 的 true 分支连到 Set Water Level 节点,设置按键 “a” 对应水位 0.25,按键 “z” 对应水位 0.5。打开 simulation view 按 “a” 和 “z”,水位会实时升降。

核心启发

1. 做一个可实时调整的粒子系统编辑器

用自定义 Component 存储粒子参数(发射速率、生命周期、颜色渐变),用 System 驱动粒子行为。美术在 Reality Composer Pro 3 里调参数,实时看到粒子效果变化。入口 API:ComponentSystemRealityComposerProPlugin

2. 把物理交互逻辑做成可视化节点

@Scriptable 宏把自定义物理组件暴露给 Script Graph。设计师可以拖拽节点来配置碰撞响应、重力场强度,不需要写代码。入口 API:@ScriptableRKS.Configuration

3. 做一套角色动画状态机

定义多个 EntityAction(Idle、Walk、Jump),每个 Action 控制骨骼动画和位移。在 sequencer 里编排状态切换的时间线,设计师可以直接在编辑器里调整过渡时长和混合权重。入口 API:EntityActionsubscribe(to: .updated)

4. 材质参数实时预览工具

把 PBR 材质的各个参数(roughness、metallic、emissive)做成自定义 Component,System 每帧同步到 Shader Graph 材质。美术调整滑块就能看到不同材质效果,比反复导出测试快得多。入口 API:ShaderGraphMaterial.setParameter(name:value:)

5. 多人协作的空间场景版本控制

利用 Reality Composer Pro 3 的自定义合并工具和 Git 集成,把场景数据(JSON)和代码放在同一个仓库。工程师改插件、美术改场景,各自提交后自动合并,减少冲突。入口:编辑器内置的 merge tool + simulation bar 关联 Xcode 项目。

关联 Session

评论

GitHub Issues · utterances