Swift 3 Bullet Firing Delay - swift

In my game, you tap anywhere on the screen and a bullet goes in that direction. The only problem is that you can shoot as fast as you can tap. Is there any way to add a delay after each shot. So I would like to shoot, wait 1 second then shoot. Here is my code in touchesEnded:
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
//Set up initial location of bullet and properties
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.name = "Bullet"
bullet.position = player.position
bullet.setScale(0.75)
bullet.zPosition = 1
bullet.physicsBody = SKPhysicsBody(circleOfRadius: bullet.size.width/2)
bullet.physicsBody?.isDynamic = true
bullet.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
bullet.physicsBody?.collisionBitMask = PhysicsCategory.None
bullet.physicsBody?.usesPreciseCollisionDetection = true
//Determine offset of location to bullet
let offset = touchLocation - bullet.position
//Stops Bullet from shooting backwards
if (offset.y < 0) { return }
addChild(bullet)
//Get the direction of where to shoot
let direction = offset.normalized()
//Make it shoot far enough to be guaranteed off screen
let shootAmount = direction * 1000
//Add the shoot amount to the current position
let realDest = shootAmount + bullet.position
//Create the actions
if currentGameState == gameState.inGame {
let actionMove = SKAction.move(to: realDest, duration: 1.0)
let actionMoveDone = SKAction.removeFromParent()
bullet.run(SKAction.sequence([actionMove, actionMoveDone]))
}
}
Thanks for any help.

This is a more simple approach, based on the use of Date:
var time = Date()
func shoot(after timeInterval: Double) {
guard Date() - timeInterval > time else {
print("WAIT")
return
}
print("SHOOT")
time = Date() // reset the timer
}
// CALL THIS INSIDE touchesEnded
shoot(after: 1)
Just modify for your needs :]

You could take a look at the Throttle implementation of RxSwift for one possible solution. Throttle is used to limit the number of events created in a defined time interval:
let timeIntervalSinceLast: RxTimeInterval
if let lastSendingTime = _lastSentTime {
timeIntervalSinceLast = now.timeIntervalSince(lastSendingTime)
}
else {
timeIntervalSinceLast = _parent._dueTime
}
let couldSendNow = timeIntervalSinceLast >= _parent._dueTime
if couldSendNow {
self.sendNow(element: element)
return
}

You can do this using action keys. An action key is a string that makes an action identifiable.
How to use it in this case?
As I said already in comments, you will fire a bullet, then run an action with a key, on a specific node, which will last one second. A presence of this key/action means that weapon is locked. So every time you try to fire a bullet, you check if this key is present on a specific node. When action finishes, the key will be automatically removed as well. Here is the code:
import SpriteKit
let kLockWeaponActionKey = "kLockWeaponActionKey"
class GameScene: SKScene {
func shoot(atPoint targetLocation:CGPoint){
// 1 check if weapon is unlocked, or return
guard self.action(forKey: kLockWeaponActionKey) == nil else {
print("Weapon locked")
return
}
let bullet = SKSpriteNode(color: .purple, size: CGSize(width: 20, height: 20))
addChild(bullet)
let shoot = SKAction.move(to: targetLocation, duration: 3)
//2 shoot
bullet.run(shoot)
//3 lock weapon
self.run(SKAction.wait(forDuration: 1), withKey: kLockWeaponActionKey)
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let targetLocation = touch.location(in: self)
self.shoot(atPoint:targetLocation)
}
}
}
If you try to spam bullets fast, you will see a log in the console which says : "weapon locked".

Related

This animate I am using works, but is it correct?

So I am a new to Swift and taking lessons on YouTube and am piecing things together. For instance to move an SKSpriteNode back to its original position I have:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.location(in: self)
let touchedNodes = self.nodes(at: location)
for node in touchedNodes.reversed() {
if node.name == "draggable" {
originalNode = touch.location(in: self)
self.currentNode = node
guard let scene = node.scene else { fatalError("Wait this node is not inside a scene!?") }
thePosition = node.convert(node.position, to: scene)
} } } }
func addMovement (obstacle:SKSpriteNode, origNP: CGPoint) {
// var actionArray = [SKAction]()
let moveAction = SKAction.move(to: CGPoint(x: origNP.x, y: origNP.y), duration: 1.0)
let sequence = SKAction.sequence([moveAction])
obstacle.run(sequence)
}
The original code I found online had just one parameter (obstacle) and called this line:
let moveAction = SKAction.move(to: CGPoint(x: obstacle.position.x, y: obstacle.position.x.y), duration: 1.0)
I would just to know that the way I am doing it, is correct. Not a scenario where I am getting lucky and it will come back to bite me later on.
Depends what you are trying to achieve. The move action by itself seems ok.
You do NOT need to use a sequence for a single action though. A sequence is intended to be used for chaining multiple actions one after the other. Since you have just one action, you can update your code to:
let moveAction = SKAction.move(to: CGPoint(x: origNP.x, y: origNP.y), duration: 1.0)
obstacle.run(moveAction)

