My SKSpriteNode speed values are not updating properly - swift

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!

Related

Node spacing issues on a vertical scrolling game with variable speeds in Spritekit

So I'm creating a game in Swift with Spritekit and have ran into an issue with getting my game to work. I'm still a beginner with programming so I've likely missed out on a solution to this myself.
Anyway, so the game concept is a simple arcade vertical scroller that involves a player trying to dodge platforms as it descends downward. The mechanics (so far) are a stationary player on the y axis that can move left and right along the x axis while the platforms scroll upward along with the background moving with the platforms to give a visual effect of descent. I've gotten a build working to be fully playable, but there's an issue with spawning the platforms perfectly spaced out. Here's a sketch:
Concept Image
The picture on the left what I'm trying to achieve, while the one on the right is my current and flawed method. The main issue with the one on the right, is that it uses a collision to trigger spawning which means the spawn trigger node (red line) has to be 1 pixel tall to allow for perfect spacing. If the spawn trigger node is more than 1 pixel tall, then the collision may not trigger on that the first pixel of contact and trigger the node a few pixels deep which throws off the entire spacing. Also if the spawn trigger is only 1 pixel tall, it often won't trigger unless the everything is scrolling at slow speeds.
I've tried to think of other methods to approach this but I'm at a loss. I cannot use a simple timer to spawn nodes at intervals because the speed at which the game scrolls is variable and is constantly changing by player controls. The only two other options I can think of (which I don't know how to do either) is either spawn node sets at fixed y-positions and set that on a loop, or change it so the player is actually descending downward while everything is generating around it (seems tougher and maybe unnecessary). I'm considering just rewriting my createPlatforms() method if I need to, but here's the code for that and the background anyway:
var platformGroup = Set<SKSpriteNode>()
var platformSpeed: CGFloat = 0.6 { didSet { for platforms in platformGroup { platforms.speed = platformSpeed } } }
var platformTexture: SKTexture!
var platformPhysics: SKPhysicsBody!
var platformCount = 0
var backgroundPieces: [SKSpriteNode] = [SKSpriteNode(), SKSpriteNode()]
var backgroundSpeed: CGFloat = 1.0 { didSet { for background in backgroundPieces { background.speed = backgroundSpeed } } }
var backgroundTexture: SKTexture! { didSet { for background in backgroundPieces { background.texture = backgroundTexture } } }
func createPlatforms() {
let min = CGFloat(frame.width / 12)
let max = CGFloat(frame.width / 3)
var xPosition = CGFloat.random(in: -min ... max)
if platformCount >= 20 && platformCount < 30 {
stage = 0
setTextures()
xPosition = frame.size.width * 0.125
} else if platformCount == 30 {
stage = 2
setTextures()
} else if platformCount >= 50 && platformCount < 60 {
stage = 0
setTextures()
xPosition = 184
} else if platformCount == 60 {
stage = 3
setTextures()
}
platformPhysics = SKPhysicsBody(rectangleOf: CGSize(width: platformTexture.size().width, height: platformTexture.size().height))
let platformLeft = SKSpriteNode(texture: platformTexture)
platformLeft.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformLeft.physicsBody?.isDynamic = false
platformLeft.physicsBody?.affectedByGravity = false
platformLeft.physicsBody?.collisionBitMask = 0
platformLeft.scale(to: CGSize(width: platformLeft.size.width * 3, height: platformLeft.size.height * 3))
platformLeft.zPosition = 20
platformLeft.name = "platform"
platformLeft.speed = platformSpeed
let platformRight = SKSpriteNode(texture: platformTexture)
platformRight.physicsBody = platformPhysics.copy() as? SKPhysicsBody
platformRight.physicsBody?.isDynamic = true
platformRight.physicsBody?.collisionBitMask = 0
platformRight.scale(to: CGSize(width: platformRight.size.width * 3, height: platformRight.size.height * 3))
platformRight.zPosition = 20
platformRight.name = "platform"
platformRight.speed = platformSpeed
let scoreNode = SKSpriteNode(color: UIColor.clear, size: CGSize(width: frame.width, height: 32))
scoreNode.physicsBody = SKPhysicsBody(rectangleOf: scoreNode.size)
scoreNode.physicsBody?.isDynamic = false
scoreNode.zPosition = 100
scoreNode.name = "scoreDetect"
scoreNode.speed = platformSpeed
let platformTrigger = SKSpriteNode(color: UIColor.orange, size: CGSize(width: frame.width, height: 4))
platformTrigger.physicsBody = SKPhysicsBody(rectangleOf: platformTrigger.size)
platformTrigger.physicsBody?.isDynamic = true
platformTrigger.physicsBody?.affectedByGravity = false
platformTrigger.physicsBody?.categoryBitMask = Collisions.detect
platformTrigger.physicsBody?.contactTestBitMask = Collisions.spawn
platformTrigger.physicsBody?.collisionBitMask = 0
platformTrigger.physicsBody?.usesPreciseCollisionDetection = true
platformTrigger.zPosition = 100
platformTrigger.name = "platformTrigger"
platformTrigger.speed = platformSpeed
let newNodes: Set<SKSpriteNode> = [platformLeft, platformRight, scoreNode, platformTrigger]
for node in newNodes {
platformGroup.insert(node)
}
let yPosition = spawnNode.position.y - transitionPlatform.size().height
let gapSize: CGFloat = -frame.size.width / 6
print(gapSize)
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: platformLeft.position.y - platformLeft.size.height / 2)
platformTrigger.position = CGPoint(x: frame.midX, y: platformLeft.position.y)
print(platformLeft.position.y)
print(platformLeft.frame.midY)
let endPosition = frame.maxY + frame.midY
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)
nodeArray.append(node)
node.run(moveSequence)
}
platformCount += 1
}
func startPlatforms() {
let create = SKAction.run { [unowned self] in
self.createPlatforms()
}
run(create)
}
func createBackground() {
for i in 0 ... 1 {
let background = backgroundPieces[i]
background.texture = backgroundTexture
background.anchorPoint = CGPoint(x: 0, y: 0)
background.zPosition = -5
background.size = CGSize(width: frame.size.width, height: frame.size.width * 2.5)
background.position = CGPoint(x: 0, y: background.size.height + (-background.size.height) + (-background.size.height * CGFloat(i)))
self.addChild(background)
nodeArray.append(background)
let scrollUp = SKAction.moveBy(x: 0, y: background.size.height, duration: 5)
let scrollReset = SKAction.moveBy(x: 0, y: -background.size.height, duration: 0)
let scrollLoop = SKAction.sequence([scrollUp, scrollReset])
let scrollForever = SKAction.repeatForever(scrollLoop)
background.run(scrollForever)
}
}
Does anybody have any suggestions on how I approach this or change it so it would work perfectly everytime?

