cocos2d - How to orient CCSprite in 3d? - objective-c

I have a circular disc sprite that always rotates around its center, spinning like a wheel. I want to re-orient it in 3d space and keep its rotation. I've tried messing with the grid property, skewX/Y, the camera, everything I can find, but I can't figure out how to produce this effect with rotation.

A couple routes:
Figure out the maths to get the correct skew value for the rotation value. (I don't know off of the top of my head, sorry, although you can probably reverse engineer it from option-2)
You could sub-class CCSprite, override CCNode's - (CGAffineTransform)nodeToParentTransform method and have to apply the skew first, THEN rotation, in order to get your desired effect. This spares you figuring out the proper skew calculation at the expense of a fairly kludgy sprite subclass.
From CCNode.h you can learn its current order of operations:
/* Order in transformations with grid disabled
-# The node will be translated (position)
-# The node will be rotated (rotation)
-# The node will be skewed (skewX, skewY)
-# The node will be scaled (scale, scaleX, scaleY)
-# The node will be moved according to the camera values (camera)
... etc. */
Here's a hacked together version of that method with skew first: (lacks optimizations found in original method in CCNode.m)
- (CGAffineTransform)nodeToParentTransform
{
if ( isTransformDirty_ ) {
// Translate values
float x = position_.x;
float y = position_.y;
if ( ignoreAnchorPointForPosition_ ) {
x += anchorPointInPoints_.x;
y += anchorPointInPoints_.y;
}
transform_ = CGAffineTransformMake(1.0f, tanf(CC_DEGREES_TO_RADIANS(skewY_)),
tanf(CC_DEGREES_TO_RADIANS(skewX_)), 1.0f,
x, y );
// Rotation values
float c = 1, s = 0;
if( rotation_ ) {
float radians = -CC_DEGREES_TO_RADIANS(rotation_);
c = cosf(radians);
s = sinf(radians);
}
CGAffineTransform rotMatrix = CGAffineTransformMake( c * scaleX_, s * scaleX_,
-s * scaleY_, c * scaleY_,
0.0f, 0.0f );
transform_ = CGAffineTransformConcat(rotMatrix, transform_);
// adjust anchor point
if( ! CGPointEqualToPoint(anchorPointInPoints_, CGPointZero) )
transform_ = CGAffineTransformTranslate(transform_, -anchorPointInPoints_.x, -anchorPointInPoints_.y);
isTransformDirty_ = NO;
}
return transform_;
}

HachiEthan's answer is good, and allows you to set the perspective with skewX/skewY while maintaining rotation. However, I think that it is easier to work with the camera for setting perspective. In that case, you can achieve the effect by wrapping your rotating sprite in a parent node.
For example:
CCNode *node3D;
CCSprite *sprite;
node3D = [CCNode node];
sprite = [CCSprite spriteWithFile:#"HugeDisc_Red.png"];
//[[node3D camera] setCenterX:15 centerY:45 centerZ:40]; // You can use the camera
[node3D setSkewX:25]; // Skew also works if you prefer it
[node3D setRotation:145]; // Skew + rotation looks pretty convincing
[node3D setPosition:ccp(middleX+105, middleY-110)];
[node3D setIgnoreAnchorPointForPosition:NO];
[node3D setAnchorPoint:ccp(0.5f,0.5f)];
[sprite setPosition:ccp(node3D.contentSize.width/2, node3D.contentSize.height/2)];
[sprite runAction:rotate];
[node3D addChild:sprite];
Ultimately, it comes down to a matter of preference. Do you want to make a custom sprite object for this? Do you prefer camera, or skew? Depending on your needs, one of these should fit your needs.

Related

Issue with Repeated Vertical Scrolling Image

I am trying to create a repeating background from top to bottom. I have got the background to actually scroll however there's a black area that appears with each repetition. I'm not entirely sure why this is happening... Below is my code:
- (void)onEnter
{
[super onEnter];
[self initBackground];
}
-(void)initBackground
{
NSString *backgroundImage = #"Stars.png";//[self getThemeBG];
background1 = [CCSprite spriteWithImageNamed:backgroundImage];
background1.position = ccp(self.contentSize.width*0.5f,self.contentSize.height*0.5f);
[self addChild:background1 z:-123];
background2 = [CCSprite spriteWithImageNamed:backgroundImage];
background2.position = ccp(self.contentSize.width*0.5f,self.contentSize.height*0.5f+background1.contentSize.height);
background2.flipY = true;
[self addChild:background2 z:-456];
}
-(void)scrollBackground:(CCTime)dt
{
CGSize s = [[CCDirector sharedDirector] viewSize];
CGPoint pos1 = background1.position;
CGPoint pos2 = background2.position;
pos1.y -= kScrollSpeed;
pos2.y -= kScrollSpeed;
if(pos1.y <=-(s.height*0.5f) )
{
pos1.y = pos2.y + background2.contentSize.height;
}
if(pos2.y <=-(s.height*0.5f) )
{
pos2.y = pos1.y + background1.contentSize.height;
}
background1.position = pos1;
background2.position = pos2;
}
-(void)update:(CCTime)delta
{
[self scrollBackground:delta];
}
I have defined kScrollSpeed as 3 and both backgroundImage1 and 2 are CCSprites. I'm not entirely sure why a black area is appearing with each cycle.
The dimensions of the background image is 640 x 1136px.
The trick to solve the "black lines" issue with tiling graphics, specifically for cocos2d but also applicable to Sprite Kit and other render engines is to ensure that content is drawn on exact pixel boundaries.
The only way to ensure that is to cast positions to integers, or on Retina round to the next nearest "half point" (ie on Retina 123.5 is a valid exact pixel coordinate, but not on non-Retina devices).
Where exactly to perform the rounding depends on your code, but in general instead of doing this:
background1.position = pos1;
You need to do:
pos1.x = round(pos1.x * 2.0) / 2.0;
pos1.y = round(pos1.y * 2.0) / 2.0;
background1.position = pos1;
Casting to int is often recommended but it means that on Retina devices your content will only move on a 2-pixel boundary. The above solution doesn't use int casting but relies on rounding to allow .5 coordinates. It works equally well on both standard and Retina resolution devices.
PS: I strongly advice to create a helper function or overriding the setPosition: method for the above rounding code. You don't want to spread these calculations all over your code.
More points to consider:
only round once, at the very end of your position calculations, before applying the new position to the node's position property
you may need to apply the same round procedure to parent's position, for example if you change position of both tile sprites and their parent (ie a layer or container node)

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.

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)

Cocos2d CCLayer.scale

I am in process of making a small game, where the main Gameplay Layer is being zoomed in and out based on a number of parameters. This is done by setting the .scale attribute to a fraction of 1. Works well.
I have a problem, however, that when calling [[CCDirector sharedDirector] winSize]; from any of the child nodes, I get a scaled window size, which kinda sucks :) Is there any way around this apart from multiplying all my off-screen checks by the scale fraction plus one?
Here is an example
I am creating a CCLayer and placing Sprites on it. All the sprites have a static variable with the screen size during initialization, basically
static CGRect screenRect; //stored as a static variable for performance reasons
...
-(id)init{
...
// make sure to initialize the screen rect only once
if (CGRectIsEmpty(screenRect)){
CGSize screenSize = [[CCDirector sharedDirector] winSize];
screenRect = CGRectMake(0, 0, screenSize.width, screenSize.height);
CCLOG(#"Screen Size during init: %f, %f", screenSize.width, screenSize.height);
}
}
I then have a check when the sprite is moving around whether it is off the screen, and if so, I take it automatically to the opposite side:
if(newPosition.x > screenRect.size.width){
newPosition.x = 0;
}else if(newPosition.x < 0){
newPosition.x = screenRect.size.width;
}
if(newPosition.y > screenRect.size.height){
newPosition.y = 0;
}else if(newPosition.y < 0){
newPosition.y = screenRect.size.height;
}
All works very well and as expected. However, if I change the scale property of the parent CCLayer then offscreen positioning "scales" together with the layer, which is kinda weird. So if I set the CCLayer.scale = 0.5f; then it appears as though the off-screen boundaries have moved "inside" the phone screen as well. I want to avoid this. How?
After talking to quite a few developers, the off-bounds check need to take scaling into account, i.e.
newPosition.x > screenRect.size.width/CCLayer.scale

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.