Multiple Contacts Registering

Any help would be greatly appreciated. I am relatively new to this and just feel like I beginning to understand coding.
My issue -----
I am having difficult trying to fix a problem with a SpriteKit tutorial that I have been enhancing as a way to hone and improve my skills as a newbie.
I am experiencing multiple contacts when my player “crashes” on the ground.
More than one life is removed, which causes my game to generate a 'Attemped to add a SKNode which already has a parent: ’ error adding the player back to the scene.
I have tried player.removeFromParent everywhere I can think of inside my code.
The game works flawlessly as long as I “crash” into “enemies” in the air. Once the player contacts the ground, it’s all over. If I “kill” the player upon contact with the ground, there is no issue, but I want the game to continue as long as the player still has lives whether the contact the ground or and enemy.
I really believe the issue would be fixed if the multiple contact problem could be resolved.
class GameScene: SKScene, SKPhysicsContactDelegate {
// Set up the Texure Atlases
var images = SKSpriteNode()
var textureAtlas = SKTextureAtlas()
var textureArray = [SKTexture]()
var touchingScreen = false
var obstacle = SKSpriteNode()
// Generates a Random number between -350 and 350 (the center of the axis being 0)
let rand = GKRandomDistribution(lowestValue: -350, highestValue: 350)
var timer: Timer?
// This method is called when the Game Launches
override func didMove(to view: SKView) {
// Adds a pixel perfect physicsBody to the player
player.physicsBody = SKPhysicsBody(texture: player.texture!, size: player.texture!.size())
player.physicsBody?.categoryBitMask = 1
player.position = CGPoint(x: -400, y: 275)
// Disables the affects of a collisions (+pushing, rotation etc.) on the player when a collision with another SKSpriteNode occurs
player.physicsBody?.allowsRotation = false
player.physicsBody?.collisionBitMask = 0
addChild(player)
physicsWorld.gravity = CGVector(dx: 0, dy: -2)
physicsWorld.contactDelegate = self
timer = Timer.scheduledTimer(timeInterval: 1.5, target: self, selector: #selector(createEnemy), userInfo: nil, repeats: true)
}
// This method is called when the User touches the Screen
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
touchingScreen = true
}
// This method is called when the User stops touching the Screen
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
touchingScreen = false
}
// This method is called before each frame is rendered
override func update(_ currentTime: TimeInterval) {
// Constrains the player to the scene area
if player.position.y > 275 {
player.position.y = 275
}
// Moves the player up when the screen is being touched
if touchingScreen {
player.physicsBody?.velocity = CGVector(dx: 0, dy: 200
)
}
}
func didBegin(_ contact: SKPhysicsContact) {
// Exits the method if either node is nil (doesn't exist)
guard let nodeA = contact.bodyA.node else { return }
guard let nodeB = contact.bodyB.node else { return }
// Check to see if either node is player and, if so, call the playerHit method and pass in the other node
if nodeA == player {
playerHit(nodeB)
} else if nodeB == player {
playerHit(nodeA)
}
}
func createEnemy() {
// Check for Bonus Creation
checkForBonusCreation()
// Choose a Random Enemy
let pickEnemy = Int(arc4random_uniform(3))
switch pickEnemy {
case 0:
obstacle = SKSpriteNode(imageNamed: "enemy-balloon")
animateBalloon()
case 1:
obstacle = SKSpriteNode(imageNamed: "enemy-bird")
animateBird()
case 2:
obstacle = SKSpriteNode(imageNamed: "enemy-plane")
animatePlane()
default:
return
}
// Positions the enemy
obstacle.zPosition = -2
obstacle.position.x = 768
obstacle.size = (CGSize(width: obstacle.size.width * 0.7, height: obstacle.size.width * 0.7))
// Prevents the obstacle from being spawned too low on the scene
if obstacle.position.y < -150 {
obstacle.position.y = -150
}
addChild(obstacle)
// Adds pixel perfect collision detection to the enemies
obstacle.physicsBody = SKPhysicsBody(texture: obstacle.texture!, size: obstacle.texture!.size())
// Then we set isDynamic to false so grivty will not affect the obstacles
obstacle.physicsBody?.isDynamic = false
// Assigns 1 to it's contactTestBitMask so that it know to detect a collision with the player
obstacle.physicsBody?.contactTestBitMask = 1
// Names the obstacle so we can track collisions properly
obstacle.name = "enemy"
// Spawn an enemy at a random y-axis
obstacle.position.y = CGFloat(rand.nextInt())
// Moves the obstacles across to and off the left hand side of the screen over 9 seconds athen removes thier nodes so they don't chew up memory
let move = SKAction.moveTo(x: -768, duration: 9)
let remove = SKAction.removeFromParent()
let action = SKAction.sequence([move, remove])
obstacle.run(action)
}
func playerHit(_ node: SKNode) {
if node.name == "enemy" {
player.removeFromParent()
node.removeFromParent()
lives -= 1
balloonPop()
showLivesRemaining()
} else if node.name == "ground" {
player.removeFromParent()
lives -= 1
balloonPop()
showLivesRemaining()
}
}
func balloonPop() {
player.removeFromParent()
}
func showLivesRemaining() {
if lives >= 3 {
lives = 3
} else if lives <= 0 {
lives = 0
player.removeFromParent()
}
}
// Jump to the restart or quit scene
func restartGame() {
player.removeFromParent()
let wait = SKAction.wait(forDuration: 2.0)
let showPlayer = SKAction.run {
player.position = CGPoint(x: -400, y: 275)
self.addChild(player)
}
let sequence = SKAction.sequence([wait, showPlayer])
run(sequence)
}
// Displays GAME OVER and takes the player to the "Restart or Quit" choice scene
func endGame() {
player.removeFromParent()
let gameOver = SKSpriteNode(imageNamed: "game-over")
gameOver.zPosition = 15
addChild(gameOver)
// Waits 2 seconds and fade into the Restart Scene
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
if let scene = RestartScene(fileNamed: "RestartScene") {
scene.scaleMode = .aspectFill
self.view?.presentScene(scene, transition: SKTransition.crossFade(withDuration: 1.5))
}
}
}
}
What a lot of games do is give a buffer period after a player is hit where they flash the player or turn the player red for a couple of seconds. during this time the player is not prone to more hits and gives the player an opportunity to move to safety.
func didBegin(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
//don't detect contact if the player is hit
if ((contactAName == "player") || (contactBName == "player")) && !player.isHit {
if (contactAName == "ground") || (contactBName == "ground") {
print("groundcontact with player")
player.isHit = true
//good idea to flash player or pulse player to let user know player is hit
self.run(.wait(forDuration: 3.0)) {
//wait for 3 seconds and make the player hittable again
self.player.isHit = false
}
return
}
}
}
Edit
the contact detection code fires at up to 60 times a second. When your player hits an enemy you remove that enemy, making more contact to the enemy not possible. However when the player hits the ground the ground does not get removed and thus player keeps losing lives. after the player hits the ground try bumping the player up a 100px away from the ground and see if that stops the multiple contact issue.

