Wednesday, November 11, 2015

My Experiments with SpriteKit: Part Five - Faking Inertia!

Hopefully, you had accepted my challenge to attempt to move the spaceship by adding the code hints that I gave you in Part Four. If so, you may have added something like this to the switch statement in the update: method
case 126:{
    const CGFloat speed = 3.0;
    const CGFloat c = M_PI/2.0;
    CGPoint pt = _spaceship.position;
    pt.x += speed * cos(_spaceship.zRotation + c);
    pt.y += speed * sin(_spaceship.zRotation + c);
    _spaceship.position = pt;
}
break;
where c is a CGFloat, possibly a const, with a value of M_PI/2.0, that rotates the direction of motion to be inline with the way our drawing of the spaceship is pointing (we should probably fix the drawing code so that we can remove that hack). And that was it!

Unfortunately, we can now "fly" our spaceship off the GameScene, and it's very hard to get back.. Let's make our GameScene a "closed universe", that is, if the spaceship flies off the scene, it will reappear at the opposite wall. This can be achieved with a few if statements in the update: method.

First, since our SKSpriteNode does not allow modifying its' position in place, we need to get a copy of it, then we compare the position of the spaceship to the bounds of the game scene:
CGPoint pt = self.spaceship.position;
if (pt.x>self.frame.size.width ) {
    pt.x = 0;
} else if(pt.x < 0) {
    pt.x = self.frame.size.width;
}

if (pt.y>self.frame.size.height) {
    pt.y = 0.0;
} else if(pt.y < 0) {
    pt.y = self.frame.size.height;
}
self.spaceship.position = pt;
finally, we set the new value of the spaceship. Make sure to do this after the switch statement in the update: method.

There! Now we won't lose our spaceship.

Uh-oh! You may have noticed that since we have added a third keypress, the up key, we seem to have broken our left and right rotation functionality. But not too badly. Because of a keyboard problem called ghosting we do not have the ability to press the up, left and right buttons at the same time and get all of the keyDown events... Oh, well. Our users will adjust.

Now, on to the motion. Our spaceship doesn't move like a spaceship, it's more like a car. Let's give it a bit of "momentum". (We're not doing sprite physics yet, I promise I'll do that later, we're going to fake it now.)

In space, no one can hear you... ah.. slow down... Let's change the forward motion. Instead of using the up arrow key to directly move the spaceship, we'll use it to change the speed of the sprite. This looks like a job for a property! Since we're playing in a 2 dimensional space we should use a CGVector to represent the velocity of our spaceship. We can add that to the GameScene interface block
@interface GameScene ()
@property SKSpriteNode* spaceship;
@property CGVector spaceshipVelocity;
@property CGPoint center;
@property CGFloat height;
@property NSMutableSet* keysPressed;
@end
as a property called spaceshipVelocity.  Now we need to set our velocity, in the switch statement we used velocity to set the position directly. But now we need to apply an acceleration to the velocity. So let's rewrite the up key handling code in the update: method
case 126:{
    const CGFloat accel = 0.1;
    CGVector v = self.spaceshipVelocity;
    v.dx += accel * cos(self.spaceship.zRotation + c);
    v.dy += accel * sin(self.spaceship.zRotation + c);
    self.spaceshipVelocity = v;
}
break;
 Now that we have set the spaceship velocity we need to apply the velocity to the spaceship position. We currently grab the position of the spaceship just before we make sure that the spaceship is trapped in our view. Insert the position update code just after that point
CGPoint pt = self.spaceship.position;

pt.x += self.spaceshipVelocity.dx;
pt.y += self.spaceshipVelocity.dy;
_spaceship.position = pt;

if (pt.x>self.frame.size.width ) {
    pt.x = 0;
} else if(pt.x < 0) {
    pt.x = self.frame.size.width;
}

if (pt.y>self.frame.size.height) {
    pt.y = 0.0;
} else if(pt.y < 0) {
    pt.y = self.frame.size.height;
}
self.spaceship.position = pt;
we still have a small problem. Go ahead and run the application, you'll find that our spaceship can accelerate to a stupid large velocity. Let's limit the velocity before we apply it to the position, and only when we change the velocity, in the up arrow handler
case 126:{
    const CGFloat accel = 0.1;
    const CGFloat maxSpeed = 5.0;
    CGVector v = self.spaceshipVelocity;
    v.dx += accel * cos(self.spaceship.zRotation + c);
    v.dy += accel * sin(self.spaceship.zRotation + c);
    CGFloat speed = hypot(v.dx, v.dy);
    if (speed>maxSpeed) {
        v.dx = (v.dx/speed) * maxSpeed;
        v.dy = (v.dy/speed) * maxSpeed;
    }
    self.spaceshipVelocity = v;
}
break;
here we limit the speed of our spaceship to 5 points per frame update. That seems to make a reasonable spaceship speed.  We use the C function hypot function to calculate the speed from the velocity. Note that when we see that the speed would be too high, we renormalize the velocity back to the maximum speed before applying it to the spaceship.

So now we have spaceship motion that simulates momentum, at least in forward motion, we could make the rotation behave the same way as well, that is, if the spaceship rotation is started it keeps rotating until a counter rotation is started. But before we make any more modifications to the spaceship we should do some refactoring first.

GameScene is getting a bit large, lets refactor it by extracting the spaceship into it's own class based SKSpriteNode... In the next blog post.


No comments: