Управление символами

Приключение поддерживает до четырех проигрывателей. Один проигрыватель, именуемый всюду по коду в качестве проигрывателя по умолчанию, использует клавиатуру (для OS X) и касание (на iOS) для управления символом героя. Другие проигрыватели, если таковые имеются, используют внешние игровые контроллеры для управления их символами героя.

Мы получаем события непосредственно через цепочку респондента или через обработчиков обратного вызова платформы Игрового контроллера, и мы управляем символами героя по мере необходимости во время следующего, проходят через цикл обновления Набора Sprite. Мы используем APAPlayer объекты представлять каждый проигрыватель; каждый раз, когда проигрыватель нажимает клавишу, касается экрана или управляет связанным игровым контроллером, мы обновляем свойство на APAPlayer экземпляр. Во время цикла обновления мы идем через все APAPlayer экземпляры и вносят необходимые корректировки в позицию символа героя каждого проигрывателя, как показано на рисунке 5-1.

Рисунок 5-1  , Образующий мост между цепочкой респондента и циклом обновления

В дополнение к управляемым проигрывателем символам у гоблинов и босса уровня есть искусственный интеллект для управления их перемещением и атакой. Мы используем отдельный объект AI, обновленный на каждом проходят через цикл событий, для поиска соседних героев; если Вы приезжаете в диапазоне набора, мы перемещаем гоблина или босса к герою, давая появление врага, преследующего героя через лабиринт. Полости гоблина также имеют искусственный интеллект, определяющий, как часто полость должна породить гоблинов — повышения ставки, поскольку герои придвигаются поближе.

Получение клавиатуры и сенсорных событий

Любой узел Набора Sprite может получить ввод данных пользователем от цепочки респондента. В OS X, SKNode класс наследовался от NSResponder класс, и в iOS, это наследовалось от UIResponder.

В Приключении мы реализуем и обработку событий цепочечного респондентом и игрового контроллера в APAMultiplayerLayeredCharacterScene класс. Мы используем условные директивы компилятора для реализации keyDown: и keyUp: при создании для OS X, и touchesBegan:withEvent:, touchesMoved:withEvent:, и touchesEnded:withEvent: для iOS.

Обработка событий клавиатуры OS X

Когда пользователь нажимает клавишу, APAMultiplayerLayeredCharacterScene класс получает keyDown: событие. Мы устанавливаем свойство на значении по умолчанию APAPlayer объект, соответствующий ключу. Например, если пользователь нажимает клавишу Up Arrow или 'W', мы устанавливаем moveForward свойство к YES. Мы реализуем keyUp: обнаружить когда разъединения абонентом ключ так, чтобы мы могли сбросить соответствующее свойство на APAPlayer.

