How do I end a game once a condition has been met? - swift

I'm making a battleship game on Xcode 11 using Storyboards, and I've pretty much done everything except a Game Over screen and stop registering the guesses once the counter reaches 0. This is also my first time making a game on my own. Here's one of the IBActions I made for the buttons:
#IBAction func square1Button(_ sender: Any) {
if randomNum == 1 {
square1.image = UIImage(named: "ShipPatrolHull")
} else {
guesses -= 1
Turns.text = String(guesses)
if guesses == 0 {
}
}
}
The if statement in the else statement is empty because I don't know what to add there to make the variable guesses stop going down and to make a Game Over screen to show. I've also added a Storyboard called GameOver and is linked back to the main storyboard.

You could display a Game Over UIAlertController and then have the game reset when they select "Play Again." However, you mention that you already have another Storyboard for a separate Game Over screen. All you need to do is present that Game Over view controller. Make sure to set the storyboard identifier for the view controller you are trying to present so you can access it in code. See below for guidance,
let storyboard = UIStoryboard(name: "Main", bundle: nil)
let controller = self.storyboard.instantiateViewController(withIdentifier: "GameOverView")
From here you can easily present it.
self.present(controller, animated: true, completion: nil)
Lastly, to make the guesses stop going down, reorganize your if-statements in order to only subtract one from the guesses if the guesses value is not equal to zero, otherwise... game over.
if guesses != 0 {
Turns.text = String(guesses)
guesses -= 1
} else {
//Game over, reset, etc.
}

Related

How to check if UIViewController is already being displayed?

I'm working on an app that displays a today extension with some information. When I tap on the today extension, it opens the app and navigates to a subview from the root to display the information. Normally the user would then click the back arrow to go back to the main view, but there is no way to tell if this is actually done. It is possible for the user to go back to the today extension and tap again. When this is done, the subview is opened once again with new information. If this is done a bunch of times, I end up with a bunch of instances of the subview and I have to click the back button on each of them to get back to the main view.
My question: Is it possible to check if the subview is already visible? I'd like to be able to just send updated information to it, instead of having to display an entirely new view.
I am currently handling this by keeping the instance of the UIViewController at the top of my root. If it is not nil, then I just pass the information to it and redraw. If it is nil, then I call performSegue and create a new one.
I just think that there must be a better way of handling this.
Edit: Thanks to the commenter below, I came up with this code that seems to do what I need.
if let quoteView = self.navigationController?.topViewController as? ShowQuoteVC {
quoteView.updateQuoteInformation(usingQuote: QuoteService.instance.getQuote(byQuoteNumber: quote))
}
else {
performSegue(withIdentifier: "showQuote", sender: quote)
}
This is different from the suggested post where the answer is:
if (self.navigationController.topViewController == self) {
//the view is currently displayed
}
In this case, it didn't work because I when I come in to the app from the Today Extension, it goes to the root view controller. I needed to check whether a subview is being displayed, and self.navigationController.topViewcontroller == self will never work because I am not checking to see if the top view controller is the root view controller. The suggestions in this post are more applicable to what I am trying to accomplish.
u can use this extension to check for currently displayed through the UIApplication UIViewController:
extension UIApplication {
class func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController {
if let selected = tab.selectedViewController {
return topViewController(base: selected)
}
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
}
and usage example:
if let topController = UIApplication.topViewController() {
if !topController.isKind(of: MainViewController.self) { //MainViewController- the controller u wish to equal its type
// do action...
}
}

Why does the UI freeze when dismissing ViewController with ARSCNView?

I am running an ARKit Session where I place SceneKit nodes. With every node I am instantiating a new view controller and pass its view as the node's content like so:
func createTextNode(anchor: ARCardAnchor) -> SCNNode? {
let plane = SCNPlane()
plane.height = 0.5
plane.width = 0.5
let sb = UIStoryboard(name: "Main", bundle: nil)
let fCVC = sb.instantiateViewController(withIdentifier: "CardViewController") as! CardViewController
plane.firstMaterial?.diffuse.contents = fCVC.view
let cardNode = SCNNode(geometry: plane)
cardNode.constraints = [billboardConstraint]
return cardNode
}
I am adding the nodes to the scene using the following ARSCNViewDelegate method and my custom ARCardAnchor (a subclass of ARAnchor):
func renderer(_ renderer: SCNSceneRenderer, didAdd node: SCNNode, for anchor: ARAnchor) {
if let fcanchor = anchor as? ARCardAnchor {
DispatchQueue.main.async {
guard let n = self.nodeCreator.createTextNode(anchor: fcanchor) else { return }
node.addChildNode(n)
}
}
}
So far everything works and the nodes are placed in 3D space. But when I navigate back to the previous View Controller the UI freezes and I can't do anything.
I've tried using an unwind segue like this
#IBAction func goBackToPrevious(_ sender: Any) {
sceneView.session.pause()
self.performSegue(withIdentifier: "unwindToPrevious", sender: self)
}
and a navigation controller where I pop the AR scene controller off the stack. Every time the previous view controllers are frozen. No errors in Xcode, the app keeps running .
If I wait for approx. 2 minutes I can use the screen again. If I don't add nodes with view controllers to my AR scene, everything works perfectly fine. My only explanation is that the UIThread is overwhelmed when adding nodes because it creates a massive memory leak somewhere (that I haven't found despite of 10 hours of debugging). Has anyone had a similar experience and can tell me how to resolve this? What can I do to debug this and ensure smooth navigation?
The problem was that I assigned my custom UIViewController views directly to the nodes' planes. My assumption is that this created reference cycles since the view controllers held references to objects that I used elsewhere in the scene. I resolved it by capturing an image of the view and assigning that one to the node.
Seems like it is generally a dangerous practice to assign UIViews directly to nodes because it invites reference cycles.
Maybe someone more qualified on memory issues and/or SceneKit can give an opinion on this.
/* I think whenever UIView are directly attached to SCNMaterial and
that SCNMaterial to SCNNode. SCNMaterial are still being used when
previous screen is visited. By resetting ARSCNView seems to resolve
the issue. */
//try this
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
//reset ARSCNView before going back to previous screen
self.sceneView = ARSCNView()
}

