I'm using the following code to implement a flick on sprites within a SpriteKit SKScene so they will continue to move and slide across the screen based on the motion of a finger from a UIPanGestureRecognizer.
For the most part it works fine, however when I get close to the edges of the viewable area of the screen and attempt this gesture to flick the sprite offscreen., it has the opposite effect and pushed the sprite back towards the center rather than offscreen.
Can anyone tell me why when the sprite it too close to the edge of the screen it's getting pushed back in rather than flicked offscreen as expected?
if (recognizer.state == UIGestureRecognizerStateEnded) {
if ([touchedNode.name isEqualToString:#"sprite"]) {
SKSpriteNode *touchedSprite = (SKSpriteNode *)touchedNode;
// Calculate the length of the velocity vector (i.e. the magnitude)
CGPoint velocity = [recognizer velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
CGFloat slideMult = magnitude / 200;
NSLog(#"magnitude: %f, slideMult: %f", magnitude, slideMult);
// Calculate a final point based on the above
float slideFactor = 0.1 * slideMult; // Increase for more of a slide
CGPoint finalPoint = CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor), recognizer.view.center.y - (velocity.y * slideFactor));
SKAction *moveAction = [SKAction moveTo:finalPoint duration:slideFactor * 2];
[moveAction setTimingMode:SKActionTimingEaseOut];
[touchedSprite runAction:moveAction];
}
}
I think that it is possible, that you forgot to convert your CGPoint from view to the scene
Let's assume that we initalize our gesture
__weak IBOutlet SKView *skView;
...
panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePan:)];
[skView addGestureRecognizer:panRecognizer];
so if we want to have touch location of this gesture recognizer in SpriteKit space, we have to conver this point from view to skScene
- (void)handlePan:(UIPanGestureRecognizer*)sender{
CGPoint point = [sender locationInView:skView];
point = [skView.scene convertPointFromView:point];
// do your things here
}
The problem is that the calculation for the final CGPoint is based on where the UIPanGestureRecognizer was added. In this case it has to be added to the SKView as an SKSpriteNode is not a UIView subclass so cannot have a UIPanGestureRecognizer added to it.
Here's the old calculation as above
CGPoint finalPoint =
CGPointMake(recognizer.view.center.x + (velocity.x * slideFactor),
recognizer.view.center.y - (velocity.y * slideFactor));
Here's the change made to use the centre of the sprite node as the base for the calculation.
CGPoint finalPoint =
CGPointMake(touchedSprite.position.x
+ (touchedSprite.size.width / 2)
+ (velocity.x * slideFactor),
touchedNode.position.y
+ (touchedNode.size.height / 2)
- (velocity.y * slideFactor));
Related
I currently have 2 circles. One big circle and one little circle. The little circle has a tap gesture recognizer that allows it to be dragged by the user. I would like the little circle's center to go no further than the big circle's radius. I have 4 auto layout constraints on the inner circle. 1 for fixed width, 1 for fixed height, 1 for distance from center for x, and 1 for distance from center for y. Here is how I am going about this:
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.view];
CGFloat x = recognizer.view.center.x + translation.x;
CGFloat y = recognizer.view.center.y + translation.y;
CGPoint desiredPoint = CGPointMake(x, y);
//check if point the user is trying to get to is outside the radius of the outer circle
//if it is, set the center of the inner circle to the right position at the distance of the radius and with the same angle
if ([self distanceBetweenStartPoint:self.outerCircleView.center endPoint:desiredPoint] > self.outerCircleRadius) {
CGFloat angle = [self angleBetweenStartPoint:self.outerCircleView.center endPoint:actualPosition];
desiredPoint = [self findPointFromRadius:self.outerCircleRadius startPoint:self.outerCircleView.center angle:angle];
}
//adjust the constraints to move the inner circle
self.innerCircleCenterXConstraint.constant += actualPosition.x - recognizer.view.center.x;
self.innerCircleCenterYConstraint.constant += actualPosition.y - recognizer.view.center.y;
[recognizer setTranslation:CGPointMake(0.0, 0.0) inView:self.view];
}
}
- (CGFloat)distanceBetweenStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
CGFloat xDif = endPoint.x - startPoint.x;
CGFloat yDif = endPoint.y - startPoint.y;
//pythagorean theorem
return sqrt((xDif * xDif) + (yDif * yDif));
}
- (CGPoint)findPointFromRadius:(CGFloat)radius startPoint:(CGPoint)startPoint angle:(CGFloat)angle {
CGFloat x = radius * cos(angle) + startPoint.x;
CGFloat y = radius * sin(angle) + startPoint.y;
return CGPointMake(x, y);
}
- (CGFloat)angleBetweenStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint {
CGPoint originPoint = CGPointMake(endPoint.x - startPoint.x, endPoint.y - startPoint.y);
return atan2f(originPoint.y, originPoint.x);
}
This works almost perfectly. The problem is I try to find the percentage that the user moved towards the outside of the circle. So I use the distanceBetweenStartPoint(center of outer circle) endPoint(center of inner circle) method and divide that by the radius of the outer circle. This should give me a value of 1 when the circle has been dragged as far to one side as it can go. Unfortunately I am getting values like 0.9994324 or 1.000923. What could be causing this? Thanks for any insight!
I'm creating a game in Spritekit and am using the Pan Gesture with the Pinch Gesture in order to view a map. Currently I am limiting the zoom of the Pinch Gesture to 2.0 scale. I'm trying to limit the Pan Gesture to the bounds of the screen even when zoomed. The map is larger than the screen and I only want the user to be able to pan around until the outer edge of the map hits the outer edge of the screen, even when zoomed. Here is my amateur way of trying to handle the situation:
-(void) handlePanFrom:(UIPanGestureRecognizer*)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) {
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(-translation.x, translation.y);
CGPoint desiredPos = CGPointSubtract(_mapNode.position, translation);
NSLog(#"Moving map to x: %f y: %f", desiredPos.x, desiredPos.y);
NSLog(#"Map node position x: %f y: %f", _mapNode.position.x, _mapNode.position.y);
NSLog(#"Map scale is %f", mapScale);
NSLog(#"Map size is x: %f y: %f", _mapNode.map.frame.size.width, _mapNode.map.frame.size.height);
if (desiredPos.y <= (mapScale * 300) && desiredPos.y >= ((1/mapScale) * 200)) {
_mapNode.position = CGPointMake(_mapNode.position.x, desiredPos.y);
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
if (desiredPos.x <= (mapScale * 250) && desiredPos.x >= ((1/mapScale) * 77)) {
_mapNode.position = CGPointMake(desiredPos.x, _mapNode.position.y);
[recognizer setTranslation:CGPointZero inView:recognizer.view];
}
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
}
}
I set a scale iVar when zooming to get the scale of the node. I should be able to use the sizes of the nodes (_mapNode.map is an SKSpriteNode) and the screen size to get the panning I want.
I thought I could do scale*xMax and (1/scale)*xMin (also with y position) but that doesn't seem to work. I would love to not have hard numbers in there (300, 200, etc) and use the sizing of the nodes/screen/etc to limit the panning. Any help is appreciated! Thanks!
Okay this is what I did for future reference for people.
I first created two iVars: one for the initial x position of the node and one for the initial y position. I then calculated a 'movable distance' by taking: sizeOfNode * 0.5 * scale and subtracting screenSize * 0.5. As long as desiredLocation <= (initialPos + movableDistance) and desiredLocation >= (initialPos - moveableDistance) then you could change the position of the node. You can do this for both x and y.
I've been working on this code for quite a while now but it just feels like one step forward and two steps back. I'm hoping someone can help me.
I'm working with Sprite Kit so I have a Scene file that manages the rendering, UI and touch controls. I have an SKNode thats functioning as the camera like so:
_world = [[SKNode alloc] init];
[_world setName:#"world"];
[self addChild:_world];
I am using UIGestureRecognizer, so I add the ones I need like so:
_panRecognizer = [[UIPanGestureRecognizer alloc]initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panRecognizer];
_pinchRecognizer = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(handlePinch:)];
[[self view] addGestureRecognizer:_pinchRecognizer];
The panning is working okay, but not great. The pinching is the real problem. The idea for the pinching is to grab a point at the center of the screen, convert that point to the world node, and then move to it while zooming in. Here is the method for pinching:
-(void) handlePinch:(UIPinchGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateBegan) {
_tempScale = [sender scale];
}
if (sender.state == UIGestureRecognizerStateChanged) {
if([sender scale] > _tempScale) {
if (_world.xScale < 6) {
//_world.xScale += 0.05;
//_world.yScale += 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale += 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale)*-1), (newWorldPoint.y*_world.yScale)*-1);
//why does the duration have to be exactly 0.3 to work
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
} else if ([sender scale] < _tempScale) {
if (_world.xScale > 0.5 && _world.xScale > 0.3){
//_world.xScale -= 0.05;
//_world.yScale -= 0.05;
//[_world setScale:[sender scale]];
[_world setScale:_world.xScale -= 0.05];
CGPoint screenCenter = CGPointMake(_initialScreenSize.width/2, _initialScreenSize.height/2);
CGPoint newWorldPoint = [self convertTouchPointToWorld:screenCenter];
//crazy method why does this work
CGPoint alteredWorldCenter = CGPointMake(((newWorldPoint.x*_world.xScale - _initialScreenSize.width)*-1), (newWorldPoint.y*_world.yScale - _initialScreenSize.height)*-1);
SKAction *moveToCenter = [SKAction moveTo:alteredWorldCenter duration:0.3];
[_world runAction:moveToCenter];
}
}
}
if (sender.state == UIGestureRecognizerStateEnded) {
[_world removeAllActions];
}
}
I've tried many iterations of this, but this exact code is what is getting me the closest to pinching on a point in the world. There are some problems though. As you get further out from the center, it doesn't work as well, as it pretty much still tries to zoom in on the very center of the world. After converting the center point to the world node, I still need to manipulate it again to get it centered properly (the formula I describe as crazy). And it has to be different for zooming in and zooming out to work. The duration of the move action has to be set to 0.3 or it pretty much won't work at all. Higher or lower and it doesn't zoom in on the center point. If I try to increment the zoom by more than a small amount, it moves crazy fast. If I don't end the actions when the pinch ends, the screen jerks. I don't understand why this works at all (it smoothly zooms in to the center point before the delay ends and the screen jerks) and I'm not sure what I'm doing wrong. Any help is much appreciated!
Take a look at my answer to a very similar question.
https://stackoverflow.com/a/21947549/3148272
The code I posted "anchors" the zoom at the location of the pinch gesture instead of the center of the screen, but that is easy to change as I tried it both ways.
As requested in the comments below, I am also adding my panning code to this answer.
Panning Code...
// instance variables of MyScene.
SKNode *_mySkNode;
UIPanGestureRecognizer *_panGestureRecognizer;
- (void)didMoveToView:(SKView *)view
{
_panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(handlePanFrom:)];
[[self view] addGestureRecognizer:_panGestureRecognizer];
}
- (void)handlePanFrom:(UIPanGestureRecognizer *)recognizer
{
if (recognizer.state == UIGestureRecognizerStateBegan) {
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(-translation.x, translation.y);
_mySkNode.position = CGPointSubtract(_mySkNode.position, translation);
[recognizer setTranslation:CGPointZero inView:recognizer.view];
} else if (recognizer.state == UIGestureRecognizerStateEnded) {
// No code needed for panning.
}
}
The following are the two helper functions that were used above. They are from the Ray Wenderlich book on Sprite Kit.
SKT_INLINE CGPoint CGPointAdd(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x + point2.x, point1.y + point2.y);
}
SKT_INLINE CGPoint CGPointSubtract(CGPoint point1, CGPoint point2) {
return CGPointMake(point1.x - point2.x, point1.y - point2.y);
}
In an app I am working on, I'm performing a transition where I'm using a transform to scale a containerView that holds on to the view of a UINavigationController. This transform is tied to a pan gesture recognizer, so depending on the distance of the pan, the transform changes.
On iOS7, the navigationBar is set to be translucent but dark. As I apply the transform, certain values cause the navigationBar to render oddly... see pictures
The result is that I get this flickering affect where the navigationBar changes the degree of translucency as the pan occurs. Any idea why this might be?
Thanks!
EDIT:
Here is the code that does it:
- (void)handlePanRecognizer:(UIPanGestureRecognizer *)recognizer
{
if ([recognizer isEqual:self.panGestureRecognizer])
{
if ([recognizer state] == UIGestureRecognizerStateBegan || [recognizer state] == UIGestureRecognizerStateChanged)
{
CGPoint translation = [recognizer translationInView:recognizer.view];
CGFloat xMin = CGRectGetMidX([self centerContainerFrame]);
CGFloat xValue = self.rightContainer.center.x + translation.x;
[self.rightContainer setCenter:CGPointMake(MAX(xMin, xValue), self.rightContainer.center.y)];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
CGFloat normalizedTranslation = self.rightContainer.frame.origin.x / self.rightContainer.frame.size.width;
CGFloat relativeZoom = (1.0 - self.transformScaleFactor) * normalizedTranslation;
CGFloat relativeAlpha = (1.0 - self.minFadeAlpha) * normalizedTranslation;
CGAffineTransform transform = CGAffineTransformMakeScale(self.transformScaleFactor + relativeZoom, self.transformScaleFactor + relativeZoom);
CGFloat newAlpha = relativeAlpha + self.minFadeAlpha;
[self.centerContainer setTransform:transform];
[self.centerContainer setAlpha:newAlpha];
}
...etc
I'm trying to build a GestureRecognizer to drag a UIView from right to left. If the user drag the View to the half of the screen, the View will be automatically dragged into the left corner to discard it, but if the user do not drag up to half the screen it will return to the corner right of the screen.
Little lost here, any tutorials or something?
Thanks
EDIT : Heres some code, its a UIImageView, inside a UIScrollView, i need to drag the entire scroll or just the image inside it :
_myScroll = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width, self.window.bounds.size.height)]; [_myScroll setContentSize:CGSizeMake(self.window.bounds.size.width , 1300)];
_tutorial = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, self.window.bounds.size.width, 1300)]; [_tutorial setImage:[UIImage imageNamed:#"Default"]];
_tutorial.contentMode = UIViewContentModeScaleAspectFit; [_myScroll addSubview:_tutorial];
_tutorial.userInteractionEnabled = YES;
UIPanGestureRecognizer * recognizer = [[UIPanGestureRecognizer alloc]initWithTarget:_tutorial action:#selector(handlePan:)];
[_tutorial addGestureRecognizer:recognizer];
[self.window addSubview:_myScroll];
And the method i try to drag the UIImageView
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_myScroll];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:_myScroll];
}
Have a look over here. This is a UIGestureRecognizer tutorial which will give you the basics to handle pinch and pan gestures. Once you've learnt the basics, it's really easy to accomplish what you want. All you need is to check the position of the view once the pan is completed to send it to the correct position. Have a look at this other tutorial on UIScrollViews, it may help you find your way through the second part.
Both tutorials come from raywenderlich.com, probably the most useful iOS tutorials site I know.
Here is some code on your handlePan: method you can work on :
- (IBAction)handlePan:(UIPanGestureRecognizer *)recognizer {
CGPoint translation = [recognizer translationInView:_myScroll];
recognizer.view.center = CGPointMake(recognizer.view.center.x + translation.x,
recognizer.view.center.y + translation.y);
if (recognizer.state == UIGestureRecognizerStateEnded) {
// Check here for the position of the view when the user stops touching the screen
// Set "CGFloat finalX" and "CGFloat finalY", depending on the last position of the touch
// Use this to animate the position of your view to where you want
[UIView animateWithDuration: aDuration
delay: 0
options: UIViewAnimationOptionCurveEaseOut
animations:^{
CGPoint finalPoint = CGPointMake(finalX, finalY);
recognizer.view.center = finalPoint; }
completion:nil];
}
[recognizer setTranslation:CGPointMake(0, 0) inView:_myScroll];
}
I won't give you the perfect answer here, you'll have to work on the code to find a way to make it work as you want it to.
Here is one last tip. You can do some kind of "smooth" animation by using a slideFactor. It will help your animation being more "realistic". Work on this code, that you add right at the beginning of the "if":
CGPoint velocity = [recognizer velocityInView:self.view];
CGFloat magnitude = sqrtf((velocity.x * velocity.x) + (velocity.y * velocity.y));
CGFloat slideMult = magnitude / 200;
float slideFactor = 0.1 * slideMult; // Increase for more slide
Then in UIView animateWithDuration:, use slideFactor as the duration.