Избегать копировать код через keyDown: и keyUp:, оба метода вызывают через к совместно используемому handleKeyEvent:keyDown: метод для обработки события.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -handleKeyEvent:keyDown:
  2. - (void)handleKeyEvent:(NSEvent *)event keyDown:(BOOL)downOrUp {
  3. ... (Check whether the key is on the numeric pad)
  4. switch (keyChar) {
  5. case NSUpArrowFunctionKey:
  6. self.defaultPlayer.moveForward = downOrUp;
  7. break;
  8. ... (Handle the Down, Left, and Right arrow keys)
  9. }
  10. ... (Check non-numeric keys for W, A, S, D, or Space)
  11. }

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

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -update:
  2. - (void)update:(NSTimeInterval)currentTime {
  3. ...
  4. for (APAPlayer *player in self.players) {
  5. ... (Continue if the player is NSNull)
  6. APAHeroCharacter *hero = player.hero;
  7. ... (Continue if there's no hero yet, or if the hero is dying)
  8. if (player.moveForward) {
  9. [hero move:APAMoveDirectionForward withTimeInterval:timeSinceLast];
  10. } else if (player.moveBack) {
  11. [hero move:APAMoveDirectionBack withTimeInterval:timeSinceLast];
  12. }
  13. ... (Handle other keys)
  14. }
  15. }
9

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

Обработка Сенсорных Событий iOS

Когда пользователь касается экрана устройства на iOS, мы принимаем вызов к touchesBegan:withEvent: на APAMultiplayerLayeredCharacterScene. Мы реализуем этот метод для установки целевого расположения, в которое должен переместиться герой; если касание произошло на враге, таком как гоблин или полость, мы вместо этого интерпретируем касание как действие огня против того врага.

Как с обработкой событий OS X, мы используем APAPlayer экземпляр для образования моста между касанием и циклом обновления но на сей раз мы используем moveRequested и targetLocation свойства.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -touchesBegan:withEvent:
  2. - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
  3. ... (Return early if we don't have any heroes in play, or if we're already tracking a touch)
  4. UITouch *touch = [touches anyObject];
  5. APAPlayer *defaultPlayer = self.defaultPlayer;
  6. defaultPlayer.targetLocation = [touch locationInNode:defaultPlayer.hero.parent];
  7. BOOL wantsAttack = NO;
  8. NSArray *nodes = [self nodesAtPoint:[touch locationInNode:self]];
  9. for (SKNode *node in nodes) {
  10. if (node.physicsBody.categoryBitMask & (APAColliderTypeCave | APAColliderTypeGoblinOrBoss) {
  11. wantsAttack = YES;
  12. }
  13. }
  14. defaultPlayer.fireAction = wantsAttack;
  15. defaultPlayer.moveRequested = !wantsAttack;
  16. defaultPlayer.movementTouch = touch;
12

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

16

Если герой атакует врага, мы не интерпретируем касание как перемещение.

При компиляции для iOS, update: метод в APAMultiplayerLayeredCharacterScene содержит этот дополнительный блок кода для контакта с символьным перемещением:

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -update:
  2. - (void)update:(NSTimeInterval)currentTime {
  3. ...
  4. #if TARGET_OS_IPHONE
  5. APAPlayer *defaultPlayer = self.defaultPlayer;
  6. if ([self.heroes count] > 0) {
  7. APAHeroCharacter *hero = defaultPlayer.hero;
  8. if (defaultPlayer.fireAction) {
  9. [hero faceTo:defaultPlayer.targetLocation];
  10. }
  11. if (defaultPlayer.moveRequested) {
  12. if (!CGPointEqualToPoint(defaultPlayer.targetLocation, hero.position)) {
  13. [hero moveTowards:defaultPlayer.targetLocation withTimeInterval:timeSinceLast];
  14. } else {
  15. defaultPlayer.moveRequested = NO;
  16. }
  17. }
  18. }
  19. #endif
  20. ...
9

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

12

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

15

Иначе, мы отменяем требуемое перемещение.

Межплатформенный код это циклично выполняется через APAPlayer экземпляры позже в update: ответственно за инициирование фактического действия огня:

  1. for (APAPlayer *player in self.players) {
  2. ...
  3. APAHeroCharacter *hero = player.hero;
  4. ...
  5. if (player.fireAction) {
  6. [hero performAttackAction];
  7. }
  8. ...
  9. }
  10. }

Поддержка внешних игровых контроллеров

Приключение использует платформу Игрового контроллера для передачи с внешними игровыми контроллерами; мы поддерживаем до четырех различных проигрывателей. Если Вы подключаете новый контроллер, новое APAPlayer экземпляр создается, и новый герой добавляется к игре.

Вы будете помнить от Создания Сцены, что делегат приложения (OS X) или просматривает контроллер (iOS), создает экземпляр APAAdventure сцена, добавляет его к представлению, и затем вызывает configureGameControllers на сцене. Мы реализуем этот метод в APAMultiplayerLayeredCharacterScene зарегистрироваться для получения уведомлений каждый раз, когда игровой контроллер соединяется или разъединяется. Мы также конфигурируем контроллеры, уже подключенные в игровом запуске, и затем начинающие поиск любых беспроводных контроллеров:

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -configureGameControllers
  2. - (void)configureGameControllers
  3. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gameControllerDidConnect:)
  4. name:GCControllerDidConnectNotification object:nil];
  5. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gameControllerDidDisconnect:)
  6. name:GCControllerDidDisconnectNotification object:nil];
  7. [self configureConnectedGameControllers];
  8. [GCController startWirelessControllerDiscoveryWithCompletionHandler:^{
  9. NSLog(@"Finished finding controllers");
  10. }
  11. }

Присвоение контроллеров к проигрывателям

Каждый раз, когда контроллер обнаруживается, мы проверяем playerIndex свойство контроллера. Если контроллер ранее использовался с Приключением, это свойство соответствует индексу проигрывателя в массиве APAPlayer экземпляры; если это - новый контроллер, свойство установлено в GCControllerPlayerIndexUnset.

