How to reference current SKScene from the AppDelegate in Swift - swift

I have a SKScene that has a pause method. I want to be able to do something like this:
func applicationWillTerminate(_ application: UIApplication) {
pauseLevel()
}
However, I don't know how to get a reference to my SKScene from the AppDelegate.
I tried using
application.inputView
However, that is an UIView. How can I get an SKScene?
EDIT
deinit {
NotificationCenter.default.removeObserver(self)
}
override func didMove(to view: SKView) {
self.anchorPoint = CGPoint(x: 0.5, y: 0.5)
self.backgroundColor = UIColor(red:0.17, green:0.24, blue:0.31, alpha:1.0)
self.physicsWorld.gravity = CGVector(dx: 0, dy: 0)
NotificationCenter.default.addObserver(self, selector: #selector(runPause), name: .UIApplicationWillResignActive, object: nil)
}
would that be sufficient and effective at removing the observer every time?

I would scrap your way of thinking. in iOS, notifications get sent for when application events happen. In your case, the notification is called UIApplicationWillTerminate
What you want to do is hook into this notification in your Scene class, I would recommend in the didMove(to:) method.
NotificationCenter.default.addObserver(self, selector: #selector(pauseLevel), name: .UIApplicationWillTerminate, object: nil)
Now when you do this, you need to remember to remove the observer
when you are removing the scene, so you want to use the code:
NotificationCenter.default.removeObserver(self)
At moments the scene is removed. I would recommend at the least putting it in the deinit
Now in Swift 4, things change a little bit. You need to add #objc to your pauseLevel function so that it can be exposed to objective c libraries.

Related

Swift scripted mouse movement unreliable inside scheduledTimer

I'm trying to write a piece of code that moves the mouse cursor on the screen to a specified location. Here are some ways I've tried to accomplish that:
Code below is used in all attempts (sandboxing disabled):
let mouseCursorPosition = CGPoint.zero;
CGAssociateMouseAndMouseCursorPosition(1)
Attempt 1:
CGWarpMouseCursorPosition(mouseCursorPosition)
Attempt 2:
CGDisplayMoveCursorToPoint(CGMainDisplayID(), CGPoint.zero)
Attempt 3:
let moveEvent = CGEvent(mouseEventSource: nil, mouseType: CGEventType.mouseMoved, mouseCursorPosition: mouseCursorPosition,
mouseButton: CGMouseButton.left)
moveEvent?.post(tap: CGEventTapLocation.cghidEventTap)
Expected behavior: update the mouse position along with the cursor on screen.
Every attempt so far works as expected if called directly inside viewDidLoad(). The cursor moves to the requested screen location as soon as the application loads. However, things get a bit weird when called inside a scheduledTimer. Using the code below:
class ViewController: NSViewController {
var mouseTimer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
mouseTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(moveCursor), userInfo: nil, repeats: true)
}
#objc func moveCursor() {
print(NSDate().timeIntervalSince1970)
let mouseCursorPosition = CGPoint.zero;
CGAssociateMouseAndMouseCursorPosition(1)
//insert attempt code here
}
}
The moveCursor method is called every 5 seconds (the timestamp appears every time), however the cursor does not move on the screen as long I don't move the physical mouse. However, when I move the mouse, the cursor first snaps to the last scripted position. What seems to happen is that the scripted location is registered, however it is not updated on screen until some sort of refresh event is triggered.
Any thoughts on the matter are greatly appreciated.
I cannot reproduce this problem on macOS 11.14. I created a new Cocoa app, using Storyboards, and edited ViewController.swift to the following:
import Cocoa
class ViewController: NSViewController {
var mouseTimer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
mouseTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(moveCursor), userInfo: nil, repeats: true)
}
#objc func moveCursor() {
print(NSDate().timeIntervalSince1970)
let mouseCursorPosition = CGPoint.zero;
CGAssociateMouseAndMouseCursorPosition(1)
CGWarpMouseCursorPosition(mouseCursorPosition)
}
}
I then ran it under Xcode. After 5 seconds (and every 5 seconds after) a timestamp is printed and the cursor jumps to the upper-left corner. I can move the cursor elsewhere and it jumps again.
I have the same behavior with or without calling CGAssociateMouseAndMouseCursorPosition. (I'm not sure why you're calling this in the test; 1 is the default value.)
Updated based on your comments and I still can't reproduce:
import Cocoa
class ViewController: NSViewController {
var mouseTimer: Timer!
override func viewDidLoad() {
super.viewDidLoad()
mouseTimer = Timer.scheduledTimer(timeInterval: 5, target: self, selector: #selector(moveCursor), userInfo: nil, repeats: true)
}
#objc func moveCursor() {
let mouseCursorPosition = CGPoint(x: Int.random(in: 0...500), y: Int.random(in: 0...500))
print("\(Date().timeIntervalSince1970): \(mouseCursorPosition)")
CGWarpMouseCursorPosition(mouseCursorPosition)
}
}
Every 5 seconds this causes the mouse pointer to jump to a new location, whether I move the mouse pointer or not. Is this identical to your code?

