Trying to move SKShapeNodes one at a time in a loop - ios

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.

Related

Animate UIView to "squiggle" its way to destination instead of going in straight line

I have an animation like this:
bubble.frame.origin = CGPoint(x: 75, y: -120)
UIView.animate(
withDuration: 2.5,
animations: {
self.bubble.transform = CGAffineTransform(translationX: 0, y: self.view.frame.height * -1.3)
}
)
However, the animation goes in a straight line. I want the animation to do a little back and forth action on its way to its destination. Like a bubble. Any ideas?
If you want to animate along a path you can use CAKeyframeAnimation. The only question is what sort of path do you want. A dampened sine curve might be sufficient:
func animate() {
let box = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
box.backgroundColor = .blue
box.center = bubblePoint(1)
view.addSubview(box)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = bubblePath().cgPath
animation.duration = 5
box.layer.add(animation, forKey: nil)
}
where
private func bubblePath() -> UIBezierPath {
let path = UIBezierPath()
path.move(to: bubblePoint(0))
for value in 1...100 {
path.addLine(to: bubblePoint(CGFloat(value) / 100))
}
return path
}
/// Point on curve at moment in time.
///
/// - Parameter time: A value between 0 and 1.
/// - Returns: The corresponding `CGPoint`.
private func bubblePoint(_ time: CGFloat) -> CGPoint {
let startY = view.bounds.maxY - 100
let endY = view.bounds.minY + 100
let rangeX = min(30, view.bounds.width * 0.4)
let midX = view.bounds.midX
let y = startY + (endY - startY) * time
let x = sin(time * 4 * .pi) * rangeX * (0.1 + time * 0.9) + midX
let point = CGPoint(x: x, y: y)
return point
}
Yielding:

Animate a view alongside a path without CAKeyframeAnimation

I want to animate a view alongside a path (without CAKeyframeAnimation) instead with UIViewPropertyAnimator.
For that I have a path with start- and endpoint and a controlpoint to make a quadcurve.
let path = UIBezierPath()
path.move(to: view.center)
path.addQuadCurve(to: target.center, controlPoint: CGPoint(x: view.center.x-10, y: target.center.y+160))
My animator uses a UIView.animateKeyframes animation to animate the position alongside the path.
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeIn)
animator.addAnimations {
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.calculationModeLinear], animations: {
let points = 100
for i in 1...points {
let pos = Double(i)/Double(points)
let x = self.quadBezier(pos: pos,
start: Double(view.center.x),
con: Double(view.center.x-10),
end: Double(target.center.x))
let y = self.quadBezier(pos: pos,
start: Double(view.center.y),
con: Double(target.center.y+160),
end: Double(target.center.y))
let duration = 1.0/Double(points)
let startTime = duration * Double(i)
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
view.center = CGPoint(x: x, y: y)
}
}
})
}
To calculate the points I use the same function as #rmaddy, think we have the same source here.
func quadBezier(pos: Double, start: Double, con: Double, end: Double) -> Double {
let t_ = (1.0 - pos)
let tt_ = t_ * t_
let tt = pos * pos
return Double(start * tt_) + Double(2.0 * con * t_ * pos) + Double(end * tt)
}
First I thought the calulation is wrong because it felt like the calculated points going through the controlpoint or something:
But after iterating through the same forloop and adding blue points to the calculated x/y positions shows that the positions are correct.
So I wonder why my animation don't follow that path.
I found this question and posted my question as an answer but I guess nobody recognized that.
View.center is changing during animation, make it fixed then there is no problem:
try the following:
let cent = view.center
UIView.animateKeyframes(withDuration: duration, delay: 0, options: [.beginFromCurrentState], animations: {
let points = 100
for i in 1...points {
let pos = Double(i)/Double(points)
let x = self.quadBezier(pos: pos,
start: Double(cent.x), //here
con: Double(cent.x-10), //here
end: Double(square.center.x))
let y = self.quadBezier(pos: pos,
start: Double(cent.y), //here
con: Double(square.center.y+160),
end: Double(square.center.y))
let duration = 1.0 / Double(points)
let startTime = duration * Double(i)
print("Animate: \(Int(x)) : \(Int(y))")
UIView.addKeyframe(withRelativeStartTime: startTime, relativeDuration: duration) {
view.center = CGPoint(x: x, y: y)
}
}
})
}

Swift GameKit Moving Sprite In Arc

