Position on Node After Rotation - objective-c

Okay so I have a node that's node.size is a (5,5) rectangle. Its position is (100,100), and its anchor point is the default (0.5, 0.5).
I tried to find the point at the farthest right, but still centre Y. Naturally this returned the point (102.5, 100). This is perfect. I found the point by simply using CGPointMake(node.position + node.size.width/2, node.position);
This works great, but the thing is I want to rotate the node while still knowing what that point is. I was wondering if their was some kind of math I could perform using the nodes zRotation in order to find the new point (the same point on the nodes frame) after the rotation.
To make it clear I want to know where the point that is originally 102.5, 100, is after the node rotates.
Thank you all very much in advance for all of your answers

Let r be the distance from the anchor point of the sprite to the point of interest. In your case,
r = sqrt(dx*dx+dy*dy) = 2.5;
where dx = 2.5 and dy = 0.
Also, let theta be the rotation angle, which is initially zero. We now have a point in polar coordinates, (r, theta), that represents the point (2.5, 0). We can now rotate the point about the sprite's anchor point by changing theta. After rotating the sprite, we convert (r, theta) to cartesian coordinates by
x = r * cos(theta);
y = r * sin(theta);
and add the sprite's position to convert the point to scene coordinates.
x = x + sprite.position.x;
y = y + sprite.position.y;
EDIT: I think this is what you are trying to implement.
#interface MyScene()
#property SKSpriteNode *object1;
#property SKSpriteNode *object2;
#end
#implementation MyScene
-(id)initWithSize:(CGSize)size
{
if(self = [super initWithSize:size]) {
_object1 = [SKSpriteNode spriteNodeWithColor:[SKColor blueColor] size:CGSizeMake(10,5)];
_object1.position = CGPointMake(100, 100);
[self addChild:_object1];
SKAction *wait = [SKAction waitForDuration: 2.5f];
SKAction *rotate = [SKAction rotateByAngle:3*M_PI/4 duration:2.0f];
SKAction *addObject = [SKAction runBlock:^{
_object2 = [SKSpriteNode spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(10,5)];
_object2.zRotation = _object1.zRotation;
CGFloat dx = _object1.size.width / 2;
CGFloat dy = 0;
CGFloat r = sqrtf(dx*dx + dy*dy);
CGFloat x = r*cosf(_object1.zRotation);
CGFloat y = r*sinf(_object1.zRotation);
_object2.position = CGPointMake(_object1.position.x + 2*x, _object1.position.y + 2*y);
[self addChild:_object2];
}];
SKAction *seq = [SKAction sequence:#[wait, rotate, addObject]];
[_object1 runAction:seq];
}
return self;
}

Okay, I had to post code so I had to make this an answer.
So I tried your method 0x141E, and it worked perfectly. The only problem I am having is that whenever the angle is not a multiple of 0.5PI it seems to calculate improperly. Maybe its just the way that I coded it though. The goal of my code was to place one object right beside another by measuring the distance between a left most, middle point of one object with the other objects right most, middle point. This way they would be touching left side with right side... Side-by-Side basically. I placed one object first and added the second using your given algorithm, but the result as I stated above got a little funky in between angles divisible by 0.5 PI. Here's what I've done, it should work if you import the code straight into XCode if you want to see what I am talking about (I'm sorry but I don't have enough rep to attach images or I would give a screenshot):
//This is the MyScene implementation file
#interface MyScene()
{
SKSpriteNode *_object1;
SKSpriteNode *_object2;
}
#end
#implementation MyScene
-(id)initWithSize:(CGSize)size
{
if(self = [super initWithSize:size]
{
object1 = [SKSpriteNode spriteNodeWithColor:[SKColor blueColor] size:CGSizeMake(10,5)];
[self addChild:object1];
//The wait is just to make it feel smoother and not so sudden when starting up the simulator
SKAction *wait = [SKAction waitForDuration: 2.5f];
SKAction *rotate = [SKAction rotateByAngle:0.25*M_PI duration:2.0f];
SKAction *addObject = [SKAction runBlock:^{
_object2 = [spriteNodeWithColor:[SKColor redColor] size:CGSizeMake(10,5)];
//I set the rotation of the second object to the rotation of the first so they will always be touching side-by-side
_object2.zRotation = _object1.zRotation;
//I used the full width and heigth rather than half to save time. The two ojects are of equal width and height so instead of finding both distances and adding them together I just doubled the distance
CGFloat r = sqrtf(_object2.frame.size.width*_object2.frame.size.width+_object2.frame.size.height*_object2.frame.size.height);
CGFloat x = r*cosf(_object2.zRotation);
CGFloat y = r*sinf(_object2.zRotation);
object2.position = CGPointMake(_object2.position.x + x, _object2.position + y);
[self addChild:_object2];
}
SKAction *seq = [SKAction sequence:#[wait, rotate, addObject]];
[self runAction:seq];
}
}
#end
Thank you again for all of your help thus far, and thank you even more for all of your potential future help.

Related

SKNodes with SKPhysicsBody under SKAction move inaccuracy

I have two SKNode with physics (one under another):
- (SKNode*) newNode {
SKShapeNode *node = [SKShapeNode shapeNodeWithCircleOfRadius:60];
node.strokeColor = [SKColor clearColor];
node.fillColor = [SKColor greenColor];
node.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:60];
node.physicsBody.categoryBitMask = 1;
node.physicsBody.collisionBitMask = 1;
node.physicsBody.contactTestBitMask = 1;
return node;
}
- (void)createSceneContents {
SKNode *n1 = [self newNode];
n1.position = CGPointMake(100,100);
[self addChild:n1];
SKNode *n2 = [self newNode];
n2.position = CGPointMake(100,300);
[self addChild:n2];
...
}
Their goal is to reach a predetermined position directly opposite them at some distance.
To achieve this, I use SKAction (this is important and can not be changed):
[n1 runAction:[SKAction moveTo:CGPointMake(800,100) duration:3]];
[n2 runAction:[SKAction moveTo:CGPointMake(800,300) duration:3]];
In addition, on the first node's movement path I place a third node - exactly the same.
- (void)createSceneContents {
...
SKNode *obstacle = [self newNode];
obstacle = CGPointMake(500,300);
[self addChild:obstacle];
}
The third node must be shifted in the direction of first node's movement by the mechanism of physics.
Click to illustration of the Scene
(the red circles marks destination position of nodes)
Then I run the app, everything goes as is conceived, except for one thing - a first node (with obstacle on the path) was not reach the target location, just a few pixels:
Click to see result
The question is simple: why? And how can I fix it?
Thank you!

iOS Sprite Kit how to make a projectile point at target in landscape orientation?

I have an icon within my sprite kit game that I intend to use as an animated projectile for when one character shoots another one. I'm trying to orient this projectile to point at the target.
I'm rotating it by a base angle of pi/4.0 to point it straight to the right. then I want the arrow to rotate from this position and point at target.
The code below almost works, but to my eye, it always looks as if the projectile is off from the correct angle. If the angle is correct, the arrow will point in the direction of movement when the arrow is given an action to move to the target x,y coordinate.
How can I correctly calculate the angle to fire projectile from one (x,y) point to another (x,y) point?
EDIT: The code below works now, just needed to do zeroCheckedY/zeroCheckedX.
//correct for pointing in the bottom right corner
float baseRotation = M_PI/4.0;
float zeroCheckedY = destination.y - origin.y;
float zeroCheckedX = destination.x - origin.x;
SKAction* rotate = nil;
if(zeroCheckedX != 0)
{
//not sure if I'm calculating this angle correctly.
float angle = atanf(zeroCheckedY/zeroCheckedX);
rotate = [SKAction rotateByAngle:baseRotation + angle duration:0];
}else
{
rotate = [SKAction rotateByAngle:baseRotation duration:0];
}
I had a similar problem: cannon must "track" a target.
In CannonNode I defined the following method:
- (void)catchTargetAtPoint:(CGPoint)target {
CGPoint cannonPointOnScene = [self.scene convertPoint:self.position fromNode:self.parent];
float angle = [CannonNode pointPairToBearingDegreesWithStartingPoint:cannonPointInScene endingPoint:target];
if (self.zRotation < 0) {
self.zRotation = self.zRotation + M_PI * 2;
}
if ((angle < self.maxAngle) && (angle> self.minAngle)) {
[self runAction:[SKAction rotateToAngle:angle duration:1.0f]];
}
}
+ (float)pointPairToBearingDegreesWithStartingPoint:(CGPoint)startingPoint endingPoint:(CGPoint) endingPoint {
CGPoint originPoint = CGPointMake(endingPoint.x - startingPoint.x, endingPoint.y - startingPoint.y); // get origin point to origin by subtracting end from start
float bearingRadians = atan2f(originPoint.y, originPoint.x); // get bearing in radians
return bearingRadians;
}
catchTargetAtPoint must be called within update method of the scene.

How to rotate an SKSpriteNode around the node's Y-axis?

I'm trying to rotate an SKSpriteNode node around it's Y-axis. I know there's the zRotation property and that will rotate the node clockwise or counter-clockwise; however, I'd like to rotate the node around it's own Y axis (like a dancing ballerina for instance), and I can't seem to find any functionality to do so. What is the best recommended way of doing this?
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
sprite.position = location;
SKAction* action0 = [SKAction scaleXTo:1.0 duration:0.5];
SKAction* action1 = [SKAction scaleXTo:0.1 duration:0.5];
SKAction* action2 = [SKAction scaleXTo:-0.1 duration:0.5];
SKAction* action3 = [SKAction scaleXTo:-1.0 duration:0.5];
SKAction* action = [SKAction sequence:#[action0, action1, action2, action3]];
[sprite runAction:[SKAction repeatActionForever:action]];
[self addChild:sprite];
This will get you what looks like a 3D card flip, but obviously if you're expecting an object to have depth you wont get that programatically with scaling.
Or well less animations (As LearnCocos2D suggested):
SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:#"Spaceship"];
sprite.position = location;
SKAction* action0 = [SKAction scaleXTo:1.0 duration:0.5];
SKAction* action1 = [SKAction scaleXTo:-1.0 duration:0.5];
SKAction* action = [SKAction sequence:#[action0, action1]];
[sprite runAction:[SKAction repeatActionForever:action]];
[self addChild:sprite];
Typically if I am wanting to rotate something like you seem to be describing, and want it to look good, I will do it via sprite frames depicting different degrees of rotation.
You can indeed fake it a little by tweening your xScale property from 1 to -1 , and visa versa.
The xScale trick is feasible for cards maybe (or paper mario), but for a ballerina, not so much. You are looking for 3D rotation of a 2D object, and that's not going to happen via that method.
So if the xScale trick is not feasible for your needs, you'll need to handle it via sprite frames.
Here's an example of a 3d SpriteSheet that would allow you to achieve the rotation you desire :
This animated gif is an example of sprite frames to get that y rotation :
SpriteNode *spriteNode = [SKSpriteNode spriteNodeWithImageNamed#"Sprite"];
spriteNode.position = position;
SKAction *action = [SKAction repeatActionForever:[SKAction customActionWithDuration:duration actionBlock:^(SKNode *node, CGFloat elapsedTime) {
node.xScale = sin(2 * M_PI * elapsedTime / duration);
}]];
[self addChild:spriteNode];
However, the sprite looks like having no thickness.
Although SpriteKit doesn't have full 2.5D capabilities (rotating and transforming along X, Y and Z axes), I've developed a work around for rotating SKSpriteNodes around either the X or Y axes. Because SKView is a subclass of UIView, we can rotate it in 3D space the same way we rotate a UIView, using CATransform3D.
The main point is that the sprite that needs to be rotated on the Y axis lives in its own scene, which is presented in its own SKView.
Triggering the following code on in a loop will animate your SKView (containing the SKScene and the SKSpriteNode) on the y axis. You can still have your main scene running at the same time in the background and swap in the sprites that need to be animated on the y axis and then swap them out when animation complete, and remove Y rotation scene. It's not a pretty solution, but it's all there is until Apple adds better 2.5D capabilities.
I've written a tutorial on SpriteKit and 2.5D with sample code if you need more info. http://www.sdkboy.com/?p=283
self.rotAngle -= 10;
float angle = (M_PI / 180.0f) * self.rotAngle/10;
CATransform3D transform3DRotation = CATransform3DMakeRotation(1.0, angle, 0.0, 0.0);
self.mySKView.layer.transform = transform3DRotation;

Zoom Layer centered on a Sprite

I am in process of developing a small game where a space-ship travels through a layer (doh!), in some situations the spaceship comes close to an enemy, and the whole layer is zoomed in on the space-ship with the zoom level being dependent on the distance between the ship and the enemy. All of this works fine.
The main question, however, is how do I keep the zoom being centered on the space-ship?
Currently I control the zooming in the GameLayer object through the update method, here is the code:
-(void) prepareLayerZoomBetweenSpaceship{
CGPoint mainSpaceShipPosition = [mainSpaceShip position];
CGPoint enemySpaceShipPosition = [enemySpaceShip position];
float distance = powf(mainSpaceShipPosition.x - enemySpaceShipPosition.x, 2) + powf(mainSpaceShipPosition.y - enemySpaceShipPosition.y,2);
distance = sqrtf(distance);
/*
Distance > 250 --> no zoom
Distance < 100 --> maximum zoom
*/
float myZoomLevel = 0.5f;
if(distance < 100){ //maximum zoom in
myZoomLevel = 1.0f;
}else if(distance > 250){
myZoomLevel = 0.5f;
}else{
myZoomLevel = 1.0f - (distance-100)*0.0033f;
}
[self zoomTo:myZoomLevel];
}
-(void) zoomTo:(float)zoom {
if(zoom > 1){
zoom = 1;
}
// Set the scale.
if(self.scale != zoom){
self.scale = zoom;
}
}
Basically my question is: How do I zoom the layer and center it exactly between the two ships? I guess this is like a pinch zoom with two fingers!
Below is some code that should get it working for you. Basically you want to:
Update your ship positions within the parentNode's coordinate system
Figure out which axis these new positions will cause the screen will be bound by.
Scale and re-position the parentNode
I added some sparse comments, but let me know if you have any more questions/issues. It might be easiest to dump this in a test project first...
ivars to put in your CCLayer:
CCNode *parentNode;
CCSprite *shipA;
CCSprite *shipB;
CGPoint destA, deltaA;
CGPoint destB, deltaB;
CGPoint halfScreenSize;
CGPoint fullScreenSize;
init stuff to put in your CCLayer:
CGSize size = [[CCDirector sharedDirector] winSize];
fullScreenSize = CGPointMake(size.width, size.height);
halfScreenSize = ccpMult(fullScreenSize, .5f);
parentNode = [CCNode node];
[self addChild:parentNode];
shipA = [CCSprite spriteWithFile:#"Icon-Small.png"]; //or whatever sprite
[parentNode addChild:shipA];
shipB = [CCSprite spriteWithFile:#"Icon-Small.png"];
[parentNode addChild:shipB];
//schedules update for every frame... might not run great.
//[self schedule:#selector(updateShips:)];
//schedules update for 25 times a second
[self schedule:#selector(updateShips:) interval:0.04f];
Zoom / Center / Ship update method:
-(void)updateShips:(ccTime)timeDelta {
//SHIP POSITION UPDATE STUFF GOES HERE
...
//1st: calc aspect ratio formed by ship positions to determine bounding axis
float shipDeltaX = fabs(shipA.position.x - shipB.position.x);
float shipDeltaY = fabs(shipA.position.y - shipB.position.y);
float newAspect = shipDeltaX / shipDeltaY;
//Then: scale based off of bounding axis
//if bound by x-axis OR deltaY is negligible
if (newAspect > (fullScreenSize.x / fullScreenSize.y) || shipDeltaY < 1.0) {
parentNode.scale = fullScreenSize.x / (shipDeltaX + shipA.contentSize.width);
}
else { //else: bound by y-axis or deltaX is negligible
parentNode.scale = fullScreenSize.y / (shipDeltaY + shipA.contentSize.height);
}
//calculate new midpoint between ships AND apply new scale to it
CGPoint scaledMidpoint = ccpMult(ccpMidpoint(shipA.position, shipB.position), parentNode.scale);
//update parent node position (move it into view of screen) to scaledMidpoint
parentNode.position = ccpSub(halfScreenSize, scaledMidpoint);
}
Also, I'm not sure how well it'll perform with a bunch of stuff going on -- but thats a separate problem!
Why don't you move the entire view, & position it so the ship is in the centre of the screen? I haven't tried it with your example, but it should be straight forward. Maybe something like this -
CGFloat x = (enemySpaceShipPosition.x - mainSpaceShipPosition.x) / 2.0 - screenCentreX;
CGFloat y = (enemySpaceShipPosition.y - mainSpaceShipPosition.y) / 2.0 - screenCentreY;
CGPoint midPointForContentOffset = CGPointMake(-x, -y);
[self setContentOffset:midPointForContentOffset];
...where you've already set up screenCentreX & Y. I haven't used UISCrollView for quite a while (been working on something in Unity so I'm forgetting all by Obj-C), & I can't remember how the contentOffset is affected by zoom level. Try it & see! (I'm assuming you're using a UIScrollView, maybe you could try that too if you're not)

Core Animation and Transformation like zoomRectToVisible

I'm trying to get an effect like the zoomRectToVisible-method of UIScrollview.
But my method should be able to center the particular rect in the layer while zooming and it should be able to re-adjust after the device orientation changed.
I'm trying to write a software like the marvel-comic app and need a view that presents each panel in a page.
For my implementation I'm using CALayer and Core Animation to get the desired effect with CATransform3D-transformations. My problem is, I'm not able to get the zoomed rect/panel centered.
the structure of my implementation looks like this: I have a subclass of UIScrollview with a UIView added as subview. The UIView contains the image/page in it's CALayer.contents and I use core animations to get the zooming and centering effect. The zoom effect on each panel works correcty but the centering is off. I'm not able to compute the correct translate-transformation for centering.
My code for the implementation of the effect is like this:
- (void) zoomToRect:(CGRect)rect animated:(BOOL)animated {
CGSize scrollViewSize = self.bounds.size;
// get the current panel boundingbox
CGRect panelboundingBox = CGPathGetBoundingBox([comicPage panelAtIndex:currentPanel]);
// compute zoomfactor depending on the longer dimension of the panelboundingBox size
CGFloat zoomFactor = (panelboundingBox.size.height > panelboundingBox.size.width) ? scrollViewSize.height/panelboundingBox.size.height : scrollViewSize.width/panelboundingBox.size.width;
CGFloat translateX = scrollViewSize.width/2 - (panelboundingBox.origin.x/2 + panelboundingBox.size.width/2);
CGFloat translateY = scrollViewSize.height/2 - (panelboundingBox.size.height/2 - panelboundingBox.origin.y);
// move anchorPoint to panelboundingBox center
CGPoint anchor = CGPointMake(1/contentViewLayer.bounds.size.width * (panelboundingBox.origin.x + panelboundingBox.size.width/2), 1/contentViewLayer.bounds.size.height * (contentViewLayer.bounds.size.height - (panelboundingBox.origin.y + panelboundingBox.size.height/2)));
// create the nessesary transformations
CATransform3D translateMatrix = CATransform3DMakeTranslation(translateX, -translateY, 1);
CATransform3D scaleMatrix = CATransform3DMakeScale(zoomFactor, zoomFactor, 1);
// create respective core animation for transformation
CABasicAnimation *zoomAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
zoomAnimation.fromValue = (id) [NSValue valueWithCATransform3D:contentViewLayer.transform];
zoomAnimation.toValue = (id) [NSValue valueWithCATransform3D:CATransform3DConcat(scaleMatrix, translateMatrix)];
zoomAnimation.removedOnCompletion = YES;
zoomAnimation.duration = duration;
// create respective core animation for anchorpoint movement
CABasicAnimation *anchorAnimatione = [CABasicAnimation animationWithKeyPath:#"anchorPoint"];
anchorAnimatione.fromValue = (id)[NSValue valueWithCGPoint:contentViewLayer.anchorPoint];
anchorAnimatione.toValue = (id) [NSValue valueWithCGPoint:anchor];
anchorAnimatione.removedOnCompletion = YES;
anchorAnimatione.duration = duration;
// put them into an animation group
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObjects:zoomAnimation, anchorAnimatione, nil] ;
/////////////
NSLog(#"scrollViewBounds (w = %f, h = %f)", self.frame.size.width, self.frame.size.height);
NSLog(#"panelBounds (x = %f, y = %f, w = %f, h = %f)", panelboundingBox.origin.x, panelboundingBox.origin.y, panelboundingBox.size.width, panelboundingBox.size.height);
NSLog(#"zoomfactor: %f", zoomFactor);
NSLog(#"translateX: %f, translateY: %f", translateX, translateY);
NSLog(#"anchorPoint (x = %f, y = %f)", anchor.x, anchor.y);
/////////////
// add animation group to layer
[contentViewLayer addAnimation:group forKey:#"zoomAnimation"];
// trigger respective animations
contentViewLayer.anchorPoint = anchor;
contentViewLayer.transform = CATransform3DConcat(scaleMatrix, translateMatrix);
}
So the view requires the following points:
it should be able to zoom and center a rect/panel of the layer/view depending on the current device orientation. (zoomRectToVisible of UIScrollview does not center the rect)
if nessesary (either device orientation changed or panel requires rotation) the zoomed panel/rect should be able to rotate
the duration of the animation is depending on user preference. (I don't know whether I can change the default animation duration of zoomRectToVisible of UIScrollView ?)
Those points are the reason why I overwrite the zoomRectToVisible-method of UIScrollView.
So I have to know how I can correctly compute the translation parameters for the transformation.
I hope someone can guide me to get the correct parameters.
Just skimmed over your code and this line is probably not being calculated as you think:
CGPoint anchor = CGPointMake(1/contentViewLayer.bounds.size.width * (panelboundingBox.origin.x + panelboundingBox.size.width/2), 1/contentViewLayer.bounds.size.height * (contentViewLayer.bounds.size.height - (panelboundingBox.origin.y + panelboundingBox.size.height/2)));
You're likely to get 0 because of the 1/ at the start. C will do your multiplication before this division, resulting in values <1 - probably not what you're after. See this
You might find it more useful to breakdown your calculation so you know it's working in the right order (just use some temporary variables) - believe me it will help enormously in making your code easier to read (and debug) later. Or you could just use more brackets...
Hope this helps.