Swift/SpriteKit: Attempting to reach a function/method in a class from another class not working

I have a function in my ScoreSystem class named addScore. The function adds 1 point to the game, updates the SKLabelNode to the current score and in turn calls the function startNewLevel every 25 points.
func addScore(scene: SKScene) {
gameScore += 1
scoreLabel.text = "\(gameScore)"
if CGFloat(gameScore).truncatingRemainder(dividingBy: 25) == 0 {
NotificationCenter.default.post(name: Notification.Name.init("start_new_level"), object: nil)
GameScreen().displayLevel(scene: scene)
}
}
The function gets called every time a torpedo that has been fired hits the enemy. I now want to add a new level where meteors (SKSpriteNode) have to be avoided. I have several SKActions in a sequence to accomplish this. Essentially, the SKSpriteNode moves from the top of the screen, reaches below the screen and gets deleted. If the meteor reaches the bottom of the screen means that it has been avoided by the player.
I'm attempting to call the function addScore but it doesn't update.
Here is the function:
let scoreSystem = ScoreSystem()
func spawnMeteor() {
let randomXStart = CGFloat.random(min: gameArea.minX, max: gameArea.maxX)
let startPoint = CGPoint(x: randomXStart, y: scene.size.height * 1.2)
let endPoint = CGPoint(x: randomXStart, y: -scene.size.height * 0.2)
let meteor = SKSpriteNode(imageNamed: "meteor")
meteor.name = "Meteor"
meteor.zPosition = 2
meteor.position = startPoint
let moveMeteor = SKAction.move(to: endPoint, duration: 3)
let deleteEnemy = SKAction.removeFromParent()
let score = SKAction.run(addToScore)
let meteorSequence = SKAction.sequence([
moveMeteor,
score,
deleteEnemy])
scene.addChild(meteor)
meteor.run(meteorSequence)
}
I have tried a function addToScore like this:
func addToScore() {
scoreSystem.addScore(scene: scene!)
}
And also tried this
func addToScore() {
NotificationCenter.default.post(name: Notification.Name.init("add_to_score"), object: nil)
}
When trying this second alternative, I add the following to the GameScene
override func sceneDidLoad() {
super.sceneDidLoad()
NotificationCenter.default.addObserver(forName: Notification.Name.init("add_to_score"), object: nil, queue: OperationQueue.main) { [weak self] (notification) in
self?.scoreSystem.addScore(scene: self!)
}
}
I removed several lines from the spawnMeteor() function so not to clog the space with unnecessary lines of code. I have yet to figure out how to call that function using SKAction.run(). Can someone point me in the right direction?
You are passing along a lot of information to your functions, probably too much.
I would suggest either you implement Protocols or Notifications to handle your information, personally I prefer protocols so my example will be protocols.
I am making some assumptions about your code because not all of it is presented in your question...
protocol ScoreSystemDelegate: class {
func displayLevel()
}
class ScoreSystem: SKNode {
weak var scoreSystemDelegate: ScoreSystemDelegate!
//your init func and any other funcs in this class (unknown)
func addScore(scene: SKScene) {
gameScore += 1
scoreLabel.text = "\(gameScore)"
if CGFloat(gameScore).truncatingRemainder(dividingBy: 25) == 0 {
NotificationCenter.default.post(name: Notification.Name.init("start_new_level"), object: nil)
//GameScreen().displayLevel(scene: scene)
self.scoreSystemDelegate.displayLevel()
}
}
}
in your class that creates the scoreSystem (assuming GameScene)...
class GameScene: SKScene, ScoreSystemDelegate {
let scoreSystem = ScoreSystem()
//assign GameScene as the delegate of ScoreSystem that way the 2 can communicate
scoreSystem.scoreSystemDelegate = self
func displayLevel() {
//do whatever you do when you display the level
}
}
now your spawn meteor func should work as you have coded because addScore no longer takes a scene property, nice thing about this approach is that you can make any object a delegate of ScoreSystem it doesn't have to be a scene.

