Animate object size within GameScenes.swift (Swift) - ios

We need to animate an object's size within GameScene.swift. Other Stack Overflow posts suggest using UIView.animateWithDuration, but this isn't available inside GameScene.swift. We need to animate inside GameScene.swift because we also need access to SKAction to run an action forever.
Right now, we are using the code below, but it is too clunky. The hope is animation will smooth out the appearance of the object as it shrinks.
runAction(SKAction.repeatActionForever(
SKAction.sequence([
SKAction.runBlock(shrinkItem),
SKAction.waitForDuration(0.5)
])
))
func shrinkItem() {
let curWidth = item.size.width
if curWidth < 15 {
return
}
item.size = CGSize( width: CGFloat(item.size.width - 20 ), height: CGFloat(bird.size.height - 20) )
}

What you are trying to do is to shrink your bird, right ? That correspond to scale it down.
Why are you doing it manually inside a runBlock ?
You might want to give a look at the SKAction Class Reference : http://goo.gl/ycPYcF.
Inside which you'll see all the possible actions, and scaleBy:duration: (or another one) might be what you're looking for.
let shrinkAction = SKAction.scaleBy(0.5, duration: 1.0)
let waitAction = SKAction.waitForDuration(0.5)
let sequenceAction = SKAction.sequence([shrinkAction, waitAction])
let repeatAction = SKAction.repeatActionForever(sequenceAction)
self.yourBirdNode.runAction(repeatAction)
Depending on what you'll do next, take note that some action are automatically reversible (through reversedAction) and some aren't.

Related

ScrollView inertia effect manually, iOS, Swift

I have UICollectionView which I'm dragging from code (don't ask me why it's very long story:)).
And my code is working pretty well:
func move(prevPoint: CGPoint, curPoint: CGPoint) {
let xDiff = curPoint.x - prevPoint.x
let yDiff = curPoint.y - prevPoint.y
let xSign = xDiff == 0 ? 1 : (xDiff / abs(xDiff))
let ySign = yDiff == 0 ? 1 : (yDiff / abs(yDiff))
let x = max(min(abs(xDiff), maxPickerStep), minPickerStep) * -xSign * xMultiplier
let y = max(min(abs(yDiff), maxPickerStep), minPickerStep) * -ySign
let offset = CGPoint(x: collectionView.contentOffset.x + x, y: collectionView.contentOffset.y)
let cell = (collectionView.visibleCells.first as? ColorsCollectionViewCell)
let innerOffset = cell?.colorCollectionView.contentOffset ?? .zero
let inset = (cell?.colorCollectionView.contentInset.top ?? 0) * 2
let innerYContentOffset = min(max(innerOffset.y + y, -inset), (cell?.colorCollectionView.contentSize.height ?? 0) - inset)
cell?.colorCollectionView.contentOffset = CGPoint(x: innerOffset.x, y: innerYContentOffset)
collectionView.contentOffset = offset
}
But in addition to scrolling, I want to achieve the same effect as in UICollectionView when scrollView moves by inertia after user takes away finger. Thanks.
First thing first, I think that moving the scroll view manually is most certainly a thing I would avoid.
Probably there is something much simpler to fulfill the behavior you need.
So I highly suggest you, and any other reader, to not go further in the reading of this post and, instead, go ahead and try to solve the problem that guided you here in the first place.
You could also ask another question here on Stack Overflow to maybe get help to try to avoid you to manually update the scrollView position.
So if you are still reading, this article is probably the way to go with implementing something that really feels like a UIScrollView. Doing anything else will probably really look and feel awful.
Basically it consists of using UIKit Dynamics to control the inertia.
So you can create an object that conforms to UIDynamicItem (with a non-zero CGRect), and change its center instead of the scrollView contentOffset, than use a UIDynamicAnimator and its UIDynamicBehavior to set up the inertia and to connect the changes during the animation to the corresponding contentOffset in the scrollView using the UIDynamicBehavior's action block.
Assuming that you have an item that is a UIDynamicItem, and an animator that is a UIDynamicAnimator, the handling of the panGesture recognizer would look something like this:
func handlGestureRecognizer(panGesture: UIPanGestureRecognizer) {
switch panGesture.state {
case .began:
self.animator.removeAllBehaviors()
case .changed:
// Update scroll view position
break
case .ended:
var velocity = panGesture.velocity(in: panGesture.view!)
velocity.x = -velocity.x
velocity.y = -velocity.y
// You probably need to check for out of bound velocity too, and also put velocity.x to 0 if the scroll is only scrolling vertically
// This is done to just save the current content offset and then change it alongside the animation from this starting point
item.center = scrollView.contentOffset
let decelerationBehavior = UIDynamicItemBehavior(items: [item])
decelerationBehavior.addLinearVelocity(velocity, for: item)
decelerationBehavior.resistance = 2.0
decelerationBehavior.action = {
// Connect the item center to the scroll contentOffset. Probably something like this:
scrollView.contentOffset = item.center
}
self.animator.addBehavior(decelerationBehavior)
default:
break
}
}
You than just need to play up with the values of the behavior and be careful with the velocity you put into the behavior having extra care in looking at the edge cases (if you scroll over the min/max for example)
PS: After all I've written, I still believe you should strongly consider not doing this and, instead, go with the standard scrollView scrolling, avoiding manual updates.
You can try to play with decelerationRate and see if it satisfies your needs.
collectionView.decelerationRate = UIScrollView.DecelerationRate(rawValue: 1)

