SpriteKit animation - ios

I need your advice, I'm new in SpriteKit I need to make animation strips. I have 3 solutions to make it, but I need advice that better and less costly for CPU.
1 solution: each stripe - SKSpriteNode with animation and texture
2 solution: background video
3 solution: each stripe - SKShapeNode with animation

This is a simple task , you don't need to build an atlas animation or use SKShapeNode, you can use SKSpriteNode as this code:
var bar = SKSpriteNode(color: SKColor.greenColor(), size: CGSizeMake(40, 200))
barra.position = CGPoint(x: CGRectGetMidX(self.frame), y: CGRectGetMidY(self.frame))
self.addChild(barra)
Build n bars with random size, and use SKAction to move them.
Whith this approach your animation will be different everytime you launch it.
Code in details:
class GameScene: SKScene {
var bars: [SKSpriteNode]!
var totBars : Int = 50
override func didMoveToView(view: SKView) {
self.backgroundColor = SKColor(red: 131/255, green: 190/255, blue: 177/255, alpha: 1)
let redBarColor = SKColor(red: 204/255, green: 75/255, blue: 75/255, alpha: 1)
let yellowBar = SKColor(red: 253/255, green: 242/255, blue: 160/255, alpha: 1)
// add here your colors
var colorSelected:SKColor = redBarColor
bars = [SKSpriteNode]()
for i in 0..<totBars-1 {
let colorNum = randomNumber(1...2)
switch (colorNum) {
case 1:
colorSelected = redBarColor
case 2:
colorSelected = yellowBar
default:
break
}
let randomWidth = randomCGFloat(5,max:40)
let randomHeight = randomCGFloat(30,max:400)
let bar = SKSpriteNode.init(color: colorSelected, size: CGSizeMake(randomWidth, randomHeight))
bar.zRotation = -45 * CGFloat(M_PI / 180.0)
bar.name = "bar\(i)"
self.addChild(bar)
bars.append(bar)
}
animateBars()
}
func animateBar(bar:SKSpriteNode) {
print("- \(bar.name) start!")
let deltaX = self.randomCGFloat(0,max:self.frame.maxX)
let deltaY:CGFloat = self.frame.maxY/2
let rightPoint = CGPointMake(self.frame.maxX + deltaX,self.frame.maxY + deltaY)
let leftPoint = CGPointMake(-self.frame.maxX + deltaX,-self.frame.maxY + deltaY)
bar.position = rightPoint
let waitBeforeExit = SKAction.waitForDuration(Double(self.randomCGFloat(1.0,max:2.0)))
let speed = self.randomCGFloat(150,max:300)
let move = SKAction.moveTo(leftPoint, duration: self.getDuration(rightPoint, pointB: leftPoint, speed: speed))
bar.runAction(SKAction.sequence([waitBeforeExit,move]), completion: {
print("\(bar.name) reached position")
self.animateBar(bar)
})
}
func animateBars() {
for bar in bars {
animateBar(bar)
}
}
func getDuration(pointA:CGPoint,pointB:CGPoint,speed:CGFloat)->NSTimeInterval {
let xDist = (pointB.x - pointA.x)
let yDist = (pointB.y - pointA.y)
let distance = sqrt((xDist * xDist) + (yDist * yDist));
let duration : NSTimeInterval = NSTimeInterval(distance/speed)
return duration
}
func randomCGFloat(min: CGFloat, max: CGFloat) -> CGFloat {
return CGFloat(Float(arc4random()) / Float(UINT32_MAX)) * (max - min) + min
}
func randomNumber(range: Range<Int> = 1...6) -> Int {
let min = range.startIndex
let max = range.endIndex
return Int(arc4random_uniform(UInt32(max - min))) + min
}
}

If your stripes are simply plain rectangles, you can use SKSpriteNodes and only give them dimensions and a color, then use actions to animate them. you can rotate the rectangles to give the effect you show in the picture.
You could actually build the whole animation in Xcode using the editor, save it in its own SKS file and load the animation using a SKReferenceNode.

To answer your question, Solution 3 is the least costly method for your CPU. Here're several reasons why this is true:
1. The first solution that you're suggesting SKSpriteNode with animation and texture" require the computer to load the texture to the view. If you have multiple .png files, this would mean that the computer would need to load all of these files. But if you were to use SKShapeNode, you would avoid having to do this and you would cheaply create the same looks as the SKShapeNode is not based on an image.
2. Solution 2 is the most costly to your CPU because running a video. This takes a lot of space in your memory which might even create lags if you run it on your phone.
Also, just another thing to note: If you were to extensively use Solution 1 and 2, you would end up using so much memory. But if you use the Solution 3, you will not deal with this.

