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.


Sunday, November 8, 2015

My Experiments with SpriteKit: Part Four - Allowing the User to Rotate The Sprite!

If you've been following along with my SpriteKit experiments you should have a cool retro spaceship in a bordered GameScene, just like this:
My implementation of GameScene looks like this:
#import "GameScene.h"

@interface GameScene ()
@property SKSpriteNode* spaceship;
@property CGPoint center;
@property CGFloat height;
@end

@implementation GameScene

-(void)addBorderOfThickness:(CGFloat)t andCornerRadius:(CGFloat)cr {
    SKSpriteNode* border;
    NSImage* image;
    image = [[NSImage alloc] initWithSize:self.frame.size];
    [image lockFocus];
    NSBezierPath* path = [NSBezierPath bezierPathWithRect:self.frame];
    [path appendBezierPathWithRoundedRect:CGRectInset(self.frame, t, t)
                                  xRadius:cr
                                  yRadius:cr];
    [[NSColor colorWithRed:(255.0/255.0)
                     green:(215.0/255.0)
                      blue:(0.0/255.0)
                     alpha:1.0] set];
    path.windingRule = NSEvenOddWindingRule;
    [path fill];
    [image unlockFocus];
    
    SKTexture* texture = [SKTexture textureWithImage:image];
    border = [SKSpriteNode spriteNodeWithTexture:texture];
    border.position = self.center;
    [self addChild:border];
}

-(void)addSpaceship {
    NSImage* image;
    image = [[NSImage alloc] initWithSize:CGSizeMake(30, 30)];
    [image lockFocus];
    NSBezierPath* path = [NSBezierPath bezierPath];
    path.lineWidth = 2.5;
    [path moveToPoint:CGPointMake(0, 0)];
    [path lineToPoint:CGPointMake(15, 30)];
    [path lineToPoint:CGPointMake(30, 0)];
    [path lineToPoint:CGPointMake(15, 10)];
    [path closePath];
    [[NSColor colorWithRed:(255.0/255.0)
                     green:(215.0/255.0)
                      blue:(0.0/255.0)
                     alpha:1.0] set];
    [path stroke];
    [image unlockFocus];
    
    SKTexture* texture = [SKTexture textureWithImage:image];
    _spaceship = [SKSpriteNode spriteNodeWithTexture:texture];
    _spaceship.position = self.center;
    [self addChild:_spaceship];
    
}

-(void)didMoveToView:(SKView *)view {
    self.backgroundColor = [NSColor blackColor];
    _height = CGRectGetHeight(self.frame);
    _center = CGPointMake(CGRectGetMidX(self.frame),
                          CGRectGetMidY(self.frame));
    [self addBorderOfThickness:15
               andCornerRadius:25];
    [self addSpaceship];
  
}

@end

Note that I've add a property to hold the spaceship sprite, now we can move it around in the update: method. Let's just rotate the spaceship one degree each update, for now. SKSpriteNode has a handy property called zRotation of type CGFloat.
-(void) update:(NSTimeInterval)currentTime {
    const CGFloat oneDegree = 2.0 * M_PI / 360.0;
    self.spaceship.zRotation += oneDegree;
}
This rotates our spaceship counter-clockwise around its' centre (the anchor point) at a rate of one degree per update. How would we rotate it clockwise? That's pretty easy, try it out.

Let's take a small detour to talk about the anchor point of our spaceship. In the debugger put a break point on a line inside of the update: method. In the (lldb) window of the debug area have a look at the anchorPoint property of our spaceship sprite:
(lldb) po self.spaceship.anchorPoint
(x = 0.5, y = 0.5)
 (x = 0.5, y = 0.5)
(lldb) 
anchorPoint is a property of type CGPoint. The ranges of the x and y values are 0.0 to 1.0. The default position of the anchorPoint for the sprite is x=0.5 and y=0.5, also known as the centre of the sprite. In the didMoveToView: method, after the call to the addSpaceship method change the anchorPoint property to (0.0, 0.0), like this:
self.spaceship.anchorPoint = CGPointMake(0.0, 0.0);
and re run the application, you will now see the spaceship rotate around the lower left part of the spaceship, or the (0.0, 0.0) point of the sprite. If you set the anchor to ( 1.0, 1.0), the sprite will rotate around the upper right of the spaceship sprite. At this point I think we want to rotate the sprite around its' centre, so remove the line that sets the anchorPoint.