Enumerate through UIImageView

In SpriteKit you are able to enumerate through each node with a specific name. Say I have 10 sprites with the name property set to "foo" I can then run the code below and it will move each "food" node up 5 pixels every time the function is called.
enumerateChildNodesWithName("foo"){node, stop in
let sprite:SKSpriteNode = node as! SKSpriteNode
sprite.position.x += 5
}
Now, I would like to do this with UIImageView (if possible).
Here's my current setup
In my app I have code that runs every second. It is supposed to add a UIImageView using the following code
var mView:UIImageView = UIImageView(image: UIImage(named: "swirl"))
self.view.addSubview(mView)
Finally, I have a for loop that doesn't seem to be targeting each specific view. But it should be moving each individual image view in a circle. Currently it only looks like a single image is moving even while I think I've added more image views.
for view in self.view.subviews as [UIView] {
if let ind = view as? UIImageView {
let OrCe:CGPoint = mView.center
mView.center = CGPoint(x: OrCe.x + sin(tick)*50,
y: OrCe.y + cos(tick)*50)
}
}
I feel like what I'm doing is really wrong :( Is it possible for me to do what I am trying to do? I would like to do this so that I do not have to use SpriteKit. I want to try and create graphics at a lower level framework. Can I go lower then this even? How can I most efficiently render these 10 moving images?
I assume all the two peaces are consecutive.
var mView:UIImageView = UIImageView(image: UIImage(named: "swirl"))
self.view.addSubview(mView)
for view in self.view.subviews as [UIView] {
if let ind = view as? UIImageView {
let OrCe:CGPoint = view.center
view.center = CGPoint(x: OrCe.x + sin(tick)*50,
y: OrCe.y + cos(tick)*50)
}
}
If got you correct this should work.

How do I make my node rotate at a slower duration?

I have this node and when I press and hold a button I want the node to rotate slower. I changed the duration parameter from 1 to 50 and it still rotates the node the same speed. What am I doing wrong?
let rotateRate = (SKAction.rotateByAngle(CGFloat(-M_PI_2), duration: 50.0))
let repeatRotate = SKAction.repeatActionForever(rotateRate)
heroNode.runAction(repeatRotate)
Once you create an action, you can't modify its duration parameter... So you can't affect on speed of an action in the way you are expecting. But you have a few options:
to re-create the action (you probably want to run an action with key for this)
to change the speed of that action:
if let action = node.actionForKey("aKey"){
action.speed = 1.5
}
Probably some more, but this will give an idea what is going on.
I would recommend Whirlwind's option first, this should be used 99% of the time, but in a case where changing the speed is not an option, just apply another action of rotateBy in the opposite direction at a smaller interval.
let rotateRate = (SKAction.rotateByAngle(CGFloat(-M_PI_2), duration: 50.0))
let repeatRotate = SKAction.repeatActionForever(rotateRate)
heroNode.runAction(repeatRotate)
...
func slowDown()
{
let rotateRate = (SKAction.rotateByAngle(CGFloat(M_PI_4), duration: 50.0))
let repeatRotate = SKAction.repeatActionForever(rotateRate)
heroNode.runAction(repeatRotate, forKey:"slowdown")
}
func removeSlowDown()
{
heroNode.removeActionForKey("slowdown")
}