I need some help in making the player in my platform game jump. The problem I'm having is that he jumps through a pillar which he's not supposed to do (I do have the physics set up correctly). I suspect it is because of my horribly inefficient code below. I know there's a better way to move him along a path but I'm not sure how to do it.
var xStep = CGFloat(18)
var yStep = CGFloat(30)
var x = CGFloat(playerJump.position.x)
var y = CGFloat(playerJump.position.y)
let follow = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y += yStep
let follow2 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y += yStep
let follow3 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y += yStep
let follow4 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y += yStep
let follow5 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y -= yStep
let follow6 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y -= yStep
let follow7 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y -= yStep
let follow8 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
x += xStep
y -= yStep
let follow9 = SKAction.move(to: CGPoint(x: x, y:y), duration: 0.1)
let group = SKAction.group([
SKAction.repeat(jumpAnimation!, count: 1),
SKAction.sequence([follow,follow2,follow3,follow4,follow5,follow6,follow7,follow8,follow9])
])
if playerJump.parent == nil {
addChild(playerJump)
}
playerJump.run(SKAction.sequence([group]), completion: {
self.player.position=self.playerJump.position
self.playerJump.removeAllActions()
self.playerJump.removeFromParent()
self.addChild(self.player)
})
Thanks in advance for any assistance.
UPDATE
The problem started when I increased the value in xStep. The total distance the character jumps would put it past the pillar. That's why I thought my code was an issue. Here's a video of what's happening.
Jumping Video
UPDATE 2
Here's my newest code which still puts the player on the other side of the pillar.
#objc func jumpTapped() {
var xStep = CGFloat(18)
let yStep = CGFloat(12)
jumping=true
xStep = player.xScale * xStep
var x = player.position.x
var y = player.position.y
var moveAnimation = [SKAction]()
for i in 0...8 {
x += xStep
if i < 5 {
y += yStep
} else {
y -= yStep
}
let move = SKAction.move(to: CGPoint(x: x, y: y), duration: 0.1)
moveAnimation.append(move)
}
let group = SKAction.group([jumpAnimation!, SKAction.sequence(moveAnimation)])
player.physicsBody?.affectedByGravity = false
player.removeAllActions()
player.run(group) {
self.endJump()
}
}
func endJump() {
player.removeAllActions()
player.physicsBody?.affectedByGravity = true
player.run(SKAction.repeatForever(self.playerAnimation!))
jumping=false
}
It looks like you are setting player.position to self.playerJump.position
That is probably your problem, it is jumping beyond where the pillar is. You might as well have said player.position.x = player.position.x + 500 which will put you on the other side of the pillar as well ignoring any physics all together.
Why are you adding a sprite and running the actions on it? why not just run the animation and the actions on the player sprite?
As far as cleaning up your code goes you can try this. there are other things you can do as well like have the character jump along a Bezier path but this might be fine for what you are doing. But I highly doubt this will have any impact on your issue of the player jumping through the pillar.
Some small tidbits for you.
Sprite positions are already a CGFloat no need to cast them
short hand completion code syntax is object.run(someAction) { //do something when complete }
running an action with repeat of 1 is pointless
running a sequence of a single action (SKAction.sequence([group])) is also pointless
var xStep = CGFloat(18)
var yStep = CGFloat(30)
var x = playerJump.position.x
var y = playerJump.position.y
var moveAnimation: [SKAction]!
for _ in 0...9 {
x += xStep
y += yStep
moveAnimation.append(SKAction.move(to: CGPoint(x: x, y: y), duration: 0.1))
}
let group = SKAction.group([jumpAnimation, SKAction.sequence(moveAnimation)])
guard playerJump.parent else { addChild(playerJump)}
playerJump.run(group) {
self.player.position = self.playerJump.position
self.playerJump.removeAllActions()
self.playerJump.removeFromParent()
self.addChild(self.player)
}
Edit try adding the below function to your code
func didBegin(_ contact: SKPhysicsContact) {
let contactAName = contact.bodyA.node?.name
let contactBName = contact.bodyB.node?.name
if (contactAName == "pillar") || (contactBName == "pillar") {
//assuming only player can hit pillar so don't need to check if one of the contacts is player
endJump()
}
}
I figured out what I was doing wrong. My mistake was moving the sprite within the SKAction. I should have been moving the sprite in the update method. So, I added this to update():
if jumping {
if jumpCount > 3 {
player.position.x += xStep // Jump straight up for a couple of frames to clear steps
}
if jumpCount < maxJump/2 {
player.position.y += yStep //Move sprite up for first half of jump
} else {
player.position.y -= yStep //Move sprite down for last half of jump
}
jumpCount += 1
if jumpCount >= maxJump {
endJump()
}
}
All the collision detection works properly and the player doesn't jump through pillars.

Swift / SpriteKit: evenly distribute SKShapeNodes horizontally and vertically into a grid

I want to equally distribute an n amount of square SKShapeNodes over the screen to create a square grid. I tried searching for other examples of this, but I cannot find the solution I'm looking for as most solutions only focus on only distributing views horizontally.
I'm creating the squares in a for loop, however I can only get them to move on either the x or the y axes, not both. The below example gives me the best result, but as expected from the code only the first two squares are visible, the third one is created off screen:
amountOfTiles is a CGFloat set to 4
tile is an SKShapeNode
func addTiles() {
let screenWidth = self.view?.frame.size.width
tileWidth = screenWidth! / (amountOfTiles / 2)
tileHeight = tileWidth
tileX = 0
tileY = 0
tileRect = CGRect(x: tileX, y: tileY, width: tileWidth, height: tileHeight)
for _ in 0...Int(amountOfTiles) {
tile = SKShapeNode(rect: tileRect)
tile.position = CGPoint(x: tileX, y: tileY)
tile.fillColor = .red
addChild(tile)
tileX += tileWidth
tileY += tileHeight
}
}
My guess is that I should not update the Y and X positions of the squares at the same time, but I'm not sure how to solve this.
Fixed it myself. I removed amountOfTiles and added two separate CGFloats for x and y, which I update in a separate loop:
func addTiles() {
let xAmount:CGFloat = 4
let yAmount:CGFloat = 4
let screenWidth = self.view?.frame.size.width
tileWidth = screenWidth! / (xAmount)
tileHeight = tileWidth
tileY = 0
tileRect = CGRect(x: tileX, y: tileY, width: tileWidth, height: tileHeight)
for _ in 0..<Int(xAmount) {
tileX = 0
for _ in 0..<Int(yAmount) {
tile = SKShapeNode(rect: tileRect)
tile.fillColor = .red
addChild(tile)
tile.position = CGPoint(x: tileX, y: tileY)
tileX += tileWidth
}
tileY += tileHeight
}
}

Swift Spritekit Scrolling Background

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)

Resources