I basically running a timer in the didMove function of the GameScene class in SpriteKit. The function basically just makes the player shoot bullets at 0.5-second intervals. However, the problem I am getting is that once the player dies, and I use removeFromParent() on the player, the bullets are still firing, as if they are just appearing in empty space.
Here is my code:
var timer1 = Timer()
self.timer1 = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true, block: { _ in self.bullet1.fireBullet1(player: self.player1, scene: self)})
And here is the fireBullet1 function:
func fireBullet2(player: Player2, scene: SKScene) {
let bulletNode = Bullet()
bulletNode.texture = self.textureAtlas.textureNamed("bullet2")
bulletNode.position = player.position
bulletNode.position.y += -sin(player.zRotation)*10
bulletNode.position.x += -cos(player.zRotation)*10
bulletNode.physicsBody = SKPhysicsBody(circleOfRadius: self.size.width/500)
bulletNode.physicsBody?.mass = 0.005
bulletNode.physicsBody?.affectedByGravity = false
bulletNode.zRotation = player.zRotation
bulletNode.physicsBody?.categoryBitMask = PhysicsCategory.bullet2.rawValue
bulletNode.physicsBody?.contactTestBitMask =
PhysicsCategory.boundary.rawValue |
PhysicsCategory.bullet1.rawValue |
PhysicsCategory.player1.rawValue
scene.addChild(bulletNode)
bulletNode.physicsBody?.applyImpulse(CGVector(dx: -cos(player.zRotation)*1, dy: -sin(player.zRotation)*1))
}
Can someone please tell me what I need to do in order to make the timer stop calling the function after the player dies.
Related
I would like something that mimics the behaviour in iOS of the ValueAnimator in Android. Under normal circumstances I would use a UIView animation, but unfortunately the objects value im trying to interpolate over time isnt working with a normal animation.
In my particular case im using a Lottie animation, and trying to change the progress of the animation via a UIView animation, but instead of the animation changing the value of the progress over time, it just jumps to the final value. eg:
let lottieAnim = ...
lottieAnim.animationProgress = 0
UIView.animate(withDuration: 2) {
lottieAnim.animationProgress = 1
}
This example does not animate the lottie animation over time, but simply jumps to the end. I know lottie has methods to play the animation, but Im trying to use a custom animation curve to set the progress (its a progress animation that has no finite duration) which is why I need to use a UIView animation to interpolate the value.
I need something that updates intermittently to mimic an animation, the first thing that comes to mind is Android ValueAnimator class.
I put together a class that will mimic a ValueAnimator from android somewhat, and will allow you to specify your own animation curve if need be (default is just linear)
Simple usage
let valueAnimator = ValueAnimator(duration: 2) { value in
animationView.animationProgress = value
}
valueAnimator.start()
Advanced usage
let valueAnimator = ValueAnimator(from: 0, to: 100, duration: 60, animationCurveFunction: { time, duration in
return atan(time)*2/Double.pi
}, valueUpdater: { value in
animationView.animationProgress = value
})
valueAnimator.start()
You can cancel it at any point as well using:
valueAnimator.cancel()
Value Animator class in Swift 5
// swiftlint:disable:next private_over_fileprivate
fileprivate var defaultFunction: (TimeInterval, TimeInterval) -> (Double) = { time, duration in
return time / duration
}
class ValueAnimator {
let from: Double
let to: Double
var duration: TimeInterval = 0
var startTime: Date!
var displayLink: CADisplayLink?
var animationCurveFunction: (TimeInterval, TimeInterval) -> (Double)
var valueUpdater: (Double) -> Void
init (from: Double = 0, to: Double = 1, duration: TimeInterval, animationCurveFunction: #escaping (TimeInterval, TimeInterval) -> (Double) = defaultFunction, valueUpdater: #escaping (Double) -> Void) {
self.from = from
self.to = to
self.duration = duration
self.animationCurveFunction = animationCurveFunction
self.valueUpdater = valueUpdater
}
func start() {
displayLink = CADisplayLink(target: self, selector: #selector(update))
displayLink?.add(to: .current, forMode: .default)
}
#objc
private func update() {
if startTime == nil {
startTime = Date()
valueUpdater(from + (to - from) * animationCurveFunction(0, duration))
return
}
var timeElapsed = Date().timeIntervalSince(startTime)
var stop = false
if timeElapsed > duration {
timeElapsed = duration
stop = true
}
valueUpdater(from + (to - from) * animationCurveFunction(timeElapsed, duration))
if stop {
cancel()
}
}
func cancel() {
self.displayLink?.remove(from: .current, forMode: .default)
self.displayLink = nil
}
}
You could probably improve this to be more generic but this serves my purpose.
I try to implement simple player with UISlider to indicate at what time is current audio file.
In code I have added two observers:
slider.rx.value.subscribe(onNext: { value in
let totalTime = Float(CMTimeGetSeconds(self.player.currentItem!.duration))
let seconds = value * totalTime
let time = CMTime(seconds: Double(seconds), preferredTimescale: CMTimeScale(NSEC_PER_SEC))
self.player.seek(to: time)
}).disposed(by: bag)
let interval = CMTime(seconds: 0.1, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
self?.updateSlider(with: time)
}
with one private function:
private func updateSlider(with time: CMTime) {
let currentTime = CMTimeGetSeconds(time)
var totalTime = CMTimeGetSeconds(player.currentItem!.duration)
if totalTime.isNaN {
totalTime = 0
}
startLabel.text = Int(currentTime).descriptiveDuration
endLabel.text = Int(totalTime).descriptiveDuration
slider.value = Float(currentTime / totalTime)
}
When audio plays, everything is fine and slider is pretty much updated. The problem occurs when I try to move slider manually while audio is playing, then it jumps. Why?
UPDATE:
I know why actually. Because I update it twice: manually and from player observer, but how to prevent from this behaviour? I have no idea;) please, help.
One simple way to go about this would be to prevent addPeriodicTimeObserver from calling self?.updateSlider(with: time) when the slider is being touched.
This can be determined via the UISliders isTracking property:
isTracking
A Boolean value indicating whether the control is currently tracking
touch events.
While tracking of a touch event is in progress, the control sets the
value of this property to true. When tracking ends or is cancelled for
any reason, it sets this property to false.
Ref: https://developer.apple.com/documentation/uikit/uicontrol/1618210-istracking
This is present in all UIControl elements which you can use in this way:
player.addPeriodicTimeObserver(forInterval: interval, queue: nil) { [weak self] time in
//check if slider is being touched/tracked
guard self?.slider.isTracking == false else { return }
//if slider is not being touched, then update the slider from here
self?.updateSlider(with: time)
}
Generic Example:
#IBOutlet var slider: UISlider!
//...
func startSlider() {
slider.value = 0
slider.maximumValue = 10
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] (timer) in
print("Slider at: \(self?.slider.value)")
guard self?.slider.isTracking == false else { return }
self?.updateSlider(to: self!.slider.value + 0.1)
}
}
private func updateSlider(to value: Float) {
slider.value = value
}
I'm sure there are other (better) ways out there but I haven't done much in RxSwift (yet).
I hope this is good enough for now.
I'm trying to make a countdown timer for iOS and when I try to make it go down by 0.10 and when I run the program it after like 8.2, 8.1, it starts printing like this 7.900000000, 7.800000012 something like this.
And also it's supposed to stop at 0 but
But it works perfectly when making it go down by 0.25, or .5 or 1.
Here is my code.
var seconds = 10.0
var timer = Timer()
#IBAction func startPressed(_ sender: Any) {
startBtn.isHidden = true
clickedTxt.isHidden = false
numClicked.isHidden = false
tapView.isEnabled = true
timerTxt.isHidden = false
secs.isHidden = false
timer = Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(ViewController.counter), userInfo: nil, repeats: true)
}
func counter()
{
if(seconds > 0.0)
{
seconds -= 0.1
secs.text = "\(seconds)"
} else {
timer.invalidate()
}
}
You're seeing an effect of the way floating point numbers are formatted, essentially. And you can read more about that in the other question rmaddy pointed you to.
I suspect you mostly want to format that label nicely. In that case you can do something like:
secs.text = String(format:"%.1f", secs)
where you replace ".1" with however many places after the decimal you want.
I'm developing a Sprite Kit Game, which I have a node named hero who dodges approaching villains. I have an issue with applyImpulse for a jump action and, in this case, the hero can jump repeatedly and fly instead of dodging the villains. I'm used a boolean variable to change the status with a timer value to jump only once in 3 seconds but that is not working. Here is my code
override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
if isGameOver == true {
self.restart()
}
if isstarted {
self.runAction(SKAction.playSoundFileNamed("jump.wav", waitForCompletion: false))
for touch: AnyObject in touches{
if jumpon == false { //hero has not jumped
let location = touch.locationInNode(self)
heroAction()
jumpon = true //hero jumped
}
}
} else {
isstarted = true
hero.stop()
hero.armMove()
hero.rightLegMove()
hero.leftLegMove()
treeMove()
villanMove()
let clickToStartLable = childNodeWithName("clickTostartLable")
clickToStartLable?.removeFromParent()
addGrass()
// star.generateStarWithSpawnTime()
}
}
func changeJump(){
jumpon = false // hero has landed
}
and function update is called in every second and then changeJump must be called
override func update(currentTime: CFTimeInterval) {
if isstarted == true {
let pointsLabel = childNodeWithName("pointsLabel") as MLpoints
pointsLabel.increment()
}
if jumpon == true { // checked everyframe that hero jumped
jumpingTimer = NSTimer.scheduledTimerWithTimeInterval(3, target: self, selector: "changeJump", userInfo: nil, repeats: true) // changing jump status
}
}
How should I update my code to make the hero jump only once in three seconds. Thanks in advance.
Some thoughts...
I suggest you use an SKAction or two instead of an NSTimer in Sprite Kit games. SKActions pause/resume appropriately when you pause/resume the scene and/or view
The update method is called ~60 times a second not every second
Checking the status of a jump in update is not need
Alternatively, you can check if the hero is on the ground before starting another jump sequence instead of using a timer. The game may frustrate the user if the hero is on the ground (and ready to jump) but the timer is still counting down
and some code...
if (!jumping) {
jumping = true
let wait = SKAction.waitForDuration(3)
let leap = SKAction.runBlock({
// Play sound and apply impulse to hero
self.jump()
})
let action = SKAction.group([leap, wait])
runAction(action) {
// This runs only after the action has completed
self.jumping = false
}
}
I have been pulling my hair out over this seemingly simple feature I'm trying to build in to a game I am working on in SpriteKit and Swift. I have searched and can't figure why this isn't working.
What I want to do:
When the player comes in contact with an enemy or an obstacle, I want to load and play through an explosion animation createExplosion() (The animation completes in 0.5 seconds) and then progress to the gameOverAction(). The problem is that only the first frame of the animation is shown and then it progresses to the gameOverAction().
I tried to make an SKAction.sequence but Xcode did not like me calling functions inside the sequence, so I found SKAction.waitForDuration. Problem is, it is having no effect on the execution of the code.
How should this be done properly?
func collisionWithPlayer(playerObject: SKSpriteNode) {
runAction(SKAction.playSoundFileNamed("spaceExplosion.mp3", waitForCompletion: false))
createExplosion(playerObject)
playerObject.removeFromParent()
runAction(SKAction.waitForDuration(NSTimeInterval(0.5)))
gameOverAction()
}
And here is the code for the gameOverAction():
func gameOverAction() {
backgroundMusicPlayer.stop()
globalHighScore = NSUserDefaults.standardUserDefaults().objectForKey("GameHighScore") as? Int
localHighScore = self.monstersDestroyed
if globalHighScore == nil {
globalHighScore = localHighScore
}
globalHighScore = self.recordHighScore(localHighScore, highScore: globalHighScore!)
println(globalHighScore!)
let reveal = SKTransition.fadeWithDuration(1.0)
let gameOverScene = GameOverScene(size: self.size, won: false)
self.view?.presentScene(gameOverScene, transition: reveal)
}
func createExplosion(object: SKSpriteNode) -> SKSpriteNode {
var explosionArray = Array<SKTexture>()
var explosionSprite = SKSpriteNode()
explosionArray.append(SKTexture(imageNamed: "explosionSprite01.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite02.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite03.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite04.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite05.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite06.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite07.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite08.png"))
explosionArray.append(SKTexture(imageNamed: "explosionSprite09.png"))
let animateAction = SKAction.animateWithTextures(explosionArray, timePerFrame: 0.05)
explosionSprite = SKSpriteNode(texture:explosionArray[0])
explosionSprite.position = CGPoint(x: object.position.x + 10.0, y: object.position.y)
addChild(explosionSprite)
explosionSprite.runAction(SKAction.sequence([animateAction, SKAction.removeFromParent()]))
return(explosionSprite)
}
The waitForDuration SKAction can be used to introduce a delay into an action sequence, but it doesn't block the thread calling runAction - so your waitForDuration action will be set to run on your node, but then gameOverAction() will be called immediately.
You can use the completion block of runAction:completion to perform gameOverAction() after the actions are complete -
func collisionWithPlayer(playerObject: SKSpriteNode) {
var actions = Array<SKAction>();
actions.append(SKAction.playSoundFileNamed("spaceExplosion.mp3", waitForCompletion: false))
actions.append(SKAction.waitForDuration(NSTimeInterval(0.5)))
actions.append(SKAction.removeFromParent())
createExplosion(playerObject)
let sequence = SKAction.sequence(actions);
playerObject.runAction(sequence,completion: { () -> Void in
self.gameOverAction()
})
}
Depending on what createExplosion does it may be possible to incorporate it into the sequence too and remove the need for the waitForDuration