Managing ECS Components

A lot of what drives my entities happens in pseudo-systems. These systems are GKComponent subclasses where the update(deltaTime:) method goes in an extension in order to separate the verb part of the component from the noun part:


// The Component
class DescriptionComponent : GKComponent {
	let label : SKLabelNode
	init( details : DescriptionStruct ) {
		self.label = SKLabelNode(text: details.name)
		self.label.fontName = details.font
		self.label.fontSize = details.fontSize
		self.label.textColor = details.colour
		self.label.isHidden = true
		self.label.zPosition = 10
		super.init()
	}
	required init?(coder aDecoder: NSCoder) {
		fatalError("Not implmented")
	}
}

// The System
extension DescriptionComponent {
	func update(deltaTime seconds : TimeInterval) {
		self.label.isHidden = true
		if let entityState = self.entity.component(ofType: StateComponent.self)?.state, entityState == .describe {
			self.label.isHidden = false
		}
	}
}

One of the benefits I’ve found using components is being able to add or remove them from entities while games are running. This allows me to easily change an entity’s look or abilities using data from an external source:

self.entity.removeComponent( self.component(ofType: DescriptionComponent.self) )
// Change the font name and colour and size
let compDescription = DescriptionComponent(data: self.newDetails)
self.entity.addComponent(  compDescription )

These two approaches (components as systems and swapping components), while compatible, are not without their pitfalls.

Updating Systems

My understanding of ECS is that components should, strictly speaking, consist of nothing but data and it’s the separate systems that should adjust properties on these components to make games happen.

That’s not how systems work in SpriteKit. The GKComponentSystem is a generic type that is specified by a GKComponent subclass type. However, all it does is collect together instances of these GKComponent subclasses and sends each of them the update(deltaTime:) message every frame.

It’s then up to the component to implement the actual system logic and this is where my tendency to hot-swap components can cause issues.

Arrays, Not Sets

GKComponentSystem stores references to components of its specified type in an array. These references are separated from any entities they may belong to.

I can populate this array with either individual component instances (using addComponent()) or I can pass it a GKEntity using addComponents(foundIn:) and it will make a reference to any components of its specified type found in that entity.

Say I have two entities and two components. I add the components to the entities like so (the letters are stand-ins for their memory addresses):

entity1 -> Component1 (A)
entity1 -> Component2 (B)
entity2 -> Component1 (C)

If I use the addComponents(foundIn:) for each entity, I’ll end up with an object graph that looks something like this

entity1 ->  Component1 (A) <- system
entity1 ->  Component2 (B) <- system
entity2 ->  Component1 (C) <- system

If I now remove a component from an entity using entity.removeComponent(Component1), then the object graph looks like this:

            Component1 (A) <- system
entity1 ->  Component2 (B) <- system
entity2 ->  Component1 (C) <- system

Then if I instantiate a new instance of Component1 and add it to entity1, I’ll end up with this:

            Component1 (A) <- system
entity1 ->  Component2 (B) <- system
entity2 ->  Component1 (C) <- system
entity1 ->  Component1 (D)

Note that the system still references the old component. It doesn’t yet know about the changes to the entity.

OK, no problem. Just call addComponents(foundIn:) again once I’ve changed my entity to add the reference to the new component.

HahaNOPE.

po systems[0].components
▿ 4 elements
  ▿ 0 : 
  ▿ 1 : 
  ▿ 2 : 
  ▿ 3 : 

Check out all those identical memory addresses!

This is because there’s an array under the hood, and each call to addComponents(foundIn:) will add another copy of the component found in the entity.

I have no idea why they don’t use a Set that only allows one instance of each component. There’s no reason why the same component instance would need to have their update(deltaTime:) called multiple times a frame.

It would be so much more convenient to just call addComponents(foundIn:) and not have to worry about whether or not I’m adding additional references to the same component.

That’s the way it is, however, so it’s now down to me to manually remove the old component from and add the new component to the system (making sure I don’t accidentally add it more than once).

Timing the Updates

This brings me to the second issue.

A lot of this is happening in the update(deltaTime:) methods of the component instances, which is happening in the update(deltaTime:) methods of the component systems, which in turn is happening in the update(:) method of the main game scene.

It’s a nested loop of updates, which means that the timing of adding or removing components has to be managed very carefully.

Take this very common example, seen in many tutorials and Apple’s own sample code, of how systems are run in SpriteKit. This is the update(:) method of an SKScene subclass:

override open func update(_ currentTime: TimeInterval) {
	if previousTime == 0 {
		previousTime = currentTime
	}

	let dt = currentTime - previousTime
	for system in systems {
		system.update(deltaTime: dt)
	}
	self.previousTime = currentTime
}

If I then have a GKComponent subclass, and I override its update(deltaTime:) method like this:

override func update(deltaTime seconds:  TimeInterval) {
	if stateNeedsToChange {
		self.entity.removeComponent( self.component(ofType: SpriteComponent.self) )
		let spriteComponent = SpriteComponent(data: self.newStateData)
		self.entity.addComponent(  spriteComponent )
		self.spriteSystem.addComponent( spriteComponent )
	}
}

Then the game will crash.

This is because, if I follow the nested loops back out, I’ll find that the spriteSystem is being changed while it’s being enumerated. Never a good idea.

Instead, something like this has to happen:

override func update(deltaTime seconds:  TimeInterval) {
	if stateNeedsToChange {
		self.entity.removeComponent( self.component(ofType: SpriteComponent.self) )
		let spriteComponent = SpriteComponent(data: self.newData)
		self.entity.addComponent(  spriteComponent )
		self.entity.node?.scene?.componentsToAdd.append( spriteComponent )
	}
}

Then, my update(:) method in my main game scene will need to look something like this:

override open func update(_ currentTime: TimeInterval) {
	if previousTime == 0 {
		previousTime = currentTime
	}
	for component in componentsToAdd {
		for system in systems {
			if component.isMemeber(of: system.componentClass) {
				system.add(component)
			}
		}
	}
	componentsToAdd.removeAll()
	let dt = currentTime - previousTime
	for system in systems {
		system.update(deltaTime: dt)
	}
	self.previousTime = currentTime
}

This ensures that component management takes place outside of the system update calls. The new components are added to their systems before the for loop over those systems, preventing the mutating while enumerating errors.

Swimming Against the Tide

I recognise that a lot of this complexity is caused by my own weird way of working. In strict ECS, components are the data source. However, I am instantiating components with model data from an external source that is then used to set them up.

In my system, the model data has different state data, which means it makes it easier for me to just throw away a component and set up a new one, passing in a different struct to get a different set up for that component.

I don’t know if GKComponent instances were designed to be swapped in and out willy-nilly. It’s possible, however, and there are APIs to do it.

I’ll continue to do it as it works really well (if I remember these implementation details, hence this post)!