simplest animation causes high cpu usage and very high energy impact SpriteKit - animation

Here is the simplest animation: a blackhole is rotating in the middle of the screen. These two lines of code increase cpu usage from 3% to 31% and energy impact from low to high (sometimes even very high):
let actionLoop = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(360), duration: 1000))
hole.run(actionLoop)
Is this normal? I've read more or less similar discussions but haven't find a clear answer. Here is the whole code and screens:
import SpriteKit
import GameplayKit
class GameScene: SKScene {
// Basic for dynamic sizes step01
var width = CGFloat()
var height = CGFloat()
override func didMove(to view: SKView) {
// Basic for dynamic sizes step02
width = self.frame.size.width
height = self.frame.size.height
// test background color
self.backgroundColor = .yellow
// set blackhole
let hole = SKSpriteNode(imageNamed: "blackhole")
let startPosition = CGPoint(x: 0, y: 0)
hole.size = CGSize(width: width/8, height: width/8)
hole.position = CGPoint(x: startPosition.x, y: startPosition.y)
let actionLoop = SKAction.repeatForever(SKAction.rotate(byAngle: CGFloat(360), duration: 1000))
hole.run(actionLoop)
self.addChild(hole)
}
}
Update: And last but not least: it is not a simulator. I test it on the real device.

There are a couple of things your code does which might have impacted the efficiency.
First, SKAction rotation angles are calculated in radians (for the 360 degrees it should be 2*PI, which is roughly 6.28, not 360). As it stands, your code does lots of calculations to 'over-rotate' the sprite, which is wasteful. A better statement would be:
SKAction.rotate(byAngle: CGFloat.pi*2, duration: 1000)
Second, before rotating the sprite, you scale it by half using the corresponding function, which is also a bit wasteful, since each time the rotation is calculated, the scaling is also re-calculated to produce an accurate result. I'd suggest to pre-render a scaled-down version of the sprite and use that instead to save calculation times and load.

Related

How to create a series of UIView animations that start with .curveEaseIn and end with .curveEaseOut, and move at a steady pace

I wrote the following little demo that rotates a UIView 360° by rotating it 90° at a time. That means the animation has 4 steps
I wanted it to ease in on the first animation step, go at a steady pace for the middle 2 steps, and then use ease out timing for the last step so it coasts to a stop. The code is below. Here is the animation timing it uses:
Animating to 90°, options = curveEaseIn
Animating to 180°, options = curveLinear
Animating to 270°, options = curveLinear
Animating to 0°, options = curveEaseOut
Each step takes 1/2 second, for a total duration of 2 seconds. However, since the first and last steps take 1/2 second but start/end at a slower pace, the "full speed" part of those animation steps is noticeably faster than the middle 2 animation steps that use linear timing. The animation is not smooth as a result.
Is there an easy way to adjust the step timing so each of the steps in the animation runs at the same pace as the beginning/end step that has ease in/ease out timing?
I guess I could instead create a keyframe animation where the entire animation uses ease-in/ease-out timing, and the intermediate steps inside the animation use linear timing. It seems like there should be an easy way to get that out of step-wise animation however.
class ViewController: UIViewController {
var rotation: CGFloat = 0
#IBOutlet weak var rotateableView: RotatableView!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
#IBAction func handleRotateButton(_ sender: UIButton? = nil) {
let duration = 0.5
var optionsValue: (options: UIView.AnimationOptions, optionsName: String) = (.curveLinear, "curveLinear")
self.rotation = fmod(self.rotation + 90, 360)
sender?.isEnabled = false
if self.rotation == 0 {
optionsValue = (.curveEaseOut, "curveEaseOut") // Ending the animatino, so ease out.
} else if self.rotation == 90 {
optionsValue = (.curveEaseIn, "curveEaseIn") // Beginning the animation, so ease in
}
let rotation = String(format:"%3.0f", self.rotation)
print("Animating to \(rotation)°, options = \(optionsValue.optionsName)")
UIView.animate(withDuration: duration, delay: 0, options: optionsValue.options) {
let angle = self.rotation / 180.0 * CGFloat.pi
self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
} completion: { finished in
if self.rotation != 0 {
self.handleRotateButton(sender)
} else {
self.rotation = 0
sender?.isEnabled = true
}
}
}
}
Ok, I couldn't figure out how to adjust the timing of a series of step-wise animations to get them to ease into the first step, use linear timing for the intermediate steps, and use ease out on the final steps.
However, keyframe animation apparently defaults to ease-in, ease-out timing for the whole animation. That makes creating ease-in, ease-out timing for a whole sequence of steps very easy. It looks like this:
#IBAction func handleRotateButton(_ sender: UIButton? = nil) {
UIView.animateKeyframes(withDuration: 1.5, delay: 0, options: []) {
for index in 1...4 {
let startTime = Double(index-1) / 4
UIView.addKeyframe(withRelativeStartTime: startTime,
relativeDuration: 0.25,
animations: {
let angle: CGFloat = CGFloat(index) / 2 * CGFloat.pi
self.rotateableView.transform = CGAffineTransform.init(rotationAngle: angle)
}
)
}
} completion: { completed in
self.rotation = 0
sender?.isEnabled = true
}
}
Further, apparently you can pass in the same animation curve flags into UIView.animateKeyframes() that you can use for the options to animate(withDuration:delay:options:animations:completion:), if you simply create UIView.KeyframeAnimationOptions using the raw value from UIView.AnimationOptions. You can do that with an extension like this:
extension UIView.KeyframeAnimationOptions {
init(animationOptions: UIView.AnimationOptions) {
self.init(rawValue: animationOptions.rawValue)
}
}
Then you can use code like this to create KeyframeAnimationOptions from AnimationOptions:
let keyframeOptions = UIView.KeyframeAnimationOptions(animationOptions: .curveLinear)

