GameplayKit Tutorial: Entity-Component System, Agents, Goals, and Behaviors

In this GameplayKit tutorial, you will learn how to create flexible and scalable games by using the Entity-Component system with Agents, Goals and Behaviors. By Ryan Ackermann.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Spawning The Monsters

This game is ready for some monsters! Let’s modify the game so you can spawn Quirk monsters.

Right-click your Entities group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file Quirk and click Create.

Open Quirk.swift and replace the contents with the following:

import SpriteKit
import GameplayKit

class Quirk: GKEntity {

  init(team: Team) {
    super.init()
    let texture = SKTexture(imageNamed: "quirk\(team.rawValue)")
    let spriteComponent = SpriteComponent(texture: texture)
    addComponent(spriteComponent)
    addComponent(TeamComponent(team: team))
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }
}

This is very similar to how you set up the castle entity. Here you set the texture according to the team and add the sprite component to the entity. Additionally you also add a team component to complete all this entity needs.

Now it’s time to create an instance of the Quirk entity. Last time, you created the Castle entity directly in GameScene, but this time you’ll move the code to spawn a quirk monster into EntityManager.

To do this, switch to EntityManager.swift and add this method to the bottom of the class:

func spawnQuirk(team: Team) {
  // 1
  guard let teamEntity = castle(for: team),
    let teamCastleComponent = teamEntity.component(ofType: CastleComponent.self),
    let teamSpriteComponent = teamEntity.component(ofType: SpriteComponent.self) else {
      return
  }

  // 2
  if teamCastleComponent.coins < costQuirk {
    return
  }
  teamCastleComponent.coins -= costQuirk
  scene.run(SoundManager.sharedInstance.soundSpawn)

  // 3
  let monster = Quirk(team: team)
  if let spriteComponent = monster.component(ofType: SpriteComponent.self) {
    spriteComponent.node.position = CGPoint(x: teamSpriteComponent.node.position.x, y: CGFloat.random(min: scene.size.height * 0.25, max: scene.size.height * 0.75))
    spriteComponent.node.zPosition = 2
  }
  add(monster)
}

Let's review this section by section:

  1. Monsters should be spawned near their team's castle. To do this, you need the position of the castle's sprite, so this is some code to look up that information in a dynamic way.
  2. This checks to see if there are enough coins to spawn the monster, and if so subtracts the appropriate coins and plays a sound.
  3. This is the code to create a Quirk entity and position it near the castle (at a random y-value).

Finally, switch to GameScene.swift and add this to the end of quirkPressed():

entityManager.spawnQuirk(team: .team1)

Build and run. You can now tap the Quirk button to spawn some monsters!

005_Quirks

Agents, Goals, and Behaviors

So far, the quirk monsters are just sitting right there doing nothing. This game needs movement!

Luckily, GameplayKit comes with a set of classes collectively known as "agents, goals, and behaviors" that makes moving objects in your game in complex ways super easy. Here's how it works:

  • GKAgent2D is a subclass of GKComponent that handles moving objects in your game. You can set different properties on it like max speed, acceleration, and so on, and the GKBehavior to use.
  • GKBehavior is a class that contains a set of GKGoals, representing how you would like your objects to move.
  • GKGoal represents a movement goal you might have for your agents - for example to move toward another agent.

So basically, you configure these objects and add the GKAgent component to your class, and GameplayKit will move everything for you from there!

Note: There is one caveat: GKAgent2D doesn't move your sprites directly, it just updates its own position appropriately. You need to write a bit of glue code to match up the sprite position with the GKAgent position.

Let's start by creating the behavior and goals. Right-click your Components group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file MoveBehavior and click Create.

Open MoveBehavior.swift and replace the contents with the following:

import GameplayKit
import SpriteKit

// 1
class MoveBehavior: GKBehavior {

