返回博客首页
← 所有文章

使用 NativeScript 在 Vision Pro 开发中通过 RealityKit 和多个场景实现粒子系统

2024 年 2 月 15 日 — 作者:Nathan Walker

让我们来看看如何在使用 NativeScript 的 Vision Pro 开发中与多个场景交互并呈现粒子系统。我们将使用 @nativescript/swift-ui 提供简洁的 API,以按需打开和关闭场景,甚至使用上下文数据动态更新场景。

我们可以使用 Vision Pro 项目探索这些功能;我已经创建了一个项目,你可以参考这里

👉 演示 Github 仓库,该仓库可以在 Mac 上的 Vision Pro 模拟器或物理设备上运行。

如果您有物理 Vision Pro,您也可以通过在 Vision Pro 应用商店中搜索并安装“NativeScript 预览”应用来完整体验此功能,该应用将允许您使用此 StackBlitz

或者您也可以随时使用以下方法创建一个

npm i -g nativescript@vision
ns create myapp --vision

如果您想使用任何首选的 JavaScript 版本,请参考此处的 visionOS 文档

与多个场景交互

为了支持多个场景,您始终需要检查您的 App_Resources/visionOS/Info.plist,以确保它至少包含此设置

<key>UIApplicationSceneManifest</key>
<dict>
    <key>UIApplicationSupportsMultipleScenes</key>
    <true/>
</dict>

我们通过其唯一的标识符和 @nativescript/swift-ui API 与场景交互

  • openScene(options: OpenSceneOptions): 切换打开/关闭场景
  • updateScene(options: OpenSceneOptions): 向任何打开的场景更新上下文数据

OpenSceneOptions 接口如下所示

export interface OpenSceneOptions {
    /**
     * The id of the scene to open
     */
    id: string;
    /**
     * Whether the scene is immersive or not.
     * Only set this when you know the WindowGroup is defined with immersive style.
     */
    isImmersive?: boolean;
    /**
     * Any data bindings to pass to the scene.
     */
    data?: any;
}

您可以根据需要扩展这些选项来为您的 data 强类型化。让我们探索一个示例。

多场景示例

首先,您需要通过 App_Resources/visionOS/src/NativeScriptApp.swift 定义您希望应用程序提供的任意数量的场景

import SwiftUI

@main
struct NativeScriptApp: App {

  var body: some Scene {
    NativeScriptMainWindow()

    // NEW SCENE: A distinct window of a playable video
    WindowGroup(id: "Video") {
      VideoSceneView()
    }
    .windowStyle(.plain)
  }
}

在这里,我们定义了一个具有 id 为 'Video' 的 WindowGroup,它具有普通的 windowStyle

VideoSceneView(在 示例仓库 中提供)是您如何在多场景应用程序中实现任意数量内容的良好示例。

我们可以从 NativeScript 应用程序中打开该场景,如下所示

import { openScene } from '@nativescript/swift-ui';

openScene({
  id: 'Video'
});

视频播放器将在一个新窗口中打开,该窗口可以放置在 3D 空间中的任何位置。再次调用 openScene 并使用相同的 id 将始终关闭具有匹配 id 的已打开场景。

向场景提供上下文数据

您也可以在打开或更新场景时为任意数量的 SwiftUI @State 属性提供数据。

import { openScene } from '@nativescript/swift-ui';

openScene({
  id: 'Video',
  data: {
    url: '<any link to a playable video resource, mp4, etc.>'
  }
});

这允许 SwiftUI 如下所示使用上下文数据来执行任何需要操作的内容

struct VideoSceneView: View {
    @State var context: NativeScriptSceneContext?
    
    func createPlayer(url: String) -> AVPlayer {
        let player = AVPlayer(url: URL(string: url)!)
        player.play()
        return player
    }
    
    var body: some View {
        ZStack {
            if context != nil {
                VideoPlayer(player: player)
                    .scaledToFill()
            } else {
                EmptyView()
            }
        }.onAppear {
            setupContext()
        }
    }
    
    func setupContext() {
        context = NativeScriptSceneRegistry.shared.getContextForId(id: "Video")
        let url = context!.data["url"] as! String
        player = createPlayer(url: url)
    }
}

动态更新打开的场景

我们可以从 NativeScript 应用程序中动态更新该场景,如下所示

import { updateScene } from '@nativescript/swift-ui';

updateScene({
  id: 'Video',
  data: {
    url: '<new url to play>'
  }
});

我们可以通过添加一个 onReceive 修饰符来支持对任何场景视图的动态更新

var body: some View {
    ZStack {
        // video player
    }.onReceive(NotificationCenter.default
        .publisher(for: NSNotification.Name("NativeScriptUpdateScene")), perform: { obj in
          // allow our scene data to be updated if already opened!
          setupContext()
    }).onAppear {
      // upon first open
      setupContext()
    }
}

与 RealityKit 的沉浸式空间和粒子交互

visionOS 中 粒子发射器 的首次亮相为我们带来了一个激动人心的 API。让我们看看如何通过沉浸式空间与粒子交互。

一个 沉浸式空间 是另一种类型的场景,我们可以使用 RealityKit 在其中显示粒子系统。

让我们看看如何使用 ParticleEmitterComponent 以及探索 Vortex,它是 @twostraws 开发的一个 Swift 包,它带来了一些不错的补充。

使用 Vortex

我们可以修改我们的 nativescript.config.ts 来包含用于 visionos 的 SPM(Swift 包管理器)

visionos: {
  SPMPackages: [
    {
      name: 'Vortex',
      libs: ['Vortex'],
      repositoryURL: 'https://github.com/twostraws/Vortex.git',
      version: '1.0.0'
    }
  ]
}

然后,我们可以尝试从 Vortex 中使用一个示例视图设置

struct FirefliesView: View {
  var body: some View {
    VortexViewReader { proxy in
      ZStack(alignment: .bottom) {
        VortexView(.fireflies.makeUniqueCopy()) {
          Circle()
              .fill(.white)
              .frame(width: 32)
              .blur(radius: 3)
              .blendMode(.plusLighter)
              .tag("circle")
        }
      }
    }
  }
}

让我们添加一个要打开的新场景

@main
struct NativeScriptApp: App {

    var body: some Scene {
        NativeScriptMainWindow()
        
        WindowGroup(id: "Fireflies") {
            FirefliesView()
        }
        .windowStyle(.plain)

我们现在可以通过 NativeScript 应用程序中的 openScene 打开动态粒子效果

import { openScene } from '@nativescript/swift-ui';

openScene({
  id: 'Fireflies'
});

在沉浸式空间中使用 RealityKit 的 ParticleEmitterComponent

现在让我们尝试添加一个带有粒子的沉浸式空间

@main
struct NativeScriptApp: App {
    @State private var immersionStyle: ImmersionStyle = .mixed

    var body: some Scene {
        NativeScriptMainWindow()
        
         ImmersiveSpace(id: "ParticleEmitter") {
            ParticleEmitterView()
        }
        .immersionStyle(selection: $immersionStyle, in: .mixed, .full)

我们可以设置我们的 ParticleEmitterView 来也支持来自 NativeScript 应用程序的动态数据

struct ParticleEmitterView: View {
  @State var context: NativeScriptSceneContext?
  @State var preset: String = "magic"

  var body: some View {
      ZStack {
          if context != nil {
              RealityView { content in
                  var particles = ParticleEmitterComponent.Presets.magic
                  
                  switch preset {
                  case "fireworks":
                      particles = ParticleEmitterComponent.Presets.fireworks
                  default:
                      particles = ParticleEmitterComponent.Presets.magic
                  }
                  
                  let particleEntity = Entity()
                  particleEntity.components[ParticleEmitterComponent.self] = particles
                  
                  content.add(particleEntity)
              }
          } else {
              EmptyView()
          }
      }.onAppear {
        setupContext()
      }
  }

  func setupContext() {
    context = NativeScriptSceneRegistry.shared.getContextForId(id: "ParticleEmitter")
    preset = context!.data["preset"] as! String
  }
}

我们可以在 NativeScript 应用程序中再次使用 openScene,但这次我们将使用 isImmersive 选项来指定这专门用于打开沉浸式空间

import { openScene } from '@nativescript/swift-ui';

openScene({
  id: 'ParticleEmitter',
  isImmersive: true,
  data: {
    preset: 'fireworks'
  }
});

太棒了!

关于 nStudio

nStudio 致力于建立一个健康的开源治理模型,以服务于围绕 NativeScript 的全球社区利益。如果您需要项目的专业协助,nStudio 提供跨多个学科的服务,您可以通过以下方式联系我们:[email protected].