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 GKARC4RandomSource
, GKLinearCongruentialRandomSource
, 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.
var initialSpawnDistribution = GKGaussianDistribution(randomSource: GKARC4RandomSource(), lowestValue: 0, highestValue: 2) var respawnDistribution = GKShuffledDistribution(randomSource: GKARC4RandomSource(), lowestValue: 0, highestValue: 2)
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:
let respawnFactor = arc4random() % 3 // Will produce a value between 0 and 2 (inclusive)
with the following:
let respawnFactor = self.initialSpawnDistribution.nextInt()
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.
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 aGKRuleSystem
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:
var ruleSystem = GKRuleSystem()
Next, add the following code snippet to the didMoveToView(_:)
method:
let playerDistanceRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in if let value = system.state["spawnPoint"] as? NSValue { let point = value.CGPointValue() let xDistance = abs(point.x - self.playerNode.position.x) let yDistance = abs(point.y - self.playerNode.position.y) let totalDistance = sqrt((xDistance*xDistance) + (yDistance*yDistance)) if totalDistance <= 200 { return true } else { return false } } else { return false } }) { (system: GKRuleSystem) -> Void in system.assertFact("spawnEnemy") } let nodeCountRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in if self.children.count <= 50 { return true } else { return false } }) { (system: GKRuleSystem) -> Void in system.assertFact("shouldSpawn", grade: 0.5) } let nodePresentRule = GKRule(blockPredicate: { (system: GKRuleSystem) -> Bool in if let value = system.state["spawnPoint"] as? NSValue where self.nodesAtPoint(value.CGPointValue()).count == 0 { return true } else { return false } }) { (system: GKRuleSystem) -> Void in let grade = system.gradeForFact("shouldSpawn") system.assertFact("shouldSpawn", grade: (grade + 0.5)) } self.ruleSystem.addRulesFromArray([playerDistanceRule, nodeCountRule, nodePresentRule])
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:
func respawn() { let endNode = GKGraphNode2D(point: float2(x: 2048.0, y: 2048.0)) self.graph.connectNodeUsingObstacles(endNode) for point in self.spawnPoints { self.ruleSystem.reset() self.ruleSystem.state["spawnPoint"] = NSValue(CGPoint: point) self.ruleSystem.evaluate() if self.ruleSystem.gradeForFact("shouldSpawn") == 1.0 { var respawnFactor = self.respawnDistribution.nextInt() if self.ruleSystem.gradeForFact("spawnEnemy") == 1.0 { respawnFactor = self.initialSpawnDistribution.nextInt() } var node: SKShapeNode? = nil switch respawnFactor { case 0: node = PointsNode(circleOfRadius: 25) node!.physicsBody = SKPhysicsBody(circleOfRadius: 25) node!.fillColor = UIColor.greenColor() case 1: node = RedEnemyNode(circleOfRadius: 75) node!.physicsBody = SKPhysicsBody(circleOfRadius: 75) node!.fillColor = UIColor.redColor() case 2: node = YellowEnemyNode(circleOfRadius: 50) node!.physicsBody = SKPhysicsBody(circleOfRadius: 50) node!.fillColor = UIColor.yellowColor() default: break } if let entity = node?.valueForKey("entity") as? GKEntity, let agent = node?.valueForKey("agent") as? GKAgent2D where respawnFactor != 0 { entity.addComponent(agent) agent.delegate = node as? ContactNode agent.position = float2(x: Float(point.x), y: Float(point.y)) agents.append(agent) let startNode = GKGraphNode2D(point: agent.position) self.graph.connectNodeUsingObstacles(startNode) let pathNodes = self.graph.findPathFromNode(startNode, toNode: endNode) as! [GKGraphNode2D] if !pathNodes.isEmpty { let path = GKPath(graphNodes: pathNodes, radius: 1.0) let followPath = GKGoal(toFollowPath: path, maxPredictionTime: 1.0, forward: true) let stayOnPath = GKGoal(toStayOnPath: path, maxPredictionTime: 1.0) let behavior = GKBehavior(goals: [followPath, stayOnPath]) agent.behavior = behavior } self.graph.removeNodes([startNode]) agent.mass = 0.01 agent.maxSpeed = 50 agent.maxAcceleration = 1000 } node!.position = point node!.strokeColor = UIColor.clearColor() node!.physicsBody!.contactTestBitMask = 1 self.addChild(node!) } } self.graph.removeNodes([endNode]) }
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 anNSValue
object, because theCGPoint
data type does not conform to Swift'sAnyObject
protocol and cannot be assigned to thisNSMutableDictionary
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 ourspawnFactor
.
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.
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.