I am trying to transition from the default, root scene to a new scene with SpriteKit. However, whenever I press the Start button, it grays out the old scene (although it remains visible) and the Drawing Board label shows up. The scene remains greyed out. All the buttons from the old scene can still be pressed but do not perform their associated actions. A UIButton triggers this func:
startButton.addTarget(self, action: "goToDrawingBoard:", forControlEvents: UIControlEvents.TouchUpInside)
The func:
#objc func goToDrawingBoard(sender: UIButton){
let drawingBoardScene = DrawingBoardScene(size: self.size)
self.scene?.view?.presentScene(drawingBoardScene, transition: SKTransition.crossFadeWithDuration(1.0))
}
And the DrawingBoardScene.swift file:
import Foundation
import SpriteKit
import UIKit
class DrawingBoardScene: SKScene {
let titleLabel = SKLabelNode(text: "DRAWING BOARD")
override func didMoveToView(view: SKView) {
/*LABEL: Displays title*/
titleLabel.fontColor = UIColor.blackColor()
titleLabel.fontSize = 60
titleLabel.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(titleLabel)
}
}
Looks like you are presenting the scene incorrectly, try the following:
#objc func goToDrawingBoard(sender: UIButton){
let drawingBoardScene = DrawingBoardScene(size: self.size)
self.view?.presentScene(drawingBoardScene, transition: SKTransition.crossFadeWithDuration(1.0))
}
There is no reason to add the new scene as a child to the old scene, and who knows why your scene has a scene object.
As a personal note, presenting your scene in this matter is not a good way to present scenes. It is the views job to be presenting scenes, so what you should be doing is when it comes time for the scene to be removes, send a notification in some way to the view that the scene is done working and is waiting for it to be removed, and have the view then present the scene. This will allow the view to properly remove the old scene without having any retainers holding it back. One method to do this is threw delegation
My friend and I spent a couple nights working on various solutions and the one we finally came up with is this:
override func willMoveFromView(view: SKView) {
self.removeAllChildren()
delete(startButton)
}
override func delete(sender: AnyObject?) {
let subviews = (self.view?.subviews)! as [UIView]
for v in subviews {
if let button = v as? UIButton {
button.removeFromSuperview()
}
}
}
The only problem with this is that when the scene shifts the buttons can take a split second longer to disappear, giving it a kind of glitchy feel. It does work though, so for a short term solution it is great.
Related
I was trying to solve this problem (TL;DR An overlaid SKScene using the overlaySKScene property in SCNView wasn't causing a redraw when children were added and removed from it) using view.setNeedsDisplay() to force a redraw since the SCNView wasn't doing it automatically.
The problem with using view.setNeedsDisplay() was that the CPU usage was spiking to 50% and I assumed it was because the entire SCNView was having to redraw its contents, which included a 3D SCNScene as well. My solution was to use view.setNeedsDisplay(_: CGRect) to minimise the region that needs to be redrawn. However, to my surprise, no matter what I put as the CGRect value the SCNView refused to render the SKScene contents that had been overlaid on it.
Steps to reproduce issue
Open SceneKit template
From the Main (Base) storyboard, set the "Scene" attribute on the SCNView to be "art.scnassets/ship.scn" or whatever the path is
Delete all boilerplate code and just leave
class CustomSKScene: SKScene {
override func didMove(to view: SKView) {
let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(userTapped(_:)))
view.addGestureRecognizer(tapGestureRecognizer)
}
#objc func userTapped(_ sender: UITapGestureRecognizer) {
let finger = convertPoint(fromView: sender.location(in: view))
let circle = SKShapeNode(circleOfRadius: 25)
circle.position = finger
addChild(circle)
}
}
class GameViewController: UIViewController {
private var gameView: SCNView { view as! SCNView }
override func viewDidLoad() {
gameView.overlaySKScene = CustomSKScene(size: gameView.bounds.size)
}
}
(This should still allow the ship scene to render when you run the app)
When you tap the screen, circles shouldn't show up. Fix this issue by adding view!.setNeedsDisplay() below the addChild function. Notice how CPU usage goes up to around 40-50% if you tap repeatedly after adding this fix.
Replace view!.setNeedsDisplay() with view!.setNeedsDisplay(view!.frame) (which should be equivalent).
At this point we are now back to square one. The circles are not showing up on screen again and confusion ensues. view.setNeedsDisplay() and view.setNeedsDisplay(view.frame) should be equivalent, yet, nothing is redrawn.
Does anyone know how to fix this problem? I feel it only happens when using the overlaySKScene property so maybe there is some caveat with its implementation that I am unaware of.
Some observations:
When you debug the view hierarchy, the overlaid SKScene doesn't show up anywhere, which is strange
sender.view === view returns true
(sender.view as! SCNScene).overlaySKScene === self also returns true
I have created a UI elements on main.storyboard which i require to be hidden until the game is over and the once the player tap the screen to dismiss. Main.storyboard is linked to GameViewController therefor all my IBOutlets and IBActions are in there and all my game code is in GameScene. How can i link the view controller to the scene for that the popup image and buttons only appear when it is game over. Would greatly appreciate some help, I have been stuck on this for quite some time now.
This seems to be quite a common problem people have with SpriteKit games so lets go through the difference between SpriteKit games and UIKit apps.
When you make a regular UIKit app, e.g. YouTube, Facebook, you would use ViewControllers, CollectionViews, Views etc for each screen/menu that you see (Home screen, Channel screen, Subscription channel screen etc). So you would use UIKit APIs for this such as UIButtons, UIImageViews, UILabels, UIViews, UICollectionViews etc. To do this visually we would use storyboards.
In SpriteKit games on the other hand it works differently. You work with SKScenes for each screen that you see (MenuScene, SettingsScene, GameScene, GameOverScene etc) and only have 1 ViewController (GameViewController). That GameViewController, which has a SKView in it, will present all your SKScenes.
So we should add our UI directly in the relevant SKScenes using SpriteKit APIs such as SKLabelNodes, SKSpriteNodes, SKNodes etc. To do this visually we would use the SpriteKit scene level editor and not storyboards.
So the general logic would be to load your 1st SKScene as usual from the GameViewController and than do the rest from within the relevant SKScenes. Your GameViewController should basically have next to no code in it beyond the default code. You can also transition from 1 scene to another scene very easily (GameScene -> GameOverScene).
If you use GameViewController for your UI it will get messy really quickly if you have multiple SKScenes because UI will be added to GameViewController and therefore all SKScenes. So you would have to remove/show UI when you transition between scenes and it would be madness.
To add a label in SpriteKit it would be something like this
class GameScene: SKScene {
lazy var scoreLabel: SKLabelNode = {
let label = SKLabelNode(fontNamed: "HelveticaNeue")
label.text = "SomeText"
label.fontSize = 22
label.fontColor = .yellow
label.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return label
}()
override func didMove(to view: SKView) {
addChild(scoreLabel)
}
}
To make buttons you essentially create a SKSpriteNode and give it a name and then look for it in touchesBegan or touchesEnded and run an SKAction on it for animation and some code after.
enum ButtonName: String {
case play
case share
}
class GameScene: SKScene {
lazy var shareButton: SKSpriteNode = {
let button = SKSpriteNode(imageNamed: "ShareButton")
button.name = ButtonName.share.rawValue
button.position = CGPoint(x: self.frame.midX, y: self.frame.midY)
return button
}()
override func didMove(to view: SKView) {
addChild(shareButton)
}
/// Touches began
override open func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let node = atPoint(location)
if let nodeName = node.name {
switch nodeName {
case ButtonName.play.rawValue:
// run some SKAction animation and some code
case ButtonName.share.rawValue:
let action1 = SKAction.scale(to: 0.9, duration: 0.2)
let action2 = SKAction.scale(to: 1, duration: 0.2)
let action3 = SKAction.run { [weak self] in
self?.openShareMenu(value: "\(self!.score)", image: nil) // image is nil in this example, if you use a image just create a UIImage and pass it into the method
}
let sequence = SKAction.sequence([action1, action2, action3])
node.run(sequence)
default:
break
}
}
}
}
}
To make this even easier I would create a button helper class, for a simple example have a look at this
https://nathandemick.com/2014/09/buttons-sprite-kit-using-swift/
You can also check out Apple's sample game DemoBots for a more feature rich example.
This way you can have things such as animations etc in the helper class and don't have to repeat code for each button.
For sharing, I would actually use UIActivityController instead of those older Social APIs which might become deprecated soon. This also allows you to share to multiple services via 1 UI and you will also only need 1 share button in your app. It could be a simple function like this in the SKScene you are calling it from.
func openShareMenu(value: String, image: UIImage?) {
guard let view = view else { return }
// Activity items
var activityItems = [AnyObject]()
// Text
let text = "Can you beat my score " + value
activityItems.append(text as AnyObject)
// Add image if valid
if let image = image {
activityItems.append(image)
}
// Activity controller
let activityController = UIActivityViewController(activityItems: activityItems, applicationActivities: nil)
// iPad settings
if Device.isPad {
activityController.popoverPresentationController?.sourceView = view
activityController.popoverPresentationController?.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0)
activityController.popoverPresentationController?.permittedArrowDirections = UIPopoverArrowDirection.init(rawValue: 0)
}
// Excluded activity types
activityController.excludedActivityTypes = [
UIActivityType.airDrop,
UIActivityType.print,
UIActivityType.assignToContact,
UIActivityType.addToReadingList,
]
// Present
view.window?.rootViewController?.present(activityController, animated: true)
}
and then call it like so when the correct button was pressed (see above example)
openShareMenu(value: "\(self.score)", image: SOMEUIIMAGE)
Hope this helps
create reference of GameViewController in GameScene class like this way
class GameScene: SKScene, SKPhysicsContactDelegate {
var referenceOfGameViewController : GameViewController!
}
in GameViewController pass the reference like this way
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// 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
scene.referenceOfGameViewController = self
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
}
by using this line you can pass reference to GameScene class
scene.referenceOfGameViewController = self
Now In GameScene class you can access all the variable of GameViewController like this way
referenceOfGameViewController.fbButton.hidden = false
referenceOfGameViewController.gameOverPopUP.hidden = false
I am trying to change the text of a field on a button cick
the button is getting called but the label displayed the old text.
If I use NSUserDefaults to save the value then I will have to close the app and reopen it to see the new value of text field.
Is there any way when a user presses a button the value gets reset instantaneously on the screen?
GameViewController Code
import UIKit
import SpriteKit
class GameViewController: UIViewController {
#IBOutlet var resetText: UIButton!
#IBAction func ResetTextPreseed(sender: AnyObject) {
GameScene().changeText()
}
override func viewDidLoad() {
super.viewDidLoad()
if let scene = GameScene(fileNamed:"GameScene") {
// 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)
}
}
override func shouldAutorotate() -> Bool {
return true
}
override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
if UIDevice.currentDevice().userInterfaceIdiom == .Phone {
return .AllButUpsideDown
} else {
return .All
}
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Release any cached data, images, etc that aren't in use.
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
GameScene Code
import SpriteKit
class GameScene: SKScene {
let myLabel = SKLabelNode(fontNamed:"Chalkduster")
var text = "Hello, WOrld"
override func didMoveToView(view: SKView) {
/* Setup your scene here */
myLabel.text = text
myLabel.fontSize = 45
myLabel.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
self.addChild(myLabel)
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
func changeText(){
text = "I got changed"
myLabel.text = text
self.addChild(myLabel)
print ("The value of text is \(text)")
}
}
Currently, what you are doing will not work because you are not referencing the current scene but rather making a new instance of a GameScene scene and calling changeText() method on that instance, which has no effect on a current scene.
There is always a debate about should you or shouldn't use UIKit elements with SpriteKit. I would not go into that topic, but in general, SpriteKit and UIKit are different beasts and even if I really like both frameworks I would stick to SpriteKit only as much as I can when it comes to games ... And about some differences... For example, there is a difference between how SKScene renders its nodes vs how views are rendered. Some quotes from docs :
In the traditional view system, the contents of a view are rendered
once and then rendered again only when the model’s contents change.
This model works very well for views, because in practice most view
content is static. SpriteKit, on the other hand, is designed
explicitly for dynamic content. SpriteKit continuously updates the
scene contents and renders it to ensure that animation is smooth and
accurate.
More differences:
Different coordinate systems.
Views are added to the views (not the the scene).
Nodes are added to the scene (not the the view).
More about view's rendering cycle can be found here.
More about how SpriteKit renders a scene can be found here .
So because of these differences you may run (not necessarily of course) into different problems when mixing these two.
You have three solutions (I can think of):
1) Accessing current scene through self.view inside GameViewControllerr Ugly solution IMO, but it will work:
#IBAction func someAction(sender: AnyObject) {
let skView = self.view as! SKView
if let currentScene = skView.scene as? GameScene {
currentScene.changeText()
}
}
2) Nice solution, but again this is just my opinion - Implementing custom buttons using SKSpriteNode. Just search SO about this.
3)Use third party SpriteKit buttons like SKAButton or AGSpriteButton.
I created a really simple game in xcode using spritekit and swift. Now I want to have a main menu that shows up when I first start the app. Then i want to have a button that when tapped on, will start the game. Is there a way to do this? Should i be using story board? thanks so much! :)
By using SpriteKit you can do it this way:
In your GameViewController.swift replace your code with this code:
import UIKit
import SpriteKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
//load your GameScene from viewController
let scene = GameScene(size: view.bounds.size)
let skView = view as! SKView
skView.showsFPS = false
skView.showsNodeCount = false
skView.ignoresSiblingOrder = true
scene.scaleMode = .ResizeFill
skView.presentScene(scene)
}
override func prefersStatusBarHidden() -> Bool {
return true
}
}
This code will load GameScene when you start your game.
Add this code in your GameScene.swift class:
import SpriteKit
class GameScene: SKScene {
//create playbutton instance
let playButton = SKSpriteNode(imageNamed: "play_unpresed")
override func didMoveToView(view: SKView) {
backgroundColor = UIColor.greenColor()
addPlayButton() //add playbutton
}
func addPlayButton(){
playButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
playButton.xScale = 0.2
playButton.yScale = 0.2
self.addChild(playButton)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch in (touches as! Set<UITouch>){
let location = touch.locationInNode(self)
//this will detect touch on play button
if self.nodeAtPoint(location) == self.playButton {
//it will transits to the next scene
let reveal = SKTransition.flipHorizontalWithDuration(0.5)
let letsPlay = playScene(size: self.size)
self.view?.presentScene(letsPlay, transition: reveal)
}
}
}
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}
This will load a scene with one button now when you press a button it will take you to your playScene And for that you have to create a new file by clicking Command + N then iOS Source -> Cocoa Touch class -> next -> add class name playScene -> Subclass of SKScene -> and create it.
Add import SpriteKit in your playScene.swift
Check THIS sample project for more info.
And HERE is the easy tutorial for spriteKit.
You can just drag one viewController to the storyBoard.
set it Is Initial View Controller, add one button on it
control+drag from the button to your Game View Controller. choose show
I've test it. It can work. When I click the button, It will navigate to the Game view controller
Storyboards
Alternatively, you can use Storyboards. In the M.W.E. for another S.O. question they have a basic "menu" set up.
In your case, what you would do is:
go to Main.storyboard.
on the right-hand tool bar, find view controller
drag view-controller into Main.storyboard
click on the new view-controller
click - on the right-hand tool bar - the identity inspector (looks like a business card)
change Class to GameViewController
click on view within the hierarchy on the left (under the new view controller)
click the identity inspector
change class to SKView
click on the original view controller
click on the identity inspector
change class to UIViewController
click on the view within the original UIViewController
click on identity inspector
change class to UIView
find button at the bottom of the right-hand side tool bar
drag it onto the first view
right click drag from the button to the second view
on the pop-up menu, under action segue, click show
right click drag from the button up, add horizontally center constraints
right click drag from the button to the right, add vertically center constraints
Images
I am having trouble figuring out how to change view controllers when your player collided with an object.
I want to like a menu to pop-up displaying a menu button and a replay button, also so extra buttons that are not important at this moment of time. I am not sure how some of those end of game menus are made, I am thinking switching view controllers, if you know exactly how they are made please tell me.
This is the code I have at the moment, and the only thing it does is display a label that the game is over and when that label is tapped the game will restart:
import Foundation
import AVFoundation
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
var movingGround: PPMovingGround!
var square1: PPSquare1!
var wallGen: PPWallGen!
var diamondGen: PPDiamondGen!
var isStarted = false
var isGameOver = false
var isDiamondContact = false
var playerNode: SKNode!
override func didMoveToView(view: SKView) {
//code that is not important was deleted
func collisionWithDiamond() {
isDiamondContact = true
}
func restart() {
let newScence = GameScene(size: view!.bounds.size)
newScence.scaleMode = .AspectFill
view!.presentScene(newScence)
}
override func touchesBegan(touches: Set<NSObject>, withEvent event: UIEvent) {
if isGameOver {
restart()
} else {
square1.flip()
}
}
override func update(currentTime: CFTimeInterval) {
}
// MARK: - SKPhysicsContactDelegate
func didBeginContact(contact: SKPhysicsContact) {
if !isGameOver {
gameOver()
} else {
!isDiamondContact
collisionWithDiamond()
}
}
Note: I have deleted code that is unrelated or not necessary.
Updates:
Link to a game play of a game: https://www.youtube.com/watch?v=WUibTETfEQY
SKIP TO 2:32 TO SEE THE GAME OVER SCREEN
Link to image of game over screen: Image
(I was unable to post an image here because I don't have the required 10 rep points yet.)
// Edited Answer
This will be the easiest. Create a new GameOverScene.swift that is a SKScene. Then customize that scene however you want with background image, SKLabelNodes for buttons. Checkout creating buttons in skview to point to different scenes
When the game ends in GameScene,
let gameOverScene: GameOverScene = GameOverScene(size: self.size)
self.view!.presentScene(gameOverScene, transition: SKTransition.doorsOpenHorizontalWithDuration(1.0))
Here is a project that has this implemented, http://www.raywenderlich.com/76741/make-game-like-space-invaders-sprite-kit-and-swift-tutorial-part-2
// First Answer -----------------------------------------------
If you want to switch viewControllers, you will have to present the new viewController like this or with segue,
self.view?.window?.rootViewController?.presentViewController(newView, animated: true, completion: nil)
self.view?.window?.rootViewController?.performSegueWithIdentifier("id", sender: AnyObject)
Otherwise create a SKView and add buttons, then add it to the scene when game is over or add it before, hide it, then show it. Once user picks a choice, remove it or hide it with,
SKView.hidden = false
SKView.hidden = true
Add SKView with,
self.view?.addSubview(SKView)
Simple SKView overlay,
let view1 = SKView(frame: CGRectMake(0, 0, 200, 200))
view1.center = self.view!.center
self.view?.addSubview(view1)
If the game is over and you want to present a selective menu, you could present a UIAlertView that presents the user with whatever options that you want. From there they could restart the game, or they could choose to go to some other view like one that manages their player stats or something ( I don't know what exactly your game is).