enumerateChildNodes not finding child nodes in sprite kit - Swift 4

I am trying to create a SpriteKit game where a ball moves across the screen. When the ball leaves the screen I would like to remove it from the parent and switch to a different scene (GameOverScene).
I am using enumerateChildNodes however it doesn't as if that is working. I am not really sure what the problem is however I think it may have something to do with the parent/child relationship...
func createBall(forTrack track: Int) {
setupTracks()
player?.physicsBody?.linearDamping = 0
player = SKSpriteNode(imageNamed: "small")
player?.name = "BALL"
player?.size = CGSize(width: 100, height: 100)
ballValue = 1
randFloat = Float(arc4random()) / Float(UINT32_MAX)
if randFloat > 0.001 {
ballSpeed = randFloat / 50
}
else {
ballSpeed = randFloat / 50
}
let ballPosition = trackArray?[track].position
player?.position = CGPoint(x: (ballPosition?.x)!, y: (ballPosition?.y)!)
player?.position.y = (ballPosition?.y)!
if ballDirection == "right" {
player?.position.x = 0
moveRight()
}
else {
player?.position.x = (self.view?.frame.size.height)!
moveLeft(speed: ballSpeed)
}
self.addChild(player!)
self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in
if node.position.x < -100 || node.position.x > (self.size.width) + 100 {
print("balls Out")
node.removeFromParent()
let transition = SKTransition.fade(withDuration: 1)
self.gameScene = SKScene(fileNamed: "GameOverScene")
self.gameScene.scaleMode = .aspectFit
self.view?.presentScene(self.gameScene, transition: transition)
}
}
}
I call this function twice, first in override func didMove():
override func didMove(to view: SKView) {
createHUD()
createBall(forTrack: track)
}
And second in override func touchesBegan:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let location = touch.previousLocation(in: self)
let node = self.nodes(at: location).first
if node?.name == "BALL" {
currentScore += ballValue
player?.removeFromParent()
createBall(forTrack: track)
}
else {
let transition = SKTransition.fade(withDuration: 1)
gameScene = SKScene(fileNamed: "GameOverScene")
gameScene.scaleMode = .aspectFit
self.view?.presentScene(gameScene, transition: transition)
}
}
}
update:
The line self.enumerateChildNodes(withName: "BALL") { (node: SKNode, nil) in works so it is not a child parent relationship issue. The if statement is not working.
reading your code I think it's not about a solution, but a different approach. These suggestions will make your life easier:
Start with a smaller code, avoid or comment out all unneeded (like the if randFloat)
Force unwrap player = SKSpriteNode(imageNamed: "small")! with the ! because you actually want to crash if the initialization fails, then you can get rid of all ?
in touchesBegan, better use for touch in touches { as it is more simple to manage
let location = touch.previousLocation(in: self) you probably mean let location = touch.location(in: self)
When the ball leaves the screen I would like to remove it from the parent so this is something happening at some time in the game. You would like to call your self.enumerateChildNodes(withName: "BALL") { into the update call, not every time you touch the screen
If this is not enough, feel free to post the least amount of code to make a playground and let me test it for you :]

How do I rotate a colour sprite by moving finger along y-axis in the BOTTOM HALF of the screen?

Code:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if currentGameType == .wobble{
main.zRotation = (location.y)/512
if location.y > maxTouchHeight { return }
}
}
}
So I am trying to rotate a paddle depending on where your finger is on the y-axis in a type of pong game. However, I have been unable to make it so that it only works in the bottom half of the screen i.e. the two extremities of rotation are at the very top and very bottom of the screen but I want the top extremity to be at the top of the bottom half of the screen. (main is a pong paddle).
Any help is appreciated :)
After followed discussion from comments and repeated answer updates, I have a solution for you. Please let me know if questions:
class GameScene: SKScene {
let paddle = SKSpriteNode()
let maximumRotation = CGFloat(45)
override func didMove(to view: SKView) {
paddle.color = .blue
paddle.size = CGSize(width: 200, height: 15)
let deg45 = CGFloat(0.785398)
let degNeg45 = -deg45
let constraint = [SKConstraint.zRotation(SKRange(lowerLimit: degNeg45, upperLimit: deg45))]
paddle.constraints = constraint
addChild(paddle)
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let location = touches.first?.location(in: self) else { return }
guard let lastLocation = touches.first?.previousLocation(in: self) else { return }
guard location.y < self.frame.midY else { return }
// Our vertical plots:
let yVal = location.y
let lastY = lastLocation.y
// How much we moved in a certain direction this frame:
let deltaY = yVal - lastY
// This value represents 100% of a 45deg angle:
let oneHundredPercent = self.frame.height/2
assert(oneHundredPercent != 0)
// The % of 100%Val (45degrees) that we moved (in radians):
let absY = abs(deltaY)
let radToDegFactor = CGFloat(0.01745329252)
let multiplier = (absY / oneHundredPercent) * radToDegFactor
// I suggest a sensitivity of 2-4:
let sensitivity = CGFloat(3)
let amountToRotate = maximumRotation * (multiplier * sensitivity)
// Rotate the correct amount in the correct direction:
if deltaY > 0 {
// Rotate counter-clockwise:
paddle.run(.rotate(byAngle: amountToRotate, duration: 0))
} else {
// Rotate clockwise:
paddle.run(.rotate(byAngle: -amountToRotate, duration: 0))
}
}
}
Just open up a new project and try it out!
There may be an easier way to do this, but this is what popped in my head. It's a little verbose too so hopefully you can see each process step-by-step.
As far as doing something using the bottom half only, you can do something like this
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
if let touch = touches.first {
let fingerPosition = touch.location(in: self)
//this is the bottom
if fingerPosition.y < self.size.height/2 {
//just replace print with your own code
print("bottom half")
}
//this is the top half
if fingerPosition.y > self.size.width/2 {
//you can just leave this as it is or if ever you want to do something with the top part, just add your own code!
print("not bottom half")
}
}
}
Hopefully this helps.
put this inside of your touches functions:
for touch in touches{
let location = touch.location(in: self)
let maxTouchHeight = self.frame.midY
if location.y > maxTouchHeight { return }
if currentGameType == .wobble{
main.zRotation = (location.y)/512
}
}

