Redirect SKNode SkAction direction while touching - ios

While a player is touching, the code below is suppose to move a node to the MaxX. Once they hit that point it is suppose to redirect the node's movement towards the MinX and vice versus.
As it stands right now, I can only move the player in once direction and it is ignoring the movePlayer() logic.
Action Declarations
var moveRightTest:Bool = true;
var moveLeftTest:Bool = false;
let moveRight = SKAction.moveByX(1000, y: 0, duration: 2);
let moveLeft = SKAction.moveByX(-1000, y: 0, duration: 2);
Inside the GameScene:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
movePlayer();
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
stopPlayer();
}
func movePlayer() {
if(moveRightTest == true) {
if(player.position.x <= CGRectGetMaxX(self.frame)) {
player.runAction(moveRight);
} else {
moveRightTest = false;
moveLeftTest = true;
}
}
else if(moveLeftTest == true) {
if(player.position.x >= CGRectGetMinX(self.frame)) {
player.runAction(moveLeft);
} else {
moveLeftTest = false;
moveRightTest = true;
}
}
}
func stopPlayer() {
player.removeAllActions();
}
Note: I subbed out some vars with ints/floats to remove the calculation from the code in order to present. Its not related to the issue. Im also just learning Swift.

Now your function movePlayer is performed only once when you're calling it, so the player comes to the right edge and stops. You need to repeat it after each run the following steps. To do this, you can use runAction with completion like this:
isRight: Bool = true
let moveRight = SKAction.moveToX(CGRectGetMaxX(self.frame), duration: 2)
let moveLeft = SKAction.moveToX(CGRectGetMinX(self.frame), duration: 2)
func movePlayer() {
if isRight {
player.runAction(moveRight, completion: { () -> Void in
isRight = false
self.movePlayer()
})
} else {
player.runAction(moveLeft, completion: { () -> Void in
isRight = true
self.movePlayer()
})
}
}

Related

How stop SKAction background music

