Optimize a views drawing code - objective-c

in a simple drawing application I have a model which has a NSMutableArray curvedPaths holding all the lines the user has drawn. A line itself is also a NSMutableArray, containing the point objects. As I draw curved NSBezier paths, my point array has the following structure: linePoint, controlPoint, controlPoint, linePoint, controlPoint, controlPoint, etc... I thought having one array holding all the points plus control points would be more efficient than dealing with 2 or 3 different arrays.
Obviously my view draws the paths it gets from the model, which leads to the actual question: Is there a way to optimize the following code (inside the view's drawRect method) in terms of speed?
int lineCount = [[model curvedPaths] count];
// Go through paths
for (int i=0; i < lineCount; i++)
{
// Get the Color
NSColor *theColor = [model getColorOfPath:[[model curvedPaths] objectAtIndex:i]];
// Get the points
NSArray *thePoints = [model getPointsOfPath:[[model curvedPaths] objectAtIndex:i]];
// Create a new path for performance reasons
NSBezierPath *path = [[NSBezierPath alloc] init];
// Set the color
[theColor set];
// Move to first point without drawing
[path moveToPoint:[[thePoints objectAtIndex:0] myNSPoint]];
int pointCount = [thePoints count] - 3;
// Go through points
for (int j=0; j < pointCount; j+=3)
{
[path curveToPoint:[[thePoints objectAtIndex:j+3] myNSPoint]
controlPoint1:[[thePoints objectAtIndex:j+1] myNSPoint]
controlPoint2:[[thePoints objectAtIndex:j+2] myNSPoint]];
}
// Draw the path
[path stroke];
// Bye stuff
[path release];
[theColor release];
}
Thanks,
xonic

Hey xon1c, the code looks good. In general it is impossible to optimize without measuring performance in specific cases.
For example, lets say the code above is only ever called once. It draws a picture in a view and it never needs redrawing. Say the code above takes 50 milliseconds to run. You could rewrite it in openGL and do every optimisation under the sun and get that time down to 20 milliseconds and realistically the 30 milliseconds that you have saved makes no difference to anyone and you pretty much just wasted your time and added a load of code-bloat that is going to be more difficult to maintain.
However, if the code above is called 50 times a second and most of those times it is drawing the same thing then you could meaningfully optimise it.
When it comes to drawing the best way to optimise is to is to eliminate unnecessary drawing.
Each time you draw you recreate the NSBezierPaths - are they always different? You may want to maintain the list of NSBezier paths to draw, keep that in sync with your model, and keep drawrect solely for drawing the paths.
Are you drawing to areas of your view which don't need redrawing? The argument to drawrect is the area of the view that needs redrawing - you could test against that (or getRectsBeingDrawn:count:), but it may be in your case that you know that the entire view needs to be redrawn.
If the paths themselves don't change often, but the view needs redrawing often - eg when the shapes of the paths aren't changing but their positions are animating and they overlap in different ways, you could draw the paths to images (textures) and then inside drawrect you would draw the texture to the view instead of drawing the path to the view. This can be faster because the texture is only created once and uploaded to video memory which is faster to draw to the screen. You should look at Core Animation if this is what you need todo.
If you find that drawing the paths is too slow you could look at CGPath
So, on the whole, it really does depend on what you are doing. The best advice is, as ever, not to get sucked into premature optimisation. If your app isn't actually too slow for your users, your code is just fine.

Related

Creating a Jump action

I'm working in spritekit/IOS where I have a button that runs an action when it is clicked. The button works fine, but my action is terrible. On click I run this action.
actionMove=[SKAction moveTo:CGPointMake(300,300)duration:2.0];
Which then causes my sprite does a spinning flying kick too that location. I was needed a little help cause I tried replacing the y axis with
_sprite.position+10
But it gives me an error.
Thanks
What you need is the action moveByX:y:duration:...
Sort of.
You can do this...
SKAction *rise = [SKAction moveByX:0 y:10 duration:1];
SKAction *fall = [SKAction moveByX:0 y:-10 duration:1];
SKAction *jump = [SKAction sequence:#[rise, fall]];
Then you can run the jump action on your sprite.
This will move the sprite up by 10 points over 1 second and then down by 10 points over 1 second.
However, it will not look like a jump. It will not accelerate or anything.
If you are including something like a jump in your game then you would normally want to have some sort of collision detection with the floor and use forces to move your sprite instead of actions.
Collision detection method
Sprite Kit Physics Engine
There are two general ways you could do this. The easiest (although longest learning curve) is to use SKPhysicsBody and the built in physics engine in SpriteKit.
You would add a SKPhysicsBody to your character and a boundary SKPhysicsBody to the floor.
Then to make the character jump you would use something like...
[character.physicsBody applyImpulse:CGVectorMake(0, 1)];
This will apply an immediate force to the character so it "jumps" in the air. Then with the physics engine you have built in gravity and so the character will start to slow down and fall until it hits the floor again.
There is a bit of setup for this (less than you would think) but once the setup is done the rest is VERY VERY VERY simple.
Manually
To do it manually is another case. This requires no setup but a LOT of maths. The way this works is to use the update: function and then each update cycle calculate what the velocity and position of the character is and to update the position accordingly.
You really don't want to do this though.
Make things really easy for yourself by adding SKPhysicsBody to the things that need to collide (the character and the floor) and then exploit the gravity and applyImpulse. It will make it much easier, it will perform faster and it will look a million times better :D
There you go :
CGPoint pos = _sprite.position;
pos.y = pos.y + 10;
_sprite.position = pos;
If you are going to use force (and physics) to move your player then using impulse will give you better results:
_sprite.physicsBody applyImpulse:CGVectorMake(0, <value>);
The above depends on a number of factors, including gravity value and the mass of the _sprite.physicsBody. Experimenting is the best way to figure out what works here.
If you are not using physics you can use the followPath action and easing to get a somewhat realistic jumping effect (similar to cocos2d's jump to action). A simple example of how this can be implemented as a category method on SKAction:
+ (SKAction *)jumpWithStartPoint:(CGPoint)startPoint endPoint:(CGPoint)endPoint height:(CGFloat)height {
UIBezierPath *bezierPath = [[UIBezierPath alloc] init];
[bezierPath moveToPoint:startPoint];
CGPoint peak = CGPointMake(endPoint.x - startPoint.x, startPoint.y + height);
[bezierPath addCurveToPoint:endPoint controlPoint1:peak controlPoint2:peak];
SKAction *jumpAction = [SKAction followPath:bezierPath.CGPath asOffset:NO orientToPath:NO duration:.5f];
jumpAction.timingMode = SKActionTimingEaseIn;
return jumpAction;
}

Check if UIImage is on the top of view

I have a small question, I have a code to generate 200 UIImages in random positions and rotations.
I need to detect the touch event on these images, but I need to check if the touched UIImage is not covered by any other image (even if they intersect in a small area).
Can anybody help me on this?
BTW : I'm trying to do something similar to this game : http://www.dressup247.com/game/1014/Bank-Note-Stack.html
I would suggest to make all images as UIButton's with background image and set for all buttons one action. One more advise - set tag order for every button from most low-lying to most high-lying (and store biggest tag in some iVar). You can do it when layout is generated. It allows you to detect how many views lay over tapped view.
Use CGRectIntersectsRect intersecting views.
-(IBAction)banknotTapped:(UIButton*)sender{
int tag = sender.tag
NSMutableArray *highestViews = [NSMutableArray array];
for (int i=sender.tag; i < biggestTag; ++i){
UIView *v = [self.view viewWithTag:i];
if (CGRectIntersectsRect(sender.frame, v.frame) ) // it allows to find intersecting views
[highestViews addObject:v];
}
}
Now highestViews will contain all highest views that intersect with sender.
Note: tags allow you to detect views, but if you will delete images from superview then it can lead to problems since tag order will broken. Hide views instead of deleting or follow #Costique method in order to determine views order.
You can use the following snippet to determine the order of a subview in its parent view:
NSUInteger order = [containerView.subviews indexOfObjectIdenticalTo: subview];
The less the order, the "higher" the subview is. The topmost subview will have zero order.
While I'd suggest that it would be faster to do this mathematically, defining the banknotes as rectangles and testing for overlap using something like the separating axes theorem, that's obviously not what you're asking and not suitable for general case images.
So I'd suggest that you create a CGBitmapContext the same size as your play area and that when seeding your play area, for each note you place you do something like:
assign new, as yet unused colour to the bank note
draw it to the bitmap context at its destination position, but as a solid object of the assigned colour — so you preserve the outline of the original shape but at each pixel you draw either the solid, assigned colour or no colour at all
count how many pixels in the entire context are now the assigned colour, store that with your object representing the note
Subsequently, when the game starts run through the buffer and count how many of each assigned colour. All notes that have the same number stored as are currently visible are on top. Whenever a note is removed, redraw all the others in order and do the colour count once.
Note that you're not doing the colour count on the total buffer once per note, just once in total. So it's a fixed cost and probably occurs less often than once per tap.
Probably the easiest way to do the drawing as a single colour is to create a mask version of each graphic when it's loaded and then to draw that with a suitable tint. There's an introduction to alpha masks here; you'll probably want to create a custom bitmap image context rather than using the implicit one returned by UIGraphicsGetCurrentContext and to post filter to test the output alpha — pushing down to 0 if its less than some threshold, up to 255 otherwise.

Error removing sprites from a spritesheet in Cocos2D

I'm in the initial building stages of an iphone game, and I'm using sprite sheets to create some random people, each one with sub-sprites for hair, clothing etc.
I'm storing my sprite images in spritesheets using CCSpriteBatchNode. I'm just doing an initial setup test now, where you tap the screen to generate a new random set of people. So the weird thing is, you can tap once and it will remove the old people and replace them with new people, but the second time, it crashes with the error:
"CCSpriteBatchNode doesn't contain the sprite. Can't remove it"
Now I'm sure I've added the sprite to the batch node, in my Person.m constructor I have this line:
[spriteSheet addChild:person];
In my test code in ccTouchesEnded I've got the following code:
//updated with changes suggested by Mazyod and Jer
for(int i=6; i>=0; i--){
Person *per = [_people objectAtIndex:i];
[_people fastRemoveObjectAtIndex:i];
[_spritesheet removeChild:per cleanup:YES];
per = nil;
}
for(int i = 0; i < 7; i++){
Person *per = nil;
per = [Citizen personFromCountry:_country1 WithSpriteSheet:_spritesheet];
per.position = ccp(100 + (50 * i),160);
[_people addObject:per];
[_spritesheet addChild:per];
}
Can anybody suggest what I'm missing? I've read a bunch about spritesheets in cocos2d and am given to understand that removing individual sprites is tricky so I'm sure there's some vital lines I need to add here. Thanks for your help!
Edit: I googled the error and found this thread: http://www.cocos2d-iphone.org/forum/topic/17170 which seems to confirm that Cocos2d thinks I'm not adding the sprite to the spritesheet - but I am, as proven by the fact that the sprites add correctly the first time, just not the second.
One solution is to simply avoid removing the sprites at all, just make them invisible and redraw them with new characteristics when they need to be reused. I'd rather know what the real solution is though because it seems cleaner.
Looks like you need to change
[_people addObject:per];
to
[_people replaceObjectAtIndex:i withObject:per];
In your first loop you are just setting the value of the object in the array to nil, but not removing it from the array. In the second loop you just add it onto the end of the array, but your array already has 7 nils in it.
Let me know if it works.
Well, I could help you clear out one thing for now:
Any CCNode can only be a child to one parent. ie It has to have a single parent.
But, what you have here:
for(int i=0; i<7; i++){
Person *per = [_people objectAtIndex:i];
[self removeChild:per cleanup:YES];
[_spritesheet removeChild:per cleanup:YES];
per = nil;
}
Suggest you are trying to add the person to both the spriteSheet and self at the same time.. Check your Log, it must have something like:
cocos2d: removeChild, child not found.
And from the error you are getting, I am betting that the person is added to self and not to the sprite sheet.
So, how to solve this?
Well, you have to add the person to the spriteSheet as a child, then add the spriteSheet to self as a child. (Actually, the order you add them doesn't matter).
Sort this out, and maybe this problem will go away, or at least it will be clearer so we can help you.
I just want to mention my experience here with this problem and how I solved it.
Remember, you are either trying to remove a child that was never added..
OR
Trying to remove a child TWICE.
This was the case for me. The collision detection in my game was solid (at least I thought). Then randomly, like 1 out of every 7-10 runs... I would get this crash. I realized it was because I had coded my projectiles to be removed once they intersected a target.
I did not however, put a failsafe where IF my tick method detected that it was in collision with MORE then 1 target at a time.
This was because for every projectile, I iterated through each target to check for a collision, then removed the respective projective if collision was detected. So I created a simple BOOL, and set it to YES if it had already collided with a target. Then I only checked for collision if the projectile had not collided with anything.
So... before:
if (CGRectIntersectsRect(projectileRect, targetRect))
{
//code to remove projectile
}
After:
if (CGRectIntersectsRect(projectileRect, targetRect) && projectile.hasHitaTarget == NO)
{
//code to remove projectile
}

UIScrollView - snap to control when decelerating

The UIScrollView has a lot of information available to the programmer, but I dont see an obvious way to control the location that the control stop at after decelerating from a scroll gesture.
Basically I would like the scrollview to snap to specific regions of the screen. The user can still scroll like normal, but when they stop scrolling the view should snap to the most relevant location, and in the case of a flick gesture the deceleration should stop at these locations too.
Is there an easy way to do something like this, or should I consider the only way to accomplish this effect to write a custom scrolling control?
Since the UITableView is a UIScrollView subclass, you could implement the UIScrollViewDelegate method:
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView
withVelocity:(CGPoint)velocity
targetContentOffset:(inout CGPoint *)targetContentOffset
And then compute what the closest desired target content offset is that you want, and set that on the inout CGPoint parameter.
I've just tried this and it works well.
First, retrieve the unguided offset like this:
CGFloat unguidedOffsetY = targetContentOffset->y;
Then Figure out through some math, where you'd want it to be, noting the height of the table header. Here's a sample in my code dealing with custom cells representing US States:
CGFloat guidedOffsetY;
if (unguidedOffsetY > kFirstStateTableViewOffsetHeight) {
int remainder = lroundf(unguidedOffsetY) % lroundf(kStateTableCell_Height_Unrotated);
log4Debug(#"Remainder: %d", remainder);
if (remainder < (kStateTableCell_Height_Unrotated/2)) {
guidedOffsetY = unguidedOffsetY - remainder;
}
else {
guidedOffsetY = unguidedOffsetY - remainder + kStateTableCell_Height_Unrotated;
}
}
else {
guidedOffsetY = 0;
}
targetContentOffset->y = guidedOffsetY;
The last line above, actually writes the value back into the inout parameter, which tells the scroll view that this is the y-offset you'd like it to snap to.
Finally, if you're dealing with a fetched results controller, and you want to know what just got snapped to, you can do something like this (in my example, the property "states" is the FRC for US States). I use that information to set a button title:
NSUInteger selectedStateIndexPosition = floorf((guidedOffsetY + kFirstStateTableViewOffsetHeight) / kStateTableCell_Height_Unrotated);
log4Debug(#"selectedStateIndexPosition: %d", selectedStateIndexPosition);
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:selectedStateIndexPosition inSection:0];
CCState *selectedState = [self.states objectAtIndexPath:indexPath];
log4Debug(#"Selected State: %#", selectedState.name);
self.stateSelectionButton.titleLabel.text = selectedState.name;
OFF-TOPIC NOTE: As you probably guessed, the "log4Debug" statements are just logging. Incidentally, I'm using Lumberjack for that, but I prefer the command syntax from the old Log4Cocoa.
After the scrollViewDidEndDecelerating: and scrollViewDidEndDragging:willDecelerate: (the last one just when the will decelerate parameter is NO) you should set the contentOffset parameter of your UIScrollView to the desired position.
You also will know the current position by checking the contentOffset property of your scrollview, and then calculate the closest desired region that you have
Although you don't have to create your own scrolling control, you will have to manually scroll to the desired positions
To add to what felipe said, i've recently created a table view that snaps to cells in a similar way the UIPicker does.
A clever scrollview delegate is definitely the way to do this (and you can also do that on a uitableview, since it's just a subclass of uiscrollview).
I had this done by, once the the scroll view started decelerating (ie after scrollViewDidEndDragging:willDecelerate: is called), responding to scrollViewDidScroll: and computing the diff with the previous scroll event.
When the diff is less than say a 2 to 5 of pixels, i check for the nearest cell, then wait until that cell has been passed by a few pixels, then scroll back in the other direction with setContentOffset:animated:.
That creates a little bounce effect that is very nice for user experience, as it gives a good feedback on the snapping.
You'll have to be clever and not do anything when the table is bouncing at the top or bottom (comparing the offset to 0 or the content size will tell you that).
It works pretty well in my case because the cells are small (about 80-100px high), you might run into problems if you have a regular scroll view with bigger content areas.
Of course, you will not always decelerate past a cell, so in this case i just scroll to the nearest cell, and the animation looks jerky. Turns out with the right tuning, it barely ever happens, so i'm cool with this.
Spend a few hours tuning the actual values depending on your specific screen and you can get something decent.
I've also tried the naive approach, calling setContentOffset:animated: on scrollViewDidEndDecelerating: but it creates a really weird animation (or just plain confusing jump if you don't animate), that gets worse the lower the deceleration rate is (you'll be jumping from a slow movement to a much faster one).
So to answer the question:
- no, there is no easy way to do this, it'll take some time polishing the actual values of the previous algorithm, which might not work at all on your screen,
- don't try to create your own scroll view, you'll just waste time and badly reinvent a beautiful piece of engineering apple created with truck loads of bug. The scrollview delegate is the key to your problem.
Try something like this:
- (void) snapScroll;
{
int temp = (theScrollView.contentOffset.x+halfOfASubviewsWidth) / widthOfSubview;
theScrollView.contentOffset = CGPointMake(temp*widthOfSubview , 0);
}
- (void) scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;
{
if (!decelerate) {
[self snapScroll];
}
}
- (void) scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
[self snapScroll];
}
This takes advantage of the int's drop of the post-decimal digits. Also assumes all your views are lined up from 0,0 and only the contentOffset is what makes it show up in different areas.
Note: hook up the delegate and this works perfectly fine. You're getting a modified version - mine just has the actual constants lol. I renamed the variables so you can read it easy

How to generate an end screen when two images collide?

how to generate an end screen when two images collide. I am making an app with a stickman you move with a very sensitive acceremeter. SO if it hits these spikes, (UIImages) it will generate the end screen. How do I make the app detect this collision and then generate an end screen.
I'm sure you know the rect of the two images because you need to draw them so you can use
bool CGRectIntersectsRect (
CGRect rect1,
CGRect rect2
);
It returns YES if the two rects have a shared point
The fact that you haven't declared any rects doesn't matter. You need rects for collision detection. I assume that you at least have x and y coordinates for the stickman and you should have some kind of idea of his height and width. Judging from the question title it seems like you're using images to draw the objects you want to check for collision, so you should know the height and width of the images you're using. If you don't have this info you can't draw the objects in the right place and you certainly can't check for collisions.
You basically want to use the same rects that you use for drawing the objects.
Some code examples:
If your coordinates point to the middle of the stickman you would use something like the following:
if (CGRectIntersectsRect(CGRectMake(stickman.x-stickman.width/2,
stickman.y-stickman.height/2,
stickman.width,
stickman.height),
CGRectMake(spikes.x-spikes.width/2,
spikes.y-spikes.height/2,
spikes.width,
spikes.height))) {
// Do whatever it is you need to do. For instance:
[self showEndScreen];
}
If your coordinates point to the top left corner of your stickman you would use:
if (CGRectIntersectsRect(CGRectMake(stickman.x,
stickman.y,
stickman.width,
stickman.height),
CGRectMake(spikes.x,
spikes.y,
spikes.width,
spikes.height))) {
// Do whatever it is you need to do. For instance:
[self showEndScreen];
}
If I might give you a suggestion, I would suggest storing the coordinates and sizes in a CGRect, so that you don't have to create a new CGRect every time you're checking for collision.