Swift Timer Check TouchesEnded

Sorry to be awkward but please can I have the full code on how to do this:
I would like a delay of 1 second after every bullet is shot in my game to prevent bullet spam. If possible without creating a separate function for the bullet spawning as I have done this in touchesEnded. So tap, shoot, wait. tap, shoot, wait. And in the wait, if screen is tapped, nothing happens. Thanks and sorry I am a beginner
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
//Set up initial location of bullet and properties
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.name = "Bullet"
bullet.position = player.position
bullet.setScale(0.75)
bullet.zPosition = 1
bullet.physicsBody = SKPhysicsBody(circleOfRadius: bullet.size.width/2)
bullet.physicsBody?.isDynamic = true
bullet.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
bullet.physicsBody?.collisionBitMask = PhysicsCategory.None
bullet.physicsBody?.usesPreciseCollisionDetection = true
//Determine offset of location to bullet
let offset = touchLocation - bullet.position
//Stops Bullet from shooting backwards
if (offset.y < 0) { return }
addChild(bullet)
//Get the direction of where to shoot
let direction = offset.normalized()
//Make it shoot far enough to be guaranteed off screen
let shootAmount = direction * 1000
//Add the shoot amount to the current position
let realDest = shootAmount + bullet.position
//Create the actions
if currentGameState == gameState.inGame {
let actionMove = SKAction.move(to: realDest, duration: 1.2)
let actionMoveDone = SKAction.removeFromParent()
bullet.run(SKAction.sequence([actionMove, actionMoveDone]))
}
}
Try this:
var fired = false
// Other code...
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else {
return
}
let touchLocation = touch.location(in: self)
if fired == false {
fired = true
//Set up initial location of bullet and properties
let bullet = SKSpriteNode(imageNamed: "bullet")
bullet.name = "Bullet"
bullet.position = player.position
bullet.setScale(0.75)
bullet.zPosition = 1
bullet.physicsBody = SKPhysicsBody(circleOfRadius: bullet.size.width/2)
bullet.physicsBody?.isDynamic = true
bullet.physicsBody?.categoryBitMask = PhysicsCategory.Projectile
bullet.physicsBody?.contactTestBitMask = PhysicsCategory.Monster
bullet.physicsBody?.collisionBitMask = PhysicsCategory.None
bullet.physicsBody?.usesPreciseCollisionDetection = true
//Determine offset of location to bullet
let offset = touchLocation - bullet.position
//Stops Bullet from shooting backwards
if (offset.y < 0) { return }
addChild(bullet)
//Get the direction of where to shoot
let direction = offset.normalized()
//Make it shoot far enough to be guaranteed off screen
let shootAmount = direction * 1000
//Add the shoot amount to the current position
let realDest = shootAmount + bullet.position
//Create the actions
if currentGameState == gameState.inGame {
let actionMove = SKAction.move(to: realDest, duration: 1.2)
let actionMoveDone = SKAction.removeFromParent()
bullet.run(SKAction.sequence([actionMove, actionMoveDone]))
}
run(SKAction.wait(forDuration: 1), completion: { fired = false })
}
}
This means that every time that a bullet is fired, a timer for 1 second occurs which prevents the bullet code from being run until the time is over. At this point, the Boolean switches back to false allowing the code to be run again.