Cocos2d CCLayer.scale - objective-c

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

Related

How can I optimize drawing Core Graphics grid for low memory?

In my app, I draw a grid in a custom view controller. This gets embedded in a scroll view, so the grid gets redrawn from time to time after the user zooms in or out (not during zooming).
The problem is, I'm trying to optimize this method for low-memory devices like the iPad Mini, and it's still crashing. If I take this drawing away entirely, the app works fine, and it doesn't give me a reason for the crash; it just tells me the connection was lost. I can see in my instruments that memory is spiking just before it crashes, so I'm pretty certain this is the issue.
The view is 2.5 times the screen size horizontally and vertically, set programmatically when it's created. It has multiple CALayers and this grid drawing happens on several of them. The crash happens either immediately when I segue to this view, or soon after when I do some zooming.
Here are my drawing methods (somewhat simplified for readability and because they're pretty redundant):
#define MARKER_FADE_NONE 0
#define MARKER_FADE_OUT 1 // fades out at widest zoom
#define MARKER_FADE_IN 2 // fades in at widest zoom
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
if (layer == gridSubdividers) [self drawGridInContext:ctx spacing:1 lineWidth:1 markers:NO fade:MARKER_FADE_NONE];
if (layer == gridDividers) [self drawGridInContext:ctx spacing:5 lineWidth:1 markers:YES fade:MARKER_FADE_OUT];
if (layer == gridSuperdividers) [self drawGridInContext:ctx spacing:10 lineWidth:2 markers:YES fade:MARKER_FADE_IN];
}
// DRAW GRID LINES
- (void)drawGridInContext:(CGContextRef)context
spacing:(float)spacing
lineWidth:(NSInteger)lineWidth
markers:(BOOL)markers
fade:(int)fade
{
spacing = _gridUnits * spacing;
CGContextSetStrokeColorWithColor(context, [UIColor gridLinesColor]);
CGContextSetLineWidth(context, lineWidth);
CGContextSetShouldAntialias(context, NO);
float top = 0;
float bottom = _gridSize.height;
float left = 0;
float right = _gridSize.width;
// vertical lines (right of origin)
CGMutablePathRef path = CGPathCreateMutable();
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGPathMoveToPoint(path, NULL, i, top);
CGPathAddLineToPoint(path, NULL, i, bottom);
}
CGContextAddPath(context, path);
CGContextStrokePath(context);
CGPathRelease(path);
}
...then I repeat this for() loop three more times to draw the other lines of the grid. I also tried this slightly different version of the loop, where I don't create an individual path, but instead just add lines to the context and then stroke only at the very end of all four of these loops:
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGContextMoveToPoint(context, i, top);
CGContextAddLineToPoint(context, i, bottom);
}
CGContextStrokePath(context);
I also tried a combination of the two, in which I began and stroked within each cycle of the loop:
for (float i = _origin.x + spacing; i <= _gridSize.width; i = i + spacing) {
CGContextBeginPath(context);
CGContextMoveToPoint(context, i, top);
CGContextAddLineToPoint(context, i, bottom);
CGContextStrokePath(context);
}
So how can I streamline this whole drawing method so that it doesn't use as much memory? If it can't draw one path at a time, releasing each one afterward, like the first loop I showed above...I'm not really sure what to do other than read the available memory on the device and turn off the grid drawing function if it's low.
I'm also totally open to alternative methods of grid drawing.

Rotate UI Elements programmatically after shouldAutorotate

I have a viewController that should not "autorotate", but manually rotate specific GUI elements. The reason is that I use the front camera for taking a picture and I don't want the UIView that contains my UIImageView to be rotated.
My code looks like this:
-(BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation {
[self performSelector:#selector(refreshView) withObject:nil afterDelay:1.0];
return NO; // don't autorotate!
}
and:
- (void) refreshView {
UIDeviceOrientation actualDeviceOrientation = [[UIDevice currentDevice] orientation];
float rotation = 0; // UIDeviceOrientationPortrait
if (actualDeviceOrientation == UIDeviceOrientationPortraitUpsideDown) rotation = 180;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeLeft) rotation = 90;
else if (actualDeviceOrientation == UIDeviceOrientationLandscapeRight) rotation = 270;
float rotationRadians = rotation * M_PI / 180;
[UIView animateWithDuration:0.4
animations:^(void) {
self.labelPrize.center = self.prizeView.center;
self.prizeView.transform = CGAffineTransformMakeRotation(rotationRadians);
} completion:^(BOOL finished){ }];
}
"labelPrize" is the label with the caption "20 EURO" that is seen on the screenshots below, "prizeView" is it's container. prizeView is the only GUI element that has constraints defined, which look like this:
Just for clarification, here's what "labelPrize" looks like:
And finally, here's what the app produces:
This is not what I want to achieve, I'd like "prizeView"/"labelPrize" to be
always aligned to the horizon
always in the exact center of the screen
Also worth mentioning: I'd like to add labels above (header) and a button below ("okay") my "labelPrize" and rotate/position them as well in refreshView().
Thanks for any help!
There are two big problems here. Let's take them one at a time.
(1)
self.labelPrize.center = self.prizeView.center;
Think about it. labelPrize is a subview of prizeView. So you are mixing apples with oranges as far as coordinate systems go: labelPrize.center is measured with respect to prizeView.bounds, but prizeView.center is measured with respect to self.view.bounds. To keep the center of labelPrize at the center of prizeView, position it at the midpoint of prizeView's bounds. (However, you should not have to move it at all because the transform transforms the bounds.)
(2)
Rotation view transforms and auto layout are deadly enemies, as I explain here. That is why rotating the transform of prizeView seems to shift its position as well. My answer there gives you several possible workarounds.

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)

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)

