Batch Remove System for GameplayKit Entities

Flag and batch remove objects from the game world by using a GameplayKit component and component system with Swift.

Batch Remove System for GameplayKit Entities
Johan Steen
by Johan Steen

In most games, objects are being added and removed from the game world as the game progresses. This is an operation that affects memory usage and also triggers possible garbage collection.

I was looking for a solution to be able to remove objects in batches instead of having custom code that handles removal of objects scattered throughout my games.

I came up with a solution based on using a component together with a component system in GameplayKit.

By removing objects in batches I get control of exactly then during the game's update loop removal of objects are happening and also get the code better structured and performant to do these operations that affects memory in one go instead of being fragmented during the frame cycle.

It has also allowed me to get cleaner and more consolidated code with a single location that handles the removal of objects.

The Remove Component

The remove component is super simple; all it does is add itself to a component system, so it can be processed later.

import GameplayKit

/// Flags an entity to be removed from the game.
class RemoveComponent: Component {
    // MARK: Component Lifecycle

    override func didAddToEntity() {
        super.didAddToEntity()

        // Register with the removeComponentSystem.
        scene?.removeComponentSystem.addComponent(self)
    }
}

We use didAddToEntity() to run our code automatically as soon as we add the component to an entity.

The component system the remove component adds itself to is defined in the scene class where the game runs. As almost all my entities and their added components needs to reference the scene file, I'm letting all my entities have a reference to the scene, and then I don't use GKComponent directly but an extended version with a helper property so I always can reference the scene from components.

It looks something like this.

class Component: GKComponent {
    // MARK: Convenience Properties

    /// The scene the owning entity is added to.
    var scene: GameScene? { (entity as? Entity)?.scene }
}

The RemoveComponent is a child of my Component and not GKComponent so I can use the scene property in didAddToEntity() method.

The Component System

In my game scenes where I define my different component systems, I've setup a dedicated GKComponentSystem which only has the purpose of handling removals of entities from the game.

class GameScene: SKScene {
  // MARK: Component Systems

  /// Component System to remove entities from the game.
  let removeComponentSystem = GKComponentSystem(componentClass: RemoveComponent.self)
}

In the lifecycle of SKScene 1 didFinishUpdate() is the last method called just before the wait for the next frame begins. That method should be a great choice for us to use for the removal of our objects from the game.

At this time everything in the game is done executing during this update loop, so we end this cycle by batch removing all objects flagged for removal.

override func didFinishUpdate() {
  super.didFinishUpdate()

  // Remove entities flagged for removal.
  for component in removeComponentSystem.components {
    if let entity = component.entity as? Entity {
      // Remove the entity and it's resources.
      remove(entity: entity)
    }
  }
}

When iteration through the remove component system to get a reference for each entity that has the RemoveCompenent added, we call a remove method that has the actual logic to remove each entity from the game world.

A remove() method would most likely be unique for each game project. For me, I usually end up with something like this.

/// Removes an entity from the game world.
func remove(entity: Entity) {
  // Remove sprite component as that removes the nodes from the scene.
  entity.removeComponent(ofType: SpriteComponent.self)

  // Remove the reference from the entities set.
  entities.remove(entity)
}

I use a GKComponent that handles the sprite and it knows how to visually remove itself from the game world in the willRemoveFromEntity() method that is automatically called when a component is removed.

I keep a set with references to all entities currently in the game world, which I remove the entity from.

Using the Remove Component

That's basically it; the only thing that remains is to actually start using the RemoveComponent when we need to flag an object for removal.

All we have to do to remove an object from the game is to add the RemoveComponent to the entity.

Let's say that we have a bullet in the scene and we want to remove it when it hits its target. Then we simply can add the RemoveComponent in the contactDidBegin method.

func contactDidBegin(with other: GKEntity, contact: SKPhysicsContact) {
  // Flag the bullet for removal.
  addComponent(RemoveComponent())
}

Or we might have a particle emitter with a lifetime of 1.5 seconds that we want to remove when done playing. Then we could add the RemoveComponent by using an SKAction.

// remove emitter when done
let wait = SKAction.wait(forDuration: 1.5)
let remove = SKAction.run { [weak self] in
    self?.addComponent(RemoveComponent())
}

run(SKAction.sequence([wait, remove]))

We use the run SKAction and a sequence to add the RemoveComponent after the specified time has elapsed.

Where to Next

With this structure in place where we have a separate system that tracks objects for removal, it would be interesting to keep building on this concept to separate some of this logic away from the game loop.

For instance, we could potentially see if it was possible to use a background thread to iterate through the component system to get more time left on the main thread used by the game for a performance increase.

But that's a project for another day.

References

  1. SKScene Lifecycle. Responding to Frame-Cycle Events.

Discuss this article

The conversation has just started. Comments? Thoughts?

If you'd like to discuss any of the topics covered in this article, then head over and hang out on Discord.

You can also get in touch with me, and keep up with what I'm up to, on Twitter or Mastodon.

Sign up to the newsletter to get occasional emails about my game development.