[VisionOS] 拆分HelloWorld的功能点 - 地球自转

1,201 阅读3分钟

背景

在体验HelloWorld时,很好奇每个功能是怎么实现的,但是这个demo复用了很多功能、数据模型,刚开始理解起来就比较困难。所以我就先从功能点来看,将复用的功能、数据模型都剔除掉,保证单一功能能解藕单独运行。

环境

Xcode:15.1 beta

VisionOS:1.0

梳理功能

graph LR;
    功能点-->A(设置光照);
    style A fill:#bbf,color:#fff
    click A "https://juejin.cn/post/7298690615046651943"
    
    功能点-->B(手势转动地球)
    style B fill:#bbf,color:#fff
    click B "https://juejin.cn/post/7298765809290706983"
    
    功能点-->C(地球自转)
    style C fill:#bbf,color:#fff
    click C "https://juejin.cn/post/7298775642261569575"
    
    功能点-->D(地球跟随鼠标拖动)
    style D fill:#bbf,color:#fff
    click D "https://juejin.cn/post/7299037876637351975"
    
    功能点-->E(卫星围绕地球转动)
    style E fill:#bbf,color:#fff
    click E "https://juejin.cn/post/7300431522255241250"
    
    功能点-->G(沉浸式与窗口之间的切换)
    style G fill:#bbf,color:#fff
    click G "https://juejin.cn/spost/7300816733525901352"

地球自转

为了让模型更立体,后续的所有例子,都默认添加了光照

Nov-08-2023 14-41-39.gif

import SwiftUI
import RealityKit
import RealityKitContent

struct EarthRotation: View {
    @State var isRotation = false
    @State var curEarth:Entity = Entity()
    
    var body: some View {
        ZStack{
            RealityView { content in
                guard let earth = await RealityKitContent.entity(named: "Globe") else {
                    return
                }
                
                earth.setSunlight(intensity: 14)
                earth.scale = SIMD3(repeating: 0.3)
                earth.orientation = .init(angle: 0, axis: [0, 1, 0])
                curEarth = earth
                content.add(earth)
            } update: { content in
                curEarth.components.set(RotationComponent(speed: isRotation ? 0.5 : 0))
            }

            Toggle("Rotation", isOn: $isRotation)
                .toggleStyle(.button)
                .padding(.top, 240)
        }
    }
    init() {
        RotationComponent.registerComponent()
        RotationSystem.registerSystem()
    }
}

#Preview {
    EarthRotation()
}

1. 加载3D资源

这里和手势转动地球不一样,可以不用给Entity添加InputComponent,因为这个时候我们并不需要一个手势去触发旋转。只要加载一个普通的3D资源就可以了。

2.添加自转组件

这里用到的了RotationComponentRotationSystem,是一个完全自定义的组件。

import SwiftUI
import RealityKit

/// Rotation information for an entity.
struct RotationComponent: Component {
    var speed: Float
    var axis: SIMD3<Float>

    init(speed: Float = 1.0, axis: SIMD3<Float> = [0, 1, 0]) {
        self.speed = speed
        self.axis = axis
    }
}

/// A system that rotates entities with a rotation component.
struct RotationSystem: System {
    static let query = EntityQuery(where: .has(RotationComponent.self))

    init(scene: RealityKit.Scene) {}

    func update(context: SceneUpdateContext) {
        for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
            guard let component: RotationComponent = entity.components[RotationComponent.self] else { continue }
            entity.setOrientation(.init(angle: component.speed * Float(context.deltaTime), axis: component.axis), relativeTo: entity)
        }
    }
}

这里面会涉及到一个ECS架构: Entity、Component、System。

Entity负责加载资源,系统提供了旋转、位置、缩放等功能,可以添加多个Component

Component就是一些功能组件,有输入、光照等系统准备好的组件,也可以自定义组件。

System官方文档解释得非常清楚。我的理解,System是为多个Entity服务的,当多个Entity需要相同的行为的时候,就可以单独封装一个System出来。理论上如果你只用一个System也是可以的,因为每一帧都会触发Systemupdate方法,而且在方法内部可以查询到所有的Entity,也就可以操作他们做一些事情了。单一职责的原则,最好还是一个行为一个System,在System很多的情况下,还可以自由组合使用。

在实际测试中发现了System.update方法触发的两个时机:

  • RealityView初始化时(只会触发Systemupdate一定时间,不会一直触发)
  • RealityViewupdate方法(每帧都会触发Systemupdate

image.png

注意: 一旦开始更新之后,Systemupdate方法每帧都会触发,相当于一直在触发entity.setOrientation,所以才可以一直旋转下去。那么update方法里面一定不能做耗时的操作,目前我只能想到两个方法来停止刷新,其他办法暂时还没找到。

1. 移除组件

其实移除组件后,并不能停止Systemupdate方法,只是查询不到RotationComponent

components.remove(RotationComponent.self)

2. 退出窗口

这个可以完全停止Systemupdate方法

3.注册Component、System

在我们使用的时候,一定要注册自定义的Component、System,不然就没有任何效果

init() {
    RotationComponent.registerComponent()
    RotationSystem.registerSystem()
}

4.代码

EarthRotation.swift