SpriteKit: Actions and Physics

SpriteKit: Actions and Physics

In this series, we're learning how to use SpriteKit to build 2D games for iOS. In this post, we'll learn about two important features of SpriteKit: actions and physics.

To follow along with this tutorial, just download the accompanying GitHub repo. It has two folders: one for actions and one for physics. Just open either starter project in Xcode and you're all set.

Actions

For most games, you'll want nodes to do something like move, scale, or rotate. The SKAction class was designed with this purpose in mind. The SKAction class has many class methods that you can invoke to move, scale, or rotate a node's properties over a period of time. 

You can also play sounds, animate a group of textures, or run custom code using the SKAction class. You can run a single action, run two or more actions one after another in a sequence, run two or more actions at the same time together as a group, and even repeat any actions.

Motion

Let's get a node moving across the screen. Enter the following within Example1.swift.

Here we create an SKAction and invoke the class method moveTo(y:duration:), which takes as a parameter the y position to move the node to and the duration in seconds. To execute the action, you must call a node's run(_:) method and pass in the SKAction. If you test now, you should see an airplane move up the screen.

There are several varieties of the move methods, including move(to:duration:), which will move the node to a new position on both the x and y axis, and move(by:duration:), which will move a node relative to its current position. I suggest you read through the documentation on SKAction to learn about all of the varieties of the move methods.

Completion Closures

There is another variety of the run method that allows you to call some code in a completion closure. Enter the following code within Example2.swift.

The run(_:completion:) method allows you to run a block of code once the action has fully completed executing. Here we execute a simple print statement, but the code could be as complex as you need it to be.

Sequences of Actions

Sometimes you'll want to run actions one after another, and you can do this with the sequence(_:) method. Add the following to Example3.swift.

Here we create two SKActions: one uses the moveTo(y:duration:), and the other uses the scale(to:duration:), which changes the x and y scale of the node. We then invoke the sequence(_:) method, which takes as a parameter an array of SKActions to be run one after the other. If you test now, you should see the plane move up the screen, and once it has reached its destination, it will then grow to three times its original size.

Grouped Actions

At other times, you may wish to run actions together as a group. Add the following code to Example4.swift.

Here we are using the same moveTo and scale methods as the previous example, but we are also invoking the group(_:) method, which takes as a parameter an array of SKActions to be run at the same time. If you were to test now, you would see that the plane moves and scales at the same time.

Reversing Actions

Some of these actions can be reversed by invoking the reversed() method. The best way to figure out which actions support the reversed() method is to consult the documentation. One action that is reversible is the fadeOut(withDuration:), which will fade a node to invisibility by changing its alpha value. Let's get the plane to fade out and then fade back in. Add the following to Example5.swift.

Here we create a SKAction and invoke the fadeOut(withDuration:) method. In the next line of code, we invoke the reversed() method, which will cause the action to reverse what it has just done. Test the project, and you will see the plane fade out and then fade back in.

Repeating Actions

If you ever need to repeat an action a specific number of times, the repeat(_:count:) and repeatForever(_:) methods have you covered. Let's make the plane repeatedly fade out and then back in forever. Enter the following code in Example6.swift.

Here we invoke the repeatForever(_:) method, passing in the fadePlayerSequence. If you test, you will see the plane fades out and then back in forever.

Stopping Actions

Many times you'll need to stop a node from running its actions. You can use the removeAllActions() method for this. Let's make the player node stop fading when we touch on the screen. Add the following within Example7.swift.

If you touch on the screen, the player node will have all actions removed and will no longer fade in and out.

Keeping Track of Actions

Sometimes you need a way to keep track of your actions. For example, if you run two or more actions on a node, you may want a way to identify them. You can do this by registering a key with the node, which is a simple string of text.
Enter the following within the Example8.swift.