I'm trying to make a game and in the main menu, there is a label to start music, and another to stop music.
let backgroundMusic = SKAction.playSoundFileNamed("background.mp3", waitForCompletion: false)
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
for touch in touches {
let location = touch.location(in: self)
let touchedNode = atPoint(location)
if touchedNode.name == self.BEGIN_MUSIC {
// start music
self.run(backgroundMusic)
}
if touchedNode.name == self.STOP_MUSIC {
// stop music
SKAction.stop()
}
}
}
the music starts fine and works on all scenes which is what i wanted. But when I press the stop music, the music keeps playing. How do I stop the music.
I've tried SKAction.pause(), i've tried using withKey: "music" and then self.removeAction(forKey: "music").
I also tried making backgroundMusic: SKAudioNode and this worked but once i stopped the music i couldn't turn it back on, also it doesn't work on any other scene just the main menu.
How do I stop the sound?
I've had the same issue a while ago: This is how I got it working. This is specific for background music.
var backgroundMusic: AVAudioPlayer!
func playMusic() {
if let musicURL = Bundle.main.url(forResource: "musicSource", withExtension: "wav") {
if let audioPlayer = try? AVAudioPlayer(contentsOf: musicURL) {
backgroundMusic = audioPlayer
backgroundMusic.numberOfLoops = -1
backgroundMusic.play()
backgroundMusic.volume = 0.2
}
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
if let touch = touches.first {
let pos = touch.location(in: self)
let node = self.atPoint(pos)
let musicOff = UserDefaults.standard.bool(forKey: "musicOff")
if node == musicButton && musicOff == false {
backgroundMusic.stop()
UserDefaults.standard.set(true, forKey: "musicOff")
}
if node == musicButton && musicOff == true {
backgroundMusic.play()
backgroundMusic.volume = 0.3
UserDefaults.standard.set(false, forKey: "musicOff")
}
}
}

Shake Gesture with swift 3

I am trying to hit a ball on the left and right side of a box with a speed depends upon the shake gesture speed/acceleration.
I am unable to get this i have tried many thing but i cannot get the speed/acceleration of gesture. I detect the gesture with core motion as well as with motionBegun and motionEnded method but unable to detect the speed so that i could create a logic of hitting the ball on both side of a box.
Here is my code that how i detect the direction of shake with CoreMotion and shake gesture detection with motionBegun and motionEnded
var startedLeftTilt = false
var startedRightTilt = false
var dateLastShake = NSDate(timeIntervalSinceNow: -2)
var dateStartedTilt = NSDate(timeIntervalSinceNow: -2)
var motionManager = CoreMotion.CMMotionManager()
let tresholdFirstMove = 3.0
let tresholdBackMove = 0.5
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
motionManager.startGyroUpdates(to: OperationQueue.current!, withHandler: { (gyroData, error) -> Void in
self.handleGyroData(rotation: (gyroData?.rotationRate)!)
})
}
private func handleGyroData(rotation: CMRotationRate) {
if fabs(rotation.z) > tresholdFirstMove && fabs(dateLastShake.timeIntervalSinceNow) > 0.3
{
if !startedRightTilt && !startedLeftTilt
{
dateStartedTilt = NSDate()
if (rotation.z > 0)
{
startedLeftTilt = true
startedRightTilt = false
}
else
{
startedRightTilt = true
startedLeftTilt = false
}
}
}
if fabs(dateStartedTilt.timeIntervalSinceNow) >= 0.3
{
startedRightTilt = false
startedLeftTilt = false
}
else
{
if (fabs(rotation.z) > tresholdBackMove)
{
if startedLeftTilt && rotation.z < 0
{
dateLastShake = NSDate()
startedRightTilt = false
startedLeftTilt = false
}
else if startedRightTilt && rotation.z > 0
{
dateLastShake = NSDate()
startedRightTilt = false
startedLeftTilt = false
}
}
}
}
override func becomeFirstResponder() -> Bool {
return true
}
override func motionBegan(_ motion: UIEventSubtype, with event: UIEvent?) {
if motion == .motionShake {
NSLog("Motion is Started");
}
}
override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
if motion == .motionShake {
NSLog("Motion is Ended");
}
}
override func viewDidLoad() {
super.viewDidLoad()
motionManager.gyroUpdateInterval = 0.01
// Do any additional setup after loading the view.
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
Core motion has lots of options. You can use high level events like shake events, or you can get gyro or acellerometer updates.
The code you posted is monitoring gyro events. If you're looking for the force of a shake you want acellerometer events, not gyro events.
Use gyro and motion at the same time to get your trigger. Gyro will provide you the direction and motion will provide you some kind of speed analysis.
Implementing both at the same time with your custom logic will you what exactly you are trying to get.
Use the last answer for finding the direction.
StackAnswer

NSTimer Too Slow

I have seen multiple questions similar to this; however, none of them resolved my problem. My timer is simply going too slowly. I am only using an interval of 0.01 seconds. Here is my code:
#IBOutlet var timerLabel: UILabel!
var miliseconds = 0
var seconds = 0
func updateLabel() {
if miliseconds == 0 {
timerLabel.text = "\(seconds).00"
} else if miliseconds < 10 {
timerLabel.text = "\(seconds).0\(miliseconds)"
} else {
timerLabel.text = "\(seconds).\(miliseconds)"
}
}
var timer = NSTimer()
func updateTime() {
miliseconds++
if miliseconds == 100 {
miliseconds = 0
seconds++
}
updateLabel()
}
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if timerState == 1 {
timer = NSTimer.scheduledTimerWithTimeInterval(0.01, target: self, selector: "updateTime", userInfo: nil, repeats: true)
NSRunLoop.mainRunLoop().addTimer(timer, forMode: NSRunLoopCommonModes)
timerLabel.textColor = UIColor.blackColor()
timerState = 2
}
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
if timerState == 0 {
miliseconds = 0
seconds = 0
updateLabel()
timerLabel.textColor = UIColor.greenColor()
timerState = 1
} else if timerState == 2 {
timerState = 0
timer.invalidate()
}
}
var timerState = 0
//timerState of 0 = Has not started
//timerState of 1 = About to start
//timerState of 2 = Timing
I have also tried using delays:
func delay(delay:Double, closure:()->()) {
dispatch_after(
dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)
}
I called updateTime in viewDidLoad, and at the end of updateTime, I added:
delay(0.01) { () -> () in
self.updateTime()
}
However, it still went at the same speed as before.
How can I fix this issue? If I missed a question while researching, please let me know. Thanks!
There is a whole mess of problems here. Let's clean this up.
class ViewController: UIViewController {
#IBOutlet var timerLabel: UILabel!
First of all, don't use a timer to update the label. Use a CADisplayLink. A display link synchronizes with the screen refresh interval, which is 1/60 of a second on most iOS devices (not 1/100), so you don't do extra work:
private var displayLink: CADisplayLink?
Next, don't try to track the elapsed time by incrementing a counter when the timer (or link) fires, because the timer or link is not guaranteed to fire as often as you requested. Instead, store the time at which the timer was started, and the time at which it was stopped (if it was stopped):
private var startTime: CFAbsoluteTime = 0
private var endTime: CFAbsoluteTime = 0 {
didSet {
updateLabel()
}
}
Track the state using an enum instead of mysterious hard-coded numbers. And since the label color depends only the state, add a property to the state that gives the label color for that state:
private enum State {
case Stopped
case Pending
case Running
var labelColor: UIColor {
switch self {
case .Pending: return UIColor.greenColor()
case .Stopped, .Running: return UIColor.blackColor()
}
}
}
The elapsed time depends on the state, so add a method to compute it:
private var elapsedTime: NSTimeInterval {
switch state {
case .Stopped: return endTime - startTime
case .Running: return CFAbsoluteTimeGetCurrent() - startTime
case .Pending: return 0
}
}
Use a format string to convert the elapsed time to a string when updating the label:
private func updateLabel() {
timerLabel.text = String(format: "%.02f", elapsedTime)
}
Changing the timer state can change both the label color and the elapsed time, so update the label color and the label text when the state changes:
private var state = State.Stopped {
didSet {
timerLabel.textColor = state.labelColor
updateLabel()
}
}
When a touch begins, create the display link if needed, then update the state. The state's didSet will handle updating the label as necessary:
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
createDisplayLinkIfNeeded()
switch state {
case .Stopped:
state = .Pending
case .Pending:
break
case .Running:
state = .Stopped
endTime = CFAbsoluteTimeGetCurrent()
displayLink?.paused = true
}
}
When a touch ends, start the timer if necessary:
override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
if state == .Pending {
startTime = CFAbsoluteTimeGetCurrent()
displayLink?.paused = false
state = .Running
}
}
Here's how you create the display link:
private func createDisplayLinkIfNeeded() {
guard self.displayLink == nil else { return }
let displayLink = CADisplayLink(target: self, selector: "displayLinkDidFire:")
displayLink.paused = true
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
self.displayLink = displayLink
}
And here's the method the display link will call:
func displayLinkDidFire(_: CADisplayLink) {
updateLabel()
}
} // end of ViewController
From my comment above ... (with encouragement)
NSTimer - from apple developer docs:
"A timer is not a real-time mechanism; it fires only when one of the
run loop modes to which the timer has been added is running and able
to check if the timer’s firing time has passed. Because of the various
input sources a typical run loop manages, the effective resolution of
the time interval for a timer is limited to on the order of 50-100
milliseconds. "

