applyForce(0, 400) - SpriteKit inconsistency
Asked Answered
U

2

16

So I have an object that has a physicsBody and gravity affects it. It is also dynamic.

Currently, when the users touches the screen, I run the code:

applyForce(0, 400)

The object moves up about 200 and then falls back down due to gravity. This only happens some of the time. Other times, it results in the object only moving 50ish units in the Y direction.

I can't find a pattern... I put my project on dropbox so it can be opened if anyone is willing to look at it.

https://www.dropbox.com/sh/z0nt79pd0l5psfg/bJTbaS2JpY

EDIT: It seems this happens when the player is bouncing off of the ground slightly for a moment after impact. Is there a way I can make it so the player doesn't bounce at all?

EDIT 2: I tried to solve this using the friction parameter and only allowing the player to "jump" when the friction was = 0 (you would think this would be all cases where the player was airborne) but friction appears to be greater than 0 at all times. How else might I detect if the player is touching an object (other than by using the y location)?

Thanks

Utricle answered 7/11, 2013 at 11:38 Comment(1)
The restitution property of the physics body controls the "bounciness" of the node.Diggins
H
51

Suggested Solution

If you're trying to implement a jump feature, I suggest you look at applyImpulse instead of applyForce. Here's the difference between the two, as described in the Sprite Kit Programming Guide:

You can choose to apply either a force or an impulse:

A force is applied for a length of time based on the amount of simulation time that passes between when you apply the force and when the next frame of the simulation is processed. So, to apply a continuous force to an body, you need to make the appropriate method calls each time a new frame is processed. Forces are usually used for continuous effects.

An impulse makes an instantaneous change to the body’s velocity that is independent of the amount of simulation time that has passed. Impulses are usually used for immediate changes to a body’s velocity.

A jump is really an instantaneous change to a body's velocity, meaning that you should apply an impulse instead of a force. To use the applyImpulse: method, figure out the desired instantaneous change in velocity, multiply by the body's mass, and use that as the impulse parameter into the function. I think you'll see better results.

Explanation for Unexpected Behavior

If you're calling applyForce: outside of your update: function, what's happening is that your force is being multiplied by the amount of time passed between when you apply the force and when the next frame of the simulation is processed. This multiplier is not a constant, so you're seeing a different change in velocity every time you call applyForce: in this manner.

Hasson answered 7/11, 2013 at 13:39 Comment(2)
Extremely good answer. Much more than you needed but info was very helpful. Thank you so muchUtricle
Thank you, solved a headache for me too. Great and correct answer.Budwig
S
19

@godel9 has a good suggested solution, although, in my own testing, the explanation given for the unexpected behaviour is not correct.

From the SKPhysicsBody Class Reference:

The force is applied for a single simulation step (one frame).

Referring back to the SKScene Class Reference's section on the -update method:

...it is called exactly once per frame, so long as the scene is presented in a view and is not paused.

So we can assume that calling -applyForce: in SKScene's -update method should not cause a problem. But as observed, the force does not exceed gravity, despite applying an upward force much greater than gravity (400 newtons vs 9.81).

I created a test project that would create two nodes, one that falls naturally, setting affectedByGravity to TRUE, and another that calls -applyForce with the same expected gravity vector (0 newtons in the x direction, and -9.81 in the y direction). I then calculated the difference in velocity of each node in one time step, and the length of time step. From this, I then logged the acceleration (change in velocity / change in time).

Here is a snippet from my SKScene subclass:

- (id)initWithSize:(CGSize)size

{

if (self = [super initWithSize:size])

{

    self.backgroundColor = [UIColor purpleColor];



    SKShapeNode *node = [[SKShapeNode alloc] init];

    node.path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 10, 10), nil);

    node.name = @"n";

    node.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:5];

    node.position = CGPointMake(0, 450);

    node.physicsBody.linearDamping = 0;

    node.physicsBody.affectedByGravity = NO;

    [self addChild:node];



    node = [[SKShapeNode alloc] init];

    node.path = CGPathCreateWithEllipseInRect(CGRectMake(0, 0, 10, 10), nil);

    node.name = @"n2";

    node.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:5];

    node.position = CGPointMake(20, 450);

    node.physicsBody.linearDamping = 0;

    [self addChild:node];


}

return self;

}



- (void)update:(NSTimeInterval)currentTime

{

SKNode *node = [self childNodeWithName:@"n"];

SKNode *node2 = [self childNodeWithName:@"n2"];

CGFloat acc1 = (node.physicsBody.velocity.dy - self.previousVelocity) / (currentTime - self.previousTime);

CGFloat acc2 = (node2.physicsBody.velocity.dy - self.previousVelocity2) / (currentTime - self.previousTime);

[node2.physicsBody applyForce:CGVectorMake(0, node.physicsBody.mass * -150 * self.physicsWorld.gravity.dy)];

NSLog(@"x:%f, y:%f, acc1:%f, acc2:%f", node.position.x, node.position.y, acc1, acc2);

self.previousVelocity = node.physicsBody.velocity.dy;

self.previousTime = currentTime;

self.previousVelocity2 = node2.physicsBody.velocity.dy;

}

The results are unusual. The node that is affected by gravity in the simulation has an acceleration that is consistently multiplied by a factor of 150 when compared to the node whose force was manually applied. I have attempted this with nodes of varying size and density, but the same scalar multiplier exists.

From this I must deduce that SpriteKit internally has a default 'pixel-to-meter' ratio. That is to say that each 'meter' is equal to exactly 150 pixels. This is sometimes useful, as otherwise the scene is often too large, meaning forces react slowly (think watching an airplane from the ground, it is travelling very fast but seemingly moving very slowly). Sprite Kit documentation frequently suggests that exact physics calculations are not recommended (seen specifically in the section 'Fudging the Numbers'), but this inconsistency took me a long time to pin down. Hope this helps!

Spode answered 17/11, 2013 at 21:52 Comment(4)
This is interesting and may be related to an issue I'm having with inconsistent animation when using applyFoce (https://mcmap.net/q/747250/-applyforce-is-not-smooth). How were you able to use this inconsistency to your benefit and/or fix any issues you were experiencing?Noah
Independent confirmation of the pixel-to-meter ratio of 150: https://mcmap.net/q/747251/-how-to-make-a-sprite-jump-to-a-specific-height-with-spritekitSeven
@Spode is it 150 pixels or points? i need a retina an non-retina constant, thanksDeterrent
Years later, I have also independently confirmed the 150 ratio. In addition to SKPhysicsWorld's gravity property, this also happens with radial gravity field nodes. I haven't tested beyond radial gravity field nodes, but I assume this happens for all field nodes. What I cannot figure out is why this ratio is used when calling applyForce and not used for field nodes and world gravity.Nertie

© 2022 - 2024 — McMap. All rights reserved.