Here we are invoking the node's run(_:withKey:) method, which, as mentioned, takes a simple string of text. Within the touchesBegan(_:with:) method, we are invoking  action(forKey:) to make sure the node has the key we assigned. If it does, we invoke .removeAction(forKey:), which takes as a parameter the key you have previously set.

Sound Actions

A lot of times you'll want to play some sound in your game. You can accomplish this using the class method playSoundFileNamed(_:waitForCompletion:). Enter the following within Example9.swift.

The playSoundFileNamed(_:waitForCompletion:) takes as parameters the name of the sound file without the extension, and a boolean that determines whether the action will wait until the sound is complete before moving on. 

For example, suppose you had two actions in a sequence, with the sound being the first action. If waitForCompletion was true then the sequence would wait until that sound was finished playing before moving to the next action within the sequence. If you need more control over your sounds, you can use an SKAudioNode. We will not be covering the SKAudioNode in this series, but is definitely something you should take a look at during your career as a SpriteKit developer.

Frame Animation

Animating a group of images is something that many games call for. The animate(with:timePerFrame:) has you covered in those cases. Enter the following within Example10.swift.

The animate(with:timePerFrame:) takes as a parameter an array of SKTextures, and a timePerFrame value which will be how long it takes between each texture change. To execute this action, you invoke a node's run method and pass in the SKAction.

Custom Code Actions

The last type of action we will look at is one that lets you run custom code. This could come in handy when you need to do something in the middle of your actions, or just need a way to execute something that the SKAction class does not provide for. Enter the following within Example11.swift.

Here we invoke the scene's run(_:) method and pass a function printToConsole() as a parameter. Remember that scenes are nodes too, so you can invoke the run(_:) method on them as well.

This concludes our study of actions. There is a lot you can do with the SKAction class, and I would suggest after reading this tutorial that you further explore the documentation on SKActions.

Physics

SpriteKit offers a robust physics engine out of the box, with little setup required. To get started, you just add a physics body to each of your nodes and you're good to go. The physics engine is built on top of the popular Box2d engine. SpriteKit's API is much easier to use than the original Box2d API, however.

Let's get started by adding a physics body to a node and see what happens. Add the following code to Example1.swift.

Go ahead and test the project now. You will see the plane sitting at the top of the scene. Once you press on the screen, the plane will fall off the screen, and it will keep falling forever. This shows how simple it is to get started using physics—just add a physics body to a node and you are all set. 

The physicsBody Shape

The physicsBody property is of type SKPhysicsBody, which is going to be a rough outline of your node's shape... or a very precise outline of your node's shape, depending on which constructor you use to initialize this property. 

Here we have used the init(circleOfRadius:) initializer, which takes as a parameter the radius of the circle. There are several other initializers, including one for a rectangle or a polygon from a CGPath. You can even use the node's own texture, which would make the physicsBody a near exact representation of the node. 

To see what I mean, update the GameViewController.swift file with the following code. I have commented the line to be added.

Now the node's physicsBody will be outlined in green. In collision detection, the shape of physicsBody is what is evaluated. This example would have the circle around the plane guiding the collision detection, meaning that if a bullet, for example, were to hit the outer edge of the circle, then that would count as a collision.

circle body

Now add the following to Example2.swift.

Here we are using the sprite's texture. If you test the project now, you should see the outline has changed to a near exact representation of the sprite's texture.

texture body

Gravity

We set physicsBody's affectedByGravity property to false in the previous examples. As soon as you add a physics body to a node, the physics engine will take over. The result is that the plane falls immediately when the project is run! 

You can also set the gravity on a per node basis, as we have here, or you can turn off gravity altogether. Add the following to Example3.swift.

We can set the gravity using the physicsWorld gravity property. The gravity property is of type CGVector. We set both the dx and dy components to 0, and then when the screen is touched we set the dy property to -9.8. The components are measured in meters, and the default is (0, -9.8), which represents Earth’s gravity.

Edge Loops

