Order of Operations in Components

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:

  1. Start
  2. Success/Failure
  3. End
  4. 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:

  1. The delegate method was being called by the Trigger Component
  2. Which called the nextState() method of the Use Component
  3. Which called the runTriggers(_:) method of the Use Component
  4. 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.