An Interest In:
Web News this Week
- April 14, 2024
- April 13, 2024
- April 12, 2024
- April 11, 2024
- April 10, 2024
- April 9, 2024
- April 8, 2024
SpriteKit Basics: Putting It All Together
In this post we'll build a simple game from scratch. Along the way, we'll touch on some of the most important aspects of the SpriteKit library.
This post builds on what we've learned earlier in the SpriteKit Basics series. If you want to refresh your SpriteKit knowledge, take a look at some of my other posts.
- SpriteKitIntroducing SpriteKit
- SpriteKitSpriteKit Basics: Nodes
- iOS SDKSpriteKit Basics: Sprites
- SpriteKitSpriteKit Basics: Actions and Physics
New Project
Open Xcode and start a new project from the menu File > New > Project. Make sure iOS is selected and choose Game as your template.
Give your project a name, and make sure that Language is set to Swift, Game Technology is set to SpriteKit, and Devices is set to iPad.
Planning the Game Scenes
One of the first things I like to do when creating a project is to determine how many scenes I will need for the project. I will usually have at least three scenes: an intro scene, a main game scene, and a scene to show high scores, etc.
For this example, we just need an intro and main gameplay scene since we won't be keeping track of lives, scores, etc. SpriteKit already comes with one scene when you create a new project, so we just need an intro scene.
From Xcode's menu, choose File > New > File. Make sure iOS is selected, and choose Cocoa Touch Class.
Name the class StartGameScene, and make sure that Subclass of is set to SKScene and Language is set to Swift.
Setting Up GameViewController
Open GameViewController.swift. Delete everything in that file and replace it with the following.
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let scene = StartGameScene(size: view.bounds.size)
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = false
scene.scaleMode = .aspectFill
skView.presentScene(scene)
}
override var prefersStatusBarHidden: Bool {
return true
}
}
When you create a new project, GameViewController.swift is set up to load GameScene.sks from disk. GameScene.sks is used along with SpriteKit's built-in scene editor, which allows you to visually lay out your projects. We will not be using GameScene.sks, and will instead create everything from code, so here we initiate a new instance of StartGameScene and present it.
Create the Intro Scene
Add the following to the newly created StartGameScene.swift.
import UIKit
import SpriteKit
class StartGameScene: SKScene {
override func didMove(to view: SKView){
scene?.backgroundColor = .blue
let logo = SKSpriteNode(imageNamed: "bigplane")
logo.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(logo)
let newGameBtn = SKSpriteNode(imageNamed: "newgamebutton")
newGameBtn.position = CGPoint(x: size.width/2, y: size.height/2 - 350)
newGameBtn.name = "newgame"
addChild(newGameBtn)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
let touchedNode = self.atPoint(touchLocation)
if(touchedNode.name == "newgame"){
let newScene = GameScene(size: size)
newScene.scaleMode = scaleMode
view?.presentScene(newScene)
}
}}
This scene is pretty simple. In the didMove
method, we add a logo and a button. Then, in touchesBegan
, we detect touches on the new game button and respond by loading the main scene GameScene
.
Planning Game Classes
The next thing I like to do when creating a new game is decide which classes I will need. I can tell right away that I will need a Player
class and an Enemy
class. Both of these classes will extend SKSpriteNode
. I think for this project we will just create the player and enemy bullets right from within their respective classes. You could make separate player bullet and enemy bullet classes if you prefer, and I suggest you try to do that as an exercise on your own.
Lastly, there are the islands. These do not have any specific functionality but to move down the screen. In this case, since they're just decorations, I think it's also okay not to create a class, and instead just create them in the main GameScene
.
Creating the Player
Class
From Xcode's menu, choose File > New > File. Make sure iOS is selected and choose Cocoa Touch Class.
Make sure that Class is set to Player, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
Now add the following to Player.swift.
import UIKit
import SpriteKit
class Player: SKSpriteNode {
private var canFire = true
private var invincible = false
private var lives:Int = 3 {
didSet {
if(lives < 0){
kill()
}else{
respawn()
}
}
}
init() {
let texture = SKTexture(imageNamed: "player")
super.init(texture: texture, color: .clear, size: texture.size())
self.physicsBody = SKPhysicsBody(texture: self.texture!,size:self.size)
self.physicsBody?.isDynamic = true
self.physicsBody?.categoryBitMask = PhysicsCategories.Player
self.physicsBody?.contactTestBitMask = PhysicsCategories.Enemy | PhysicsCategories.EnemyBullet
self.physicsBody?.collisionBitMask = PhysicsCategories.EdgeBody
self.physicsBody?.allowsRotation = false
generateBullets()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func die (){
if(invincible == false){
lives -= 1
}
}
func kill(){
let newScene = StartGameScene(size: self.scene!.size)
newScene.scaleMode = self.scene!.scaleMode
let doorsClose = SKTransition.doorsCloseVertical(withDuration: 2.0)
self.scene!.view?.presentScene(newScene, transition: doorsClose)
}
func respawn(){
invincible = true
let fadeOutAction = SKAction.fadeOut(withDuration: 0.4)
let fadeInAction = SKAction.fadeIn(withDuration: 0.4)
let fadeOutIn = SKAction.sequence([fadeOutAction,fadeInAction])
let fadeOutInAction = SKAction.repeat(fadeOutIn, count: 5)
let setInvicibleFalse = SKAction.run {
self.invincible = false
}
run(SKAction.sequence([fadeOutInAction,setInvicibleFalse]))
}
func generateBullets(){
let fireBulletAction = SKAction.run{ [weak self] in
self?.fireBullet()
}
let waitToFire = SKAction.wait(forDuration: 0.8)
let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
let fire = SKAction.repeatForever(fireBulletSequence)
run(fire)
}
func fireBullet(){
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.position.x = self.position.x
bullet.position.y = self.position.y + self.size.height/2
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
bullet.physicsBody?.categoryBitMask = PhysicsCategories.PlayerBullet
bullet.physicsBody?.allowsRotation = false
scene?.addChild(bullet)
let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y:(scene?.size.height)! + bullet.size.height), duration: 1.0)
let removeBulletAction = SKAction.removeFromParent()
bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction]))
}
}
Within the init()
method, we set up the physicsBody
and invoke generateBullets()
. The generateBullets
method repeatedly calls fireBullet()
, which creates a bullet, sets its physicsBody
, and moves it down the screen.
When the player loses a life, the respawn()
method is invoked. Within the respawn
method, we fade the plane in and out five times, during which time the player will be invincible. One the player has exhausted all the lives, the kill()
method is invoked. The kill method simply loads the StartGameScene
.
Creating the Enemy Class
Choose File > New > File from Xcode's menu. Make sure iOS is selected and choose Cocoa Touch Class.
Make sure that Class is set to Enemy, Subclass of: is set to SKSpriteNode, and Language is set to Swift.
Add the following to Enemy.swift.
import UIKit
import SpriteKit
class Enemy: SKSpriteNode {
init() {
let texture = SKTexture(imageNamed: "enemy1")
super.init(texture: texture, color: .clear, size: texture.size())
self.name = "enemy"
self.physicsBody = SKPhysicsBody(texture: self.texture!, size: self.size)
self.physicsBody?.isDynamic = true
self.physicsBody?.categoryBitMask = PhysicsCategories.Enemy
self.physicsBody?.contactTestBitMask = PhysicsCategories.Player | PhysicsCategories.PlayerBullet
self.physicsBody?.allowsRotation = false
move()
generateBullets()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
func fireBullet(){
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.position.x = self.position.x
bullet.position.y = self.position.y - bullet.size.height * 2
bullet.physicsBody = SKPhysicsBody(rectangleOf: bullet.size)
bullet.physicsBody?.categoryBitMask = PhysicsCategories.EnemyBullet
bullet.physicsBody?.allowsRotation = false
scene?.addChild(bullet)
let moveBulletAction = SKAction.move(to: CGPoint(x:self.position.x,y: 0 - bullet.size.height), duration: 2.0)
let removeBulletAction = SKAction.removeFromParent()
bullet.run(SKAction.sequence([moveBulletAction,removeBulletAction])
)
}
func move(){
let moveEnemyAction = SKAction.moveTo(y: 0 - self.size.height, duration: 12.0)
let removeEnemyAction = SKAction.removeFromParent()
let moveEnemySequence = SKAction.sequence([moveEnemyAction, removeEnemyAction])
run(moveEnemySequence)
}
func generateBullets(){
let fireBulletAction = SKAction.run{ [weak self] in
self?.fireBullet()
}
let waitToFire = SKAction.wait(forDuration: 1.5)
let fireBulletSequence = SKAction.sequence([fireBulletAction,waitToFire])
let fire = SKAction.repeatForever(fireBulletSequence)
run(fire)
}
}
This class is pretty similar to the Player
class. We set its physicsBody
and invoke generateBullets()
. The move()
simply moves the enemy down the screen.
Creating the Main Game Scene
Delete everything within GameScene.swift and add the following.
import SpriteKit
import GameplayKit
import CoreMotion
class GameScene: SKScene, SKPhysicsContactDelegate {
let player = Player()
let motionManager = CMMotionManager()
var accelerationX: CGFloat = 0.0
override func didMove(to view: SKView) {
physicsWorld.gravity = CGVector(dx:0.0, dy:0.0)
self.physicsWorld.contactDelegate = self
scene?.backgroundColor = .blue
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.categoryBitMask = PhysicsCategories.EdgeBody
player.position = CGPoint(x: size.width/2, y: player.size.height)
addChild(player)
setupAccelerometer()
addEnemies()
generateIslands()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
}
func addEnemies(){
let generateEnemyAction = SKAction.run{ [weak self] in
self?.generateEnemy()
}
let waitToGenerateEnemy = SKAction.wait(forDuration: 3.0)
let generateEnemySequence = SKAction.sequence([generateEnemyAction,waitToGenerateEnemy])
run(SKAction.repeatForever(generateEnemySequence))
}
func generateEnemy(){
let enemy = Enemy()
addChild(enemy)
enemy.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width - enemy.size.width))), y: size.height - enemy.size.height)
}
func didBegin(_ contact: SKPhysicsContact) {
var firstBody: SKPhysicsBody
var secondBody: SKPhysicsBody
if(contact.bodyA.categoryBitMask < contact.bodyB.categoryBitMask){
firstBody = contact.bodyA
secondBody = contact.bodyB
}else{
firstBody = contact.bodyB
secondBody = contact.bodyA
}
if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.Enemy != 0)){
player.die()
secondBody.node?.removeFromParent()
createExplosion(position: player.position)
}
if((firstBody.categoryBitMask & PhysicsCategories.Player != 0) && (secondBody.categoryBitMask & PhysicsCategories.EnemyBullet != 0)){
player.die()
secondBody.node?.removeFromParent()
}
if((firstBody.categoryBitMask & PhysicsCategories.Enemy != 0) && (secondBody.categoryBitMask & PhysicsCategories.PlayerBullet != 0)){
if(firstBody.node != nil){
createExplosion(position: (firstBody.node?.position)!)
}
firstBody.node?.removeFromParent()
secondBody.node?.removeFromParent()
}
}
func createExplosion(position: CGPoint){
let explosion = SKSpriteNode(imageNamed: "explosion1")
explosion.position = position
addChild(explosion)
var explosionTextures:[SKTexture] = []
for i in 1...6 {
explosionTextures.append(SKTexture(imageNamed: "explosion\(i)"))
}
let explosionAnimation = SKAction.animate(with: explosionTextures,
timePerFrame: 0.3)
explosion.run(SKAction.sequence([explosionAnimation, SKAction.removeFromParent()]))
}
func createIsland() {
let island = SKSpriteNode(imageNamed: "island1")
island.position = CGPoint(x: CGFloat(arc4random_uniform(UInt32(size.width - island.size.width))), y: size.height - island.size.height - 50)
island.zPosition = -1
addChild(island)
let moveAction = SKAction.moveTo(y: 0 - island.size.height, duration: 15)
island.run(SKAction.sequence([moveAction, SKAction.removeFromParent()]))
}
func generateIslands(){
let generateIslandAction = SKAction.run { [weak self] in
self?.createIsland()
}
let waitToGenerateIslandAction = SKAction.wait(forDuration: 9)
run(SKAction.repeatForever(SKAction.sequence([generateIslandAction, waitToGenerateIslandAction])))
}
func setupAccelerometer(){
motionManager.accelerometerUpdateInterval = 0.2
motionManager.startAccelerometerUpdates(to: OperationQueue(), withHandler: { accelerometerData, error in
guard let accelerometerData = accelerometerData else {
return
}
let acceleration = accelerometerData.acceleration
self.accelerationX = CGFloat(acceleration.x)
})
}
override func didSimulatePhysics() {
player.physicsBody?.velocity = CGVector(dx: accelerationX * 600, dy: 0)
}
}
We create an instance of Player
and an instance of CMMotionManager
. We are using the accelerometer to move the player in this game.
Within the didMove(to:)
method we turn off the gravity, set up the contactDelegate
, add an edge loop, and set the player
's position before adding it to the scene. We then invoke setupAccelerometer()
, which sets up the accelerometer, and invoke the addEnemies()
and generateIslands()
methods.
The addEnemies()
method repeatedly calls the generateEnemy()
method, which will create an instance of Enemy
and add it to the scene.
The generateIslands()
method works similarly to the addEnemies()
method in that it repeatedly calls createIsland()
which creates an SKSpriteNode
and adds it to the scene. Within createIsland()
, we also create an SKAction
that moves the island down the scene.
Within the didBegin(_:)
method, we check to see which nodes are making contact and respond by removing the appropriate node from the scene and invoking player.die()
if necessary. The createExplosion()
method creates an explosion animation and adds it to the scene. Once the explosion is finished, it is removed from the scene.
Conclusion
During this series, we learned some of the most important concepts used in almost all SpriteKit games. We ended the series by showing how simple it is to get a basic game up and running. There are still some improvements that could be made, like a HUB, high scores, and sounds (I included a couple of MP3s you can use for this in the repo). I hope you learned something useful throughout this series, and thanks for reading!
If you want to learn more about game programming with SpriteKit, check out one of our comprehensive video courses! You'll learn how to build a SpriteKit game from A to Z.
Original Link:
TutsPlus - Code
Tuts+ is a site aimed at web developers and designers offering tutorials and articles on technologies, skills and techniques to improve how you design and build websites.More About this Source Visit TutsPlus - Code