switching between enum states with touch - ios

I have a simple player class like this:
enum State: Int {
case dead, alive, idle
}
class Player {
var playerSprite : SKSpriteNode
var currentState : State
init(passedInState: State) {
currentState = passedInState
self.playerSprite = SKSpriteNode(imageNamed: "playersprite.png")
switch currentState {
case .dead:
println("Player is dead")
case .alive:
println("Player is alive")
}
}
}
In my main class i create an instance of the Player class and try to change the players state by tapping on it in the touch method like so:
class GameScene: SKScene {
override func didMoveToView(view: SKView) {
let player1 = Player(passedInState: .alive)
self.addChild(player1.playerSprite)
player1.playerSprite.position = CGPointMake(100, 100)
}
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
for touch in touches {
let touch = touches.anyObject() as UITouch
let location = touch.locationInNode(self)
var locationSprites = nodesAtPoint(location)
for bla in locationSprites {
(bla as Player).currentState = .dead
}
}
}
I always get a crash when touching the Sprite. Do you have any idea why?

It seems there's a downcast failure, and in your code I see there are 2 forced donwcasts:
let touch = touches.anyObject() as UITouch
and
(bla as Player).currentState = .dead
I presume the error is in the last one, so I suggest setting a breakpoint and inspecting bla to check whether it's actually an instance of Player. Basing on your logic, if it can happen that it is not an instance of Player, I recommend using optional downcast:
if let player = bla as? Player {
player.currentState = .dead
}
The advantage is that if bla is not an instance of Player, the if block won't be executed, instead of making the app crash.
Comment follow up
If you want to do some custom processing when the player status changes, you can add a property observer to the currentState property of the Player class:
var currentState : State {
didSet {
switch currentState {
case .dead:
println("Player is dead")
case .alive:
println("Player is alive")
case .idle:
break
}
}
}

Related

Swift/Xcode: How do I detect key presses?

I am wondering how I could detect key presses in Xcode - my goal is to be able to detect when keys are pressed, specifically the space, escape, enter and WASD keys.
What I have tried
I have tried pressesBegan, pressesEnded and its other 'variants', and I have also tried keyDown. After looking for 2 hours, the only ways I could find to detect key presses were from the two methods listed above, which don't seem to be working for me.
When using pressesBegan, I tried putting the following code in both the GameViewController.swift file and the AppDelegate.swift file, however it doesn't seem to work:
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
print(key.keyCode)
}
The code shows no errors, and even when I put a print file at the very start of the function to see if it is being called it does not give any output.
After this, I tried a keyDown function (in the GameViewController.swift file), however it threw the error "Cannot find type NSEvent in scope". I fixed this error by adding ", NSObject" to the top of the file in the line "class GameViewController: UIViewController {" but that threw the error "Multiple inheritance from classes 'UIViewController' and 'NSObject'". I have looked for about an hour but there are no solutions which match my issue/have not worked for me.
import UIKit
import SpriteKit
import GameplayKit
class GameViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let view = self.view as! SKView? {
// Load the SKScene from 'GameScene.sks'
if let scene = SKScene(fileNamed: "SplashScreen") {
// Set the scale mode to scale to fit the window
scene.scaleMode = .aspectFill
// Present the scene
view.presentScene(scene)
}
view.ignoresSiblingOrder = true
view.showsFPS = true
view.showsNodeCount = true
}
}
override var shouldAutorotate: Bool {
return true
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
if UIDevice.current.userInterfaceIdiom == .phone {
return .allButUpsideDown
} else {
return .all
}
}
override var prefersStatusBarHidden: Bool {
return true
}
override func pressesBegan(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
case .keyboardR:
print("Roll dice")
case .keyboardH:
print("Show help")
default:
super.pressesBegan(presses, with: event)
}
}
override func keyDown(with event: NSEvent) {
print("a")
}
}
If you need any additional things I'll try to get back to you as quick as possible. I'm really sorry if I've missed something extremely obvious like putting it in a different file.
Thank you!
the pressesEnd() method works in the same way; Try to import UIKit first:
override func pressesEnded(_ presses: Set<UIPress>, with event: UIPressesEvent?) {
guard let key = presses.first?.key else { return }
switch key.keyCode {
case .keyboardSpacebar:
print("Continue the quiz…")
default:
super.pressesEnded(presses, with: event)
}
}
More can be found at:
https://www.hackingwithswift.com/example-code/uikit/how-to-detect-keyboard-input-using-pressesbegan-and-pressesended

Swift: Using switch statement in touchesBegan

