Swift Spritekit Scrolling Background - ios

I am trying to find the best method to do a vertically scrolling background in Swift,
without using the Update Method. The Background is 1 Image that should loop infinite.
The Image should be in the middle of the screen (full-size), moving down, and on top of that image should always be another image spawned.
However, mine doesn't work quite right, it spawns 2 images, and than after a delay they pop up and start the Actions again.
Also, it breaks sometimes down from 30 FPS to 26, which I want to avoid.
I hope somebody, who is more into Swift can help me.
let Background = SKTexture(imageNamed: "Background.png")
Background.filteringMode = .Nearest
let BackgroundMove = SKAction.moveByX(0, y: -self.frame.size.height, duration: 10)
let BackgroundReset = SKAction.moveByX(0, y: self.frame.size.height, duration: 0.0)
let BackgroundMoveAndResetForever = SKAction.repeatActionForever(SKAction.sequence([BackgroundMove,BackgroundReset]))
for var i:CGFloat = 0; i < 2.0 + self.frame.size.height / (Background.size().height * 2); ++i {
let sprite = SKSpriteNode(texture: Background)
sprite.size = CGSize(width: self.frame.size.width / 2, height: self.frame.size.height)
sprite.position = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2 * i)
sprite.zPosition = 1
sprite.runAction(BackgroundMoveAndResetForever)
self.addChild(sprite)
}

This is the code I used to make a background that moves right to left across the screen. It used a loop to generate three backgrounds, all of which formed a line which passed through the screen and returned to their original position when they were done. To convert it to a screen that moves top to bottom, you would have to replace the moveByX SKActions to moveToY, and exchange changing x values with changing y values. Hope this helps!
func makeBackground() {
var backgroundTexture = SKTexture(imageNamed: "img/bg.png")
//move background right to left; replace
var shiftBackground = SKAction.moveByX(-backgroundTexture.size().width, y: 0, duration: 9)
var replaceBackground = SKAction.moveByX(backgroundTexture.size().width, y:0, duration: 0)
var movingAndReplacingBackground = SKAction.repeatActionForever(SKAction.sequence([shiftBackground,replaceBackground]))
for var i:CGFloat = 0; i<3; i++ {
//defining background; giving it height and moving width
background=SKSpriteNode(texture:backgroundTexture)
background.position = CGPoint(x: backgroundTexture.size().width/2 + (backgroundTexture.size().width * i), y: CGRectGetMidY(self.frame))
background.size.height = self.frame.height
background.runAction(movingAndReplacingBackground)
self.addChild(background)
}
}

I wrote this one... It's probably not as short as it could be, but it only uses 2 copies of the image, unlike the one posted above as best-answer. It also allows you to specify a direction (true to scroll right, false to scroll left) and a speed (indicates how many seconds it takes for an image to scroll across the screen, lower numbers = faster). It requires you to specify the view as well, and I haven't tested it with images that aren't the same size as the desired output (although I included scaling, so it probably works)
func scrollingBackground (view: SKView, rightward: Bool, speed: CGFloat) {
let background = SKSpriteNode(imageNamed: "space")
let background2 = SKSpriteNode(imageNamed: "space")
// variables to manage x position, will be set based on which
// direction this will be moving
var b1startX, b2startX, destination, resetPos: CGFloat
// Set values based on desired direction
if(rightward){
b1startX = frame.size.width * -0.5 // value to start offscreen
b2startX = frame.size.width * 0.5 // value to start on screen
destination = frame.size.width * 1.5 // position where image will disappear
resetPos = frame.size.width * -0.5 // position where image will reappear
}
else{
b1startX = frame.size.width * 1.5
b2startX = frame.size.width * 0.5
destination = frame.size.width * -0.5
resetPos = frame.size.width * 1.5
}
background.position = CGPoint(x: b1startX, y: frame.size.height / 2)
background.scale(to: view.bounds.size)
background.zPosition = -1
background2.position = CGPoint(x: b2startX, y: frame.size.height / 2)
background2.scale(to: view.bounds.size)
background2.zPosition = -1
// Define actions based on desired direction
let scrollFromMid = SKAction.moveTo(x: destination, duration: TimeInterval(speed))
let scrollFromLeft = SKAction.moveTo(x: destination, duration: TimeInterval(speed * 2.0))
let resetPosition = SKAction.moveTo(x: resetPos, duration: 0)
//background starts in the default starting position, totally offscreen
background.run(SKAction.repeatForever(SKAction.sequence([scrollFromLeft, resetPosition])))
// background2 starts fully displayed, moves offscreen, then into default loop
background2.run(SKAction.sequence([scrollFromMid, resetPosition,
SKAction.repeatForever(
SKAction.sequence([scrollFromLeft, resetPosition]))
]))
addChild(background)
addChild(background2)
}

when you create the BackgroundReset action you set the y change to a negative value, this will move the background further down the screen. Remove the minus sign to move the background back up the screen.
let BackgroundReset =
SKAction.moveByX(0, y: /* removed minus sign here */ self.frame.size.height * 2, duration: 0.0)

Related

Rotate a view from its origin towards another view

I am trying to rotate a view towards another views center point(Remember not around, its towards).
Assume I have 2 views placed like this
Now I want to rotate the topview to point the bottom view like this
so this what I did
Change the top views anchor point to its origin. so that it can rotate and point its edge to the bottom view
Calculated the angle between the first views origin point and the bottom views center
And applied the calculated transform to the top view.
Below the code I am using
let rect = CGRect(x: 70, y: 200, width: 300, height: 100)
let rectView = UIView(frame: rect)
rectView.layer.borderColor = UIColor.green.cgColor;
rectView.layer.borderWidth = 2;
let endView = UIView(frame: CGRect(x: 250, y: 450, width: 70, height: 70))
endView.layer.borderColor = UIColor.green.cgColor;
endView.layer.borderWidth = 2;
let end = endView.center;
self.view.addSubview(endView)
self.view.addSubview(rectView!)
rectView.setAnchorPoint(CGPoint.zero)
let angle = rectView.bounds.origin.angle(to: end);
UIView.animate(withDuration: 3) {
rectView.transform = rectView.transform.rotated(by: angle)
}
I am using this extension from Get angle from 2 positions to calculate the angle between 2 points
extension CGPoint {
func angle(to comparisonPoint: CGPoint) -> CGFloat {
let originX = comparisonPoint.x - self.x
let originY = comparisonPoint.y - self.y
let bearingRadians = atan2f(Float(originY), Float(originX))
var bearingDegrees = CGFloat(bearingRadians).degrees
while bearingDegrees < 0 {
bearingDegrees += 360
}
return bearingDegrees
}
}
extension CGFloat {
var degrees: CGFloat {
return self * CGFloat(180.0 / M_PI)
}
}
extension UIView {
func setAnchorPoint(_ point: CGPoint) {
var newPoint = CGPoint(x: bounds.size.width * point.x, y: bounds.size.height * point.y)
var oldPoint = CGPoint(x: bounds.size.width * layer.anchorPoint.x, y: bounds.size.height * layer.anchorPoint.y);
newPoint = newPoint.applying(transform)
oldPoint = oldPoint.applying(transform)
var position = layer.position
position.x -= oldPoint.x
position.x += newPoint.x
position.y -= oldPoint.y
position.y += newPoint.y
layer.position = position
layer.anchorPoint = point
}
}
But this isn't working as expected, the rotation is way off. Check the below screen capture of the issue
I assume this issue is related to how the angle is calculated, but I could't figure out what?
any help is much appreciated.
Angles need to be in radians
The rotation angle needs to be specified in radians:
Change:
rectView.transform = rectView.transform.rotated(by: angle)
to:
rectView.transform = rectView.transform.rotated(by: angle / 180.0 * .pi)
or change your angle(to:) method to return radians instead of degrees.
Use frame instead of bounds
Also, you need to use the frame of your rectView when computing the angle. The bounds of a view is its internal coordinate space, which means its origin is always (0, 0). You want the frame which is the coordinates of the view in its parent's coordinate system.
Change:
let angle = rectView.bounds.origin.angle(to: end)
to:
let angle = rectView.frame.origin.angle(to: end)
Note: Because your anchorPoint is the corner of rectView, this will point the top edge of rectView to the center of endView. One way to fix that would be to change your anchorPoint to the center of the left edge of rectView and then use that point to compute your angle.

Trying to move SKShapeNodes one at a time in a loop

I want to move the nodes from an offscreen position and line them up across the X-axis spaced out. The nodes have been created and stored in an array. I have the following code to do it. However, all of the nodes move at the same time. I want them to move successively when the prior node is in its new position. I have tried altering the duration of the wait action, but it doesn't work.
Thanks
var shapeNodes: [SKShapeNode] = [SKShapeNode]()
let w = (size.width + size.height) * 0.05
for _ in 1...5 {
let s = SKShapeNode(rectOf: CGSize(width: w, height: w), cornerRadius: w * 0.3)
s.position = CGPoint(x: frame.midX, y: frame.maxY + 75)
s.zPosition = 3
s.fillColor = UIColor.cyan
shapeNodes.append(s)
addChild(s)
}
var posX: CGFloat = 0.0
for n in shapeNodes {
let m = SKAction.moveBy(x: posX, y: -300, duration: 2.0)
m.timingMode = .easeIn
let w = SKAction.wait(forDuration: 2.0)
let seq = SKAction.sequence([ m, w ])
n.run(seq)
posX += 125
}
Give each node a wait action with a duration equal to the animation time of all previous nodes. The logic is very similar to your logic with the posX variable.
var posX: CGFloat = 0.0
var waitDuration: CGFloat = 0
for n in shapeNodes {
let duration = 2
let m = SKAction.moveBy(x: posX, y: -300, duration: duration)
m.timingMode = .easeIn
let w = SKAction.wait(forDuration: waitDuration)
let seq = SKAction.sequence([w, m])
n.run(seq)
posX += 125
waitDuration += duration
}
In the example above, I created another variable called waitDuration, which keeps track of the total time the node should wait before animating.
The duration of the wait action is set to the waitDuration, and at the end of the loop, I increase the waitDuration by the duration of the move action.

Xcode - Swift SpriteKit: SKTileMapNode with a "physics gap"

I am playing around with SpriteKit and the TileMapNode and i've got an annoying problem.
That's how it should look like.
That's how its actually looking in the simulator/device.
The white spaces are terrible and i have no idea, how to get rid of them.
My Tiles are about 70x70 in the "Sprite Atlas - Part" of the assets, i've configured my tilemapnode with a scale of 0.5 and tile size of 70x70.
While testing some cases, i figured out that this part of code triggers the error, but i have no idea, what could be wrong. Changing the SKPhysicsBody size to a smaller one, did not helped.
guard let tilemap = childNode(withName: "LevelGround") as? SKTileMapNode else { return }
let tileSize = tilemap.tileSize
let halfWidth = CGFloat(tilemap.numberOfColumns) / 2.0 * tileSize.width
let halfHeight = CGFloat(tilemap.numberOfRows) / 2.0 * tileSize.height
for row in 0..<tilemap.numberOfRows {
for col in 0..<tilemap.numberOfColumns {
if tilemap.tileDefinition(atColumn: col, row: row) != nil {
let x = CGFloat(col) * tileSize.width - halfWidth
let y = CGFloat(row) * tileSize.height - halfHeight
let rect = CGRect(x: 0, y: 0, width: tileSize.width, height: tileSize.height)
let tileNode = SKShapeNode(rect: rect)
tileNode.position = CGPoint(x: x, y: y)
tileNode.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 70, height: 70), center: CGPoint(x: tileSize.width / 2.0, y: tileSize.height / 2.0))
tileNode.physicsBody?.isDynamic = false
tileNode.physicsBody?.collisionBitMask = 2
tileNode.physicsBody?.categoryBitMask = 1
tileNode.physicsBody?.contactTestBitMask = 2 | 1
tileNode.name = "Ground"
tilemap.addChild(tileNode)
}
}
}
tileNode.strokeColor = .clear
solved the problem
Update:
problem not solved... just moved :/
When checking, if ground & player are in contact, every new tile the status is switching between "contact" and "no contact".
When using a cube instead a circle, the cube begins to rotate. It seems, the corner of the cube get's stuck at the minimal space between the tiles.