Going to the previous scene SpriteKit

I'm making a game. On the main menu I have a button for "Start Game" "High Scores" "Options" "Store".
I have various scenes for the different levels. Each of those scenes has a "pause button" which takes you to the "Options" scene. The Options scene has a "BACK" button.
How do I get that "BACK" button to return to what have scene called it?
Right now the "BACK" button only has the ability to return to the menu ie SceneSkip in my code, regardless of what scene called it:
func back(){
let sceneSkip = SceneSkip(fileNamed: "SceneSkip")
sceneSkip?.scaleMode = .aspectFill
self.view?.presentScene(sceneSkip!, transition: SKTransition.fade(withDuration: 0.5))
}
So I've been working on a game in my spare time and I have solved pretty much exactly what you're doing. Here are a couple things I learned in the process.
1) When you present a scene, your scene's didMove(to: view) method is going to be called. Usually you do your initialization in that method so if you are just pausing, and you present the scene again, you're likely going to mess up the game state.
2) Because of number 1, you can't really present a new scene each time the user wants to pause.
So... the solution I've been using, which is the result of a number of google searches, experimenting and etc, is to actually use a separate view for all my popup/pause type scenes. I set it up like this in my GameViewController viewWillLayoutSubviews where skView is the main view:
let psize = view.bounds
popup = SKView.init(frame: psize)
skView.addSubview(popup!)
popup?.allowsTransparency=true
popup?.isHidden=true
Then, later when any scene anywhere in the game wants to add a popup, I added the following functions:
func showPopupScene(_ scene : SKScene) {
(self.view as? SKView)?.scene?.isPaused = true
popup?.isHidden=false
popup?.presentScene(scene)
}
func closePopup() {
popup?.isHidden=true
if let v=view as? SKView {
(v.scene as? PopupNotify)?.onPopupClosed()
v.scene?.isPaused=false
}
}
Now, any scene can create a scene, and show it as a popup with showPopupScene. The popup scene then needs to call closePopup and the game returns to the scene where it left off.
Other items:
I wanted my game scenes to behave correctly with pausing popups as well as when they are paused from coming out of background etc... so I override isPaused:
override var isPaused : Bool {
get {
guard let v = self.view?.window?.rootViewController as? GameViewController else {
return super.isPaused
}
guard let p = v.popup else { return super.isPaused }
return super.isPaused || !p.isHidden
}
set(newPaused) {
super.isPaused = newPaused
physicsWorld.speed = newPaused ? 0 : 1.0
}
}
Also, the PopupNotify protocol for my scenes helped where I wanted scenes to be aware that the popup was closed in case they needed to make any changes according to whatever the popup was showing.
Hope I'm not forgetting anything, but the combination of these additions has provided pretty easy popup/pause management.
I think that your are going about "levels" in a difficult way.
What I suggest you do is...
Have a scene for your menu which transitions to your GameScene.
In your GameScene you load all generic objects that are common to the game regardless of the level (such as gameHud, score labels, pause buttons, options buttons etc.)
you then load your level scene into GameScene based on a parameter (levelID).
this way none of your generic objects have to be created in multiple scenes greatly reducing the chance of redundancy errors.
Completely optional portion
I don't like to switch scenes in the middle of a game for pausing or options. it creates to many opportunities for something to go wrong (failing to load data, failing to save locations, score, animations etc.).
I create my pause and option dialogs as SKSpriteNodes and pause the game and display the dialog over top of the GameScene. That way when I am done with the dialog I can just remove it, and unpause the game and everything goes back to how it was without having to load the scene all over.

Transition to Root View Controller

I've tried literally countless and various ways to back to my initial viewController but I can't get anything to work. If any of you guys can see what i'm doing wrong I would be incredibly happy.
Just to make sure I have a print statement in every scene and every viewcontroller and initially the rootviewcontroller print statement does work but I know it's not fully transitioning over since the print statement isn't happening again.
Below is a snippet of the transition code and below that a picture of my storyboard:
func menu()
{
self.view?.presentScene(nil)
//let theVC = self.viewController?.storyboard?.instantiateViewController(withIdentifier: "menuVC") as! MenuVC?
//self.viewController?.navigationController?.pushViewController(theVC!, animated: true)
self.viewController?.dismiss(animated: false, completion: nil)
self.viewController?.navigationController?.popToRootViewController(animated: true)
print("LastScene")
}

Warning: Attempt to present view controller on another view controller whose view is not in the window hierarchy

I have a working simple single player game, where the initial view controller has a button to start the game. This button performs a segue and all game logic in the GameViewController is working as expected.
I've followed this tutorial to add multi player functionality to my game.
On the initial view controller, a button now calls
GameKitHelper.sharedGameKitHelper.findMatchWithMinPlayers(2, maxPlayers: 2, viewController: self, delegate: MultiPlayerNetworking)
}
which has the following implementation in GameKitHelper.swift:
func findMatchWithMinPlayers (minPlayers: Int, maxPlayers: Int, viewController: UIViewController, delegate: GameKitHelperDelegate) {
matchStarted = false
let request = GKMatchRequest()
self.delegate = delegate
request.minPlayers = 2
request.maxPlayers = 2
presentingViewController = viewController
presentingViewController.dismissViewControllerAnimated(false, completion: nil)
let mmvc = GKMatchmakerViewController(matchRequest: request)
mmvc?.matchmakerDelegate = self
presentingViewController.presentViewController(mmvc!, animated: true, completion: nil)
self.delegate?.matchStarted()
}
The Class MultiPlayerNetworking implements the GameKitHelper protocol, and gets called on the matchStarted function.
The MultiPlayerNetworking class in essence takes over here, and starts sending out messages to hosts and remote players.
Note that some time later, When auto-matching finishes, the following function gets called in GameKitHelper:
func matchmakerViewController(viewController: GKMatchmakerViewController, didFindMatch match: GKMatch) {
viewcontroller.dismissViewControllerAnimated(true, completion: {})
self.match = match
match.delegate = self
}
Now, I think this says that the GKMatchmakerViewController is dismissed, thereby showing me the initial view controller again (and this is what happens on screen).
Now my issue! After the GKMatchmakerViewController is dismissed, I'm back at the initial view controller and want to 'simulate' an automatic segue to my gameView (which has logic to deal with a multi player game as well).
I've made the initial view controller conform to the MultiPlayerNetworking protocol, which has a function to simulate a segue:
func segueToGVC() {
self.performSegueWithIdentifier("game", sender: nil) // self = initial view controller
}
However, xCode complains with:
Warning: Attempt to present <GameViewController: 0x7d440050> on <GKMatchmakerViewController: 0x7c8fbc00> whose view is not in the window hierarchy!
I'm stuck here, and have tried so many different methods of dismissing the view controller, to making sure I'm calling the performSegue function on the topViewController via this link, but nothing works.
My question: why is the GKMatchmakerViewController visually dismissed, but still present in the view hierarchy, such that calling a performSegue function on the initial view controller give the above error/warning?
Views are greatly appreciated!
why is the GKMatchmakerViewController visually dismissed, but still present in the view hierarchy
Here are two suggestions:
Perhaps it's because dismissal takes time. You are saying:
viewcontroller.dismissViewControllerAnimated(true, completion: {})
So there's an animation. Don't attempt to perform the next segue until the animation is over.
Perhaps you are just wrong about who self is. You are saying:
self.performSegueWithIdentifier("game", sender: nil)
// self = initial view controller
We have only your word, in that comment, for who self is. Meanwhile, the runtime seems to think differently about the matter:
Attempt to present <GameViewController: 0x7d440050> on <GKMatchmakerViewController: 0x7c8fbc00>
It might be good to believe the runtime; after all, it knows more than you do.