SKAction.colorizeWithColor makes SKLabelNode disappear

I'm using SKLabelNode. I'm creating it and add it to my scene as a child and it displays with no problem, but when I try to change it's color (not fontColor) with the colorizeWithColor() method the label fades out.
Here is the line with the problem:
myLabel.runAction(SKAction.colorizeWithColor(SKColor.blueColor(), colorBlendFactor: 1.0, duration: duration))
I printed to the console the myLabel.color property after the completion of this action and here is what I get:
Optional(UIDeviceRGBColorSpace 0.99178 0.99178 1 0.00822043)
As you can see, the alpha value is almost 0, so I guess this is why the label disappears, but I don't understand why this is happening.
Thanks in advance.
UPDATE:
Ok, so I found actually this and is my bad that I didn't searched before asking. Here is the documentation about colorizeWithColor method:
This action can only be executed by an SKSpriteNode object. When the
action executes, the sprite’s color and colorBlendFactor properties
are animated to their new values.
So maybe anybody does know a good work around for colorize a SKLabelNode that is always updating?
!!!LAST UPDATE!!!
I was able to find a solution, but 0x141E came up with a even more nice solution, which I used and created the next method that works nice when you need to make transition from color A to color B. In solution suggested by 0x141E you ever come back to fontColor and it causes to blink when you change color. In my case it changes the fontColor, not the color property, which causes a pretty nice transition (Of course not great).
Thanks again 0x141E for the really nice approach!!!
Here is my solution:
This particular case works great when you call the method for parameter withDuration = 0.5
However if you need other time, you can play around with the sent withDuration parameter or the multiplier, that in my code is 5.
Even there of course should be a better solution so if you find please share it. For my needs this one works fantastic.
First of all a video so you can see how it works: https://www.youtube.com/watch?v=ZIz8Bn0-hUA&feature=youtu.be
func changeColorForLabelNode(labelNode: SKLabelNode, toColor: SKColor, withDuration: NSTimeInterval) {
labelNode.runAction(SKAction.customActionWithDuration(withDuration, actionBlock: {
node, elapsedTime in
let label = node as SKLabelNode
let toColorComponents = CGColorGetComponents(toColor.CGColor)
let fromColorComponents = CGColorGetComponents(label.fontColor.CGColor)
let finalRed = fromColorComponents[0] + (toColorComponents[0] - fromColorComponents[0])*CGFloat(elapsedTime / (CGFloat(withDuration)*5))
let finalGreen = fromColorComponents[1] + (toColorComponents[1] - fromColorComponents[1])*CGFloat(elapsedTime / (CGFloat(withDuration)*5))
let finalBlue = fromColorComponents[2] + (toColorComponents[2] - fromColorComponents[2])*CGFloat(elapsedTime / (CGFloat(withDuration)*5))
let finalAlpha = fromColorComponents[3] + (toColorComponents[3] - fromColorComponents[3])*CGFloat(elapsedTime / (CGFloat(withDuration)*5))
labelNode.fontColor = SKColor(red: finalRed, green: finalGreen, blue: finalBlue, alpha: finalAlpha)
}))
}
You can colorize an SKLabelNode with an SKAction by creating a custom action. Here's an example of how to do that
myLabel.color = SKColor.blueColor()
myLabel.colorBlendFactor = 0.0
let duration:NSTimeInterval = 2.0
myLabel.runAction(SKAction.customActionWithDuration(duration, actionBlock: {
node, elapsedTime in
let label = node as SKLabelNode
label.colorBlendFactor = elapsedTime / CGFloat(duration);
}))

