iphone - Core Graphics Performance en iOS
core-graphics quartz-2d (3)
Resumen
Estoy trabajando en un juego de defensa de torres en 2D bastante sencillo para iOS.
Hasta ahora, he estado usando Core Graphics exclusivamente para manejar la representación. No hay archivos de imagen en la aplicación en absoluto (todavía). He estado experimentando algunos problemas de rendimiento significativos con el dibujo relativamente simple, y estoy buscando ideas sobre cómo puedo solucionar esto, a menos que me cambie a OpenGL.
Configuración del juego
En un nivel alto, tengo una clase de Tablero, que es una subclase de UIView
, para representar el tablero de juego. Todos los demás objetos en el juego (torres, arrastres, armas, explosiones, etc.) también son subclases de UIView
y se agregan como subvistas al Tablero cuando se crean.
Mantengo el estado del juego totalmente separado de las propiedades de vista dentro de los objetos, y el estado de cada objeto se actualiza en el bucle principal del juego (disparado por un NSTimer
a NSTimer
Hz, dependiendo de la configuración de la velocidad del juego). El juego es totalmente jugable sin tener que dibujar, actualizar o animar las vistas.
CADisplayLink
actualizaciones de vista usando un temporizador CADisplayLink
a la frecuencia de actualización nativa (60 Hz), que llama a setNeedsDisplay
en los objetos del tablero que necesitan tener sus propiedades de vista actualizadas en función de los cambios en el estado del juego. Todos los objetos en el tablero reemplazan a drawRect:
para pintar algunas formas 2D muy simples dentro de su marco. Entonces, cuando un arma, por ejemplo, está animada, se volverá a dibujar basándose en el nuevo estado del arma.
Problemas de desempeño
Probando en un iPhone 5, con aproximadamente 2 docenas de objetos de juego en el tablero, la velocidad de cuadros cae significativamente por debajo de 60 FPS (la velocidad de cuadros objetivo), generalmente en el rango de 10-20 FPS. Con más acción en la pantalla, va cuesta abajo desde aquí. Y en un iPhone 4, las cosas son aún peores.
Usando instrumentos, he determinado que solo aproximadamente el 5% del tiempo de CPU se está gastando en actualizar realmente el estado del juego; la gran mayoría se destina a la representación. Específicamente, la función CGContextDrawPath
(que a mi entender es donde se realiza la rasterización de rutas vectoriales) está consumiendo una enorme cantidad de tiempo de CPU. Ver la captura de pantalla de instrumentos en la parte inferior para más detalles.
De algunas investigaciones sobre StackOverflow y otros sitios, parece que Core Graphics simplemente no está a la altura de lo que necesito. Aparentemente, trazar trazados vectoriales es extremadamente costoso (especialmente cuando se dibujan cosas que no son opacas y tienen algún valor alfa <1.0). Estoy casi seguro de que OpenGL resolvería mis problemas, pero es un nivel bastante bajo y no estoy realmente emocionado de tener que usarlo, no parece que deba ser necesario para lo que estoy haciendo aquí.
La pregunta
¿Hay optimizaciones que debería tener en cuenta para intentar obtener 60 FPS sin problemas de Core Graphics?
Algunas ideas...
Alguien me sugirió que considerara dibujar todos mis objetos en un solo CALayer
lugar de tener cada objeto en su propio CALayer
, pero no estoy convencido de que esto ayude en base a lo que muestran los instrumentos.
Personalmente, tengo la teoría de que usar CGAffineTransforms
para hacer mi animación (es decir, dibujar la (s) forma (s) del objeto en drawRect:
una vez, luego hacer transformaciones para mover / rotar / cambiar el tamaño de su capa en cuadros posteriores) resolvería mi problema, ya que son basado directamente en OpenGL. Pero no creo que sea más fácil hacer eso que simplemente usar OpenGL directamente.
Código de muestra
Para darte una idea del nivel de dibujo que estoy haciendo, aquí tienes un ejemplo de drawRect:
implementación de uno de mis objetos de arma (un "rayo" disparado desde una torre).
Nota: esta viga se puede "redireccionar" y atraviesa todo el tablero, por lo que, para simplificar, su marco tiene las mismas dimensiones que el tablero. Sin embargo, la mayoría de los otros objetos en el tablero tienen su marco establecido en el rectángulo circunscrito más pequeño posible.
- (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);
}
Instrumentos de ejecución
He aquí un vistazo a Instruments después de dejar correr el juego por un tiempo:
La clase TDGreenBeam
tiene el exacto drawRect:
implementación que se muestra arriba en la sección Código de ejemplo.
El trabajo de Core Graphics es realizado por la CPU. Los resultados son luego empujados a la GPU. Cuando llama a setNeedsDisplay
, indica que el trabajo de dibujo debe realizarse de nuevo.
Suponiendo que muchos de sus objetos conservan una forma consistente y simplemente se mueven o giran, simplemente debe llamar a setNeedsLayout
en la vista principal, luego presione las últimas posiciones de los objetos en las layoutSubviews
esa vista, probablemente directamente a la propiedad center
. El mero ajuste de posiciones no hace que se deba volver a dibujar nada; el compositor simplemente le pedirá a la GPU que reproduzca el gráfico que ya tiene en una posición diferente.
Una solución más general para los juegos podría ser ignorar el center
, los bounds
y el frame
para la configuración inicial. Simplemente presione las transformaciones afines que desea transform
, probablemente creadas con alguna combinación de estos ayudantes . Eso le permitirá, de manera arbitraria, reposicionar, rotar y escalar sus objetos sin la intervención de la CPU, todo será un trabajo de GPU.
Si desea aún más control, cada vista tiene un CALayer
con su propio affineTransform
pero también tiene un sublayerTransform
que se combina con las transformaciones de las subcapas. Entonces, si está tan interesado en 3d, la forma más sencilla es cargar una matriz de perspectiva adecuada como subcapaTransformar en la super capa y luego enviar transformaciones 3d adecuadas a las subcapas o subvistas.
Hay un único inconveniente obvio en este enfoque: si dibuja una vez y luego aumenta la escala, podrá ver los píxeles. Puede ajustar la escala de contentsScale
su capa por adelantado para intentar mejorarla, pero de lo contrario, verá la consecuencia natural de permitir que la GPU continúe con la composición. Hay una propiedad magnificationFilter
en la capa si desea cambiar entre el filtrado lineal y el más cercano; lineal es el predeterminado.
Lo más probable es que estés sobregirando. Es decir, dibujando información redundante.
Así que querrá dividir su jerarquía de vistas en capas (como también mencionó). Actualizar / dibujar solo lo que se necesita. Las capas pueden almacenar en caché los intermedios compuestos, luego la GPU puede componer todas esas capas rápidamente. Pero debe tener cuidado de dibujar solo lo que necesita dibujar e invalidar solo las regiones de las capas que realmente cambian.
Depuración: abra "Depuración de cuarzo" y habilite "Actualizaciones de pantalla idénticas de Flash", luego ejecute su aplicación en el simulador. Quieres minimizar esos destellos de colores.
Una vez que se solucione el sobregiro, considere lo que puede representar en un subproceso secundario (por ejemplo, CALayer.drawsAsynchronously
), o cómo podría acercarse a la composición de representaciones intermedias (por ejemplo, almacenamiento en caché) o rasterizar capas / rectas invariantes. Tenga cuidado al medir los costos (por ejemplo, la memoria y la CPU) al realizar estos cambios.
Si su código "tiene que volver a dibujar" en cada fotograma, entonces es mejor pensar en cómo puede usar el hardware de gráficos para implementar lo mismo, en lugar de volver a llamar a su código para volver a dibujar el CALayer cada vez.
Por ejemplo, con su ejemplo de línea, podría crear una lógica de representación que consiste en una serie de mosaicos. La parte central rellena podría ser una baldosa sólida que se almacena en caché como CALayer y luego se dibuja una y otra vez para llenar el área hasta una cierta altura. Luego, tenga otro CALayer que sea el borde del rayo, donde el alfa se desvanece hasta ser transparente y se aleja del borde del rayo. Renderice este CALayer en la parte superior e inferior (con 180 grados de rotación) para que termine con un rayo de cierta altura que tenga un borde de mezcla agradable por debajo y por encima. Luego, repita este proceso haciendo que el rayo sea más ancho y luego más corto hasta que finalmente termine.
Luego puede generar la forma más grande con la aceleración de hardware de la tarjeta gráfica, pero su código de llamada no necesita dibujar y luego transferir datos de imagen en cada bucle. Cada "mosaico" ya habrá sido transferido desde la CPU a la GPU, y las transformaciones afines en la GPU son muy rápidas. Básicamente, simplemente no desea renderizar cada vez y luego tiene que esperar a que toda la memoria de imagen renderizada tenga que transferirse a la GPU.