I want to clean up my touchesBegan(..) in SKScene. I wanted to make a case statement instead of my if .. else chain. However, I get errors when implementing, which suggests that I don't know how equality is done under the hood.
Before code:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if self.nodeAtPoint(location) === playLabel {
buildLevelSelect()
} else if self.nodeAtPoint(location) === aboutLabel {
createAboutView()
} else if self.nodeAtPoint(location) === storeLabel {
createStore()
}
}
}
After code: After clicking around, some label clicks work, but some others raise a Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode 0x0) error:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
switch(self.nodeAtPoint(location)){
case playLabel :
buildLevelSelect()
case aboutLabel :
createAboutView()
case storeLabel :
createStore()
default : break
}
}
You can write a sort of odd looking but functional switch if you want the === behavior as follows:
switch(true){
case self.nodeAtPoint(location) === playLabel : buildLevelSelect()
case self.nodeAtPoint(location) === aboutLabel : createAboutView()
case self.nodeAtPoint(location) === storeLabel : createStore()
default : break
}
In my opinion, this is still cleaner looking than the if-else chain.
The easiest way to accomplish this would be by populating the .name property of your nodes, so then you could switch on the name instead. So then your switch would look like this:
switch (self.nodeAtPoint(location).name ?? "") {
case "playLabel" :
buildLevelSelect()
case "aboutLabel" :
...
You can also cast the objects in the switch statement and then your code works as expected
if let touch = touches.first as UITouch! {
let touchLocation = touch.location(in: self)
switch self.atPoint(touchLocation) {
case redBox as SKSpriteNode:
print("touched redBox")
case greenBox as SKSpriteNode:
print("touched greenBox")
default:
print("touched nothing")
}
}

memory grows when switching to a new scene

I have the game in which the new scene is called by pressing buttons. When you click in a new scene sent the corresponding pictures. When you return back to the game map memory always increases by 30 MB. I do not understand where the strongest link. Instruments can not detect leaks. Sorry my english. Help me please.
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch: AnyObject in touches {
let location = touch.location(in: self)
for i in 0...3 {
if childNode(withName: "button\(i)")!.isHidden == false && childNode(withName: "button\(i)")!.contains(location) {
buttonOfBattlefield = childNode(withName: "button\(i)")
}
}
switch buttonOfBattlefield?.name {
case "button0"?:
battlefieldName = "A"
case "button1"?:
battlefieldName = "B"
case "button2"?:
battlefieldName = "C"
case "button3"?:
battlefieldName = "D"
default:
break
}
if battlefieldName != nil {
let myScene = GameScene(size: self.size , battlefield: battlefieldName!)
myScene.scaleMode = self.scaleMode
let reveal = SKTransition.fade(withDuration: 2.0)
self.view?.presentScene(myScene, transition: reveal)
}
}
}
Essentially there could be many factors that cause the increase of memory in your game.
I try to help you with some useful correction.
Speaking about custom protocols, you could break the strong references by adding class at the end of the line, and declare weak var to the delegate:
protocol ResumeBtnSelectorDelegate: class {
func didPressResumeBtn(resumeBtn:SKSpriteNode)
}
weak var resumeBtnDelegate:ResumeBtnSelectorDelegate?
...
Speaking about completion could be a strong reference to self so you can do like this example:
self.launchVideo(completion: {
[weak self] success in
guard let strongSelf = self else { return }
//self.showMyVideo()
strongSelf.showMyVideo()
}
same thing to run actions blocks:
let exp = SKAction.run {
[weak self] in
guard let strongSelf = self else { return }
strongSelf.getExplosion(percentageDimension: 15,type: 0,position: enemy.position)
}
If you use third part library in objective C , you may need to remove the strong references also there:
__weak __typeof__(self) weakSelf = self;
SKAction *sequence = [SKAction sequence:#[[SKAction followPath:_ascentPath duration:1.5], [SKAction runBlock:^(void){[weakSelf explode];}]]];
[self runAction:sequence];
}
If you have some observer try to remove it, samething for NSTimers to the willMoveFromView methods.
override func willMove(from view: SKView) {
//remove UIKit objects, observers, timers..
}
The solution found. The new Game Scene need to use the method.
self.enumerateChildNodes(withName: "//*"){ (node, stop) -> Void in
node.removeAllActions()
node.removeAllChildren()
node.removeFromParent()
}
Thank you all for your help. I'm happy!

How to switch on score to create an SKEmitterNode using SpriteKit?

I want to be able to toggle my SKEmitterNode (rain particles) off and on based on the score. But my update function gets called constantly, i.e. I end up with millions of particles on the screen with my current code below...how can I structure my code so that the rain particles will only get called once when a score is achieved?
class GameScene: SKScene, SKPhysicsContactDelegate {
func setUpRain() {
if let rainParticle = SKEmitterNode(fileNamed: "Rain") {
rainParticle.position = CGPointMake(frame.size.width, frame.size.height)
rainParticle.name = "rainParticle"
rainParticle.zPosition = Layer.Flash.rawValue
worldNode.addChild(rainParticle)
}
}
func makeItRain() {
let startRaining = SKAction.runBlock {
self.setUpRain()
}
runAction(startRaining, withKey: "Rain")
}
func stopRaining() {
removeActionForKey("Rain")
worldNode.enumerateChildNodesWithName("rainParticle", usingBlock: { node, stop in
node.removeFromParent()
})
}
}
class PlayingState: GKState {
unowned let scene: GameScene //used to gain access to our scene
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if scene.score > 2 {
scene.makeItRain()
}
if scene.score > 4 {
scene.stopRaining()
}
}
There's a few ways you can do this, but the simplest of these is to only call makeItRain() or stopRaining() once per toggle. What I mean by this is once makeItRain is called, it cannot be called again until stopRaining is called. This can be done with a boolean like so:
var rainToggle: Bool = false; //True = Raining
override func updateWithDeltaTime(seconds: NSTimeInterval) {
scene.updateForegroundAndBackground()
scene.updateScore()
if (scene.score > 4){
scene.stopRaining()
rainToggle = false;
}
else if (scene.score > 2 && !rainToggle) {
scene.makeItRain()
rainToggle = true;
}
}
This is only slightly inefficient since you are calling stopRaining() every frame for no reason, however it gets the job done and is easy to understand. Note also that I had to flip the order in which your if statements came (otherwise it wouldn't work).

How to detect all touches in Swift 2

I'm trying to create a timeout function for an app I'm develop using Swift 2 but in swift 2, you can put this code in the app delegate and it works but it does not detect any keyboard presses, button presses, textfield presses, and etc:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
super.touchesBegan(touches, withEvent: event);
let allTouches = event!.allTouches();
if(allTouches?.count > 0) {
let phase = (allTouches!.first as UITouch!).phase;
if(phase == UITouchPhase.Began || phase == UITouchPhase.Ended) {
//Stuff
timeoutModel.actionPerformed();
}
}
}
Before swift 2, I was able to have the AppDelegate subclass UIApplication and override sendEvent: like this:
-(void)sendEvent:(UIEvent *)event
{
[super sendEvent:event];
// Only want to reset the timer on a Began touch or an Ended touch, to reduce the number of timer resets.
NSSet *allTouches = [event allTouches];
if ([allTouches count] > 0) {
// allTouches count only ever seems to be 1, so anyObject works here.
UITouchPhase phase = ((UITouch *)[allTouches anyObject]).phase;
if (phase == UITouchPhaseBegan || phase == UITouchPhaseEnded)
[[InactivityModel instance] actionPerformed];
}
}
The code above works for every touch but the swift equivalent only works when a view does not exist above that UIWindow's hierarchy?
Does anyone know a way to detect every touch in the application?
As I have something similar in my application, I just tried to fix it:
override sendEvent in UIWindow - doesn't work
override sendEvent in delegate - doesn't work
So the only way is to provide custom UIApplication subclass. My code so far (works on iOS 9) is:
#objc(MyApplication) class MyApplication: UIApplication {
override func sendEvent(event: UIEvent) {
//
// Ignore .Motion and .RemoteControl event
// simply everything else then .Touches
//
if event.type != .Touches {
super.sendEvent(event)
return
}
//
// .Touches only
//
var restartTimer = true
if let touches = event.allTouches() {
//
// At least one touch in progress?
// Do not restart auto lock timer, just invalidate it
//
for touch in touches.enumerate() {
if touch.element.phase != .Cancelled && touch.element.phase != .Ended {
restartTimer = false
break
}
}
}
if restartTimer {
// Touches ended || cancelled, restart auto lock timer
print("Restart auto lock timer")
} else {
// Touch in progress - !ended, !cancelled, just invalidate it
print("Invalidate auto lock timer")
}
super.sendEvent(event)
}
}
Why there's #objc(MyApplication). That's because Swift mangles names in a different way then Objective-C and it just says - my class name in Objective-C is MyApplication.
To make it working, open your info.plist and add row with Principal class key and MyApplication value (MyApplication is what's inside #objc(...), not your Swift class name). Raw key is NSPrincipalClass.
UIWindow also has a sendEvent method that you can override. That would allow you to track the time since the last screen touch. Swift 4:
class IdlingWindow: UIWindow {
/// Tracks the last time this window was interacted with
var lastInteraction = Date.distantPast
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
lastInteraction = Date()
}
}
If you're using a storyboard, you can load it in didFinishLaunchingWithOptions:
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: IdlingWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
window = IdlingWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UIStoryboard.init(name: "Main", bundle: nil).instantiateInitialViewController()
window?.makeKeyAndVisible()
return true
}
:
}
extension UIApplication {
/// Conveniently gets the last interaction time
var lastInteraction: Date {
return (keyWindow as? IdlingWindow)?.lastInteraction ?? .distantPast
}
}
Now elsewhere in your app, you can check for inactivity like this:
if UIApplication.shared.lastInteraction.timeIntervalSinceNow < -2 {
// the window has been idle over 2 seconds
}
#markiv answer works like a charm, with two issues:
keyWindow
"'keyWindow' was deprecated in iOS 13.0: Should not be used for applications that support multiple scenes as it returns a key window across all connected scenes"
Can be solved like this:
let kw = UIApplication.shared.windows.filter {$0.isKeyWindow}.first
found here
see answer of #matt
AppDelegate - I get this message:
The app delegate must implement the window property if it wants to use a main storyboard file
One can subclass UIWindow - found the answer here. Combine this with the IdlingWindow class.
Message is gone.
var customWindow: IdlingWindow?
var window: UIWindow? {
get {
customWindow = customWindow ?? IdlingWindow(frame: UIScreen.mainScreen().bounds)
return customWindow
}
set { }
}

Resources