You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何让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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.12 05:04:10