Created
July 17, 2018 00:51
-
-
Save topherPedersen/9d44e2831e879c1ea97c0fcfe103bb5f to your computer and use it in GitHub Desktop.
Vertical Scene Scrolling in SpriteKit (using multiple images)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This gist is comprised of two swift files which demonstrate how to do | |
// vertical scene scrolling with SpriteKit using two images. This code | |
// is based on the example app from 'Beginning Swift Games Development for iOS' | |
// by James Goodwill and Wesley Matlock. In Goodwill's example app from the | |
// book, SuperSpaceMan, the app uses a single background image for scene | |
// scrolling, and scrolling is limited to the size of the single background. | |
// I was interested in achieving the same effect using multiple images to | |
// allow for further scrolling, so I've gone ahead and modified Goodwill's | |
// app and posted it here for my own personal reference. Specifically, this | |
// code is being posted for use in my app team class at theCoderSchool in | |
// Flower Mound, TX where I teach children to code. | |
// | |
// GameScene.swift | |
// SuperSpaceMan | |
// | |
// Created by Christopher Pedersen on 6/28/18. | |
// Copyright © 2018 Christopher Pedersen. All rights reserved. | |
// | |
// import the SpriteKit 2D game development framework | |
import SpriteKit | |
// import the CoreMotion framework so we can access the devices accelerometer | |
import CoreMotion | |
class GameScene: SKScene { | |
// Declare & Instantiate Background and Player Sprite Objects | |
let backgroundNode = SKSpriteNode(imageNamed: "Background") | |
let nextBackgroundNode = SKSpriteNode(imageNamed: "NextBackground") | |
let foregroundNode = SKSpriteNode() | |
let playerNode = SKSpriteNode(imageNamed: "Player") | |
// declare game-state variables | |
var impulseCount = 4 | |
// create an instance of CMMotionManager | |
// This will allow us to access the devices accelerometer | |
let coreMotionManager = CMMotionManager() | |
// Add Bit Masks | |
let CollisionCategoryPlayer: UInt32 = 0x1 << 1 | |
let CollisionCategoryPowerUpOrbs: UInt32 = 0x1 << 2 | |
func addOrbsToForeground() { | |
var orbNodePosition = CGPoint(x: playerNode.position.x, y: playerNode.position.y + 100) | |
var orbXShift: CGFloat = -1.0 | |
for _ in 1...50 { | |
let orbNode = SKSpriteNode(imageNamed: "PowerUp") | |
if orbNodePosition.x - (orbNode.size.width * 2) <= 0 { | |
orbXShift = 1.0 | |
} | |
if orbNodePosition.x + orbNode.size.width >= size.width { | |
orbXShift = -1.0 | |
} | |
orbNodePosition.x += 40.0 * orbXShift | |
orbNodePosition.y += 120 | |
orbNode.position = orbNodePosition | |
orbNode.physicsBody = SKPhysicsBody(circleOfRadius: orbNode.size.width / 2) | |
orbNode.physicsBody?.isDynamic = false | |
orbNode.physicsBody?.categoryBitMask = CollisionCategoryPowerUpOrbs | |
orbNode.physicsBody?.collisionBitMask = 0 | |
orbNode.name = "POWER_UP_ORB" | |
foregroundNode.addChild(orbNode) | |
} | |
} | |
required init?(coder aDecoder: NSCoder) { | |
super.init(coder: aDecoder) | |
} | |
override init(size: CGSize) { | |
super.init(size: size) | |
// Make GameScene the delegate of the scene's physicsWorld.contactDelegate | |
physicsWorld.contactDelegate = self | |
physicsWorld.gravity = CGVector(dx: 0.0, dy: -5.0) | |
backgroundColor = SKColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 1.0) | |
// Enable User Input (enabling allows user to control the game with touch input) | |
isUserInteractionEnabled = true | |
// adding the background | |
backgroundNode.anchorPoint = CGPoint(x: 0.5, y: 0.0) | |
backgroundNode.position = CGPoint(x: size.width / 2.0, y: 0.0) | |
addChild(backgroundNode) | |
// add another background image (this background is placed above the first background on the y-axis) | |
nextBackgroundNode.anchorPoint = CGPoint(x: 0.5, y: 0.0) | |
nextBackgroundNode.position = CGPoint(x: size.width / 2.0, y: 1000.0) | |
// print("backgroundNode.position.y: \(backgroundNode.position.y)") | |
// print("backgroundNode.size.height: \(backgroundNode.size.height)") | |
addChild(nextBackgroundNode) | |
// add the foreground (holds sprites allowing background to scroll past) | |
addChild(foregroundNode) | |
// add the player | |
playerNode.physicsBody = SKPhysicsBody(circleOfRadius: playerNode.size.width / 2) | |
playerNode.physicsBody?.isDynamic = false | |
playerNode.position = CGPoint(x: size.width / 2.0, y: 180.0) | |
playerNode.physicsBody?.linearDamping = 1.0 | |
playerNode.physicsBody?.allowsRotation = false | |
playerNode.physicsBody?.categoryBitMask = CollisionCategoryPlayer | |
playerNode.physicsBody?.contactTestBitMask = CollisionCategoryPowerUpOrbs | |
playerNode.physicsBody?.collisionBitMask = 0 | |
foregroundNode.addChild(playerNode) | |
// add orbs | |
addOrbsToForeground() | |
} | |
// define what happens when a user taps | |
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { | |
if !playerNode.physicsBody!.isDynamic { | |
playerNode.physicsBody?.isDynamic = true | |
coreMotionManager.accelerometerUpdateInterval = 0.3 // set coreMotionManager update rate in seconds | |
coreMotionManager.startAccelerometerUpdates() // set event listener (accelerometer) | |
} | |
if impulseCount > 0 { | |
playerNode.physicsBody!.applyImpulse(CGVector(dx: 0.0, dy: 40.0)) | |
// impulseCount decrement below disabled for experimentation purposes | |
// impulseCount -= 1 | |
} | |
} | |
// This method is automatically called by default whenever a collision is detected. | |
// However, for this method to be called automatically we must extend the current | |
// class, GameScene, using SKPhysicsContactDelegate (see 'extension GameScene' below) | |
func didBegin(_ contact: SKPhysicsContact) { | |
let nodeB = contact.bodyB.node! | |
if nodeB.name == "POWER_UP_ORB" { | |
// give player additional "power ups" by incrementing the impulseCount | |
impulseCount += 1 | |
// remove orb sprite upon collision detection | |
nodeB.removeFromParent() | |
} | |
} | |
// override the didSimulatePhysics method so we can add our own custom code. | |
// Specifically, we will be adding code that responds to accelerometer events. | |
// The game will thus respond to accelerometer events when appropriate every | |
// time the didSimulatePhysics method is called. | |
override func didSimulatePhysics() { | |
if let accelerometerData = coreMotionManager.accelerometerData { | |
playerNode.physicsBody!.velocity = CGVector(dx: CGFloat(accelerometerData.acceleration.x * 380.0), dy: playerNode.physicsBody!.velocity.dy) | |
} | |
if playerNode.position.x < -(playerNode.size.width / 2) { | |
playerNode.position = CGPoint(x: size.width - playerNode.size.width / 2, y: playerNode.position.y) | |
} else if playerNode.position.x > self.size.width { | |
playerNode.position = CGPoint(x: playerNode.size.width / 2, y: playerNode.position.y) | |
} | |
} | |
// override the update method so we can add our own code | |
// the update method serves as a sort of main game loop which | |
// is called repeatedly while the game is running | |
override func update(_ currentTime: TimeInterval) { | |
/* | |
if playerNode.position.y >= 180.0 { | |
backgroundNode.position = CGPoint(x: backgroundNode.position.x, y: -((playerNode.position.y - 180.0)/8)) | |
print("position: \(backgroundNode.position)") | |
foregroundNode.position = CGPoint(x: foregroundNode.position.x, y: -(playerNode.position.y - 180.0)) | |
nextBackgroundNode.position = CGPoint(x: nextBackgroundNode.position.x, y: nextBackgroundNode.position.y - 180.0) | |
} | |
*/ | |
if playerNode.position.y >= 180.0 { | |
backgroundNode.position = CGPoint(x: backgroundNode.position.x, y: -((playerNode.position.y - 180.0)/8)) | |
print("position: \(backgroundNode.position)") | |
foregroundNode.position = CGPoint(x: foregroundNode.position.x, y: -(playerNode.position.y - 180.0)) | |
nextBackgroundNode.position = CGPoint(x: nextBackgroundNode.position.x, y: -((playerNode.position.y - 180.0)/8) + 1000.0) | |
} | |
} | |
// turn off accelerometer updates when the GameScene is no longer used | |
deinit { | |
coreMotionManager.stopAccelerometerUpdates() | |
} | |
} | |
extension GameScene: SKPhysicsContactDelegate { | |
} | |
// | |
// GameViewController.swift | |
// SuperSpaceMan | |
// | |
// Created by Christopher Pedersen on 6/28/18. | |
// Copyright © 2018 Christopher Pedersen. All rights reserved. | |
// | |
import SpriteKit | |
class GameViewController: UIViewController { | |
var scene: GameScene! | |
override var prefersStatusBarHidden: Bool { | |
return true | |
} | |
override func viewDidLoad() { | |
super.viewDidLoad() | |
// 1. Configure the main view | |
let skView = view as! SKView | |
skView.showsFPS = true | |
// 2. Create and configure our game scene | |
scene = GameScene(size: skView.bounds.size) | |
scene.scaleMode = .aspectFill | |
// 3. Show the scene. | |
skView.presentScene(scene) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment