Unexpected SpriteKit batch drawing results - ios

I am currently working on a iOS game using SpriteKit and Objective-C. I've recently been trying to optimize my draw calls, but I've run into an issue where I am either misunderstand the batch draw operation and/or I am receiving some unexpected results. Additionally I have read the Apple documentation regarding batch drawing, I'm just afraid I may have misinterpreted some of the guidelines.
So here is the rundown: (assume that all atlases are preloaded and that self is a SKScene)
This works as you would expect. The first touch adds +1 to both the node and draw count. Subsequent touches add to the node count but not the draw count.
/*Code Block 1:*/
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
SKSpriteNode *touch1 = [SKSpriteNode spriteNodeWithTexture:[[SKTextureAtlas atlasNamed:#"TouchAtlas1"] textureNamed:#"texture1"]];
[touch1 setPosition:location];
[self addChild:enemyTouch];
}
When I start adding two sprites from different atlases to the parent I start to get too many draw calls. What I expected: on the first touch I would get +2 nodes and +2 draw calls and subsequent touches produce +2 nodes and +0 draw calls. What I got: on the first touch I got +2 nodes and +2 draw calls and subsequent touches produced +2 nodes and +2 draw calls.
/*Code Block 2:*/
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
SKSpriteNode *touch1 = [SKSpriteNode spriteNodeWithTexture:[[SKTextureAtlas atlasNamed:#"TouchAtlas1"] textureNamed:#"texture1"]];
[touch1 setPosition:location];
[self addChild:touch1];
SKSpriteNode *touch2 = [SKSpriteNode spriteNodeWithTexture:[[SKTextureAtlas atlasNamed:#"TouchAtlas2"] textureNamed:#"texture2"]];
[touch2 setPosition:location];
[self addChild:touch2];
}
I tried one additional step of adding a child SKNode *extraLayer to the scene and adding one sprite to the scene and one sprite to the extraLayer. This produced the desired results of +2 nodes and +0 draw calls I wanted above.
/*Code Block 3:*/
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[super touchesBegan:touches withEvent:event];
UITouch *touch = [touches anyObject];
CGPoint location = [touch locationInNode:self];
SKNode *node = [self nodeAtPoint:location];
SKSpriteNode *touch1 = [SKSpriteNode spriteNodeWithTexture:[[SKTextureAtlas atlasNamed:#"TouchAtlas1"] textureNamed:#"texture1"]];
[touch1 setPosition:location];
[self addChild:touch1];
SKSpriteNode *touch2 = [SKSpriteNode spriteNodeWithTexture:[[SKTextureAtlas atlasNamed:#"TouchAtlas2"] textureNamed:#"texture2"]];
[touch2 setPosition:location];
[self.extraLayer addChild:touch2];
}
The question: Is this in fact the way batching draw calls needs to be handled? All sprites whose textures come from common atlases need to be children of the same parent, but additionally that parent has no children who's textures come from a different atlas? Or is there a way of making the method of block 2 batch correctly?

Related

Move the SKSpriteNode to the actual touch point in ios Game

I have a SKSpriteNode, which is a ball. My view don't have gravity. I'm able to move the ball by applying an impulse over it in the start and I want to make it to move to the user touch point which we can get in -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event method without affecting its motion.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches)
{
CGPoint location = [touch locationInView:self.view];
SKAction *moveToPoint = [SKAction moveByX:location.x y:location.y duration:2];
[ball runAction:moveToPoint];
}
}
Its not seems to be working. Help me guys.
If you want to send the node directly to a point use the method:
[SKAction moveTo:location duration:2];
The SKAction moveBy: method moves the node by the x and y values provided, whereas the moveTo method to the point in the node's parent.
EDIT:
Also, change the line:
CGPoint location = [touch locationInView:self.view];
to
CGPoint location = [touch locationInNode:self];
A UIView has a different coordinate system than a SKScene.
EDIT 2: You can remove affect of physics while touch exists
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
for (UITouch *touch in touches)
{
ball.physicsBody.dynamic = NO;
CGPoint location = [touch locationInNode: self];
SKAction *moveToPoint = [SKAction moveTo:location duration:2];
[ball runAction:moveToPoint];
}
}
-(void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
ball.physicsBody.dynamic = YES;
}

Node that was "touched" wasn't actually touched

i trying to make an app with spriteKit in which when the user touched a sprite node, there's a NSLog message that says that the node was touched.
I tried to do it with comparing the name of the touched node with the name of the node that i check if was touched. The problem is that the system recognise that the node was touched although it wasn't touched. I think that there is a problem with the node area, the system thinks that a node is somewhere while it's absolutely not.
-(void)selectNodeForTouch:(CGPoint)touchLocation
{
if([[touchedNode name] isEqualToString:#"menuPlayButton"]){
NSLog(#"Pressed");
}
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
/* Called when a touch begins */
for (UITouch *touch in touches) {
UITouch *touch = [[event allTouches] anyObject];
CGPoint location = [touch locationInView:touch.view];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
}
Any ideas what might the problem be?
Thanks in advance
Your selectNodeForTouch: method is completely wrong. It uses touchedNode variable, which is not defined anywhere in the code you posted. This method should look like this:
- (void)selectNodeForTouch:(CGPoint)touchLocation
{
NSArray *nodes = [self nodesAtPoint:touchLocation];
for (SKNode *node in nodes) {
if([node.name isEqualToString:#"menuPlayButton"]) {
NSLog(#"Pressed");
}
}
}

Drag a SKSpriteNode (follow your finger movements) with SKPhysics (collision)

Want to drag a SKSpriteNode (follow your finger movements) with SKPhysics (collision enabled)
2 failed solutions:
1)
a) Solution: Change the position of the SKSpriteNode
b) Problem: The SKSpriteNode will "magically" pass through a wall unexpectedly when user drags the spriteNode quickly through the wall (Physics seems not to work)
c) code:
#property (nonatomic) CGPoint translationBy;
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPoint previousPosition = [touch previousLocationInNode:self];
CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);
[self panForTranslation:translation];
}
- (void)panForTranslation:(CGPoint)translation {
CGPoint position = [_selectedNode position];
if([[_selectedNode name] isEqualToString:PLAYER_NAME]) {
[_selectedNode setPosition:CGPointMake(position.x + translation.x, position.y + translation.y)];
}
}
2) After searching around here found this advice by #LearnCocos2D "When you use physics, stick to moving the physics body through force, impulse or changing the body velocity directly." So I tried:
a) Solution: Apply impulse on the sprite node.
b) Problem: The sprite node will keep on moving even if I didn't drag the sprite node. (Behaving like a floating boat. Expect it to act like a car once the drag is stopped the sprite node will stop; once drag begins it will follow the finger.)
c) Code:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
[self selectNodeForTouch:positionInScene];
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPoint previousPosition = [touch previousLocationInNode:self];
CGPoint translation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);
CGVector forceVector = CGVectorMake(translation.x, translation.y);
[self.player.physicsBody applyImpulse:forceVector];
//[self.player.physicsBody setVelocity:CGVectorMake(0, 0)];
}
-->>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
Updated:
From the comment:
"i) You could determine the distance of the node to the finger location, then set the velocity every update: so that it will exactly be on the finger's position the next frame. ii) If both finger and node position are identical this will cancel out any movement, otherwise the node will stick to your finger while performing all physics collisions and will continue to push objects between itself and the finger in cases where it is obstructed."
a) Question: How to disable flick?
b) Problem: The sprite node will move very far away with a minor (accidental) flick.
c) code:
#property (nonatomic) CGPoint translationBy;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch *touch = [touches anyObject];
CGPoint positionInScene = [touch locationInNode:self];
CGPoint previousPosition = [touch previousLocationInNode:self];
// 1) You could determine the distance of the node to the finger location,
CGPoint xyTranslation = CGPointMake(positionInScene.x - previousPosition.x, positionInScene.y - previousPosition.y);
if (fabsf(positionInScene.x - previousPosition.x) < kPlayerMovementThreshold) {
xyTranslation.x = 0;
}
if (fabsf(positionInScene.y - previousPosition.y) < kPlayerMovementThreshold) {
xyTranslation.y = 0;
}
self.translationBy = xyTranslation;
}
static const CGFloat kPlayerMovementSpeed = 55.0f;
static const CGFloat kPlayerMovementThreshold = 1.0f;
-(void)update:(CFTimeInterval)currentTime{
// ii) then set the velocity every update: so that it will exactly be on the finger's position the next frame.
CGFloat dx = self.translationBy.x*kPlayerMovementSpeed;
CGFloat dy = self.translationBy.y*kPlayerMovementSpeed;
CGVector velocityVector = CGVectorMake(dx,dy);
[self.player.physicsBody setVelocity:velocityVector];
}
What I do is make an SKNode, let's call it 'a', with a very small physics body attached to it. This new node is connected via an SKPhysicsJointSpring to the node I want to move, let's call that 'b'. The new node a tracks my finger movement, and the spring makes sure the target node b follows that. Because the node b is positioned using physics, the collision detection works as expected.