UIView coordinate systems - offsetting and translating

I'm pretty sure this is more a of a math question, but I'll phrase it in the context of UIView and iPad-related objective C
I am importing raw data from a mapping file I have created from some public domain material downloaded elsewhere, then split out to isolate various regions within the map. Each region has a number of sub-regions, much like, for example, the continental US and then the various states which appear within the US, and then each sub-region is broken down again, into, let's say, counties.
Each state, and each county has a bounding box which tells me the origin, the width, and height each is.
In my initial setup, I created a separate view for each state, and then another view for each county. The polygon representing the area of the state/county was rendered (obviously with the county on top of the state so it would be visible) relative to a view I created through interface builder, called mainContainerView. This initial setup worked correctly.
Now I am trying to change things a bit, by adding the counties to the UIView holding the polygon for the state, so I will be able to overlay the state as a clipping mask on the counties. The problem is that no matter what I try, I cannot seem to get the county to translate to the right place within the state's view.
It seems like it should be straightforward addition or subtraction as the scaling for each item is exactly the same, and I'm not trying to do any major transformations, so I do not believe the CFAffineTransformation family is needed.
I can post code if necessary, but I'm not trying to get someone to write my program for me; I just want someone to point me in the right direction here, by giving me a suggestion on how to set the county relative to the state within the state's view.
As per a request, here's the relevant code that I am working on right now. This code does not work, but it gives you the idea as to what I'm doing. Posting sample data is a little more difficult, as it involves arrays of points and data extracted from a .SHP file designed to produce a map (and subdivisions). I'll include some comments in the code with some real point values as I step through the program to show you what's happening to them.
MASK_MAX_EASTING, MASK_MAX_NORTHING, MASK_MIN_EASTING, and MASK_MIN_NORTHING are constants which define the bounding box for the entire map of the country when made up of states.
DIST_MAX_EASTING, DIST_MAX_NORTHING, DIST_MIN_EASTING, and DIST_MIN_NORTHING are constants which define the bounding box for a map of the country when made up of the counties. The scales of the two maps are slightly different, so, by using the different bounding boxes, I've been able to scale the two maps to the same size.
-(void)didLoadMap:(NSNotification *)notification {
id region = [notification object];
ShapePolyline *polygon = [region polygon];
if ([notification name] == #"MapsLoadingForState") {
// m_nBoundingBox is an array which contains the RAW northing and easting values for each subdivision. [0] - west limit, [1] - south limit, [2] - east limit, [3] - north limit.
// The code below, combined with the drawrect method in DrawMap.m (below) puts all the states on the map in precisely the right places, so for the state maps, it works just fine.
CGFloat originX = ((polygon->m_nBoundingBox[0]-MASK_MIN_EASTING)*stateScaleMultiplier)+([mainContainerView frame].size.width/2);
CGFloat originY = ((MASK_MAX_NORTHING-(polygon->m_nBoundingBox[3]))*stateScaleMultiplier)+[mainContainerView frame].origin.y;
CGFloat width = polygon->m_nBoundingBox[2] - polygon->m_nBoundingBox[0];
CGFloat height = polygon->m_nBoundingBox[3] - polygon->m_nBoundingBox[1];
CGFloat scaledWidth = width*stateScaleMultiplier;
CGFloat scaledHeight = height*stateScaleMultiplier;
UIColor *subViewColor = [UIColor colorWithRed:0.0 green:1.0 blue:1.0 alpha:0.0];
stateMapView = [[DrawMap alloc] initWithFrame:CGRectMake(originX, originY, scaledWidth, scaledHeight)];
[stateMapView setBackgroundColor:subViewColor];
[stateMapView setStateScale:stateScaleMultiplier];
[stateMapView setCountyScale:countyScaleMultiplier]; // Not actually needed.
[stateMapView setClippingMask:polygon];
UIColor *colorMask = [UIColor colorWithWhite:1.0 alpha:1.0];
[stateMapView setForeground:colorMask];
[states addObject:stateMapView]; // Add the state map view to an array (for future use)
[mapView addSubview:stateMapView]; // MapView is a UIView of equivalent size and shape as mainContainerView.
} else {
// This is where the problems occur.
CGFloat originX = (polygon->m_nBoundingBox[0]-DIST_MIN_EASTING); // 4431590 (raw data)
originX *= countyScaleMultiplier; // 303.929108
originX += ([mainContainerView frame].size.width/2); // 815.929077
CGFloat originY = (DIST_MAX_NORTHING-polygon->m_nBoundingBox[3]); 4328997
originY *= countyScaleMultiplier; // 296.893036
originY -= [mainContainerView frame].origin.y; // 340.893036
CGRect frame = [stateMapView frame]; // Dummy variable created for watches in the debugger. x=856.237183, y=332.169922 width=34.3800087, height=28.7534008
// When I was invoking DrawMap.h and the included drawrect method, the county map would easily be displayed in the right place, as you can see by the values above.
// This is where I think the problem is. The X value is WAY off as far as I can tell.
originX -= frame.origin.x; // -40.3081055
originY -= frame.origin.y; // 8.72311401
CGPoint countyOrigin = CGPointMake(originX,originY);
// Translate the county's origin so it is relative to the origin of stateMapView, not MainContainerView (doesn't work)
[stateMapView addCountyMap:[region polygon] withColor:winner translatedBy:countyOrigin];
[stateMapView setNeedsDisplay];
}
I am aware that there are several issues with this code and some stuff outside the scope of this question may make a few of you raise an eyebrow (or two) but this is definitely a work in progress...
Here's the relevant code from DrawMap.m; I've cut a bunch of stuff out because it is extraneous.
- (void)drawRect:(CGRect)rect {
// Set up
for (int i=0;i<[countyMaps count];i++) {
// Draw the polygon.
[[countyColors objectAtIndex:i] setFill];
[self drawPolygon:[countyMaps objectAtIndex:i]
usingScale:stateScale
translatedBy:CGPointMake([[countyTranslations objectAtIndex:2*i] floatValue],
[[countyTranslations objectAtIndex:2*i+1] floatValue])];
}
// Set the blend mode to multiply
CGContextSetBlendMode(context, kCGBlendModeMultiply);
// Draw a path with clippingMask
[[UIColor colorWithWhite:1.0 alpha:1.0] setFill];
// CGPoint translate = CGPointMake(0,0);
[self drawPolygon:clippingMask usingScale:stateScale translatedBy:CGPointMake(0,0)];
}
-(void)drawPolygon:(ShapePolyline *)aPolygon usingScale:(float)mapScale translatedBy:(CGPoint)trans {
for (int j=0;j<[aPolygon numParts];j++) {
UIBezierPath *path = [UIBezierPath bezierPath];
[path setLineJoinStyle:kCGLineJoinRound];
int startIndex = [[[aPolygon m_Parts] objectAtIndex:j] intValue];
int endIndex = [aPolygon numPoints];
CGPoint startPoint;
[[[aPolygon m_Points] objectAtIndex:startIndex] getValue:&startPoint];
startPoint.x *=mapScale;
startPoint.y *=mapScale;
startPoint.x -= trans.x;
startPoint.y -= trans.y;
[path moveToPoint:startPoint];
if (j+1 != [aPolygon numParts]){
endIndex = [[[aPolygon m_Parts] objectAtIndex:j+1] intValue];
}
for (int k=startIndex+1; k<endIndex; k++)
{
CGPoint nextPoint;
[[[aPolygon m_Points] objectAtIndex:k] getValue:&nextPoint];
nextPoint.x *= mapScale;
nextPoint.y *= mapScale;
nextPoint.x -= trans.x;
nextPoint.y -= trans.y;
[path addLineToPoint:nextPoint];
}
[path closePath];
// [path stroke];
[path fill];
}
}
This tome is really may be too much information, or it may not be enough. Either way, hopefully by adding code, I've given you some information to go on...
-SOLVED-
And it was so simple. I'm surprised it took me this long to figure it out, as I was right in my initial question - it was simple addition and subtraction:
All translations are now done inside the methods which render the polygons. For each point in the polygon, I needed to add the origin of the state's view, and subtract the origin of the county's bounding box, then subtract 44 from the Y-value (the height of the control bar).
This, I think, is an example of over-thinking a problem, getting frustrated, over-thinking more, only to find out three days later that the answer is staring you in the face, waving a red flag, and shouting, "I'M OVER HERE!!!!"