The SpriteKit Framework is a general-purpose framework for drawing shapes, particles, text, images, and video in two dimensions. SpriteKit offers a simple programming interface to make graphics-intensive 2 dimensional games. SpriteKit works on iOS, macOS, tvOS, and watchOS, as well as integrates well with frameworks like GameplayKit and SceneKit.
https://developer.apple.com/documentation/spritekit
Documentation topics include:
The GameplayKit framework focuses on gameplay topics like AI, pathfinding, and autonomous movement (kinda like how Apple has SpriteKit, SceneKit, and Metal for a visual framework). Think of GameplayKit as an API of gameplay solutions with seven major features.
Classic Problem - In games, there are many entities and components. Say we have a tower defense game
and there are objects of say towers and archers, where does [shoot:]
, [move:]
, [isTargetable]
go?
GameObject
> Projectile
> Tower
> Archer
We can copy and paste code across archers and towers, but we now have duplicate code where you have to update. Another option is to use an inheritance model so that a higher class (say GameObject), but that also means shared functionality needs to move to a higher and higher hierarchies (to the point where a basic game object is now complicated with layers of inheritance). So what are other options? Entities and Components!
Components encapsulate singular elements of our game logic. Changing the above into components would make it have loose coupling from the hierarchy.
MoveComponent - deals with moving
ShootComponent - deals with shooting
TargetComponent - what it means to be targetable
When you create a new entity in the game, just look up what components you have available.
An example of this might be an archer that gets rooted (we can apply this affect by temporarily
removing the ‘MoveComponent’). In SwiftKit, all components are a GKComponent
.
Entities are defined through GKEntity
, the entity base class, and it has a collection of components
that you can add or remove dynamically.
Example might look like:
/* Make our archer */
GKEntity *archer = [GKEntity entity];
/* Archers can move, shoot, be targeted */
[archer addComponent: [MoveComponent component]];
[archer addComponent: [ShootComponent component]];
[archer addComponent: [TargetComponent component]];
/* Create MoveComponentSystem */
GKComponentSystem *moveSystem = [GKComponentSystem systemWithComponentClass:MoveComponent.class];
/* Add archer's MoveComponent to the System */
[moveSystem addComponent: [archer componentForClass:MoveComponent.class]]
State Machines are the backbone of many gameplay elements like Animation, AI, UI, levels, etc. An example of a state machine might be PacMan, where we have:
Chase <-> Flee
Respawn
Defeated
Another example might be animations with:
IdleAnimation <-> MoveAnimation
AttackAnimation
Or a game user interface using states to show what UI elements to show and what other game elements are running:
Menu
Playing
Paused
GameOverr
Different types of state machines include:
GKStateMachine
GKStateMachine
is the general purpose finite state machine, which can only have a single current state, but
can have all possible states too. [enterState
causes state transition and checks if the transition is valid.
Calls [exit]
on previous, [enter]
on next state and updates currentState
GKState
GKState
is an abstract class that implements logic in Enter/Exit/Update. You would subclass GKState to define
each state and the rules for allowed transitions between states. An instance of GKStateMachine
would be used
to manage a machine that combines several states. The idea behind this system is that it provides a way to organize
code in your game by organizing state-dependent actions into methods that run when entering a state, exiting a
state, and periodically while in a state (e.g. animation frame).
So what does agents, behaviors, and goals solve? Games need believable movements (e.g. walk around a square building, you don’t do sharp 90 degree turns when bumped into object)
Agents are a component that moves a game entity according to a set of goals and realistic constraints
Class is GKAgent
that is really just a GKComponent
.
Behaviors are made up of a group of goals that influence the movement of an agent
Class is GKBehavior
, a dictionary-like container of goals wheer you can add/remove goals, adjust weights of goals, etc.
Goals are an influence that motivates the movement of one or more agents. Class is GKGoal
.
Sample Code for Agents, Behaviors, and Goals
/* Make some goals, we want to seek the enemy, avoid obstacles, target speed */
GKGoal *seek = [GKGoal goalToSeekAgent:enemyAgent];
GKGoal *avoid = [GKGoal goalToAvoidObstacles:obstacles];
GKGoal *targetSpeed = [GKGoal goalToReachTargetSpeed:50.0f];
/* Combine goals into behavior */
GKBehavior *behavior = [GKBehavior behaviorWithGoals:@[seek,avoid,targetSpeed] andWeights:@[@1.0,@5.0,@0.5]];
/* Make an agent - add the behavior to it */
GKAgent2D *agent = [[GKAgent2D* alloc] init];
agent.behavior = behavior;
AgentDelegate is a protocol that synchronizes the state of an agent with its visual representation in
your game; defined through GKAgentDelegate
. Allows you to sync graphics, animations, physics with two callbacks:
[agentWillUpdate:]
called before updates and [agentDidUpdate:]
called after updates for say
a SpriteKit Node, a SceneKit Node, or a Render Component. Sample code looks like:
@implementation MyAgentSpriteNode
(void)agentWillUpdate:(GKAgent2D *)agent {
/* Position the agent to match our sprite */
agent.position = self.position;
agent.rotation = self.zRotation;
}
(void)agentDidUpdate:(GKAgent2D *)agent {
/* Update the sprite's position to match the agent */
self.position = agent.position;
self.zRotation = agent.rotation;
}
So why use this? For say a space shooter game, you can have projectiles attack your ship with smart pathing based off the goals that are already predefined.
Pathfinding is an issue in many games. Let’s first define a few things:
GKGraph
is the abstract graph base class. It is the container of graph nodes and dynamically allows you to
add or remove nodes, connect new nodes, find paths between nodes. There are two specializations of the GKGraph
,
which are:
A GKGraph
that works with 2D grids as class GKGridGraph
.
A GKGraph
that works with pathing around obstacles as class GKObstacleGraph
.
For advanced pathfinding, consider using GKGraphNode
, the graph node base class. Use this if you need
more advanced features (e.g. terrain type of mountains costs twice as much vs flat ground) so that you are
not just using the shortest path. Or say you have portals that are a shortcut between places.
SpriteKit Integration
You can easily generate obstacles from SKNode bounds, physics bodies, or textures.
/* Makes obstacles from sprite textures */
(NSArray*)obstaclesFromSpriteTextures:(NSArray*)sprites accuracy:(float)accuracy;
/* Makes obstacles from node bounds */
(NSArray*)obstaclesFromNodeBounds:(NSArray*)nodes;
/* Makes obstacles from node physics bodies */
(NSArray*)obstaclesFromNodePhysicsBodies:(NSArray*)nodes;
MinMax AI looks at all player moves, builds a decision tree, and maximizes potential gain while minimizing potential loss. An example of this is say tic-tac toe. You can use this for AI-controlled opponents or for suggesting moves for the human player. You might see this in say turn-based games (or any game with discrete moves). You can adjust the difficulty (e.g. how far the AI looks ahead or selecting suboptimal moves).
The GKGameModel
protocol the an abstraction fo the current game state. For a chess game, you might have:
The GKGameModelUpdate
protocol is an abstraction of a game move; implement this to describe a move in your
turn-based game so that a GKStrategist
object can plan game moves.
GKGameModel
to change stateThe GKGameModelPlayer
is an abstraction of a player; implement this to describe a player in your
turn-based game so that a GKStrategist
object can plan game moves.
GKGameModelUpdate
The GKMinmaxStrategist
class is an AI that chooses moves in turn-based games using a deterministic strategy.
The GKMonteCarloStrategist
class is an AI that chooses moves in turn-based games using a probabilistic strategy.
Example Code
/* ChessGameModel implements GKGameModel */
ChessGameModel *chessGameModel = [ChessGameModel new];
GKMinmaxStrategist *minmax = [GKMinmaxStrategist new];
minmax.gameModel = chessGameModel;
minmax.maxLookAheadDepth = 6;
/* Find the best move for the active player */
ChessGameUpdate *chessGameUpdate = [minmax bestMoveForPlayer:chessGameModel.activePlayer];
/* Apply update to the game model */
[chessGameModel applyGameModelUpdate:chessGameUpdate];
We have rand()
, but games have unique random number needs. We need:
Features include:
GKRandomSource class
GKRandomSource
is the base class for random sources.
GKRandomDistribution class
GKRandomDistribution
is the base class for random distribution; purely random.
GKRandomDistribution class
GKGaussianDistribution
is a “bell curve” distribution (leans more towards the mean)
GKShuffledDistribution class
GKShuffledDistribution
is an anti-clustering distribution that reduces or eliminates ‘runs’.
Code examples (simple usage):
/* Create a six-sided dice with its own random source */
let d6 = GKRandomDistribution.d6()
/* Get die value between 1 and 6 */
let choice = d6.nextInt()
Code examples (custom die):
/* Create a custom 256-sided die with its own random source */
let d256 = GKRandomDistribution.die(lowest:1, highest:256)
/* Get die value between 1 and 256 */
let choice = d256.nextInt()
Code examples (array shuffling)
/* Make a deck of cards */
var deck = [Ace, King, Queen, Jack, Ten]
/* Shuffle them */
deck = GKRandomSource.sharedRandom().shuffle(deck)
/* Possible result - [Jack, King, Ten, Queen, Ace] */
/* Get a random card from the deck */
let card = deck[0]
Games consist of three elements:
What is a Rule System?
Say you have a role-playing game with turn-based combat that include rules governing what happens when opposing characters move into the same space. You might have to calculate say an attack of opportunity for the monster that just landed next to your hero. These rules can quickly get complex with conditional logic statements. So what happens is that we have these systems with emergent behavior, cases where the interactions between simple entities follow simple rules, but lead to interesting patterns in the system as a whole.
Rule systems provide an abstraction that treats certain elements of game logic as data, decompose your game into functional, reusable, and extensible pieces. By incorporating fuzzy logic, rule systems can treat decisions as continuous variables instead of discrete states, leading to complex behavior from even simple combinations of rules.
Another example is binary driver AI
We separate what we should do from how we should do it
We do this by designing a rule system, which indlues two main classes:
GKRule
to represent a specific decision to be made based on external stateGKRuleSystem
which evaluates a set of rules against state data to determine a set of factsGKRule class
A GKRule
is made up of two parts, a boolean predicate and an action.
A rule’s predicate is the decision-making part; it evaluates a set of state information and returns a Boolean result.
A rule’s action is code whose execution is triggered only when the predicate produces a true result.
GameplayKit evaluates rules in the context of the rule system. Typically, a rule’s predicate tests state data
maintained by the rule system and its action either changes the system’s state or asserts a fact.
A rule does not need local state data; this allows you to design individual rules as functional units with well-defined
behavior and reuse them in any rule system.
GKRuleSystem class
A GKRuleSystem
has three key parts: an agenda of rules, state data, and facts.
The agenda is where you combine a set of rules by adding them to the agenda of a rule system object with
(addRule:
or addRulesFromArray:
methods). By default, rules are evaluated based on order (or change salience
property).
The state data is the rule system’s state
dictionary, which contains information rules can be tested against.
The state dictionary can reference anything useful to your set of rules, including strings, numbers, or other classes.
The facts represent a conclusion drawn from the evaluation of rules in a system and can be any type of object.
When a rule system evaluates its rules, the rule actions can assert a fact or retract a fact. Facts can vary
in grade to define some fuzzy logic. For example, a character moving to a space might not always trigger
an attack; you might instead trigger an attack only when the character moving is also in a vulnerable state.
For details, see: https://developer.apple.com/library/archive/documentation/General/Conceptual/GameplayKit_Guide/RuleSystems.html
Example Code:
/* Make a rule system */
GKRuleSystem* sys = [[GKRuleSystem alloc] init];
/* Getting distance and asserting facts */
float distance = sys.state[@"distance"];
[sys assertFact:@"close" grade:1.0f - distance / kBrakingDistance];
[sys assertFact:@"far" grade:distance / kBrakingDistance];
/* Grade our facts - farness and closeness */
float farness = [sys gradeForFact@"far"]
float closeness = [sys gradeForFact@"close"];
/* Derive Fuzzy acceleration */
float fuzzyAcceleration = farness - closeness;
[car applyAcceleration:fuzzyAcceleration withDeltaTime:seconds];
The tools and technology used for DemoBots include:
There are a lot of animation states for our bots. To keep these textures to a minimum, we can create some of the animations as Actions. For example, a zap action is a reference action. You create the action once, then can apply to multiple bots.
Depending on the size of your device (e.g. ipad has more pixels, iphone has less pixels), you can specify different assets depending on device. As the scene height scales, the player height scales.
The camera is a node in the scence (and has a position); this allows you to move the camera and change its position. You can add constraints (like following the hero). You can also add more constraints like if you are approaching the corner of the screen (when you get close to an edge, the camera stops following the hero).
The PlayerBot state has the following states using a GKStateMachine
:
We also use states for the game state, e.g.
https://developer.apple.com/documentation/gameplaykit
You can use the built-in simulator to simulate a phone or you can build directly to your phone. Just plug in your phone to your computer’s usb and select your iPhone during the build in Xcode.
Some keyboard shortcuts:
SKView
- an object that displays SpriteKit content - this content is provided by an SKScene
object
e.g. look at the Interface Builder, this is just a view that holds a scene objectSKScene
- the root node for all SpriteKit objects displayed in a view
e.g. you place background images, all your nodes, etc.
SKNode
- provides baseline behavior (does not actually draw)
SKSpriteNode
- a subclass of SKNode
; the basic building blocks of your game
SKAction
- an animation that is executed by a node in the scene (i.e. change a node in some way like move its
position over time, an animation that fades out an object)SKPhysicsBody
- add physics simulation to the nodeSKView SKScene (the root node) SKNode - SKSpriteNode1 - SKSpriteNode2 - SKSpriteNode3
You can create a ‘Single Page Application’ or a ‘Game’. Select a ‘Game’ and then pick ‘SpriteKit’ so that it will prefill in some defaults.
A SPA will get you a:
SKView
class)With a SPA, you will need to add in:
SKScene
)GameScene.sks
with the GameScene.swift
)Storyboards allow you to prototype and design multiple view controller views in one file. Storyboards also
let you create transitions between view controllers. View controllers are called scenes on the storyboard.
By default, you have a Main.storyboard
A delegate is a type that represents references to methods. Think of it as a ‘pointer to a function’. The idea is that you need to be able to invoke a piece of code, but that piece of code you’re going to invoke isn’t known until runtime. Delegates are useful for things like event handlers, where you do different things based on different events. So basically someone sends a request to an object, then that object forwards the request to another object (the delegate).
So what is an AppDelegate.swift
file and class: AppDelegate
do?
The AppDelegate handles application lifecycle events. That means AppDelegate controls things like
the app being launched, backgrounded, foregrounded, receiving data, etc.
A bit more in detail, the system calls the @UIApplicationMain
function and creates a singleton UIApplication
object.
Under Assets.xcassets
, you can place your images, music, etc. If you want a spritekit .atlas
, you need to
click on the +
on the bottom of Xcode, then click Add Files to <game>
(Note: just dragging files over in Finder
does not do the same, you hvae to add files this way). If for a .atlas
you see a Yellow folder, it means the
folder was not added correctly (should be blue).
An example of an asset folder with an atlas might be an animation, e.g.
BearImages.atlas
bear1.png
bear2.png
bear3.png
bear4.png
There are four tile sets you can create:
For additional details, see this talk: https://developer.apple.com/videos/play/wwdc2016/610/
https://developer.apple.com/documentation/spritekit/skscene
The GameScene.sks
file allows you to modify your SKScene
through a GUI. You can:
Anchor Point
(e.g. reference for all your assets). E.g. anchor point 0,0 is bottom leftAssets
that you dragged in from aboveSKSpriteNode
in your scene (and can then modify sprite sizes, textures, etc.Actions
. E.g. Move Action
on the sprite node that looks like a cloud
The ‘Action’ can be found in your ‘Object Library’ (where you click on the ‘+’ sign)In GameScene.swift
, we are able to access the GameScene.sks
through code (e.g. access Sprites through code).
You will see your class GameScene: SKScene
with functions like update
(called before each frame is rendered)
and you can do your custom scene setup (e.g. look at self.childNode(withName: ...) as? SKSpriteNode ...
Important functions that you might want in code might be movement functions like:
func touchDown
func touchUp
func touchMoved
The rendering loop is tied to the SKScene
object. Rendering runs only when the
scene is presented. SpriteKit only renders the scene when something changed so its efficient.
Here’s what goes on in the rendering loop each frame:
update
- make changes to nodes, evaluate nodesSKScene
evaluates actionsdidEvaluateActions
-SKScene
- simulates physicsdidSimulatePhysics
-SKScene
- applies constraintsdidApplyConstraints
didFinishUpdate
SKView
renders the sceneSKAction is an animation that is executed by a node in the scene. Actions are used to change a node in some way (e.g. its position over time, its size or scaling, its visibility, or node’s contents so that it animates over a series of textures).
SKActions can be combined together either as a group (multiple actions at once) or as a sequence (actions one after another). You can also repeat actions.
Nodes include:
To add particles, you can use SKEmitterNode
to create smoke, fire, sparks, and other particle effects.
Keep in mind that this is privately owned by SpriteKit so you cannot modify this class.
Important settings for this particle emitter include:
When you import GameplayKit
, you have access to a lot of useful pre-built functions like:
GKRandomSource.sharedRandom().nextInt(upperBound: 6)
Sets up your Scene
A physics world is the simulation space for running physics calculations; one is setup by default. In SpriteKit, you can associate a shape with each sprite for collision detection purposes; this is a physics body. One of the properties that you can set on a physics body is a category, which is a bitmask indicating the group or groups it belongs to. Example categories could be one for projectiles and one for monsters. When two physics bodies collide, you can tell what kind of sprite you’re dealing with by looking at its category. You can set a contact delegate to be notified when two physics bodies collide (and what to do if they are the categories you want).
SKPhysicsWorld
, which is the driver of the physics engine in a scene.
It allows you to configure and query the physics system.SKPhysicsBody
, which adds physics simulation to a node.You can add an SKPhysicsBody
to add physics simulation to a node. The physics body must be associated with a node
object before you can apply forces or impulses to it.
# Apply SKPhysicsBody
sprite = self.childNode(withName: "sprite") as! SKSpriteNode
sprite.physicsBody = SKPhysicsBody(rectangleOf: sprite.size)
# Apply impulse to a sprite body
sprite.physicsBody?.applyImpulse(CGVector(dx: 50, dy: 0))
Types of Physics Bodies:
This can be accessed through code with:
sprite.physicsBody?.isDynamic = true
A body’s physical properties also include:
How does this all look in code?
let spaceShipTexture = SKTexture(imageNamed: "spaceShip.png")
// Spaceship 1 with circular physics body
let circularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
circularSpaceShip.physicsBody = SKPhysicsBody(circleOfRadius: max(circularSpaceShip.size.width / 2, circularSpaceShip.size.height / 2))
// Spaceship 2 with rectangular physics body
let rectangularSpaceShip = SKSpriteNode(texture: spaceShipTexture)
rectangularSpaceShip.physicsBody = SKPhysicsBody(rectangleOf: CGSize(rectangularSpaceShip.size.width, rectangularSpaceShip.size.height))
// Spaceship 3 with polygonal physics body (using Paths)
let polygonalSpaceShip = SKSpriteNode(texture: spaceShipTexture)
let path = CGMutablePath()
path.addLines(between: [CGPoint(x: -5, y: 37), GGPoint(x: 5, y:37), ....])
path.closeSubpath()
polygonalSpaceShip.physicsBody = SKPhysicsBody(polygonFrom: path)
// Spaceship 4 with using texture's alpha channel
let texturedSpaceShip = SKSpriteNode(texture: spaceShipTexture)
texturedSpaceShip.physicsBody = SKPhysicsBody(
texture: spaceShipTexture,
size: CGSize(width: texturedSpaceShip.size.width,
height: texturedSpaceShip.size.height))
A bitmask is data used for bitwise operations. You use a mask (multiple bits in a byte) that can be set either on or off in a bitwise operation. Some of the types of bitmasks we can use are:
Say we have a bitmask of 1 (0x00000001
). Here is a bitwise shift to the left.
0x00000001 << 1 = 0x00000010 # 1 * 2^1 = 2
0x00000001 << 2 = 0x00000100 # 1 & 2^2 = 4
So how does this work in a real example?
# Ball hitting grass
ballSprite.physicsBody.collisionBitMask = 0x00000001
grassSprite.physicsBody.categoryBitMask = 0x00000001
SpriteKit does a calculation for collision and does 0x00000001 AND 0x00000001 = 1
so there is collision.
Once the ball actually hits the grass, we can take an action/do something:
SKPhysicsContactDelegate
and assign it to our Physics WorlddidBegin(_:)
function is called (thanks to SKPhysicsContactDelegate
)What happens is that:
// Adopt the class SKPhysicsContactDelegate
class GameScene: SKScene, SKPhysicsContactDelegate {
...
let redBallCategory:UInt32 = 0x00000001 << 0 // 1
let blueBallCategory:UInt32 = 0x00000001 << 1 // 2
let groundCategory:UInt32 = 0x00000001 << 2 // 4
# this function is called when collision happens
func didBegin( contact: SKPhysicsContact) {
# bitwise OR operation
let collision: UInt32 = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask # e.g. 5
let collision == groundCategory | redBallCategory { // e.g. 5
print("Contact")
}
}
}
A CGPoint
is a structure that contains a point in a two-dimensional coordinate system.
A CGVector
is a vector that specifies the gravitational acceleration applied to physics bodies in the physics world.