  init(targetSpeed: Float, seek: GKAgent, avoid: [GKAgent]) {
    super.init()
    // 2
    if targetSpeed > 0 {
      // 3
      setWeight(0.1, for: GKGoal(toReachTargetSpeed: targetSpeed))
      // 4
      setWeight(0.5, for: GKGoal(toSeekAgent: seek))
      // 5
      setWeight(1.0, for: GKGoal(toAvoid: avoid, maxPredictionTime: 1.0))
    }
  }
}

There's a lot of new stuff here, so let's review this section by section:

  1. You create a GKBehavior subclass here so you can easily configure a set of movement goals.
  2. If the speed is less than 0, don't set any goals as the agent should not move.
  3. To add a goal to your behavior, you use the setWeight(_:for:) method. This allows you to specify a goal, along with a weight of how important it is - larger weight values take priority. In this instance, you set a low priority goal for the agent to reach the target speed.
  4. Here you set a medium priority goal for the agent to move toward another agent. You will use this to make your monsters move toward the closest enemy.
  5. Here you set a high priority goal to avoid colliding with a group of other agents. You will use this to make your monsters stay away from their allies so they are nicely spread out.

Now that you've created your behavior and goals, you can set up your agent. Right-click your Components group, select New File.., select the iOS/Source/Swift File template, and click Next. Name the new file MoveComponent and click Create.

Open MoveComponent.swift and replace the contents with the following:

import SpriteKit
import GameplayKit

// 1
class MoveComponent: GKAgent2D, GKAgentDelegate {

  // 2
  let entityManager: EntityManager

  // 3
  init(maxSpeed: Float, maxAcceleration: Float, radius: Float, entityManager: EntityManager) {
    self.entityManager = entityManager
    super.init()
    delegate = self
    self.maxSpeed = maxSpeed
    self.maxAcceleration = maxAcceleration
    self.radius = radius
    print(self.mass)
    self.mass = 0.01
  }
  
  required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
  }

  // 4
  func agentWillUpdate(_ agent: GKAgent) {
    guard let spriteComponent = entity?.component(ofType: SpriteComponent.self) else {
      return
    }

    position = float2(spriteComponent.node.position)
  }

  // 5
  func agentDidUpdate(_ agent: GKAgent) {
    guard let spriteComponent = entity?.component(ofType: SpriteComponent.self) else {
      return
    }

    spriteComponent.node.position = CGPoint(position)
  }
}

There's lots of new stuff here as well, so let's review this section by section:

  1. Remember that GKAgent2D is a subclass of GKComponent. You subclass it here so customize its functionality. Also, you implement GKAgentDelegate - this is how you'll match up the position of the sprite with the agent's position.
  2. You'll need a reference to the entityManger so you can access the other entities in the game. For example, you need to know about your closest enemy (so you can seek to it) and your full list of allies (so you can spread apart from them).
  3. GKAgent2D has various properties like max speed, acceleration, and so on. Here you configure them based on passed in parameters. You also set this class as its own delegate, and make the mass very small so objects respond to direction changes more easily.
  4. Before the agent updates its position, you set the position of the agent to the sprite component's position. This is so that agents will be positioned in the correct spot to start. Note there's some funky conversions going on here - GameplayKit uses float2 instead of CGPoint, gah!
  5. Similarly, after the agent updates its position agentDidUpdate(_:) is called. You set the sprite's position to match the agent's position.

You still have a bit more to do in this file, but first you need to add some helper methods. Start by opening EntityManager.swift and add these new methods:

func entities(for team: Team) -> [GKEntity] {
  return entities.flatMap{ entity in
    if let teamComponent = entity.component(ofType: TeamComponent.self) {
      if teamComponent.team == team {
        return entity
      }
    }
    return nil
  }
}

func moveComponents(for team: Team) -> [MoveComponent] {
  let entitiesToMove = entities(for: team)
  var moveComponents = [MoveComponent]()
  for entity in entitiesToMove {
    if let moveComponent = entity.component(ofType: MoveComponent.self) {
      moveComponents.append(moveComponent)
    }
  }
  return moveComponents
}