OK, we have a spinning spaceship, but that's not the goal. I would like to let our user use the left arrow key(ASCII Code: 123) to rotate the sprite count-clockwise and the right arrow key(ASCII Code: 124) to rotate the sprite clockwise. NSResponder, gives our SKSpriteNode a couple of useful methods that we can override: keyDown: and keyUp:. Here's the initial simplistic implementation of keyDown:
-(void) update:(NSTimeInterval)currentTime {
}

-(void)keyDown:(NSEvent *)theEvent {
    const CGFloat oneDegree = 2.0 * M_PI / 360.0;
    switch (theEvent.keyCode) {
        case 123:
            self.spaceship.zRotation += oneDegree;
            break;
        case 124:
            self.spaceship.zRotation -= oneDegree;
            break;
        default:
            break;
    }
}
I removed the rotation code from the update: method and moved it to the keyDown: method... Now we have a spaceship that rotates.. Very slowly, because keyDown: is only called one time for about 5 updates:.  We need to do something better. Somehow we need to move the rotation into the update: method.

Let's make use of keyUp: method and a rotation property that we will add to our GameScene.

-(void) update:(NSTimeInterval)currentTime {
    const CGFloat oneDegree = 2.0 * M_PI / 360.0;
    switch (self.keyPressed) {
        case 123:
            self.spaceship.zRotation += oneDegree;
            break;
        case 124:
            self.spaceship.zRotation -= oneDegree;
            break;
        default:
            break;
    }
}

-(void)keyDown:(NSEvent *)theEvent {
    self.keyPressed = theEvent.keyCode;
}

-(void)keyUp:(NSEvent *)theEvent {
    self.keyPressed = 0;
}
remember to add the keyPressed NSUInteger property int he GameScene interface block.

It's better, the spaceship rotates a bit quicker, but pressing two keys at once will give us problems. For example, press and hold the left arrow key, our spaceship rotates counter-clockwise. Now, also press the right arrow key, our spaceship will rotate clockwise. Now release the right key, and the spaceship stops, even though the left key is still pressed...

What should happen? Let's write a specification:

  1. tapping a left, or right, arrow key must cause the spaceship to rotate and stop.
  2. tapping an holding the left arrow key must cause the spaceship to rotate counter-clockwise until the the left arrow key is released, at which point the rotation will stop.
  3. tapping an holding the right arrow key must cause the spaceship to rotate clockwise until the the left arrow key is released, at which point the rotation will stop.
  4. tapping and holding both arrow keys should not cause the spaceship to rotate as each rotation will cancel the other out.
  5. if the spaceship is being rotated by tapping and holding a left or right arrow key,  tapping and holding the other key should cause the rotation of the spaceship to stop until one of the arrow keys is released.
We've got points 1, 2 and 3. Let's work on point 4.

Currently we keep track of only one key, let's fix that. Objective-C gives us many containers to choose from. We don't need to store multiple copies of our keypress, and we don't need them ordered. So NSSet seems to be the container to use, actually, since we want to add and remove keystrokes, NSMutableSet. Replace
@property NSUInteger keyPressed;
with
@property NSMutableSet* keysPressed;
and the keyDown: method becomes easy
-(void)keyDown:(NSEvent *)theEvent {
    [self.keysPressed addObject:[NSNumber numberWithUnsignedShort:theEvent.keyCode]];
}
the only wrinkle is that we need to remember that theEvent.keyCode  is not an NSObject, but a POD unsigned short, so we need to wrap it in an NSNumber. I don't think I'd do this in production code. Using an NS containers may slow us down a bit, but for what we are doing here, this should be fine.

Let's look at the keyUp: method, it's a bit fancier..
-(void)keyUp:(NSEvent *)theEvent {
    NSNumber* toRemove = [NSNumber numberWithUnsignedShort:theEvent.keyCode];
    for (NSNumber* n  in self.keysPressed) {
        if ([toRemove isEqualToNumber:n]) {
            toRemove = n;
            break;
        }
    }
    [self.keysPressed removeObject:toRemove];
}
aaaand there's one small thing I forgot.

Did you figure out what I missed? Yup, we need to instantiate _keysPressed. Add the line
_keysPressed = [NSMutableSet new];
in the didMoveToView: method, so that we have an object to add our keystrokes to. 

