Child SkShapeNode positioning confusion - objective-c

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!

Related

SKSpriteNode not showing up when added

I was using Objective-C and SpriteKit. I made a SKSpriteNode of a simple ball, then added it. Unfortunately, it's not showing up.
-(void)didMoveToView:(SKView *)view {
/* Setup your scene here */
// Set background color
self.backgroundColor = [SKColor whiteColor];
// Creating a new spritenode from an image
SKSpriteNode *ball = [SKSpriteNode spriteNodeWithImageNamed: #"ball"];
// Add the sprite node to the scene
[self addChild: ball];
Did you forget to set the sprite's position?
ball.position = CGPointMake(CGRectGetMidX(self.scene.frame), CGRectGetMidY(self.scene.frame));

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.

Is it possible to use a circle (SKShapeNode) as a mask in Sprite Kit?

I'm trying to create a circular mask in a Sprite Kit project. I create the circle like this (positioning it at the centre of the screen):
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES);
circleMask.path = circle;
circleMask.lineWidth = 0;
circleMask.fillColor = [SKColor blueColor];
circleMask.name=#"circleMask";
and further down the code, I set it as the mask for the cropNode:
[cropNode setMaskNode:circleMask];
... but instead of the content showing inside a circle, the mask appears as a square.
Is it possible to use a SKShapeNode as a mask, or do I need to use an image?
After much swearing, scouring the web, and experimentation in Xcode, I have a really hacky fix.
Keep in mind that this is a really nasty hack - but you can blame that on Sprite Kit's implementation of SKShapeNode. Adding a fill to a path causes the following:
adds an extra node to your scene
the path becomes unmaskable - it appears 'over' the mask
makes any non-SKSpriteNode sibling nodes unmaskable (e.g. SKLabelNodes)
Not an ideal state of affairs.
Inspired by Tony Chamblee's progress timer, the 'fix' is to dispense with the fill altogether, and just use the stroke of the path:
SKCropNode *cropNode = [[SKCropNode alloc] init];
SKShapeNode *circleMask = [[SKShapeNode alloc ]init];
CGMutablePathRef circle = CGPathCreateMutable();
CGPathAddArc(circle, NULL, CGRectGetMidX(self.frame), CGRectGetMidY(self.frame), 50, 0, M_PI*2, YES); // replace 50 with HALF the desired radius of the circle
circleMask.path = circle;
circleMask.lineWidth = 100; // replace 100 with DOUBLE the desired radius of the circle
circleMask.strokeColor = [SKColor whiteColor];
circleMask.name=#"circleMask";
[cropNode setMaskNode:circleMask];
As commented, you need to set the radius to half of what you'd normally have, and the line width to double the radius.
Hopefully Apple will look at this in future; for now, this hack is the best solution I've found (other than using an image, which doesn't really work if your mask needs to be dynamic).
Since I cannot use a SKShapeNode as mask I decided to convert it to a SKSpriteNode.
Here it is my Swift code:
let shape : SKShapeNode = ... // create your SKShapeNode
var view : SKView = ... // you can get this from GameViewController
let texture = view.textureFromNode(shape)
let sprite = SKSpriteNode(texture: texture)
sprite.position = ...
cropNode.mask = sprite
And it does work :)
Yes, it is impossible to use fill-colored shapenode in current Sprite-Kit realization. It's a bug, I think.
But!
You always can render the shape to texture and use it as mask!
For ex, let edge is SKShapeNode created before.
First, render it to texture before adding to view (in this case it will clean from another nodes)
SKTexture *Mask = [self.view textureFromNode:edge];
[self addChild:edge]; //if you need physics borders
Second,
SKCropNode *cropNode = [[SKCropNode alloc] init];
[cropNode setMaskNode:[SKSpriteNode spriteNodeWithTexture:Mask]];
[cropNode addChild: [SKSpriteNode spriteNodeWithTexture:rocksTiles size:CGSizeMake(w,h)]];
cropNode.position = CGPointMake(Mask.size.width/2,Mask.size.height/2);
//note, anchorpoint of shape in 0,0, but rendered texture is in 0.5,0.5, so we need to dispose it
[self addChild:cropNode];
There is a slightly awkward solution to this issue that I believe is slightly less nasty than any of the mooted hacks around this issue. Simply wrap your SKShapeNode in an SKNode. I haven't tested what kind of performance hit this might cause, but given that SKNode is non-drawing, I'm hoping it won't be too significant.
i.e.
let background = SKSpriteNode(imageNamed: "Background")
let maskNode = SKShapeNode(path: MyPath)
let wrapperNode = SKNode()
let cropNode = SKCropNode()
wrapperNode.addChild(maskNode)
cropNode.maskNode = wrapperNode
cropNode.addChild(background)
Setting alpha of mask node to .0 does the trick:
circleMask.alpha = .0;
EDIT:
Seems to work for Mac OS only.

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.

