Подтвердить что ты не робот

Производительность базовой графики на iOS

Резюме

Я работаю над довольно простой 2D-оборонной игрой для iOS.

До сих пор я использовал Core Graphics исключительно для обработки рендеринга. В приложении нет файлов изображений (пока). Я испытываю некоторые существенные проблемы с производительностью, делая относительно простой рисунок, и я ищу идеи относительно того, как я могу это исправить, не переходя в OpenGL.

Настройка игры

На высоком уровне у меня есть класс Board, который является подклассом UIView, для представления игрового поля. Все остальные объекты в игре (башни, ползунки, оружие, взрывы и т.д.) Также являются подклассами UIView и добавляются в качестве подзонов к Совету при их создании.

Я сохраняю состояние игры полностью отдельно от свойств вида внутри объектов, и каждое состояние объекта обновляется в основном игровом цикле (срабатывает NSTimer на 60-240 Гц, в зависимости от настройки скорости игры). Игра полностью воспроизводится без рисования, обновления или анимации представлений.

Я обрабатываю обновления с помощью таймера CADisplayLink с частотой обновления (60 Гц), которая вызывает setNeedsDisplay на объектах платы, которые должны обновлять свои свойства в зависимости от изменений состояния игры. Все объекты на доске переопределяют drawRect:, чтобы нарисовать некоторые довольно простые 2D-фигуры в пределах их рамки. Поэтому, когда оружие, например, анимируется, оно будет перерисовываться на основе нового состояния оружия.

Проблемы с производительностью

Тестирование на iPhone 5 с примерно двумя десятками игровых объектов на борту, частота кадров значительно падает ниже 60 FPS (целевая частота кадров), обычно в диапазоне 10-20 FPS. С большим количеством действий на экране, он идет вниз отсюда. А на iPhone 4 все еще хуже.

Используя инструменты, я определил, что на фактическое обновление состояния игры затрачивается примерно 5% времени процессора, и подавляющее большинство из них направлено на рендеринг. В частности, функция CGContextDrawPath (которая, по моему мнению, является местом растеризации векторных путей), занимает огромное количество процессорного времени. Подробнее см. Снимок экрана "Инструменты" внизу.

Из некоторых исследований, посвященных StackOverflow и другим сайтам, похоже, что Core Graphics просто не соответствует задаче для того, что мне нужно. По-видимому, поглаживание векторных дорожек чрезвычайно дорого (особенно при рисовании объектов, которые не являются непрозрачными и имеют некоторое альфа-значение < 1,0). Я почти уверен, что OpenGL решит мои проблемы, но это довольно низкий уровень, и я не очень рад, что должен его использовать - похоже, что это не обязательно для того, что я здесь делаю.

Вопрос

Есть ли какие-либо оптимизации, на которые я должен обратить внимание, чтобы попытаться получить гладкую 60 FPS из Core Graphics?

Некоторые идеи...

Кто-то предположил, что я рассматриваю возможность рисования всех моих объектов на одном CALayer вместо того, чтобы каждый объект сам по себе CALayer, но я не уверен, что это поможет на основе того, что демонстрирует инструменты.

Лично у меня есть теория, использующая 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);

}

Запуск инструментов

Посмотрите на инструменты, после чего игра продлится некоторое время:

Класс TDGreenBeam имеет точную реализацию drawRect:, показанную выше в разделе Sample Code.

Скриншот полного размера Instruments run of the game, with the heaviest stack trace expanded.

4b9b3361

Ответ 1

Работа с Core Graphics выполняется процессором. Затем результаты переносятся на GPU. Когда вы вызываете setNeedsDisplay, вы указываете, что работа над рисунком должна происходить заново.

