I have a trigger component that can cause things to happen in my games as a response to other events. Each trigger has various properties (animation, sound, movement) that the trigger component uses to update other components.
The trigger component works through an array of these triggers in order, waiting for the relevant components to finish before moving on to the next one (a lot like SKAction
, which I don’t use because, uh, reasons. [I should probably just use SKAction
.]).
For example, if a trigger has an animation, the trigger component will ask the animation component to start this animation and will then query the animation component every frame until it’s done. Once the animation component is done, the trigger component will dispose of the trigger.
Planting the Seed of my Own Destruction
The trigger component has a triggers
property that is an array of triggers that it gradually consumes. It has a currentTrigger
property to keep track of the current one it’s waiting to complete. Finally, it has a delegate that it calls when all of the triggers in the array have completed:
override func update(deltaTime seconds: TimeInterval) { /// Consume all of the current triggers in the `triggers` array guard allTriggersComplete() else { return } /// If everything is finished, call the delegate method if self.currentTrigger == nil, triggers.isEmpty { self.completionDelegate.triggerComponentDidFinish(self) } }
The completionDelegate
is designed to be disposable. The idea is that it is set on the trigger component at the same time the array of triggers is set, then called once at some later date when the triggers are completed before being removed.
However, because this delegate method is called in the trigger component’s update method, I could easily end up in the situation where the delegate is being sent the completion notice multiple times a second.
Leaving it up to an outside object to remove this delegate was just asking for bugs. As there was no reason that the trigger component itself couldn’t remove it, I changed it to this:
override func update(deltaTime seconds: TimeInterval) { /// If everything is finished, call the delegate method if let hasCompletionDelegate = self.completionDelegate, self.currentTrigger == nil, triggers.isEmpty { hasCompletionDelegate.triggerComponentDidFinish(self) self.completionDelegate = nil } }
Problem solved!
The Absolute State of This
I also have a use component that, when activated, runs through the following states:
- Start
- Success/Failure
- End
- None
Each state can optionally have triggers that it can set on the trigger component:
class UseComponent : GKComponent { func nextState() { var nextStateTriggers : [Trigger]? = nil switch self.useState { case .none: self.useState = .start nextStateTriggers = self.startTriggers case .start: nextStateTriggers = self.determineSuccess() case .success, .failure: self.useState = .end nextStateTriggers = self.endTriggers case .end: self.useState = .none } runTriggers(nextStateTriggers) } func run(_ nextStateTriggers : [Trigger]? ) { if let hasTriggers = nextStateTriggers { self.entity?.component(ofType: TriggerComponent.self)?.triggers = hasTriggers self.entity?.component(ofType: TriggerComponent.self)?.completionDelegate = self } else { self.nextState() } } }
If a state does set some triggers, I need to wait for them to finish before moving on to the next state. This is where the delegate method comes in:
extension UseComponent : TriggerCompletionDelegate { func triggerComponentDidFinish(_ comp: TriggerComponent) { self.nextState() } }
If you can immediately see the problem with this, you’re doing better than I did.
Bugs, Bugs, Bugs
In testing, there was an entity that never reached its end state and it took me a while to figure out why. It’s only when I looked carefully at the stack trace did I realise what was happening:
- The delegate method was being called by the Trigger Component
- Which called the
nextState()
method of the Use Component - Which called the
runTriggers(_:)
method of the Use Component - Which set the delegate of the Trigger Component again
Once I followed all of that back up the stack, I realised that it was all being kicked off by this one line of code:
hasCompletionDelegate.triggerComponentDidFinish(self)
Then this next line would be called:
self.completionDelegate = nil
Which would nil out the completion delegate that the use component had just set up.
Of course, it’s an easy fix once you see it:
override func update(deltaTime seconds: TimeInterval) { /// ... if let hasCompletionDelegate = self.completionDelegate, self.currentTrigger == nil, triggers.isEmpty { self.completionDelegate = nil hasCompletionDelegate.triggerComponentDidFinish(self) } }
Switching the operations ensures that the trigger component is still in charge of removing the delegate once it had been called once but also allows the delegate to re-add itself if it has another round of triggers to work through.
In Conclusion
Order matters.
Also, watch those stack traces. They’re very helpful.