Now specifications 4 and 5 are complete, without invalidating specifications 1, 2 and 3.

Our spaceship rotates.

This was a really long post, lets move the spaceship in the next post. But first, try doing it yourself. Here's what you need to know: the key code for the up arrow is  126. And to move the spaceship in the direction that the spaceship is point you'll need something like
CGPoint pt = _spaceship.position;
pt.x += speed * cos(_spaceship.zRotation + c);
pt.y += speed * sin(_spaceship.zRotation + c);
_spaceship.position = pt;
we'll set that up next time.  We'll also talk about making the motion a little bit more... I wanted to say realistic... but what  I really mean is more like the game we're trying to recreate.

Friday, November 6, 2015

My Experiments with SpriteKit: Part Three - Textures!

Let's create an interesting sprite from the ground up. We'll do everything!

Start with the code we've written already. Remove the sprite code that we've written, even remove the image from the Assets.xcassets folder. Your GameScene should look like this:
#import "GameScene.h"

@interface GameScene ()
@property CGPoint center;
@property CGFloat height;
@end

@implementation GameScene

-(void)didMoveToView:(SKView *)view {
    self.backgroundColor = [NSColor blackColor];
    _height = CGRectGetHeight(self.frame);
    _center = CGPointMake(CGRectGetMidX(self.frame),
                          CGRectGetMidY(self.frame));
}

-(void)update:(CFTimeInterval)currentTime {
    
}

@end
I've also set the background colour of the scene to black. Build and run this and you'll get an application with a black window.

What does our scene need? It needs a border...

Let's make a nice gold (RED=255, GREEN=215, BLUE=0) border. It should hug the scene frame, have rounded inside corners and be 15 points thick.

SKSpriteKit has a couple of methods we can use to create our sprite: spriteNodeWithImageNamed: and spriteNodeWithTexture:. The first one is great if we already have a named image, for example we may already have an image that we have placed in our Assets.xcassets folder. And the second one is great if we have a Texture of type SKTexture.

Texture objects manage graphics resources that can be applied to our sprites when they are being rendered. Let's use spriteNodeWithTexture: method to create a sprite that we can use as a border to our scene. Sometimes it's necessary to work backwards towards our goal, let's try that.

