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

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)
}

Related

My SKSpriteNode speed values are not updating properly

What I'm trying to do is update my SKSpriteNodes so I can change their scrolling speeds dynamically, however they aren't really working consistently. I didn't include the code, but I have another method with a switch case that sets the value of platformSpeed whenever the state is changed (in this case, the switch case is changed with UIButtons). In my code I have an SKSpriteNode array and a platformSpeed property that includes didSet so my value is updated properly.
In my method to create the platforms, I grouped my SpriteNodes into platformGroup then looped through them with addChild(). Not sure why it's acting this way but here's a quick video of what it looks like in action:
demonstration clip
So with the buttons I'm changing the switch case, and as you can see, not all of the nodes speeds are updating properly and some get faster than others and eventually pass them. I need them to stay equal distance between each other.
Now here's my code:
class GameScene: SKScene, SKPhysicsContactDelegate {
var platformGroup = [SKSpriteNode]()
var platformSpeed: CGFloat = 1.0 {
didSet {
for platforms in platformGroup {
platforms.speed = platformSpeed
}
}
}
let platformTexture = SKTexture(imageNamed: "platform")
var platformPhysics: SKPhysicsBody!
func createPlatforms() {
let platformLeft = SKSpriteNode(texture: platformTexture)
platformLeft.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformLeft.physicsBody?.isDynamic = false
platformLeft.scale(to: CGSize(width: platformLeft.size.width * 4, height: platformLeft.size.height * 4))
platformLeft.zPosition = 20
let platformRight = SKSpriteNode(texture: platformTexture)
platformRight.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformRight.physicsBody?.isDynamic = false
platformRight.scale(to: CGSize(width: platformRight.size.width * 4, height: platformRight.size.height * 4))
platformRight.zPosition = 20
let scoreNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: frame.width, height: 32))
scoreNode.physicsBody = SKPhysicsBody(rectangleOf: scoreNode.size)
scoreNode.physicsBody?.isDynamic = false
scoreNode.name = "scoreDetect"
scoreNode.zPosition = 40
platformGroup = [platformLeft, platformRight, scoreNode]
let yPosition = frame.width - platformRight.frame.width
let max = CGFloat(frame.width / 4)
let xPosition = CGFloat.random(in: -80...max)
let gapSize: CGFloat = -50
platformLeft.position = CGPoint(x: xPosition + platformLeft.size.width - gapSize, y: -yPosition)
platformRight.position = CGPoint(x: xPosition + gapSize, y: -yPosition)
scoreNode.position = CGPoint(x: frame.midX, y: yPosition - (scoreNode.size.width / 1.5))
let endPosition = frame.maxY + (platformLeft.frame.height * 3)
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
platformCount += 1
}
func loopPlatforms() {
let create = SKAction.run { [unowned self] in
self.createPlatforms()
platformCount += 1
}
let wait = SKAction.wait(forDuration: 1.1)
let sequence = SKAction.sequence([create, wait])
let repeatForever = SKAction.repeatForever(sequence)
run(repeatForever)
}
I think I can see what's going wrong. When you change platformSpeed, it changes the speed of all the platforms in platformGroup. And createPlatforms() is being called multiple times. Now, each time it's called you create a pair of platforms and assign these to platformGroup. Since you call the function multiple times, it's overwriting any existing values in the array. That's why changing platformSpeed only updates the speed of the latest platforms you've created---the older platforms stay the same speed because they're not in platformGroup anymore.
To fix this, my advice would be to have platformGroup store all the platforms currently on the screen. You could do this by changing
platformGroup = [platformLeft, platformRight, scoreNode]
to something like
let newNodes = [platformLeft, platformRight, scoreNode]
platformGroup += newNodes
// Alternatively, platformGroup.append(contentsOf: newNodes)
Now you need to make sure you're 1) only adding the new nodes to the scene, and 2) removing the old nodes from platformGroup when they're removed from the parent. You could do this by changing
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
let moveSequence = SKAction.sequence([moveAction, SKAction.removeFromParent()])
for platforms in platformGroup {
addChild(platforms)
platforms.run(moveSequence)
}
to something like
let moveAction = SKAction.moveBy(x: 0, y: endPosition, duration: 7)
for node in newNodes {
let moveSequence = SKAction.sequence([
moveAction,
SKAction.removeFromParent(),
SKAction.run {
self.platformGroup.remove(node)
}
])
addChild(node)
node.run(moveSequence)
}
Now that you're keeping a track of all platforms ever made, your speed changes should be applied consistently to every platform on the screen. Hope this works!

