Change the text of a SKLabelNode added from the scene editor - ios

I want to change the text of a SKLabelNode created in the scene editor named myScoreLabel to update a score on collision of two objects. Here's the relevant code:
class holeOne: SKScene, SKPhysicsContactDelegate {
var myScoreLabel: SKLabelNode!
var myscore:Int = 0
func addScore() {
myscore += 1
myScoreLabel.text = "\(myscore)"
}
func didBegin(_ contact: SKPhysicsContact) {
addScore()
}
}
At the moment after the collision the app crashes with "unexpectedly found nil while unwrapping an Optional value". What am I doing wrong and how can I do it right? Thanks!

From code that you provide var myScoreLabel: SKLabelNode! is not created.
Try to create SKLabelNode firstly. And then set value.
Example:
myScoreLabel = SKLabelNode(fontNamed: "Chalkduster")
myScoreLabel.text = "Test"
myScoreLabel.horizontalAlignmentMode = .right
myScoreLabel.position = CGPoint(x: 0, y:10)
addChild(scoreLabel)
Or you can connect it from .sks scene.
override func sceneDidLoad() {
if let label = self.childNode(withName: "myScoreLabel") as? SKLabelNode {
label.text = "Test" //Must add '.text' otherwise will not compile
}
}

Great, so in the end I did this:
class holeOne: SKScene, SKPhysicsContactDelegate {
var myScoreLabel: SKLabelNode!
var myscore:Int = 0
func addScore() {
myscore += 1
if let myScoreLabel = self.childNode(withName: "myScoreLabel") as? SKLabelNode {
myScoreLabel.text = "\(myscore)"
}
}
func didBegin(_ contact: SKPhysicsContact) {
addScore()
}
}

Related

WatchKit Move a SimpleSpriteNode in SpriteKit Game

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()
}
}

Passing a string between SKScenes using UserDefaults

