使用 Xcode 扩展 Reality Composer Pro 3 的功能
Extend Reality Composer Pro 3 functionality with Xcode
2026年6月10日
一句话判断
✅ Reality Composer Pro 终于补齐了类似 Unity Editor Scripting 的能力,技术美术(TA)和艺术家不用再等工程师编译 App,就能在编辑器里实时跑自定义 Swift 逻辑了。
这场 Session 讲了什么
以前搞 visionOS 空间计算项目,工程师写 RealityKit 代码,艺术家在 Reality Composer Pro (RCP) 里摆模型。如果艺术家想调一个自定义 Shader 的参数,或者看自定义 System 跑起来的效果,只能等工程师打包 App 跑在头显或模拟器里。RCP 里的 Script Graph(可视化脚本)虽然能写点逻辑,但稍微复杂点就变成“意大利面条”,而且根本调不了 SwiftUI 等原生 API。
现在 RCP3 引入了插件系统(Plugin System)。Xcode 和 RCP3 共享同一个 Git 仓库,工程师在 Xcode 里写自定义的 Component(组件)和 System(系统),编译成动态库(Dynamic Library)。RCP3 直接加载这个动态库,艺术家的 Inspector(检查器)里立刻出现自定义属性,修改后自定义 System 在编辑器里实时运行。
这对包含 TA 和 3D 艺术家的中大团队影响最大。它把 RCP 从一个“高级场景预览器”拉到了“真正的游戏引擎编辑器”的水平,彻底打破了代码和美术之间的编译壁垒。
值得深挖的点
动态库插件模式与 Editor 崩溃的 Trade-off
Apple 这次用了非常硬核的 C 互操作来实现插件加载。你需要用 @_cdecl("createRealityComposerProPlugin") 导出一个 C 函数,返回 UnsafeMutableRawPointer。这种设计让 RCP3 能在运行时动态加载你编译出的 .framework。
好的一面是,这实现了真正的 Editor Scripting。艺术家在 Inspector 里拉动大釜水位的 Slider,你的 CauldronSystem 就在 RCP3 进程里实时跑,水面 Shader 立刻跟着变。但代价是,你的插件代码和 RCP3 跑在同一个进程里。如果你的 Swift 代码里有个 Force Unwrap 崩了,或者数组越界,整个 RCP3 编辑器会直接闪退。开发时千万别直接盲调,必须在 Xcode 里用 Attach to Process 连上 RCP3 进程打断点。另外,每次改代码还是得重新 Build 动态库并让 RCP 重载,虽然比编译整个 visionOS App 快,但比起 Unity C# 的热重载还是差了点意思。
EntityAction 与 Script Graph 的降维打击
Session 里演示了用 EntityAction 协议自定义 Sequencer(序列器)动画,以及用 @Scriptable 宏把 Component 暴露给 Script Graph。这是解决“可视化脚本难以维护”的终极方案。
以前为了让艺术家控制逻辑,工程师不得不写一堆复杂的 Script Graph,最后变成没人敢动的屎山。现在,工程师可以用纯 Swift 写高性能、带复杂数学计算的底层逻辑(比如计算漩涡水面的曲率),然后通过 @Scriptable 宏直接生成 Script Graph 节点。艺术家在可视化界面里只需要连线调用这个节点。这种“代码写底层,节点搭上层”的分工,才是大型空间计算项目该有的工作流。
代码片段
1. 注册自定义 Component 和 System (Plugin 入口)
场景:让 RCP3 认识你的自定义逻辑,并在 Inspector 中显示。
import RealityComposerPro
// 实现插件协议,注册组件和系统
final class RCPCustomComponentsPlugin: RealityComposerProPlugin {
public func setup(context: any RealityComposerProContext) {
context.registerComponent(Cauldron.self)
context.registerSystem(CauldronSystem.self)
}
}
// 必须用 @_cdecl 导出 C 函数,返回原始指针供 RCP3 动态加载
@_cdecl("createRealityComposerProPlugin")
public func createRealityComposerProPlugin() -> UnsafeMutableRawPointer {
return RCPCustomComponentsPlugin().passRetained()
}
坑:自定义的 Cauldron Component 必须实现 Codable 协议,否则 RCP3 无法将其序列化到 Reality File 中,编辑器里也刷不出来。
2. 在 System 中驱动 ShaderGraphMaterial
场景:艺术家在 Inspector 调参,实时改变水面漩涡 Shader。
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 }
// 计算漩涡参数并传递给 Shader Graph
let surface = computeSurface(cauldron: cauldron)
try? mat.setParameter(name: "Level Radius", value: .float(surface.radius))
// 重新赋值材质
model.materials = [mat]
water.components.set(model)
}
}
坑:setParameter 会抛出异常,记得用 try?。最坑的是参数名(如 “Level Radius”)必须和 RCP3 Shader Graph 里的拼写、大小写完全一致,错一个字母就静默失败。
3. 自定义 Sequencer 动画 (EntityAction)
场景:在时间轴上控制大釜水位动画。
import RealityKit
// 实现 EntityAction 协议,定义动画参数
public struct SetWaterLevelAction: EntityAction, Codable {
public let startWaterLevel: Float
public let endWaterLevel: Float
// 必须返回一个 AnimatableData 类型,即使你实际动画的是 Float
public var animatedValueType: (any AnimatableData.Type)? { Transform.self }
}
extension SetWaterLevelAction {
static func subscribe() {
Task { @MainActor in
// 订阅动画更新事件,手动插值计算当前水位
SetWaterLevelAction.subscribe(to: .updated) { event in
let normalizedTime = (event.playbackController.time - event.startTime) / event.duration
let currentLevel = event.action.startWaterLevel +
Float(normalizedTime) * (event.action.endWaterLevel - event.action.startWaterLevel)
guard var cauldron = event.targetEntity?.components[Cauldron.self] else { return }
cauldron.waterLevel = currentLevel
event.targetEntity?.components.set(cauldron)
}
}
}
}
坑:animatedValueType 是个历史包袱,必须返回一个类型(如 Transform.self),否则 RealityKit 动画系统不认。实际插值逻辑得自己在 subscribe 里写。
最佳实践
已有项目如果有一堆臃肿的 Script Graph,赶紧把核心计算逻辑抽离成 Swift Component/System,通过 Plugin 暴露给 RCP3,给项目减负。
新项目一开始就要建立 Xcode + RCP3 共享 Git 仓库的结构。在 Xcode 里配好两个 Scheme:一个 Build App,一个 Build Plugin 动态库。
实战中最容易踩的坑是调试。Plugin 崩溃会带走整个 RCP3,开发时务必在 Xcode 中 Attach to Process 连上 RCP3 进程进行断点调试。另外,RCP3 首次加载插件会弹窗询问是否 Trust(信任),如果是 CI/CD 环境自动化打包,记得处理这个信任授权。
还有什么值得关注
- 内置 Git Merge Tool:RCP3 项目文件是 JSON 格式,Apple 专门写了个自定义 Merge Tool 来处理场景冲突,比直接 merge JSON 靠谱多了。
@Scriptable宏:加个宏就能自动为 Swift 代码生成 Script Graph 节点,免去了手动写节点定义的繁琐。- 自定义 Plugin 目录:可以在 RCP3 的 Project Settings 里手动指定动态库路径,方便大团队统一管理构建产物。