Dragging 1 sprite node relevant to finger movement?

I have created a node called and i have amanged to make it drag from across the screen when the node it touched and dragged. For some reason this one method (code below) lets me drap any node on the screen. How can i make effect only the one "testNode2".
Also i would the node to drag to the movement of the finger but this can work if the finger is touch anywhere on the screen, not just when the node itself is touched? (but not jump to the position of the finger, just move relevant to the finger movement). For example is the screen is pressed anywhere then dragged 100 pixels left the node will move 100 pixels left.
my code is below
-(void) colourSprite2:(CGSize)size {
self.testNode2 = [SKSpriteNode spriteNodeWithColor:[SKColor greenColor] size:CGSizeMake(30, 30)];
self.testNode2.position = CGPointMake(self.size.width/2, self.size.height/1.1);
self.testNode2.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:self.testNode2.frame.size];
[self addChild:self.testNode2];
}
-(void)touchesBegan:(NSSet*) touches withEvent:(UIEvent*) event
{ self.testNode2 = [self nodeAtPoint:[[touches anyObject] locationInNode:self]]; }
-(void)touchesMoved:(NSSet*) touches withEvent:(UIEvent*) event
{ self.testNode2.position = [[touches anyObject] locationInNode:self]; }
-(void)touchesEnded:(NSSet*) touches withEvent:(UIEvent*) event
{ self.testNode2 = nil; }
The touch delegate returns an NSSet of touches which holds multiple UITouch objects, each of which correspond to a touch relevant to the object the delegate methods are implemented in.
In your case, the node will move itself to the position of any touch the delegate encounters. This includes multiple touches on the screen.
You should read about UITouch and the UIResponder classes.
The solution for your problem will be to keep a track of the specific touch that is being used to move the node.
Maintain a UITouch object as an instance variable:
#implementation MyScene
{
UITouch *currentTouch;
}
Then keep a track of the specific touch as follows:
-(void)touchesBegan:(NSSet*) touches withEvent:(UIEvent*) event
{
UITouch *touch = [touches anyObject];
SKNode *node = [self nodeAtPoint:[touch locationInNode:self]];
if (currentTouch == nil && [node isEqual:self.testNode2])
{
currentTouch = touch;
}
}
-(void)touchesMoved:(NSSet*) touches withEvent:(UIEvent*) event
{
UITouch *touch = [touches anyObject];
if ([touch isEqual:currentTouch])
{
self.testNode2.position = [touch locationInNode:self];
}
}
-(void)touchesEnded:(NSSet*) touches withEvent:(UIEvent*) event
{
UITouch *touch = [touches anyObject];
if ([touch isEqual:currentTouch])
{
self.testNode2 = nil;
currentTouch = nil;
}
}

How to detect touches in physicsBody area?

I did create SKSpriteNode using this code and physicsBody is circle. How I could check that user did touch eare in the physicsBody?
self = [super initWithColor:[SKColor clearColor] size:CGSizeMake(200, 200)];
//...
self.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:self.size.width/2.0f];
self.userInteractionEnabled = YES;
I did found solution for my problem using this code:
CGMutablePathRef pathRef;
// Fill pathRef with points and create phisycs body using pathRef.
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInNode:self.scene];
if (CGPathContainsPoint(pathRef, nil, touchLocation, NO)) {
// If we are here it means user did touche physic body.
}
}
The easiest solution would be to create a second invisible sprite, with the size you need, and place it directly over the area you want to record touches on.
The other option is to store ALL the coordinates which trigger a valid touch and compare them against the actual touch coordinate in your touch method, like this:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
UITouch *touch = [touches anyObject];
CGPoint touchLocation = [touch locationInNode:self.scene];
NSLog(#"%#", NSStringFromCGPoint(touchLocation));
// Compare the touchLocation coordinate against your list of acceptable coordinates...
}

Resources