Another option, in addition to the ones put forth already, would be to write a GLSL shader for the background. If you just want a cool backdrop that doesn't interact with the rest of your game, this would probably be the most performant option, since it would all happen on the GPU.
You could even, conceivably, render the rectangles without requiring any images at all, since they are just areas of color.
There's a decent introduction to the topic here: Battle of Brothers GLSL Introduction.

Related

Are there recommended ways for drawing 2d array in fast time?

I want to draw a 2d array on iOS in fast time. The 2d array is like heat map, depth map or segmentation map, etc.
In my case, with UIKit framework, drawing big size array, like 500x500 shape, is too slow.
// my solution, but it's too slow
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext(),
let heatmap = self.heatmap else { return }
ctx.clear(rect);
let size = self.bounds.size
let heatmap_w = heatmap.count
let heatmap_h = heatmap.first?.count ?? 0
let w = size.width / CGFloat(heatmap_w)
let h = size.height / CGFloat(heatmap_h)
for j in 0..<heatmap_w {
for i in 0..<heatmap_h {
let value = heatmap[i][j]
let alpha: CGFloat = CGFloat(value)
guard alpha > 0 else { continue; }
let rect: CGRect = CGRect(x: CGFloat(i) * w, y: CGFloat(j) * h, width: w, height: h)
let color: UIColor = UIColor(red: 1.0, green: 0.0, blue: 0.0, alpha: alpha*0.58)
let bpath: UIBezierPath = UIBezierPath(rect: rect)
color.set()
bpath.stroke()
bpath.fill()
}
}
} // end of draw(rect:)
I think Metal or CoreGraphics framework are related to this problem, but I couldn't find the proper example or material. Are there any recommended ways?
heatmap example
updated:
Here is segmentation post-processing implementation example with MetalKit. The latency of post-processing was down to 1 ms from 240 ms on iPhone 11 Pro.
I referred MetalCamera a lot.
new updated:
Here is depth prediction post-processing implementation example with MetalKit and Accelerate framework. The latency of post-processing was down to 1 ms from 15 ms on iPhone 11 Pro.
The best way is to use Metal Compute Function to achieve maximum performance.
Apple has a good tutorial that shows the basics. Additionally you can use Metal Performance Shaders to implement custom rendering functions or to take advantage of a large library of existing functions.

Superimpose two textures on an SKSpriteNode

