I'm building a PauseButton class in my SpriteKit game, and I want to draw a label to the GameScene when it is activated. However, I'm required to instantiate the button outside of didMoveToView (right before it- in GameScene) so that I can access it more "globally" from the touchBegan method.
let pauseButton = PauseButton(theTexture: pauseButtonTexture,gameScene:GameScene)
override func didMoveToView(view: SKView) {
Ideally I would be able to pass in the view by instantiating it within didMoveToView but as of now I don't know how to do this and also access it from the rest of the scene. That said, is there a way to pass in the current GameScene instance I'm working with so that I can do something like gameScene.addchild(label) from inside a method of my button class? I couldn't find anything useful on this, so any help would be great!
You basically can do it 2 ways.
1) Just create the property like this
var pauseButton: PauseButton!
and than instantiate it in didMoveToView like so.
override func didMoveToView(view: SKView) {
pauseButton = PauseButton(theTexture: pauseButtonTexture,gameScene: self)
addChild(pauseButton)
}
2) Use lazy instantiation so you can do what you tried to do initially.
A lazy var in in simplest form allows you to use self without doing step 1.
lazy var pauseButton: PauseButton = PauseButton(theTexture: pauseButtonTexture,gameScene: self)
override func didMoveToView(view: SKView) {
You could even do it like so to add more properties of this button all in the same spot.
lazy var pauseButton: PauseButton = {
let button = PauseButton(theTexture: pauseButtonTexture,gameScene: self)
button.zPosition = 200
button.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
button.alpha = 0.5
return button
}()
override func didMoveToView(view: SKView) {
addChild(pauseButton)
}
Remember every SKNode/SKSpriteNode etc has a scene property. If your button class is a subclass of SKNode/SKSpriteNode than you can get its scene property and use the "as" operator to get a reference to GameScene without passing in the scene in the initialisation method.
e.g
class GameScene: SKScene {
lazy var pauseButton: PauseButton = PauseButton(theTexture: pauseButtonTexture,gameScene: self)
var someBool = false
override func didMoveToView(view: SKView) {
addChild(pauseButton)
pauseButton.loadPausedLabel()
}
func someMethod() {
}
}
class PauseButton: SKSpriteNode {
func loadPauseLabel() {
// Way 1, reference to current scene but not to specific class
guard let scene = scene else { return }
// Scene property is optional and is nil until button is added to a Scene so add the guard check.
scene.someBool = true // NOT WORKING
scene.someMethod() // NOT WORKING
let label = SKLabelNode(...
scene.addChild(label) // WORKING
// Way 2, use "as" to cast to specific scene (e.g GameScene) so you can call properties/methods on it. This is the same as passing GameScene in the init method.
guard let gameScene = scene as? GameScene else { return }
// Scene property is optional and is nil until button is added to a Scene so add the guard check.
// The button must be added to the scene you are trying the as? cast with, in this example GameScene.
gameScene.someBool = true // WORKING
gameScene.someMethod() // WORKING
let label = SKLabelNode(...
gameScene.addChild(label) // WORKING
}
}
As a tip, I would not add the pause label in the pauseButton subclass, I would add it from within the scene it self or from a pause menu subclass.
Also I would not subclass each button, I would crate 1 class that can be used for all buttons in your game. You could create an enum with button names and pass that into the init method to distinguish between your buttons when they are pressed.
Your button subclass should only handle animations, textures etc. You can check out apples game DemoBots for an example.
Hope this helps
Related
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 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)")
}
}
I'm creating a Swift project for a high school programming class. I can't seem to figure out this problem, and everyone else in my class doesn't seem to have any ideas.
To start, I created a new Swift project, and chose a game format.
I then used some basic code to make the first level for my game, a maze game where the maze moves around instead of the ball based on how the user tilts the device.
This is my GameScene.swift:
import SpriteKit
import CoreMotion
var accelupdateinterval = 0.1
var accelmultiplier = 15.0
class GameScene: SKScene {
let manager = CMMotionManager()
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
I want to have a main menu that the app opens into, which is mainMenu.storyboard:
I successfully have the app launching into the mainMenu.storyboard (and the level physics work well when I've tested the level1.sks), but I'm having trouble figuring out how to segue.
GOAL: I want people to be segued into the level1.sks (and the levels that I add later), when they tap the corresponding image in mainMenu.storyboard.
I can't use the method of adding a Storyboard Reference to segue it, as the Storyboard Reference won't let me choose level1.sks.
I'd also love to find out how to send users back to the main menu when the player icon touches the goal (the blue thing up near the top in this screenshot):
So to do this I think the best approach is to create another ViewController subclass, maybe named LauncherViewController, which will present your SKScene. Then in your storyboard add this viewController and have your menu segue to it on an image press.
Here is a start for the LauncherViewController
class LauncherViewController: UIViewController {
var gameScene: GameScene!
override func viewDidLoad() {
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
}
Where in your menu controller you have a prepareForSegue like this:
override func prepareForSegue(segue: UIStoryBoardSegue, sender: AnyObject?) {
if segue.identifier == "yourSegue" {
let destinationViewController = segue.destinationViewController as! LauncherViewController
destinationViewController.gameScene = GameScene()
}
to have your LauncherViewController dismiss the gameScene when the user finishes the maze, use a delegate pattern. So in GameScene add a protocol above your class
protocol GameDelegate {
func gameFinished()
}
have your LauncherViewController conform to this delegate and set the gameScene's delegate variable to self (see below)
class LauncherViewController: UIViewController, GameDelegate {
var gameScene: GameScene!
override func viewDidLoad() {
gameScene.delegate = self
}
override func viewWillAppear() {
presentGameScene()
}
func presentGameScene(){
let skView = self.view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
gameScene.size = skView.bounds.size
gameScene.scaleMode = .AspectFill
skView.presentScene(gameScene)
}
func gameFinished(){
// this forces LauncherViewController to dismiss itself
dismissViewControllerAnimated(true, completion: nil)
}
}
add a variable in GameScene to hold the delegate (LauncherViewController) and add a function that calls the delegate function. You will also need to add the logic to know when the game is over as I haven't done that.
class GameScene: SKScene {
let manager = CMMotionManager()
var delegate: GameDelegate!
override func didMoveToView(view: SKView) {
manager.startAccelerometerUpdates()
manager.accelerometerUpdateInterval = accelupdateinterval
manager.startAccelerometerUpdatesToQueue(NSOperationQueue.mainQueue()){
(data, error) in
self.physicsWorld.gravity = CGVectorMake(CGFloat((data?.acceleration.x)!) * CGFloat(accelmultiplier), CGFloat((data?.acceleration.y)!) * CGFloat(accelmultiplier))
}
}
func gameOver(){
delegate.gameFinished()
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
// it's probably easiest to add the logic for a gameOver here
if gameIsOver {
gameOver()
}
}
}
There will probably be some mistakes in here as I wrote this on my phone so just comment below for anything you are unsure about or doesn't work.
so i found some posts about transitioning from Skscene to uiviewcontroller and I got it to work. This segue and unwind segue is called everytime the user win the level of my game or loses.
this works for level1 but as soon as I win level2 I get
fatal error: unexpectedly found nil while unwrapping an Optional value
on the line where I call the game over function below
in my game scene I have :
class GameScene: SKScene, SKPhysicsContactDelegate {
//next level / try again segue
var viewController : GameViewController!
in the GameViewController i initialize this property
var currentLevel: Int!
override func viewDidLoad() {
super.viewDidLoad()
currentLevel = gameScene.currentLevel
if let scene = GameScene.level(currentLevel) {
// Configure the view.
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .AspectFill
skView.presentScene(scene)
//initialize VC
scene.viewController = self
}
func gameOver() {
performSegueWithIdentifier("gameOver", sender: nil)
}
#IBAction func perpareForUnwind(unwindSegue: UIStoryboardSegue) {
}
and finally i call gameOver from my win() function in gameScene
func newGame() {
view!.presentScene(GameScene.level(currentLevel))
}
func win() {
if (currentLevel < 3) {
currentLevel++
//present win view - with option to restart, next level, or home
}
println(currentLevel)
runAction(SKAction.sequence([SKAction.waitForDuration(2),
SKAction.runBlock(newGame)]))
self.viewController.gameOver() // this is the error!
}
So this works from level1 to level2 but wont work from level2 to level3
Since viewDidLoad is only called once it is only initialized from lvl1 to lvl2 and then becomes nil. How can i make sure that it is initialized everytime. Shoud I put this set up code somewhere other than viewDidLoad?
From level 1 to level 2 your var viewController is not nil, because you initialize it to self on viewDidLoad.
But then, on your next level, viewController is nil, because your code
//initialize VC
scene.viewController = self
only gets executed on viewDidLoad in your GameViewController.
You should set the value of your var viewController in your scene init method or didMoveToView, so it gets set for every new scene.
I thought it would be interesting to learn how to pass an image from an imagePicker to my GameScene to use as a SKSpriteNode(texture: SKTexture(image: UIImage)) but i keep getting an error after selecting my image and pressing a button to go into my gameScene
override func prepareForSegue(segue: UIStoryboardSegue, sender: AnyObject?) {
if (segue.identifier == "startSegue") {
var gameViewController = segue.destinationViewController as GameScene
gameViewController.imageToPass = pickedImage.image
}
anyone know what im doing wrong?
keep getting exc_breakpoint (code=exc_i386_bpt, subcode 0x0)
when initializing gameViewController as GameScene
I am trying to pass imageToPass to the GameScene.swift that is created when i create a new game project in Xcode
class GameScene: SKScene {
var imageToPass = UIImage()
var bird = SKSpriteNode()
override func didMoveToView(view: SKView) {
/* Setup your scene here */
var birdTexture = SKTexture(image: imageToPass)
bird = SKSpriteNode(texture: birdTexture)
bird.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(bird)
}
The destination view controller is a UIViewController or its subclass. Not an SKScene. So I don't think it can be converted to GameScene. GameScene is usually a property of a UIViewController that holds it. In my project the UIViewController is called GameViewController. GameViewController has a property called gameScene
So the code should be
if let gameViewController = segue.destinationViewController as? GameViewController
{
gameViewController.imageToPass = pickedImage.image
}
create a property in GameViewController called imageToPass. Set the imageToPass to GameScene.imageToPass in viewDidLoad.
class GameViewController: UIViewController {
var pickedImage : UIImage!
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
let skView = self.view as SKView
skView.showsFPS = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
scene.scaleMode = .AspectFill
scene.imageToPass = pickedImage
skView.presentScene(scene)
}
}
}
You can create a property observer on imageToPass to change it dynamically.
var bird : SKSpriteNode!
var imageToPass : UIImage!
{
didSet
{
createBird()
}
}
func createBird()
{
var birdTexture = SKTexture(image: imageToPass)
bird?.removeFromParent()
bird = SKSpriteNode(texture: birdTexture)
bird.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(bird)
}
In the default SpriteKit implementation, the View Controller created is a GameViewController. In Interface Builder, it has for its view an SKView. In viewDidLoad, the key lines are
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
// Configure the view.
let skView = self.view as SKView
/* more setup */
skView.presentScene(scene)
}
It looks like you've edited your question to reflect this. I'm not a SpriteKit expert, but if you get imageToPass to the GameViewController, then you should be able to pass it to your GameScene in viewDidLoad before it's presented. Good luck.
Note "GameScene.swift" is just the name of the text file that contains the code, so you wouldn't pass anything to it, the class is created in class GameScene: SKScene { /* class properties and methods */
Original Answer:
Are you sure the destinationViewController is a GameScene? Really sure? Because you're using as, you're force downcasting the segue view controller as a "GameScene".
Take advantage of Swift's safety features:
if var gameViewController = segue.destinationViewController as? GameScene {
gameViewController.imageToPass = pickedImage.image
}
or using optional chaining:
var gameViewController = segue.destinationViewController as? GameScene
gameViewController?.imageToPass = pickedImage.image
The pickedImage won't get through if not a GameScene, regardless, but you won't get a crash.
EDIT: By the way, it really should be GameSceneViewController so its clear everywhere it's a View Controller. If it's not a View Controller, that's a problem and has no chance to work.