Предполагая, что многие из ваших объектов сохраняют согласованную форму и просто перемещаются или вращаются, вы должны просто вызвать setNeedsLayout в родительском представлении, а затем нажать последние позиции объекта в этом представлении layoutSubviews, возможно, непосредственно на center свойство. Простое регулирование позиций не вызывает необходимости перерисовывать вещь; композитор попросит GPU воспроизвести графику, которую он уже имеет в другой позиции.

Более общее решение для игр может состоять в том, чтобы игнорировать center, bounds и frame, кроме как для начальной настройки. Просто нажмите аффинные преобразования, которые вы хотите transform, вероятно, созданные с помощью некоторой комбинации этих помощников. Это позволит вам изменить положение, повернуть и масштабировать объекты без вмешательства ЦП - все это будет работать с графическим процессором.

Если вы хотите еще больше контроля, каждый вид имеет CALayer со своим affineTransform, но также имеет sublayerTransform, который объединяется с преобразованиями подслоев. Поэтому, если вас так интересует 3d, самый простой способ - загрузить подходящую перспективную матрицу в качестве sublayerTransform на суперслое, а затем подтолкнуть соответствующие 3d-преобразования к подслоям или подзонам.

Есть один очевидный недостаток этого подхода в том, что если вы рисуете один раз, а затем увеличиваете масштаб, вы сможете увидеть пиксели. Вы можете настроить свой слой contentsScale заранее, чтобы попытаться улучшить его, но в противном случае вы просто увидите естественное следствие того, что GPU может продолжить композицию. На слое есть свойство magnificationFilter, если вы хотите переключиться между линейной и ближайшей фильтрацией; linear по умолчанию.

Ответ 2

Скорее всего, вы переодеваетесь. То есть, выводя лишнюю информацию.

Итак, вы захотите разбить свою иерархию взглядов на слои (как вы уже упоминали). Обновите/нарисуйте только то, что необходимо. Слои могут кэшировать составные промежуточные элементы, тогда графический процессор может быстро собрать все эти слои. Но вам нужно быть осторожным, чтобы рисовать только то, что нужно рисовать, и аннулировать только те области слоев, которые действительно меняются.

Отладить его: Откройте "Quartz Debug" и включите "Flash Identical Screen Updates", а затем запустите приложение в симуляторе. Вы хотите свести к минимуму эти цветные вспышки.

Как только переопределение будет исправлено, рассмотрите, что вы можете сделать на вторичном потоке (например, CALayer.drawsAsynchronously), или как вы могли бы подойти к компоновке промежуточных представлений (например, кеширования) или растеризующих инвариантных слоев/прямоугольников. Будьте осторожны при измерении затрат (например, памяти и ЦП) при выполнении этих изменений.

Ответ 3

Если ваш код "должен перерисовываться" на каждом кадре, тогда лучше подумать о том, как вы можете использовать графическое оборудование для реализации того же самого, вместо того, чтобы переходить в ваш код, чтобы каждый раз перерисовывать CALayer.

Например, с примером вашей строки вы можете создать визуализационную логику, состоящую из серии фрагментов. Заполненная центральная часть может быть сплошной плиткой, которую вы кешируете как CALayer, а затем рисовать снова и снова, чтобы заполнить область до определенной высоты. Затем добавьте еще один CALayer, который является краем луча, где альфа исчезает до прозрачного, уходящего от края луча. Возьмите этот CALayer сверху и снизу (с 180 степенями вращения), чтобы вы попали в луч с определенной высотой, которая имеет приятный смешанный край внизу и выше. Затем повторите этот процесс, сделав луч более широким, а затем короче, пока он не закончится.

Затем вы можете визуализировать большую фигуру с помощью аппаратного ускорения графической карты, но ваш код вызова не нужно на самом деле рисовать, а затем передавать данные изображения в каждом цикле. Каждая "плитка" уже будет перенесена с CPU на GPU, а аффинные преобразования в GPU будут очень быстрыми. В принципе, вы просто не хотите визуализировать каждый раз, а затем должны ждать, пока вся отображаемая память изображений будет перенесена на графический процессор.