Animating a view in a real time

I am trying to create animation that is connected to real time.
I have two points on my screen and an image moves between them. So for example I have an image that is at the Strat point at 12:00 and I want it to be at the end point at 12:10.
func animate_this_image(start: Date, end: Date, imageView: UIImageView, start_point: CGPoint, end_point: CGPoint) {
let duration = end.timeIntervalSince(start)
let frame = CGRect(x: end_point.x, y: end_point.y, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
UIView.animate(withDuration: duration) {
imageView.frame = frame
}
}
My problem is that I want it to be connected to the actual clock. So for example if I close the view at 12:02 and open it at 12:09 I want the image to be at the correct place between the two point (roughly at the end). Instead at the moment the image starts again from the start point. That is happening because I just take the two dates and use the difference as the animation duration.
Anyone that has had similar problem or a suggestion on how I can solve this?
This is connected to a map. I have two points and a poly line between the points. I wanted an image to glide between the points. I could not figure anything easy with the map so I created a view on top of the map that has the image view on. I transfer the points from the map to the other view to get the start and stop points. Maybe there is a better way to do this by using the map?
Cheers,
Jonas
This was easier than I thought... this is my solution
func animate_this_image(start: Date, end: Date, imageView: UIImageView, start_point: CGPoint, end_point: CGPoint) {
// find the total duration and the time left until reaching next point
let duration = end.timeIntervalSince(start)
let time_left = end.timeIntervalSince(Date())
// the percentage is used to calculate how far is left on the line
let percentage = time_left / duration
//calculate the correct point at this time
let x_dist = (end_point.x - start_point.x) * CGFloat(percentage)
let y_dist = (end_point.y - start_point.y) * CGFloat(percentage)
imageView.frame = CGRect(x: end_point.x - x_dist, y: end_point.y - y_dist, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
// define end point
let frame = CGRect(x: end_point.x, y: end_point.y, width: 60, height: 60).offsetBy(dx: -30, dy: -30)
// Then animate it
UIView.animate(withDuration: time_left) {
imageView.frame = frame
}
}

How do I deal with fullscreen in a 2d game?

I'm making a game using Go with the rendering library "pixel".
I am trying to get fullscreen/resizing working, but I have an issue, and that's how to deal with stretching of images
I've seen and have used the solution of a "letterbox" effect for the game.
That is, drawing the game in the same aspect ratio, so stretching wouldn't be an issue, leaving the extra space as black bars.
My issue is, when trying to do this in this rendering library, I can only scale the matrix of the "Canvas" I'm drawing on.
I'm used to SFML with C++ where I can just define a fixed size for the "View" [what's being drawn on], not scaling it.
This is how I'm getting the current scaling for the matrix, it's incorrect, but it's what I have.
camZoom is 2.0, it's in there so the screen is bigger. If the camZoom is 1.0 [normal], the images are too small.
func letterBox(win *pixelgl.Window) {
windowRatio := winWidth / winHeight
viewRatio := win.Bounds().W() / win.Bounds().H()
sizeX := 1.
sizeY := 1.
horizontalSpacing := true
if windowRatio < viewRatio {
horizontalSpacing = false
}
if horizontalSpacing {
sizeX = viewRatio / windowRatio
} else {
sizeY = windowRatio / viewRatio
}
viewMatrix = pixel.IM.
Moved(pixel.V(win.Bounds().Center().X/camZoom, win.Bounds().Center().Y/camZoom)).
ScaledXY(pixel.V(win.Bounds().Center().X/camZoom, win.Bounds().Center().Y/camZoom), pixel.V(sizeY, sizeX))
}
Here's what it currently looks like:
Normal [no resizing done, 1024x768]:
Width of window increased [shrinks]
[]2
Height of window increased [stretches on X, hiding most of 'Canvas']
[]3
Fullscreen [just keeps it's original size, but the width of the 'canvas' is slightly shrunken]
[]4
I just can't really figure out the math to it.
If this is not the best way to solve the full-screen issue I have, then let me know and I can make another question, but I was told this is how you should do it.
Ended up being super simple just a math error on my part.
func letterBox(win *pixelgl.Window) {
sizeX := 1.
sizeY := 1.
if win.Bounds().H()-winHeight > win.Bounds().W()-winWidth {
sizeX = win.Bounds().W() / winWidth
sizeY = win.Bounds().W() / winWidth
} else {
sizeX = win.Bounds().H() / winHeight
sizeY = win.Bounds().H() / winHeight
}
viewMatrix = pixel.IM.
Moved(pixel.V(win.Bounds().Center().X/camZoom, win.Bounds().Center().Y/camZoom)).
ScaledXY(pixel.V(win.Bounds().Center().X/camZoom, win.Bounds().Center().Y/camZoom), pixel.V(sizeX, sizeY))
}
Just had to figure out if the width difference of the newly-sized window was greater, or the height, then apply the same ratio [new window width / width of old frame] to scale the matrix properly. Dumb answer to a dumb question.

How to get the same layout dimensions for all iPhones in my sprite kit game Swift

This is the code the set the layout for my game the problem is that when my game is play on a smaller version phone the layout changes how do i keep the same dimensions
Code:
override func viewWillLayoutSubviews() {
super.viewWillLayoutSubviews()
if let scene = GameScene.unarchiveFromFile("GameScene") as? GameScene {
let sKView = self.view! as! SKView
sKView.ignoresSiblingOrder = true
scene.size = sKView.bounds.size
let reveal = SKTransition.fadeWithDuration(0.45)
sKView.presentScene(scene, transition: reveal)
}
}
The code above doesn't work well it does but only for iPhone 6 plus which is the iPhone I'm test my application with but it doesn't change dimensions when on smaller iPhones.
The only way that I'm aware of is to manually resize and move nodes based on screen size, for example:
if size.width == 320 && size.height == 568 {
//iPhone 5/5S screen dimensions
spriteNode!.size = CGSize(height: 30, width: 45)
spriteNode!.position = CGPoint(x: 100, y: 50)
} else if size.width == 768 && size.height == 1024 {
//iPad screen dimensions
spriteNode!.size = CGSize(height: 60, width: 90) /* Twice the normal size */
spriteNode!.position = CGPoint(x: 200, y: 80) /* Moved over and above a bit */
}
It's a pain in the neck, but AFAIK it's the easiest way to resize nodes to fit different screen sizes.
The way I'd usually go about this is as follows: when creating the scene, I will set the size to specific defaults based on the device; eg: for landscape only mode (sizes are just for example):
let size =
UIDevice.currentDevice().userInterfaceIdiom == .Pad ?
CGSizeMake(3200, 2400) : // all ipads, 4:3 ratio
UIScreen.mainScreen().bounds.landscapeSize().width < 568 ?
CGSizeMake(2400, 1600) : // iphone 4s rows; 3:2 ratio
CGSizeMake(2850, 1600) // other iphones 16:9 ratio
This allows me to have known sizes for the scene on any device.
To layout UI elements, I use the scene size and use offsets, so a button at the top left will have coordinates (0, scene.height) (for a sprite anchored to (0,1)), and that button will always appear on the top left of any scene in any device.
It is usually better to have a node hiearchy in your scene
Scene
BackgroundNode: SKNode, zPosition = 0
WorldNode: SKNode, zPosition = 10
HudNode: SKNode, zPosition = 20
This will allows you to zoom in / zoom out on the world node (which contain gameplay elements) while keeping UI elements (buttons) in the HUD node a fixed size.
To zoom in/out you modify the worldNode scale directly, for example scene.worldNode.setScale(scaleAmount). (assuming you are not using SKCameraNode)
This scheme will allow you to not have to position/resize individual sprites for each device; rather changes are done at the root level - in this case the worldNode, and also allows you to have UI elements that more or less scale appropriately based on device, and remain fixed independent of your worldNode scale.

radialGravityField goes nuts after a while

I'm trying to create a SpriteKit game using Swift that looks somewhat like a 2D gravity simulator, but when my objects approach a bit they go to the opposite direction with crazy speeds.
This may be related with the minimumRadius, categoryBitMask or fieldBitMask properties on the field and physicsBody, but I don't know exactly.
Here's the relevant code:
override func mouseDown(theEvent: NSEvent) {
let location = theEvent.locationInNode(self)
let sprite = SKShapeNode(circleOfRadius: 5)
sprite.fillColor = SKColor(red: 0, green: 0.5, blue: 1, alpha: 1)
sprite.strokeColor = SKColor.clearColor()
sprite.position = location
sprite.physicsBody = SKPhysicsBody(circleOfRadius: 5)
let gravityField = SKFieldNode.radialGravityField()
//gravityField.minimumRadius = 5
sprite.addChild(gravityField)
self.addChild(sprite)
}
And a gif demonstration of the problem:
As you can see, the distance that the objects need to move to trigger the issue seems to be random.

Resources