In a game I'm developing with Swift, I want the player to be able to choose a background in a shop-like scene and thus change the background of every SKScene. I'm trying to accomplish this using UserDefaults, but for some reason it isn't working. Here's the important code of the Shop Scene (I removed the irrelevant code):
import SpriteKit
class ShopScene: SKScene {
var backNumber = 90
var backRemainder = 0
var background = SKSpriteNode()
var backName:String = "back1"
override func didMove(to view: SKView) {
background.texture = SKTexture(imageNamed: "\(backName)")
background.size = self.size
self.addChild(background)
let nextButton: NButton = NButton(defaultButtonImage: "next", activeButtonImage: "nextP", buttonAction: nextAction)
addChild(nextButton)
let selectButton: SButton = SButton(defaultButtonImage: "next", activeButtonImage: "nextP", buttonAction: selectAction)
addChild(selectButton)
}
func selectAction() {
UserDefaults.standard.set(backName, forKey: "backSaved")
let sceneToMoveTo = MainMenuScene(size: self.size)
sceneToMoveTo.scaleMode = self.scaleMode
let sceneTransition = SKTransition.fade(withDuration: 0.4)
self.view!.presentScene(sceneToMoveTo, transition: sceneTransition)
}
func nextAction() {
backNumber += 1
backRemainder = backNumber % 3
switch backRemainder {
case 0:
backName = "back1"
case 1:
backName = "back2"
case 2:
backName = "back3"
default:
backName = "back1"
}
background.texture = SKTexture(imageNamed: "\(backName)")
}
}
As you can see, when the select button is pressed backName is saved. Now, this is the relevant code of the Main Menu Scene:
Import SpriteKit
class MainMenuScene: SKScene {
var backName = UserDefaults.standard.string(forKey: "backSaved")
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: "\(backName)")
background.size = self.size
self.addChild(background)
When the 'Select' button is pressed, you should transition to Main Menu Scene and see that the background is the one you selected. However, I get a red X in a white background when I run this. I've worked before with UserDefaults to save scores, but I can't figure out why it's not working in this case. Any idea on how to pass strings between SKScenes using UserDefaults? Am I doing something wrong?
NOTE: Although Knight0fDragon's answer is marked correct, his answer works for passing a string between scenes but not for saving that value permanently. In you wish to pass the value and save it, check my answer.
I would recommend not doing UserDefaults, that is meant for preferences to your application. Instead use userData
func selectAction() {
let sceneToMoveTo = MainMenuScene(size: self.size)
sceneToMoveTo.scaleMode = self.scaleMode
sceneToMoveTo.userData = sceneToMoveTo.userData ?? NSMutableDictionary() //This lets us ensure userdata exists
sceneToMoveTo.userData!["backSaved"] = backName
let sceneTransition = SKTransition.fade(withDuration: 0.4)
self.view!.presentScene(sceneToMoveTo, transition: sceneTransition)
}
Then implement it with:
import SpriteKit
class MainMenuScene: SKScene {
lazy var backName:String = {return self.userData?["backSaved"] as? String ?? "back1"}() //This allows us to load backName at the time it is needed, or assign back1 if doesn't exist
override func didMove(to view: SKView) {
let background = SKSpriteNode(imageNamed: backName)
self.addChild(background)
Note, I am not sure if the as? String is needed, try it without it and see if Swift can infer it
Apparently, backName behaves as an optional because it can have no value, thus being nil. To make my code work I made these changes to Shop Scene:
import SpriteKit
class ShopScene: SKScene {
var backName:String? = UserDefaults.standard.string(forKey: "backSaved")
override func didMove(to view: SKView) {
if backName != nil {
backName = UserDefaults.standard.string(forKey: "backSaved")
} else {
backName = "back1"
}
background.texture = SKTexture(imageNamed: "\(backName!)")
self.addChild(background)
and these changes to Main Menu Scene:
import SpriteKit
class MainMenuScene: SKScene {
var backName:String? = UserDefaults.standard.string(forKey: "backSaved")
override func didMove(to view: SKView) {
if backName != nil {
backName = UserDefaults.standard.string(forKey: "backSaved")
} else {
backName = "back1"
}
let background = SKSpriteNode(imageNamed: "\(backName!)")
self.addChild(background)

Segue between SKScene and UIViewController

I'm writing simple math game with swift 3 and I have a problem with segues. I have two GameViewController's, oneUIViewController and one NavigationViewController.
I want to make two menus, one with game modes and one with difficulties, I want that the difficulties menu be interactive and have gravity so I decided to make it in SkScene. Everything works fine except segue between SkScene with dark screen and difficulties menu when user lost the game. I write a protocol which works when I'm going from main menu to second menu but when I'm going from SkScene with game it doesn't. I was trying to make a manual segue but protocol doesn't want to execute.
MyGVViewController code:
import UIKit
import SpriteKit
import GameplayKit
enum skad1 {
case game,menu
}
var skad = skad1.gra
class MyGVViewController: UIViewController, GameViewControllerDelegate {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "Menugame") {
let gameScene = scene as! MySKScene
gameScene.gameViewControllerDelegate = self
gameScene.scaleMode = .aspectFill
gameScene.size = view.bounds.size
// Present the scene
view.presentScene(gameScene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override var prefersStatusBarHidden: Bool {
return true
}
func goback() {
print("goback executed")
let view = self.view as! SKView?
view?.presentScene(nil)
navigationController?.popViewController(animated: true)
}
func goback2() {
print("goback2 executed")
let view = self.view as! SKView?
view?.presentScene(nil)
self.performSegue(withIdentifier: "comeback", sender: nil)
}
}
My SkScene with difficulties menu code:
import GameKit
import SpriteKit
import CoreMotion
protocol GameViewControllerDelegate: class {
func goback()
func goback2()
}
class MySKScene: SKScene {
//Variables
var motionManager: CMMotionManager!
var ball = SKSpriteNode()
var ball1 = SKSpriteNode()
var ball2 = SKSpriteNode()
var ball3 = SKSpriteNode()
var ball4 = SKSpriteNode()
weak var gameViewControllerDelegate: GameViewControllerDelegate?
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
if ball.contains(location){
showgame(zakres: 200)
}else if ball1.contains(location){
showgame(zakres: 100)
}else if ball2.contains(location) {
showgame(zakres: 50)
}else if ball3.contains(location) {
showgame(zakres: 150)
}else if ball4.contains(location) {
if skad == .menu{
print("should execut goback")
self.gameViewControllerDelegate?.goback()
}else{
print("should execut goback2")
self.gameViewControllerDelegate?.goback2()
}
}
}
}
func showgame(zakres:Int) {
if let view = self.view {
range = zakres
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "GameScene") {
// Set the scale mode to scale to fit the window
let gameScene = scene as! GameScene
gameScene.scaleMode = .aspectFill
// Present the scene
gameScene.size = view.bounds.size
view.presentScene(gameScene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override func didMove(to view: SKView) {
ball = self.childNode(withName: "ball") as! SKSpriteNode
ball1 = self.childNode(withName: "ball1") as! SKSpriteNode
ball2 = self.childNode(withName: "ball2") as! SKSpriteNode
ball3 = self.childNode(withName: "ball3") as! SKSpriteNode
ball4 = self.childNode(withName: "ball4") as! SKSpriteNode
motionManager = CMMotionManager()
motionManager.startAccelerometerUpdates()
let border = SKPhysicsBody(edgeLoopFrom: self.frame)
border.friction = 0
border.restitution = 0
self.physicsBody = border
}
override func update(_ currentTime: TimeInterval) {
if let accelerometerData = motionManager.accelerometerData {
physicsWorld.gravity = CGVector(dx: accelerometerData.acceleration.x * 20, dy: accelerometerData.acceleration.y * 35)
}
}
}
PS.
I'm reading what I wrote and I have to correct it, in difficulties menu I have a button which execute protocol that hide SKScene and show main menu. It works only when I'm going from main menu but when segue is from game when user lost, protocol isn't executed

Updating nodes in a SpriteKit scene from an outside UIViewController

How can I update SpriteKit Scene from outside the scene itself. E.g. how do I update a SKLabelNode by pressing a UIButton.
Scene
class GameScene: SKScene {
var myLabel:SKLabelNode = SKLabelNode()
override func didMoveToView(view: SKView) {
myLabel.text = "Initial State"
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(myLabel)
}
func didPressButton() {
myLabel.text = "Pressed! πŸ‘»"
}
ViewController
import UIKit
import SpriteKit
class GameViewController: UIViewController {
#IBAction func didPressButton(sender: AnyObject) {
if let scene = GameScene(fileNamed:"GameScene") {
scene.didPressButton()
}
}
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
let skView = self.view as! SKView
skView.presentScene(scene)
}
}
…
}
I understand that the usual touchesBegan inside the scene works just fine for updating the SKLabelNode inside the scene. But I am specifically interested in updating the scene from outside events. Any ideas?
In the file where is your button:
protocol PauseBtnSelectorDelegate {
func didPressPauseBtn(pauseBtn:SKSpriteNode)
}
class HUD: SKNode {
var pauseBtnDelegate:PauseBtnSelectorDelegate?
var pauseBtn :SKSpriteNode!
...
func pauseBtnTap(sender:SKSpriteNode) {
print("pauseBtn touched")
pauseBtnDelegate!.didPressPauseBtn(sender)
}
}
In the file where you want to notify the tap event:
class GameScene: SKScene,PauseBtnSelectorDelegate {
var hud: HUD!
...
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
// Adding HUD
self.hud = HUD(currentScene: self,nodeSize: self.frame.size)
self.hud.pauseBtnDelegate = self
self.addChild(hud)
}
func didPressPauseBtn(pauseBtn:SKSpriteNode) {
print("pauseBtn touched")
// Pause the game
}
}

Adding a sprite to GameScene from another class

I want to add a new sprite to my GameScene within my new file "Constructor.swift" Constructor class. I tried to make a getter within my GameViewController so I could get the current instance of my GameScene and then adding the sprite to it but this doesn't work.
Conclusion;
How can I get the current instance of my GameScene in another class, so my code would be easy maintainable because of all the sprites getting constructed in the constructor class. I want to prevent getting massive amount lines of code in my "GameScene.sks" file.
You can subclassing with a generic class named for example:
class MyDefaultScene: SKScene {
var sprite1: SKSpriteNode!
var followTrack: SKAction!
var followTrackForever: SKAction!
var clockWise : Bool = false
func setDefaultPhisics() {
self.physicsBody!.mass = 0
self.physicsBody!.friction = 0
self.physicsBody!.affectedByGravity = false
self.physicsBody?.linearDamping = 0
self.physicsBody?.angularDamping = 0
self.physicsBody?.restitution = 1
self.physicsBody?.dynamic = false
}
...
}
class GameScene1: MyDefaulScene {
override func didMoveToView(view: SKView) {
print("βˆ™ \(NSStringFromClass(self.dynamicType))")
print("clockwise: \(self.clockWise)")
}
}
class GameScene2: MyDefaulScene {
override func didMoveToView(view: SKView) {
print("βˆ™ \(NSStringFromClass(self.dynamicType))")
print("clockwise: \(self.clockWise)")
}
}

Resources