Spritekit - Shifting between two functions with a timer

I'm making a game where I have two different functions that holds different types of enemies. I want to make a code that shifts between the two every 10 score. So from 0-10 "function 1" is active, and from 10-20 "function 2" is active, and then it changes back again and then back again and so on.
This is my two functions containing enemies:
var score = 0
func createPipes() {
PipesHolder = SKNode()
PipesHolder.name = "Pipe"
let pipeLeft = SKSpriteNode(imageNamed: "PipeRight")
pipeLeft.name = "Pipe"
pipeLeft.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pipeLeft.position = CGPoint(x: 300, y: 0)
pipeLeft.physicsBody = SKPhysicsBody(rectangleOf: pipeLeft.size)
pipeLeft.physicsBody?.categoryBitMask = ColliderType.Pipe
pipeLeft.physicsBody?.affectedByGravity = false
let pipeRight = SKSpriteNode(imageNamed: "PipeLeft")
pipeRight.name = "Pipe"
pipeRight.anchorPoint = CGPoint(x: 0.5, y: 0.5)
pipeRight.position = CGPoint(x: -300, y: 0)
pipeRight.physicsBody = SKPhysicsBody(rectangleOf: pipeRight.size)
pipeRight.physicsBody?.categoryBitMask = ColliderType.Pipe
pipeRight.physicsBody?.affectedByGravity = false
PipesHolder.zPosition = 2
PipesHolder.xScale = 1.5
PipesHolder.yScale = 0.8
PipesHolder.position.x = CGFloat.randomBetweenNumbers(firstNum:
-220, secondNum: 220)
PipesHolder.position.y = self.frame.height + 100
PipesHolder.addChild(pipeLeft)
PipesHolder.addChild(pipeRight)
self.addChild(PipesHolder)
let destination = self.frame.height * 2
let move = SKAction.moveTo(y: -destination, duration: 10)
let remove = SKAction.removeFromParent()
let moveRight = SKAction.moveBy(x: 200, y: 0, duration: 1)
let moveLeft = SKAction.moveBy(x: -200, y: 0, duration: 1)
let moveBackAndForth =
SKAction.repeatForever(SKAction.sequence([moveRight, moveLeft]))
PipesHolder.run(moveBackAndForth)
PipesHolder.run(SKAction.sequence([move, remove]), withKey:
"MovePipes")
}
func spawnPipes() {
let spawn = SKAction.run({ () -> Void in
self.createPipes()
})
let delay = SKAction.wait(forDuration: 1)
let sequence = SKAction.sequence([spawn, delay])
self.run(SKAction.repeatForever(sequence), withKey: "SpawnPipes")
}
func createRedEnemies() {
let enemyHolder = SKNode()
enemyHolder.name = "Holder"
let enemyLeft = SKSpriteNode(imageNamed: "Enemy")
let enemyMiddle = SKSpriteNode(imageNamed: "Enemy")
let enemyRight = SKSpriteNode(imageNamed: "Enemy")
enemyLeft.name = "Enemy"
enemyLeft.anchorPoint = CGPoint(x: 0.5, y: 0.5)
enemyLeft.position = CGPoint(x: 200, y: 0)
enemyLeft.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width:
enemyLeft.size.width - 5, height: enemyLeft.size.height - 5))
enemyLeft.physicsBody?.categoryBitMask = ColliderType.Enemy
enemyLeft.physicsBody?.collisionBitMask = 0
enemyLeft.physicsBody?.affectedByGravity = false
enemyMiddle.name = "Enemy"
enemyMiddle.anchorPoint = CGPoint(x: 0.5, y: 0.5)
enemyMiddle.position = CGPoint(x: 0, y: 0)
enemyMiddle.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width:
enemyMiddle.size.width - 5, height: enemyMiddle.size.height - 5))
enemyMiddle.physicsBody?.categoryBitMask = ColliderType.Enemy
enemyLeft.physicsBody?.collisionBitMask = 0
enemyMiddle.physicsBody?.affectedByGravity = false
enemyRight.name = "Enemy"
enemyRight.anchorPoint = CGPoint(x: 0.5, y: 0.5)
enemyRight.position = CGPoint(x: -200, y: 0)
enemyRight.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width:
enemyRight.size.width - 5, height: enemyRight.size.height - 5))
enemyRight.physicsBody?.categoryBitMask = ColliderType.Enemy
enemyLeft.physicsBody?.collisionBitMask = 0
enemyRight.physicsBody?.affectedByGravity = false
enemyHolder.zPosition = 2
enemyHolder.position.y = self.frame.height + 100
enemyHolder.position.x = CGFloat.randomBetweenNumbers(firstNum:
-100, secondNum: 100)
enemyHolder.addChild(enemyLeft)
enemyHolder.addChild(enemyMiddle)
enemyHolder.addChild(enemyRight)
self.addChild(enemyHolder)
let destination = self.frame.height * 4
let move = SKAction.moveTo(y: -destination, duration: 9)
let remove = SKAction.removeFromParent()
enemyHolder.run(SKAction.sequence([move, remove]), withKey:
"MoveEnemies")
}
func spawnEnemies() {
let spawn = SKAction.run({ () -> Void in
self.createRedEnemies()
})
let delay = SKAction.wait(forDuration: 0.4)
let sequence = SKAction.sequence([spawn, delay])
self.run(SKAction.repeatForever(sequence), withKey: "SpawnEnemies")
}
Here is the code I have for the shifted functions that I add in the "didMove":
func shiftEnemies() {
if score >= 0 && score <= 10 {
spawnEnemies()
} else if score >= 11 && score <= 20 {
spawnPipes()
} else if score >= 21 && score <= 30 {
spawnEnemies()
} else if score >= 31 && score <= 40 {
spawnPipes()
}
}
Two problems with the "shiftedEnemies()". The first one is obvious, I cant write a code for every 10 score. The second problem is that this code doesn't even work. "SpawnEnemies()" is the only function that is shown. "spawnPipes()" doesn't show, ever. Maybe problem number two will be solved when I fix problem number 1.
Thx guys!
the reason your SpawnEnemies function is the only function that is being called is because you put the function shiftEnemies in the didMove(toView:) method and didMove(toView:) only gets called one time when you present your scene
what i recommend is try calling the function shiftEnemies() in the part of code where the score is being added (most likely in your didBeginContact method)
So you want to start by calling spawnenemies and when the score goes past a multiple of 10, switch to calling spawnPipes and then back to spawnEnemies at the next multiple of 10 etc?
Simply have a bool called shouldSpawnEnemies:
var shouldSpawnEnemies = true // Start by spawning enemies
(if you want to start by spawning pipes, initialise this to false).
Initialise the score at which functions should switch:
var switchFunctionScore = 10
Put a property watcher on your score. When the score passes the 'switch' score, set the bool indicating which function to use to false. Then set the next score at which functions should be switched.
var score : int = 0 {
didSet {
if (score >= switchFunctionScore) && oldValue < switchFunctionScore) {
shouldSpawnEnemies = !shouldSpawnEnemies
switchFunctionScore += 10
}
}
Then, whenever you need to call one of these functions; just check the value of shouldSpawnEnemies:
if shouldSpawnenemies {
spawnEnemies
} else {
spawnPipes
}
I would avoid using Timer, Timer works outside of the SpriteKit time system, so for me to cheat in your game, I could constantly exit and return the app, and since Timer is based on real time and not game time, the time that is spent outside of the game will still be accounted for.
What you want to do is use SKAction wait(duration:), 'sequence, 'run(_ block:) repeatForever and repeat(_:count)
To do this, you need to break it down into steps:
1st, we want to wait 1 second and fire function 1:
let wait1Sec = SKAction.wait(duration:1)
let function1 = SKAction.run({[weak self] in self?.function1()})
let seq1 = SKAction.sequence([wait1Sec,function1])
2nd, we want to create an action that repeats it 10 times:
let repeat1 = SKAction.repeat(seq1,count:10)
3rd, we want to do this again for function 2:
let function2 = SKAction.run({[weak self] in self?.function2()})
let seq2 = SKAction.sequence([wait1Sec,function2])
let repeat2 = SKAction.repeat(seq2,count:10)
Finally, we want to combine the 2 and run it indefinetely
let seqForever = SKAction.sequence([repeat1,repeat2])
let repeatForever = SKAction.repeatForever(seqForever)
Now that we have the action, we can attach it to the scene once
scene.run(repeatForever,withKey:"forever")
You now have a solution that will constantly fire a method 10 times in 10 seconds, then switch to the other function for 10 more times in 10 seconds, repeating forever.
override func didMove(to view:SKView)
{
let wait1Sec = SKAction.wait(duration:1)
let function1 = SKAction.run({[weak self] in self?.function1()})
let seq1 = SKAction.sequence([wait1Sec,function1])
let repeat1 = SKAction.repeat(seq1,count:10)
let function2 = SKAction.run({[weak self] in self?.function2()})
let seq2 = SKAction.sequence([wait1Sec,function2])
let repeat2 = SKAction.repeat(seq2,count:10)
let seqForever = SKAction.sequence([repeat1,repeat2])
let repeatForever = SKAction.repeatForever(seqForever)
scene.run(repeatForever,withKey:"forever")
}

