An Introduction to GameplayKit: Part 3

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

Introduction

In this third and final tutorial, I am going to teach you about two more features you can use in your own games:

  • random value generators
  • rule systems

In this tutorial, we will first use one of GameplayKit's random value generators to optimize our initial enemy spawning algorithm. We will then implement a basic rule system in combination with another random distribution to handle the respawning behavior of enemies.

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

1. Random Value Generators

Random values can be generated in GameplayKit by using any class that conforms to the GKRandom protocol. GameplayKit provides five classes that conform to this protocol. These classes contains three random sources and two random distributions. The main difference between random sources and random distributions is that distributions use a random source to produce values within a specific range and can manipulate the random value output in various other ways.

The aforementioned classes are provided by the framework so that you can find the right balance between performance and randomness for your game. Some random value generating algorithms are more complex than others and consequently impact performance.

For example, if you need a random number generated every frame (sixty times per second), then it would be best to use one of the faster algorithms. In contrast, if you are only infrequently generating a random value, you could use a more complex algorithm in order to produce better results.

The three random source classes provided by the GameplayKit framework are GKARC4RandomSourceGKLinearCongruentialRandomSource, and GKMersenneTwisterRandomSource.

GKARC4RandomSource

This class uses the ARC4 algorithm and is suitable for most purposes. This algorithm works by producing a series of random numbers based on a seed. You can initialize a GKARC4RandomSource with a specific seed if you need to replicate random behavior from another part of your game. An existing source's seed can be retrieved from its seed read-only property.

GKLinearCongruentialRandomSource

This random source class uses the basic linear congruential generator algorithm. This algorithm is more efficient and performs better than the ARC4 algorithm, but it also generates values that are less random. You can fetch a GKLinearCongruentialRandomSource object's seed and create a new source with it in the same manner as a GKARC4RandomSource object.

GKMersenneTwisterRandomSource

This class uses the Mersenne Twister algorithm and generates the most random results, but it is also the least efficient. Just like the other two random source classes, you can retrieve a GKMersenneTwisterRandomSource object's seed and use it to create a new source.

The two random distribution classes in GameplayKit are GKGaussianDistribution and GKShuffledDistribution.

GKGaussianDistribution

This distribution type ensures that the generated random values follow a Gaussian distribution—also known as a normal distribution. This means that the majority of the generated values will fall in the middle of the range you specify.

For example, if you set up a GKGaussianDistribution object with a minimum value of 1, a maximum value of 10, and a standard deviation of 1, approximately 69% of the results would be either 4, 5, or 6. I will explain this distribution in more detail when we add one to our game later in this tutorial.

GKShuffledDistribution

This class can be used to make sure that random values are uniformly distributed across the specified range. For example, if you generate values between 1 and 10, and a 4 is generated, another 4 will not be generated until all of the other numbers between 1 and 10 have also been generated.

It's now time to put all this in practice. We are going to be adding two random distributions to our game. Open your project in Xcode and go to GameScene.swift. The first random distribution we'll add is a GKGaussianDistribution. Later, we'll also add a GKShuffledDistribution. Add the following two properties to the GameScene class.

In this snippet, we create two distributions with a minimum value of 0 and a maximum value of 2. For the GKGaussianDistribution, the mean and deviation are automatically calculated according to the following equations:

  • mean = (maximum - minimum) / 2
  • deviation = (maximum - minimum) / 6

The mean of a Gaussian distribution is its midpoint and the deviation is used to calculate what percentage of values should be within a certain range from the mean. The percentage of values within a certain range is:

  • 68.27% within 1 deviation from the mean
  • 95% within 2 deviations from the mean
  • 100% within 3 deviations from the mean

This means that approximately 69% of the generated values should be equal to 1. This will result in more red dots in proportion to green and yellow dots. To make this work, we need to update the initialSpawn method.

In the for loop, replace the following line:

with the following:

The nextInt method can be called on any object that conforms to the GKRandom protocol and will return a random value based on the source and, if applicable, the distribution that you are using.

Build and run your app, and move around the map. You should see a lot more red dots in comparison to both green and yellow dots.

Large proportion of red dots

The second random distribution that we'll use in the game will come into play when handling the rule system-based respawn behavior.

2. Rule Systems

GameplayKit rule systems are used to better organize conditional logic within your game and also introduce fuzzy logic. By introducing fuzzy logic, you can make entities within your game make decisions based on a range of different rules and variables, such as player health, current enemy count, and distance to the enemy. This can be very advantageous when compared to simple if and switch statements.

Rule systems, represented by the GKRuleSystem class, have three key parts to them:

  • Agenda. This is the set of rules that have been added to the rule system. By default, these rules are evaluated in the order that they are added to the rule system. You can change the salience property of any rule to specify when you want it to be evaluated.
  • State Information. The state property of a GKRuleSystem object is a dictionary, which you can add any data to, including custom object types. This data can then be used by the rules of the rule system when returning the result.
  • Facts. Facts within a rule system represent the conclusions drawn from the evaluation of rules. A fact can also be represented by any object type within your game. Each fact also has a corresponding membership grade, which is a value between 0.0 and 1.0. This membership grade represents the inclusion or presence of the fact within the rule system.

Rules themselves, represented by the GKRule class, have two major components:

  • Predicate. This part of the rule returns a boolean value, indicating whether or not the requirements of the rule have been met. A rule's predicate can be created by using an NSPredicate object or, as we will do in this tutorial, a block of code.
  • Action. When the rule's predicate returns true, it's action is executed. This action is a block of code where you can perform any logic if the rule's requirements have been met. This is where you generally assert (add) or retract (remove) facts within the parent rule system.

Let's see how all this works in practice. For our rule system, we are going to create three rules that look at:

  • the distance from the spawn point to the player. If this value is relatively small, we will make the game more likely to spawn red enemies.
  • the current node count of the scene. If this is too high, we don't want any more dots being added to the scene.
  • whether or not a dot is already present at the spawn point. If there isn't, then we want to proceed to spawn a dot here.

First, add the following property to the GameScene class:

Next, add the following code snippet to the didMoveToView(_:) method:

With this code, we create three GKRule objects and add them to the rule system. The rules assert a particular fact within their action block. If you do not provide a grade value and just call the assertFact(_:) method, as we do with the playerDistanceRule, the fact is given a default grade of 1.0.

You will notice that for the nodeCountRule we only assert the "shouldSpawn" fact with a grade of 0.5. The nodePresentRule then asserts this same fact and adds on a grade value of 0.5. This is done so that when we check the fact later on, a grade value of 1.0 means that both rules have been satisfied.

You will also see that both the playerDistanceRule and nodePresentRule access the "spawnPoint" value of the rule system's state dictionary. We will assign this value before evaluating the rule system.

Finally, find and replace the respawn method in the GameScene class with the following implementation:

This method will be called once every second and is very similar to the initialSpawn method. There are a number of important differences in the for loop though.

  • We first reset the rule system by calling its reset method. This needs to be done when a rule system is sequentially evaluated. This removes all asserted facts and related data to ensure no information is left over from the previous evaluation that might interfere with the next.
  • We then assign the spawn point to the rule system's state dictionary. We use an NSValue object, because the CGPoint data type does not conform to Swift's AnyObject protocol and cannot be assigned to this NSMutableDictionary property.
  • We evaluate the rule system by calling its evaluate method.
  • We then retrieve the rule system's membership grade for the "shouldSpawn" fact. If this is equal to 1, we continue with respawning the dot.
  • Finally, we check the rule system's grade for the "spawnEnemy" fact and, if equal to 1, use the normally distributed random generator to create our spawnFactor.

The rest of the respawn method is the same as the initialSpawn method. Build and run your game one final time. Even without moving around, you will see new dots spawn when the necessary conditions are met.

Respawning dots

Conclusion

In this series on GameplayKit, you have learned a lot. Let's briefly summarize what we've covered.

  • Entities and Components
  • State Machines
  • Agents, Goals, and Behaviors
  • Pathfinding
  • Random Value Generators
  • Rule Systems

GameplayKit is an important addition to iOS 9 and OS X El Capitan. It eliminates a lot of the complexities of game development. I hope that this series has motivated you to experiment more with the framework and discover what it is capable of.

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

Tags:

Comments

Related Articles