[SpriteKit Swift]Trying to spawn cars and move them down the screen at a pace that varies - swift

So, I am making an app for my friend and I need to make cars spawn in 1 of 3 lanes on a road randomly, then move down the screen at a pace that can and will vary at multiple points during the car's lifetime.
let createCarAction = SKAction.runBlock({ //Creates the cars every 4 seconds w/ a waitAction
let laneNum = laneList[Int(arc4random_uniform(3))] //There is an array higher up with the X values of each lane that the cars can use
let newCar: SKSpriteNode
let num = arc4random_uniform(4) //picks random car color
if num == 0 {
newCar = SKSpriteNode(imageNamed: "Audi")
} else if num == 1 {
newCar = SKSpriteNode(imageNamed: "Audi2")
} else if num == 2 {
newCar = SKSpriteNode(imageNamed: "Audi3")
} else {
newCar = SKSpriteNode(imageNamed: "Audi4")
}
newCar.xScale = 0.30
newCar.yScale = 0.30
newCar.position = CGPoint(x: laneNum, y: CGRectGetMaxY(self.frame) + newCar.size.height/2) //puts the car in the picked lane and just above the screen
newCar.zPosition = CGFloat(2)
self.addChild(newCar)
let carAction = SKAction.runBlock({ //this is where it starts going bad. This action is run so many times in the first second the scene starts that it does not switch transition from the scene before it.
if newCar.position.y > (CGRectGetMinY(self.frame)-newCar.size.height) { //if the car is on the screen basically
newCar.position = CGPoint(x: newCar.position.x, y: newCar.position.y-CGFloat(spd))
print("test")
} else { // not on the screen
newCar.removeFromParent()
print("car y: \(CGRectGetMaxY(self.frame) + newCar.size.height/2)")
newCar.removeAllActions() // This does not seem to have any affect as when only one car has been created, carAction is still running seconds after the car has left the screen
}
})
newCar.runAction(SKAction.repeatActionForever(carAction))
self.newCarList.append(newCar)
print("made new car")
})
I have the spawning working, but when newCar.runAction(SKAction.repeatActionForever(carAction)) comes into play, the app does not move to the GameScene from the MenuScene. I think that the car just instantly moves to the bottom, then the game just lags out trying to print where the car is. I have the road moving in the update function.
override func update(currentTime: CFTimeInterval) {
/* Called before each frame is rendered */
if road1.position.y > CGRectGetMinY(self.frame) {
road1.position = CGPoint(x: CGRectGetMidX(self.frame), y: road1.position.y - CGFloat(spd)/2)
road2.position = CGPoint(x: road1.position.x, y: road1.position.y + road1.size.height)
} else {
road1.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
road2.position = CGPoint(x: road1.position.x, y: road1.position.y + road1.size.height)
}
}
I can't think of a way to move the cars that doesn't involve a massive array or a complex SKAction that moves the car instantly instead of at the same pace as the road.
Thanks in advance.
Edit: Formatting

I ended up figuring out a way to use an SKAction.
let carWaitAction = SKAction.waitForDuration(1/60)
let carAction = SKAction.runBlock({
if newCar.position.y > (CGRectGetMinY(self.frame)-newCar.size.height) {
newCar.position = CGPoint(x: newCar.position.x, y: newCar.position.y-CGFloat(spd/3))
} else {
self.score = self.score+5
newCar.removeFromParent()
print("car removed")
newCar.removeAllActions()
print (newCar.hasActions())
}
})
newCar.runAction(SKAction.repeatActionForever(SKAction.group([carWaitAction, carAction])))

Related

How to place nodes on the ground?

I am creating a game with SpriteKit. The background is a png image, endlessly moving (parallax scroll):
func parallaxScroll(image: String, y: CGFloat, z: CGFloat, duration: Double, needsPhysics: Bool) {
for i in 0 ... 1 {
// position the first node on the left, and position second on the right
let node = SKSpriteNode(imageNamed: image)
node.position = CGPoint(x: 1023 * CGFloat(i), y: y)
node.zPosition = z
addChild(node)
if needsPhysics {
node.physicsBody = SKPhysicsBody(texture: node.texture!, size: node.texture!.size())
node.physicsBody?.isDynamic = false
node.physicsBody?.contactTestBitMask = 1
node.name = "ground"
}
// make this node move the width of the screen by whatever duration was passed in
let move = SKAction.moveBy(x: -1024, y: 0, duration: duration)
// make it jump back to the right edge
let wrap = SKAction.moveBy(x: 1024, y: 0, duration: 0)
// make these two as a sequence that loops forever
let sequence = SKAction.sequence([move, wrap])
let forever = SKAction.repeatForever(sequence)
// run the animations
node.run(forever)
}
}
The example function below places a box at random y position:
#objc func createObstacle() {
let obstacle = SKSpriteNode(imageNamed: "rectangle")
obstacle.zPosition = -2
obstacle.position.x = 768
addChild(obstacle)
obstacle.physicsBody = SKPhysicsBody(texture: obstacle.texture!, size: obstacle.texture!.size())
obstacle.physicsBody?.isDynamic = false
obstacle.physicsBody?.contactTestBitMask = 1
obstacle.name = "obstacle"
let rand = GKRandomDistribution(lowestValue: -200, highestValue: 350)
obstacle.position.y = CGFloat(rand.nextInt())
// make it move across the screen
let action = SKAction.moveTo(x: -768, duration: 9)
obstacle.run(action)
}
func playerHit(_ node: SKNode) {
if node.name == "obstacle" {
player.removeFromParent()
}
}
func didBegin(_ contact: SKPhysicsContact) {
guard let nodeA = contact.bodyA.node else { return }
guard let nodeB = contact.bodyB.node else { return }
if nodeA == player {
playerHit(nodeB)
} else if nodeB == player {
playerHit(nodeA)
}
}
Instead of placing it at random, I would like to place it on the "ground". The following image illustrates the current placement and the desired placement:
Does anyone know how to place an obstacle node (object) on the ground which is not flat instead of random y position?
You could cast a ray, at the box's intended x position, from the top of the screen to the bottom, and when the ray hits the ground, place the box directly above the hitpoint. See enumerateBodies(alongRayStart:end:using:). In your case you might want to insert into createObstacle something like:
let topOfScreen = size.height / 2
let bottomOfScreen = -size.height / 2
obstacle.position.x = -768
physicsWorld.enumerateBodies(alongRayStart: CGPoint(x: obstacle.position.x, y: topOfScreen), end: CGPoint(x: obstacle.position.x, y: bottomOfScreen)) { body, point, normal, stop in
if body.node?.name == "ground" {
// body is the ground's physics body. point is the point that an object
// falling from the sky would hit the ground.
// Assuming the obstacle's anchor point is at its center, its actual position
// would be slightly above this point
let positionAboveBody = point + (obstacle.size.height / 2)
obstacle.position.y = positionAboveBody
}
}

Sprites Spawning In Straight Away At The Start

I am currently making a game on Xcode 11, I am having a problem where in which my sprite will spawn in straight away at the start, even through their is a wait delay, but after the first spawn the sprites spawn in at their current time. How can I make it so the sprites wont spawn in untill 3 seconds after the scene has started.
Any help would be greatly appreciated.
The photo attached shows what happens at the start of the game.
I have attached part of my code for one of the sprites as their setup is similar to each other.
//Setup Bird
func setupBird() {
bird = SKSpriteNode(imageNamed: "bird-1")
bird.name = "Bird"
bird.zPosition = 20.0
bird.setScale(1.5)
let birdHeight = bird.frame.height
let random = CGFloat.random(min: -birdHeight, max: birdHeight*2.0)
bird.position = CGPoint(x: cameraRect.maxX + bird.frame.width, y: size.height/2.0 + birdHeight + random)
bird.physicsBody = SKPhysicsBody(circleOfRadius: bird.size.width/2.0)
bird.physicsBody!.affectedByGravity = false
bird.physicsBody!.isDynamic = false
bird.physicsBody!.categoryBitMask = PhysicsCategory.Bird
bird.physicsBody!.contactTestBitMask = PhysicsCategory.Player
addChild(bird)
bird.run(.sequence([.wait(forDuration: 15, withRange: 5), .removeFromParent()]))
//Animation For Birds
var textures: [SKTexture] = []
for i in 1...3 {
textures.append(SKTexture(imageNamed: "bird-\(i)"))
}
bird.run(.repeatForever(.animate(with: textures, timePerFrame: 0.15)))
}
func spawnBird() {
let random = CGFloat.random(min: 15.0, max: 30.0)
run(.repeatForever(.sequence([
.wait(forDuration: TimeInterval(random)),
.run { [weak self] in
self?.setupBird()
}
])))
}
I fixed the problem by adding frame.width/2.0 to the position, which moves the inital bird position.
bird.position = CGPoint(x: cameraRect.maxX + frame.width/2.0 + bird.frame.width, y: size.height/2.0 + birdHeight + random)
If I understand correctly, you want to spawn a bird periodically, starting after an initial delay.
You could delay the repeating action, either with another SpriteKit action or with DispatchQueue.
func spawnBirds() {
let spawnBirdAfterDelayAction = SKAction.sequence([
.wait(forDuration: 22.5, withRange: 7.5),
.run { [weak self] in self?.setupBird() }
])
run(
.sequence([
.wait(forDuration: 3), // initial delay
.repeatForever(spawnBirdAfterDelayAction)
])
)
}
/// As Knight0fDragon pointed out, this approach is inferior because it circumvents the SpriteKit runtime.
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
self.spawnBirds()
}

How do you create random nodes to appear at random times but they do not overlap current nodes in scene?

I am fairly new to iOS Swift and sprite kit, but I am currently developing a game where I would like items (SKSpriteNodes) to appear at random times and when they do appear, they don't overlap with existing nodes in the game scene.
Below is my code so far for producing a random node and a specified with range time interval to appear across the scene width/height. However, I haven't incorporated the code that allows them to not overlap or be on top of other present nodes in the scene.
override func didMove(to view: SKView) {
let wait = SKAction.wait(forDuration: 10, withRange: 5)
let spawn = SKAction.run {
self.randomFood()
}
let spawning = SKAction.sequence([wait,spawn])
self.run(SKAction.repeat((spawning), count: 5))
}
func randomFood() {
//supposed to pick random point within the screen width
let xPos = randomBetweenNumbers(firstNum: 0, secondNum: frame.width )
let foodNode = SKSpriteNode(imageNamed: "burgerFood") //create a new food item each time
foodNode.position = CGPoint(x: xPos, y: self.frame.size.height/2)
foodNode.setScale(0.10)
foodNode.physicsBody = SKPhysicsBody(circleOfRadius: foodNode.size.width/2)
foodNode.zPosition = 10
foodNode.alpha = 1
foodNode.physicsBody?.affectedByGravity = false
foodNode.physicsBody?.categoryBitMask = 0
foodNode.physicsBody?.contactTestBitMask = 0
addChild(foodNode)
}
func randomBetweenNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat {
return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum)
}

Stop repeatActionForever in Sprite Kit swift

