An Introduction to GameplayKit: Part 2

This is the second part of An Introduction to GameplayKit. If you haven't yet gone through the first part, then I recommend reading that tutorial first before continuing with this one.

Introduction

In this tutorial, I am going to teach you about two more features of the GameplayKit framework you can take advantage of:

  • agents, goals, and behaviors
  • pathfinding

By utilizing agents, goals, and behaviors, we are going to build in some basic artificial intelligence (AI) into the game that we started in the first part of this series. The AI will enable our red and yellow enemy dots to target and move towards our blue player dot. We are also going to implement pathfinding to extend on this AI to navigate around obstacles.

For this tutorial, you can use your copy of the completed project from the first part of this series or download a fresh copy of the source code from GitHub.

1. Agents, Goals, and Behaviors

In GameplayKit, agents, goals and behaviors are used in combination with each other to define how different objects move in relation to each other throughout your scene. For a single object (or SKShapeNode in our game), you begin by creating an agent, represented by the GKAgent class. However, for 2D games, like ours, we need to use the concrete GKAgent2D class.

The GKAgent class is a subclass of GKComponent. This means that your game needs to be using an entity- and component-based structure as I showed you in the first tutorial of this series.

Agents represent an object's position, size, and velocity. You then add a behavior, represented by the GKBehaviour class, to this agent. Finally, you create a set of goals, represented by the GKGoal class, and add them to the behavior object. Goals can be used to create many different gameplay elements, for example:

  • moving towards an agent
  • moving away from an agent
  • grouping close together with other agents
  • wandering around a specific position

Your behavior object monitors and calculates all of the goals that you add to it and then relays this data back to the agent. Let's see how this works in practice.

Open your Xcode project and navigate to PlayerNode.swift. We first need to make sure the PlayerNode class conforms to the GKAgentDelegate protocol.

Next, add the following code block to the PlayerNode class.

We start by adding a property to the PlayerNode class so that we always have a reference to the current player's agent object. Next, we implement the two methods of the GKAgentDelegate protocol. By implementing these methods, we ensure that the player dot displayed on screen will always mirror the changes that GameplayKit makes.

The agentWillUpdate(_:) method is called just before GameplayKit looks through the behavior and goals of that agent to determine where it should move. Likewise, the agentDidUpdate(_:) method is called straight after GameplayKit has completed this process.

Our implementation of these two methods ensures that the node we see on screen reflects the changes GameplayKit makes and that GameplayKit uses the last position of the node when performing its calculations.

Next, open ContactNode.swift and replace the file's contents with the following implementation:

By implementing the GKAgentDelegate protocol in the ContactNode class, we allow for all of the other dots in our game to be up to date with GameplayKit as well as our player dot.

It's now time to set up the behaviors and goals. To make this work, we need to take care of three things:

  • Add the player node's agent to its entity and set its delegate.
  • Configure agents, behaviors, and goals for all of our enemy dots.
  • Update all of these agents at the correct time.

Firstly, open GameScene.swift and, at the end of the didMoveToView(_:) method, add the following two lines of code:

With these two lines of code, we add the agent as a component and set the agent's delegate to be the node itself.

Next, replace the implementation of the initialSpawn method with the following implementation:

The most important code that we've added is located in the if statement that follows the switch statement. Let's go through this code line by line:

  • We first add the agent to the entity as a component and configure its delegate.
  • Next, we assign the agent's position and add the agent to a stored array, agents. We'll add this property to the GameScene class in a moment.
  • We then create a GKBehavior object with a single GKGoal to target the current player's agent. The weight parameter in this initializer is used to determine which goals should take precedence over others. For example, imagine that you have a goal to target a particular agent and a goal to move away from another agent, but you want the targeting goal to take preference. In this case, you could give the targeting goal a weight of 1 and the moving away goal a weight of 0.5. This behavior is then assigned to the enemy node's agent.
  • Lastly, we configure the massmaxSpeed, and maxAcceleration properties of the agent. These affect how fast the objects can move and turn. Feel free to play around with these values and see how it affects the movement of the enemy dots.

Next, add the following two properties to the GameScene class:

The agents array will be used to keep a reference to the enemy agents in the scene. The lastUpdateTime property will be used to calculate the time that has passed since the scene was last updated.

Finally, replace the implementation of the update(_:) method of the GameScene class with the following implementation:

In the update(_:) method, we calculate the time that has passed since the last scene update and then update the agents with that value.

Build and run your app, and begin moving around the scene. You will see that the enemy dots will slowly begin moving towards you.

Targeting enemies

As you can see, while the enemy dots do target the current player, they do not navigate around the white barriers, instead they try to move through them. Let's make the enemies a bit smarter with pathfinding.

