Update 5/12/16: Updated for Xcode 7.3 by Jorge Jordán. Original tutorial by Ken Toh.
If you’ve ever added an animated character to a game, you probably pre-created animations for running, punching, jumping and so forth, and played them when the game executes the associated actions.
While this “canned” approach works for simple scenarios, there are often cases where you’d prefer your character to interact more realistically with other game objects.
For instance, if your character needs to reach out to pick up a nearby object at a variable height, you may not want to use the same animation for every picking action, or it could look really silly when the predefined hand motions don’t line up exactly with the object’s position!
Since iOS 8, Sprite Kit comes with inverse kinematics, a technique that alleviates this issue by allowing you to procedurally generate joint motions such as reaching out to a point in space. This means you don’t have to resort to making a possibly infinite number of animations to reach every possible position.
In this tutorial, you’ll learn how to use Sprite Kit’s inverse kinematics feature to implement dynamic punches and kicks in a fun and simple ninja game. :]
In particular, you’ll learn how to:
- Set up joint hierarchies in the Sprite Kit Scene Editor.
- Define joint constraints for more realistic joint behavior.
- Dynamically animate selected joint hierarchies such that the terminal bones can reach out to various positions in 2D space.
Let’s get moving!
Getting Started
Download the starter project, open it in Xcode and build and run it. You’ll see a static ninja already set up in the middle of the scene, begging to be released from his yoga stretch.
Introducing a new iOS Framework: YogaKit!
Take some time to familiarize yourself with the structure of the sample project. In particular, select GameScene.sks in your project navigator. The file should open up in the integrated Sprite Kit Scene Editor, a feature available since Xcode 6 and iOS 8.
Note: Xcode’s Scene Editor provides a convenient user interface for initializing a scene by allowing you to visually manipulate your sprites and their properties such as position, scale and physics bodies. Later, you can create SKScene
objects by unarchiving the .sks files you configured in the Scene Editor.
In the following steps, you’ll take advantage of the Scene Editor to set up and tweak the behavior of the joint hierarchy for your ninja, which would be difficult to visualize and set up programmatically.
Overview of Skeletal Hierarchy
In the scene, you can see that the ninja already has a basic skeleton comprised of a hierarchy of bones, each represented by a sprite node. The nodes are connected in a tree of parent-child relationships, as depicted below:
The lower torso node, in red, is the root parent of the entire skeleton hierarchy, which in turn is a child of the scene itself.
The anchor point for each sprite node acts as a joint connecting the node with its parent. These have been adjusted with an offset so that one end of the node can rotate naturally about the joint.
Setting up a Rest Pose
While the ninja looks cool in that mid-air pose, you unfortunately aren’t building a game involving levitation, so you’ll have to bring him back to earth.
Let’s start by adjusting the legs so they touch the shadow on the ground. In the scene, select the front upper leg node, leg_upper_front, by clicking on it.
Set the rotation of leg_upper_front to -14. You can do this either by rotating the handle of the selected sprite or by setting the Rotation property manually in the editor, as shown below:
Note: Some of the other values (such as position) may be slightly different for you than this screenshot; just make sure the Rotation is -14 and you should be good!
Next, select leg_lower_front and set its rotation to -9 to keep it slightly bent backwards.
Moving on to the back leg nodes, set the rotations of leg_upper_back and leg_lower_back to 22 and -30, respectively. You should see the following:
Now that your ninja is finally standing, let’s work on the arms. Set the rotation angles of both upper arm nodes, arm_upper_front and arm_upper_back, to -10. Also, set the rotation angles of the lower arm nodes, arm_lower_front and arm_lower_back, to 130.
Note: Selecting a particular node can be tricky, especially when it’s obscured by other nodes. One way around this is to right-click on the obscuring node(s) and select Lock Node Selection. This locks the obscuring node, allowing you to select what’s behind it.
A second way, which I find more effective, is to use the jump bars running across the top of the Editor to navigate the scene graph hierarchy and select the node you want, as shown below:
You should see the following:
Finally, you’re going to do some housekeeping to ensure that the ninja and the shadow always stays centralized, regardless of the screen size. (Currently, the ninja would be off-center on the iPad 2.)
Switch to GameScene.swift and add the following code within and at the top of the GameScene
class:
//1
var shadow: SKNode!
var lowerTorso: SKNode! |
In didMoveToView()
, add the following code:
//2
lowerTorso = childNodeWithName("torso_lower")
lowerTorso.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame) - 30)
//3
shadow = childNodeWithName("shadow")
shadow.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame) - 100) |
Let’s go through what you just did:
- You create two
SKNode
properties to reference the shadow node and lower torso node (the root node of the ninja), respectively.
- You obtain a reference to the lower torso node by its name, “torso_lower”, and assign its value to the
lowerTorso
property. Next, you set its position to the center of the screen with an offset of -30
units.
- Similarly, you grab a reference to the shadow node by its name, “shadow”, and assign its value to the
shadow
property. Finally, you set its position to the center of the screen with an offset of -100
units.
Build and run the project, and you’ll see your ninja in its rest stance, ready for a fight!
With the joint hierarchy all set up and your ninja ready for action, you can begin using Sprite Kit’s inverse kinematics feature to deploy some serious martial arts.
What Is Inverse Kinematics?
Before you continue, it’s important to appreciate what inverse kinematics is all about, so the following is a quick primer to get you started.
Forward Kinematics
Fundamentally, kinematics is the study of the motion of an object through space and time.
For an animated character represented by a skeletal hierarchy, forward kinematics is about how the tip of the last bone in a joint hierarchy (called the end-effector) moves in space when you rotate each bone lying along the chain leading up to it.
Take the example of an arm. If I rotate my upper arm about the shoulder joint in combination with extending my lower arm from my elbow joint, I would be able to geometrically determine the new position of my hand by means of forward kinematics.
Note: End-effector is a term commonly used in robotics and character animation to describe the point at the end of a skeleton chain that interacts with the environment.
Inverse Kinematics
However, it’s often useful to look at the positioning of the joint hierarchy from the reverse point of view.
For example, given a desired goal position of the end-effector, in this case my hand, I would solve for all the joint angles along the chain leading up to the end-effector that would allow me to achieve the desired hand position.
This approach, as you might have guessed already, is known as inverse kinematics, and it’s particularly valuable for achieving goal-oriented actions such as touching, kicking or punching at various points in space. One of the most common uses of inverse kinematics in 3D games is to adjust a character’s limbs to position its feet properly when walking over uneven terrain.
In Sprite Kit, simply specifying a single end-effector goal position in 2D space allows you to position the entire joint hierarchy automatically, as opposed to the forward kinematics approach of fiddling with multiple joint angles.
For the purposes of this tutorial, that’s all the background you need to get started. If you’re interested in delving deeper into the mathematics of inverse kinematics, I highly recommend reading this and this.
In the meantime, let’s learn what you can do with Sprite Kit’s inverse kinematics feature!
Inverse Kinematics Actions
Let’s bring the ninja to life by making his front arm reach out to a position upon a tap event.
Add the following properties to the GameScene
class:
var upperTorso: SKNode!
var upperArmFront: SKNode!
var lowerArmFront: SKNode! |
These properties will hold references to the upper torso, upper arm and lower arm nodes, respectively.
Next, add the following code at the end of didMoveToView()
:
upperTorso = lowerTorso.childNodeWithName("torso_upper")
upperArmFront = upperTorso.childNodeWithName("arm_upper_front")
lowerArmFront = upperArmFront.childNodeWithName("arm_lower_front") |
Here, you traverse down the parent-child hierarchy manually from the lower torso to the right lower arm, grabbing the appropriate child nodes by their node names and storing them in the variables you declared earlier.
Defining an Inverse Kinematics Action
Next, add the following methods to the GameScene
class:
func punchAtLocation(location: CGPoint) {
// 1
let punch = SKAction.reachTo(location, rootNode: upperArmFront, duration: 0.1)
// 2
lowerArmFront.runAction(punch)
}
// 3
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
punchAtLocation(location)
}
} |
There are a few important things going on here, so let’s run through them step by step:
reachTo()
is an action responsible for performing inverse kinematics actions for a joint hierarchy reaching out to a point in space.
It takes in three arguments: (1) the desired end position to reach; (2) the highest node of the hierarchy that you want to rotate; and (3) the duration of the animation.
In your case, you set your root node to the upper arm node, since you want to move the upper arm and its descendant nodes. The animation will last for 0.1 seconds.
Note: reachTo()
can take a maximum velocity as an argument instead of an animation duration. In addition, there is a variant method, reachToNode()
, that takes in a node instead of a position. For more details, refer to the documentation.
- Next, you run the action on the end node that is going to reach out to touch the end position in space, which in this case is the lower arm.
- Upon a touch event, you run the punch action with the tap location as the end position that you want the lower arm to reach.
Build and run the project. Tap on various locations near the ninja, and you’ll see the arm joints move in an attempt to reach to the tap position.
This ninja looks like he has some problems!
Setting Up an End-Effector
While the arm joints move following your taps at various locations, there is something fundamentally strange about the actions—it seems the elbow is reaching out to the desired point instead of the fist! Why would that be happening?
Recall that each bone node has an anchor point that has been adjusted with an offset so that it’s flush with one end of the node, enabling it to rotate about its parent.
In your case, when you set the inverse kinematics action to run on the lower arm node, Sprite Kit uses the anchor point as the node reference point or end-effector. The lower arm’s anchor point is the elbow, so this is why the elbow seems to be the point reaching out to the target position.
Let’s fix this by attaching a child end-effector “fist” node at the lower tip of the lower arm.
Since the fist is already visually included with the lower arm, this node will simply be a point node without a texture.
Switch to GameScene.sks. Select the Object Library at the bottom of the Utilities pane in Xcode. From the Object Library, drag an Empty Node onto the front lower arm in the scene. Make sure to align the node with the furthest tip of the hand, as shown below:
With the new node selected, set its name to fist_front and its Parent to arm_lower_front in the SKNode Inspector on the right.
Sprite Kit will add the fist as a child, as shown below:
In terms of joint hierarchy, the entire front arm now looks like this: arm_upper_front (root) -> arm_lower_front -> fist_front (end-effector).
Switch back to GameScene.swift. Add the following line near the top of the GameScene
class:
In addition, add the following line to the end of didMoveToView()
:
fistFront = lowerArmFront.childNodeWithName("fist_front") |
Finally, replace the following line in punchAtLocation()
:
lowerArmFront.runAction(punch) |
With this:
fistFront.runAction(punch) |
Now that you’ve switched the end-effector node to the fist node, build and run the project. Try tapping various locations again, and you’ll see your ninja reaching out with his fist instead of his elbow. It’s a karate chop!
Defining Joint Constraints
While tapping at various locations, you may have noticed that inverse kinematics attempts to solve for the set of joint angles that will get your end-effector as near to the target position as possible.
In some cases, however, you may get strange-looking artifacts like the following:
That looks painful!
In the case above, while the joint angles indeed enable the fist to achieve the target position (illustrated by a red dot), Sprite Kit has bent the elbow joint beyond its realistic limit. Ouch!
This highlights one of the key characteristics of the inverse kinematics problem: Given a desired end-effector position, there could be more than one set of joint positions that will get the end-effector to the goal position. Imagine trying to reach out to touch a nearby object—I’m sure you can find more than one way to twist or extend your upper or lower arm and yet still touch the object!
Fortunately, you can guide the inverse kinematics engine to favor more realistic joint configurations by defining joint constraints to limit joint angles to a suitable lower and upper range.
You’ll define joint constraints for the lower arms so they don’t over-extend, but you’ll continue to allow the upper arms to rotate through the default range of 0-360 degrees.
Switch to GameScene.sks. Select the arm_lower_front node. Under IK Constraints in the SKNode Inspector on the right, keep the Min Angle‘s value 0 and set the Max Angle to 160, as shown below:
This ensures that the zRotation
values of the lower arm node don’t cross the lower limit of 0 degrees (lower arm fully extended) and the upper limit of 160 degrees (lower arm fully retracted, with a gap angle of 20 degrees between the upper and lower arm).
Select the arm_lower_back node and repeat the same steps to enforce similar constraints.
You can also add an IK Constraint programmatically by creating a SKReachConstraint and setting it to the reachConstraints property of the desired node. The instructions above specified can be replaced in code by adding the following lines after the initialization of fistFront:
let rotationConstraintArm = SKReachConstraints(lowerAngleLimit: CGFloat(0), upperAngleLimit: CGFloat(160))
lowerArmFront.reachConstraints = rotationConstraintArm |
Note: At the time of updating this tutorial there is a bug on the Scene Editor when editing the IK Constraints property of a node. If you modify either Min Angle
or Max Angle
none of these changes will be saved. If you experience this issue you can follow the programmatically workaround explained above.
Now, build and run the project. Tap on various locations to test the new constraints. No more over-extension!
Creating a Punching Motion
At this point, your ninja can reach out to any arbitrary tap position, but you know ninjas can do better than that. In this section, you’ll equip your ninja with the ability to punch at a location. Oooofff!
Start by adding the following properties to GameScene
inside GameScene.swift:
let upperArmAngleDeg: CGFloat = -10
let lowerArmAngleDeg: CGFloat = 130 |
The upperArmAngleDeg
and lowerArmAngleDeg
properties hold the default rest pose angle values of the upper arm and lower arm joints, respectively. Recall that these are the same values you defined earlier when you set up a rest pose for the ninja.
Finally, replace punchAtLocation
with the following implementation:
func punchAtLocation(location: CGPoint) {
// 1
let punch = SKAction.reachTo(location, rootNode: upperArmFront, duration: 0.1)
// 2
let restore = SKAction.runBlock {
self.upperArmFront.runAction(SKAction.rotateToAngle(self.upperArmAngleDeg.degreesToRadians(), duration: 0.1))
self.lowerArmFront.runAction(SKAction.rotateToAngle(self.lowerArmAngleDeg.degreesToRadians(), duration: 0.1))
}
// 3
fistFront.runAction(SKAction.sequence([punch, restore]))
} |
That’s quite a bit of code, so let’s dissect it line by line:
- The
punch
action is the same reaching action you defined before—it animates the joint hierarchy to reach out to a desired position.
- The
restore
action animates the restoration of the joint hierarchy to its rest pose. Unfortunately, inverse kinematics actions don’t have reversed actions, so here, you resort to using the traditional forward kinematics to rotate the joint angles back to their rest angles.
Within this action, you run rotateToAngle()
actions concurrently on both the upper and lower arm nodes to restore them to their rest angles as defined by upperArmAngleDeg
and lowerArmAngleDeg
, respectively. degreesToRadians
is defined in a category on CGFloat
to convert values between degrees and radians. You can find the function in CGFloat+Extensions.swift, included in the SKTUtils source folder.
- Finally, you concatenate the two actions,
punch
and restore
, into a sequenced action, which you then run on the fist.
Build and run the project. The ninja will now punch to wherever you tap!
Punching With Both Fists
Now that your ninja can punch, wouldn’t it be cool if he could use both arms? In this section, you’ll give him the ability to alternate between his left and right fists for consecutive punches.
To do so, you’ll first need to set up the back arm the same way you did the front arm.
Switch to GameScene.sks and add a fist end-effector as a child to arm_lower_back.
Name the fist fist_back. If you’ve set this up properly, you should see the following:
Next, switch to GameScene.swift and add the following properties to GameScene
:
var upperArmBack: SKNode!
var lowerArmBack: SKNode!
var fistBack: SKNode! |
Next, add the following code at the end of didMoveToView()
:
upperArmBack = upperTorso.childNodeWithName("arm_upper_back")
lowerArmBack = upperArmBack.childNodeWithName("arm_lower_back")
fistBack = lowerArmBack.childNodeWithName("fist_back") |
The three properties now hold references to all the back arm nodes.
After the above code, add the reachConstraint to the lowerArmBack to avoid injuries:
lowerArmBack.reachConstraints = rotationConstraintArm |
You want to alternate which fist punches, so add the following property to GameScene
:
This Boolean’s role is to keep track of whether it’s time to punch using the left or the right fist.
Finally, you’re going to do a little refactoring of punchAtLocation()
into two smaller functions to neatly handle punching with either set of arm nodes.
Replace punchAtLocation()
with the following code:
// 1
func punchAtLocation(location: CGPoint, upperArmNode: SKNode, lowerArmNode: SKNode, fistNode: SKNode) {
let punch = SKAction.reachTo(location, rootNode: upperArmNode, duration: 0.1)
let restore = SKAction.runBlock {
upperArmNode.runAction(SKAction.rotateToAngle(self.upperArmAngleDeg.degreesToRadians(), duration: 0.1))
lowerArmNode.runAction(SKAction.rotateToAngle(self.lowerArmAngleDeg.degreesToRadians(), duration: 0.1))
}
fistNode.runAction(SKAction.sequence([punch, restore]))
}
func punchAtLocation(location: CGPoint) {
// 2
if rightPunch {
punchAtLocation(location, upperArmNode: upperArmFront, lowerArmNode: lowerArmFront, fistNode: fistFront)
}
else {
punchAtLocation(location, upperArmNode: upperArmBack, lowerArmNode: lowerArmBack, fistNode: fistBack)
}
// 3
rightPunch = !rightPunch
} |
Let’s go through the essence of what’s happening here:
- This first function is similar to the version you had previously, except that it now lets you specify the arm nodes as arguments. This enables you to use the same function for both the left and right arms.
- In the second function, you simply check if it’s time to use the left or the right arm based on the value of the
rightPunch
Boolean, and execute the actions accordingly.
- Finally, you toggle the
rightPunch
flag such that when the function is called again on the next tap, the Boolean flag is flipped accordingly, allowing you to alternate between the two arms.
Build and run the project. Your ninja will punch with both fists!
Facing the Target
You may have noticed that your ninja isn’t quite as adept at handling target positions behind him.
That looks more like a dance move than a punch! Since this isn’t a dance simulator (although that sounds like a fun game idea!), let’s fix this by making the ninja always face the target position.
In touchesBegan()
, add the following code just before the line that calls punchAtLocation()
:
lowerTorso.xScale =
location.x < CGRectGetMidX(frame) ? abs(lowerTorso.xScale) * -1 : abs(lowerTorso.xScale) |
If you detect the tap on the left half of the screen, you negate the scale of the ninja (rooted at the lower torso), which has the effect of flipping the entire ninja horizontally.
Build the project again and run it. Upon a tap, the ninja will now face the direction of the tap location.
Head Tracking
As a bonus, you can make the ninja behave more even realistically by using Sprite Kit’s SKConstraint
s to make his head track the target.
SKConstraint
s allow you to ensure that certain relationships are true between nodes, before a scene is rendered.
For example, you can set up a constraint to make sure a node stays within a certain rectangle, to orient one node toward another, or to limit the z rotation of a node. You will combine a few constraints here to easily make the ninja’s face look toward where he’s punching.
Begin by adding the following properties to GameScene
:
var head: SKNode!
let targetNode = SKNode() |
Next, add the following code to the bottom of didMoveToView()
:
head = upperTorso.childNodeWithName("head") |
This grabs a reference to the head node and stores it in head
.
Now, add the following line to the end of the for
loop in touchesBegan
, just after the call to punchAtLocation()
:
targetNode.position = location |
targetNode
now stores the location of the latest tap location.
Finally, you’ll make use of SKConstraint
s to ensure that the head node is always oriented so that it “looks” at the target node. Add the following code at the end of didMoveToView()
:
// 1
let orientToNodeConstraint = SKConstraint.orientToNode(targetNode, offset: SKRange(constantValue: 0.0))
// 2
let range = SKRange(lowerLimit: CGFloat(-50).degreesToRadians(),
upperLimit: CGFloat(80).degreesToRadians())
// 3
let rotationConstraint = SKConstraint.zRotation(range)
// 4
rotationConstraint.enabled = false
orientToNodeConstraint.enabled = false
// 5
head.constraints = [orientToNodeConstraint, rotationConstraint] |
Let’s go through the code line by line:
- You create an
orientToNode
constraint, passing in targetNode
as the node toward which to orient.
- Here, you define an angle range from
-50
degrees to 80
degrees, converted to radians.
- You define a rotation constraint that limits the
zRotation
property of the head node to the angle range defined in step 2.
- You disable the two constraints by default, as there may not be any target node yet.
- Finally, you add the two constraints to the head node.
To get the constraints to work, you’ll enable the constraints upon the first tap. Add the following property to GameScene
:
Next, add the following code to the beginning of touchesBegan()
:
if !firstTouch {
for c in head.constraints! {
let constraint = c
constraint.enabled = true
}
firstTouch = true
} |
On the very first tap event, the code above loops through the list of constraints defined for the head node and enables all of them. It then sets the firstTouch
flag to true
, which ensures that the code won’t be executed again on subsequent taps.
Build and run the project once more. Voila! Your ninja now maintains constant eye contact with his target position as he punches.
Hitting Moving Targets
At this point, your ninja is raring for “real” things to punch, so let’s spawn some flying shurikens from either side of the screen for the ninja to hit.
Begin by adding the following function to the GameScene
class:
func addShuriken() {
// 1
let shuriken = SKSpriteNode(imageNamed: "projectile")
// 2
let minY = lowerTorso.position.y - 60 + shuriken.size.height/2
let maxY = lowerTorso.position.y + 140 - shuriken.size.height/2
let rangeY = maxY - minY
let actualY = (CGFloat(arc4random()) % rangeY) + minY
// 3
let left = arc4random() % 2
let actualX = (left == 0) ? -shuriken.size.width/2 : size.width + shuriken.size.width/2
// 4
shuriken.position = CGPointMake(actualX, actualY)
shuriken.name = "shuriken"
shuriken.zPosition = 1
addChild(shuriken)
// 5
let minDuration = 4.0
let maxDuration = 6.0
let rangeDuration = maxDuration - minDuration
let actualDuration = (Double(arc4random()) % rangeDuration) + minDuration
// 6
let actionMove = SKAction.moveTo(CGPointMake(size.width/2, actualY), duration: actualDuration)
let actionMoveDone = SKAction.removeFromParent()
shuriken.runAction(SKAction.sequence([actionMove, actionMoveDone]))
// 7
let angle = left == 0 ? CGFloat(-90).degreesToRadians() : CGFloat(90).degreesToRadians()
let rotate = SKAction.repeatActionForever(SKAction.rotateByAngle(angle, duration: 0.2))
shuriken.runAction(SKAction.repeatActionForever(rotate))
} |
Let’s briefly run through the code in this function:
- You create a brand new sprite node from the projectile.png image.
- You set the spawn height of the shuriken to a value between
60
units below and 130
units above the lower torso. This ensures the shuriken will be within reach of the ninja.
- You set the x-position of the shuriken to be either slightly left or slightly right of the screen.
- You then set the position of the shuriken based on the values determined in steps 2 and 3. You also assign the hard-coded name “shuriken” to the node before adding it as a child to the scene.
- You randomize the move duration of the shuriken to be between
4
and 6
seconds to add some sense of variance to the game.
- You define a sequence of two actions to run on the shuriken. The first action moves the shuriken toward the center of the screen based on the duration defined in step 5. The second action removes the shuriken once it reaches the center of the screen.
- Concurrently, you rotate the shuriken continuously in the direction of its motion for a more realistic effect.
You’ll want to spawn shurikens periodically for the life of the game. Add the following properties to GameScene
to keep track of the time between spawns:
var lastSpawnTimeInterval: NSTimeInterval = 0
var lastUpdateTimeInterval: NSTimeInterval = 0 |
Then add the following methods to GameScene
, as well:
func updateWithTimeSinceLastUpdate(timeSinceLast: CFTimeInterval) {
lastSpawnTimeInterval = timeSinceLast + lastSpawnTimeInterval
if lastSpawnTimeInterval > 0.75 {
lastSpawnTimeInterval = 0
addShuriken()
}
}
override func update(currentTime: CFTimeInterval) {
var timeSinceLast = currentTime - lastUpdateTimeInterval
lastUpdateTimeInterval = currentTime
if timeSinceLast > 1.0 {
timeSinceLast = 1.0 / 60.0
lastUpdateTimeInterval = currentTime
}
updateWithTimeSinceLastUpdate(timeSinceLast)
} |
updateWithTimeSinceLastUpdate
adds the time since the last update to lastSpawnTimeInterval
. Once it is greater than 0.75 seconds, you call addShuriken()
to spawn a new shuriken and reset the time. You call this method inside update()
each frame, with a bit of math to ensure the time between frames doesn’t get out of hand.
This code is similar to the monster-spawning code in the Sprite Kit Swift Tutorial for Beginners on this site. You can refer to that tutorial if you’d like more details.
Build and run the project, and you’ll see shurikens fly in at your ninja from both sides of the screen!
The shurikens look dangerous, but they simply run into the ninja as if he weren’t even there. Not to worry; you’ll fix this by adding some code to check for possible intersections between the end-effector and any flying shuriken.
Add the following function to the GameScene
class:
func intersectionCheckActionForNode(effectorNode: SKNode) -> SKAction {
let checkIntersection = SKAction.runBlock {
for object: AnyObject in self.children {
// check for intersection against any sprites named "shuriken"
if let node = object as? SKSpriteNode {
if node.name == "shuriken" {
// convert coordinates into common system based on root node
let effectorInNode = self.convertPoint(effectorNode.position, fromNode:effectorNode.parent!)
var shurikenFrame = node.frame
shurikenFrame.origin = self.convertPoint(shurikenFrame.origin, fromNode: node.parent!)
if shurikenFrame.contains(effectorInNode) {
// play a hit sound
self.runAction(SKAction.playSoundFileNamed("hit.mp3", waitForCompletion: false))
// show a spark effect
let spark = SKSpriteNode(imageNamed: "spark")
spark.position = node.position
spark.zPosition = 60
self.addChild(spark)
let fadeAndScaleAction = SKAction.group([
SKAction.fadeOutWithDuration(0.2),
SKAction.scaleTo(0.1, duration: 0.2)])
let cleanUpAction = SKAction.removeFromParent()
spark.runAction(SKAction.sequence([fadeAndScaleAction, cleanUpAction]))
// remove the shuriken
node.removeFromParent()
}
else {
// play a miss sound
self.runAction(SKAction.playSoundFileNamed("miss.mp3", waitForCompletion: false))
}
}
}
}
}
return checkIntersection
} |
intersectionCheckActionForNode()
takes in an end-effector node and returns an action that runs a block; you’ll see in the next section why you define the function in this manner. Within this block, you check to see if the end-effector intersects with any shurikens present in the scene.
In the event of an intersection, you play a “hit” sound, show a little spark effect and remove the shuriken sprite from the scene. Otherwise, you simply play a “miss” sound.
Next, replace the last line of punchAtLocation(_:upperArmNode:lowerArmNode:fistNode:)
with the two lines below:
let checkIntersection = intersectionCheckActionForNode(fistNode)
fistNode.runAction(SKAction.sequence([punch, checkIntersection, restore])) |
You’ve simply added an additional action right after the punch action, which runs a block to check for possible intersections of the fist node with the shurikens in the scene.
Since you perform the intersection test exactly at the moment when the reaching motion is complete, your ninja has to be rather precise when executing a punch, which should be the case for all ninjas anyway. :]
Build and run the project. Your ninja can now handle those shurikens like a boss!
The shurikens still don’t hurt the ninja; you’ll implement that in a later section.
Creating a Kicking Motion
Your ninja is now a punching machine, but there’s no doubt he can do more to showcase his well-roundedness. Let’s equip him with the ability to kick at shurikens below a certain height.
You’ll begin by setting up the leg joint hierarchy for inverse kinematics. In particular, you’ll:
- Define an end-effector node for the back leg, which will be the leg the ninja will use for kicking.
- Set up joint constraints for each back leg joint.
- Define inverse kinematics actions for the joint hierarchy to reach the tap location for taps made below a certain height.
These steps are similar to what you’ve done for the arms, so let’s get on with it!
Switch to GameScene.sks. Drag an Empty Node onto the back lower leg (leg_lower_back) in the scene. Make sure to align the node with the farthest tip of the foot. Keeping the node selected, set its name to foot_back and its parent to leg_lower_back in the SKNode Inspector on the right. Once done, you’ll have something like this:
Next, you’ll set the IK Constraints for the leg’s nodes.
Don’t do this yet; I want to explain things first.
For leg_upper_back, you’ll constrain the rotation angle to between -45 and 160 degrees, as illustrated below:
As for leg_lower_back, you’ll constrain the rotation angle to between -45 and 0 degrees, as shown below:
Strangely, Scene Editor only allows positive values for the min and max angles. It also doesn’t allow the min angle to be larger than the max angle, which means it would consider a normalized range of 315 (-45) to 160 for the upper leg to be invalid, as well. Nonetheless, you will overcome this limitation by defining the constraints programmatically.
Now you can go ahead and implement this.
Inside GameScene.swift, add the following properties to GameScene
:
var upperLeg: SKNode!
var lowerLeg: SKNode!
var foot: SKNode! |
And add the following code to initialize these new properties in didMoveToView()
:
upperLeg = lowerTorso.childNodeWithName("leg_upper_back")
lowerLeg = upperLeg.childNodeWithName("leg_lower_back")
foot = lowerLeg.childNodeWithName("foot_back")
lowerLeg.reachConstraints = SKReachConstraints(lowerAngleLimit: CGFloat(-45).degreesToRadians(), upperAngleLimit: 0)
upperLeg.reachConstraints = SKReachConstraints(lowerAngleLimit: CGFloat(-45).degreesToRadians(), upperAngleLimit: CGFloat(160).degreesToRadians()) |
In the code above, you obtain references to the three leg nodes and save them in their respective properties. You then set the reachConstraints
property of the lower and upper legs to the limits described previously. That’s about it!
Next, you’ll define a function that runs a kicking action on the foot
node. Before you do so, add the following properties to GameScene
:
let upperLegAngleDeg: CGFloat = 22
let lowerLegAngleDeg: CGFloat = -30 |
These two properties hold the rest angles of the upper and lower leg joints, respectively.
Next, add the following function to GameScene
:
func kickAtLocation(location: CGPoint) {
let kick = SKAction.reachTo(location, rootNode: upperLeg, duration: 0.1)
let restore = SKAction.runBlock {
self.upperLeg.runAction(SKAction.rotateToAngle(self.upperLegAngleDeg.degreesToRadians(), duration: 0.1))
self.lowerLeg.runAction(SKAction.rotateToAngle(self.lowerLegAngleDeg.degreesToRadians(), duration: 0.1))
}
let checkIntersection = intersectionCheckActionForNode(foot)
foot.runAction(SKAction.sequence([kick, checkIntersection, restore]))
} |
This function is similar to the one you constructed for the arms, except it’s tailored for the leg nodes. Notice how you’re able to reuse intersectionCheckActionForNode()
, this time for the foot
end-effector node.
Finally, you’ll run the leg action for tap locations below a certain height. Within the for
loop in touchesBegan()
, replace the following line:
punchAtLocation(location) |
With the code below:
let lower = location.y < lowerTorso.position.y + 10
if lower {
kickAtLocation(location)
}
else {
punchAtLocation(location)
} |
Here, you simply run the kicking action if the tap position is below the lower torso’s height plus 10
units; otherwise you do the usual punching action.
Build and run the project. Your ninja can now dynamically punch and kick shurikens that are within range!
Finishing Touches
You are almost done! Let’s tweak this project to make it into a playable game. To spice things up, you’ll give the ninja three lives, make him take damage from missed shurikens and allow him to earn points for each shuriken he hits.
Add the following properties to GameScene
:
var score: Int = 0
var life: Int = 3 |
These properties will store the score and number of lives remaining, respectively.
Add the following lines after the code above:
let scoreLabel = SKLabelNode()
let livesLabel = SKLabelNode() |
You’ll use these label nodes to display the score and remaining lives, respectively.
Next, add the following code to didMoveToView()
to set up the properties of the label nodes and add them to the scene:
// setup score label
scoreLabel.fontName = "Chalkduster"
scoreLabel.text = "Score: 0"
scoreLabel.fontSize = 20
scoreLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Left
scoreLabel.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Top
scoreLabel.position = CGPoint(x: 10, y: size.height - 10)
addChild(scoreLabel)
// setup lives label
livesLabel.fontName = "Chalkduster"
livesLabel.text = "Lives: 3"
livesLabel.fontSize = 20
livesLabel.horizontalAlignmentMode = SKLabelHorizontalAlignmentMode.Right
livesLabel.verticalAlignmentMode = SKLabelVerticalAlignmentMode.Top
livesLabel.position = CGPoint(x: size.width - 10, y: size.height - 10)
addChild(livesLabel) |
With the label nodes set up, add the following lines within the innermost if
block in intersectionCheckActionForNode()
, right before the line node.removeFromParent()
:
self.score += 1
self.scoreLabel.text = "Score: \(Int(self.score))" |
This increments the score by 1 whenever the ninja successfully destroys a shuriken with a punch or a kick.
Now, let’s handle the lives. Every time a shuriken hits the ninja, you’ll decrement the ninja’s life by 1. If he has no remaining lives, you’ll show a “Game Over” screen briefly and restart the scene.
Begin by creating a new scene to display the “Game Over” message. Create a new file with the iOS\Source\Swift class template, name the file GameOverScene, click Next and then click Create.
Replace the contents of GameOverScene.swift with the following code:
import SpriteKit
class GameOverScene: SKScene {
override func didMoveToView(view: SKView) {
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Game Over"
myLabel.fontSize = 65
myLabel.position = CGPoint(x:CGRectGetMidX(frame), y:CGRectGetMidY(frame))
addChild(myLabel)
runAction(SKAction.sequence([
SKAction.waitForDuration(1.0),
SKAction.runBlock({
let transition = SKTransition.fadeWithDuration(1.0)
let scene = GameScene(fileNamed:"GameScene")
scene!.scaleMode = .AspectFill
scene!.size = self.size
self.view?.presentScene(scene!, transition: transition)
})]))
}
} |
The code above displays a label showing a “Game Over” message. It then runs an action on the scene that presents a new GameScene
with a fading transition after a delay of one second.
Now, switch back to GameScene.swift. In addShuriken()
, add the following code right after the line that creates actionMoveDone
:
let hitAction = SKAction.runBlock({
// 1
if self.life > 0 {
self.life -= 1
}
// 2
self.livesLabel.text = "Lives: \(Int(self.life))"
// 3
let blink = SKAction.sequence([SKAction.fadeOutWithDuration(0.05), SKAction.fadeInWithDuration(0.05)])
// 4
let checkGameOverAction = SKAction.runBlock({
if self.life <= 0 {
let transition = SKTransition.fadeWithDuration(1.0)
let gameOverScene = GameOverScene(size: self.size)
self.view?.presentScene(gameOverScene, transition: transition)
}
})
// 5
self.lowerTorso.runAction(SKAction.sequence([blink, blink, checkGameOverAction]))
}) |
In the code you just added, you create an additional action to be run when the shuriken reaches the center of the screen. The action runs a block that does the following:
- It decrements the number of lives.
- It accordingly updates the label depicting the number of lives remaining.
- It defines a blink action with fade-in and fade-out durations of
0.05
seconds each.
- It defines another action running a block that checks if the number of remaining lives has hit zero. If so, the game is over, so the code presents an instance of
GameOverScene
.
- It then runs the actions in steps 3 and 4 in sequence on the lower torso, the root of the ninja.
Finally, add hitAction
to the sequence of actions you run on each shuriken. Replace the following line in addShuriken
:
shuriken.runAction(SKAction.sequence([actionMove, actionMoveDone])) |
With this:
shuriken.runAction(SKAction.sequence([actionMove, hitAction, actionMoveDone])) |
Build and run the project. You’ll see the new number of lives and score labels. In addition, your ninja is no longer immune, which makes the game a bit more challenging!
Gratuitous Background Music
Last but not least, every ninja game needs good background music, so switch to GameViewController.swift and add the following import at the top of the file:
This gives you access to the AVFoundation framework, which is necessary to play background music.
Add the following property to the GameViewController
class:
var audioPlayer: AVAudioPlayer? |
Next, you’ll write a function that plays your background music. Add the following function to the GameViewController
class:
func startBackgroundMusic() {
if let path = NSBundle.mainBundle().pathForResource("bg", ofType: "mp3") {
audioPlayer = try! AVAudioPlayer(contentsOfURL: NSURL(fileURLWithPath: path), fileTypeHint: "mp3")
if let player = audioPlayer {
player.prepareToPlay()
player.numberOfLoops = -1
player.play()
}
}
} |
The function looks for the background music file bg.mp3 in the resource path, loads it into memory and plays it indefinitely.
Note: The AVAudioPlayer()
initializer can throw errors that need to be handled. As the audio resource is provided on the project no error will be thrown so that’s why you wrote try!
, in order to disable error propagation. For more details, refer to the documentation.
Finally, add the following line to the bottom of viewDidLoad()
in GameViewController
to play the music upon loading the main view:
That’s it! Build and run the project, and strike shurikens to the beat of your new background music.
Where to Go From Here?
You can download the complete project for this tutorial here.
At this point, you should have a solid grasp of the key concepts of inverse kinematics in Sprite Kit.
Inverse kinematics is a powerful technique that brings a new level of dynamism to traditional character animation. There are many possible goal-oriented scenarios where inverse kinematics would apply an additional touch of realism—from having a snake whip its neck around to face the user, to a goalkeeper adjusting both hands to catch an oncoming ball, to a wizard dexterously waving a wand in mid-air—it’s all up to your imagination!
In case you’re wondering, 3D inverse kinematics is also available in Scene Kit, Apple’s 3D Graphics API. The feature allows you to blend inverse kinematics on top of existing animations, bringing an even greater sense of realism.
If you’d like to learn more, you should check out our book 2D iOS Games by Tutorials. The book teaches you everything you need to know to make 2D iOS games, by making a series of mini-games like this one, from an action game to a puzzle game to a tower defense game.
I hope you enjoyed this tutorial, and I look forward to seeing how you use inverse kinematics to spice up character animations in your own games!
Credits: Art by Vicki Wenderlich from gameartguppy.com. Background music from Kevin MacLeod. Hit Sound Effect from Mike Koenig, and Miss Sound Effect from Mark DiAngelo.
The post Sprite Kit and Inverse Kinematics with Swift appeared first on Ray Wenderlich.