How to control one IB custom view with mouseEntered/-Exited in another IB custom view

I'm sorry for my incompetence, but I'm new to Cocoa, Swift, and object-oriented programming, in general. My primary sources have been Cocoa Programming for OS X (5th ed.), and Apple's jargon- and Objective-C-riddled Developer pages. But I'm here because I haven't seen (or didn't realize that I saw) anything that speaks to this problem.
I want to change the contents of one IB-created custom view, LeftView, by mouseEntered/-Exited actions in another IB-created custom view, RightView. Both are in the same window. I created a toy program to try to figure things out, but to no avail.
Here's the class definition for RightView (which is supposed to change LeftView):
import Cocoa
class RightView: NSView {
override func drawRect(dirtyRect: NSRect) {
// Nothing here, for now.
}
override func viewDidMoveToWindow() {
window?.acceptsMouseMovedEvents = true
let options: NSTrackingAreaOptions =
[.MouseEnteredAndExited, .ActiveAlways, .InVisibleRect]
let trackingArea = NSTrackingArea(rect: NSRect(),
options: options,
owner: self,
userInfo: nil)
addTrackingArea(trackingArea)
}
override func mouseEntered(theEvent: NSEvent) {
Swift.print("Mouse entered!")
LeftView().showStuff(true)
}
override func mouseExited(theEvent: NSEvent) {
Swift.print("Mouse exited!")
LeftView().showStuff(false)
}
}
And here's the class definition for LeftView (which is supposed to be changed by RightView):
import Cocoa
class LeftView: NSView {
var value: Bool = false {
didSet {
needsDisplay = true
Swift.print("didSet happened and needsDisplay was \(needsDisplay)")
}
}
override func mouseUp(theEvent: NSEvent) {
showStuff(true)
}
override func drawRect(dirtyRect: NSRect) {
let backgroundColor = NSColor.blackColor()
backgroundColor.set()
NSBezierPath.fillRect(bounds)
Swift.print("drawRect was called when needsDisplay was \(needsDisplay)")
switch value {
case true: NSColor.greenColor().set()
case false: NSColor.redColor().set()
}
NSBezierPath.fillRect(NSRect(x: 40, y: 40,
width: bounds.width - 80, height: bounds.height - 80))
}
func showStuff(showing: Bool) {
Swift.print("Trying to show? \(showing)")
value = showing
}
}
I'm sure I'm missing something "completely obvious," but I'm a little dense. If you could tell me how to fix the code/xib files, I would very much appreciate it. If you could explain things like when talking to a child, I would be even more appreciative. When I take over the world (I'm not incompetent at everything), I will remember your kindness.
I came up with a workaround that is much simpler than my prior strategy. Instead of having mouseEntered/-Exited actions in one custom view try to control what is displayed in another custom view, I simply put the mouseEntered/-Exited code into the view that I wanted to control, and then I altered the location of rect: in NSTrackingArea.
Before moving code over by for that approach, I had tried changing the owner: in NSTrackingArea to LeftView() and just moving over the mouseEntered/-Exited code. That generated lots of scary error messages (naive newbie talking here), so I gave up on it. It would be nice to know, though, how one can correctly assign an owner other than self.
In any case, any further thoughts or insights would be appreciated.

How do I detect which SKSpriteNode has been touched

