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.

No comments: