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.
The three random source classes provided by the GameplayKit framework are
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.
The second random distribution that we'll use in the game will come into play when handling the rule system-based respawn behavior.
You will notice that for the
You will also see that both the
Finally, find and replace the
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 theGKRandom
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
The two random distribution classes in GameplayKit are GKMersenneTwisterRandomSource
object's seed and use it to create a new source.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
In the initialSpawn
method.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
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.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.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
Rule systems, represented by the if
and switch
statements.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.
Post a Comment