how to carry across values across scenes

I am trying to make a 'money' value show on 3 different scenes, and it is showing on the GameScene but not on the GameOverScene or the ShopScene.
this is the relevant code of the GameScene that is working:
func adjustMoney(by points: Int){
var money = UserDefaults().integer(forKey: "moneyscore")
money += points
MoneyLabel.text = "Money = " + UserDefaults().integer(forKey: "moneyscore").description
UserDefaults().set(money, forKey: "moneyscore")
}
func projectileDidCollideWithMonster(projectile: SKSpriteNode, monster: SKSpriteNode) {
print("Hit")
projectile.removeFromParent()
monster.removeFromParent()
monstersDestroyed += 1
adjustScore(by: 1)
adjustMoney(by: 2)
and this is the total code in the other scenes (there is just the ShopScene as it is the same in the other):
import Foundation
import SpriteKit
var welcomeLabel: SKLabelNode!
class ShopScene: SKScene {
var background = SKSpriteNode(imageNamed: "background")
override func didMove(to view: SKView) {
background.zPosition = -1
background.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
addChild(background) welcomeLabel = SKLabelNode(fontNamed: "Chalkduster")
welcomeLabel.text = "Welcome to the shop!"
welcomeLabel.fontSize = 40
welcomeLabel.fontColor = SKColor.black
welcomeLabel.position = CGPoint(x: size.width/2, y: size.height/1.2)
addChild(welcomeLabel)
MoneyLabel = SKLabelNode(fontNamed: "Chalkduster")
MoneyLabel.text = "Money = \(money)"
MoneyLabel.fontSize = 20
MoneyLabel.fontColor = SKColor.black
MoneyLabel.position = CGPoint(x: size.width/6.2, y: size.height/1.35)
addChild(MoneyLabel)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
let gameScene = GameScene(size: self.size)
gameScene.scaleMode = .aspectFill
self.view?.presentScene(gameScene, transition: SKTransition.doorsCloseHorizontal(withDuration: 1.0))
}
}
However it only says money = 0
---- I'm not sure if it is of any relevance but before my 'projectile' and 'monster' first collide each game all my values of high score and score and money are 0 then they go back to their saved values, apart from score obviously that goes to 0. Score is the only value that isn't saved and that does move across to the GameOverScene.
You need to access UserDefaults().integer(forKey: "moneyscore") to use this in both the GameOverScene or the ShopScene class as you are updating userdefaults value(money).
This is actually for your specific case only. On the other hand, if you are interested to pass data in different classes(you can mention them as screens if they have views), you can follow different methodologies. You can follow this link for references.

How do I make 'n' number of SKSpriteNode move in all direction continuously Swift?

I want 'n' number of nodes keep moving in all direction continuously.
Below is how I coded
func spriteCollection(count : Int) -> [SKSpriteNode]{
var spriteArray = [SKSpriteNode]()
for _ in 0..<count {
let sprite = SKSpriteNode(imageNamed: "node.png")
//giving random position to sprites,
let x = CGFloat(arc4random_uniform(UInt32(((self.scene?.frame.width)! - sprite.size.width))))
let y = CGFloat(arc4random_uniform(UInt32(((self.scene?.frame.height)! - sprite.size.height))))
sprite.position = CGPoint(x: x, y: y)
spriteArray.append(sprite)
}
return spriteArray
}
override func didMoveToView(view: SKView) {
//here if I pass more than 2, the nodes won't even get displayed sometimes
let spriteCollection = spriteCollection(2)
for sprite in spriteCollection{
let blockOFaction = SKAction.runBlock({
let x = CGFloat(arc4random_uniform(UInt32(((self.scene?.frame.width)! - sprite.size.width))))
let y = CGFloat(arc4random_uniform(UInt32(((self.scene?.frame.height)! - sprite.size.height))))
let action = SKAction.moveTo(CGPoint(x: x , y: y), duration: 2)
print("moving to \(x,y)")
action.speed = 3.0
sprite.runAction(action)
})
let waitAction = SKAction.waitForDuration(1)
sprite.runAction(SKAction.repeatActionForever(SKAction.sequence([blockOFaction, waitAction])))
addChild(sprite)
}
}
The nodes are moving randomly in all direction, but if I add more than 2 sprites, sometimes none of the sprites are visible(just a blank screen) and also I want to increase the number of sprites eventually. Here what I'm doing is, just passing the number of sprites I want(It cannot be increased eventually) So is there any other way to do it. Could anyone please suggest me how can I achieve this..

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

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])))