Staggered animations with CAKeyframeAnimation?

I want to animate 3 different images at specific point in time such that it behaves this way.
1) 1st image moves from (Xx, Yx) to (Xz,Yz)
2) Wait 10 seconds
3) 2nd image appears in place at Xa,Yb
4) Wait half as long as in step 2
5) Fade out 2nd image
6) 3rd image appears at the same place as 2nd image
If each of these image's animations are on their own CALayers, can I use CAKeyframeAnimation with multiple layers? If not, what's another way to go about doing staggered animations?
I'm trying to animate a playing card move from offscreen to a particular spot and then few other tricks to appear on screen several seconds later.
Edited
When I wrote this, I thought you could not use a CAAnimationGroup to animate multiple layers. Matt just posted an answer demonstrating that you can do that. I hereby eat my words.
I've taking the code in Matt's answer and adapted it to a project which I've uploaded to Github (link.)
The effect Matt's animation creates is of a pair of feet walking up the screen. I found some open source feet and installed them in the project, and made some changes, but the basic approach is Matt's. Props to him.
Here is what the effect looks like:
(The statement below is incorrect)
No, you can't use a keyframe animation to animate multiple layers. A given CAAnimation can only act on a single layer. This includes group layers, by the way.
If all you're doing is things like moving images on a straight line, fading out, and fading in, why don't you use UIView animation? Take a look at the methods who's names start with animateWithDuration:animations: Those will let you create multiple animations at the same time, and the completion block can then trigger additional animations.
If you need to use layer animation for some reason, you can use the beginTime property (which CAAnimation objects have because they conform to the CAMediaTiming protocol.) For CAAnimations that are not part of an animation group, you use
animation.beginTime = CACurrentMediaTime() + delay;
Where delay is a double which expresses the delay in seconds.
If the delay is 0, the animation would begin.
A third option would be to set your view controller up as the delegate of the animation and use the animationDidStop:finished: method to chain your animations. This ends up being the messiest approach to implement, in my opinion.
The claim that a single animation group cannot animate properties of different layers is not true. It can. The technique is to attach the animation group to the superlayer and refer to the properties of the sublayers in the individual animations' key paths.
Here is a complete example just for demonstration purposes. When launched, this project displays two "footprints" that proceed to step in alternation, walking off the top of the screen.
class ViewController: UIViewController, CAAnimationDelegate {
let leftfoot = CALayer()
let rightfoot = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.leftfoot.name = "left"
self.leftfoot.contents = UIImage(named:"leftfoot")!.cgImage
self.leftfoot.frame = CGRect(x: 100, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.leftfoot)
self.rightfoot.name = "right"
self.rightfoot.contents = UIImage(named:"rightfoot")!.cgImage
self.rightfoot.frame = CGRect(x: 170, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.rightfoot)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.start()
}
}
func start() {
let firstLeftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
firstLeftStep.byValue = -80
firstLeftStep.duration = 1
firstLeftStep.fillMode = .forwards
func rightStepAfter(_ t: Double) -> CABasicAnimation {
let rightStep = CABasicAnimation(keyPath: "sublayers.right.position.y")
rightStep.byValue = -160
rightStep.beginTime = t
rightStep.duration = 2
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CABasicAnimation {
let leftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
leftStep.byValue = -160
leftStep.beginTime = t
leftStep.duration = 2
leftStep.fillMode = .forwards
return leftStep
}
let group = CAAnimationGroup()
group.duration = 11
group.animations = [firstLeftStep]
for i in stride(from: 1, through: 9, by: 4) {
group.animations?.append(rightStepAfter(Double(i)))
group.animations?.append(leftStepAfter(Double(i+2)))
}
group.delegate = self
self.view.layer.add(group, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("done")
self.rightfoot.removeFromSuperlayer()
self.leftfoot.removeFromSuperlayer()
}
}
Having said all that, I should add that if you are animating a core property like the position of something, it might be simpler to make it a view and use a UIView keyframe animation to coordinate animations on different views. Still, the point is that to say that this cannot be done with CAAnimationGroup is just wrong.

Resources