How to properly copy SKTileMapNode in swift - swift

When I run loop on node everything works fine, node moving correct. But when i run loop on next action interrupts when next reaches left border of the screen (halfway). I assume something wrong with copy. Thanks!
func moveBackground (name: String, speed: CGFloat = 0.01) {
guard let node = childNode(withName: name) as? SKTileMapNode else {
fatalError("\(name) node not loaded")
}
let width = node.frame.size.width
let startPositionX = width
node.position = CGPoint(x: startPositionX , y: 0)
let next = node.copy() as! SKTileMapNode
next.tileSet = node.tileSet
next.position = CGPoint(x: startPositionX , y: 0)
self.addChild(next)
let distance = width * 2
let duration = TimeInterval(speed * distance)
let moveAction = SKAction.moveBy(x: -distance, y: 0, duration: duration)
let resetAction = SKAction.moveTo(x: startPositionX, duration: 0)
let sequence = SKAction.sequence([moveAction, resetAction])
let loop = SKAction.repeatForever(sequence)
//node.run(loop)
next.run(loop)
}

Related

Creating a scnplain that is the same size as half of the screen using SceneKit

I am trying to figure out how to create a plainNode in SceneKit that takes up exactly half of the screen.
So I found this routine to projectValues that seems correct.
extension CGPoint {
func scnVector3Value(view: SCNView, depth: Float) -> SCNVector3 {
let projectedOrigin = view.projectPoint(SCNVector3(0, 0, depth))
return view.unprojectPoint(SCNVector3(Float(x), Float(y), projectedOrigin.z))
}
}
And I fed these values into it...
let native = UIScreen.main.bounds
let maxMax = CGPoint(x: native.width, y: native.height * 0.5)
let newPosition1 = maxMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition1)")
let minMin = CGPoint(x: 0, y: 0)
let newPosition2 = minMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition2)")
let minMax = CGPoint(x: 0, y: native.height * 0.5)
let newPosition3 = minMax.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition3)")
let maxMin = CGPoint(x: native.width, y: 0)
let newPosition4 = maxMin.scnVector3Value(view: view, depth: Float(0))
print("newPosition \(newPosition4)")
// approximations that look almost correct, but they are not...
let width = (maxMax.x - minMin.x) / 100 * 2
let height = (maxMax.y - minMin.y) / 100 * 2
let plainGeo = SCNPlane(width: width, height: height)
let planeNode = SCNNode(geometry: plainGeo)
planeNode.geometry?.firstMaterial?.diffuse.contents = UIColor.red
view.scene.rootNode?.addChildNode(planeNode)
But it isn't right? What am I doing wrong here?

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

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

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.