In iOS 7's SpriteKit framework, I am attempting to build a simple game for the purposes of learning the framework. One area I am tripping over a little bit is how to detect a specific node when there are multiple nodes overlapping under a touch. Let me give an example:
In a basic chess training game, I can drag a piece forward one tile, but what happens after that is dependent on what other nodes are in that space. I want to know which tile the touch is on, regardless of any other nodes which happen to also be on that tile node. The problem I am running into is that the touch seems to detect the uppermost node. So my question would be:
What is the recommended solution for detecting the tile node? I was thinking about using zPosition in some way but I have yet to determine how to do that. Any suggestions?
Another approach would be to detect ALL nodes under a touch. Is there a way to grab all nodes and put them in an array?
Iterate through the nodes at the touched point:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
NSArray *nodes = [self nodesAtPoint:[touch locationInNode:self]];
for (SKNode *node in nodes) {
//go through nodes, get the zPosition if you want
int nodePos = node.zPosition;
//or check the node against your nodes
if ([node.name isEqualToString:#"myNode1"]) {
//...
}
if ([node.name isEqualToString:#"myNode2"]) {
//...
}
}
}
You could calculate the tile at a given point, since you would know the frame of the board and therefore the size of each tile.
E.g. Assume you have a board whose frame is {10, 50, 160, 160}. You know therefore that the size of each tile would be 20x20. If you have a touch at point {x,y} You know that the index of the row touched is (x-10)/20 and the index of the column is (y-50)/20. Oh, and you would probably need to use floorf in that calculation too.
Alternatively, and to actually answer your question, you could use the nodesAtPoint: method to get all the nodes at a given point :)
Related
I am new to game development and was wondering what is the best approach for displaying game data on screen.
I have an endless runner where the sprite collects gems and I want the total to be displayed on screen. I was looking at placing a UIView with a UILabel above the SKView and getting the label to update after each touch.
I also want a game over screen to appear at the end and was also looking at creating this using a UIView.
Is this the best approach or should all data be kept within the SKView itself?
Hopefully this makes sense.
While I don't think there is anything very wrong with using UIKit components within a SpriteKit application, I would recommend using SpriteKit's own components as much as possible.
For text I would recommend using SKLabelNode instead of UILabel.
Also, for buttons and other GUI elements, I think SKSpriteNodes would be most sufficient.
Simple functional button example:
SKSpriteNode *fireNode = [SKSpriteNode spriteNodeWithImageNamed:#"fireButton.png"];
fireNode.position = CGPointMake(fireButtonX,fireButtonY);
fireNode.name = #"fireButtonNode";//how the node is identified later
Handle touches:
//handle touch events
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
//if fire button touched, bring the rain
if ([node.name isEqualToString:#"fireButtonNode"]) {
//do whatever...
}
}
I'm trying to determine if a point is inside a node that's been rotated. I've tried the following code in my scene:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
UITouch *touch = [touches anyObject];
if ([self.cardNode containsPoint:[touch locationInNode:self.cardNode.parent]]) {
NSLog(#"HIT");
}
}
But it seems that the nodes frame is still the same as to when it's not rotated. In other words, points outside of the rotated node (which are in the node when it's not rotated) are considered to be inside of it.
I've also tried using CGRectContainsPoint which produced the same results.
How can I determine if a point is in a rotated node?
In case if you have two textures you can use .contactTestBitMask property for two physical bodies so if they overlapping you will get notification and you can do whatever you want, that is it! (notice that you don't have to make any collision just notification) Here you should read about physicsBody property and didBeginContact method.
Because physics bodies created by texture so if they rotating it means that it's texture rotating so you will get what do you want.
But in case if you want to check if you touch is in location of some node, so you can use property
yourTouch.locationInNode(SOME_NODE)
Hope it helps!
I have 25 identical sprites in one category spread out in my scene, created with a for-loop. I want to be able to click one specific sprite to, for example, start rotating that sprite. Would I have to make nonatomic properties for every node?
You could use the - (SKNode *)nodeAtPoint:(CGPoint)p method of SKNode (a class from which SKScene inherits) to pin-point a specific node. Say you have 25 random nodes about the screen and you want to make one of them perform an action, you would have to find the touch location, then locate the node in that position, then make it perform an action.
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
CGPoint touchLocation = [[touches anyObject] locationInNode:self];
SKNode *specificNode = [self nodeAtPoint:touchLocation];
// make specificNode perform some SKAction
}
i followed this wonderful guide about mario-style game:
http://www.raywenderlich.com/62053/sprite-kit-tutorial-make-platform-game-like-super-mario-brothers-part-2
however, i wanted to convert the movement controls to arrow keys, implemented by SKSpriteNodes with names that are detected by:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode* node = [self nodeAtPoint:location];
// other left, up, down arrows with same code here
if ([node.name isEqualToString:#"rightArrow"]) {
self.player.moveRight = YES;
self.rightOriginalTouchLocation = location;
...
}
}
self.player.moveRight is a boolean value (much like moveForward in the guide), that tells the character at update to move.
it is terminated at:
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode* node = [self nodeAtPoint:location];
// other left, up, down arrows with same code here
if ([node.name isEqualToString:#"rightArrow"]) {
self.player.moveRight = NO;
}
...
}
however, i encounter the following problem - when i start the touch on the arrow, drag it outside the arrow, and then release the tap, it is not recognized as 'touch ended' for the arrow node (and it doesn't stop moving because of that).
i tried to solve it in many ways (even calculating touch move distance from original location and see if its too far, then cancel movement), but i always manage to reproduce the constant motion problem.
the issue lies with the fact that i can tap two arrows at the same time, so it is not enough to remember the last node tapped.
since i want to allow movement for different directions at the same time, i cant stop all movements in case one button is dismissed. i need to specifically know which button was released so i can stop that direction's movement only.
do you have any ideas for me? should i implement it in another way considering i want the arrow keys, or is there a method to detect which node is released even though it is not at its original location (of the tap)?
Thank you very much!
if anyone is interested about the issue - i had a problem where touching and moving from SKSpriteNode didnt call the touchesEnded for that SKSpriteNode (since the 'release' of the touch was not in the node).
i solved it by keeping the CGPoint of the touch at touchesBegan, and when touchesEnded called, i calculated distances to nearest key using simple distance function:
-(int)calculateDistanceWithPoints:(CGPoint)point1 andPoint:(CGPoint)point2 {
float d = sqrtf(pow((point1.x - point2.x), 2) + pow((point1.y - point2.y), 2));
return (int)d;
}
and then, in touchesEnded, i checked to which key the distance is minimal(i have 3 keys, so which key is most likely to be the key that was 'released') - and did the action required for that key release.
I've been busy for a few days trying to figure out how to handle touch in my Cocos2d project. The situation is a bit different as normal. I have a few different game layers that have items on it that I need to control with touch:
ControlLayer: Holds the game controls
(movement, action button). This layer is on top.
GameplayLayer: Holds the game objects
(CCSprites). This layer is directly beneath the ControlLayer.
Now my touches work fine in the ControlLayer, I can move my playable character around and make him jump and do other silly stuff. Yet I cannot grasp how to implement the touches to some of my CCSprites.
The information I've gathered so far makes me think I need get all my touch input from the control layer. Then I somehow need to 'cascade' the touch information to the GameplayLayer so I can handle the input there. Another option would be for me to get the CGRect information from my sprites by somehow creating an array with pointers to the objects that should be touchable. I should be able to use that information in the ControlLayer to check for each item in that list if the item was touched.
What is the best option to do this, and how do I implement this? I'm kind of new to programming with cocoa and Objective C so I'm not really sure what the best option is for this language and how to access the sprites CGRect information ([mySpriteName boundingBox]) in another class then the layer it is rendered in.
At the moment the only way I'm sure to get it to work is create duplicate CGRects for each CCSprite position and so I can check them, but I know this is not the right way to do it.
What I have so far (to test) is this:
ControlLayer.m
- (void)ccTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInView: [touch view]];
CGRect rect = CGRectMake(0.0f, 0.0f, 100.0f, 100.0f);
//Tried some stuff here to get see if I could get a sprite by tagname so I could use it's bounding box but that didn't work
// Check for touch with specific location
if (CGRectContainsPoint([tree boundingBox], location)) {
CCLOG(#"CGRect contains the location, touched!");
}
CCLOG(#"Layer touched at %#", NSStringFromCGPoint(location));
}
Thanks in advance for helping me!
The easiest and simplest way to solve your problem, IMO, is by using ccTouchBegan/Moved/Ended instead of ccTouchesBegan/Moved/Ended. Meaning, you are handling a single touch at a particular moment so you avoid getting confuses over multiple touches, plus the most important feature of ccTouchBegan is a CCLayer can 'consume' the touch and stop it from propagating to the next layers. More explanation after code samples below.
Here are steps to do it. Implement these sets of methods in all CCLayer subclasses that should handle touch events:
First, register with CCTouchDispatcher:
- (void)registerWithTouchDispatcher {
[[CCTouchDispatcher sharedDispatcher] addTargetedDelegate:self priority:0 swallowsTouches:YES];
}
Next, implement ccTouchBegan, example below is from a game I've created (some part omitted of course):
- (BOOL)ccTouchBegan:(UITouch *)touch withEvent:(UIEvent *)event {
if (scene.state != lvlPlaying) {
// don't accept touch if not playing
return NO;
}
CGPoint location = [self convertTouchToNodeSpace:touch];
if (scene.mode == modePlaying && !firstTouch) {
if (CGRectContainsPoint(snb_putt.sprite.boundingBox, location)) {
touchOnPutt = touch.timestamp;
// do stuff
// return YES to consume the touch
return YES;
}
}
// default to not consume touch
return NO;
}
And finally implement ccTouchMoved and ccTouchEnded like the ccTouches* counterparts, except that they handle single touch instead of touches. The touch that is passed to these methods is restricted to the one that is consumed in ccTouchBegan so no need to do validation in these two methods.
Basically this is how it works. A touch event is passed by CCScene to each of its CCLayers one by one based on the z-ordering (i.e starts from the top layer to the bottom layer), until any of the layers consume the touch. So if a layer at the top (e.g. control layer) consume the touch, the touch won't be propagated to the next layer (e.g. object layer). This way each layer only has to worry about itself to decide whether to consume the touch or not. If it decides that the touch cannot be used, then it just has to not consume the touch (return NO from ccTouchBegan) and the touch will automatically propagate down the layers.
Hope this helps.