How to accelerate the identification of a single tap over a double tap?

I have a UITableView with row where I added single tap and double tap gestures:
let doubleTap = UITapGestureRecognizer(target: self, action: "doubleTap:")
doubleTap.numberOfTapsRequired = 2
doubleTap.numberOfTouchesRequired = 1
let singleTap = UITapGestureRecognizer(target: self, action: "singleTap:")
singleTap.numberOfTapsRequired = 1
singleTap.numberOfTouchesRequired = 1
singleTap.requireGestureRecognizerToFail(doubleTap)
tableView.addGestureRecognizer(doubleTap)
tableView.addGestureRecognizer(singleTap)
Is there a way to reduce the time between when the first tap is made and when the gesture recognizer realize that it is a single tap and not a double tap?
I'm asking this because when I do a single tap, the new viewController appear quite late, giving a feeling that the app lags.
I found the answer on this link
The swift version:
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
let tapMaxDelay: Double = 0.3
override func touchesBegan(touches: NSSet!, withEvent event: UIEvent!) {
super.touchesBegan(touches, withEvent: event)
delay(tapMaxDelay) {
// Enough time has passed and the gesture was not recognized -> It has failed.
if self.state != UIGestureRecognizerState.Ended {
self.state = UIGestureRecognizerState.Failed
}
}
}
}
With delay(delay: Double, closure:()->()):
class func delay(delay:Double, closure:()->()) {
dispatch_after(dispatch_time( DISPATCH_TIME_NOW, Int64(delay * Double(NSEC_PER_SEC))), dispatch_get_main_queue(), closure)
}
Full Implementation of Markus's Swift 3 version of eladleb's original solution.
Create subclass file UIShortTapGestureRecogninzer
import UIKit
import UIKit.UIGestureRecognizerSubclass
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
let tapMaxDelay: Double = 0.3 //anything below 0.3 may cause doubleTap to be inaccessible by many users
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
DispatchQueue.main.asyncAfter(deadline: .now() + tapMaxDelay) { [weak self] in
if self?.state != UIGestureRecognizerState.recognized {
self?.state = UIGestureRecognizerState.failed
}
}
}
}
Note: When adding UIGestureRecognizer only doubleTap needs to be of type UIShortTapGestureRecognizer & singleTap.require(toFail: doubleTap) is required.
func addBoth (views: UIView, selectorSingle: Selector, selectorDouble: Selector) {
let doubleTap:UIShortTapGestureRecognizer = UIShortTapGestureRecognizer(target: self, action: selectorDouble)
doubleTap.numberOfTapsRequired = 2
views.addGestureRecognizer(doubleTap)
let singleTap:UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: selectorSingle)
singleTap.numberOfTapsRequired = 1
singleTap.require(toFail: doubleTap)
views.addGestureRecognizer(singleTap)
}
Swift 5 implementation of Nico's accepted answer.
class UIShortTapGestureRecognizer: UITapGestureRecognizer {
var maximumTapLength: Double = 0.3
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
delay(delay: maximumTapLength) {
// Enough time has passed and the gesture was not recognized -> It has failed.
if self.state != .ended {
self.state = .failed
}
}
}
func delay(delay:Double, closure:#escaping ()->()) {
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: closure)
}
}
for future, full implementation by Howard Yang, here's links:
https://github.com/oney/SingleDoubleTapGestureRecognizer
let tap = SingleDoubleTapGestureRecognizer(target: self, singleAction: Selector("singleTap"), doubleAction: Selector("doubleTap"))
tap.duration = 0.8
view.addGestureRecognizer(tap)
https://github.com/oney/SingleDoubleTapGestureRecognizer/blob/master/Pod/Classes/SingleDoubleTapGestureRecognizer.swift
//
// SingleDoubleTapGestureRecognizer.swift
// SingleDoubleTapGestureRecognizer
//
// Created by Howard Yang on 08/22/2015.
// Copyright (c) 2015 Howard Yang. All rights reserved.
//
import UIKit
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var duration: CFTimeInterval = 0.3 {
didSet {
self.targetDelegate.duration = duration
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: Selector("fakeAction:"))
numberOfTapsRequired = 1
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
var target: AnyObject
var singleAction: Selector
var doubleAction: Selector
var duration: CFTimeInterval = 0.3
var tapCount = 0
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
func fakeAction(g: UITapGestureRecognizer) {
tapCount = tapCount + 1
if tapCount == 1 {
delayHelper(duration, task: {
if self.tapCount == 1 {
NSThread.detachNewThreadSelector(self.singleAction, toTarget:self.target, withObject: g)
}
else if self.tapCount == 2 {
NSThread.detachNewThreadSelector(self.doubleAction, toTarget:self.target, withObject: g)
}
self.tapCount = 0
})
}
}
typealias DelayTask = (cancel : Bool) -> ()
func delayHelper(time:NSTimeInterval, task:()->()) -> DelayTask? {
func dispatch_later(block:()->()) {
dispatch_after(
dispatch_time(
DISPATCH_TIME_NOW,
Int64(time * Double(NSEC_PER_SEC))),
dispatch_get_main_queue(),
block)
}
var closure: dispatch_block_t? = task
var result: DelayTask?
let delayedClosure: DelayTask = {
cancel in
if let internalClosure = closure {
if (cancel == false) {
dispatch_async(dispatch_get_main_queue(), internalClosure);
}
}
closure = nil
result = nil
}
result = delayedClosure
dispatch_later {
if let delayedClosure = result {
delayedClosure(cancel: false)
}
}
return result;
}
func cancel(task:DelayTask?) {
task?(cancel: true)
}
}
I'd rather recommend to use canBePrevented(by:) function, it takes in account amount of taps to perform and won't run you double-tap gesture recognizer unless first one is recognized/failed.
canBePrevented(by:)
Inspired by Howard Yang's implementation, Swift 5.1 using DispatchWorkItem:
public class SingleDoubleTapGestureRecognizer: UITapGestureRecognizer {
var targetDelegate: SingleDoubleTapGestureRecognizerDelegate
public var timeout: TimeInterval = 0.3 {
didSet {
self.targetDelegate.timeout = timeout
}
}
public init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
targetDelegate = SingleDoubleTapGestureRecognizerDelegate(target: target, singleAction: singleAction, doubleAction: doubleAction)
super.init(target: targetDelegate, action: #selector(targetDelegate.recognizerAction(recognizer:)))
}
}
class SingleDoubleTapGestureRecognizerDelegate: NSObject {
weak var target: AnyObject?
var singleAction: Selector
var doubleAction: Selector
var timeout: TimeInterval = 0.3
var tapCount = 0
var workItem: DispatchWorkItem?
init(target: AnyObject, singleAction: Selector, doubleAction: Selector) {
self.target = target
self.singleAction = singleAction
self.doubleAction = doubleAction
}
#objc func recognizerAction(recognizer: UITapGestureRecognizer) {
tapCount += 1
if tapCount == 1 {
workItem = DispatchWorkItem { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.singleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
DispatchQueue.main.asyncAfter(
deadline: .now() + timeout,
execute: workItem!
)
} else {
workItem?.cancel()
DispatchQueue.main.async { [weak self] in
guard let weakSelf = self else { return }
weakSelf.target?.performSelector(onMainThread: weakSelf.doubleAction, with: recognizer, waitUntilDone: false)
weakSelf.tapCount = 0
}
}
}
}
Usage:
let singleDoubleTapRecognizer = SingleDoubleTapGestureRecognizer(
target: self,
singleAction: #selector(handleTapGesture),
doubleAction: #selector(handleDoubleTapGesture)
)

Gamescene does not have a member named "waitEvent" error

I am currently learning Apple's new software programming language (Swift) and I have encountered this error:
Gamescene does not have a member named "waitEvent"
This is becoming quite frustrating, if anyone knows why this might be please respond.
This is in Swift and Sprite Kit!
Here is the code:
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
/* Called when a touch begins */
for touch: AnyObject in touches {
let location = touch.locationInNode(self)
if (destroyerNode? != nil){
destroyerNode!.position = location
destroyerNode!.physicsBody?.velocity = CGVectorMake(0, 0)
destroyerNode!.physicsBody?.dynamic = true
let waitAction = SKAction.waitForDuration(2)
destroyerNode!.runAction(waitAction, completion: {
self.waitEvent()
//there is an issue with someEvent
}
)
}
else{
println("that destroyer was really not there")
}
}
func waitEvent(){
//make the destroyer a dynamic object
destroyerNode!.makeBodyDynamic()
println("The wait is up")
}
func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
}
}

Resources