Если индекс проигрывателя был уже установлен, мы проверяем, чтобы видеть, существует ли уже существующий контроллер, связанный с соответствующим APAPlayer экземпляр; если так, мы обрабатываем контроллер, как будто он никогда не использовался прежде, чтобы избежать иметь многократные контроллеры управляют единственным героем. Иначе, мы создаем нового героя для проигрывателя и добавляем его к миру:

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -assignPresetController:toIndex:
  2. - (void)assignPresetController:(CGController *)controller toIndex:(NSInteger)playerIndex {
  3. APAPlayer *player = self.players[playerIndex];
  4. ... (Check if the player is NSNull and create a new APAPlayer if so)
  5. if (player.controller && player.controller != controller) {
  6. [self assignUnknownController:controller];
  7. return;
  8. }
  9. [self configureController:controller forPlayer:player];
  10. }
6

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

Подготовка к событиям контроллера

configureController:forPlayer: метод устанавливает valueChangedHandler блочное свойство на всех вводах контроллера, которые мы распознаем. Например, если проигрыватель управляет thumbstick или нажимает одну из кнопок клавиатуры направления, мы устанавливаем heroMoveDirection свойство на APAPlayer экземпляр; если проигрыватель нажимает кнопку A, мы устанавливаем fireAction свойство.

  1. - (void)configureController:(GCController *)controller forPlayer:(APAPlayer *)player {
  2. player.controller = controller;
  3. GCControllerDirectionPadValueChangedHandler dpadMoveHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
  4. float length = hypotf(xValue, yValue);
  5. if (length > 0.0f) {
  6. float invLength = 1.0f / length;
  7. player.heroMoveDirection = CGPointMake(xValue * invLength, yValue * invLength);
  8. } else {
  9. player.heroMoveDirection = CGPointZero;
  10. }
  11. };
  12. controller.extendedGamepad.leftThumbstick.valueChangedHandler = dpadMoveHandler;
  13. controller.gamepad.dpad.valueChangedHandler = dpadMoveHandler;
  14. GCControllerButtonValueChangedHandler fireButtonHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
  15. player.fireAction = pressed;
  16. };
  17. controller.gamepad.buttonA.valueChangedHandler = fireButtonHandler;
  18. controller.gamepad.buttonB.valueChangedHandler = fireButtonHandler;
  19. ... (Add a hero for the player, if necessary)
  20. }

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

update: метод ответственен за проверку их APAPlayer свойства, и инициировавший любое соответствующее действие или перемещение на герое проигрывателя.

  1. Adventure: APAMultiplayerLayeredCharacterScene.m -update:
  2. - (void)update:(NSTimeInterval)currentTime {
  3. ...
  4. for (APAPlayer *player in self.players) {
  5. ...
  6. APAHeroCharacter *hero = player.hero;
  7. ...
  8. if (player.fireAction) {
  9. [hero performAttackAction];
  10. }
  11. CGPoint heroMoveDirection = player.heroMoveDirection;
  12. if (hypotf(heroMoveDirection.x, heroMoveDirection.y) > 0.0f) {
  13. [hero moveInDirection:heroMoveDirection withTimeInterval:timeSinceLast];
  14. }
  15. }
  16. }
12

Мы используем hypotf() вычислить длину гипотенузы треугольника с длинами стороны x и y. Если этот результат больше, чем нуль, мы должны переместить героя.

Вместо того, чтобы обходить вперед предварительно установленную сумму после нажатия клавиши или идти к отдельному моменту от касания, heroMoveDirection указывает x и y значение с плавающей точкой между –1.0 и 1.0, в зависимости от того, как далеко и в которое направление пользователь переместил thumbstick.

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

  1. Adeventure: APACharacter.m -moveInDirection:withTimeInterval:
  2. - (void)moveInDirection:(CGPoint)direction withTimeInterval:(NSTimeInterval)timeInterval {
  3. CGPoint curPosition = self.position;
  4. CGFloat movementSpeed = self.movementSpeed;
  5. CGFloat dx = movementSpeed * direction.x;
  6. CGFloat dy = movementSpeed * direction.y;
  7. CGFloat dt = movementSpeed * kMinTimeInterval;
  8. CGPoint targetPosition = CGPointMake(curPosition.x + dx, curPosition.y + dy);
  9. CGFloat ang = APA_POLAR_ADJUST(APARadiansBetweenPoints(targetPosition, curPosition));
  10. self.zRotation = ang;
  11. CGFloat distRemaining = hypotf(dx, dy);
  12. if (distRemaining < dt) {
  13. self.position = targetPosition;
  14. } else {
  15. self.position = CGPointMake(curPosition.x - sinf(ang)*dt, curPosition.y + cosf(ang)*dt);
  16. }
  17. ... (Set up the walk animation)
  18. }
10

APA_POLAR_ADJUST макрос препроцессора, определенный в APAGraphicsUtilities.h это корректирует угол половиной радианов пи, или 90 °.

Все активы в Приключении созданы с «прямым» направлением, другими словами, ориентированы вдоль оси y. Система координат в игре обрабатывает вперед как обращающийся вправо, ориентированный вдоль оси X, таким образом, мы должны скорректировать все ориентации текстуры спрайта путем вращения 90 ° вправо.

Управление врагами с искусственным интеллектом

Врагами в Приключении — гоблинами, боссом уровня, и полостями гоблина — все управляют отдельные объекты искусственного интеллекта. Гоблины и босс уровня используют экземпляр APAChaseAI преследовать любого героя, который является в предопределенном расстоянии; полости гоблина используют APASpawnAI продиктовать, как сгенерированы часто новые гоблины.

Все вражеские символы в Приключении убывают от APAEnemyCharacter класс, переопределяющий updateWithTimeSinceLastUpdate: метод, предоставленный APACharacter вызывать updateWithTimeSinceLastUpdate: на искусственном интеллекте:

  1. Adventure: APAEnemyCharacter.m -updateWithTimeSinceLastUpdate:
  2. - (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
  3. [super updateWithTimeSinceLastUpdate:interval];
  4. [self.intelligence updateWithTimeSinceLastUpdate:interval];
  5. }

Это означает, что объекты искусственного интеллекта обновляются на каждом, проходят через цикл обновления.

Реализация преследования AI

APAChaseAI класс переопределяет краткий обзор APAArtificialIntelligence класс update: метод для преследования самого близкого героя, но только если это в kEnemyAlertRadius расстояние врага. Если герой достаточно близок к атаке, мы поворачиваем врага, чтобы быть обращенным к герою и инициировать действие атаки.

  1. Adventure: APAChaseAI.m -updateWithTimeSinceLastUpdate:
  2. - (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
  3. APACharacter *ourCharacter = self.character;
  4. ... (Return early if our enemy character is dying)
  5. CGPoint position = ourCharacter.position;
  6. APAMultiplayerLayeredCharacterScene *scene = [ourCharacter characterScene];
  7. CGFloat closestHeroDistance = MAXFLOAT;
  8. for (APAHeroCharacter *hero in scene.heroes) {
  9. CGPoint heroPosition = hero.position;
  10. CGFloat distance = APADistanceBetweenPoints(position, heroPosition);
  11. if (distance < kEnemyAlertRadius && distance < closestHeroDistance && !hero.dying) {
  12. closestHeroDistance = distance;
  13. self.target = hero;
  14. }
  15. }
  16. APACharacter *target = self.target;
  17. ... (Return early if there's no target)
  18. CGPoint heroPosition = target.position;
  19. CGFloat chaseRadius = self.chaseRadius;
  20. if (closestHeroDistance > self.maxAlertRadius) {
  21. self.target = nil;
  22. } else if (closestHeroDistance > chaseRadius) {
  23. [self.chararacter moveTowards:heroPosition];
  24. } else if (closestHeroDistance < chaseRadius) {
  25. [self.character faceTo:heroPosition];
  26. [self.character performAttackAction];
  27. }
  28. }

Реализация икры AI

APASpawnAI класс переопределяет updateWithTimeSinceLastUpdate: метод, чтобы определить, как часто полость гоблина должна породить новых гоблинов. Как с искусственным интеллектом преследования, мы ищем самого близкого героя. Мы тогда корректируем время между икрой гоблина относительно расстояния между полостью и самым близким героем так, чтобы полость генерировала гоблинов более часто, поскольку герой становится ближе.

  1. Adventure: APASpawnAI.m -updateWithTimeSinceLastUpdate:
  2. - (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
  3. APACave *cave = (id)self.character;
  4. ... (Return early if our cave is destroyed)
  5. CGFloat closestHeroDistance = kMinimumHeroDistance;
  6. CGPoint closestHeroPosition = CGPointZero;
  7. ... (Calculate the distance to, and get the location of, the closest hero)
  8. CGFloat distScale = (closestHeroDistance / kMinimumHeroDistance);
  9. cave.timeUntilNextGenerate -= interval;
  10. NSUInteger goblinCount = [cave.activeGoblins count];
  11. if (goblinCount < 1 || cave.timeUntilNextGenerate <= 0.0f || (distScale < 0.35f && cave.timeUntilNextGenerate > 5.0f)) {
  12. if (goblinCount < 1 || (!CGPointEqualToPoint(closestHeroPosition, CGPointZero) && [scene closestHeroPosition from:cave.position])) {
  13. [cave generate];
  14. }
  15. cave.timeUntilNextGenerate = (4.0f * distScale);
  16. }
  17. }