Summary
I'm working on a fairly straightforward 2D tower defense game for iOS.
So far, I've been using Core Graphics exclusively to handle rendering. There are no image files in the app at all (yet). I've been experiencing some significant performance issues doing relatively simple drawing, and I'm looking for ideas as to how I can fix this, short of moving to OpenGL.
Game Setup
At a high level, I have a Board class, which is a subclass of UIView
, to represent the game board. All other objects in the game (towers, creeps, weapons, explosions, etc) are also subclasses of UIView
, and are added as subviews to the Board when they are created.
I keep game state totally separate from view properties within the objects, and each object's state is updated in the main game loop (fired by an NSTimer
at 60-240 Hz, depending on the game speed setting). The game is totally playable without ever drawing, updating, or animating the views.
I handle view updates using a CADisplayLink
timer at the native refresh rate (60 Hz), which calls setNeedsDisplay
on the board objects that need to have their view properties updated based on changes in the game state. All the objects on the board override drawRect:
to paint some pretty simple 2D shapes within their frame. So when a weapon, for example, is animated, it will redraw itself based on the weapon's new state.
Performance Issues
Testing on an iPhone 5, with about 2 dozen total game objects on the board, the frame rate drops significantly below 60 FPS (the target frame rate), usually into the 10-20 FPS range. With more action on the screen, it goes downhill from here. And on an iPhone 4, things are even worse.
Using Instruments I've determined that only roughly 5% of the CPU time is being spent on actually updating the game state -- the vast majority of it is going towards rendering. Specifically, the CGContextDrawPath
function (which from my understanding is where the rasterization of vector paths is done) is taking an enormous amount of CPU time. See the Instruments screenshot at the bottom for more details.
From some research on StackOverflow and other sites, it seems as though Core Graphics just isn't up to the task for what I need. Apparently, stroking vector paths is extremely expensive (especially when drawing things that aren't opaque and have some alpha value < 1.0). I'm almost certain OpenGL would solve my problems, but it's pretty low level and I'm not really excited to have to use it -- it doesn't seem like it should be necessary for what I'm doing here.
The Question
Are there any optimizations I should be looking at to try to get a smooth 60 FPS out of Core Graphics?
Some Ideas...
Someone suggested that I consider drawing all my objects onto a single CALayer
instead of having each object on its own CALayer
, but I'm not convinced that this would help based on what Instruments is showing.
Personally, I have a theory that using CGAffineTransforms
to do my animation (i.e. draw the object's shape(s) in drawRect:
once, then do transforms to move/rotate/resize its layer in subsequent frames) would solve my problem, since those are based directly on OpenGL. But I don't think it would be any easier to do that than just use OpenGL outright.
Sample Code
To give you a feel for the level of drawing I'm doing, here's an example of the drawRect:
implementation for one of my weapon objects (a "beam" fired from a tower).
Note: this beam can be "retargeted" and it crosses the entire board, so for simplicity its frame is the same dimensions as the board. However most other objects on the board have their frame set to the smallest circumscribed rectangle possible.
- (void)drawRect:(CGRect)rect
{
CGContextRef c = UIGraphicsGetCurrentContext();
// Draw beam
CGContextSetStrokeColorWithColor(c, [UIColor greenColor].CGColor);
CGContextSetLineWidth(c, self.width);
CGContextMoveToPoint(c, self.origin.x, self.origin.y);
CGPoint vector = [TDBoard vectorFromPoint:self.origin toPoint:self.destination];
double magnitude = sqrt(pow(self.board.frame.size.width, 2) + pow(self.board.frame.size.height, 2));
CGContextAddLineToPoint(c, self.origin.x+magnitude*vector.x, self.origin.y+magnitude*vector.y);
CGContextStrokePath(c);
}
Instruments Run
Here's a look at Instruments after letting the game run for a while:
The TDGreenBeam
class has the exact drawRect:
implementation shown above in the Sample Code section.