William Liu

Swift Spritekit

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.

Overview

https://developer.apple.com/documentation/spritekit

Documentation topics include:

WWDC Videos

Best Practices for Building Spritekit Games

Introducing GameplayKit

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.

Entities and Components

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

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).

Agents, Behaviors, and Goals

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

Pathfinding is an issue in many games. Let’s first define a few things:

GKGraph

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:

Grid Graphs

A GKGraph that works with 2D grids as class GKGridGraph.

Obstacle Graphs

A GKGraph that works with pathing around obstacles as class GKObstacleGraph.

GKGraphNode

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

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).

GKGameModel protocol

The GKGameModel protocol the an abstraction fo the current game state. For a chess game, you might have:

GKGameModelUpdate protocol

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.

GKGameModelPlayer protocol

The 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.

GKMinmaxStrategist class

The GKMinmaxStrategist class is an AI that chooses moves in turn-based games using a deterministic strategy.

GKMonteCarloStrategist class

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];

Random Sources

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]

Rule Systems

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 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];

Deeper into GameplayKit with DemoBots

The tools and technology used for DemoBots include:

ActionEditor

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.

Assets Catalog

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.

SKCameraNode

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).

GKStateMachine

The PlayerBot state has the following states using a GKStateMachine:

We also use states for the game state, e.g.

GKEntity and GKComponent

Sample Code

https://developer.apple.com/documentation/gameplaykit

Simulator and Building on an iPhone

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.

Format your code

Some keyboard shortcuts:

Important Classes

How do these all fit together?

SKView SKScene (the root node) SKNode - SKSpriteNode1 - SKSpriteNode2 - SKSpriteNode3

How do I start creating a Game

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:

With a SPA, you will need to add in:

Storyboard

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

AppDelegate

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.

Assets

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

Tile Maps and Sets

There are four tile sets you can create:

For additional details, see this talk: https://developer.apple.com/videos/play/wwdc2016/610/

GameScene

https://developer.apple.com/documentation/spritekit/skscene

GameScene.sks (access GameScene through GUI)

The GameScene.sks file allows you to modify your SKScene through a GUI. You can:

GameScene.swift (access GameScene through Code)

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:

The Rendering Loop

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:

SKAction

SKAction 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

Nodes include:

SKEmitterNode

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:

GameplayKit

When you import GameplayKit, you have access to a lot of useful pre-built functions like:

GameViewController

Sets up your Scene

Physics

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).

SKPhysicsBody

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))

Bitmasks and Collisions

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.

Collision

Once the ball actually hits the grass, we can take an action/do something:

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")
        }
    }
}

CGPoint

A CGPoint is a structure that contains a point in a two-dimensional coordinate system.

CGVector

A CGVector is a vector that specifies the gravitational acceleration applied to physics bodies in the physics world.