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.
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.
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.
Are there any optimizations I should be looking at to try to get a smooth 60 FPS out of Core Graphics?
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.
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); }
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.
Full Size Screenshot
iOS includes the Core Graphics framework to provide low-level drawing support. These frameworks are what enable the rich graphical capabilities within UIKit. Core Graphics is a low-level 2D graphics framework that allows drawing device independent graphics. All 2D drawing in UIKit uses Core Graphics internally.
Does Core Graphics Use Gpu? A CPU is used for rendering in Core Graphics, and graphics cards are GPU-based in Core Animation.
Core Animation is supposed to be the graphics system of the framework, but there is also Core Graphics. Core Graphics is entirely done on the CPU, and cannot be performed on the GPU.
Offscreen rendering (software rendering) happens when it is necessary to do the drawing in software (offscreen) before it can be handed over to the GPU. Hardware does not handle text rendering and advanced compositions with masks and shadows.
Core Graphics work is performed by the CPU. The results are then pushed to the GPU. When you call setNeedsDisplay
you indicate that the drawing work needs to occur afresh.
Assuming that many of your objects retain a consistent shape and merely move around or rotate you should simply call setNeedsLayout
on the parent view, then push the latest object positions in that view's layoutSubviews
, probably directly to the center
property. Merely adjusting positions does not cause a thing to need to be redrawn; the compositor will simply ask the GPU to reproduce the graphic it already has at a different position.
A more general solution for games might be to ignore center
, bounds
and frame
other than for initial setup. Simply push the affine transforms you want to transform
, probably created using some combination of these helpers. That'll allow you aribtrarily to reposition, rotate and scale your objects without CPU intervention — it'll all be GPU work.
If you want even more control then each view has a CALayer
with its own affineTransform
but they also have a sublayerTransform
that combines with the transforms of sublayers. So if you're so interested in 3d then the easiest way is to load a suitable perspective matrix as the sublayerTransform
on the superlayer and then push suitable 3d transforms to the sublayers or subviews.
There's a single obvious downside to this approach in that if you draw once and then scale up you'll be able to see the pixels. You can adjust your layer's contentsScale
in advance to try to ameliorate for that but otherwise you're just going to see the natural consequence of allowing the GPU to proceed with compositing. There's a magnificationFilter
property on the layer if you want to switch between linear and nearest filtering; linear is the default.
Chances are, you're overdrawing. That is, drawing redundant information.
So you will want to break up your view hierarchy into layers (as you also mentioned). Update/draw only what is needed. The layers can cache the composited intermediates, then the GPU can composite all those layers quickly. But you need to be careful to draw only what you need to draw, and invalidate only regions of layers which actually change.
Debug it: Open "Quartz Debug" and enable "Flash Identical Screen Updates", then run your app in the simulator. You want to minimize those colored flashes.
Once overdrawing is fixed, consider what you can render on a secondary thread (e.g. CALayer.drawsAsynchronously
), or how you could approach compositing intermediate representations (e.g. caching) or rasterizing invariant layers/rects. Be careful to measure costs (e.g. memory and CPU) when performing these changes.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With