如何让16个节点按指定序列发射粒子?iOS粒子系统开发问题
解决SceneKit节点按顺序触发粒子效果的问题
你想要实现16个节点按特定时序发射粒子,之前的两种方法都踩了坑:while循环导致所有节点同时触发,Timer方案又引发内存崩溃。咱们一步步来搞定这个问题:
先拆解之前的问题
- while循环的坑:你在主线程里用
while (self.fWorksequence == 2)反复调用Sequence2(),这个循环会疯狂执行,fWCycle会瞬间从1跳到100+,所有阶段的粒子都会被快速添加,视觉上就像同时触发了。而且这种循环会阻塞主线程,导致UI卡顿甚至无响应。 - Timer的坑:如果每次调用
Sequence2()都创建新Timer,旧的Timer没被invalidate,就会累积多个Timer同时运行,重复触发逻辑;再加上Timer会持有self,self又持有Timer的强引用循环,最终引发内存崩溃。
推荐方案1:用GCD异步延迟精准控制时序
这种方式既不会阻塞主线程,也能精准把控每个阶段的触发时间,还能避免Timer的强引用问题。
步骤1:先定义触发时序表
把需要触发的节点组和对应的延迟时间整理成数组,清晰明了:
// 定义触发序列:(延迟时间(秒), 要触发的节点名称数组) let particleSequence: [(TimeInterval, [String])] = [ (0, ["f1"]), // 立即触发f1 (1, ["f2", "f16"]), // 1秒后触发f2和f16 (2, ["f3", "f15"]), // 再等1秒(总共2秒)触发f3和f15 (3, ["f4", "f14"]), // 继续补充剩下的节点组,直到中间的节点,比如(7, ["f8", "f9"]) // ... ]
步骤2:实现按序列触发的函数
用GCD的asyncAfter依次执行每个阶段的任务,每个阶段都会在前一个阶段的延迟后触发:
func startParticleSequence() { // 遍历时序表,逐个添加延迟任务 for (index, (delay, nodeNames)) in particleSequence.enumerated() { DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in guard let self = self else { return } // 处理当前阶段的所有节点 for nodeName in nodeNames { self.sceneView.scene.rootNode.enumerateChildNodes { (node, _) in if node.name == nodeName { // 先移除旧的粒子系统,避免重复添加导致内存累积 node.particleSystems?.forEach { node.removeParticleSystem($0) } let fWorkBoom = self.launchFWork1(color: UIColor.red) node.addParticleSystem(fWorkBoom) } } } // 如果是最后一个阶段,重置序列状态 if index == self.particleSequence.count - 1 { self.fWorkSequence = 1 } } } }
步骤3:替换原来的while循环
不用再写阻塞主线程的while循环,直接调用这个函数就行:
if self.fWorkSequence == 2 { startParticleSequence() }
推荐方案2:用SceneKit的SKAction实现(更贴合生态)
因为SceneKit节点支持SKAction,我们可以用SKAction.sequence组合等待和触发动作,逻辑更直观:
func startParticleSequenceWithActions() { var actionSequence = [SKAction]() // 第一步:触发f1 let triggerF1 = SKAction.run { [weak self] in guard let self = self else { return } self.triggerNodes(["f1"]) } actionSequence.append(triggerF1) // 等待1秒 actionSequence.append(SKAction.wait(forDuration: 1)) // 第二步:触发f2和f16 let triggerF2F16 = SKAction.run { [weak self] in guard let self = self else { return } self.triggerNodes(["f2", "f16"]) } actionSequence.append(triggerF2F16) // 再等待1秒 actionSequence.append(SKAction.wait(forDuration: 1)) // 继续添加后续的触发和等待动作... // 最后:重置序列状态 actionSequence.append(SKAction.run { [weak self] in self?.fWorkSequence = 1 }) // 把序列动作加到根节点执行 sceneView.scene.rootNode.runAction(SKAction.sequence(actionSequence)) } // 封装触发节点的工具函数,避免重复代码 private func triggerNodes(_ nodeNames: [String]) { nodeNames.forEach { name in sceneView.scene.rootNode.enumerateChildNodes { (node, _) in if node.name == name { node.particleSystems?.forEach { node.removeParticleSystem($0) } let boom = launchFWork1(color: .red) node.addParticleSystem(boom) } } } }
这种方式的好处是,SceneKit的Action自带取消机制(调用node.removeAllActions()就能终止序列),完全不用操心Timer的生命周期问题。
如果非要修复Timer方案?
要是你想继续用Timer,得解决两个核心问题:避免多个Timer同时运行,防止强引用循环:
var timer = Timer() var nodeCounter = 0 func Sequence2() { // 先取消旧的Timer,避免重复创建 timer.invalidate() nodeCounter = 0 // 设置Timer的时间间隔为1秒,对应你的需求节奏 timer = Timer.scheduledTimer(timeInterval: 1.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true) } @objc func timerAction() { nodeCounter += 1 switch nodeCounter { case 1: triggerNodes(["f1"]) case 2: triggerNodes(["f2", "f16"]) case 3: triggerNodes(["f3", "f15"]) // ... 补充其他阶段的判断 default: // 所有阶段执行完,停止Timer并重置状态 timer.invalidate() fWorkSequence = 1 } } // 记得在页面销毁时取消Timer,避免强引用 deinit { timer.invalidate() } // 同样用封装的triggerNodes函数 private func triggerNodes(_ nodeNames: [String]) { nodeNames.forEach { name in sceneView.scene.rootNode.enumerateChildNodes { (node, _) in if node.name == name { node.particleSystems?.forEach { node.removeParticleSystem($0) } let boom = launchFWork1(color: .red) node.addParticleSystem(boom) } } } }
内容的提问来源于stack exchange,提问作者JAlves
相关产品推荐
相关产品推荐

