Is there a clever way to set a total duration for a sequence of SKActions? - objective-c

I think the answer is no, but here’s what I’m doing and I’ll then show what I wish I could do. Overall, what I’m going for is a sequence of dampening rotations that total 24 seconds total.
#define START_ANGLE M_PI/4.0
#define SPRING_TENSION 0.9
#define START_DURATION 1.3 // Hack!
NSMutableArray *animationA = [NSMutableArray array];
double a = START_ANGLE;
double d = START_DURATION;
while (a * a > 0.01) {
// Bounce to the other side.
SKAction *a1 = [SKAction rotateToAngle:a duration:d];
a1.timingMode = SKActionTimingEaseIn;
[animationA addObject:a1];
// Return to rest
SKAction *a2 = [SKAction rotateToAngle:0 duration:d];
a2.timingMode = SKActionTimingEaseOut;
[animationA addObject:a2];
// Reduce the height of the bounce by the spring's tension
a *= -SPRING_TENSION;
d *= SPRING_TENSION;
}
SKAction *seqA = [SKAction sequence:animationA];
[mySprite runAction:seqA];
What I want is for the total sequence duration to be 24 seconds and for the intervening actions to be inferred from that. Since the API doesn’t allow that, in order to arrive at this solution I had to precompute how many iterations of the loop there’d be and track the angle’s attentuation in order to arrive at START_DURATION.
It’s hacky and inflexible -- bad enough that I had to unroll the loop to figure out each component duration, but what if I want to change the SPRING_TENSION later but maintain the total duration? I’d have to unroll the loop again and recompute START_DURATION.
I wish I could do something more flexible like this:
#define START_ANGLE M_PI/4.0
#define SPRING_TENSION 0.9
#define TOTAL_DURATION 24.0
NSMutableArray *animationA = [NSMutableArray array];
double a = START_ANGLE;
while (a * a > 0.01) {
// Bounce to the other side.
SKAction *a1 = [SKAction rotateToAngle:a]; // Duration will be computed from total duration
a1.timingMode = SKActionTimingEaseIn;
[animationA addObject:a1];
// Return to rest
SKAction *a2 = [SKAction rotateToAngle:0]; // Duration will be computed from total duration
a2.timingMode = SKActionTimingEaseOut;
[animationA addObject:a2];
// Reduce the height of the bounce by the spring's tension
a *= -SPRING_TENSION;
d *= SPRING_TENSION;
}
SKAction *seqA = [SKAction sequence:animationA duration:TOTAL_DURATION]; // This computes all undefined component durations
[mySprite runAction:seqA];
That way, I get what I want: I get an animation sequence of a chosen duration and let the framework figure out the intervening rotations. This is akin to how keyframes are inferred by CAKeyframeAnimation. Is there a clever way to do this with SpriteKit that I’ve overlooked?

Related

how to increase the value of an NSNumber every 5 seconds?

