So I'm trying to make an app where you pick up objects with your finger, it moves with your finger and then you flick it away to throw it. I found some threads for similar games but not with this particular problem.
Since I couldn't really keep the object along with my finger by applying force or impulse I disable the physics on it while I'm "holding it", measure velocity so when I release it I apply that vector as an impulse.
This works excellent visually but when I try to read the position of the object it's not synced up with "reality". I've made a condition so if velocity isn't strong enough when you release the object it is supposed to return to its starting position but it doesn't move at all (except if you manage to throw it, that works fine) so when I tried to output the .position to the console it didn't correlate to what I was seeing on the screen.
Any ideas? I know you shouldn't set position and simulate physics at the same time but I disable the physics for when I'm dragging the object. Why won't it keep track of .position just because it is moving it with physics?
Here I try to throw a potion:
- (void)handlePan:(UIPanGestureRecognizer *)recognizer {
if (recognizer.state == UIGestureRecognizerStateBegan) { //Picked up
potion.physicsBody.dynamic = NO;
potion.physicsBody.velocity = CGVectorMake(0.0, 0.0);
potion.physicsBody.angularVelocity = 0.0;
potion.zRotation = 0.0;
CGPoint touchLocation = [recognizer locationInView:recognizer.view];
touchLocation = [self convertPointFromView:touchLocation];
[self selectNodeForTouch:touchLocation];
} else if (recognizer.state == UIGestureRecognizerStateChanged) { //Moved around
CGPoint translation = [recognizer translationInView:recognizer.view];
translation = CGPointMake(translation.x, -translation.y);
[self panForTranslation:translation];
[recognizer setTranslation:CGPointZero inView:recognizer.view];
velocity = CGVectorMake(translation.x, translation.y);
} else if (recognizer.state == UIGestureRecognizerStateEnded) { //Dropped
_selectedNode = nil;
potion.physicsBody.dynamic = YES;
if (potion.position.y < 250 && velocity.dy < 20) { //Returned to starting position
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
potion.physicsBody.velocity = CGVectorMake(0.0, 0.0);
potion.physicsBody.angularVelocity = 0.0;
potion.zRotation = 0.0;
potion.position = CGPointMake(gameWidth / 2, 150);
NSLog(#"Returning potion");
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
} else { //Flung away
[potion.physicsBody applyImpulse:velocity];
NSLog(#"Add impulse: %f, %f", velocity.dx, velocity.dy);
NSLog(#"Pos:%f,%f",potion.position.x,potion.position.y);
}
}
}
EDIT:After some more testing I've observed that if it has never been thrown (dynamic with impulse) it always shows (160, 150) as its position no matter where it is. Sometimes it shows something like 160.00031 and I don't know why. After it has been thrown it shows the correct position in the NSLog but changing the position still doesn't work
EDIT2: Just saw that it doesn't show the correct position after a throw. Just different but they seem offset. I throw objects in the +y direction (up) and when I put it down in the left corner the position.x is correct but position.y is offset by about 300. So where y should be 0 it's 300.
Related
I'm following Ray Wenderlich's 'iOS Games by Tutorials' & I got everything in my world setup & working: The entire game is in Landscape mode & there's one SKNode called _worldNode & everything, except _uiNode (in-game UI), is added to it. Player character walks to a touched location & _worldNode moves under him like a treadmill. However, like all functionality (or as they call it: "juice") addicts I wanted to add zoom in/out functionality through UIPinchGestureRecognizer by scaling _worldNode, which I did. But now every time I zoom in, the "camera" moves to the bottom left. Zooming out moves the view to the top right of the screen. It's a mess. I need the view to stay centered on the player character & I've tried everything I could come up with & find online. The closest thing I came to was using the technique from SKNode scale from the touched point but I still get the bloody bottom left/top right mess. I realized this mess happens only when I update the camera/view (it's really _worldNode.position). Therefore, 'didSimulatePhysics' or 'didFinishUpdate' methods don't help. In fact, even a one time button that slightly moves/updates the camera view (_worldNode.position) still gives me the bottom left/top right problem. Here is my code. I hope someone can take a look & tell me what to modify to get things working.
#interface GameScene () <SKPhysicsContactDelegate, UIGestureRecognizerDelegate>
{
UIPinchGestureRecognizer *pinchGestureRecognizer;
}
//Properties of my GameScene.
#property SKNode *worldNode;
#property etc. etc.
//Called by -(id)initWithSize:(CGSize)size method & creates the in-game world.
-(void)createWorld
{
[_worldNode addChild:_backgroundLayer];
[self addChild:_worldNode];
self.anchorPoint = CGPointMake(0.5, 0.5); //RW tutorial did it this way.
_worldNode.position = CGPointMake(-_backgroundLayer.layerSize.width/2, -_backgroundLayer.layerSize.height/2); //Center.
//Then I add every node to _worldNode from this point on.
}
//Neccessary for gesture recognizers.
-(void)didMoveToView:(SKView *)view
{
pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:#selector(handleZoomFrom:)];
[view addGestureRecognizer:pinchGestureRecognizer];
}
//"Camera" follows player character. 'didSimulatePhysics' doesn't help either.
-(void)didFinishUpdate
{
//IF '_pinchingScreen' == YES then screen pinching is in progress. Thus, camera position update will seize for the duration of pinching.
if (!_pinchingScreen)
{
_worldNode.position = [self pointToCenterViewOn:_player.position];
}
}
//Method that is called by my UIPinchGestureRecognizer. Answer from: https://stackoverflow.com/questions/21900614/sknode-scale-from-the-touched-point?lq=1
-(void)handleZoomFrom:(UIPinchGestureRecognizer*)recognizer
{
CGPoint anchorPoint = [recognizer locationInView:recognizer.view];
anchorPoint = [self convertPointFromView:anchorPoint];
if (recognizer.state == UIGestureRecognizerStateBegan)
{
// No code needed for zooming...
_player.movementMode = 2; //Stop character from moving from touches.
_pinchingScreen = YES; //Notifies 'didFinishUpdate' method that pinching began & camera position update should stop for now.
}
else if (recognizer.state == UIGestureRecognizerStateChanged)
{
//Technique from the above Stack Overflow link - Commented out.
// CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
//
// [_worldNode setScale:(_worldNode.xScale * recognizer.scale)];
//
// CGPoint mySkNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
// CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, mySkNodeAnchorPointInScene);
//
// _worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//
// recognizer.scale = 1.0;
//Modified scale: 2.0
if(recognizer.scale > _previousWorldScale)
{
_previousWorldScale = recognizer.scale;
CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
[_worldNode setScale:2.0];
CGPoint worldNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, worldNodeAnchorPointInScene);
_worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//[_worldNode runAction:[SKAction scaleTo:2.0 duration:0]]; //This works too.
}
//Original scale: 1.0
if(recognizer.scale < _previousWorldScale)
{
_previousWorldScale = recognizer.scale;
CGPoint anchorPointInMySkNode = [_worldNode convertPoint:anchorPoint fromNode:self];
[_worldNode setScale:1.0];
CGPoint worldNodeAnchorPointInScene = [self convertPoint:anchorPointInMySkNode fromNode:_worldNode];
CGPoint translationOfAnchorInScene = CGPointSubtract(anchorPoint, worldNodeAnchorPointInScene);
_worldNode.position = CGPointAdd(_worldNode.position, translationOfAnchorInScene);
//[_worldNode runAction:[SKAction scaleTo:1.0 duration:0]]; //This works too.
}
}
else if (recognizer.state == UIGestureRecognizerStateEnded)
{
// No code needed here for zooming...
_pinchingScreen = NO; //Notifies 'didFinishUpdate' method that pinching has stopped & camera position update should resume.
_player.movementMode = 0; //Resume character movement.
}
}
So could anyone please tell me, by looking at the above code, why the camera/view shifts to the bottom left upon zooming in? I've sat several days on this problem & I still can't figure it out.
Thanks to JKallio, who wrote detailed code in his answer to Zoom and Scroll SKNode in SpriteKit, I've been able to find a piece of code that solves the problem. There's a method called 'centerOnNode' that is small, elegant & solves my problem perfectly. Here it is for anyone that just needs that:
-(void) centerOnNode:(SKNode*)node
{
CGPoint posInScene = [node.scene convertPoint:node.position fromNode:node.parent];
node.parent.position = CGPointMake(node.parent.position.x - posInScene.x, node.parent.position.y - posInScene.y);
}
Then you call that method inside your 'didSimulatePhysics' or inside 'didFinishUpdate' like so:
//"Camera" follows player character.
-(void)didFinishUpdate
{
//IF '_pinchingScreen' == YES then screen pinching is in progress. Thus, camera position update will seize for the duration of pinching.
if (!_pinchingScreen)
{
if (_previousWorldScale > 1.0) //If _worldNode scale is greater than 1.0
{
[self centerOnNode:_player]; //THIS IS THE METHOD THAT SOLVES THE PROBLEM!
}
else if (_previousWorldScale == 1.0) //Standard _worldNode scale: 1.0
{
_worldNode.position = [self pointToCenterViewOn:_player.position];
}
}
}
P.S. Just remember the question wasn't HOW to zoom in. But how to fix the issue with the camera once the world is ALREADY zoomed in.
I's using this method:
- (void)setTranslation:(CGPoint)translation inView:(UIView *)view
From UIPanGestureRecognizer class, and I don't understand the velocity discussions around that.
Appel's documentation says:
Sets the translation value in the coordinate system of the specified
view. Changing the translation value resets the velocity of the pan.
What's that exactly mean? What is "reset velocity of the pan"?
Any help will be appreciated.. Thanks in advance!
--EDIT
See this code:
if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.cardsScrollView];
recognizer.view.center = CGPointMake(recognizer.view.center.x, recognizer.view.center.y + translation.y);
[recognizer setTranslation:CGPointMake(0, 0) inView:recognizer.view];
...
}
What it means is that if you're in the middle of the panning gesture (you're dragging something) and you call setTranslation:inView: on that gesture, it's velocity is going to be reset to 0;
A pan gesture not only gives you the translation but also the velocity of the gesture (how fast you're dragging) in units per second (points per second in this case). You can access the velocity by calling velocityInView:.
If you don't use the velocity then don't worry about it, otherwise keep the above in mind.
UPDATE:
I'm guessing you're trying to move the view as you drag it. I would do it slightly differently.
if (recognizer.state == UIGestureRecognizerStateBegan) {
CGAffineTransform transform = recognizer.view.transform;
[recognizer setTranslation:CGPointMake(transform.tx, transform.ty) inView:self];
} else if (recognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [recognizer translationInView:self.cardsScrollView];
recognizer.view.transform = CGAffineTransformMakeTranslation(translation.x, translation.y);
}
The thing to keep in mind is that self.cardsScrollView should be the superview of the view you're trying to move.
I've been working on this problem for a few days now and I just can't seem to figure it out. I've done a ton of searching for an answer, and I've seen hints that maybe the problem is with Sprite-Kit itself, so I am debating moving to Cocos2D and starting over. But, I hope that someone here can help me.
I have a basic camera node called _world that I am using to pan around the world and to zoom. The panning and zooming works fine, but I've been trying to get the world node to move to the center of where the pinch occurs. It sort of works, but converting the point to a position in the world node seems to be the problem. Here is my code:
I use this code to convert a scene point to a world point elsewhere in the code and it works fine:
CGPoint positionInScene = [touch locationInNode:self];
CGPoint locationInWorld = [self.scene convertPoint:positionInScene toNode:_world];
But later I try to do this using the middle point of the two points found in the pinch, and it doesn't seem to convert properly:
originalLocationOne = [sender locationOfTouch:0 inView:self.view];
originalLocationTwo = [sender locationOfTouch:1 inView:self.view];
//I do this because SpriteKit doesn't have a ccpAdd method
originalAddedMidPoint = CGPointMake(originalLocationOne.x + originalLocationTwo.x, originalLocationOne.y + originalLocationTwo.y);
//same thing here but for ccpMidPoint
originalMidPoint = CGPointMake(originalAddedMidPoint.x * 0.5f, originalAddedMidPoint.y * 0.5f);
_newWorldPos = [self convertPoint:originalMidPoint toNode:_world];
I would really appreciate it if someone can point me in the right direction! Thank you so much!
EDIT: I've been working on this problem some more, and certainly something weird is happening but I still can't tell what.
Here is my complete touchesbegan method:
-(void) touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
_myTouches = [[NSMutableArray alloc]init];
for (UITouch *touch in touches) {
[_myTouches addObject:touch];
}
if (_myTouches.count > 1) {
UITouch *touch1 = [_myTouches objectAtIndex:0];
UITouch *touch2 = [_myTouches objectAtIndex:1];
CGPoint touch1Location = [touch1 locationInView:self.view];
CGPoint touch2Location = [touch2 locationInView:self.view];
NSLog(#"tpoint1 = %#, tpoint2 = %#", NSStringFromCGPoint(touch1Location), NSStringFromCGPoint(touch2Location));
CGPoint touchAddedPoint = CGPointMake(touch1Location.x + touch2Location.x, touch1Location.y + touch2Location.y);
CGPoint touchMidPoint = CGPointMake(touchAddedPoint.x * 0.5f, touchAddedPoint.y * 0.5f);
NSLog(#"touch mid point = %#", NSStringFromCGPoint(touchMidPoint));
CGPoint newWorldPoint = [self convertTouchPointToWorld:touchMidPoint];
//camera needs to be offset to work properly
CGPoint alteredWorldPoint = CGPointMake(-newWorldPoint.x * 0.75f, -newWorldPoint.y * 0.75f);
_firstTouch = alteredWorldPoint;
_tempWorldLocation = _firstTouch;
_worldMovedForUpdate = YES;
}
}
Here is the method that I extracted to convert the position to the world node:
-(CGPoint) convertTouchPointToWorld:(CGPoint)touchLocation {
CGPoint firstLocationInWorld = [self.scene convertPoint:touchLocation toNode:_world];
NSLog(#"inside converting method %#", NSStringFromCGPoint(firstLocationInWorld));
return firstLocationInWorld;
}
Here is the entire method that places a building in the game map based on its position in the world map:
-(void)selectNodeForTouch:(CGPoint)touchLocation {
SKSpriteNode *touchedNode = (SKSpriteNode *)[self nodeAtPoint:touchLocation];
//NSLog(#"node name is = %#", touchedNode.name);
_selectedNode = touchedNode;
if ([[touchedNode name] isEqualToString:#"hudswitch1"]) {
if([self.theGame getMovesLeft] == 0){
_hudNeedsUpdate = YES;
_turnNeedsUpdating = YES;
}
}
if ([self.theGame getMovesLeft] > 0) {
[self handleButtonsForTouch:touchedNode];
}
//this inserts the tile at the location in world node
NSLog(#"touchLocation.x = %f, touchlocation.y = %f", touchLocation.x, touchLocation.y);
if ([[touchedNode name] isEqualToString:#"tile"] && _selectedBuildingType != 0) {
//CGPoint locationInWorld = [self.scene convertPoint:touchLocation toNode:_world];
CGPoint locationInWorld = [self convertTouchPointToWorld:touchLocation];
CGPoint gameLocation = [self convertWorldPointToGamePoint:locationInWorld];
NSLog(#"locationWorld.x = %f, locationWorld.y = %f", locationInWorld.x, locationInWorld.y);
if(![self.theGame isBuildingThere:gameLocation] && [self.theGame isValidLocation:gameLocation]) {
[self updateActiveTilePos:locationInWorld];
}
}
}
Inserting a tile into the map works fine. It gets the world location and then divides it by the tile size to figure out the position to place the tile in the world map. It is always inserting a tile in the proper place.
However when I use the middle point of the two touches in a pinch and convert it to a world point, it returns a value but the value is off by a large amount, and different amounts depending on the distances from the center...
Maybe I just need to figure out how to offset the camera properly? Thanks again for any help with this problem, it is driving me crazy!
I had the same problem.
It is counterintuitive but point conversion is not smart.
It does not convert from one node to another node.
It can convert to scene coordinates and to node coordinates from scene coordinates.
It does not work from one node to another node.
What you need to do is convert point from node and then convert point to node. After two these calls coordinates will be in the node you need.
I am trying to check whether my SKSpriteNode will remain in bounds of the screen during a drag gesture. I've gotten to the point where I am pretty sure my logic toward approaching the problem is right, but my implementation is wrong. Basically, before the player moves from the translation, the program checks to see whether its in bounds. Here is my code:
-(CGPoint)checkBounds:(CGPoint)newLocation{
CGSize screenSize = self.size;
CGPoint returnValue = newLocation;
if (newLocation.x <= self.player.position.x){
returnValue.x = MIN(returnValue.x,0);
} else {
returnValue.x = MAX(returnValue.x, screenSize.width);
}
if (newLocation.y <= self.player.position.x){
returnValue.y = MIN(-returnValue.y, 0);
} else {
returnValue.y = MAX(returnValue.y, screenSize.height);
}
NSLog(#"%#", NSStringFromCGPoint(returnValue));
return returnValue;
}
-(void)dragPlayer: (UIPanGestureRecognizer *)gesture {
CGPoint translation = [gesture translationInView:self.view];
CGPoint newLocation = CGPointMake(self.player.position.x + translation.x, self.player.position.y - translation.y);
self.player.position = [self checkBounds:newLocation];
}
For some reason, my player is going off screen. I think my use of the MIN & MAX macros may be wrong, but I am not sure.
Exactly, you mixed up MIN/MAX. The line MIN(x, 0) will return the lower value of x or 0, meaning the result will be 0 or less.
At one line you're using -returnValue.y which makes no sense.
You can (and should for readability) omit the if/else because MIN/MAX, if used correctly, make if/else unnecessary here.
I have a pan gesture recogniser acting as a slider, code as follows:
- (void)minutePan:(UIPanGestureRecognizer *)gesture
{
if ((gesture.state == UIGestureRecognizerStateChanged) ||
(gesture.state == UIGestureRecognizerStateEnded)){
CGPoint translation = [gesture translationInView:gesture.view.superview];
float leftBound = gesture.view.bounds.size.width / 2.0f;
float rightBound = gesture.view.bounds.size.width + (leftBound - 60);
float position;
if(gesture.view.center.x < leftBound){
position = leftBound;
}else if(gesture.view.center.x > rightBound){
position = rightBound;
}else{
position = gesture.view.center.x + translation.x;
}
gesture.view.center = CGPointMake(position, gesture.view.center.y);
[gesture setTranslation:CGPointZero inView:gesture.view.superview];
}
}
I have a UIView with the gesture recogniser attached inside another UIView acting as the boundary for it. Its working fine except when I get to the left and right edges. The position variable should not get set lower than half the panned views width but it seems to be updating the view on the screen before the code runs. This causes the view to go outside the bounds and flicker back as long as the user is dragging. Any help would be greatly appreciated thanks.