SKPhysicsJointFixed in SpriteKit and Swift

I'm making a game in Sprite Kit and Swift and I have a Sprite at the bottom of the screen and falling Sprites from the top which I want to catch and stick to the Sprite at the bottom, so I'm trying to use SKPhysicsJointFixed but when the objects collide instead of the falling object sticking to the one at the bottom which is supposed to catch and have it attached, it seems as if the bottom Sprite adapts the physics of the falling sprite and then falls off the screen with it. Here's the code I have in my didBeginContact method. and skewer is the name of the Sprite at the bottom which should always be at the bottom and not disappear.
if contact.bodyA.node!.name == "Skewer"
{
let boundX = skewer.physicsBody?.node?.position.x
let fixedJoint = SKPhysicsJointFixed.jointWithBodyA(contact.bodyA.node!.physicsBody, bodyB: contact.bodyB.node!.physicsBody, anchor: CGPoint(x: boundX!, y: boundY))
physicsWorld.addJoint(fixedJoint)
// contact.bodyB.node!.removeFromParent()
}
else
{
contact.bodyA!.node!.removeFromParent()
}
and the physics for the bottom screen Sprite are here
func makeSkewer()
{
skewer.name = "Skewer"
skewer.position = CGPoint(x: size.width * 0.5, y: size.height * 0.244)
skewer.physicsBody = SKPhysicsBody(rectangleOfSize: skewer.size)
skewer.physicsBody?.affectedByGravity = false
skewer.physicsBody?.categoryBitMask = kSkewerCategory
skewer.physicsBody?.contactTestBitMask = kFoodCategory
skewer.physicsBody?.collisionBitMask = kSceneEdgeCategory
addChild(skewer)
}
and physics for the falling Sprites are here
func random() ->CGFloat
{
return CGFloat(Float(arc4random()) / 0xFFFFFFFF)
}
func random(#min: CGFloat,max: CGFloat) -> CGFloat
{
return random() * (max - min) + min
}
func addFood()
{
let randomCatchIndex = Int(arc4random_uniform(UInt32(foods.count)))
let food = SKSpriteNode(imageNamed: foods[randomCatchIndex])
let actualX = random(min: food.size.width/2, max: size.width - food.size.width/2)
let actualDuration = random(min: CGFloat(1.5), max: CGFloat(8.0))
let actionMove = SKAction.moveTo(CGPoint(x: actualX, y: -food.size.height/2), duration: NSTimeInterval(actualDuration))
let actionMoveDone = SKAction.removeFromParent()
food.physicsBody = SKPhysicsBody(rectangleOfSize: food.size)
food.position = CGPoint(x: actualX, y: size.height + food.size.height/2)
food.physicsBody?.categoryBitMask = kFoodCategory
food.physicsBody?.contactTestBitMask = kSkewerCategory
food.physicsBody?.collisionBitMask = 0x0
food.physicsBody?.dynamic = true
food.runAction(SKAction.sequence([actionMove, actionMoveDone]))
addChild(food)
}
Set the skewer to not have dynamic physics. What you have currently is it not being affected by gravity, and as soon as it locks onto the food (which is traveling down and has momentum), the skewer moves with it.
In the creation of the skewer, run the following line:
skewer.physicsBody?.dynamic = false
You can also now ignore the affectedByGravity as that is something that only affects dynamic objects.