i'm building a game for which i the gravity as:
self.physicsworld.gravity = CGVectorMake(gravityX , gravity-Y);
i want to change the forces acting on the sprite node with time.
since i'm a newbie, i'm not able to code a loop(s) so that the gravity changes over time thereby changing the difficulty of the game. if i want the float variable gravityX to increment by 0.5 every 5 seconds what should be the code implemented.
the main problem i'm facing with using the FOR LOOP here is in the CONDITION; I don't know how to make the computer understand actual time since the game started.
This can be done using the SKAction class. Add the following method to your SKScene subclass, and call it when you want gravity to start increasing. I've included both the Objective-C and the Swift version.
Objective-C:
- (void)startGravityIncrease {
SKAction *blockAction = [SKAction runBlock:^{
CGVector gravity = self.physicsWorld.gravity;
gravity.dx += 0.5;
self.physicsWorld.gravity = gravity;
}];
SKAction *waitAction = [SKAction waitForDuration:5];
SKAction *sequenceAction = [SKAction sequence:#[waitAction, blockAction]];
SKAction *repeatAction = [SKAction repeatActionForever:sequenceAction];
[self runAction:repeatAction];
}
Swift:
func startGravityIncrease() {
let blockAction = SKAction.runBlock { () -> Void in
self.physicsWorld.gravity.dx += 0.5
}
let waitAction = SKAction.waitForDuration(5)
let sequenceAction = SKAction.sequence([waitAction, blockAction])
let repeatAction = SKAction.repeatActionForever(sequenceAction)
self.runAction(repeatAction)
}

How to reference the SKSpriteNode that's running an SKAction runblock?

I have a method that creates a random amount of SKSpriteNodes that are supposed to fall from the roof, sit on the ground a little while, then fade out. They need an SKPhysicsBody so they'll fall and bounce properly, but when I create a lot, it's using a lot of CPU, so I was trying to remove their physicsbody when they'd been sitting on the ground a little while.
I can't figure out how to use a runBlock to just say "do (blah) to the object calling this runBlock", is there a way?
for (int i=0; i < howManyDollars; i++) {
SKSpriteNode *bills = [SKSpriteNode spriteNodeWithImageNamed:#"bills.png"];
int startX = arc4random() % (int)self.size.width/3;
int startY = self.size.height;
bills.position = CGPointMake(startX, startY);
bills.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake(bills.size.width/2, bills.size.height/2)];
bills.physicsBody.affectedByGravity = YES;
[self addChild:bills];
SKAction *wait1 = [SKAction waitForDuration:1.5];
SKAction *wait2 = [SKAction waitForDuration:2];
SKAction *removeDynamics = [SKAction runBlock:^(void) {
// this is the spot I am confused at.
//bills.physicsBody = nil;
}];
SKAction *fade = [SKAction fadeOutWithDuration:15];
SKAction *remove = [SKAction removeFromParent];
[bills runAction:[SKAction sequence:#[wait1, removeDynamics, wait2, fade, remove]]];
}
You have it already:
SKAction *removeDynamics = [SKAction runBlock:^(void) {
bills.physicsBody = nil;
NSLog(#"bills is: %# (%p)", bills, bills);
}];
The cool thing about blocks is that they retain (copy) the objects in local scope. So if you create a block like the above and dereference the bills object, then for each block it will use the corresponding bills object that runs the action.
I've added the log statement so you can see for yourself that each block when executed is referencing a different object even though the variable name is the same and even though one would think based on the traditional sequential programming model that each bills object would have long been gone or referencing only the last object created.

Why ever use `keyTimes`, `timingFunctions` or `timingFunction` on CAKeyframeAnimation?

Interpolating the values with a custom function is very easy. But is it bad practice? Should I instead (or in addition) use keyTimes, timingFunctions or timingFunction to explain the animation-curve to the framework? When working with custom animation curves I really don't see why I should use those properties. I want to do this right.
This works just fine. As expected it animates the views-position with a custom cubic-ease-out animation curve:
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position.x"];
anim.duration = 5;
NSUInteger numberOfFrames = anim.duration * 60;
NSMutableArray *values = [NSMutableArray new];
for (int i = 0; i < numberOfFrames; i++)
{
CGFloat linearProgress = (double) i / (double) numberOfFrames;
CGPoint position = view.layer.position;
position.x = 10 + (300 * CubicEaseOut(linearProgress));
[values addObject:[NSValue valueWithCGPoint:position]];
}
anim.values = values;
[view.layer addAnimation:anim forKey:#"position"];
Your method adds 300 keyframes to the animation, for Core Animation to interpolate, linearly. There are two reasons this might not be worse than using fewer keyframes with non-linear interpolation to to get the same result: (1) more data to send to CA, i.e. more data to store and read every animation frame; (2) if you ever wanted to slow down the animation so that more than 300 frames are rendered from it the linear interpolation artifacts may become visible.
If you just have one 3s animation, it's likely neither of those reasons are important, but e.g. if you had 100 10s animations all running at once you may see worse performance than if using fewer keyframes.

How many times a second should CADisplayLink's displayLink be called?

I have a CADisplayLink running in line with Chipmunk Physics, and I'm getting very slow performance. I put an NSLog in the method that's called on the CADisplayLink update, and it's being called an average of 22 times per second. I was under the impression that that should be nearer 60. I have the frameInterval set to 1, so should it be 60fps, in a perfect world? The delta times are averaging around 0.0167 seconds (and 1 / 60 IS 0.0167, which is confusing me even further).
I just have four walls around the bounds of my screen and just eight circle-shaped bodies on-screen, updating to UIButton instances on each call, so I don't think I'm doing anything that should tax it to this extent on both my 4S and iPad3. I'm applying a random force to each button once every 2.5 seconds in a separate method. Running in the simulator is butter-smooth, so it's a device-only issue. Can anyone help me spot what's causing the slowdown here, and what I can do about it?
Here's the relevant code, first that which sets up the link:
[NSTimer scheduledTimerWithTimeInterval: 2.5f target: self selector: #selector(updateForces) userInfo: nil repeats: YES];
_displayLink = [CADisplayLink displayLinkWithTarget: self selector: #selector(update)];
_displayLink.frameInterval = 1;
[_displayLink addToRunLoop: [NSRunLoop mainRunLoop] forMode: NSRunLoopCommonModes];
Here's the method that should be called (I think!) 60 times per second, but is called only 22 or so:
if (!gameIsPaused) {
cpFloat dt = _displayLink.duration * _displayLink.frameInterval;
cpSpaceStep([[AGChipmunkSpace sharedInstance] space], dt);
for (LCBall *i in balls) {
cpVect pos1 = cpBodyGetPos(i.body);
CGAffineTransform trans1 = CGAffineTransformMakeTranslation(pos1.x, pos1.y);
CGAffineTransform rot1 = CGAffineTransformMakeRotation(cpBodyGetAngle(i.body));
i.button.transform = CGAffineTransformConcat(rot1, trans1);
}
}
And finally, here's the method that's called every 2.5 seconds, to apply the random forces (updateForces):
if (!gameIsPaused) {
for (LCBall *i in balls) {
int randomAngle = arc4random() % 360;
CGPoint point1 = [self getVectorFromAngle: randomAngle AndMagnitude: (arc4random() % 40) + ((arc4random() % 20) + 15)];
i.body -> f = cpv(point1.x, point1.y);
}
}
(Also, here's my method to get a vector from an angle, which I doubt is causing the issue):
angle = (angle / 180.0) * M_PI;
float x = magnitude * cos(angle);
float y = magnitude * sin(angle);
CGPoint point = CGPointMake(x, y);
return point;
Turns out I had a method on a different UIViewController in my storyboard that was firing every 0.1 seconds that hadn't turned off, and combined with the physics processing, was bogging things down.

UIView Delta Time

I'm animating a UIView by updating its anchorpoint 60 times a second using an NSTimer.
The location of the UIView changes depending on its angle, so it always appears to be down relative to the device...
However, the NSTimer doesn't fire precisely 60 times a second. It's always a little off, causing jerky animation. I've searched this a lot, I know a bit about delta time, but I don't know how to apply it to my situation.
Here's the movement code I'm using:
float rotation = 0;
if (leftSideIsBeingHeldDown) {
rotation += (0.05f/rotationFactor);
} else if (rightSideIsBeingHeldDown) {
rotation -= (0.05f/rotationFactor);
}
movementX += -sinf(rotation);
movementY += -cosf(rotation);
float finalX = 0.0001 * movementX;
float finalY = 0.0001 * movementY;
mapView.layer.anchorPoint = CGPointMake(finalX, finalY);
mapView.transform = CGAffineTransformMakeRotation(rotation);
Does anyone know how to apply delta time to this?
You might want to look into the CADisplayLink class which provides you a timer that is tied to the display refresh rate. It should be a better solution than an NSTimer in this case.
Additionally, you need to remember the time of each "tick" and calculate the rotation or movement that should have been done since the last tick. For example (pseudo-code):
- (void)displayLinkTick:(id)sender
{
NSTimeInterval timespan;
NSDate *now;
now = [NSDate date];
if (myPreviousTick) {
timespan = [now timeintervalSinceDate:myPreviousTick];
} else {
// The very first tick.
timespan = 0;
}
// Calculate the angle according to the timespan. You need a
// value that specifies how many degrees/radians you want to
// revolve per second and simply multiply that with the timespan.
angle += myRadiansPerSecond * timespan;
// You'd do the same with the position. I guess this involves
// minor vector math which I don't remember right now and am
// too lazy to look up. You need to have a distance per second
// which you multiply with the timespan. Together with the
// direction vector you can calculate the new position.
// At the end, remember when this tick ran.
[myPreviousTick release];
myPreviousTick = [now retain];
}
You want to record the time you last rotated, and the difference in time between then and now, and use that to work out a factor, which you can use to adjust the rotation and x/y values.
for example:
NSDate now = [NSDate now];
timeDiff = now - lastRotateTime;
factor = timeDiff / expectedTimeDiff;
x = x + xIncrement * factor;
y = y + yIncrement * factor;
angle = angle + angleIncrement * factor;
There are many better examples on game dev forums, which explain it in more detail.