34

概括

我正在为 iOS 开发一款相当简单的 2D 塔防游戏。

到目前为止,我一直在专门使用 Core Graphics 来处理渲染。应用程序中根本没有图像文件(还)。我在做相对简单的绘图时遇到了一些重大的性能问题,我正在寻找关于如何解决这个问题的想法,而不是转向 OpenGL。

游戏设置

在高层次上,我有一个 Board 类,它是 的子类UIView,用于表示游戏板。游戏中的所有其他对象(塔、小兵、武器、爆炸等)也是 的子类UIView,并在创建时作为子视图添加到 Board。

我将游戏状态与对象内的视图属性完全分开,并且每个对象的状态都会在主游戏循环中更新(由NSTimer60-240 Hz 触发,取决于游戏速度设置)。该游戏完全可以玩,无需绘制、更新或动画视图。

我使用CADisplayLink计时器以本机刷新率 (60 Hz) 处理视图更新,该计时器调用setNeedsDisplay需要根据游戏状态变化更新其视图属性的棋盘对象。板上的所有对象都覆盖drawRect:以在其框架内绘制一些非常简单的 2D 形状。因此,例如,当一个武器被激活时,它会根据武器的新状态重新绘制自己。

性能问题

在 iPhone 5 上进行测试,板上总共有大约 2 打游戏对象,帧速率明显低于 60 FPS(目标帧速率),通常在 10-20 FPS 范围内。随着屏幕上的更多动作,它从这里开始走下坡路。而在 iPhone 4 上,情况更糟。

使用 Instruments,我确定只有大约 5% 的 CPU 时间用于实际更新游戏状态——其中绝大多数用于渲染。具体来说,该CGContextDrawPath函数(据我了解,它是矢量路径的光栅化完成的地方)占用了大量的 CPU 时间。有关详细信息,请参阅底部的仪器屏幕截图。

从对 StackOverflow 和其他网站的一些研究来看,Core Graphics 似乎无法满足我的需求。显然,描边矢量路径非常昂贵(尤其是在绘制不透明且 alpha 值 < 1.0 的东西时)。我几乎可以肯定 OpenGL 会解决我的问题,但它的级别很低,我对不得不使用它并不感到兴奋——对于我在这里所做的事情来说,它似乎不是必需的。

问题

是否有任何优化我应该考虑尝试从 Core Graphics 中获得流畅的 60 FPS?

一些想法...

有人建议我考虑将我的所有对象都绘制到一个单独CALayer的对象上,而不是让每个对象单独存在CALayer,但我不相信这会根据 Instruments 显示的内容有所帮助。

就个人而言,我有一个理论,即使用CGAffineTransforms来做我的动画(即drawRect:一次绘制对象的形状,然后在后续帧中进行变换以移动/旋转/调整其图层的大小)将解决我的问题,因为这些直接基于OpenGL。但我认为这样做不会比直接使用 OpenGL 更容易。

示例代码

为了让您了解我正在做的绘图水平,这是我的drawRect:一个武器对象(从塔发射的“光束”)的实现示例。

注意:此光束可以“重新定位”并且它穿过整个电路板,因此为简单起见,它的框架与电路板的尺寸相同。然而,板上的大多数其他对象都将其框架设置为可能的最小外接矩形。

- (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:

全尺寸截图 Instruments 运行游戏,扩展了最重的堆栈跟踪。

4

3 回答 3

20

核心图形工作由 CPU 执行。然后将结果推送到 GPU。当您打电话时,setNeedsDisplay您表示需要重新进行绘图工作。

假设您的许多对象保持一致的形状并且只是移动或旋转,您应该简单地调用setNeedsLayout父视图,然后将该视图中的最新对象位置推送到该属性layoutSubviews,可能直接推送到center属性。仅仅调整位置不会导致需要重绘的东西;合成器将简单地要求 GPU 再现它在不同位置已有的图形。

一个更通用的游戏解决方案可能是忽略center,boundsframe不是初始设置。只需推送您想要的仿射变换,可能是使用这些助手transform的某种组合创建的。这将允许您在没有 CPU 干预的情况下随意重新定位、旋转和缩放对象——这一切都将由 GPU 工作。

如果你想要更多的控制,那么每个视图都有一个CALayer自己的视图,affineTransform但它们也有一个sublayerTransform与子层的变换相结合的视图。因此,如果您对 3d 如此感兴趣,那么最简单的方法是在超层上加载合适的透视矩阵sublayerTransform,然后将合适的 3d 变换推送到子层或子视图。

这种方法有一个明显的缺点,即如果您绘制一次然后按比例放大,您将能够看到像素。您可以contentsScale提前调整图层以尝试改善这一点,但否则您只会看到允许 GPU 继续进行合成的自然结果。magnificationFilter如果要在线性过滤和最近过滤之间切换,图层上有一个属性;线性是默认值。

于 2012-11-07T21:32:31.837 回答
7

很有可能,你正在透支。即绘制冗余信息。

因此,您需要将视图层次结构分解为层(正如您也提到的)。仅更新/绘制需要的内容。这些层可以缓存合成的中间层,然后 GPU 可以快速合成所有这些层。但是您需要小心仅绘制您需要绘制的内容,并且仅使实际更改的图层区域无效。

调试它:打开“Quartz Debug”并启用“Flash Identical Screen Updates”,然后在模拟器中运行您的应用程序。你想尽量减少那些彩色的闪光。

一旦修复了过度绘制,请考虑您可以在辅助线程上渲染什么(例如CALayer.drawsAsynchronously),或者您如何处理合成中间表示(例如缓存)或光栅化不变层/矩形。执行这些更改时要小心衡量成本(例如内存和 CPU)。

于 2012-11-07T19:58:05.217 回答
0

如果您的代码“必须在每一帧上重绘”,那么最好考虑如何使用图形硬件来实现相同的东西,而不是每次都回调到您的代码中重绘 CALayer。

例如,对于您的线条示例,您可以创建由一系列图块组成的渲染逻辑。填充的中心部分可以是一个实体图块,您将其缓存为 CALayer,然后一遍又一遍地绘制以将该区域填充到某个高度。然后,有另一个 CALayer,它是光线的边缘,它的 alpha 淡化为透明,远离光线边缘。在顶部和底部渲染这个 CALayer(旋转 180 度),这样你最终会得到一条特定高度的射线,它的上下有一个很好的混合边缘。然后,重复这个过程,使光线变宽然后变短,直到它最终结束。

然后,您可以使用显卡硬件加速渲染更大的形状,但您的调用代码不需要实际绘制,然后在每个循环中传输图像数据。每个“瓦片”已经从 CPU 传输到 GPU,GPU 中的仿射变换非常快。基本上,您只是不想每次都渲染然后必须等待所有渲染的图像内存都必须传输到 GPU。

于 2013-03-24T03:20:25.777 回答