As it stands now, any nodes added to the scene will just fall off the screen forever. We can add an edge loop around the scene using the init(edgeLoopFrom:) method. Add the following to Example4.swift.

Here we have added a physics body to the scene itself. The init(edgeLoopFrom:) takes as a parameter a CGRect that defines its edges. If you test now, you will see that the plane still falls; however, it interacts with this edge loop and no longer falls out of the scene. It also bounces and even turns a little on its side. This is the power of the physics engine—you get all this functionality out of the box for free. Writing something like this on your own would be quite complex.

Bounciness

We have seen that the plane bounces and turns on its side. You can control the bounciness and whether the physics body allows rotation. Enter the following into Example5.swift.

If you test now, you'll see that player is very bouncy and takes a few seconds to settle down. You will also notice that it no longer rotates. The restitution property takes a number from 0.0 (less bouncy) to 1.0 (very bouncy), and the allowsRotation property is a simple boolean.

Friction

In the real world, when two objects move against each other, there is a bit of friction between them. You can change the amount of friction a physics body has—this equates to the “roughness” of the body. This property must be between 0.0 and 1.0. The default is 0.2. Add the following to Example6.swift.

Here we create a rectangular Sprite and set the friction property on its physicsBody to 0.0. If you test now, you will see the plane very quickly glides down the rotated rectangle. Now change the friction property to 1.0 and test again. You will see the plane does not glide down the rectangle quite as fast. This is because of the friction. If you wanted it to move even more slowly, you could apply more friction to the player's physicsBody (remember the default is 0.2).

Density and Mass

There are a couple of other properties that you can change on the physics body, such as density and mass. The density and mass properties are interrelated, and when you change one, the other is automatically recalculated. When you first create a physics body, the body's area property is calculated and never changes afterward (it is read only). The density and mass are based on the formula mass = density * area.

When you have more than one node in a scene, the density and mass would affect the simulation of how the nodes bounce off each other and interact. Think of a basketball and a bowling ball—they're roughly the same size, but a bowling ball is much denser. When they collide, the basketball will change direction and velocity much more than the bowling ball.

Force and Impulse

You can apply forces and impulses to move the physics body. An impulse is applied immediately and only one time. A force, on the other hand, is usually applied for a continuous effect. The force is applied from the time you add the force until the next frame of the simulation is processed. To apply a continuous force, you would need to apply it on each frame.
Add the following to Example7.swift.

Run the project and wait till the player comes to rest on the bottom of the screen, and then tap on the player. You will see the player fly up the screen and eventually come to rest again at the bottom. We apply an impulse using the method applyImpulse(_:), which takes as a parameter a CGVector and is measured in Newton-seconds. 

Why not try the opposite and add a force to the player node? Remember you will need to add the force continuously for it to have the desired effect. One good place to do that is in the scene's update(_:) method. Also, you may want to try increasing the restitution property on the player to see how it affects the simulation.

Collision Detection

The physics engine has a robust collision and contact detection system. By default, any two nodes with physics bodies can collide. You have seen this in previous examples—no special code was required to tell the objects to interact. However, you can change this behaviour by setting a "category" on the physics body. This category can then be used to determine what nodes will collide with each other and also can be used to inform you when certain nodes are making contact.

The difference between a contact and a collision is that a contact is used to tell when two physics bodies are touching each other. A collision, on the other hand, prevents two physics bodies from crossing into each other's space—when the physics engine detects a collision, it will apply opposing impulses to bring the objects apart again. We have seen collisions in action with the player and the edge loop and the player and the rectangle from the previous examples.

Types of physicsBodies