How to rotate a CAShapeLayer

I am trying to rotate a CAShapeLayer with respect to a particular anchor point. But when i apply
firstLayer.transform = CATransform3DMakeRotation(CGFloat(M_2_PI), 0, 0, 0)
nothing happens.
i am making a custom UIButton , in which i am adding a layer
import UIKit
#IBDesignable
class CustomButtonTwo: UIButton {
var context = UIGraphicsGetCurrentContext()
#IBInspectable var Thickness : CGFloat = 2
let firstLayer = CAShapeLayer()
var width = CGFloat()
var height = CGFloat()
override func awakeFromNib() {
super.awakeFromNib()
width = self.frame.width
height = self.frame.height
print("\(width) : \(height)")
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
let afirstStartPoint = CGPointMake(width * 0.1, (height - 3 * Thickness) / 6)
let bfirstStartPoint = CGPointMake(width * 0.1, (height - 3 * Thickness) / 6 + Thickness)
let afirstMiddlePoint = CGPointMake(width * 0.5, (height - 3 * Thickness) / 6 )
let bfirstMiddlePoint = CGPointMake(width * 0.5, (height - 3 * Thickness) / 6 + Thickness)
print(afirstMiddlePoint)
print(afirstStartPoint)
print(bfirstMiddlePoint)
print(bfirstStartPoint)
let firstPath = UIBezierPath()
firstPath.moveToPoint(afirstStartPoint)
firstPath.addLineToPoint(afirstMiddlePoint)
firstPath.addLineToPoint(bfirstMiddlePoint)
firstPath.addLineToPoint(bfirstStartPoint)
firstPath.addLineToPoint(afirstStartPoint)
firstPath.closePath()
firstLayer.frame = self.frame
UIColor.greenColor().setFill()
firstPath.fill()
firstLayer.path = firstPath.CGPath
firstLayer.anchorPoint = afirstStartPoint
firstLayer.transform = CATransform3DMakeRotation(CGFloat(M_2_PI), 0, 0, 1)
layer.addSublayer(firstLayer)
}
}
i want to make a line with a particular thickness and rotate it along a particular point (while animating).
any help appreciated!
simulator Screenshot
Rotating by 2_PI (360) will mean layer will end up back at its original place (0 rotation). Try PI_2 (90) or PI (180) or another angle. Also you need to specify the axis of rotation.
This call rotates by 90 degrees around z-axis:
CATransform3DMakeRotation(CGFloat(M_PI_2), 0, 0, 1.0)

Animate a UIView on a sine wave path

How would you make a UIView move along a sine wave path? Ideally, I'd be able to stop the animation when the UIView goes off the screen and release it. I see that UIView->animateWithDuration can only animate on one property at a time, but this seems like two exclusive things are happening simultaneously: it's moving to the right and it's moving up/down.
You can actually do this with a simple CAKeyframeAnimation.
override func viewDidLoad() {
super.viewDidLoad()
let myView = UIView(frame: CGRect(x: 10, y: 100, width: 50, height: 50))
myView.backgroundColor = UIColor.cyan
view.addSubview(myView)
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.duration = 60 // 60 seconds
animation.isAdditive = true // Make the animation position values that we later generate relative values.
// From x = 0 to x = 299, generate the y values using sine.
animation.values = (0..<300).map({ (x: Int) -> NSValue in
let xPos = CGFloat(x)
let yPos = sin(xPos)
let point = CGPoint(x: xPos, y: yPos * 10) // The 10 is to give it some amplitude.
return NSValue(cgPoint: point)
})
myView.layer.add(animation, forKey: "basic")
}

Resources