Draw Rectangle/ Circle and Triangle in Spritekit with Two Colors. . . - core-graphics

I can draw Rectangle using simple SKSpriteNode. But i can not draw other types of drawings in it like Triangle, Circle etc with TWO SPLIT COLORS. Someone suggested to go with CGPath. But i am newbie and dont know to draw such type of complex things . Please can anyone illustrate simple way to go with these drawings with MULTICOLOR in SPRITEKIT. Mean their upper part is one color and lower part in 2nd color. More concise to say that Shape is divided into two colors whether that is star, rectangle, triangle or else.
Any Help will be greatly appreciated.
Thanks .

You can use SKShapeNode to draw shapes in sprite kit, but each SKShapeNode is limited to one line color (strokeColor) and one fill color.
However, you can create a custom SKNode subclass that contains two SKShapeNodes as children, each with different strokeColors/fillColors.
Something like this will work for a custom SKNode that draws a square with left and top red, right and bottom green:
- (id) init {
if (self = [super init]) {
SKShapeNode* topLeft = [SKShapeNode node];
UIBezierPath* topLeftBezierPath = [[UIBezierPath alloc] init];
[topLeftBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[topLeftBezierPath addLineToPoint:CGPointMake(0.0, 100.0)];
[topLeftBezierPath addLineToPoint:CGPointMake(100.0, 100.0)];
topLeft.path = topLeftBezierPath.CGPath;
topLeft.lineWidth = 10.0;
topLeft.strokeColor = [UIColor redColor];
topLeft.antialiased = NO;
[self addChild:topLeft];
SKShapeNode* bottomRight = [SKShapeNode node];
UIBezierPath* bottomRightBezierPath = [[UIBezierPath alloc] init];
[bottomRightBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[bottomRightBezierPath addLineToPoint:CGPointMake(100.0, 0.0)];
[bottomRightBezierPath addLineToPoint:CGPointMake(100.0, 100.0)];
bottomRight.path = bottomRightBezierPath.CGPath;
bottomRight.lineWidth = 10.0;
bottomRight.strokeColor = [UIColor greenColor];
bottomRight.antialiased = NO;
[self addChild:bottomRight];
}
return self;
}

Related

Child SkShapeNode positioning confusion

I've written this code for the game over scene I have for a game:
#import "GameOverScene.h"
#import "SharedInfo.h"
#implementation GameOverScene
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
[self setupView];
}
-(void)showGameEndingWithGameInformation:(NSDictionary *)gameEndingInformation{
}
-(void)setupView{
SKLabelNode *GOTitle = [SKLabelNode labelNodeWithFontNamed:#"Generica Bold"];
GOTitle.fontSize = 40.f;
NSString* text = [NSString stringWithFormat:#"GAME OVER"];
[GOTitle setText:text];
GOTitle.position = CGPointMake(CGRectGetMidX(self.frame), self.frame.size.height- GOTitle.frame.size.height*1.5);
[GOTitle setFontColor:[[SharedInfo sharedManager]colorFromHexString:#"#2EB187"]];
[self addChild: GOTitle];
SKLabelNode *replayButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
replayButton.fontSize = 25.f;
NSString* replayText = [NSString stringWithFormat:#"Play Again"];
[replayButton setText:replayText];
replayButton.name = kGOSceneReplayButton;
replayButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5);
[replayButton setFontColor:[SKColor whiteColor]];
SKShapeNode *bgNode = [SKShapeNode shapeNodeWithRectOfSize:replayButton.frame.size];
[bgNode setFillColor:[UIColor redColor]];
[replayButton addChild:bgNode];
[self addChild:replayButton];
NSLog(#"replay dimensions: %#",NSStringFromCGRect(replayButton.frame));
SKLabelNode *returnButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
returnButton.fontSize = 25.f;
NSString* returnText = [NSString stringWithFormat:#"Return To Main Menu"];
[returnButton setText:returnText];
returnButton.name = kGOSceneReturnToMainButton;
returnButton.position = CGPointMake(CGRectGetMidX(self.frame), replayButton.position.y -self.frame.size.height/7 );
[returnButton setFontColor:[SKColor whiteColor]];
[self addChild:returnButton];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *sprite = [self nodeAtPoint:location];
NSLog(#"sprite name: %#",sprite.name);
if ([sprite.name isEqualToString:kGOSceneReturnToMainButton]||[sprite.name isEqualToString:kGOSceneReturnToMainButton]) {
//return to main menu or retry
[self.gameEndingSceneDelegate goToScene:sprite.name withOptions:nil]; //Sort out the options later on.
}
}
#end
When I run it though, I get this:
There are two issues I'm really confused about. Firstly, why do I have 8 nodes in the scene, where I should really have 4? I think something is doubling the nodes, but that's just a guess.
The more confusing issue is the red SKShapeNode positioning. I've read that scaling the parent node can cause problems to the child SKShapeNode, but I'm not scaling anything. Also, why does it place my red rectangle at a random position (it's not the middle of the parent, or corresponding with the bottom).
Thanks a lot for all the help in advance.
UPDATE 1: So following the suggestion, I checked if my method is being called twice, and thus creating the duplicates. No luck there, as it is only called once. The mystery still going strong!
As for the positioning shenanigans, I changed the code slightly to set the position of the red rectangle to match its parent node:
SKLabelNode *replayButton = [SKLabelNode labelNodeWithFontNamed:#"Quicksand-Bold"];
replayButton.fontSize = 25.f;
NSString* replayText = [NSString stringWithFormat:#"Play Again"];
[replayButton setText:replayText];
replayButton.name = kGOSceneReplayButton;
replayButton.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5);
[replayButton setFontColor:[SKColor whiteColor]];
SKShapeNode *bgNode = [SKShapeNode shapeNodeWithRectOfSize:replayButton.frame.size];
[bgNode setFillColor:[UIColor redColor]];
[self addChild:replayButton];
bgNode.position = replayButton.position;
[replayButton addChild:bgNode];
But after updating, I got this:
In case it helps, this is what I do to present the scene:
SKView * skView = (SKView *)self.view;
skView.showsFPS = YES;
skView.showsNodeCount = YES;
/* Sprite Kit applies additional optimizations to improve rendering performance */
skView.ignoresSiblingOrder = YES;
scene = [GameOverScene sceneWithSize:self.view.frame.size];
[(GameOverScene*)scene setGameEndingSceneDelegate:self];
[(GameOverScene*)scene setScaleMode: SKSceneScaleModeAspectFill];
[(GameOverScene*)scene showGameEndingWithGameInformation:self.gameEndingInfo];
// Present the scene.
[skView presentScene:scene transition:sceneTransition];
Also, this is the output of my NSLog:
replay dimensions: {{221, 91}, {127, 25}
I've got a feeling that because I set my scene's setScaleMode, it gets strange, but nothing else is out of ordinary, so not sure what to do. I'm thinking maybe just create an image for my label and change the SKLabelNode to SKSpriteNode and set the image, so I skip adding the red rectangle as background for the label node. The reason I wanted to add the rectangle is actually to provide bigger hit target for when the 'Button' is tapped, so if anyone knows an easier, more straightforward way, I'd really appreciate it.
UPDATE 3:
I also tried setting the position of the rectangle to match that of parent label node:
bgNode.position = [replayButton convertPoint:CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame)- self.frame.size.height/5) fromNode:self];
the rectangle ends up at the same place as the last update (all the way to the right of the screen)
There are few issues with your code:
lineWidth property and it's default value of 1.0. It should be 0.0f
verticalAlignmentMode property and it's default baseline alignment. It should be SKLabelVerticalAlignmentModeCenter.
Wrong positioning of a shape node. It should be (0,0)
To fix it, change label's vertical alignment:
replayButton.verticalAlignmentMode = SKLabelVerticalAlignmentModeCenter;
set shapenode's lineWidth property to 0.0f:
bgNode.lineWidth = 0.0f;
and remove this line:
//bgNode.position should be CGPointZero which is (0,0)
bgNode.position = replayButton.position;
Still, I would stay away of this approach. SKShapeNode is not needed in this situation. You can do the same with SKSpriteNode. What is important is that both SKShapeNode and SKLabelNode can't be drawn in batches, which means, can't be drawn in a single draw pass when rendered like SKSpriteNode. Take a look at this. Your example is too simple to make performance issues, but in general you should keep all this in mind.
If your button's text never change during the game, you should consider using SKSpriteNode initialized with texture. If interested in a pre made buttons for SpriteKit, take a look at SKAButton.
Hope this helps!

NSView with masked CIFilter for OS X app

I am developing an app that contains lots of custom NSView objects being moved around. I have implemented a gaussian blur background filter for one of the custom NSView subclasses like so:
- (id)init {
self = [super init];
if (self) {
...
CIFilter *saturationFilter = [CIFilter filterWithName:#"CIColorControls"];
[saturationFilter setDefaults];
[saturationFilter setValue:#.5 forKey:#"inputSaturation"];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:#2.0 forKey:#"inputRadius"];
self.wantsLayer = YES;
self.layer.backgroundColor = [NSColor clearColor].CGColor;
self.layer.masksToBounds = YES;
self.layer.needsDisplayOnBoundsChange = YES;
self.layerUsesCoreImageFilters = YES;
[self updateFrame]; //this is where the frame size is set
self.layer.backgroundFilters = #[saturationFilter, blurFilter];
...
return self;
}
else return nil;
}
This works great and creates a gaussian blur effect within the entire contents of the view. The problem is that I do not want the gaussian blur to cover the entire view. There is about an (intentional) 12px padding between the actual size of the NSView and the drawing of its content box:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
NSColor* strokeColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:1];
NSColor* fillColor = [NSColor colorWithRed:.5 green:.8 blue:1 alpha:.2];
...
[strokeColor setStroke];
[fillColor setFill];
NSBezierPath *box = [NSBezierPath bezierPathWithRoundedRect:NSMakeRect(self.bounds.origin.x + 12, self.bounds.origin.y + 12, self.bounds.size.width - 24, self.bounds.size.height - 24) xRadius:6 yRadius:6];
box.lineWidth = 6;
[box stroke];
[box fill];
...
}
The reason for this padding is that there are some pieces of the GUI that inhabit this region and are drawn seamlessly into the containing box. I would like to mask the Blur effect to only have effect on the interior of the drawn box rather than the entire view. Here is what I have tried.
ATTEMPT 1: Create a sublayer
I created a sublayer in the NSView with the appropriately sized frame, and added the blur effect to this sublayer. PROBLEM: The blur effect seems to only apply to the immediate parent layer, so rather than blur the contents behind the NSView, it blurs the contents of the NSView's self.layer (which is basically empty).
ATTEMPT 2: Create a masking layer
I tried to create a masking layer and set it to self.layer.mask. However, since the positions of the GUI content do change (via the DrawRect function), I would need to get a copy of the current layer to use as the masking layer. I tried the following code, but it had no effect.
self.layer.mask = nil;
NSArray *bgFilters = self.layer.backgroundFilters;
self.layer.backgroundFilters = nil;
CALayer *maskingLayer = self.layer.presentationLayer;
self.layer.mask = maskingLayer;
self.layer.backgroundFilters = bgFilters;
ATTEMPT 3: Draw a masking layer directly
I could not find any examples of how to draw directly on a layer. I can not use a static UIImage to mast with, because, as I said above, the mask has to change with user interaction. I was looking for something equivalent to the DrawRect function. Any help would be appreciated.
SO...
It seems to me that the sublayer way would be the best and simplest way to go, if I could just figure out how to change the priority of the blur effect to be the background behind the NSView not the NSView's background layer behind the sublayer.
Well, I would still like to know if there is a more elegant way, but I have found a solution that works. Basically, I have created a masking layer from an NSImage drawn from a modified version of the drawRect function:
- (id)init {
self = [super init];
if (self) {
// SETUP VIEW SAME AS ABOVE
CALayer *maskLayer = [CALayer layer];
maskLayer.contents = [NSImage imageWithSize:self.frame.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
[self drawMask:self.bounds];
return YES;
}];
maskLayer.frame = self.bounds;
self.layer.mask = maskLayer;
return self;
}
else return nil;
}
- (void)drawMask:(NSRect)dirtyRect {
[[NSColor clearColor] set];
NSRectFill(self.bounds);
[[NSColor blackColor] set];
// SAME DRAWING CODE AS drawRect
// EXCEPT EVERYTHING IS SOLID BLACK (NO ALPHA TRANSPARENCY)
// AND ONLY NEED TO DRAW PARTS THAT EFFECT THE EXTERNAL BOUNDARIES
}

UIButton displaying a triangle

I have a UIButton and i want it to display a triangle. Is there a function to make it a triangle? Since im not using a UIView class im not sure how to make my frame a triangle.
ViewController(m):
- (IBAction)makeTriangle:(id)sender {
UIView *triangle=[[UIView alloc] init];
triangle.frame= CGRectMake(100, 100, 100, 100);
triangle.backgroundColor = [UIColor yellowColor];
[self.view addSubview: triangle];
Do i have to change my layer or add points and connect them to make a triangle with CGRect?
If im being unclear or not specific add a comment. Thank you!
A button is a subclass of UIView, so you can make it any shape you want using a CAShape layer. For the code below, I added a 100 x 100 point button in the storyboard, and changed its class to RDButton.
#interface RDButton ()
#property (strong,nonatomic) UIBezierPath *shape;
#end
#implementation RDButton
- (id)initWithCoder:(NSCoder *)aDecoder
{
self = [super initWithCoder:aDecoder];
if (self) {
self.titleEdgeInsets = UIEdgeInsetsMake(30, 0, 0, 0); // move the title down to make it look more centered
self.shape = [UIBezierPath new];
[self.shape moveToPoint:CGPointMake(0,100)];
[self.shape addLineToPoint:CGPointMake(100,100)];
[self.shape addLineToPoint:CGPointMake(50,0)];
[self.shape closePath];
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = self.shape.CGPath;
shapeLayer.fillColor = [UIColor yellowColor].CGColor;
shapeLayer.strokeColor = [UIColor blueColor].CGColor;
shapeLayer.lineWidth = 2;
[self.layer addSublayer:shapeLayer];
}
return self;
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self.shape containsPoint:[touches.anyObject locationInView:self]])
[super touchesBegan:touches withEvent:event];
}
The touchesBegan:withEvent: override restricts the action of the button to touches within the triangle.
A view's frame is always a rect, which is a rectangle. Even if you apply a transform to it so it no longer looks like a rectangle, the view.frame property will still be a rectangle -- just the smallest possible rectangle that contains the new shape you have produced.
So if you want your UIButton to look like a triangle, the simplest solution is probably to set its type to UIButtonTypeCustom and then to set its image to be a png which shows a triangle and is transparent outside of the triangle.
Then the UIButton itself will actually be rectangle, but will look like a triangle.
If you want to get fancy, you can also customize touch delivery so that touches on the transparent part of the PNG are not recognized (as I believe they would be by default), but that might be a bit trickier.

In SpriteKit, SKCropNode has no effect over a SKShapeNode

I've been trying to apply a mask to a SKShapeNode using SKCropNode, and so far without success. Thinking that it's a SpriteKit bug - Here is the code snippet:
SKNode* contentNode = [SKNode node];
// picture - use an image bigger than 50x50
SKSpriteNode *pictureNode = [SKSpriteNode spriteNodeWithImageNamed:#"tree"];
// triangle
SKShapeNode* triangleNode = [SKShapeNode node];
UIBezierPath* triangleNodeBezierPath = [[UIBezierPath alloc] init];
[triangleNodeBezierPath moveToPoint:CGPointMake(0.0, 0.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(0.0, 100.0)];
[triangleNodeBezierPath addLineToPoint:CGPointMake(50.0, 100.0)];
[triangleNodeBezierPath closePath];
triangleNode.path = triangleNodeBezierPath.CGPath;
triangleNode.fillColor = [UIColor colorWithRed:1.0 green:1.0 blue:1.0 alpha:1];
// create a mask
SKSpriteNode *mask = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size: CGSizeMake(50, 50)]; //50 by 50 is the size of the mask
// create a SKCropNode
SKCropNode *cropNode = [SKCropNode node];
[cropNode addChild: contentNode];
[cropNode setMaskNode: mask];
[self addChild: cropNode];
[contentNode addChild:pictureNode]; // pictureNode is being cropped
[contentNode addChild:triangleNode]; // triangleNode is not
cropNode.position = CGPointMake( CGRectGetMidX (self.frame), CGRectGetMidY (self.frame));
Does anyone have a workaround about this issue? Thanks a lot!
This had been bugging me for most of the day. I had planned to create a timer similar to the excellent TCProgressTimer by Tony Chamblee. However, as my application uses multiple progress timers I didn't want to have to design dozens of different sized sprites for use at different resolutions.
My solution was to convert SKShapeNode objects to SKSpriteNode objects. I ended up having to go back to basics and use Core Graphics to do the heavy lifting. This is a rather messy way of doing things, I'm sure, but I wanted quick results to dynamically create objects that would resemble the results obtained when using SKShapeNode.
I am only interested in making circle objects at present, so I did it like this:
-(SKSpriteNode *)createSpriteMatchingSKShapeNodeWithRadius:(float)radius color:(SKColor *)color {
CALayer *drawingLayer = [CALayer layer];
CALayer *circleLayer = [CALayer layer];
circleLayer.frame = CGRectMake(0,0,radius*2.0f,radius*2.0f);
circleLayer.backgroundColor = color.CGColor;
circleLayer.cornerRadius = circleLayer.frame.size.width/2.0;
circleLayer.masksToBounds = YES;
[drawingLayer addSublayer:circleLayer];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(circleLayer.frame.size.width, circleLayer.frame.size.height), NO, [UIScreen mainScreen].scale);
CGContextSetAllowsAntialiasing(UIGraphicsGetCurrentContext(), TRUE);
CGContextSetFillColorWithColor(UIGraphicsGetCurrentContext(), [UIColor clearColor].CGColor);
CGContextFillRect(UIGraphicsGetCurrentContext(), CGRectMake(0,0,circleLayer.frame.size.width,circleLayer.frame.size.height));
[drawingLayer renderInContext: UIGraphicsGetCurrentContext()];
UIImage *layerImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithTexture:[SKTexture textureWithImage:layerImage]];
return sprite;
}
The resulting sprite can now be masked by an SKCropNode as expected. As these sprites are all generated before the scene begins, I do not notice a performance hit. However, I would imagine this method is highly inefficient if you are generating multiple nodes on the fly.
I would be eager to hear solutions from other users. Hope that helps.
-DC
I have the similar task in my app. I need to draw multiple irregular shapes based on user input and then use them as crop node's masks.
The solution that works for me is to:
create a SKShapeNode with the required path
retrieve a SKTexture from it using SKView method textureFromNode:crop:
create a SKSpriteNode from that texture.
use SKSprite node as a mask.
Your title is correct. I've also discovered having a ShapeNode in the CropNode's hierarchy affects the children above it as well. I created a quick experiment. You can create a new game project and try it yourself. By commenting out one of the addChild:shapeContent lines you can see how it affects the cropping of the spaceship image.
As DoctorClod points out the current solution seems to be making sure all children of the cropNode are SpriteNodes.
-(void)didMoveToView:(SKView *)view {
SKSpriteNode* colorBackground = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(800, 600)];
SKSpriteNode *spaceshipImage = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
SKShapeNode* shapeContent = [SKShapeNode shapeNodeWithRect:CGRectMake(0, 0, 200, 100)];
shapeContent.fillColor = [SKColor greenColor];
SKSpriteNode *maskShape = [SKSpriteNode spriteNodeWithColor:[SKColor blackColor] size:CGSizeMake(500, 100)];
SKCropNode *cropNode = [SKCropNode new];
[cropNode addChild:colorBackground];
[cropNode addChild:shapeContent]; // comment this one out
[cropNode addChild:spaceshipImage];
[cropNode addChild:shapeContent]; // or comment this one out
[cropNode setMaskNode:maskShape];
cropNode.position = CGPointMake(CGRectGetMidX (self.frame), CGRectGetMidY(self.frame));
[self addChild:cropNode];
}

Can't vertically orient a SKLabelNode with anchorPoint

I want to make a text-based game using Sprite Kit (a la those Learn to Type games).
I thought I'd use SKLabelNode for strings, but when I try to set the anchorPoint in order to rotate it, I get an error that SKLabelNode doesn't have the anchorPoint property:
SKLabelNode *hello = [SKLabelNode labelNodeWithFontNamed:#"Courier-Bold"];
hello.text = #"Hello,";
//this throws an error:
hello.anchorPoint = CGPointMake(0.5,1.0);
What's a good workaround? How can I vertically orient my text strings, while treating them like physical objects using physicsBody?
SKLabelNode doesn't have anchor point.
Use verticalAlignmentMode property to align SKLabelNode vertically.
SKLabelVerticalAlignmentModeBaseline
Positions the text so that the font’s baseline lies on the node’s origin.
SKLabelVerticalAlignmentModeCenter
Centers the text vertically on the node’s origin.
SKLabelVerticalAlignmentModeTop
Positions the text so that the top of the text is on the node’s origin.
SKLabelVerticalAlignmentModeBottom
Positions the text so that the bottom of the text is on the node’s origin.
https://developer.apple.com/library/ios/documentation/SpriteKit/Reference/SKLabelNode_Ref/Reference/Reference.html#//apple_ref/doc/uid/TP40013022-CH1-SW15
You can add the SKLabelNode as a child of a SKSpriteNode. Then apply the anchorPoint
(and rotation etc) to the parent node:
- (SKSpriteNode *)testNode {
SKSpriteNode *testNode = [[SKSpriteNode alloc] init];//parent
SKLabelNode *hello = [SKLabelNode labelNodeWithFontNamed:#"Courier-Bold"];//child
hello.text = #"Hello,";
[testNode addChild:hello];
testNode.anchorPoint = CGPointMake(0.5,1.0);
testNode.position=CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
return testNode;
}
This is how I would tackle your problem
SKLabelNode *labNode = [SKLabelNode labelNodeWithFontNamed:#"Arial"];
labNode.fontSize = 30.0f;
labNode.fontColor = [SKColor yellowColor];
labNode.text = #"TEST";
SKTexture *texture;
SKView *textureView = [SKView new];
texture = [textureView textureFromNode:labNode];
texture.filteringMode = SKTextureFilteringNearest;
SKSpriteNode *spriteText = [SKSpriteNode spriteNodeWithTexture:texture];
//spriteText.position = put me someplace good;
[self addChild:spriteText];
In the end, I realized I didn't need anchorPoint at all for what I was trying to achieve — instead, I used
hello.zRotation = M_PI/2;
This works for SKLabelNode.