I am trying to detect when the player object collides with the other objects in my game. This is my current code:
import SpriteKit
class GameScene: SKScene {
let player = SKSpriteNode(imageNamed: “Box”)
override func didMoveToView(view: SKView) {
backgroundColor = SKColor.whiteColor()
player.position = CGPoint(x: size.width/2, y: size.height/2)
addChild(player)
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addObject),
SKAction.waitForDuration(1)
])
))
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(addSecondObject),
SKAction.waitForDuration(1)
])
))
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
player.position = location
}
}
func EndGame() {
println("GAME OVER")
}
func Collision() {
if (CGRectIntersectsRect(player.frame, object.frame )) {
[EndGame];
}
if (CGRectIntersectsRect(player.frame, object1.frame)) {
[EndGame];
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
func addObject() {
let object = SKSpriteNode(imageNamed: "object1”)
object.name = "object1”
object.position = CGPoint(x: size.width/4, y: size.height/4)
self.addChild(object)
}
func addSecondObject() {
let object = SKSpriteNode(imageNamed: "object2”)
object.name = "object2”
object.position = CGPoint(x: size.width/2, y: size.height/2)
self.addChild(object)
}
}
So you can see my collision code is this:
func Collision() {
if (CGRectIntersectsRect(player.frame, object.frame )) {
[EndGame];
}
if (CGRectIntersectsRect(player.frame, object1.frame)) {
[EndGame];
}
}
The problem is that because object and object 1 variables are private to func (addObject) and func (addSecondObject), I can't call them in the above code. When they collide, currently I just want EndGame() to run which prints "Game Over" in the console.
I don't know if the method I have taken for collision detection is correct, but any help would be great! Thanks :)
For very basic sprites, yes, its right. You can imagine a diagonal line image "/" would trigger a collision if the top left corner of it overlapped with something, even though the actual object doesn't overlap.
Apple has a nice page discussing collisions here : https://developer.apple.com/library/ios/documentation/GraphicsAnimation/Conceptual/CodeExplainedAdventure/HandlingCollisions/HandlingCollisions.html
Also, numerous youtube videos talk about how to get this sort of thing working. eg : https://www.youtube.com/watch?v=dX0KvKtc3_w
You need to make it so the collision detection code runs(possible on a timer or something) and then, perhaps, pass in the objects you want to check or make those objects members of the class (IBOutlet) or similar strategy to gain access to them.
You have to use SKPhysicsbody:
struct PhysicsCategory {
static let None : UInt32 = 0
static let Player : UInt32 = 0b1
static let Object : UInt32 = 0b10
}
player.physicsBody = SKPhysicsBody(rectangleOfSize: player.size) // 1
player.physicsBody?.dynamic = true // 2
player.physicsBody?.categoryBitMask = PhysicsCategory.Player// 3
player.physicsBody?.contactTestBitMask = PhysicsCategory.Object//4
player.physicsBody?.collisionBitMask = PhysicsCategory.None // 5
You set the SKPhysicsBody and make it the size of your player-object
Then you make it dynamic. That the physicsbody is dynamic.
That is the categoryBitMask. It's like the identifier of your SKNode. So that other SKNodes can register on you and say: "Hey I want to know if I touch that guy".
That's the contactTestbitMask which handles which SKNodes should interact with you. So you say "If that node touches me, say something".
What should happen, if this node collides.
For more informations and a nice starter tutorial, use raywenderlichs tutorial.
Related
I am making a game with Sprite Kit where the user has tap balls that pass through the screen. The balls are spawned every 1 second. However, if two balls have spawned and the user taps the first ball only the second (and any that have spawned after that) will be removed/recorded and not the one the user actually tapped.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
}
else {
gameOver()
}
}
}
override func didMove(to view: SKView) {
setupTracks()
createHUD()
self.run(SKAction.repeatForever(SKAction.sequence([SKAction.run {
self.createBall(forTrack: self.track)
}, SKAction.wait(forDuration: 2)])))
}
func createBall(forTrack track: Int) {
setLevel()
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
player?.zPosition = 1
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft()
}
}
I’m pretty sure it’s because you do:
player?.removeFromParent()
no matter which sprite is touched, but player is always the last sprite spawned. You’ve already assigned the node that was touched to node, so I think you need to do:
node.removeFromParent()
instead.
I want to build an inventory for my SpriteKit game. For this I want to go to another SKScene File which represents my inventory when I press the pause button. My problem is that when I make a transition from my InventoryScene back to my GameScene, the GameScene loads completely new. This is the transition code from my GameScene class:
func loadInventory(){
if !transitionInProgress{
transitionInProgress = true
if let scene = InventoryScene(fileNamed: "Inventory"){
Globals.InventoryGlobals.levelBeforeSwitch = currentLevel
scene.scaleMode = .aspectFill
let transition = SKTransition.push(with: .down, duration: 0.5)
self.view?.presentScene(scene, transition: transition)
}
}
}
With this code I'll go to my InventoryScene.
Now in my InventoryScene I want to go back to my GameScene with this:
func loadLevel(level: String){
if !transitionInProgress{
transitionInProgress = true
if let scene = GameScene(fileNamed: level){
scene.currentLevel = level
scene.scaleMode = .aspectFill
let transition = SKTransition.doorsOpenHorizontal(withDuration: 1)
self.view?.presentScene(scene, transition: transition)
}
}
}
The transitions are working but my problem is that the GameScene loads completely new, which is obviously because I instantiate a new GameScene. So when the player is in the middle of the level and then goes to the Inventory and back to the GameScene, the player is back at the beginning of the level. If I go from the Inventory back to the scene I want to be the GameScene as it was before (player position, enemy health, etc.)
Has anyone an idea how i can do this?
Retaining your scene is very simple, all you need to do is retain the scene with a strong reference
In your ViewController, it is as simple as storing a variable
class ViewController : UIViewController
{
var gameScene = GameScene(fileNamed:"GameScene")
}
Now for as long as your view controller is alive, your scene will be alive.
To access it, you just need to find a way to tell the MenuScene where your view controller is, then present the scene.
class ViewController : UIViewController
{
var gameScene = GameScene(fileNamed:"GameScene")
lazy var skView : SKView = self.view as! SKView
func gotoMenu()
{
let menu = MenuScene(fileNamed"MenuScene")
menu.viewController = self
skView.presentScene(menu)
}
}
class MenuScene : SKScene
{
var viewController : ViewController!
func returnToGame()
{
view.presentScene(viewcontroller.gameScene)
}
}
But, what if you don't want to use custom SKScene classes all the time, use a view controller, or would rather rely on components, why isn't there a convenient way to go back to a scene.
Well my friend, there is, and it is where userData comes into play
class GameScene : SKScene
{
func gotoMenu()
{
let menu = MenuScene(fileNamed:"MenuScene")
menu.userData = menu.userData ?? ["":Any]()
menu.userData["backToScene"] = self
view.presentScene(menu)
}
}
class MenuScene : SKScene
{
func returnToGame()
{
guard let userData = userData, let scene = userData["backToScene"] as? SKScene
view.presentScene(scene)
}
}
Since we are retaining it in the user data, we can now present the old scene anywhere we have access to the menu scene.
userData is also great in transferring inventory, of course I would create a class to manage the inventory, and just pass the reference via userData
Now, to create a menu that overlays the current scene, that is as simple as applying a new node onto your scene.
You can even use a separate SKS file to layout your menu, and overlay it:
class GameScene : SKScene
{
let menu = MenuScene(fileNamed:"MenuScene")
func overlayMenu()
{
scene.addChild(menu) //You probably want to add an SKCameraNode, and add it to the camera instead
}
override func update(currentTime: CFTimeInterval)
{
if menu.parent != nil
{
menu.update(currentTime:currentTime) //do this only when you need to have a constant update call, be sure to include additional functionality like `didFinishUpdate` in the approprate functions when needed
}
}
}
Of course now would be a good time to develop what is called a worldNode, also may be referred to as gameNode
Essentially what this node is, is the node that holds all your game elements.
This allows you to add overlay nodes that can pause your game.
Your scene hierarchy would like like this:
SKScene
--worldNode
----all nodes that belong in the game
--menuNode
----all nodes that belong on the menu
Now at any time, menu can set the worldNode's isPaused state to true, allowing the game to pause and still giving you the ability to interact with the menuNode
I do layover windows all the time in my Spritekit games, and it doesn't have to be as complicated as you are thinking. Here is how you can do it all in Spritekit without leaving the Scene.
Create a new class which is a subclass of SKSpriteNode for your InventoryDialog.
import SpriteKit
protocol InventoryDialogDelegate: class {
func close()
}
class InventoryDialog: SKSpriteNode {
private var closeButton: SKSpriteNode!
weak var delegate: InventoryDialogDelegate?
init(size: CGSize) {
super.init(texture: nil, color: .clear, size: size)
name = "inventoryDialog"
//do some background design work here
let background = SKSpriteNode(color: .white, size: self.size)
background.zPosition = 1
addChild(background)
closeButton = SKSpriteNode(texture: SKTexture(imageNamed: "closeButton"))
closeButton.position = CGPoint(x: self.size.width / 2 - closeButton.size.width / 2, y: self.size.height / 2 - closeButton.size.height / 2)
closeButton.zPosition = 2
addChild(closeButton)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
let touch = touches.first
let touchLocation = touch!.location(in: self)
if closeButton.contains(touchLocation) {
close()
}
}
func close() {
self.delegate?.close()
}
}
inside your GameScene file
class GameScene: SKScene {
var inventoryDialog: InventoryDialog!
var openButton: SKSpriteNode!
override func didMove(to view: SKView) {
openButton = SKSpriteNode(texture: SKTexture(imageNamed: "openButton"))
openButton.position = CGPoint(x: self.size.width / 2 - closeButton.size.width / 2, y: self.size.height / 2 - closeButton.size.height / 2)
openButton.zPosition = 2
addChild(openButton)
}
func displayInventoryDialog() {
backgroundBlocker = SKSpriteNode(imageNamed: "background3")
backgroundBlocker.size = self.size
backgroundBlocker.zPosition = 4999
addChild(backgroundBlocker)
inventoryDialog = InventoryDialog(size: CGSize(width: 500, height: 800))
inventoryDialog.delegate = self
inventoryDialog.zPosition = 5000
addChild(inventoryDialog)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
//pause any action that you don't want running while the dialog is open
gameLayer.isPaused = true
let touch = touches.first
let touchLocation = touch!.location(in: self)
if openButton.contains(touchLocation) {
displayInventoryDialog()
}
}
}
//MARK: - InventoryDialogDelegate Methods
extension GameScene: InventoryDialogDelegate {
func close() {
//at this point you could update any GUI nesc. based on what happened in your dialog
backgroundBlocker.removeFromParent()
inventoryDialog?.removeFromParent()
gameLayer.isPaused = false
}
}
here is exactly what you wanted.
class GameScene: SKScene {
class InventoryManager: UIView {
override init(frame: CGRect) {
super.init(frame: CGRect(x: 0, y: 0, width: 300, height: 200))
//here is where you set up anything you want to display inside the view
self.backgroundColor = UIColor.green
let Button = UIButton()
Button.frame = CGRect(x: 0, y: 0, width: 40, height: 20)
Button.backgroundColor = UIColor.red
let TouchView = UIView()
TouchView.frame = self.frame
Button.center.x = TouchView.center.x
Button.center.y = TouchView.center.y
TouchView.backgroundColor = UIColor.brown
self.addSubview(TouchView)
//make sure you add any objects that you want to the TouchView and not to the self
TouchView.addSubview(Button)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
var InventoryView = InventoryManager()
var Node = SKSpriteNode()
override func didMove(to view: SKView) {
//this keeps the view centered in the screen
InventoryView.center.x = (self.view?.center.x)!
InventoryView.center.y = (self.view?.center.y)!
Node = SKSpriteNode(color: UIColor.blue, size: CGSize(width: 40, height: 40))
Node.position = CGPoint(x: self.frame.size.width / 3, y: self.frame.size.height / 3)
self.addChild(Node)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if !(self.view?.frame.contains(location))! {
InventoryView.removeFromSuperview()
}
if Node.contains(location) && InventoryView.isDescendant(of: self.view!) {
self.view?.addSubview(InventoryView)
}
}
}
override func update(_ currentTime: TimeInterval) {
// Called before each frame is rendered
}
}
clicking the blue button adds it to the scene again and clicking anywhere else removes it from the scene
I would like to know if anyone has a way to move a SKSpriteNode in SpriteKit Watch Game Using WKCrownDelegate. either in Y direction or X direction
Hope this Helps Others that are Starting with WatchKit.
This is my GameElement.swift:
extension GameScene {
func addPlayer() {
player = SKSpriteNode(imageNamed: "Spaceship")
player.setScale(0.15)
player.position = CGPoint(x: 5, y: -60)
player.name = “ONE”
player.physicsBody?.isDynamic = false
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player2 = SKSpriteNode(imageNamed: "Spaceship")
player2.setScale(0.15)
player2.position = CGPoint(x: 5, y: -60)
player2.name = “ONE”
player2.physicsBody?.isDynamic = false
player2.physicsBody = SKPhysicsBody(rectangleOf: player2.size)
addChild(player)
addChild(player2)
playerPosition = player.position
}
}
This is my GameScene.swift:
class GameScene: SKScene, SKPhysicsContactDelegate, WKCrownDelegate {
var watchParticles:SKEmitterNode!
var player:SKSpriteNode!
var player2:SKSpriteNode!
var playerPosition:CGPoint!
override func sceneDidLoad() {
self.scaleMode = SKSceneScaleMode.aspectFill
watchParticles = SKEmitterNode(fileNamed: "watchParticles")
addChild(watchParticles)
self.physicsWorld.gravity = CGVector(dx: 0 , dy: 0)
physicsWorld.contactDelegate = self
addPlayer()
}
func moveSprite(player : SKSpriteNode,moveDirection: String){
switch moveDirection {
case "UP":
print("UP")
player.childNode(withName: "ONE")?.physicsBody?.applyImpulse(CGVector(dx: 60, dy: 0))
case "DOWN":
print("DOWN")
player.childNode(withName: "ONE")?.physicsBody?.applyImpulse(CGVector(dx: -60, dy: 0))
case "STOP":
print("STOPPED")
player.childNode(withName: "ONE")?.physicsBody?.velocity = CGVector(dx: 0, dy: 0)
default:
break
}
}
}
This is My InterfaceController.swift:
class InterfaceController: WKInterfaceController, WKCrownDelegate {
#IBOutlet var skInterface: WKInterfaceSKScene!
private var moveDirection = ""
private var game = GameScene()
private var player = GameScene()
override func awake(withContext context: Any?) {
super.awake(withContext: context)
crownSequencer.delegate = self
crownSequencer.focus()
// Configure interface objects here.
// Load the SKScene from 'GameScene.sks'
if let scene = GameScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
self.skInterface.presentScene(scene)
crownSequencer.delegate = self
crownSequencer.focus()
// Use a value that will maintain a consistent frame rate
self.skInterface.preferredFramesPerSecond = 30
}
}
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
if rotationalDelta > 0{
moveDirection = "UP"
game.moveSprite(player: player.player, moveDirection: moveDirection)
}else if rotationalDelta < 0{
moveDirection = "DOWN"
game.moveSprite(player: player.player, moveDirection: moveDirection)
}
}
func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
moveDirection = "STOP"
game.moveSprite(player: player.player, moveDirection: moveDirection)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
Welcome to SO!
Ok, there is a lot to unpack so get some popcorn... I think that you are on the right track here, mostly you need to check for nil when you get errors.
First, this is wrong in your interface controller, and the part that concerned me. Here, you are just instantiating new GameScene instances, which are completely separate from the gameScene instance created by your interface controller a few lines down. Then, you were sending the crown delegate functions to these totally empty gameScenes.:
private var game = GameScene() // You are referencing nothing here, just creating a new gamescene.
private var player = GameScene() // I don't think that player is supposed to be a gamescene!
I fixed it by doing assigning the actual gameScene you want to use to the properties (so they can be used by the crown delegate).
private var game: GameScene!
lazy private var player: SKSpriteNode = self.game.player
override func awake(withContext context: Any?) {
// ... Stuff...
if let scene = GameScene(fileNamed: "GameScene") {
game = scene
This was also changed to represent the new code in your crown delegates:
game.moveSprite(player: player, moveDirection: moveDirection)
In addPlayer you were doing this:
player.physicsBody?.isDynamic = true // This needs to go AFTER you init your pb.
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
...and I fixed it by swapping the lines.
Personally I like to do the following, to help ensure no small mistakes are made:
let pb = SKPhysicsBody(...)
pb.isDynamic = true
player.physicsBody = pb
moveSprite had a bunch of issues with it, so I'm not going to enumerate them as I did above. Check out what I did then ask me if you have any questions. Basically, you were doomed with this func from the start, because you were calling this method from the interface controller with the whacked out player values that were all nil.
Also, the .applyImpulse was giving me pretty bad controls, so I changed it to a plain adjustment of .position. There is still a small issue with coasting before the player stops, but that can be handled in another question :) (note, I only tested this on simulator.. may not be an issue on-device).
Also also, I hate errors caused by spelling mistakes in strings, so I converted this to an enum for you.
func moveSprite(player : SKSpriteNode, moveDirection: Direction) {
// This will give us an equal amount of pixels to move across the watch devices:
// Adjust this number for shorter / longer movements:
let percentageOfScreenToMovePerRotation = CGFloat(1) // One percent
let modifier = percentageOfScreenToMovePerRotation / 100
let amountToMove = self.frame.maxX * modifier
switch moveDirection {
case .UP:
player.position.x += amountToMove
case .DOWN:
player.position.x -= amountToMove
case .STOP:
break
}
}
The real moral of the story here is to check for nil. If you just use someOptional?.someMethod() all the time, then you likely will not be able to easily determine whether or not someMethod() is actually being called or not.. thus, you don't know if the problem is with the calling logic, the method, or with the object not existing, and etc.
Force unwrapping is frowned upon in production code, but IMO it is extremely valuable when first starting out--because it helps you to quickly identify errors.
Later on, you can start using things like if let and guard to help check for nil without crashing your programs, but that adds more clutter and complexity to your code when you are trying to just learn the basics of a new API and language.
And as a final tip, try to not use hard-coded strings whenever possible: put them into an enum or a constant as I have in your code:
// Because I hate string spelling erros, and you probably do too!
enum Direction {
case UP, DOWN, STOP
}
// Because I hate errors related to spelling in strings:
let names = (ONE: "ONE", TWO: "TWO")
Here are the two files in their entirety.. note, I had to comment out a few things to get it to work in my project:
GameScene:
// Because I hate string spelling erros, and you probably do too!
enum Direction {
case UP, DOWN, STOP
}
class GameScene: SKScene, SKPhysicsContactDelegate, WKCrownDelegate {
var watchParticles:SKEmitterNode!
var player: SKSpriteNode!
var player2: SKSpriteNode!
var playerPosition:CGPoint!
// Because I hate errors related to spelling in strings:
let names = (ONE: "ONE", TWO: "TWO")
func addPlayer() {
player = SKSpriteNode(color: .blue, size: CGSize(width: 50, height: 50))
// player = SKSpriteNode(imageNamed: "Spaceship")
// player.setScale(0.15)
player.position = CGPoint(x: 5, y: -60)
player.name = names.ONE
player.physicsBody = SKPhysicsBody(rectangleOf: player.size)
player.physicsBody!.isDynamic = true // This was placed *before* pb initialzier (thus never got called)
player2 = SKSpriteNode(color: .yellow, size: CGSize(width: 50, height: 50))
// player2 = SKSpriteNode(imageNamed: "Spaceship")
// player2.setScale(0.15)
player2.position = CGPoint(x: 5, y: -60)
player2.name = names.TWO
player2.physicsBody = SKPhysicsBody(rectangleOf: player2.size)
player2.physicsBody!.isDynamic = false // This was placed *before* pb initialzier (thus never got called)
addChild(player)
addChild(player2)
playerPosition = player.position
}
override func sceneDidLoad() {
self.scaleMode = SKSceneScaleMode.aspectFill
//watchParticles = SKEmitterNode(fileNamed: "watchParticles")
//addChild(watchParticles)
self.physicsWorld.gravity = CGVector.zero
physicsWorld.contactDelegate = self
addPlayer()
}
func moveSprite(player : SKSpriteNode, moveDirection: Direction) {
// This will give us an equal amount of pixels to move across the watch devices:
// Adjust this number for shorter / longer movements:
let percentageOfScreenToMovePerRotation = CGFloat(1) // One percent
let modifier = percentageOfScreenToMovePerRotation / 100
let amountToMove = self.frame.maxX * modifier
switch moveDirection {
case .UP:
player.position.x += amountToMove
case .DOWN:
player.position.x -= amountToMove
case .STOP:
break
}
}
}
InterfaceController:
class InterfaceController: WKInterfaceController, WKCrownDelegate {
#IBOutlet var skInterface: WKInterfaceSKScene!
private var moveDirection = Direction.STOP
private var game: GameScene!
lazy private var player: SKSpriteNode = self.game.player
override func awake(withContext context: Any?) {
super.awake(withContext: context)
crownSequencer.delegate = self
crownSequencer.focus()
if let scene = GameScene(fileNamed: "GameScene") {
game = scene // VERY IMPORTANT!
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
self.skInterface.presentScene(scene)
crownSequencer.delegate = self
crownSequencer.focus()
// Use a value that will maintain a consistent frame rate
self.skInterface.preferredFramesPerSecond = 30
}
else {
fatalError("scene not found")
}
}
func crownDidRotate(_ crownSequencer: WKCrownSequencer?, rotationalDelta: Double) {
if rotationalDelta > 0{
moveDirection = .UP
game.moveSprite(player: player, moveDirection: moveDirection)
} else if rotationalDelta < 0{
moveDirection = .DOWN
game.moveSprite(player: player, moveDirection: moveDirection)
}
}
func crownDidBecomeIdle(_ crownSequencer: WKCrownSequencer?) {
moveDirection = .STOP
game.moveSprite(player: player, moveDirection: moveDirection)
}
override func willActivate() {
// This method is called when watch view controller is about to be visible to user
super.willActivate()
}
override func didDeactivate() {
// This method is called when watch view controller is no longer visible
super.didDeactivate()
}
}
So for a school project, I have been tasked with making a 2D game. The game is fine but I'm struggling with how to make a back button (In the middle of the page) so was wondering if there was specific code to make this work. I am using spriteKit so I'm trying to go back to the previous scene after clicking on a colour sprite.
I apologise if this is a stupid question but I am slightly new to Swift.
Kind Regards,
James
Here is an example of how you can create a button using a colored sprite. It shows how you can set up a button to receive touch events and how you can use those touch events to navigate between scenes.
In this example you can navigate forward to new scenes and backwards to previous scenes.
import SpriteKit
class Button: SKSpriteNode {
var tapped: (() -> Void)?
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
tapped?()
}
}
class GameScene: SKScene {
var parentScene: SKScene?
var sceneCount = 1
override func didMove(to view: SKView) {
if parentScene != nil {
let backButton = addButton(color: .red, position: CGPoint(x: -200, y: 0))
backButton.tapped = {
if let previousScene = self.parentScene {
view.presentScene(previousScene)
}
}
}
let nextButton = addButton(color: .blue, position: CGPoint(x: 200, y: 0))
nextButton.tapped = {
if let nextScene = SKScene(fileNamed: "GameScene") as? GameScene {
nextScene.scaleMode = self.scaleMode
nextScene.parentScene = self
nextScene.sceneCount = self.sceneCount + 1
view.presentScene(nextScene)
}
}
let label = SKLabelNode(text: "Scene \(sceneCount)")
addChild(label)
}
func addButton(color: SKColor = .white, position: CGPoint = .zero) -> Button {
let button = Button(color: color, size: CGSize(width: 200, height: 200))
button.position = position
button.isUserInteractionEnabled = true
addChild(button)
return button
}
}
Too add a button the simplest way is to detect touches on your sprite(s) in the relevant SKScene.
enum NodeName: String {
case coloredSprite1
case coloredSprite2
}
class GameScene: SKScene {
let coloredSprite = SKSpriteNode(imageNamed: "YourImageName")
/// Scene setup
override func didMove(to view: SKView) {
// set up your colored sprite if necessary
// Give your sprites unique names to identify them
coloredSprite.name = NodeName.coloredSprite1.rawValue // always use enums for things like string identifiers so you avoid typos
}
/// Touches
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let touchedNode = atPoint(location)
// Way 1 by node (probably less preferable)
switch touchedNode {
case coloredSprite:
// do something (e.g call loadScene method)
// see below
default:
break
}
// Way 2 by node name (probably more preferable)
// name is an optional so we have to unwrap it when using it in the switch statement.
// The easiest way is by providing an alternative string, by using the nil coalescing operator (?? "NoNodeNameFound")
switch touchedNode.name ?? "NoNodeNameFound" {
case NodeName.coloredSprite1.rawValue:
// do something (e.g call loadScene method)
// see below
default:
break
}
}
}
// Also call touchesEnded, touchesMoved and touchesCancelled and do necessary stuff
}
For a more reusable solution you ideally want to create a button subclass. There is quite a few tutorials to google on how to do this.
To than transition between SKScenes you can create a loadScene method in each scene and than call them when necessary.
// Start Scene
class StartScene: SKScene {
...
func loadGameScene() {
// If you do everything in code
let gameScene = GameScene(size: self.size)
view?.presentScene(gameScene, transition: ...)
// If you use SpriteKit scene editor
guard let gameScene = SKScene(fileNamed: "GameScene") else { return } // fileNamed is the name you gave the .sks file
view?.presentScene(gameScene, transition: ...)
}
}
// Game scene
class GameScene: SKScene {
....
func loadStartScene() {
// If you do everything in code
let startScene = StartScene(size: self.size)
view?.presentScene(startScene, transition: ...)
// If you use SpriteKit scene editor
guard let startScene = SKScene(fileNamed: "StartScene") else { return } // fileNamed is the name you gave the .sks file
view?.presentScene(startScene, transition: ...)
}
}
Hope this helps
I have 2 different textures for my character that overlap/are displayed too fast
while moving the character. How can I set a duration for the animation, so the textures always switch at the same speed while moving the character?
This is my code:
override func touchesMoved(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
let animatePlayerStart = SKAction.setTexture(SKTexture(imageNamed: "Player\(i).png"))
// Determine speed for character movement
var minDuration:CGFloat = 0.7;
var maxDuration:CGFloat = 1.8;
var rangeDuration:CGFloat = maxDuration - minDuration;
var actualDuration:NSTimeInterval = NSTimeInterval((CGFloat(arc4random())%rangeDuration) + minDuration)
let move = SKAction.moveTo(location, duration:actualDuration)
player.runAction(SKAction.sequence([animatePlayerStart, move]))
// i determines which texture is going to be displayed
if(self.i == 2) {
self.i = 1
}
else{
self.i++
}
}
}
You are changing texture in touchesMoved which is called fast, thus the effect you are currently getting. To change textures after pre-defined period of time you can use this method:
+ animateWithTextures:timePerFrame:
import SpriteKit
class GameScene: SKScene {
let hero = SKSpriteNode(imageNamed: "heroState_A")
let textureA = SKTexture(imageNamed: "heroState_A")
let textureB = SKTexture(imageNamed: "heroState_B")
override func didMoveToView(view: SKView) {
/* Setup your scene here */
//Because hero is already initialized with textureA, start from textureB
let animation = SKAction.animateWithTextures([textureB,textureA], timePerFrame:0.5)
hero.position = CGPoint(x: CGRectGetMidX(frame), y: CGRectGetMidY(frame))
addChild(hero)
//Start animation
hero.runAction(SKAction.repeatActionForever(animation),withKey:"heroAnimation")
//Removing heroAnimation
//You can stop this animation by hero.removeAllActions, but if you run animation with key, you can remove just that particular action, which gives you more control
let stop = SKAction.runBlock({
if(self.hero.actionForKey("heroAnimation") != nil){
self.hero.removeActionForKey("heroAnimation")
}
})
//Just an example, in real app you will do this at certain events (eg. when player stops his movement)
runAction(SKAction.sequence([SKAction.waitForDuration(5),stop]))
}
}