Совершенствование

Игры Набора Sprite отличаются от традиционного iOS или приложений OS X в этом все, что Вы видите, на экране постоянно перерисовывается много раз секунда. С точки зрения разработчика работа обновления и отображения узлов сцены, таких как спрайты или эмиттеры частицы, главным образом выполняется автоматически Набором Sprite. Когда Вы добавили спрайт в сцену, это продолжает быть перерисованным, пока Вы не удаляете его. Точно так же эмиттеры частицы продолжают испускать частицы, пока Вы явно не приостанавливаете их или удаляете их из сцены.

Для превращения статической сцены во что-то более интересное Вы создаете и выполняете действия со спрайтами. Моделирование физики заботится о взаимодействиях (таких как коллизии) между организациями физики и препятствует тому, чтобы наложились спрайты. Для каждого кадра Набор Sprite автоматически обновляет позицию каждого узла для изменений, инициированных действиями или после применения результатов моделирования физики.

В Приключении мы также вносим программируемые корректировки в узлы в сцене. Например, мы перемещаем героя в ответ на ввод данных пользователем, и мы перемещаем камеру, таким образом, что символ всегда видим. Каждый раз, когда мы перемещаем камеру, мы должны также скорректировать смещения уровня спрайтов параллакса. Вся эта программируемая логика обновления происходит как часть цикла обновления Набора Sprite.

Цикл обновления

Для каждого кадра Набор Sprite пробегает цикл обновления, имеющий шесть ключевых фаз, как показано на рисунке 4-1.

Рисунок 4-1  цикл обновления

В этих фазах существует три возможности внести программируемые корректировки, прежде чем Набор Sprite представит кадр:

  1. Запуск цикла update: метод, который вызывают на сцене прежде, чем оценить любые действия или моделировать физику.

  2. didEvaluateActions метод вызывают на сцене после Sprite, Кит оценил любые выдающиеся действия с узлами, но перед рассмотрением импликаций от моделирования физики.

  3. didSimulatePhysics метод вызывают на сцене после того, как Набор Sprite внес любые корректировки от моделирования физики. Это - последний переопределяемый вызов метода, прежде чем представление представит сцену, и цикл продолжается снова.

В Приключении мы набрасываемся на этот цикл в двух местах:

  1. Мы реализуем update: в APAMultiplayerLayeredCharacterScene перемещать героя на основе ввода данных пользователем. update: метод поочередно вызывает updateWithTimeSinceLastUpdate:, который реализован APAAdventureScene обновить отдельные символы в сцене, а также искусственный интеллект всех врагов.

    Ввод данных пользователем и искусственный интеллект описаны в Управлении Символами.

  2. Мы реализуем didSimulatePhysics в APAMultiplayerLayeredCharacterScene перемещать камеру, при необходимости, на основе позиции героя. APAAdventureScene класс переопределяет этот метод, чтобы скрыть или показать эмиттеры частицы на основе позиции героя, тогда это обновляет относительные позиции различных слоев изображения, составляющих деревья параллакса и полости.

Мы, возможно, имели дело с перемещением камеры, эмиттерами частицы и обновлениями параллакса во время начальной буквы update: фаза, но каждая из этих задач связывается к позиции героя. Во время update: этап, узел Набора Sprite не обязательно в его заключительной позиции для текущего кадра — это могло переместиться из-за действий, или из-за побочных эффектов от моделирования физики. Например, если бы герой был вовлечен в коллизию с другим символом или стеной, то позиция героя изменилась бы между update: и заключительный рендеринг.

Путем задержки позиционно-зависимой работы до didSimulatePhysics, мы гарантируем, что позиция героя является окончательной для текущего кадра.

Перемещение камеры

Сцена Приключения является очень большой — полный фон является 4096 x 4096 — поэтому только небольшая часть сцены видима в любое время. Термин камера касается понятия видимой области сцены; нет никакого явного включенного объекта камеры Набора Sprite, но Вы будете часто встречаться со ссылками на “перемещение камеры”.

В Приключении все связанные с миром узлы, включая фоновые мозаики, символы и листва, являются дочерними элементами a world узел, который поочередно является дочерним элементом сцены. Мы меняем положение этой вершины дерева world узел в сцене для предоставления эффекта перемещения камеры через уровень. В отличие от этого, узлы, составляющие HUD, являются дочерними элементами отдельного узла, который является прямым дочерним элементом сцены, а не world узел, так, чтобы элементы в HUD не перемещались, когда мы “перемещаем камеру”.

Мы, возможно, приняли решение корректироваться world позиция узла так, чтобы герой всегда, казалось, был в центре видимой части сцены, которая означала бы перемещать камеру каждый раз, когда переместился герой. Вместо этого мы решили определить прямоугольную область, в которую герой мог переместиться, и перемещать камеру, только если герой отклонился вне тех границ, как показано на рисунке 4-2.

  Граничные вставки Камеры рисунка 4-2

Мы используем didSimulatePhysics метод в APAMultiplayerLayeredCharacterScene сменить положение world узел, только если герой в kMinHeroToEdgeDistance (256) точки края текущей видимой области.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. APAHeroCharacter *defaultHero = self.defaultPlayer.hero;
  4. if (defaultHero) {
  5. CGPoint heroPosition = defaultHero.position;
  6. CGPoint worldPos = self.world.position;
  7. CGFloat yCoordinate = worldPos.y + heroPosition.y;
  8. if (yCoordinate < kMinHeroToEdgeDistance) {
  9. worldPos.y = worldPos.y - yCoordinate + kMinHeroToEdgeDistance;
  10. self.worldMovedForUpdate = YES;
  11. } else if (yCoordinate > (self.frame.size.height - kMinHeroToEdgeDistance)) {
  12. worldPos.y = worldPos.y + self.frame.size.height - yCoordinate - kMinHeroToEdgeDistance;
  13. self.worldMovedForUpdate = YES;
  14. }
  15. ... (Repeat for horizontal axis)
  16. self.world.position = worldPos;
  17. }
  18. [self performSelector:@selector(clearWorldMoved) withObject:nil afterDelay:0.0f];
  19. }
3

Если существуют многократные проигрыватели, камера всегда следует за героем проигрывателя по умолчанию.

18

Используя performSelector:withObject:afterDelay: со специальным значением задержки 0.0 средние значения, что селекторный вызов происходит после текущей передачи через цикл выполнения. Используя это значение гарантирует что реализация подкласса didSimulatePhysics произойдет перед worldMovedForUpdate свойство сбрасывается к NO.

Мы устанавливаем worldMovedForUpdate свойство к YES каждый раз, когда перемещена камера. APAAdventure реализация didSimulatePhysics проверки worldMovedForUpdate определить, должны ли мы обновить спрайты параллакса или удалить какие-либо эмиттеры частицы, которые больше не видимы.

Создание эффекта параллакса

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

Для понимания параллакса с помощью реального примера предположите, что Вы - пассажир в движущемся автомобиле, просматривая окна при зданиях и деревьях рядом с дорогой. Когда автомобиль перемещается, деревья, самые близкие к Вам, кажется, перемещаются через Ваше поле зрения намного быстрее, чем делают здания в далеком расстоянии, как показано на рисунке 4-3.

Рисунок 4-3  Реальный параллакс

Мы подражаем этому эффекту в Приключении путем перемещения верхних слоев дерева или полости гоблина большее расстояние относительно центра камеры, чем нижние уровни. Поддержка параллакса предоставлена APAParallaxSprite класс, от которого наследовались все символы и деревья. Большинство символьных подклассов устанавливает usesParallaxEffect к NO, но APATree и APACave классы оба устанавливают свойство в YES.

Каждый раз, когда мы инициализируем спрайт для параллакса, мы используем определяемый инициализатор для APAParallaxSprite, initWithSprites:usingOffset:, который берет массив узлов спрайта и добавляет их как дочерние элементы контейнерного спрайта в уменьшающихся z-позициях.

  1. Adventure: APAParallaxSprite.m -initWithSprites:usingOffset:
  2. - (id)initWithSprites:(NSArray *)sprites usingOffset:(CGFloat)offset {
  3. self = [super init];
  4. if (self) {
  5. _usesParallaxEffect = YES;
  6. CGFloat zOffset = 1.0f / (CGFloat)[sprites count];
  7. CGFloat ourZPosition = self.zPosition;
  8. NSUInteger childNumber = 0;
  9. for (SKNode *node in sprites) {
  10. node.zPosition = ourZPosition + (zOffset + (zOffset * childNumber));
  11. [self addChild:node];
  12. childNumber++;
  13. }
  14. _parallaxOffset = offset;
  15. }
  16. return self;
  17. }
10

Чем выше нарисовано значение z-position, тем ранее в получении упорядочивают спрайт.

Мы создаем совместно использованный APATree экземпляры при загрузке активов в APAAdventureScene, и предоставьте дочерние узлы спрайта для различных уровней.

  1. Adventure: APAAdventureScene.m +loadSceneAssets
  2. + (void)loadSceneAssets {
  3. SKTextureAtlas *atlas = [SKTextureAtlas atlasNamed:@"Environment"];
  4. ...
  5. sSharedSmallTree = [[APATree alloc] initWithSprites:@[
  6. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_base.png"]],
  7. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_middle.png"]],
  8. [SKSpriteNode spriteNodeWithTexture: [atlas textureNamed:@"small_tree_top.png"]]]
  9. usingOffset:25.0f];
  10. ...
  11. }

Мы инициализируем полости гоблина с помощью APACave определяемый инициализатор, initAtPosition:, который инициализирует полость таким же образом, с помощью дочерних спрайтов с cave_base и cave_top текстуры.

Каждый раз мы добавляем спрайт параллакса к миру в APAAdventureScene, мы добавляем его к массиву parallaxSprites.

Обновление смещений параллакса

Мы обновляем смещения спрайтов параллакса каждый раз через цикл обновления. Поскольку смещение изменяется на основе положения камеры, и потому что положение камеры зависит от позиции героя, APAAdventureScene класс обновляет смещения параллакса в didSimulatePhysics, после APAMultiplayerLayeredCharacterScene реализация класса метода скорректировала мировую позицию.

  1. Adventure: APAAdventureScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. [super didSimulatePhysics];
  4. ... (Get the position of the default hero, or hero spawn point)
  5. if (!self.worldMovedForUpdate) {
  6. return;
  7. }
  8. ...
  9. for (APAParallaxSprite *sprite in self.parallaxSprites) {
  10. if (APADistanceBetweenPoints(sprite.position, position) >= 1024) {
  11. continue;
  12. }
  13. [sprite updateOffset];
  14. }
  15. }
5

Мы должны обновить смещения, только если переместился мировой узел.

10

Мы должны обновить только смещения спрайтов, которые в настоящее время видимы, или скоро быть видимыми. Поэтому мы проверяем расстояние между спрайтом и героем; если никакой герой не видим, мы используем точку икры героя — начальная точка на уровне.

updateOffset метод реализован APAParallaxSprite класс для корректировки смещений каждого дочернего узла спрайта относительно центра экрана.

  1. Adventure: APAParallaxSprite.m -updateOffset
  2. - (void)updateOffset {
  3. SKScene *scene = self.scene;
  4. SKNode *parent = self.parent;
  5. ... (Return early if parallax is disabled)
  6. CGPoint scenePos = [scene convertPoint:self.position fromNode:parent];
  7. CGFloat offsetX = (-1.0f + (2.0 * (scenePos.x / scene.size.width)));
  8. CGFloat offsetY = (-1.0f + (2.0 * (scenePos.y / scene.size.height)));
  9. CGFloat delta = self.parallaxOffset / (CGFloat)self.children.count;
  10. int childNumber = 0;
  11. for (SKNode *node in self.children) {
  12. node.position = CGPointMake(offsetX*delta*childNumber, offsetY*delta*childNumber);
  13. childNumber++;
  14. }
  15. }
8

Мы смещаем направления смещения к (–1.0, 1.0) диапазон относительно центра экрана.

14

Каждый дочерний узел смещается увеличивающейся суммой; главный дочерний элемент поэтому смещается больше всего.

Хранение игровой работы без сбоев

Для хранения всего выглядящего гладким пользователю оптимальный уровень перерисовки является 60 кадрами в секунду. Это означает, что весь цикл обновления для каждого кадра должен взять меньше чем 16,7 миллисекунд. В дополнение к оптимизации кода, для которого мы записали update: и didSimulatePhysics, мы приняли различные стратегии попытаться минимизировать количество времени, оно берет Набор Sprite для обновления всего в сцене.

Эмиттеры являются одним из самых интенсивных производительностью аспектов игры Набора Sprite, таким образом, мы использовали редактора эмиттера частицы XCode для минимизации числа частиц на экране в любое время, все еще достигая желаемого эффекта. Каждый раз через цикл обновления, мы также пауза любые эмиттеры, которые не видимы:

  1. Adventure: APAAdventureScene.m -didSimulatePhysics
  2. - (void)didSimulatePhysics {
  3. [super didSimluatePhysics];
  4. ...
  5. if (!self.worldMovedForUpdate) {
  6. return;
  7. }
  8. for (SKEmitterNode *particles in self.particleSystems) {
  9. BOOL particlesAreVisible = APADistanceBetweenPoints(particles.position, position) < 1024;
  10. if (!particlesAreVisible && !particles.paused) {
  11. particles.paused = YES;
  12. } else if (particlesAreVisible && particles.paused) {
  13. particles.paused = NO;
  14. }
  15. }
  16. ...
  17. }
5

Большая часть работы в didSimulatePhysics потребности, которые будут сделаны, только если камера переместилась, таким образом, мы проверяем worldMovedForUpdate свойство (установленный APAMultiplayerLayeredCharacterScene) и return рано, если не перемещался мировой узел.