I would like to achieve the effect shown in this gif.
Currently I do this with a series of ~7 png images with a red background and a white line, these are animated through the sprite with an SKAction.
There are a few others additions I would like to make to the sprite, that can change depending on situation, and also I would like to repeat this with a number of colours.
This results in: 6 colours, 7 shine effects, 5 edge effects and 4 corner effects resulting in 136 combinations of textures I would need to create and store.
I feel like there has to be a way to superimpose png's with transparent backgrounds when setting the texture of a sprite but I cannot seem to find a way to do this anywhere.
Is this possible so that I can reduce the number of assets to 22 or do I have to make all 136 and build in logic to the class to decide which to use?
I wanted an effect like this for my game, I tried a lot of options. I tried using particles for performance but couldn't even get close. I know you can accomplish it with Shaders, but i didn't go that route and in iOS 12 Shaders won't be support Open GL anyway. In the end I opted to go with CropNodes.
This is my glare image, it is hard to see because it slightly transparent whiteish image.
This is the results I achieved using CropNodes
class Glare: SKSpriteNode {
var glare = SKSpriteNode()
private var cropNode = SKCropNode()
init(color: UIColor, size: CGSize) {
super.init(texture: nil, color: color, size: size)
alpha = 0.7
zPosition = 10
setupGlare()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func setupGlare() {
let buffer: CGFloat = 10
let mask = SKSpriteNode(texture: nil, color: .black, size: self.size)
let texture = SKTextureAtlas(named: "Sprites").textureNamed("glare")
glare = SKSpriteNode(texture: texture)
glare.position = CGPoint(x: 0 - (self.size.width / 2 + buffer), y: self.size.height / 2 + buffer)
glare.setScale(3.50)
glare.zPosition = 1
cropNode.zPosition = 2
cropNode.maskNode = mask
cropNode.addChild(glare)
let random = Double(CGFloat.random(min: 0, max: 1))
let pause = SKAction.wait(forDuration: random)
let move = SKAction.moveBy(x: self.size.width + buffer * 2, y: 0 - (self.size.height + buffer * 2), duration: 0.5)
let wait = SKAction.wait(forDuration: 1.0)
let reset = SKAction.moveBy(x: 0 - (self.size.width + buffer * 2), y: self.size.height + buffer * 2, duration: 0.0)
let seq = SKAction.sequence([move, wait, reset])
let repeater = SKAction.repeatForever(seq)
let repeaterSeq = SKAction.sequence([pause, repeater])
glare.run(repeaterSeq)
}
func showGlare(texture: SKTexture) {
let mask = SKSpriteNode(texture: texture)
cropNode.maskNode = mask
glare.isHidden = false
if cropNode.parent == nil { self.addChild(cropNode)}
}
func hideGlare() {
glare.isHidden = true
//remove cropnode from the node tree
cropNode.removeFromParent()
}
}
and then in my GameScene...
I add my glares to a glare layer but that isn't necessary. I also go through when the game loads and create my glares for all 15 slots ahead of time and put them in an array. This way I do not have to create them on the fly and I can just turn on slot 10 any time I want and turn it off as well.
private var glares = [Glare]()
let glare = Glare(color: .clear, size: CGSize(width: kSymbolSize, height: kSymbolSize))
glare.position = CGPoint(x: (CGFloat(x - 1) * kSymbolSize) + (kSymbolSize / 2), y: 0 - (CGFloat(y) * kSymbolSize) + (kSymbolSize / 2))
glare.zPosition = 100
glareLayer.addChild(glare)
glares.append(glare)
When I want to show the glare on a slot
EDIT texture here for you would just be a blank square texture the size of your tile.
glares[index].showGlare(texture: symbol.texture!)
When I want to hide it
glares[index].hideGlare()

What is the right way of creating circle animation?

I just saw this image and it's interesting to me, how to create such type of animation in Swift:
So, I have many gray teeth in circle and when I set the angle, for example 45degree it will fill these gray teeth into blue within 0..45 degree.
You can just explain me the right way of doing it or you can show different snippets(it would be great). And later I will search or read about it.
Thanks in advance!
If you only need the individual 'teeth' to change color, instead of using the teeth as masks for a solid fill, you can use Core Graphics instead of Core Animation (although Core Animation is generally preferred). So in order to do this, we should be doing the following:
Subclass UIView to insert our drawing code
Create an array of path objects, wrapped in UIBezierPath
Setup a timer to update a progress value and setNeedsDisplay
In drawRect:, draw the paths and assign a fill to each depending on the progress
First of all, lets define the variables we're going to be working with in this UIView subclass.
class TeethLoaderView : UIView {
let numberOfTeeth = UInt(60) // Number of teeth to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
var progress = NSTimeInterval(0.0) // The progress of the loader
var paths = [UIBezierPath]() // The array containing the UIBezier paths
var displayLink = CADisplayLink() // The display link to update the progress
var teethHighlighted = UInt(0) // Number of teeth highlighted
...
Now let's add a function to create our paths.
func getPaths(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> [UIBezierPath] {
let halfHeight = size.height*0.5;
let halfWidth = size.width*0.5;
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil);
var pathArray = [UIBezierPath]()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight);
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
let pathCopy = CGPathCreateCopyByTransformingPath(p, &rotate)!
pathArray.append(UIBezierPath(CGPath: pathCopy)) // Populate the array
}
return pathArray
}
This is fairly simple. We just create a path for a single 'tooth' and then copy this path for how many teeth we need, translating and rotating the path for each one.
Next we want to setup our view. I'm going to a CADisplayLink for the timer so that the animation performs at the same speed on all devices.
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
private func commonSetup() {
self.backgroundColor = UIColor.whiteColor()
paths = getPaths(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
displayLink = CADisplayLink(target: self, selector: #selector(displayLinkDidFire));
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSRunLoopCommonModes)
}
Here we just set the background color, as well as setup our timer and initialise the paths we're going to be using. Next we want to setup a function to change the progress of the view when the CADisplayLink fires.
func displayLinkDidFire() {
progress += displayLink.duration/animationDuration
if (progress > 1) {
progress -= 1
}
let t = teethHighlighted
teethHighlighted = UInt(round(progress*NSTimeInterval(numberOfTeeth))) // Calculate the number of teeth to highlight
if (t != teethHighlighted) { // Only call setNeedsDisplay if the teethHighlighted changed
setNeedsDisplay()
}
}
Nothing complicated here, we just update the progress and teethHighlighted and call setNeedsDisplay() to redraw the view, if teethHighlighted changed.
Finally, we want to draw the view.
override func drawRect(rect: CGRect) {
let ctx = UIGraphicsGetCurrentContext()
CGContextScaleCTM(ctx, -1, -1) // Flip the context to the correct orientation
CGContextTranslateCTM(ctx, -rect.size.width, -rect.size.height)
for (index, path) in paths.enumerate() { // Draw each 'tooth'
CGContextAddPath(ctx, path.CGPath);
let fillColor = (UInt(index) <= teethHighlighted) ? highlightColor:inactiveColor;
CGContextSetFillColorWithColor(ctx, fillColor.CGColor)
CGContextFillPath(ctx)
}
}
If you wanted to go down the Core Animation path, I adapted this code into a Core Animation layer
Final Result
Full project: https://github.com/hamishknight/Circle-Loader
Well, in the spirit of "go big or go home" (and because I'm actually having some fun doing this), I created a Core Animation version of my Core Graphics answer. It's quite a bit less code and animates smoother, so I'd actually prefer to use this.
First off, let's subclass a UIView again (this isn't strictly necessary, but it's nice to contain everything in a single view) and define our variables:
class TeethLoaderViewCA : UIView {
let numberOfTeeth = UInt(60) // Number of teetch to render
let teethSize = CGSize(width:8, height:45) // The size of each individual tooth
let animationDuration = NSTimeInterval(5.0) // The duration of the animation
let highlightColor = UIColor(red: 29.0/255.0, green: 175.0/255.0, blue: 255.0/255.0, alpha: 1) // The color of a tooth when it's 'highlighted'
let inactiveColor = UIColor(red: 233.0/255.0, green: 235.0/255.0, blue: 236.0/255.0, alpha: 1) // The color of a tooth when it isn't 'hightlighted'
let shapeLayer = CAShapeLayer() // The teeth shape layer
let drawLayer = CAShapeLayer() // The arc fill layer
let anim = CABasicAnimation(keyPath: "strokeEnd") // The stroke animation
...
This is mostly the same as the Core Graphics version, but with a couple of Core Animation objects and without the timing logic. Next, we can pretty much copy the getPaths function we created in the other version, except with a few tweaks.
func getPathMask(size:CGSize, teethCount:UInt, teethSize:CGSize, radius:CGFloat) -> CGPathRef? {
let halfHeight = size.height*0.5
let halfWidth = size.width*0.5
let deltaAngle = CGFloat(2*M_PI)/CGFloat(teethCount); // The change in angle between paths
// Create the template path of a single shape.
let p = CGPathCreateWithRect(CGRectMake(-teethSize.width*0.5, radius, teethSize.width, teethSize.height), nil)
let returnPath = CGPathCreateMutable()
for i in 0..<teethCount { // Copy, translate and rotate shapes around
let translate = CGAffineTransformMakeTranslation(halfWidth, halfHeight)
var rotate = CGAffineTransformRotate(translate, deltaAngle*CGFloat(i))
CGPathAddPath(returnPath, &rotate, p)
}
return CGPathCreateCopy(returnPath)
}
This time, all the paths are grouped into one big path and the function returns that path.
Finally, we just have to create our layer objects & setup the animation.
private func commonSetup() {
// set your background color
self.backgroundColor = UIColor.whiteColor()
// Get the group of paths we created.
shapeLayer.path = getPathMask(frame.size, teethCount: numberOfTeeth, teethSize: teethSize, radius: ((frame.width*0.5)-teethSize.height))
let halfWidth = frame.size.width*0.5
let halfHeight = frame.size.height*0.5
let halfDeltaAngle = CGFloat(M_PI/Double(numberOfTeeth))
// Creates an arc path, with a given offset to allow it to be presented nicely
drawLayer.path = UIBezierPath(arcCenter: CGPointMake(halfWidth, halfHeight), radius: halfWidth, startAngle: CGFloat(-M_PI_2)-halfDeltaAngle, endAngle: CGFloat(M_PI*1.5)+halfDeltaAngle, clockwise: true).CGPath
drawLayer.frame = frame
drawLayer.fillColor = inactiveColor.CGColor
drawLayer.strokeColor = highlightColor.CGColor
drawLayer.strokeEnd = 0
drawLayer.lineWidth = halfWidth
drawLayer.mask = shapeLayer
layer.addSublayer(drawLayer)
// Optional, but looks nice
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
}
All we're doing here is assigning the group of paths to a CAShapeLayer, which we will use as a mask over the drawLayer, which we will be animating around the view (using a stroke on an arch path).
Final Result
Full project: https://github.com/hamishknight/Circle-Loader

How can I make my backgrounds use less memory?

I am trying to add a vertical scrolling background to my project. From what I scene on the internet. My background consists of 8 images, each [320x1000px].png files. So what I ended up doing for it was this:
//Layered Nodes
var backgroundNode: SKNode!
override init(size: CGSize) {
super.init(size: size)
scaleFactor = self.size.width / 320.0
// Background
backgroundNode = createBackgroundNode()
addChild(backgroundNode)
}
func createBackgroundNode() -> SKNode {
let backgroundNode = SKNode()
let ySpacing = 1000.0 * scaleFactor
for index in 0...3 {
let node = SKSpriteNode(imageNamed:String(format: "bg%d", index + 1))
node.setScale(scaleFactor)
node.anchorPoint = CGPoint(x: 0.5, y: 0.0)
node.position = CGPoint(x: self.size.width / 2, y: ySpacing * CGFloat(index))
backgroundNode.addChild(node)
}
return backgroundNode
}
Problem is, they use up to 50Mb of the project. I am trying to find a way to do it so it would take much less memory off my game but I can't seem to find it. Is there anything wrong with this? If not, should I best focus on other parts of the project and keep it this way?
You can create background on the go from vector drawing using paint-code app.
Another point. Are your images optimized? If no you can use ImageOptim for free.

How do I get pixel color on touch from inside a SKScene?

I have a spritekit application written in swift and I want to get the color on the pixel that my finger is touching.
I have seen multiple post regarding this and tried them all out but can't seam to get it to work for me. Accourding to other post it should be possible to get the color from a UIView and as a SKScene has a SKIView that inherits from UIView it should be possible to get the color from there.
So to make the question easy and understandable I have an example.
Create a new spritekit application and add a image to it.
In my case I created a png image 200x200 pixels with a lot of different colors in it.
This is the GameScene.swift file, it is the only file I have changes from the auto generated:
import SpriteKit
extension UIView {
func getColorFromPoint(point:CGPoint) -> SKColor {
var pixelData:[UInt8] = [0,0,0,0]
let colorSpace:CGColorSpace = CGColorSpaceCreateDeviceRGB()
let bitmapInfo = CGBitmapInfo(CGImageAlphaInfo.PremultipliedLast.toRaw())
let context = CGBitmapContextCreate(&pixelData, 1, 1, 8, 4, colorSpace, bitmapInfo)
CGContextTranslateCTM(context, -point.x, -point.y);
self.layer.renderInContext(context)
var red:CGFloat = CGFloat(pixelData[0])/CGFloat(255.0)
var green:CGFloat = CGFloat(pixelData[1])/CGFloat(255.0)
var blue:CGFloat = CGFloat(pixelData[2])/CGFloat(255.0)
var alpha:CGFloat = CGFloat(pixelData[3])/CGFloat(255.0)
var color:SKColor = SKColor(red: red, green: green, blue: blue, alpha: alpha)
return color
}
}
class GameScene: SKScene {
var myColorWheel:SKSpriteNode!
override func didMoveToView(view: SKView) {
let recognizerTap = UITapGestureRecognizer(target: self, action:Selector("handleTap:"))
view.addGestureRecognizer(recognizerTap)
myColorWheel = SKSpriteNode(imageNamed: "ColorWheel.png")
myColorWheel.anchorPoint = CGPoint(x: 0, y: 0)
myColorWheel.position = CGPoint(x: 200, y: 200)
self.addChild(myColorWheel)
}
func handleTap(recognizer : UITapGestureRecognizer)
{
let location : CGPoint = self.convertPointFromView(recognizer.locationInView(self.view))
if(myColorWheel.containsPoint(location))
{
let color = self.view?.getColorFromPoint(location)
println(color)
}
}
}
It don't matter where I press on the image on the display, the result is always:
Optional(UIDeviceRGBColorSpace 0 0 0 0)
Have you tried to take a snapshot first using:
- (UIView *)snapshotViewAfterScreenUpdates:(BOOL)afterUpdates
Then picking the colours from that view?
Not sure how the system renders the .layer in a SKView.
Hope that helps.
Cheers

Resources