Before we move on to setting up our Categories for the physics bodies, we should talk about the types of physicsBodies. There are three:

  1. A dynamic volume simulates objects with volume and mass. These objects are affected by forces and collisions in the physics world (e.g. the airplane in previous examples).
  2. A static volume is not affected by forces and collisions. However, because it does have volume itself, other bodies can bounce off and interact with it. You set the physics body's isDynamic property to false to create a static volume. These volumes are never moved by the physics engine. We saw this in action earlier with example six, where the airplane interacted with the rectangle, but the rectangle was not affected by the plane or by gravity. To see what I mean, go back to example six and remove the line of code which sets rectangle.physicsBody?.isDynamic = false.
  3. The third type of physics body is an edge, which is a static, volume-less body. We have seen this type of body in action with the edge loop we created around the scene in all the previous examples. Edges interact with other volume-based bodies, but never with another edge.

The categories use a 32-bit integer with 32 individual flags that can be either on or off. This also means you can only have a maximum of 32 categories. This should not present a problem for most games, but it is something to keep in mind.

Creating Categories

Create a new Swift file by going to File > New > File and making sure Swift File is highlighted.

New File

Enter PhysicsCategories as the name and press Create.

Physics Categories

Enter the following into the file you've just created.

We use a structure PhysicsCategories to create categories for Player, EdgeLoop, and RedBall. We are using bit shifting to turn the bits on.

Now enter the following in Example8.swift.

Here we create the player as usual, and create two variables dx and dy, which will be used as the components of a CGVector when we apply an impulse to the player.

Inside didMove(to:), we set up the player and add the categoryBitMask, contactBitMask, and collisionBitMask. The categoryBitMask should make sense—this is the player, so we set it to PhysicsCategories.Player. We are interested in when the player makes contact with the redBall, so we set the contactBitMask to PhysicsCategories.RedBall. Lastly, we want it to collide with and be affected by physics with the edge loop, so we set its collisionBitMask to PhysicsCategories.EdgeLoop. Finally, we apply an impulse to get it moving.

On the redBall, we just set its categoryBitMask. With the edgeLoop, we set its categoryBitMask, and because we are interested in when the player makes contact with it, we set its contactBitMask.

When setting up the contactBitMask and collisionBitMask, only one of the bodies needs to reference the other. In other words, you do not need to set up both bodies as contacting or colliding with the other.

For the edgeLoop, we set it to contact with the player. However, we could have instead set up the player to interact with the edgeLoop by using the bitwise or (|) operator. Using this operator, you can set up multiple contact or collision bit masks. For example:

To be able to respond when two bodies make contact, you have to implement the SKPhysicsContactDelegate protocol. You may have noticed this in the example code.

To respond to contact events, you can implement the didBegin(_:) and didEnd(_:) methods. They will be called when the two objects have begun making contact and when they have ended contact respectively. We'll stick with the didBegin(_:) method for this tutorial.

Here is the code once again for the didBegin(_:) method.

First, we set up two variables firstBody and secondBody. The two bodies in the contact parameter are not passed in a guaranteed order, so we'll use an if statement to determine which body has a lower contactBitMask and set that to firstBody.

We can now check and see which physics bodies are making contact. We check to see which physics bodies we are dealing with by anding (&&) the bodies' categoryBitMask with the PhysicsCategorys we set up previously, and if the result is non-zero we know we have the right body.

Finally, we print which bodies are making contact. If it was the player and edgeLoop, we also invert the dx and dy properties and apply an impulse to the player. This keeps the player constantly moving.

This concludes our study of SpriteKit's physics engine. There is a lot that was not covered such as SKPhysicsJoint, for example. The physics engine is very robust, and I highly suggest you read through all the various aspects of it, starting with SKPhysicBody.

Conclusion

In this post we learned about actions and physics—two very important parts of the SpriteKit framework. We looked at a lot of examples, but there is still a lot you can do with actions and physics, and the documentation is a great place to learn. 

In the next and final part of this series, we'll put together everything we have learned by making a simple game. Thanks for reading, and I will see you there!

In the meantime, check out some of our comprehensive courses on Swift and SpriteKit development!

Source: Tuts Plus

About the Author