Control the animation by touch

i have this type of code....
// create a UIImageView
UIImageView *rollDiceImageMainTemp = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"rollDiceAnimationImage1.png"]];
// position and size the UIImageView
rollDiceImageMainTemp.frame = CGRectMake(0, 0, 100, 100);
// create an array of images that will represent your animation (in this case the array contains 2 images but you will want more)
NSArray *savingHighScoreAnimationImages = [NSArray arrayWithObjects:
[UIImage imageNamed:#"rollDiceAnimationImage1.png"],
[UIImage imageNamed:#"rollDiceAnimationImage2.png"],
nil];
// set the new UIImageView to a property in your view controller
self.viewController.rollDiceImage = rollDiceImageMainTemp;
// release the UIImageView that you created with alloc and init to avoid memory leak
[rollDiceImageMainTemp release];
// set the animation images and duration, and repeat count on your UIImageView
[self.viewController.rollDiceImageMain setAnimationImages:savingHighScoreAnimationImages];
[self.viewController.rollDiceImageMain setAnimationDuration:2.0];
[self.viewController.rollDiceImageMain.animationRepeatCount:3];
// start the animation
[self.viewController.rollDiceImageMain startAnimating];
// show the new UIImageView
[self.viewController.view addSubview:self.rollDiceImageMain];
Instead of startAnimation directly..there any way to control this code using touchesMoved??
Actually, what you want is the - (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event. Below is a sample of code that will allow a UIView to move on the x-axis only. Adapt to whatever you need your code to do.
- (void)touchesMoved:(NSSet*)touches withEvent:(UIEvent*)event
{
CGPoint original = self.center;
UITouch* touch = [touches anyObject];
CGPoint location = [touch locationInView:self.superview];
// Taking the delta will give us a nice, smooth movement that will
// track with the touch.
float delta = location.x - original.x;
// We need to substract half of the width of the view
// because we are using the view's center to reposition it.
float maxPos = self.superview.bounds.size.width
- (self.frame.size.width * 0.5f);
float minPos = self.frame.size.width * 0.5f;
float intendedPos = delta + original.x;
// Make sure they can't move the view off-screen
if (intendedPos > maxPos)
{
intendedPos = maxPos;
}
// Make sure they can't move the view off-screen
if (intendedPos < minPos)
{
intendedPos = minPos;
}
self.center = CGPointMake(intendedPos, original.y);
// We want to cancel all other touches for the view
// because we don't want the touchInside event firing.
[self touchesCancelled:touches withEvent:event];
// Pass on the touches to the super
[super touchesMoved:touches withEvent:event];
}
Notice here that I am taking the delta of the movement and not with the finger. If you track with the finger you will get very erratic behavior that is very undesirable. Applying the delta will give nice, fluid movement of the view that will track perfectly with the touch input.
Update: Also, for those wondering why I chose to multiply by 0.5f rather than divide by 2, the ARM processor doesn't support division in hardware, so there is a minuscule performance bump by going with multiplication. This performance optimization may only get called only a few times during the life of a program so it might not make a difference, but this particular message is called many, many times when dragging. Because it is in this case, it might be worth the multiplication, instead.
You can detect touches by using UIResponder methods like - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event in that you can call the [self.viewController.rollDiceImageMain startAnimating]; method.
And once it starts then you can stop after some time and restrict the touches.