SpriteKit and Inverse Kinematics with Swift
In this tutorial, learn how to use Sprite Kit’s inverse kinematics to make a ninja punch and kick dynamically! By Jorge Jordán.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
SpriteKit and Inverse Kinematics with Swift
50 mins
- Getting Started
- Overview of Skeletal Hierarchy
- Setting up a Rest Pose
- What Is Inverse Kinematics?
- Forward Kinematics
- Inverse Kinematics
- Inverse Kinematics Actions
- Defining an Inverse Kinematics Action
- Setting Up an End-Effector
- Defining Joint Constraints
- Creating a Punching Motion
- Punching With Both Fists
- Facing the Target
- Head Tracking
- Hitting Moving Targets
- Creating a Kicking Motion
- Finishing Touches
- Gratuitous Background Music
- Where to Go From Here?
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 didMove(to:)
:
upperLeg = lowerTorso.childNode(withName: "leg_upper_back")
lowerLeg = upperLeg.childNode(withName: "leg_lower_back")
foot = lowerLeg.childNode(withName: "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 kickAt(_ location: CGPoint) {
let kick = SKAction.reach(to: location, rootNode: upperLeg, duration: 0.1)
let restore = SKAction.run {
self.upperLeg.run(SKAction.rotate(toAngle: self.upperLegAngleDeg.degreesToRadians(), duration: 0.1))
self.lowerLeg.run(SKAction.rotate(toAngle: self.lowerLegAngleDeg.degreesToRadians(), duration: 0.1))
}
let checkIntersection = intersectionCheckAction(for: foot)
foot.run(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 intersectionCheckAction(for:)
, 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(:with:)
, replace the following line:
punchAt(location)
With the code below:
let lower = location.y < lowerTorso.position.y + 10
if lower {
kickAt(location)
}
else {
punchAt(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 didMove(to:)
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 = .left
scoreLabel.verticalAlignmentMode = .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 = .right
livesLabel.verticalAlignmentMode = .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 intersectionCheckAction(for:)
, 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 File 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 didMove(to view: SKView) {
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
myLabel.text = "Game Over"
myLabel.fontSize = 65
myLabel.position = CGPoint(x:frame.midX, y:frame.midY)
addChild(myLabel)
run(SKAction.sequence([
SKAction.wait(forDuration: 1.0),
SKAction.run({
let transition = SKTransition.fade(withDuration: 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.run({
// 1
if self.life > 0 {
self.life -= 1
}
// 2
self.livesLabel.text = "Lives: \(Int(self.life))"
// 3
let blink = SKAction.sequence([SKAction.fadeOut(withDuration: 0.05), SKAction.fadeIn(withDuration: 0.05)])
// 4
let checkGameOverAction = SKAction.run({
if self.life <= 0 {
let transition = SKTransition.fade(withDuration: 1.0)
let gameOverScene = GameOverScene(size: self.size)
self.view?.presentScene(gameOverScene, transition: transition)
}
})
// 5
self.lowerTorso.run(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.run(SKAction.sequence([actionMove, actionMoveDone]))
With this:
shuriken.run(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!