SpriteKit - correct location of sprites during SKAction - swift

I'm writing an app with a continuously (sdieways) scrolling terrain, which is made up dynamically of images. So I create a sprite, scroll it sideways, & when it's about to show its right edge as it continues to scroll, I add a second sprite to its right, which also scrolls, etc, etc. As they disappear off the left, they're removed.
The problem is that when I add the new sprite, there's a small gap (around 4 px) between it & the previous one, as if in the time it takes to put it on screen, the scroll animation has moved a few pixels.
The scrolling class is this -
import UIKit
import SpriteKit
private let lagOffset: CGFloat = 4.0 // currently, this is the only way I can get rid of the gap between terrain blocks
class GameScene: SKScene
{
private var terrainNumber = 2
private var followingTerrainBlock: TerrainBlock? = .None
override init(size: CGSize)
{
super.init(size: size)
// create the first terrain block
let terrainBlock = TerrainBlock(size: Size.terrainBlock, terrainNumber: 1)
terrainBlock.position = CGPoint(x: size.width / 2.0 + Size.terrainBlock.width, y: size.height / 2.0)
addChild(terrainBlock)
followingTerrainBlock = terrainBlock
moveTerrainBlock(terrainBlock)
}
required init?(coder aDecoder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
func moveTerrainBlock(terrainBlock: TerrainBlock)
{
// create the next terrain block on a background thread as it's quite expensive
// it'll be ready in plenty of time for when it's needed
GCD.async { () -> () in
self.followingTerrainBlock = TerrainBlock(size: Size.terrainBlock, terrainNumber: self.terrainNumber)
}
var targetX = size.width / 2.0
let y = terrainBlock.position.y
let move = SKAction.moveTo(CGPoint(x: targetX, y: y), duration: Time.terrainScroll)
// move the terrain block to centre on the screen - at this point we add the following terrain block immediately behind it, off-screen
// the scroll animation lasts 5 seconds before the completion closure runs, by which time self.followingTerrainBlock has been ready for ages
terrainBlock.runAction(move) { [weak self]() -> Void in
if let strongSelf = self
{
strongSelf.terrainNumber++
// we have to put an offset into the position of the new terrain block, or there'll be a gap between the old & new blocks
strongSelf.followingTerrainBlock!.position = CGPoint(x: targetX + Size.terrainBlock.width - lagOffset, y: y)
strongSelf.addChild(strongSelf.followingTerrainBlock!)
// now we tell the new terrain block to follow the existing one
strongSelf.moveTerrainBlock(strongSelf.followingTerrainBlock!)
// then we continue moving the existing terrain block until it's off-screen, then remove the sprite
targetX -= Size.terrainBlock.width - lagOffset
let move = SKAction.moveTo(CGPoint(x: targetX, y: y), duration: Time.terrainScroll)
terrainBlock.runAction(move) { () -> Void in
terrainBlock.removeFromParent()
}
}
}
}
}
What I'd like to know is how to put the following terrain blocks on screen so that they're exactly lined up with the right edge of the previous one. If I stop the animation & add one, the position is correct without the offset, so there's nothing wrong with the position calculation.

As you create each block, create a constraint (with constant = 0) attaching it to previous block...etc for each block there after.
I believe that as each block moves off the view the constraint will dealloc as well, but you might have to consider this part too.

Related

Creating endless background without image

I'm creating a simple game with Swift and SpriteKit.
I want to add an endless background (vertically), I only found answers for background with images but I need to do it without image, only background color.
I thought about checking if the player is in the frame.maxY, if so, to move it back to the starting point, but I was wondering if there is a better idea.
//Does not matter which ring we chose as all ring's 'y' position is the same.
func moveBackgroundUp(){
if ((mPlayer.position.y >= self.frame.maxY) || (mRingOne.position.y >= self.frame.maxY)) {
mPlayer.position.y = 150 //mPlayers original starting point.
for ring in mRings {
ring.position.y = 350
}
}
}
Thanks in advance!
Don't just move a background up the screen, that' really isn't the way to go about it. What you should do is detect the position of the camera (assuming it moves with the player), and when it's position is about to occupy space outside of the occupied space of your current background sprite, add a new background sprite to the scene where the last one left off. Here is an example of how to do that with just a red sprite:
First add a property to the scene to track level position:
// To track the y-position of the level
var levelPositionY: CGFloat = 0.0
Now create a method to update your background:
func updateBackground() {
let cameraPos = camera!.position
if cameraPos.y > levelPositionY - (size.height * 0.55) {
createBackground()
}
}
func createBackground() {
// Create a new sprite the size of your scene
let backgroundSprite = SKSpriteNode(color: .red, size: size)
backgroundSprite.anchorPoint = CGPoint(x: 0.5, y: 0)
backgroundSprite.position = CGPoint(x: 0, y: levelPositionY)
// Replace backgroundNode with the name of your backgroundNode to add the sprite to
backgroundNode.addChild(backgroundSprite)
levelPositionY += backgroundSprite.size.height
}
Now you want to call updateBackground inside your overridden update(_:) method:
override func update(_ currentTime: TimeInterval) {
// All your other update code
updateBackground()
}
Also, make sure to create an initial background when you first create the scene:
override func didMove(to view: SKView) {
createBackground()
}
NOTE! - It's important to set the custom anchor point for the background sprite for this code to work properly. An anchor of (0.5, 0) allows the background sprite to be anchored in the middle of the scene on the x-axis, but at the bottom of the scene on the y-axis. This allows you to easily stack one on top of the other.
EDIT - I forgot to mention that it's also a good idea to conserve resources and remove any background nodes that are outside the viewable area and won't be coming back in (i.e. a continuous scrolling game where you can't go backwards). You could do that by updating your updateBackground method above:
func updateBackground() {
let cameraPos = camera!.position
if cameraPos.y > levelPositionY - (size.height * 0.55) {
createBackground()
}
// Make sure to change 'backgroundNode' to whatever the name of your backgroundNode is.
for bgChild in backgroundNode.children {
// This will convert the node's coordinates to scene's coordinates. See below for this function
let nodePos = fgNode.convert(fgChild.position, to: self)
if !isNodeVisible(bgChild, positionY: nodePos.y) {
// Remove from it's parent node
bgChild.removeFromParent()
}
}
}
func isNodeVisible(_ node: SKNode, positionY: CGFloat) -> Bool {
if !camera!.contains(node) {
if positionY < camera!.position.y - size.height * 2.0 {
return false
}
}
return true
}
So above you just loop through all the children inside your background node and detect if they are out of view, and if so remove them from the parent. Make sure to change my generic backgroundNode to whatever the name of your background node is.

Why are objects in the same SKNode layer not interacting with each other?

I have less than 1 year using SpriteKit so I didn't use SKNodes as layers before until recently.
I have an SKNode layer that holds all of the fish and the user's position, for example:
var layerMainGame = SKNode()
layerMainGame.zPosition = 50
layerMainGame.addChild(userPosition)
layerMainGame.addChild(pipFish)
addChild(layerMainGame)
The interaction whether the user touched a fish or not is handled with this function, which is basically checking if their frames crossed:
if CGRectIntersectsRect(CGRectInset(node.frame, delta.dx, delta.dy), self.userPosition.frame) {
print("You got hit by \(name).")
gameOver()
}
It works. The interaction between the userPosition and pipFish works. What doesn't work is fish that are added as the game progresses. I have a function spawning different types of fish in intervals like this:
func spawnNew(fish: SKSpriteNode) {
layerMainGame.addChild(fish)
}
The interaction between the user and those fish that get added to the same layer later in the game does not work. I can pass right through them and no game over happens. When I completely remove the entire layerMainGame variable and just add them to the scene like normal, all the interactions work. Adding them all to the same SKNode layer doesn't work.
This is the function that creates a hit collision for every fish.
func createHitCollisionFor(name: String, GameOver gameOver: String!, delta: (dx: CGFloat, dy: CGFloat), index: Int = -1) {
enumerateChildNodesWithName(name) { [unowned me = self] node, _ in
if CGRectIntersectsRect(CGRectInset(node.frame, delta.dx, delta.dy), self.userPosition.frame) {
me.gameOverImage.texture = SKTexture(imageNamed: gameOver)
didGetHitActions()
me.runAction(audio.playSound(hit)!)
if index != -1 {
me.trophySet.encounterTrophy.didEncounter[index] = true
}
print("You got hit by \(name).")
}
}
}
And I call it like this:
createHitCollisionFor("GoldPiranha", GameOver: model.gameOverImage["Gold"], delta: (dx: 50, dy: 50), index: 1)
It works when the fish are not in the layer, but doesn't work when they are added to the layer.
When a node is placed in the node tree, its position property places it within a coordinate system provided by its parent.
Sprite Kit uses a coordinate orientation that starts from the bottom left corner of the screen (0, 0), and the x and y values increase as you move up and to the right.
For SKScene, the default value of the origin – anchorPoint is (0, 0), which corresponds to the lower-left corner of the view’s frame rectangle. To change it to center you can specify (0.5, 0.5)
For SKNode, the coordinate system origin is defined by its anchorPoint which by default is (0.5, 0.5) which is center of the node.
In your project you have layerMainGame added for example to the scene, his anchorPoint is by default (0.5,0.5) so the origin for the children like your fish is the center, you can see it if you change the fish positions like:
func spawnNew(fish: SKSpriteNode) {
layerMainGame.addChild(fish)
fish.position = CGPointZero // position 0,0 = parent center
}
Hope it help to understand how to solve your issue.
Update: (after your changes to the main question)
To help you better understand what happens I will give an example right away:
override func didMoveToView(view: SKView) {
var layerMainGame = SKNode()
addChild(layerMainGame)
let pipFish = SKSpriteNode(color: UIColor.yellowColor(), size: CGSizeMake(50,50))
pipFish.name = "son"
self.addChild(pipFish)
let layerPipFish = SKSpriteNode(color: UIColor.yellowColor(), size: CGSizeMake(50,50))
layerPipFish.name = "son"
layerMainGame.addChild(layerPipFish)
enumerateChildNodesWithName("son") { [unowned me = self] node, _ in
print(node)
}
}
Output:
Now I will simply change the line:
layerMainGame.addChild(layerPipFish)
with:
self.addChild(layerPipFish)
Output:
What happened?
As you can see enumerateChildNodesWithName written as your and my code print only childs directly added to self (because actually we launch enumerateChildNodesWithName which it is equal to launch self.enumerateChildNodesWithName )
How can I search in the full node tree?
If you have a node named "GoldPiranha" then you can search through all descendants by putting a // before the name. So you would search for "//GoldPiranha":
enumerateChildNodesWithName("//GoldPiranha") { [unowned me = self] ...

How to generate a new node when another one hits a certain y value

I have a circle moving up a line, and when that circle reaches a certain y point, how can I make it so that another node would generate from below?
Here is the code I currently have for populating the circles, but I am not able to use it with a physics body, as it generates too many nodes and slows down my app:
func createCirclesOnLine(line: CGFloat) {
var currentY : CGFloat = -110
let maxY = self.size.width * 15
let spacing : CGFloat = 120
while currentY < maxY {
let circle = SKSpriteNode(imageNamed: "first#2x")
circle.physicsBody?.dynamic = false
circle.position = CGPointMake(line, currentY)
//circle.physicsBody?.restitution = -900
circle.size = CGSizeMake(75, 75)
// circle.physicsBody = SKPhysicsBody(rectangleOfSize: circle.size)
let up = SKAction.moveByX(0, y: 9000, duration: 90)
circle.runAction(up)
foregroundNode.addChild(circle)
currentY += CGFloat((random() % 400) + 70)
}
Will post more code if necessary.
There are two ways you can go about this. One is to simply check every circle's y position to see if it's above the screen. You'll need a reference to the circles so...
class GameScene: SKScene {
var circles = Array<SKSpriteNode>()
...
In your createCirlcesOnLine function, add each circle to the array as you create it.
...
self.addChild(circle)
circles.append(circle)
Then, in your update method, enumerate through the circles to see if any of them are above the top of the screen.
override func update(currentTime: NSTimeInterval) {
for circle in circles {
if circle.position.y > self.size.height + circle.size.height/2 {
//Send circle back to the bottom using the circle's position property
}
}
}
This solution will work but causes a lot of unnecessary checks on every update cycle.
A second more efficient (and slightly more complicated) recommendation is to add an invisible node above the top of the screen that stretches the screen width. When the circle collides with it, just move it to the bottom of the screen. Look into implementing the SKPhysicsContactDelegate protocol and what needs to happen for that to work. If you run into problems with this solution, post a separate question with those issues.

Swift SpriteKit: How to generate objects and make the game view move upwards as my player moves up the screen?

Im creating a game sort of like Doodle Jump where my player bounces off objects and moves upwards while scoring points.
How do I make the Y-axis move up infinitely and generate the same object, but in random positions as my player moves up?
I don't have any code for this, because I really need some parameters on how to do it and I'm a little new to this.
You create a subclass of SKSpriteNode, lets call it Bar.
class Bar: SKSpriteNode {
init(position p: CGPoint){
super.init(texture: nil, color: SKColor.greenColor(), size: CGSizeMake(50, 30))
position = p
name = "bar"
}
}
At the begin you can use arc4random_uniform() to generate x and y coordinates at which you place a bar element. (in your scene’s initializer)
class Scene: SKScene {
init(size: CGSize){
super.init(size: size)
placeBarAtRandomPositions();
}
func randomPoint(scene: CGRect) -> CGPoint {
let x = CGFloat(arc4random_uniform(UInt32(scene.width)))
let y = CGFloat(arc4random_uniform(UInt32(scene.height)))
return CGPoint(x: x, y: y)
}
}
You make the Y-axis infinitely by moving the bars down. You move down the bars for instance when the player is moving up. If a Bar moves out of the scene at the bottom you generate a new random position at the top where you insert a new Bar. Example:
override func didSimulatePhysics() {
enumerateChildNodesWithName("bar") { (node, _) -> () in
if(node.position.y < -node.size.height/2){ //Because a node’s origin is the center
node.remove()
self.spawnBarNodeAtTop()
}
}
}

Lag when trying to implement parallax effect in swift (spritekit)

I am trying to create game in SpriteKit with parallax effect, but when I implement my method, my hero lags (quickly shakes) from time to time (no pattern observed)...
I wrote this simple code that illustrates the method and also contains the same problem.
The hero should stay in the center of the screen and only his position relative to his parent (foreground) should change. But it lags (quickly shakes for a really short moment) every now and then. I also noticed that when I run the game, the hero sinks down very, very slowly a few pixels (maybe 5-10) before it starts holding its position after ~15 sec.
I can't figure out what is causing this behavior. Any help would be appreciated. I am testing on IPhone 6.
import SpriteKit
class GameScene: SKScene, SKPhysicsContactDelegate {
let hero = SKSpriteNode(imageNamed: "hero")
let foreground = SKNode()
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(size: CGSize) {
super.init(size: size)
physicsWorld.gravity = CGVector(dx: 0.0, dy: -0.5)
physicsWorld.contactDelegate = self
// anchor point of scene is at (x: 0.5, y: 0.5)
hero.position = CGPoint(x: 0, y: 0)
hero.physicsBody = SKPhysicsBody(rectangleOfSize: hero.size)
hero.physicsBody?.dynamic = true
hero.physicsBody?.allowsRotation = false
addChild(foreground)
foreground.addChild(hero)
}
override func update(currentTime: NSTimeInterval) {
foreground.position = CGPoint(x: 0, y: -(hero.position.y))
}
}
// I need the foreground to move in opposite direction so I can add stars,
// monsters, etc while keeping the player in the view... Update method does
// this: hero is pulled down by gravity so his y position would be -1 pixel
// relative to foreground but I assign foreground the opposite y-position of
// hero (+1) and since hero's position relative to foreground is -1, he should
// be positioned at y-position = 0 which, in this code example, should create
// an illusion that he is positioned at the center and doesn't move