向心力場(RadialForceEffect)特色在於力的方向總是朝力場中心(也就是附掛元件的個體座標原點),強度會依距離而變化,距離越遠,拉力越強,就像彈簧的虎克定律,可想成力場中心與每個動態本體之間,有一個彈簧相連接。
Radial 是放射狀、輻射狀的意思。向心力場的力其實有兩個方向,若將強度(strength)改成負值,向心力就變成離心力,往外放射,束縛力場轉為排斥力場,讓所有動態本體遠離力場中心。
實際使用時,向心力場有兩個必要參數,除了第一個強度(strength)之外,並不需要指定方向。第二個參數是 restDistance,表示彈簧不受力(或不做功、休息 rest)時的長度:
let 向心力 = RadialForceEffect(strength: 5.0, restDistance: 0.0)當 restDistance = 0.0,動態本體會往力場中心來回震盪,初始距離越遠,震盪越快。執行結果如下:
這樣的來回震盪,其實就是高中物理學過的簡諧運動。
若 restDistance 大於 0,例如 restDistance = 1.0:
let 向心力 = RadialForceEffect(strength: 5.0, restDistance: 1.0)
此時可想像彈簧不受力時的長度為1米,一端固定在力場中心,另一端牽引著動態本體。彈簧拉長時會產生拉力(拉向力場中心),壓縮時會有推力(推離力場中心),於是動態本體將會在距離中心1米的平衡點來回震盪,效果如下:
利用這樣的特性,本節範例做出12個氣泡,散佈在不同位置,讓他們在距離中心1米左右來回震盪,並增加一個「開始/暫停」開關,以便控制與觀察:
有沒有看出來,一開始,每個震盪幅度比較大的氣泡,不管遠近,幾乎都會同時到達力場中心而發生碰撞,這是為什麼呢?答案請參考註解(作業4)。
完整範例程式如下:
// 6-13c 向心力場
// Created by Heman Lu on 2025/07/15
// Minimum Requirement: macOS 15 or iPadOS 18 + Swift Playground 4.6
import SwiftUI
import RealityKit
struct 物理力場: View {
var 隨機數: Float { .random(in: -1.0 ..< 1.0) }
@State var 暫停 = true
var body: some View {
RealityView { 內容 in
內容.add(座標軸())
var 玻璃材質 = PhysicallyBasedMaterial()
玻璃材質.roughness = 0.1
玻璃材質.metallic = 0.9
玻璃材質.blending = .transparent(opacity: 0.3)
// 氣泡模型,設定為動態(.dynamic)才會受力場影響
let 氣泡 = MeshResource.generateSphere(radius: 0.1)
let 氣泡模型 = ModelEntity(mesh: 氣泡, materials: [玻璃材質])
氣泡模型.name = "氣泡"
氣泡模型.physicsBody = .init(mode: .dynamic)
氣泡模型.physicsBody?.isAffectedByGravity = false
氣泡模型.generateCollisionShapes(recursive: false)
// 複製12個氣泡模型
for i in 0 ..< 12 {
let 模型 = 氣泡模型.clone(recursive: false)
模型.position = [隨機數, 隨機數, 隨機數] * 2.0
內容.add(模型)
}
// 增加力場
let 向心力 = RadialForceEffect(strength: 5.0, restDistance: 1.0)
let 力場 = ForceEffect(effect: 向心力)
let 力場元件 = ForceEffectComponent(
effects: [力場],
simulationState: .pause) // 初始狀態:暫停
let 力場中心 = Entity()
力場中心.name = "力場"
力場中心.components.set(力場元件)
內容.add(力場中心)
if let 天空盒 = try? await EnvironmentResource(named: "威尼斯清晨") {
內容.environment = .skybox(天空盒)
}
} update: { 內容 in
for 個體 in 內容.entities where 個體.name == "力場" {
個體.components[ForceEffectComponent.self]?.simulationState = 暫停 ? .pause : .start
}
}
.realityViewCameraControls(.orbit)
.overlay(alignment: .bottom) {
Button(暫停 ? "開始" : "暫停", systemImage: 暫停 ? "play.fill" : "pause.fill") {
暫停.toggle()
}
.buttonStyle(.borderedProminent)
.padding()
}
}
}
import PlaygroundSupport
PlaygroundPage.current.setLiveView(物理力場())
這裡面有一行程式值得注意:
模型.position = [隨機數, 隨機數, 隨機數] * 2.0等號右邊,並不是任何矩陣都可以和數字相乘,只有 SIMD 類型才這麼方便,這行程式將 -1 ..< 1 之間的隨機數範圍擴大到 -2 ..< 2。另外,「隨機數」用 “computed property” 定義,所以每次取值都會不同。
最後還有一行用到新語法:
個體.components[ForceEffectComponent.self]?.simulationState = 暫停 ? .pause : .start「個體.components」其實是一個集合(而非陣列)— 個體已載入元件的集合,要取集合中某個元素,用法為「個體.components[ForceEffectComponent.self]」,[ ] 裡面是索引,因為元件是動態載入,不一定會有這個元素,所以傳回 Optional 類型,必須加 ? 才能進一步取得元素的屬性。
此處元件集合 components 是以「元件類型」為索引,每一種元件類型最多只會有一個元素,不會重複,例如:
個體.components.set(力場元件1)這個用法和陣列的 append() 完全不同。
個體.components.set(粒子元件)
...
個體.components.set(力場元件2) // 力場元件1會被取代
還記得集合、陣列與字典的差異嗎?請參考第5單元語法說明:字典(Dictionary)資料類型,每個集合的索引方式都可能不同。集合是比較少見的組合類型,這應該是我們第一次使用集合索引。
💡註解
- RadialForceEffect 照字面意思也可譯為「輻射力場」,不過這樣容易想成往外輻射,也就是強度為負值時(離心力)的方向,與習慣不符。
- RadialForceEffect 同樣有第3個屬性 forceMode,用法參考上一節(6-13b)。
- 作業1:請修改 RadialForceEffect(strength: 5.0, restDistance: 0.0),觀察不同的 restDistance 數值有何影響。
- 作業2:請將力場強度改為負值(如strength: -5.0),觀察效果有何變化。
- 作業3:請改用固定強度的 ConstantRadialForceEffect,看執行結果有何不同。
- 作業4:簡諧運動的週期跟什麼有關?答案是彈力係數及物體質量。向心力場的強度(strength)相當於彈力係數,而物理本體預設質量均為1Kg,因此力場中每個本體的震盪週期都相同。請改變部分氣泡的質量,觀察週期是否變化。