I find a similar question, but I am trying to detect and identify which Sprite the user touch, and I don't know how to do that. This is my variable:
var sprites: [[SKSpriteNode]] = [[SKSpriteNode(imageNamed: "a"), SKSpriteNode(imageNamed: "b")], [SKSpriteNode(imageNamed: "c"),SKSpriteNode(imageNamed: "d")]]
The idea is identify the spriteNode and then replace it for other sprite or change the color, but I don´t know how to do it with this matrix of spriteNodes, I guess the first step it´s identify the sprite.
What you are trying to do (even if I don't see a reason for this) can be accomplished using delegation pattern. Basically, you will tell your delegate (the scene, or whatever you set as a delegate) to do something for you, and you will do that directly from within the button's touchesBegan method. Also, you will pass the button's name to a scene.
To make this happen, first you have to define a protocol called ButtonDelegate. That protocol defines a requirement which states that any conforming class has to implement a method called printButtonsName(_:):
protocol ButtonDelegate:class {
func printButtonsName(name:String?)
}
This is the method which will be implemented in your GameSceneclass, but called from within button's touchesBegan. Also, this method will be used to pass a button's name to its delegate (scene), so you will always know which button is tapped.
Next thing is button class itself. Button might look like this:
class Button : SKSpriteNode{
weak var delegate:ButtonDelegate?
init(name:String){
super.init(texture: nil, color: .purpleColor(), size: CGSize(width: 50, height: 50))
self.name = name
self.userInteractionEnabled = true
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
delegate?.printButtonsName(self.name)
}
}
The important thing here is userInteractionEnabled = true, which means that button will accept touches. Another important thing is a delegate property. As already mentioned, buttons will have the scene set as their delegate. Setting a scene as delegate of buttons will be done later when we create some buttons... To make this easier for you, think of a delegate as a worker who works for his boss :) The boss (a button) tells his worker (a scene) to do something for him (to prints his name).
Okay, so lets make sure that scene conforms to a ButtonDelegate protocol...Why is this important? It is important because the worker (scene) must follow the orders of his boss (a button). By conforming to this protocol, the worker is making a contract with his boss where confirming that he knows how to do his job and will follow his orders :)
class GameScene: SKScene, ButtonDelegate {
override func didMoveToView(view: SKView) {
let play = Button(name:"play")
play.delegate = self
let stop = Button(name:"stop")
stop.delegate = self
play.position = CGPoint(x: frame.midX - 50.0, y: frame.midY)
stop.position = CGPoint(x: frame.midX + 50.0, y: frame.midY)
addChild(play)
addChild(stop)
}
func printButtonsName(name: String?) {
if let buttonName = name {
print("Pressed button : \(buttonName) ")
}
//Use switch here to take appropriate actions based on button's name (if you like)
}
}
And that's it. When you tap the play button, the touchesBegan on a button itself will be called, then the button will tell its delegate to print its name using the method defined inside of scene class.
First, you need another way to create the sprite, here are a way:
let spriteA = SKSpriteNode(imageNamed: "a")
scene.addChild(spriteA)
let spriteB = SKSPriteNode(imageNamed: "b")
scene.addChild(spriteB)
...and so on...
Now we needs to set a name for the sprite so we can know which node is tapped later. To add a name for a sprite just do this:
spriteNode.name = "name of the sprite"
Putting this code in the above example will look something like this:
let spriteA = SKSpriteNode(imageNamed: "a")
spriteA.name = "a"
scene.addChild(spriteA)
let spriteB = SKSPriteNode(imageNamed: "b")
spriteB.name = "b"
scene.addChild(spriteB)
...and so on...
To detect touches put this into your SKScene subclass:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
let touch = touches.first as UITouch!
let touchLocation = touch.locationInNode(self)
let targetNode = nodeAtPoint(touchLocation) as! SKSpriteNode
}
The targetNode is the node you tapped.
If you wants to get the name of the sprite you can use targetNode.name.

SpriteKit scene transition good practices