Randomly generating two nodes

I am creating a mobile application in Swift 3's SpriteKit. I am trying to create two rocks that a character has to dodge. I have the random generation working with ->
func createTopRock(){
var topRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rockDown")), SKTexture(image: #imageLiteral(resourceName: "rockGrassDown")), SKTexture(image: #imageLiteral(resourceName: "rockSnowDown")), SKTexture(image: #imageLiteral(resourceName: "rockIceDown"))]
let topRock = SKSpriteNode.init(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.zPosition = -9
topRock.name = "TopRock"
topRock.position = CGPoint(x: self.frame.width + topRock.size.width * 2, y: frame.maxY - topRock.frame.height / 2)
topRock.physicsBody = SKPhysicsBody(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
topRock.physicsBody?.collisionBitMask = physicsCatagory.plane
topRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
topRock.physicsBody?.affectedByGravity = false
topRock.physicsBody?.isDynamic = false
self.addChild(topRock)
let randomNumTop = arc4random_uniform(3) + 3
spawnDelayForeverTop = Timer.scheduledTimer(timeInterval: TimeInterval(randomNumTop), target: self, selector: #selector(self.createTopRock), userInfo: nil, repeats: false)
}
func createBtmRock(){
var btmRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rock")), SKTexture(image: #imageLiteral(resourceName: "rockGrass")), SKTexture(image: #imageLiteral(resourceName: "rockSnow")), SKTexture(image: #imageLiteral(resourceName: "rockIce"))]
let btmRock = SKSpriteNode.init(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.zPosition = -9
btmRock.position = CGPoint(x: self.frame.width, y: frame.minY + btmRock.frame.height / 2)
btmRock.name = "BtmRock"
btmRock.physicsBody = SKPhysicsBody(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
btmRock.physicsBody?.collisionBitMask = physicsCatagory.plane
btmRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
btmRock.physicsBody?.affectedByGravity = false
btmRock.physicsBody?.isDynamic = false
self.addChild(btmRock)
let randomNumBtm = arc4random_uniform(2) + 1
spawnDelayForeverBtm = Timer.scheduledTimer(timeInterval: TimeInterval(randomNumBtm), target: self, selector: #selector(createBtmRock), userInfo: nil, repeats: false)
}
As you can see from the code everything works. The one thing I do not want to do is have it like Flappy Bird and have the btmRock and topRock to have the same position, so to prevent that I have added the timeIntervals to be different. This works to an extent; but the player still runs into some rocks that are nearly impossible to go through.
I feel like I am going about this wrong and I don't know how to fix this. I want them to be close enough sometimes to be hard but not nearly impossible. When I tried to implement a bool system to when a top rock is created and is within a certain distance don't create a bottom rock. What I ran into trying to do this is that I would never have a bottom rock after that occurred. Thanks in advance.
Personally I wouldn't use Timer in my SpriteKit game (for reasons that you can find all over SO). A very simple solution could be to call the generation of the rocks from your Update func.
I would also create a base bottom rock and base top rock and make copies of it as needed, because creating physics objects on the fly can get expensive (or I would create an array of the objects at start and pull from the array as needed)
P.S. I wrote this off the cuff, so I cannot guarantee typo's don't exist ;)
var genInterval = 4 //How many seconds between rocks on bottom or top (not between top & bottom. that will be half of this value)
var genOffset = genInterval / 2 //offset the bottom rocks half of the interval so that they do't line up
var bottomRock: SKSpriteNode! //your base bottom rock for copying
var topRock: SKSpriteNode! //your base top rock for copying
var updateTopTime: Double = 0
var updateBottomTime: Double = 0
func setupRocks() {
//create the base bottom rock
var btmRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rock")), SKTexture(image: #imageLiteral(resourceName: "rockGrass")), SKTexture(image: #imageLiteral(resourceName: "rockSnow")), SKTexture(image: #imageLiteral(resourceName: "rockIce"))]
bottomRock = SKSpriteNode(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.zPosition = -9
btmRock.position = CGPoint(x: self.frame.width, y: frame.minY + btmRock.frame.height / 2)
btmRock.name = "BtmRock"
btmRock.physicsBody = SKPhysicsBody(texture: btmRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
btmRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
btmRock.physicsBody?.collisionBitMask = physicsCatagory.plane
btmRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
btmRock.physicsBody?.affectedByGravity = false
btmRock.physicsBody?.isDynamic = false
//create the base top rock
var topRockChoice = [SKTexture(image: #imageLiteral(resourceName: "rockDown")), SKTexture(image: #imageLiteral(resourceName: "rockGrassDown")), SKTexture(image: #imageLiteral(resourceName: "rockSnowDown")), SKTexture(image: #imageLiteral(resourceName: "rockIceDown"))]
topRock = SKSpriteNode(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.zPosition = -9
topRock.name = "TopRock"
topRock.position = CGPoint(x: self.frame.width + topRock.size.width * 2, y: frame.maxY - topRock.frame.height / 2)
topRock.physicsBody = SKPhysicsBody(texture: topRockChoice[mapChoice], size: CGSize(width: (self.scene?.size.width)! / 10, height: (self.scene?.size.height)! / 2.2))
topRock.physicsBody?.categoryBitMask = physicsCatagory.topRock
topRock.physicsBody?.collisionBitMask = physicsCatagory.plane
topRock.physicsBody?.contactTestBitMask = physicsCatagory.plane
topRock.physicsBody?.affectedByGravity = false
topRock.physicsBody?.isDynamic = false
}
override func update(_ currentTime: CFTimeInterval) {
//optional prevents generation if game is not playing
guard gameState == .playing else { return }
if updateTopTime == 0 {
updateTopTime = currentTime
}
if updateBottomTime == 0 {
updateBottomTime = currentTime
}
if currentTime - updateBottomTime > genOffset {
createBtmRock()
genOffset = genInterval
updateBottomTime = currentTime
}
else if currentTime - updateTopTime > genInterval {
createTopRock()
updateTopTime = currentTime
}
}
func createTopRock() {
//You can make this number a class variable to increase the rate as the game progresses
let randomNum = arc4random_uniform(3)
//there is a 1 in 3 chance that this rock will get created
if randomNum == 1 {
let rock = topRock.copy as! SKSpriteNode()
self.addChild(rock)
}
}
func createBtmRock() {
//You can make this number a class variable to increase the rate as the game progresses
let randomNum = arc4random_uniform(2)
//there is a 1 in 2 chance that this rock will get created
if randomNum == 0 {
let rock = bottomRock.copy as! SKSpriteNode()
self.addChild(rock)
}
}
The logic in your final paragraph seems like a plausible solution. You will need to implement some kind of custom logic to prevent a top and bottom rock from spawning within some set distance.
Inside your createBtmRock function, when you set the new rock's position, check to see if it is close to any existing top rocks. You can determine closeness by comparing the position property of the new rock to all existing rocks.
If you find the new rock would be too close to existing rocks, don't add the new rock to the scene, and continue the timer to make sure you generate bottom rocks in the future.
This is a simple solution to your immediate problem. You may want to look into procedural generation strategies for generating your rocks. If you want to refine the behavior later (by making rocks harder to pass as the game progresses, for example), you will need a more permanent solution.
Consider creating a separate function that will generate positions for your rocks. You can handle all of the position generation logic separately and use the result to position your rocks.
func generateRockPositions() -> [CGPoint]
You could do this in advance when the user starts the game, or generate positions as the game progresses. This will allow you to make changes to your procedural generation algorithm in the future more easily.
Hopefully that helps, best of luck with your game!

Getting different colour backgrounds to scroll up

I am coding for my A2 coursework project, and making a well-known game 'Fall Down'. However i am trying to get the background to change every cycle while it scrolls - so it changes from blue to red to yellow etc. However when I run it, this only works for the first two colours, then flashes back to the first colour. Here is the code i currently have
var background = SKSpriteNode()
override func didMoveToView(view: SKView) {
let blueTexture = SKTexture(imageNamed: "BlueBackground")
let redTexture = SKTexture(imageNamed: "RedBackground")
let yellowTexture = SKTexture(imageNamed: "YellowBackground")
let greenTexture = SKTexture(imageNamed: "GreenBackground")
let purpleTexture = SKTexture(imageNamed: "PurpleBackground")
let TextureArray = [blueTexture, redTexture, yellowTexture, greenTexture, purpleTexture]
let levelProgress = SKAction.moveByX(0, y:blueTexture.size().height, duration: 5)
let newLevel = SKAction.moveByX(0 , y: -blueTexture.size().height, duration: 0)
let sequenceForever = SKAction.repeatActionForever(SKAction.sequence([ levelProgress, newLevel]))
var a = 0
for var i: CGFloat = 0; i<5; i++ {
let currentBG = TextureArray[a]
a++
background = SKSpriteNode(texture: currentBG)
background.position = CGPoint(x: CGRectGetMidX(self.frame), y: -blueTexture.size().height/2 + blueTexture.size().height * i)
background.size.width = self.frame.width
background.runAction(sequenceForever)
self.addChild(background)
}
if anyone could point me in the right direction or if any more information is needed let me know! This is my first post so any posting advice would be great too.
It might by because of this:
let levelProgress = SKAction.moveByX(0, y:blueTexture.size().height, duration: 5)
let newLevel = SKAction.moveByX(0 , y: -blueTexture.size().height, duration: 0)
let sequenceForever = SKAction.repeatActionForever(SKAction.sequence([ levelProgress, newLevel]))
background.position = CGPoint(x: CGRectGetMidX(self.frame), y: -blueTexture.size().height/2 + blueTexture.size().height * i
you always use the blueTexture.size. try to change it to a constant value.

Spawn Balls random position out of the screen

I would like to bring up enemy (var enemis) from outside the screen whether the top, bottom, left and right of the screen. And these enemy have a random direction in tranversant the screen. For the moment, my code do spawning enemy out of the screen top, bottom, left and right but with one direction only and I want make a random direction
func CreationEnnemis(){
let Enemis = SKSpriteNode(imageNamed: "Meteroites.png")
let choixDeCote = arc4random() % 4 + 1
switch choixDeCote {
case 1 : //Haut
let MinValue = self.size.width / 8
let MaxValue = self.size.width - 200
SpawnX = UInt32(MaxValue - MinValue)
SpawnX = arc4random_uniform(SpawnX)
SpawnY = UInt32(self.size.height)
break
case 2 ://Bas
let MinValue = self.size.width / 8
let MaxValue = self.size.width - 200
SpawnX = UInt32(MaxValue - MinValue)
SpawnX = arc4random_uniform(SpawnX)
SpawnY = UInt32(self.size.height) - UInt32(self.size.height)
break
case 3 : //Gauche
let MinValue = self.size.height / 8
let MaxValue = self.size.height - 200
SpawnX = 0
SpawnY = UInt32(MaxValue - MinValue)
SpawnY = arc4random_uniform(SpawnY)
break
case 4 ://Droite
let MinValue = self.size.height / 8
let MaxValue = self.size.height - 200
SpawnX = UInt32(self.size.width)
SpawnY = UInt32(MaxValue - MinValue)
SpawnY = arc4random_uniform(SpawnY)
break
default :
break
}
Enemis.position = CGPoint(x: CGFloat(SpawnX), y: CGFloat(SpawnY))
Enemis.setScale(4)
Enemis.physicsBody = SKPhysicsBody(rectangleOfSize: Enemis.size)
Enemis.physicsBody?.affectedByGravity = false
Enemis.physicsBody?.dynamic = true
let action = SKAction.moveTo(CGPoint(x: -50,y: -10),duration: 2.5)
let actionFini = SKAction.removeFromParent()
Enemis.runAction(SKAction.sequence([action, actionFini]))
Enemis.runAction(SKAction.repeatActionForever(action))
self.addChild(Enemis)
}
This is just an example to give you an idea how you can spawn enemies at random positions and move them in random directions. I don't use Swift extensively, and this is more like just to show you at which direction you can go, and how to solve the problem. I left to you to care about Swift 2 syntax :D Also, I am currently on outdated version of Swift, so not sure what works for me, will work for you, but the logic is the same.
Here you will see how you can:
spawn a node and move it to the opposite side of a screen
move a node to the random point of the opposite side of a screen
randomize duration of spawning
create a random point along the one of the screen's borders
create a random number between two numbers
using SKAction to do all this
One thing which is important here is how to use strong reference to self inside closure. Because of my Swift version, as I said, what works for me, probably will not work for you, but the logic is the same. Read more here about strong reference cycles if interested :
Shall we always use [unowned self] inside closure in Swift
Always pass weak reference of self into block in ARC?
What is the difference between a weak reference and an unowned reference?
Here is an code example:
import SpriteKit
class GameScene:SKScene, SKPhysicsContactDelegate{
override func didMoveToView(view: SKView) {
self.physicsWorld.contactDelegate = self
createEnemies()
}
deinit{
print("deinit called")
}
func randomBetweenNumbers(firstNum: CGFloat, secondNum: CGFloat) -> CGFloat{
return CGFloat(arc4random()) / CGFloat(UINT32_MAX) * abs(firstNum - secondNum) + min(firstNum, secondNum)
}
//Helper method for spawning a point along the screen borders. This will not work for diagonal lines.
func randomPointBetween(start:CGPoint, end:CGPoint)->CGPoint{
return CGPoint(x: randomBetweenNumbers(start.x, secondNum: end.x), y: randomBetweenNumbers(start.y, secondNum: end.y))
}
func createEnemies(){
//Randomize spawning time.
//This will create a node every 0.5 +/- 0.1 seconds, means between 0.4 and 0.6 sec
let wait = SKAction .waitForDuration(0.5, withRange: 0.2)
weak var weakSelf = self //Use weakSelf to break a possible strong reference cycle
let spawn = SKAction.runBlock({
var random = arc4random() % 4 + 1
var position = CGPoint()
var moveTo = CGPoint()
var offset:CGFloat = 40
println(random)
switch random {
//Top
case 1:
position = weakSelf!.randomPointBetween(CGPoint(x: 0, y: weakSelf!.frame.height), end: CGPoint(x: weakSelf!.frame.width, y: weakSelf!.frame.height))
//Move to opposite side
moveTo = weakSelf!.randomPointBetween(CGPoint(x: 0, y: 0), end: CGPoint(x:weakSelf!.frame.width, y:0))
break
//Bottom
case 2:
position = weakSelf!.randomPointBetween(CGPoint(x: 0, y: 0), end: CGPoint(x: weakSelf!.frame.width, y: 0))
//Move to opposite side
moveTo = weakSelf!.randomPointBetween(CGPoint(x: 0, y: weakSelf!.frame.height), end: CGPoint(x: weakSelf!.frame.width, y: weakSelf!.frame.height))
break
//Left
case 3:
position = weakSelf!.randomPointBetween(CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: weakSelf!.frame.height))
//Move to opposite side
moveTo = weakSelf!.randomPointBetween(CGPoint(x: weakSelf!.frame.width, y: 0), end: CGPoint(x: weakSelf!.frame.width, y: weakSelf!.frame.height))
break
//Right
case 4:
position = weakSelf!.randomPointBetween(CGPoint(x: weakSelf!.frame.width, y: 0), end: CGPoint(x: weakSelf!.frame.width, y: weakSelf!.frame.height))
//Move to opposite side
moveTo = weakSelf!.randomPointBetween(CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: weakSelf!.frame.height))
break
default:
break
}
weakSelf!.spawnEnemyAtPosition(position, moveTo: moveTo)
})
let spawning = SKAction.sequence([wait,spawn])
self.runAction(SKAction.repeatActionForever(spawning), withKey:"spawning")
}
func spawnEnemyAtPosition(position:CGPoint, moveTo:CGPoint){
let enemy = SKSpriteNode(color: SKColor.brownColor(), size: CGSize(width: 40, height: 40))
enemy.position = position
enemy.physicsBody = SKPhysicsBody(rectangleOfSize: enemy.size)
enemy.physicsBody?.affectedByGravity = false
enemy.physicsBody?.dynamic = true
enemy.physicsBody?.collisionBitMask = 0 // no collisions
//Here you can randomize the value of duration parameter to change the speed of a node
let move = SKAction.moveTo(moveTo,duration: 2.5)
let remove = SKAction.removeFromParent()
enemy.runAction(SKAction.sequence([move, remove]))
self.addChild(enemy)
}
func didBeginContact(contact: SKPhysicsContact) {
}
/*
Added for debugging purposes
override func touchesBegan(touches: NSSet, withEvent event: UIEvent?) {
//Just make a transition to the other scene, in order to check if deinit is called
//You have to make a new scene ... I named it WelcomeScene
var scene:WelcomeScene = WelcomeScene(fileNamed: "WelcomeScene.sks")
scene.scaleMode = .AspectFill
self.view?.presentScene(scene )
}
*/
}
And here is the result:
The important part is located in createEnemies() method:
//Top
case 1:
position = weakSelf!.randomPointBetween(CGPoint(x: 0, y: weakSelf!.frame.height), end: CGPoint(x: weakSelf!.frame.width, y: weakSelf!.frame.height))
//Move to opposite side
moveTo = weakSelf!.randomPointBetween(CGPoint(x: 0, y: 0), end: CGPoint(x:weakSelf!.frame.width, y:0))
break
Here you define spawning location, which can be any point along the top border. Or more precisely a little bit above top border. Nodes are spawned offscreen. And next, you create (randomize) a point where you would like to move a node, and that is an opposite side in compare to spawn location. So, that can be any random point along bottom border.
If you want to stop spawning, you will do this:
if(self.actionForKey("spawning") != nil){
self.removeActionForKey("spawning")
}
About your physics bodies setup... Note that I've set collisionBitMask of nodes to 0.
enemy.physicsBody?.collisionBitMask = 0 // no collisions
When moving nodes by actions in SpriteKit you are pulling them out of physics simulation and you can get unexpected behaviours if you are expecting to see realistic physics simulation. So, use actions only if you are not interested in collisions (or other sort of physics simulation), but rather just in contact detection. If you need collisions as well, use physics engine and move nodes by applying impulses or forces.
Hope this helps!
Thanks a lot !
I make a different version of your code because i found solution before your answer
func CreationMeteorites(){
let Meteorites = SKSpriteNode(imageNamed: "Meteroites.png")
let choixDeCote = arc4random() % 4 + 1
switch choixDeCote {
case 1 : //Haut
let MinValue = self.size.width / 8
let MaxValue = self.size.width - 200
SpawnX = UInt32(MaxValue - MinValue)
SpawnX = arc4random_uniform(SpawnX)
SpawnY = UInt32(self.size.height)
directionX = Int(arc4random()) % Int(self.frame.size.width)
directionY = 0
action = SKAction.moveTo(CGPoint(x: CGFloat(directionX),y: CGFloat(directionY)),duration: 4)
break
case 2 ://Bas
let MinValue = self.size.width / 8
let MaxValue = self.size.width - 200
SpawnX = UInt32(MaxValue - MinValue)
SpawnX = arc4random_uniform(SpawnX)
SpawnY = 0
directionX = Int(arc4random()) % Int(self.frame.size.width)
directionY = Int(self.frame.size.height)
action = SKAction.moveTo(CGPoint(x: CGFloat(directionX),y: CGFloat(directionY)),duration: 4)
break
case 3 : //Gauche
let MinValue = self.size.height / 8
let MaxValue = self.size.height - 200
SpawnX = 0
SpawnY = UInt32(MaxValue - MinValue)
SpawnY = arc4random_uniform(SpawnY)
directionY = Int(arc4random()) % Int(self.frame.size.height)
directionX = Int(self.frame.size.width)
action = SKAction.moveTo(CGPoint(x: CGFloat(directionX),y: CGFloat(directionY)),duration: 3)
break
case 4 ://Droite
let MinValue = self.size.height / 8
let MaxValue = self.size.height - 200
SpawnX = UInt32(self.size.width)
SpawnY = UInt32(MaxValue - MinValue)
SpawnY = arc4random_uniform(SpawnY)
directionY = Int(arc4random()) % Int(self.frame.size.height)
directionX = 0
action = SKAction.moveTo(CGPoint(x: CGFloat(directionX),y: CGFloat(directionY)),duration: 3)
break
default :
break
}
//Positioner les météorites
Meteorites.position = CGPoint(x: CGFloat(SpawnX), y: CGFloat(SpawnY))
Meteorites.setScale(4)
Meteorites.physicsBody = SKPhysicsBody(circleOfRadius: 30)
Meteorites.physicsBody?.affectedByGravity = false
Meteorites.physicsBody?.dynamic = true
Meteorites.physicsBody?.categoryBitMask = PhysicsCategories.Meteorites
Meteorites.physicsBody?.contactTestBitMask = PhysicsCategories.Meteorites
let actionFini = SKAction.removeFromParent()
Meteorites.runAction(SKAction.sequence([action, actionFini]))
Meteorites.runAction(SKAction.repeatActionForever(action))
self.addChild(Meteorites)
}
And about the collisions do you know a tutorial with a good explain because i don't understand how make collisions.
For anyone that is interested to do this in objective C inside GameScene:
-(void) randomSpawnPosition{
NSUInteger randPos = arc4random_uniform(4);
CGPoint spawnPosition;
CGFloat randFloatX = arc4random_uniform(self.frame.size.width + 10);
CGFloat randFloatY = arc4random_uniform(self.frame.size.height + 10);
switch (randPos) {
//top
case 1:
spawnPosition = CGPointMake(randFloatX, self.frame.size.height+10);
break;
//bottom
case 2:
spawnPosition = CGPointMake(randFloatX, 0-10);
break;
//left
case 3:
spawnPosition = CGPointMake(0 - 10, randFloatY);
break;
//right
case 4:
spawnPosition = CGPointMake(self.frame.size.width + 10, randFloatY);
break;
}
[self addEnemy:spawnPosition];
}