I have created a game where the main menu is contained within a normal ViewController and the actual game is played within an SKScene.
I use a modal segue from a button on the main menu to open the SKScene.
The problem I get is when I try to return from the SKScene to the main menu.
I'm using this code to present the main menu
override func touchesEnded(touches: NSSet, withEvent event: UIEvent) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if homeBtn.containsPoint(location)
{
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let settingController: UIViewController = storyboard.instantiateViewControllerWithIdentifier("homeViewController") as UIViewController
let vc = self.view?.window?.rootViewController
vc?.presentViewController(settingController, animated: true, completion: nil)
}
}
}
However this causes the following message:
Warning: Attempt to present ViewController on ViewController whose view is not in the window hiearchy!
Your Scene should not call back to the ViewController. It creates too much dependence between the View and the ViewController.
Here is how I use the delegate in Objective-C (sorry I don't know swift). Here are some examples in Swift.
// GameViewController.h
#interface GameViewController : UIViewController <GameSceneDelegate>
// GameViewController.m
// where we setup new scenes. Don't forget this.
newScene.delegate = self;
-(void)displayMenu {
... setup and display the menu
}
Create a simple delegate class
// GameSceneDelegate.m
#protocol GameSceneDelegate <NSObject>
-(void)displayMenu;
#end
Your game scene now keeps a reference to the delegate. Other ViewControllers can now use your GameScene without backward references.
// GameScene.h
#interface GameScene : SKScene
#property (assign) id<GameSceneDelegate> gameSceneController;
// GameScene.m
[self.gameSceneController displayMenu];
Other ways you can get the message back to the GameViewController is using an NSController, but this is a bit messy. Or you could also consider something like Reactive Cocoa.
Related
I'm completely stuck at trying to perform a segue out of the 5th and final scene of my SpriteKit game to another View Controller in the project(not the GameViewController, nor the root view controller).
I tried running self.view!.window!.rootViewController!.performSegueWithIdentifier("finalSegue", sender: self), from my finalScene, but it literally does nothing (the line gets triggered, it reads the right ViewController - i check by "print(self.view!.window!.rootViewController!)" console prints "segue read" as I instructed it, right after the segue command, as a check, the segue identifier is correct, but nothing happens).
Have tried calling a method that performs the segue from the GameViewController ( the view controller from which I am launching the view of the 5 SKScenes), I get "unexpectedly found nil while unwrapping an optional value". Tried performing the segue from the final scene ("finalScene.swift"), same error.
Have tried everything and all relevant solutions in other questions in the forum, as well as all combinations of nil/self/viewController in the "sender:" field of the performSegue method, to no avail. Here is the code that I am trying to make work which gives "unexpectedly found nil while unwrapping an optional value", pointing at the viewController var, but giving uncomprehensible debugging when loaded both on the device and on the simulator. It seems "nil" passes into the viewController var I am declaring, instead of the original GameViewController?
All my segue identifiers are correct in Storyboard, everything checked multiple times...What am I doing wrong? Should I do something different, given its the 5th SKScene and not the 1st (as in other solutions)? The segue into the SKScenes by segueing into the GameViewController from another UIViewController works fine - its the exit out of them that does not work. Many thanks for any help, completely stuck here!
Here is my relevant code:
In my GameViewController (UIViewController that launches my 5 consecutive SKScenes):
class GameViewController: UIViewController {
let myScene = finalScene()
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
//this is the 1st out of 5 SKScenes in the project
let scene = GameScene(size: CGSize(width: 2048, height: 2742))
//this is the 5th out of 5 scenes, that I am trying to trigger the segue out of
myScene.viewController = self
view.presentScene(scene)
view.ignoresSiblingOrder = true
}
}
}
Im my 5th scene, finalScene:
class finalScene: SKScene {
var viewController: GameViewController!
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches{
let positionOfTouch = touch.location(in: self)
let tappedNode = atPoint(positionOfTouch)
let nameOfTappedNode = tappedNode.name
if nameOfTappedNode == "continue" {
self.viewController.performSegue(withIdentifier: "finalSegue", sender: self)
}
}
}
Just for anyone who might come across this, I solved it by using notification center. Ie added a notification observer on the parent view controller of the game scenes (which calls a function that performs the segue), and at the end point in the game where I wanted to do the segue, I posted to that notification, and it works great.
adding observer in ViewDidLoad of UIViewController:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(GameViewController.doaSegue), name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
}
func doaSegue(){
performSegue(withIdentifier: "toNext", sender: self)
self.view.removeFromSuperview()
self.view = nil
}
And then calling it from within the game SKScene:
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "doaSegue"), object: nil)
You are calling the segue on the root viewController. I think that is the problem. You need to call the segue on the scene's viewController instead (where I am assuming you have created the segue, hence it is not being found on the root viewController).
Now the problem is that an SKScene does not have direct access to it's viewController, but just the view in which it is contained. You need to create a pointer to it manually. This can be done by creating a property for the SKScene:
class GameScene: SKScene {
var viewController: UIViewController?
...
}
Then, in the viewController class, just before skView.presentScene(scene)
scene.viewController = self
Now, you can access the viewController directly. Simply call the segue on this viewController:
func returnToMainMenu(){
self.viewController.performSegueWithIdentifier("menu", sender: vc)
}
Found this method to work just as well. But I do prefer the method mentioned above since it's fewer lines. I do wonder which is better for performance...
For example if i have:
class SpriteKitScene: SKScene {
...
}
And in there i want to have an image, that when tapped(pressed, clicked, touched whatever)
loads another file with:
class UiViewcontrollerScene: UIViewcontroller {
...
}
I know how to transition from SKScene to SKScene, but i need to transition from SKScene to UIViewcontroller.
First, set yourself up a delegate using a protocol for your view controller.
protocol UIViewControllerDelegate{
}
See here: https://makeapppie.com/2014/07/01/swift-swift-using-segues-and-delegates-in-navigation-controllers-part-1-the-template/ for a nice tutorial on how to do that
Create an SKView class that will be hosting this delegate
class GameView : SKView
{
var delegate : UIViewControllerDelegate?
}
Then on your viewDidLoad in your UIViewController class, assign the delegate to your view controller.
override func viewDidLoad()
{
if let view = self.view as? GameView
{
view.delegate = self
}
}
Now your view has a delegate to your view controller, From this point, in your protocol file, make a method to transition
E.G.
protocol UIViewControllerDelegate
{
optional func transitionToMenuVC()
}
Then apply the code to your view controller class.
class ViewController : UIViewController, UIViewControllerDelegate
{
...
func transitionToMenuVC()
{
// do transition code here
}
}
Now you have it all set up for your view to communicate with your view controller.
In your Scene, you would just cast the scene's view to the GameView, and use the delegate to transition
class GameScene : SKScene
{
...
func transition()
{
if let view = self.view as? GameView
{
view.delegate.transitionToMenuVC()
}
}
}
Do note however, it is impossible to transition from scene to view controller, because they are 2 different animals. You will be transitioning the views, and are therefor stuck using the animations provided for views.
There seems to be so many contradicting ideas on this topic.
I simply wish to have my menu in a UIViewController and my game in the SKScene
In my SKScene I used:
self.removeFromParent()
self.view?.presentScene(nil)
The nodes are removed but the scene is still in place as I still have the grey background and fps counter. Can I return to the View aspect of the UIViewController and hide the scene?
My method one implementation:
RootViewController:
class RootViewController: UIViewController {
var menu = MenuViewController()
var game = GameViewController()
override func viewDidLoad() {
super.viewDidLoad()
print("root")
MenuPresent()
}
func GamePresent() {
self.addChildViewController(game)
self.view.addSubview((game.view)!)
game.didMoveToParentViewController(self)
}
func MenuPresent() {
self.addChildViewController(menu)
self.view.addSubview((menu.view)!)
menu.didMoveToParentViewController(self)
}
func menuDismiss() {
menu.willMoveToParentViewController(nil)
menu.removeFromParentViewController()
menu.view.removeFromSuperview()
}
}
MenuViewController:
class MenuViewController: UIViewController {
//var root = RootViewController()
override func viewDidLoad() {
super.viewDidLoad()
print("menu")
}
}
The print("menu") appears in my console, But the actual view and all assets of the MenuViewController do no appear.
On the other hand my GameViewController and it's SKScene work fine.
Views and scenes are two different things. Scenes are held inside of a view. You would have to simply present a scene within your menu view or transition to another view and present an SKScene from there. The code to present the scene might look like this:
let scene = GameScene(fileNamed: "GameScene")
let skView = self.view as! SKView
scene?.scaleMode = .AspectFit
skView.presentScene(scene)
First you need to know that SKScene and UIViewController are totally two different things. The hierarchy is typically as following:
UIViewController --> UIView(SKView) --> SKScene
So you SKScene is presented in a SKView which can be a UIView, then the UIView is presented in a UIViewController.
Once you know the hierarchy, everything is easy. There are many ways to use different UIViewController for menu and GameScene stuff.
Method One
For example, you can have a RootViewController, a GameViewController and a MenuViewController. The RootViewController is the initial ViewController when the app launches.
In the RootViewController, you can create a function to present the GameViewController:
func setupGameViewController() {
self.gameViewController = GameViewController()
self.addChildViewController(gameViewController!)
self.view.addSubview((gameViewController!.view)!)
gameViewController?.didMoveToParentViewController(self)
}
You need to present you SKScene in GameViewController, I guess you should be familiar with this step.
Then when you need to display the menu, you can add the MenuViewController to RootViewController with a function like:
func setupMenuViewController() {
self.menuViewController = MenuViewController()
self.addChildViewController(menuViewController!)
self.view.addSubview((menuViewController!.view)!)
menuViewController?.didMoveToParentViewController(self)
}
You also need to present you menuView in this ViewController, which I suppose you already know.
Also create a function to dismiss the MenuViewController:
func removeMenuViewController(){
self.menuViewController?.willMoveToParentViewController(nil)
self.menuViewController?.removeFromParentViewController()
self.menuViewController?.view.removeFromSuperview()
}
And everything is done.
Method Two
You can also have only one UIViewController, but create you menu as a UIView, then you can use self.view.addSuview(menuView) to present your menu. Of course you root view, which is the self.view as SKView is still there, but it doesn't matter because it's hidden behind the menuView.
A Note for your updated question
You cannot remove the scene because the scene is actually self.view as SKView, self.view is the root view of a UIViewController, it cannot be removed. If you really want to remove you scene (for most cases, it's not necessary), you can create a new SKView that present your SKScene, then add this SKView to your UIViewController by self.view.addSubview(skView), when you want to completely remove the scene, just use skView.removeFromSuperview().
QUESTION: How can I dismiss a ViewController from my GameScene.swift ?
SITUTATION: I have 2 VCs in my SpriteKit Game, like so:
ViewController.swift ----Press Play-----> GameViewController
When the player loses, I want to dismiss the GameViewController so the player can press play again. I check for the player's loss in my GameScene.swift and would like to dismiss the GameVC from there.
N.B.: Googled this without success.
WHAT I TRIED:
1) Creating a gameVC instance in my GameScene.swift and dismissing it like so:
let gameVC = GameViewController()
gameVC.dismissViewController(false,completion: nil)
2) Doing:
self.view.window!.rootViewController?.dismissViewControllerAnimated(false, completion: nil)
Those don't work for obvious reasons ^^
You don't want to "grab" the existing instance: https://pragprog.com/articles/tell-dont-ask
You need to either hand GameScene a reference to the view controller so it can dismiss it, or use the delegate pattern to communicate backwards to a controlling object that the VC should be dismissed/dismiss itself.
A simple example… you can add a GameViewController property to GameScene, then dismiss the VC at the appropriate time:
class GameScene: SKScene {
var gameVC: GameViewController?
func gameDidEnd() {
gameVC?.dismissViewControllerAnimated(true) {
// if desired, do any cleanup after the VC is dismissed
}
}
}
Then, just set this property when creating the GameScene object in the first place:
if let gameScene = GameScene(fileNamed: "MyScene") {
gameScene.gameVC = someGameVC
}
This simple approach will tightly couple GameScene and GameViewController, making it a bit more difficult if you ever want to use one of these objects without the other. But for this simple use case, it may be fine.
I've follow some of your discussion. I want to add some code, because usually I prefeer to work with one ViewController or two and many SKScene and SKNode, but in this case could be useful to have a currentViewController reference:
class MyModelScene: SKScene {
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
var currentViewController : MyModelViewController! = MyModelViewController()
// MyModelViewController is a customized UIViewController
override func didMoveToView(view: SKView) {
super.didMoveToView(view)
print("---")
print("∙ \(NSStringFromClass(self.dynamicType))")
print("---")
}
}
class Level1Scene: MyModelScene {
...
}
In the UIViewController:
class PreloadViewController: MyModelViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let scene = Level1Scene(fileNamed:"Level1Scene") {
// Configure the view.
let skView = self.view as! SKView
skView.showsFPS = true
skView.showsPhysics = true
skView.showsNodeCount = true
skView.ignoresSiblingOrder = true
/* Set the scale mode to scale to fit the window */
scene.scaleMode = .ResizeFill
scene.currentViewController = self
skView.presentScene(scene)
}
}
}
With this code , you've always a currentViewController reference in your SKScene and you can check if it's the correct viewController you want to dismiss or not.
Hi I have a segues from the main menu UIViewController to start playing the game I created. But if i want to exit back to main menu there is no prepareForSegue function in SpriteKit.
How can I unwind back to the main menu from my scene?
I am using touches began to handle events instead of UIButtons
You have call performSegue from the ViewController containing the SKView not the SKScene. Create a weak variable inside SKScene pointing to the GameViewController. Set it when the SKScene is created. Then call performSegue on this property.
//Game Scene
class GameScene {
weak var gameViewController : GameViewController?
}
// GameViewController
override func viewDidLoad() {
//Other code
scene.gameViewController = self
skView.presentScene(scene)
}
When you want to perform the segue inside GameScene, call
// Inside GameScene
self.gameViewController?.performSegueWithIdentifier("indentifer", sender: self)
To go back the the main view.
self.gameViewController?.navigationController?.popViewControllerAnimated(true)