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.

No comments: