-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathEntitySwiftSplash.swift
More file actions
310 lines (263 loc) · 12.8 KB
/
EntitySwiftSplash.swift
File metadata and controls
310 lines (263 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/*
See the LICENSE.txt file for this sample’s licensing information.
Abstract:
An extension on Entity containing app-specific functions.
*/
import Foundation
import SwiftSplashTrackPieces
import RealityKit
public extension Entity {
/// Sets visibility for animation entities for build mode and for ride mode when this piece isn't the active ride piece.
func setUpAnimationVisibility() {
forEachDescendant(withComponent: IdleAnimationComponent.self) { entity, component in
// Only the start piece shows idle animations during build mode.
entity.isEnabled = self.name == startPieceName
}
forEachDescendant(withComponent: RideAnimationComponent.self) { entity, component in
entity.isEnabled = component.isPersistent
}
forEachDescendant(withComponent: GlowComponent.self) { entity, component in
guard let isSelected = connectableStateComponent?.isSelected,
let material = connectableStateComponent?.material else { return }
if component.isTopPiece && material == .metal {
entity.isEnabled = true
}
entity.isEnabled = isSelected
}
forEachDescendant(withComponent: RideWaterComponent.self) { entity, component in
entity.setWaterLevel(level: 0.0)
}
}
/// Sets visibility for animation entities for when this piece is active during the ride.
func setVisibilityForTrackStart() {
forEachDescendant(withComponent: IdleAnimationComponent.self) { entity, component in
entity.isEnabled = false
}
forEachDescendant(withComponent: RideAnimationComponent.self) { entity, component in
entity.isEnabled = true
}
}
func setWaterLevel(level: Float) {
isEnabled = true
func setWaterlevelOnMaterials(_ modelEntity: Entity, _ modelComponent: ModelComponent, _ level: Float) {
modelEntity.isEnabled = level > 0
var modelComponent = modelComponent
modelComponent.materials = modelComponent.materials.map {
guard var material = $0 as? ShaderGraphMaterial else { return $0 }
if material.parameterNames.contains(waterLevelParameterName) {
do {
try material.setParameter(name: waterLevelParameterName,
value: MaterialParameters.Value.float(level))
} catch {
logger.error("Error setting ride_running material parameter: \(error.localizedDescription)")
}
}
return material
}
modelEntity.modelComponent = modelComponent
}
if let connectableStateComponent {
// Plastic and Wood ride pieces are open on the top, while Metal ride pieces have a
// glass enclosure. For Plastic and Wood ride pieces, emit water flowing sounds from
// each piece, with the volume equivalent to the level of water.
if [.plastic, .wood].contains(connectableStateComponent.material) {
if let controller = SoundEffectPlayer.shared.play(.waterFlowing, from: self) {
controller.gain = decibels(amplitude: Double(level))
}
}
}
forEachDescendant(withComponent: RideWaterComponent.self) { entity, component in
if let modelComponent = entity.modelComponent {
setWaterlevelOnMaterials(entity, modelComponent, level)
}
entity.forEachDescendant(withComponent: ModelComponent.self) { modelEntity, component in
setWaterlevelOnMaterials(modelEntity, component, level)
}
}
}
func hideAllIdleAndNonPersistentAnimations() {
forEachDescendant(withComponent: IdleAnimationComponent.self) { entity, component in
entity.isEnabled = component.playAtEndInsteadOfBeginning
if component.playAtEndInsteadOfBeginning {
entity.playIdleAnimations()
}
}
forEachDescendant(withComponent: RideAnimationComponent.self) { entity, component in
if !component.isPersistent {
entity.isEnabled = false
}
}
}
/// Recursively plays all animations on this entity and all descendant entities.
func playRideAnimations() {
setVisibilityForTrackStart()
var animDuration: Double = 0
forEachDescendant(withComponent: RideAnimationComponent.self) { entity, component in
if component.duration > animDuration {
animDuration = component.duration / animationSpeedMultiplier
}
for animation in entity.availableAnimations {
var animation = animation
if component.alwaysAnimates {
animation = animation.repeat(count: Int.max)
}
let controller = entity.playAnimation(animation, transitionDuration: 0.0, startsPaused: false)
rideAnimationControllers.append(controller)
controller.resume()
controller.speed = Float(animationSpeedMultiplier)
}
}
// Conditionally play a random fish sound from the ride piece
maybePlayAFishSound()
Task(priority: .high) {
await loopThroughRidePieces(animDuration: animDuration)
}
}
private func loopThroughRidePieces(animDuration: Double) async {
var rideStartTime: TimeInterval = Date.timeIntervalSinceReferenceDate
var adjustedStartTime = rideStartTime
var later = Date.timeIntervalSinceReferenceDate
while later - adjustedStartTime < animDuration {
// Keep sleep duration to less than one frame for precision (90fps = 11.11111ms).
try? await Task.sleep(for: .milliseconds(11))
if shouldCancelRide { return }
later = Date.timeIntervalSinceReferenceDate
if shouldPauseRide {
handleRidePause(adjustedStartTime: &adjustedStartTime, rideStartTime: &rideStartTime)
} else {
if rideStartTime > 0 {
for controller in rideAnimationControllers {
controller.resume()
}
pauseStartTime = 0
rideStartTime = adjustedStartTime
}
}
}
if shouldCancelRide { return }
if let nextPiece = self.connectableStateComponent?.nextPiece, nextPiece.name == "end" {
handleEndPiece()
}
if shouldCancelRide { return }
self.hideAllIdleAndNonPersistentAnimations()
// Check if there's another piece after this one.
guard let nextPiece = self.connectableStateComponent?.nextPiece else {
return
}
if shouldCancelRide { return }
// See if there's another piece connected after this one.
logger.info("Triggering animation from \(self.name) on \(nextPiece.name) at \(Date.timestamp)")
nextPiece.playRideAnimations()
}
private func handleEndPiece() {
guard let endPiece = self.connectableStateComponent?.nextPiece else { fatalError("Next piece is not the end piece.") }
endPiece.setAllParticleEmittersTo(to: true, except: [waterFallParticlesName, fishSplashParticleName])
SoundEffectPlayer.shared.play(.endRide, from: endPiece)
Task {
try await Task.sleep(for: .seconds(1))
endPiece.makeFishSplash()
try await Task.sleep(for: .seconds(9))
endPiece.setAllParticleEmittersTo(to: false, except: [waterFallParticlesName, fishSplashParticleName])
}
}
private func handleRidePause(adjustedStartTime: inout TimeInterval, rideStartTime: inout TimeInterval ) {
if pauseStartTime == 0 {
pauseStartTime = Date.timeIntervalSinceReferenceDate
}
for controller in rideAnimationControllers {
controller.pause()
}
SoundEffectPlayer.shared.pause(.startRide)
adjustedStartTime = rideStartTime + Date.timeIntervalSinceReferenceDate - pauseStartTime
}
/// Sets all particle systems contained in this entity's hierarchy on or off.
func setAllParticleEmittersTo(to isOn: Bool, except emittersToIgnore: [String] = [String]()) {
self.forEachDescendant(withComponent: ParticleEmitterComponent.self) { entity, component in
guard !emittersToIgnore.contains(entity.name) else { return }
logger.info("Turning emitter on entity \(entity.name) to \(isOn)")
var component = component
component.isEmitting = isOn
component.simulationState = (isOn) ? .play : .stop
if isOn {
entity.isEnabled = true
} else {
Task {
// Give the particles that have already been emitted a chance to finish.
try await Task.sleep(for: .seconds(4))
entity.isEnabled = false
}
}
entity.components.set(component)
}
}
func stopAllParticleEmittersEmitting(except emittersToIgnore: [String] = [String]()) {
self.forEachDescendant(withComponent: ParticleEmitterComponent.self) { entity, component in
guard !emittersToIgnore.contains(entity.name) else { return }
logger.info("Turning off emission of particles on entity \(entity.name).")
var component = component
component.isEmitting = false
entity.components.set(component)
}
}
func makeFishSplash() {
guard let splashParticlesEntity = findEntity(named: fishSplashParticleName),
var splashParticlesComponent = splashParticlesEntity.particleEmitterComponent else { return }
splashParticlesComponent.isEmitting = true
splashParticlesComponent.simulationState = .play
splashParticlesEntity.components.set(splashParticlesComponent)
isEnabled = true
}
func stopWaterfall() {
self.forEachDescendant(withComponent: ParticleEmitterComponent.self) { entity, component in
var component = component
component.isEmitting = false
component.simulationState = .stop
entity.components.set(component)
entity.isEnabled = false
}
}
func disableAllParticleEmitters(except emittersToIgnore: [String] = [String]()) {
Task(priority: .high) {
stopAllParticleEmittersEmitting(except: emittersToIgnore)
try? await Task.sleep(for: .seconds(3))
setAllParticleEmittersTo(to: false, except: emittersToIgnore)
}
}
/// Plays idle animations so they loop.
func playIdleAnimations() {
forEachDescendant(withComponent: IdleAnimationComponent.self) { entity, component in
for animation in entity.availableAnimations {
logger.info("Found idle animation: \(String(describing: animation.name))")
let animation = animation.repeat(count: Int.max)
let controller = entity.playAnimation(animation, transitionDuration: 0.0, startsPaused: false)
controller.resume()
}
}
}
func adjustCollisionBox(scaleBy: SIMD3<Float>, offsetBy: SIMD3<Float>) {
if var component = collisionComponent {
if let shape = component.shapes.first {
let calculatedBounds = shape.bounds
let newBoxOffset = ShapeResource.generateBox(width: calculatedBounds.extents.x * scaleBy.x,
height: calculatedBounds.extents.y * scaleBy.y,
depth: calculatedBounds.extents.z * scaleBy.z)
let newBox = newBoxOffset.offsetBy(translation: offsetBy)
component.shapes.removeAll()
component.shapes.append(newBox)
}
components.set(component)
}
}
private func maybePlayAFishSound() {
// Don't play fish sounds from the start or goal pieces as the associated files already
// have fish sounds in them.
guard !["start", "end"].contains(name) else { return }
// Only play a fish sound roughly one out of four ride pieces.
if Int.random(in: 0..<4) == 0 {
// Play a random fish sound from the audio file group resource
SoundEffectPlayer.shared.play(.fishSounds, from: self)
}
}
}
/// - Returns: The Decibels in [-inf,0] for the given amplitude in [0,1].
func decibels(amplitude: Double) -> Audio.Decibel { 20 * log10(amplitude) }