I created an SKAction that repeatsForever (it's the planet spinning). But I want to stop it once the lander lands on the landing pad. So far my code is not working. I tried both removeAllActions() and removeActionForKey as you'll see. The contact detection works just fine, and there's a bunch of code not shown which includes the collision delegate, etc.
func createPlanet() {
var planet = SKSpriteNode()
planet.zPosition = 1
planet.name = "mars"
redPlanet = SKSpriteNode(imageNamed: "redPlanet")
redPlanet.name = "red"
redPlanet.zPosition = 2
redPlanet.physicsBody = SKPhysicsBody(texture: redPlanet.texture!, size: size)
redPlanet.physicsBody!.dynamic = false
redPlanet.physicsBody!.categoryBitMask = planetMask
redPlanet.physicsBody!.contactTestBitMask = 0
planet.addChild(redPlanet)
landingPad = SKSpriteNode(imageNamed: "landingPad")
landingPad.name = "pad"
landingPad.zPosition = 3
landingPad.position = CGPoint(x: 0, y: redPlanet.size.height / 2 - 60)
landingPad.physicsBody = SKPhysicsBody(rectangleOfSize: landingPad.size)
landingPad.physicsBody!.dynamic = false
landingPad.physicsBody!.categoryBitMask = landingPadMask
landingPad.physicsBody!.collisionBitMask = landerMask
landingPad.physicsBody!.contactTestBitMask = landerMask
planet.addChild(landingPad)
planet.position = CGPoint(x: frame.size.width / 2, y: -redPlanet.size.height / 6)
let spinner = SKAction.rotateByAngle(CGFloat(M_PI), duration: 3)
planet.runAction(SKAction.repeatActionForever(spinner), withKey: "planetSpin")
addChild(planet)
}
And this...
func didBeginContact(contact: SKPhysicsContact) {
if !hasLanded {
if contact.bodyA.node!.name == "lander" {
hasLanded = true
print("bodyA contact")
physicsWorld.speed = 0
removeAllActions()
removeActionForKey("planetSpin")
} else if contact.bodyB.node!.name == "lander" {
print("bodyB contact")
hasLanded = true
physicsWorld.speed = 0
removeAllActions()
removeActionForKey("planetSpin")
}
}
}
You're not removing actions from the planet node, you're removing them from the scene or parent node or whatever your didBeginContact is a member of.
Make planet a class variable and in didBeginContact, call planet.removeAllActions()

Swift: Pause endless scrolling background (i.e. infinite loop)

In my game I would like certain things to pause when the "endGame" function is called, while other things stay running. I have been able to accomplish this for most actions by using the "paused" boolean, but I can't get it to work for my scrolling background. I know the problem has to do with how the background action is contained in a for loop, but I'm not sure how to get around this. Here is an example of my code:
class GameScene: SKScene {
var sun = SKSpriteNode()
var background = SKSpriteNode()
override func didMoveToView(view: SKView) {
sun = SKSpriteNode(imageNamed: "Sun")
sun.zPosition = -2
sun.setScale(0.8)
sun.position = CGPoint(x: self.frame.size.width * 0.632, y: self.frame.size.height * 0.90)
let rotate = SKAction.rotateByAngle(CGFloat(M_PI_2), duration: NSTimeInterval(10))
let rotateForever = SKAction.repeatActionForever(rotate)
sun.runAction(rotateForever)
addChild(sun)
let backgroundTexture = SKTexture(imageNamed: "Background")
backgroundTexture.filteringMode = .Nearest
let moveBackground = SKAction.moveByX(-backgroundTexture.size().width, y: 0, duration: NSTimeInterval(0.004 * backgroundTexture.size().width * 2.0))
let resetBackground = SKAction.moveByX(backgroundTexture.size().width, y: 0, duration: 0.0)
let moveBackgroundForever = SKAction.repeatActionForever(SKAction.sequence([moveBackground,resetBackground]))
for var i:CGFloat = 0; i < 4.0 + self.frame.size.width / ( backgroundTexture.size().width ); ++i {
background = SKSpriteNode(texture: backgroundTexture)
background.setScale(0.5)
background.zPosition = -1
background.position = CGPointMake(i * background.size.width, backgroundVert)
background.runAction(moveBackgroundForever)
addChild(background)
}
}
func endGame {
sun.paused = true
background.paused = true
}
}
In this example the "paused" boolean works perfectly for the rotating SKSpriteNode called "sun", but it doesn't work for the background. Any thoughts?
What you should do is create 1 SKNode for your background sprites, add all the background nodes to this node, then add the master node to your scene. Then, when you need to pause, you only pause the master node, and the master node will then pause all of its children
for var i:CGFloat = 0; i < 4.0 + self.frame.size.width / (backgroundTexture.size().width ); ++i {
var background = SKSpriteNode(texture: backgroundTexture)
background.setScale(0.5)
background.zPosition = -1
background.position = CGPointMake(i * background.size.width, backgroundVert)
background.runAction(moveBackgroundForever)
self.background.addChild(background)
}
addChild(background)
Looks like you are creating multiple backgrounds in the for loop. But you are pausing only one.