iOS上的核心图形性能
概要
我正在为iOS设计一款相当简单的2D塔防游戏。
到目前为止,我一直在使用Core Graphics来处理渲染。 应用中根本没有图像文件(尚未)。 我在做相对简单的绘图时遇到了一些重大的性能问题,我正在寻找我如何解决这个问题的想法,而不是转向OpenGL。
游戏设置
在高层次上,我有一个Board类,它是UIView
的子类,用来表示游戏板。 游戏中的所有其他物体(塔,怪物,武器,爆炸等)也是UIView
子类,并在创建时作为子视图添加到Board中。
我保持游戏状态完全独立于对象内的视图属性,并且每个对象的状态在主游戏循环中更新(由NSTimer
以60-240 Hz开启,具体取决于游戏速度设置)。 该游戏完全可以玩,而不需要绘制,更新或动画化视图。
我使用本地刷新率(60 Hz)的CADisplayLink
定时器处理视图更新,该视图更新根据游戏状态的变化调用需要更新视图属性的板对象上的setNeedsDisplay
。 板上的所有对象都会覆盖drawRect:
在框架内绘制一些相当简单的2D形状。 因此,例如,当武器动画时,它会根据武器的新状态重新绘制自己。
性能问题
在iPhone 5上进行测试,在电路板上有大约二十个总的游戏对象,帧速率显着下降到60 FPS(目标帧速率)以下,通常在10-20 FPS范围内。 随着屏幕上更多的动作,它从这里走下坡路。 而在iPhone 4上,情况更糟。
使用仪器我已经确定只有大约5%的CPU时间用于实际更新游戏状态 - 绝大部分时间都用于渲染。 具体来说, CGContextDrawPath
函数(根据我的理解,矢量路径光栅化完成的地方)占用了大量的CPU时间。 有关更多详细信息,请参见底部的乐器截图。
从对StackOverflow和其他站点的一些研究中,似乎Core Graphics不能满足我需要的任务。 显然,抚摸矢量路径是非常昂贵的(特别是绘制不透明的东西时,其alpha值<1.0)。 我几乎肯定OpenGL能够解决我的问题,但它的水平很低,我并不是很兴奋必须使用它 - 似乎并不需要我在这里做的事情。
问题
是否有任何优化我应该考虑尝试从Core Graphics中获得平滑的60 FPS?
一些想法...
有人建议我考虑将所有对象绘制到一个CALayer
而不是让每个对象都有自己的CALayer
,但我不相信这将有助于基于Instruments显示的内容。
就个人而言,我有一个理论,使用CGAffineTransforms
来做我的动画(即在drawRect:
绘制对象的形状drawRect:
一次,然后在后续帧中进行变换以移动/旋转/调整其图层的大小)将解决我的问题,因为这些是直接基于OpenGL。 但我不认为这会比单纯使用OpenGL更容易。
示例代码
为了让你感受一下我正在做的绘图级别,下面是我的一个武器对象(从塔架发射的“射束”)的drawRect:
implementation实例。
注意:这个光束可以被“重新定位”并且穿过整个电路板,所以为了简单起见,它的框架与电路板的尺寸相同。 然而,电路板上的大多数其他物体都将其框架设置为可能的最小外接矩形。
- (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:
TDGreenBeam
类具有上面示例代码部分中显示的确切的drawRect:
实现。
全尺寸屏幕截图
核心图形工作由CPU执行。 结果然后推送到GPU。 当你打电话给setNeedsDisplay
你表明绘图工作需要重新发生。
假设许多对象保持一致的形状,只是四处移动或旋转,则应该简单地在父视图上调用setNeedsLayout
,然后将该视图的layoutSubviews
的最新对象位置推送到center
属性。 仅仅调整职位并不会导致需要重新绘制的东西; 合成器会简单地要求GPU再现它已经在不同位置上的图形。
对于游戏来说,更通用的解决方案可能是忽略center
, bounds
和frame
而不是初始设置。 只需推送您想要transform
的仿射变换,可能是使用这些助手的某种组合创建的。 这将允许您在没有CPU干预的情况下重新定位,旋转和缩放您的对象 - 这些都将是GPU的工作。
如果你想要更多的控制,那么每个视图都有一个带有自己的affineTransform
的CALayer
,但是他们也有一个sublayerTransform
结合sublayerTransform
的变换。 因此,如果您对3D非常感兴趣,那么最简单的方法是将适当的透视矩阵作为超sublayerTransform
的sublayerTransform加载,然后将适当的3d变换推送到子层或子视图。
这种方法有一个明显的缺点,如果你画一次然后放大,你就可以看到像素。 您可以事先调整图层的contentsScale
以尝试改进,但除此之外,您将看到允许GPU继续进行合成的自然结果。 如果您想要在线性滤波和最近滤波之间切换,则图层上会有magnificationFilter
滤镜属性; 线性是默认值。
机会是,你透支。 也就是说,绘制冗余信息。
所以你会想要把你的视图层次分解成图层(就像你刚才提到的那样)。 只更新/绘制需要的内容。 这些图层可以缓存合成中间体,然后GPU可以快速合成所有这些图层。 但是,您需要小心绘制只需绘制的内容,并仅使实际更改的图层区域失效。
调试它:打开“Quartz Debug”并启用“Flash相同屏幕更新”,然后在模拟器中运行您的应用程序。 你想尽量减少这些闪光。
一旦透支是固定的,考虑你可以在辅助线程上渲染什么(例如CALayer.drawsAsynchronously
),或者你如何处理合成中间表示(例如缓存)或栅格化不变层/ rects。 执行这些更改时,请小心测量成本(例如内存和CPU)。
如果您的代码“必须在每一帧上重新绘制”,那么最好考虑如何使用图形硬件来实现相同的功能,而不是每次都调用您的代码重绘CALayer。
例如,通过您的行示例,您可以创建由一系列图块组成的渲染逻辑。 填充的中心部分可以是一个实体拼贴,您可以将其缓存为CALayer,然后反复绘制以填充该区域至特定高度。 然后,让另一个CALayer成为光线边缘,使Alpha渐变为透明,并远离光线边缘。 在顶部和底部渲染这个CALayer(180度旋转),这样你就可以得到一定高度的光线,这个光线在下面和上面都有很好的混合边缘。 然后,重复这个过程,使光线变宽,然后变短,直到最后结束。
然后,您可以使用图形卡硬件加速来渲染较大的形状,但您的调用代码无需真正绘制然后在每个循环中传输图像数据。 每个“瓦片”将已经从CPU传输到GPU,并且GPU中的仿射变换非常快。 基本上,你只是不想每次渲染,然后必须等待所有渲染图像内存不得不传输到GPU。
链接地址: http://www.djcxy.com/p/61687.html