In my app I need some animation but if it already animated it doens't need to have a duration. But my problem is that it automatically adds a duration.
Here you can see 2 functions, second one is without duration but it really has a duration (from maybe 1 second) , the first function has a duration (and it should have and that's okay) but it isn't 0.6 seconds because if I set it on 30 it still animates very fast.
What am I doing wrong, thanks in advance!
func openMessage() {
UIView.animate(withDuration: 0.6, delay: 0.0, options: [], animations: {
var t = CATransform3DIdentity;
t = CATransform3DMakeRotation(CGFloat(3 * Float.pi / 4), 0, 0, 1)
self.moveableLineLayer.transform = t;
}, completion:{(finished:Bool) in })
}
func openMessageWithoutAnimation() {
self.moveableLineLayer.transform = CATransform3DIdentity
var t = CATransform3DIdentity;
t = CATransform3DMakeRotation(CGFloat(3 * Float.pi / 4), 0, 0, 1)
self.moveableLineLayer.transform = t;
}
Consider using the velocity parameter. From the documentation:
velocity
The initial spring velocity. For smooth start to the animation, match this value to the view’s velocity as it was prior to attachment.
A value of 1 corresponds to the total animation distance traversed in one second. For example, if the total animation distance is 200 points and you want the start of the animation to match a view velocity of 100 pt/s, use a value of 0.5.
Related
Is it normal to have very low FPS (~7fps to ~10fps) with Sprite Kit using the code below?
Use case:
I'm drawing just lines from bottom to top (1024 * 64 lines). I have some delta value that determines the positions of a single line for every frame. These lines represent my CGPath, which is assigned to the SKShapeNode every frame. Nothing else. I'm wondering about the performance of SpriteKit (or maybe of Swift).
Do you have any suggestions to improve the performance?
Screen:
Code:
import UIKit
import SpriteKit
class SKViewController: UIViewController {
#IBOutlet weak var skView: SKView!
var scene: SKScene!
var lines: SKShapeNode!
let N: Int = 1024 * 64
var delta: Int = 0
override func viewDidLoad() {
super.viewDidLoad()
scene = SKScene(size: skView.bounds.size)
scene.delegate = self
skView.showsFPS = true
skView.showsDrawCount = true
skView.presentScene(scene)
lines = SKShapeNode()
lines.lineWidth = 1
lines.strokeColor = .white
scene.addChild(lines)
}
}
extension SKViewController: SKSceneDelegate {
func update(_ currentTime: TimeInterval, for scene: SKScene) {
let w: CGFloat = scene.size.width
let offset: CGFloat = w / CGFloat(N)
let path = UIBezierPath()
for i in 0 ..< N { // N -> 1024 * 64 -> 65536
let x1: CGFloat = CGFloat(i) * offset
let x2: CGFloat = x1
let y1: CGFloat = 0
let y2: CGFloat = CGFloat(delta)
path.move(to: CGPoint(x: x1, y: y1))
path.addLine(to: CGPoint(x: x2, y: y2))
}
lines.path = path.cgPath
// Updating delta to simulate the changes
//
if delta > 100 {
delta = 0
}
delta += 1
}
}
Thanks and Best regards,
Aiba ^_^
CPU
65536 is a rather large number. Telling the CPU to comprehend this many loops will always result in slowness. For example, even if I make a test Command Line project that only measures the time it takes to run an empty loop:
while true {
let date = Date().timeIntervalSince1970
for _ in 1...65536 {}
let date2 = Date().timeIntervalSince1970
print(1 / (date2 - date))
}
It will result in ~17 fps. I haven't even applied the CGPath, and it's already appreciably slow.
Dispatch Queue
If you want to keep your game at 60fps, but your rendering of specifically your CGPath may be still slow, you can use a DispatchQueue.
var rendering: Bool = false // remember to make this one an instance value
while true {
let date = Date().timeIntervalSince1970
if !rendering {
rendering = true
let foo = DispatchQueue(label: "Run The Loop")
foo.async {
for _ in 1...65536 {}
let date2 = Date().timeIntervalSince1970
print("Render", 1 / (date2 - date))
}
rendering = false
}
}
This retains a natural 60fps experience, and you can update other objects, however, the rendering of your SKShapeNode object is still quite slow.
GPU
If you'd like to speed up the rendering, I would recommend looking into running it on the GPU instead of the CPU. The GPU (Graphics Processing Unit) is much better fitted for this, and can handle huge loops without disturbing gameplay. This may require you to program it as an SKShader, in which there are tutorials for.
Check the number of subdivisions
No iOS device has a screen width over 3000 pixels or 1500 points (retina screens have logical points and physical pixels where a point is equivalent to 2 or 3 pixels depending on the scale factor; iOS works with points, but you have to also remember pixels), and the ones that even come close are those with the biggest screens (iPad Pro 12.9 and iPhone Pro Max) in landscape mode.
A typical device in portrait orientation will be less than 500 points and 1500 pixels wide.
You are dividing this width into 65536 parts, and will end up with pixel (not even point) coordinates like 0.00, 0.05, 0.10, 0.15, ..., 0.85, which will actually refer to the same pixel twenty times (my result, rounded up, in an iPhone simulator).
Your code draws twenty to sixty lines in the exact same physical position, on top of each other! Why do that? If you set N to w and use 1.0 for offset, you'll have the same visible result at 60 FPS.
Reconsider the approach
The implementation will still have some drawbacks, though, even if you greatly reduce the amount of work to be done per frame. It's not recommended to advance animation frames in update(_:) since you get no guarantees on the FPS, and you usually want your animation to follow a set schedule, i.e. complete in 1 second rather than 60 frames. (Should the FPS drop to, say, 10, a 60-frame animation would complete in 6 seconds, whereas a 1-second animation would still finish in 1 second, but at a lower frame rate, i.e. skipping frames.)
Visibly, what your animation does is draw a rectangle on the screen whose width fills the screen, and whose height increases from 0 to 100 points. I'd say, a more "standard" way of achieving this would be something like this:
let sprite = SKSpriteNode(color: .white, size: CGSize(width: scene.size.width, height: 100.0))
sprite.yScale = 0.0
scene.addChild(sprite)
sprite.run(SKAction.repeatForever(SKAction.sequence([
SKAction.scaleY(to: 1.0, duration: 2),
SKAction.scaleY(to: 0.0, duration: 0.0)
])))
Note that I used SKSpriteNode because SKShapeNode is said to suffer from bugs and performance issues, although people have reported some improvements in the past few years.
But if you do insist on redrawing the entire texture of your sprite every frame due to some specific need, that may indeed be something for custom shaders… But those require learning a whole new approach, not to mention a new programming language.
Your shader would be executed on the GPU for each pixel. I repeat: the code would be executed for each single pixel – a radical departure from the world of SpriteKit.
The shader would access a bunch of values to work with, such a normalized set of coordinates (between (0.0,0.0) and (1.0,1.0) in a variable called v_tex_coord) and a system time (seconds elapsed since the shader has been running) in u_time, and it would need to determine what color value the pixel in question would need to be – and set it by storing the value in the variable gl_FragColor.
It could be something like this:
void main() {
// default to a black color, or a three-dimensional vector v3(0.0, 0.0, 0.0):
vec3 color = vec3(0.0);
// take the fraction part of the time in seconds;
// this value will go from 0.0 to 0.9999… every second, then drop back to 0.0.
// use this to determine the relative height of the area we want to paint white:
float height = fract(u_time);
// check if the current pixel is below the height of the white area:
if (v_tex_coord.y < height) {
// if so, set it to white (a three-dimensional vector v3(1.0, 1.0, 1.0)):
color = vec3(1.0);
}
gl_FragColor = vec4(color,1.0); // the fourth dimension is the alpha
}
Put this in a file called shader.fsh, create a full-screen sprite mySprite, and assign the shader to it:
mySprite.shader = SKShader.init(fileNamed: "shader.fsh")
Once you display the sprite, its shader will take care of all of the rendering. Note, however, that your sprite will lose some SpriteKit functionalities as a result.
I'm trying to create a game using Apple's SpriteKit game engine.
While implementing some physics-based calculations in the game, I noticed that the calculated results differ from what effectively then happens to objects.
Example: calculating a body's trajectory through projectile motion's equations causes the body to actually fall down much sooner/quicker than what calculated.
How can I make the physics engine match the real-world physics laws when calculating something gravity-related?
I think I know what's going on with the sample code you have supplied on GitHub, which I'll reproduce here as questions on SO should contain the code:
//
// GameScene.swift
// SpriteKitGravitySample
//
// Created by Emilio Schepis on 17/01/2020.
// Copyright © 2020 Emilio Schepis. All rights reserved.
//
import SpriteKit
import GameplayKit
class GameScene: SKScene {
private var subject: SKNode!
override func didMove(to view: SKView) {
super.didMove(to: view)
// World setup (no friction, default gravity)
// Note that this would work with any gravity set to the scene.
physicsBody = SKPhysicsBody(edgeLoopFrom: frame)
physicsBody?.friction = 0
subject = SKShapeNode(circleOfRadius: 10)
subject.position = CGPoint(x: frame.midX, y: 30)
subject.physicsBody = SKPhysicsBody(circleOfRadius: 10)
subject.physicsBody?.allowsRotation = false
// Free falling body (no damping)
subject.physicsBody?.linearDamping = 0
subject.physicsBody?.angularDamping = 0
addChild(subject)
// Set an arbitrary velocity to the body
subject.physicsBody?.velocity = CGVector(dx: 30, dy: 700)
// Inaccurate prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let inaccuratePosition = SKShapeNode(circleOfRadius: 2)
inaccuratePosition.strokeColor = .red
// These lines use the projectile motion equations as-is.
// https://en.wikipedia.org/wiki/Projectile_motion#Displacement
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * pow(time, 2)
inaccuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(inaccuratePosition)
}
// Actual prediction of position over time
for time in stride(from: CGFloat(0), to: 1, by: 0.01) {
let accuratePosition = SKShapeNode(circleOfRadius: 2)
accuratePosition.strokeColor = .green
// These lines use the projectile motion equations
// as if the gravity was 150 times stronger.
// The subject follows this curve perfectly.
let v = subject.physicsBody?.velocity ?? .zero
let x = v.dx * time
let y = v.dy * time + 0.5 * physicsWorld.gravity.dy * 150 * pow(time, 2)
accuratePosition.position = CGPoint(x: x + subject.position.x,
y: y + subject.position.y)
addChild(accuratePosition)
}
}
}
What you've done is to:
Created an object called subject with a physicsBody and placed it
on screen with a initial velocity.
Plotted predicted positions for an object with that velocity under
gravity via the inaccuratePosition node, using Newton's laws of
motion (v = ut + 1/2at²)
Plotted predicted positions for an object with that velocity under
gravity * 150 via the accuratePosition node, using Newton's laws of
motion
All this is is didMoveTo. When the simulation runs, the path of the node subject follows the accuratePosition path accurately.
I think what's happening is that you are calculating the predicted position using subject's physicsBody's velocity, which is in m/s, but the position is in points, so what you should do is convert m/s into point/s first.
So what's the scale factor? Well from Apple's documentation here; it's.... 150 which is too much of a coincidence 😀, so I think that's the problem.
Bear in mind that you set the vertical velocity of your object to 700m/s - that's 1500mph or 105000 SK point/s. You'd expect it to simply disappear out through the top of the screen at high speed, as predicted by your red path. The screen is somewhere between 1,000 and 2,000 points.
Edit - I created a sample project to demonstrate the calculated paths with and without the multiplier.
https://github.com/emilioschepis/spritekit-gravity-sample
TL;DR - When calculating something gravity-related in SpriteKit multiply the gravity of the scene by 151 to obtain an accurate result.
When trying to solve this issue I first started reading the SpriteKit documentation related to gravity:
https://developer.apple.com/documentation/spritekit/skphysicsworld/1449623-gravity
The documentation says:
The components of this property are measured in meters per second. The default value is (0.0,-9.8), which represent’s Earth’s gravity.
Gravity, however is calculated in m/s^2 and not in m/s.
Thinking that was an error in the implementation of gravity in SpriteKit I began thinking that maybe real-world-based physics laws could not be applied in the scene.
I did, however, come across another documentation page about the linear gravity field that correctly reported that gravity is measured in m/s^2 in SpriteKit.
https://developer.apple.com/documentation/spritekit/skfieldnode/1520145-lineargravityfield
I then setup a simple free falling scene where I applied an initial velocity to a physics body and then calculated the expected trajectory, while comparing it to the actual trajectory.
The x-axis calculations were accurate from the start, suggesting that the only problem was with the gravity's value.
I then tried manually modified the gravity in the scene until the actual trajectory matched the predicted one.
What I found is that there is a "magic" value of ~151 that has to be factored in when using the physics world's gravity property in the game.
Modifying, for example, the y-axis calculations for the trajectory from
let dy = velocity.dy * time + 0.5 * gravity * pow(time, 2)
to
let dy = velocity.dy * time + 0.5 * 151 * gravity * pow(time, 2)
resulted in accurate calculations.
I hope this is useful to anyone who might encounter the same problem in the future.
Im using the accelerometer as the steering mechanism for my game. It works fine usually but occasionally it will act very odd. Randomly there is suddenly a large amount of input delay and every rotation I make isn't registered for a while. In some cases there might be a lot of delay in between commands entered right after each other leading to my character drifting in one direction for far too long. Is this due to game lag or is my code at fault? My code below.
actionMoveLeft = SKAction.moveBy(x: -3, y: 0, duration: 0.1)
actionMoveRight = SKAction.moveBy(x: 3, y: 0, duration: 0.1)
self.addChild(ship)
if motionManager.isAccelerometerAvailable == true {
motionManager.startAccelerometerUpdates(to: OperationQueue.current!, withHandler:{
data, error in
if (data!.acceleration.y) < -0.05 {
self.ship.run(self.actionMoveLeft)
}
else if data!.acceleration.y > 0.05 {
self.ship.run(self.actionMoveRight)
}
})
}
It's not guaranteed that the accelerometer will give you updates with regular intervals of 0.1 seconds (assuming that the deviceMotionUpdateInterval property of your CMMotionManager instance is set to 0.1). SKActions work fine when every action is executed at a discrete interval of time. But since the accelerometer gives you irregular interval updates, you may end up executing more actions at the same time.
An easy fix for that would be to remove the previous action every time:
if (data!.acceleration.y) < -0.05 {
self.ship.removeAction(forKey: "Move")
self.ship.run(self.actionMoveLeft, withKey: "Move")
}
But I still not recommend to use this approach, because the movement still doesn't look smooth, but it looks like your ship is moving jerkily. I suggest to use a SKPhysicsBody and directly manipulate its velocity property. Something like this:
// Inside the update method, assuming you have saved the acceleration vector
// and you have a deltaTime variable that holds the difference of time
// elapsed from the previous update
self.ship.physicsBody.velocity = CGVector(dx: self.ship.physicsBody.velocity.dx + CGFloat(acceleration.x * deltaTime), dy: self.ship.physicsBody.velocity.dy + CGFloat(acceleration.y * deltaTime))
If you don't want to use a physics body because you're just handling physics using your custom functions, then I suggest to compute the position of the ship manually at every frame.
I'm working on a top-down space game built using Swift and SceneKit with the following setup:
SCNNode representing a spaceship
Rotation is constrained to the y axis; values range from -M_PI_2 to M_PI + M_PI_2
Movement is constrained to the x and z axes.
Game controller thumbstick input
Values range from -1.0 to 1.0 on the x and y axes.
When the game controller's thumbstick changes position, the spaceship should rotate using the physics body to match the thumbstick's radian.
The target radian of the thumbstick can be calculated with the following:
let targetRadian = M_PI_2 + atan2(-y, -x)
The current radian of the node can be obtained with the following:
let currentRadian = node.presentationNode.rotation.w * node.presentationNode.rotation.y
NSTimeInterval deltaTime provides the time in seconds since the last rotation calculation.
How can the node be rotated using angularVelocity, applyTorque, or another physics method to reach the targetRadian?
The difference between the targetRadian and the currentRadian ranged from 0.0 to -2π depending on the value of currentRadian. This equation will determine the shortest direction to turn, .Clockwise or .CounterClockwise, to reach the targetRadian:
let turnDirection = (radianDifference + (M_PI * 2)) % (M_PI * 2) < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
Using applyTorque, there is a possibility to over-rotate past the targetRadian resulting in a wobbling effect, like a compass magnetizing toward a point, as the rotation changes direction back and forth to reach the targetRadian. The following, while not a perfect solution, dampened the effect:
let turnDampener = abs(radianDifference) < 1.0 ? abs(radianDifference) : 1.0
The complete solution is thus:
enum RotationDirection: Double {
case Clockwise = -1.0
case CounterClockwise = 1.0
}
func rotateNodeTowardDirectionalVector(node: SCNNode, targetDirectionalVector: (x: Double, y: Double), deltaTime: NSTimeInterval) {
guard abs(targetDirectionalVector.x) > 0.0 || abs(targetDirectionalVector.y) > 0.0 else { return }
let currentRadian = Double(node.presentationNode.rotation.w * node.presentationNode.rotation.y)
let targetRadian = M_PI_2 + atan2(-targetDirectionalVector.y, -targetDirectionalVector.x)
let radianDifference = targetRadian - currentRadian
let π2 = M_PI * 2
let turnDirection = (radianDifference + π2) % π2 < M_PI ? RotationDirection.CounterClockwise : RotationDirection.Clockwise
let absRadianDifference = abs(radianDifference)
let turnDampener = absRadianDifference < 1.0 ? absRadianDifference : 1.0
node.physicsBody?.applyTorque(SCNVector4Make(0, CGFloat(turnDirection.rawValue), 0, CGFloat(deltaTime * turnDampener)), impulse: true)
}
I was faced with a problem today when I wanted to make a 360° rotation of a spritenode before go to a specified angle, I found a way but I don't know if this is the best.
I tried many ways and this code doesn't work as expected:
let angle = CGFloat(self.angle * number) // angle in degrees
let flip = CGFloat(360+angle).degreesToRadians // 360°+angle in radians
SKAction.rotateToAngle(-flip, duration: 0.4, shortestUnitArc:false)
After trying differents methods this code works as expected, it makes a complete loop before rotate to the angle. Is this the best way?
let angle = CGFloat(self.angle * number)
let flip = CGFloat(360+angle).degreesToRadians
let needleTurn = SKAction.sequence([
SKAction.rotateToAngle(-flip/2, duration: 0.2, shortestUnitArc:true),
SKAction.rotateToAngle(-flip, duration: 0.2, shortestUnitArc:false)
])