entities(for:) returns all entities for a particular team, and moveComponents(for:) returns all move components for a particular team. You'll need these shortly.

Switch back to MoveComponent.swift and add this new method:

func closestMoveComponent(for team: Team) -> GKAgent2D? {

  var closestMoveComponent: MoveComponent? = nil
  var closestDistance = CGFloat(0)

  let enemyMoveComponents = entityManager.moveComponents(for: team)
  for enemyMoveComponent in enemyMoveComponents {
    let distance = (CGPoint(enemyMoveComponent.position) - CGPoint(position)).length()
    if closestMoveComponent == nil || distance < closestDistance {
      closestMoveComponent = enemyMoveComponent
      closestDistance = distance
    }
  }
  return closestMoveComponent
  
}

This is some code to find the closest move component on a particular team from the current move component. You will use this to find the closest enemy now.

Add this new method to the bottom of the class:

override func update(deltaTime seconds: TimeInterval) {
  super.update(deltaTime: seconds)

  // 1
  guard let entity = entity,
    let teamComponent = entity.component(ofType: TeamComponent.self) else {
      return
  }

  // 2
  guard let enemyMoveComponent = closestMoveComponent(for: teamComponent.team.oppositeTeam()) else {
    return
  }

  // 3
  let alliedMoveComponents = entityManager.moveComponents(for: teamComponent.team)

  // 4
  behavior = MoveBehavior(targetSpeed: maxSpeed, seek: enemyMoveComponent, avoid: alliedMoveComponents)
}

This is the update loop that puts it all together.

  1. Here you find the team component for the current entity.
  2. Here you use the helper method you wrote to find the closest enemy.
  3. Here you use the helper method you wrote to find all your allies move components.
  4. Finally, you reset the behavior with the updated values.

Almost done; just a few cleanup items to do. Open EntityManager.swift and update the line that sets up the componentSystems property as follows:

lazy var componentSystems: [GKComponentSystem] = {
  let castleSystem = GKComponentSystem(componentClass: CastleComponent.self)
  let moveSystem = GKComponentSystem(componentClass: MoveComponent.self)
  return [castleSystem, moveSystem]
}()

Remember, this is necessary so that your update(_:) method gets called on your new MoveComponent.

Next open Quirk.swift and modify your initializer to take the entityManager as a parameter:

init(team: Team, entityManager: EntityManager) {

Then add this to the bottom of init(team:entityManager:):

addComponent(MoveComponent(maxSpeed: 150, maxAcceleration: 5, radius: Float(texture.size().width * 0.3), entityManager: entityManager))

This creates your move component with some values that work well for the quick Quirk monster.

You need a move component for the castle too - this way they can be one of the agents considered for the "closest possible enemy". To do this, open Castle.swift and modify your initializer to take the entityManager as a parameter:

init(imageName: String, team: Team, entityManager: EntityManager) {

Then add this to the bottom of init(imageName:team:entityManager:):

addComponent(MoveComponent(maxSpeed: 0, maxAcceleration: 0, radius: Float(spriteComponent.node.size.width / 2), entityManager: entityManager))

Finally, move to EntityManager.swift and inside spawnQuirk(team:), modify the line that creates the Quirk instance as follows:

let monster = Quirk(team: team, entityManager: self)

Also open GameScene.swift and modify the line in didMove(to:) that creates the humanCastle:

let humanCastle = Castle(imageName: "castle1_atk", team: .team1, entityManager: entityManager)

And similarly for aiCastle:

let aiCastle = Castle(imageName: "castle2_atk", team: .team2, entityManager: entityManager)

Build and run, and enjoy your moving monsters:

006_MovingMonsters

Congratulations! At this point you have a good understanding of how to use the new Entity-Component system in GameplayKit, along with using Agents, Goals, and Behaviors for movement.

Contributors

Jairo A. Cepeda

Tech Editor

Michael Briscoe

Final Pass Editor

Tammy Coron

Team Lead

Over 300 content creators. Join our team.