-(void)didMoveToView:(SKView *)view {
    self.backgroundColor = [NSColor blackColor];
    _height = CGRectGetHeight(self.frame);
    _center = CGPointMake(CGRectGetMidX(self.frame),
                          CGRectGetMidY(self.frame));
    
    SKSpriteNode* border;
    border = [SKSpriteNode spriteNodeWithTexture:<#(nullable SKTexture *)#>];
    border.position = self.center;
    [self addChild:border];
}
So we need a SKTexture. Let's add one of those:
SKSpriteNode* border;
SKTexture* texture = [SKTexture textureWithImage:<#(nonnull NSImage *)#>];
border = [SKSpriteNode spriteNodeWithTexture:texture];
border.position = self.center;
[self addChild:border];
Right, now we need an NSImage:
SKSpriteNode* border;
NSImage* image;
image = [[NSImage alloc] initWithSize:self.frame.size];
// ???
SKTexture* texture = [SKTexture textureWithImage:image];
border = [SKSpriteNode spriteNodeWithTexture:texture];
border.position = self.center;
[self addChild:border];
OK, we're back to a build-able and run-able project. But it's still a black window. We have a sprite, having a texture that contains a image that has the same size as the window. Maybe... Let's find out by drawing the border.

In order to draw in the image we need to obtain a graphics context for the image, this turns out to be pretty easy with the NSImage method lockFocus.  When we're done drawing our image, we'll call unlockFocus.
NSImage* image;
image = [[NSImage alloc] initWithSize:self.frame.size];
[image lockFocus];
// ???
[image unlockFocus];
We just need to figure out how to draw the border. It's easy: use NSBezierPath to describe the outline of our border and then fill it in.
[image lockFocus];
NSBezierPath* path = [NSBezierPath bezierPathWithRect:self.frame];
[path appendBezierPathWithRoundedRect:CGRectInset(self.frame, 15, 15)
                              xRadius:25
                              yRadius:25];
// ???
[image unlockFocus];
That defines the path we want to fill: a large rect that is the size of view, and a smaller rounded rect with the same centre 15 points inside of the large rect. Now, we need to apply the path, with the gold colour to the image.
[image lockFocus];
NSBezierPath* path = [NSBezierPath bezierPathWithRect:self.frame];
[path appendBezierPathWithRoundedRect:CGRectInset(self.frame, 15, 15)
                              xRadius:25
                              yRadius:25];
[[NSColor colorWithRed:(255.0/255.0)
                 green:(215.0/255.0)
                  blue:(0.0/255.0)
                 alpha:1.0] set];
path.windingRule = NSEvenOddWindingRule;
[path fill];
[image unlockFocus];
the set method, called on an NSColor, sets the drawing fill and stroke colour to that of the receiver. The windingRule applies to the fill method that we use to actually draw the path and fill it with the gold colour. I chose the NSEvenOddWindingRule simply because it caused the fill to work correctly, inside the two rectangles as opposed to filling both rectangles (actually, there's a good reason to use NSEvenOddWindingRule, I explain why in another post).

Finally, call the fill method on our path. Here's the application:


A very nice border. Let's add one more sprite to set us up for the next blog post in this series. But first, a bit of refactoring. The didMoveToView: method is getting a bit ungainly:
-(void)didMoveToView:(SKView *)view {
    self.backgroundColor = [NSColor blackColor];
    _height = CGRectGetHeight(self.frame);
    _center = CGPointMake(CGRectGetMidX(self.frame),
                          CGRectGetMidY(self.frame));
    
    SKSpriteNode* border;
    NSImage* image;
    image = [[NSImage alloc] initWithSize:self.frame.size];
    [image lockFocus];
    NSBezierPath* path = [NSBezierPath bezierPathWithRect:self.frame];
    [path appendBezierPathWithRoundedRect:CGRectInset(self.frame, 15, 15)
                                  xRadius:25
                                  yRadius:25];
    [[NSColor colorWithRed:(255.0/255.0)
                     green:(215.0/255.0)
                      blue:(0.0/255.0)
                     alpha:1.0] set];
    path.windingRule = NSEvenOddWindingRule;
    [path fill];
    [image unlockFocus];
    
    SKTexture* texture = [SKTexture textureWithImage:image];
    border = [SKSpriteNode spriteNodeWithTexture:texture];
    border.position = self.center;
    [self addChild:border];
}
So we should pull the border creation code out into it's own method.
-(void)addBorderofThickness:(CGFloat)t andCornerRadius:(CGFloat)cr {
    SKSpriteNode* border;
    NSImage* image;
    image = [[NSImage alloc] initWithSize:self.frame.size];
    [image lockFocus];
    NSBezierPath* path = [NSBezierPath bezierPathWithRect:self.frame];
    [path appendBezierPathWithRoundedRect:CGRectInset(self.frame, t, t)
                                  xRadius:cr
                                  yRadius:cr];
    [[NSColor colorWithRed:(255.0/255.0)
                     green:(215.0/255.0)
                      blue:(0.0/255.0)
                     alpha:1.0] set];
    path.windingRule = NSEvenOddWindingRule;
    [path fill];
    [image unlockFocus];
    
    SKTexture* texture = [SKTexture textureWithImage:image];
    border = [SKSpriteNode spriteNodeWithTexture:texture];
    border.position = self.center;
    [self addChild:border];
}
Now we can create the border in didMoveToView with just one line.

On to the new sprite! I want a retro game style spaceship... Here's the drawing code:
NSImage* image;
image = [[NSImage alloc] initWithSize:CGSizeMake(30, 30)];
[image lockFocus];
NSBezierPath* path = [NSBezierPath bezierPath];
path.lineWidth = 2.5;
[path moveToPoint:CGPointMake(0, 0)];
[path lineToPoint:CGPointMake(15, 30)];
[path lineToPoint:CGPointMake(30, 0)];
[path lineToPoint:CGPointMake(15, 10)];
[path closePath];
[[NSColor colorWithRed:(255.0/255.0)
                 green:(215.0/255.0)
                  blue:(0.0/255.0)
                 alpha:1.0] set];
[path stroke];
[image unlockFocus];
 Go ahead and make the sprite, you'll get something like:



In part four we'll let the user rotate the space ship and move it around.