I am writing a game using SpriteKit with Swift and have run into a memory concern.
The layout of my game is such that the GameViewController (UIViewController) presents the first SKScene (levelChooserScene) in the viewDidLoad Screen. This scene does nothing more than display a bunch of buttons. When the user selects a button the scene then transitions to the correct scene using skView.presentScene, and when the level is complete, that scene then transitions back to the levelChooserScene and the game is ready for the user to select the next level.
The problem is that when the transition back to the levelChooserScene occurs the memory allocated for the game play scene is not deallocated, so after selecting only a few levels I start receiving memory errors.
Is my design correct in transitioning from SKScene to SKScene, or should I instead return to the GameViewController each time and then transition to the next SKScene from there?
I have found a few posts on here that say I should call skView.presentScene(nil) between scenes, but I am confused on how or where to implement that.
I simply want to transition from one SKScene to another and have the memory used from the outgoing scene to be returned to the system.
This is an example of how I have implemented the SKScene:
class Level3: SKScene
{
var explodingRockTimer = NSTimer()
var blowingUpTheRocks = SKAction()
override func didMoveToView(view: SKView)
{
NSTimer.scheduledTimerWithTimeInterval(5.0, target: self, selector: "dismissTheScene:", userInfo: nil, repeats: false)
var wait = SKAction.waitForDuration(0.5)
var run = SKAction.runBlock{
// your code here ...
self.explodeSomeRocks()
}
let runIt = SKAction.sequence([wait,run])
self.runAction(SKAction.repeatActionForever(runIt), withKey: "blowingUpRocks")
var dismissalWait = SKAction.waitForDuration(5.0)
var dismissalRun = SKAction.runBlock{
self.removeActionForKey("blowingUpRocks")
self.dismissTheScene()
}
self.runAction(SKAction.sequence([dismissalWait,dismissalRun]))
}
func explodeSomeRocks()
{
println("Timer fired")
}
//MARK: - Dismiss back to the level selector
func dismissTheScene()
{
let skView = self.view as SKView?
var nextScene = SKScene()
nextScene = LevelChooserScene()
nextScene.size = skView!.bounds.size
nextScene.scaleMode = .AspectFill
var sceneTransition = SKTransition.fadeWithColor(UIColor.blackColor(), duration: 1.5) //WithDuration(2.0)
//var sceneTransition = SKTransition.pushWithDirection(SKTransitionDirection.Down, duration: 0.75) //WithDuration(2.0)
//var sceneTransition = SKTransition.crossFadeWithDuration(1.0)
//var sceneTransition = SKTransition.doorwayWithDuration(1.0)
sceneTransition.pausesOutgoingScene = true
skView!.presentScene(nextScene, transition: sceneTransition)
}
}
Well the thing that was causing my trouble was inserting particle emitters every half second for 5 seconds using SKAction.repeatActionForever() to call the emitter insert function.
This repeatAction apparently was not killed by transitioning to another scene, and was causing the memory for the whole scene to be retained. I switched to SKAction.repeatAction() instead and specify how many time it should fire. The scene now returns all of its memory when I transition to the new scene.
I am not sure I understand this behavior though.
SpriteKit it's not strongly documented when it comes to create complex games. I personally had a problem like this for days until I managed to figure it out.
Some objects retain the reference, so it doesn't deinit. (SKActions, Timers, etc)
Before presenting a new scene I call a prepare_deinit() function where I manually remove the strong references which are usually not deallocated by swift.
func prepare_deinit()
{
game_timer.invalidate() // for Timer()
removeAction(forKey: "blowingUpRocks") // for SKAction in your case
// I usually add the specific actions to an object and then remove
object.removeAllActions()
// If you create your own object/class that doesn't deinit, remove all object
//actions and the object itself
custom_object.removeAllActions()
custom_object.removeFromParent()
}
deinit
{
print("GameScene deinited")
}
The last problem I encountered was that the new scene was presented much faster than my prepare_deinit() so I had to present the new scene a little later, giving the prepare_deinit() enough time to deallocate all objects.
let new_scene =
{
let transition = SKTransition.flipVertical(withDuration: 1.0)
let next_scene = FinishScene(fileNamed: "FinishScene")
next_scene?.scaleMode = self.scaleMode
next_scene?.name = "finish"
self.view?.presentScene(next_scene!, transition: transition)
}
run(SKAction.sequence([SKAction.run(prepare_deinit), SKAction.wait(forDuration: 0.25), SKAction.run(exit_to_finish)]))