2. Pathfinding

With the GameplayKit framework, you can add complex pathfinding to your game by combining physics bodies with GameplayKit classes and methods. For our game, we are going to set it up so that the enemy dots will target the player dot and at the same time navigate around obstacles.

Pathfinding in GameplayKit begins with creating a graph of your scene. This graph is a collection of individual locations, also referred to as nodes, and connections between these locations. These connections define how a particular object can move from one location to another. A graph can model the available paths in your scene in one of three ways:

  • A continuous space containing obstacles: This graph model allows for smooth paths around obstacles from one location to another. For this model, the GKObstacleGraph class is used for the graph, the GKPolygonObstacle class for obstacles, and the GKGraphNode2D class for nodes (locations).
  • A simple 2D grid: In this case, valid locations can only be those with integer coordinates. This graph model is useful when your scene has a distinct grid layout and you do not need smooth paths. When using this model, objects can only move horizontally or vertically in a single direction at any one time. For this model, the GKGridGraph class is used for the graph and the GKGridGraphNode class for nodes.
  • A collection of locations and the connections between them: This is the most generic graph model and is recommended for cases where objects move between distinct spaces, but their specific location within that space is not essential to the gameplay. For this model, the GKGraph class is used for the graph and the GKGraphNode class for nodes.

Because we want the player dot in our game to navigate around the white barriers, we are going to use the GKObstacleGraph class to create a graph of our scene. To begin, replace the spawnPoints property in the GameScene class with the following:

The spawnPoints array contains some altered spawn locations for the purposes of this tutorial. This is because currently GameplayKit can only calculate paths between objects that are relatively close to each other.

Due to the large default distance between dots in this game, a couple of new spawn points must be added to illustrate pathfinding. Note that we also declare a graph property of type GKObstacleGraph to keep a reference to the graph we will create.

Next, add the following two lines of code at the start of the didMoveToView(_:) method:

In the first line, we create an array of obstacles from the physics bodies in the scene. We then create the graph object using these obstacles. The bufferRadius parameter in this initializer can be used to force objects to not come within a certain distance of these obstacles. These lines need to be added at the start of the didMoveToView(_:) method, because the graph we create is needed by the time the initialSpawn method is called.

Finally, replace the initialSpawn method with the following implementation:

We begin the method by creating a GKGraphNode2D object with the default player spawn coordinates. Next, we connect this node to the graph so that it can be used when finding paths.

Most of the initialSpawn method remains unchanged. I have added some comments to show you where the pathfinding portion of the code is located in the first if statement. Let's go through this code step by step:

  • We create another GKGraphNode2D instance and connect this to the graph.
  • We create a series of nodes which make up a path by calling the findPathFromNode(_:toNode:) method on our graph.
  • If a series of path nodes has been created successfully, we then create a path from them. The radius parameter works similar to the bufferRadius parameter from before and defines how much an object can move away from the created path.
  • We create two GKGoal objects, one for following the path and another for staying on the path. The maxPredictionTime parameter allows for the goal to calculate as best it can ahead of time whether anything is going to interrupt the object from following/staying on that particular path.
  • Lastly, we create a new behavior with these two goals and assign this to the agent.

You will also notice that we remove the nodes we create from the graph once we are finished with them. This is a good practice to follow as it ensures that the nodes you have created do not interfere with any other pathfinding calculations later on.

Build and run your app one last time, and you will see two dots spawn very close to you and begin moving towards you. You may have to run the game multiple times if they both spawn as green dots.

Pathfinding enemies

Important!

In this tutorial, we used GameplayKit's pathfinding feature to enable enemy dots to target the player dot around obstacles. Note that this was just for a practical example of pathfinding.

For an actual production game, it would be best to implement this functionality by combining the player targeting goal from earlier in this tutorial with an obstacle-avoiding goal created with the init(toAvoidObstacles:maxPredictionTime:) convenience method, which you can read more about in the GKGoal Class Reference.

Conclusion

In this tutorial, I showed you how you can utilize agents, goals, and behaviors in games that have an entity-component structure. While we only created three goals in this tutorial, there are many more available to you, which you can read more about in the GKGoal Class Reference.

I also showed you how to implement some advanced pathfinding in your game by creating a graph, a set of obstacles, and goals to follow these paths.

As you can see, there is a vast amount of functionality made available to you through the GameplayKit framework. In the third and final part of this series, I will teach you about GameplayKit's random value generators and how to create your own rule system to introduce some fuzzy logic into your game.

As always, please be sure to leave your comments and feedback below.

Tags:

Comments

Related Articles