Управление символами
Приключение поддерживает до четырех проигрывателей. Один проигрыватель, именуемый всюду по коду в качестве проигрывателя по умолчанию, использует клавиатуру (для OS X) и касание (на iOS) для управления символом героя. Другие проигрыватели, если таковые имеются, используют внешние игровые контроллеры для управления их символами героя.
Мы получаем события непосредственно через цепочку респондента или через обработчиков обратного вызова платформы Игрового контроллера, и мы управляем символами героя по мере необходимости во время следующего, проходят через цикл обновления Набора Sprite. Мы используем APAPlayer
объекты представлять каждый проигрыватель; каждый раз, когда проигрыватель нажимает клавишу, касается экрана или управляет связанным игровым контроллером, мы обновляем свойство на APAPlayer
экземпляр. Во время цикла обновления мы идем через все APAPlayer
экземпляры и вносят необходимые корректировки в позицию символа героя каждого проигрывателя, как показано на рисунке 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:
метод для обработки события.
Adventure: APAMultiplayerLayeredCharacterScene.m -handleKeyEvent:keyDown:
- (void)handleKeyEvent:(NSEvent *)event keyDown:(BOOL)downOrUp {
... (Check whether the key is on the numeric pad)
switch (keyChar) {
case NSUpArrowFunctionKey:
self.defaultPlayer.moveForward = downOrUp;
break;
... (Handle the Down, Left, and Right arrow keys)
}
... (Check non-numeric keys for W, A, S, D, or Space)
}
Несмотря на то, что мы устанавливаем APAPlayer
свойства как прямой результат ввода через цепочку респондента, мы выполняем символьное перемещение и атакуем действия во время цикла обновления. Каждый раз через цикл обновления, мы проверяем свойства на всех APAPlayer
объекты и перемещение соответствующий герой или триггер действие атаки по мере необходимости.
Adventure: APAMultiplayerLayeredCharacterScene.m -update:
- (void)update:(NSTimeInterval)currentTime {
...
for (APAPlayer *player in self.players) {
... (Continue if the player is NSNull)
APAHeroCharacter *hero = player.hero;
... (Continue if there's no hero yet, or if the hero is dying)
if (player.moveForward) {
[hero move:APAMoveDirectionForward withTimeInterval:timeSinceLast];
} else if (player.moveBack) {
[hero move:APAMoveDirectionBack withTimeInterval:timeSinceLast];
}
... (Handle other keys)
}
}
Обработка Сенсорных Событий iOS
Когда пользователь касается экрана устройства на iOS, мы принимаем вызов к touchesBegan:withEvent:
на APAMultiplayerLayeredCharacterScene
. Мы реализуем этот метод для установки целевого расположения, в которое должен переместиться герой; если касание произошло на враге, таком как гоблин или полость, мы вместо этого интерпретируем касание как действие огня против того врага.
Как с обработкой событий OS X, мы используем APAPlayer
экземпляр для образования моста между касанием и циклом обновления но на сей раз мы используем moveRequested
и targetLocation
свойства.
Adventure: APAMultiplayerLayeredCharacterScene.m -touchesBegan:withEvent:
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
... (Return early if we don't have any heroes in play, or if we're already tracking a touch)
UITouch *touch = [touches anyObject];
APAPlayer *defaultPlayer = self.defaultPlayer;
defaultPlayer.targetLocation = [touch locationInNode:defaultPlayer.hero.parent];
BOOL wantsAttack = NO;
NSArray *nodes = [self nodesAtPoint:[touch locationInNode:self]];
for (SKNode *node in nodes) {
if (node.physicsBody.categoryBitMask & (APAColliderTypeCave | APAColliderTypeGoblinOrBoss) {
wantsAttack = YES;
}
}
defaultPlayer.fireAction = wantsAttack;
defaultPlayer.moveRequested = !wantsAttack;
defaultPlayer.movementTouch = touch;
Мы устанавливаем wantsAttack
к YES
если касание поражает какого-либо гоблина, босса уровня или полость, и затем используйте это значение, чтобы определить, пытается ли проигрыватель переместить символ или выполнить действие атаки.
Если герой атакует врага, мы не интерпретируем касание как перемещение.
При компиляции для iOS, update:
метод в APAMultiplayerLayeredCharacterScene
содержит этот дополнительный блок кода для контакта с символьным перемещением:
Adventure: APAMultiplayerLayeredCharacterScene.m -update:
- (void)update:(NSTimeInterval)currentTime {
...
#if TARGET_OS_IPHONE
APAPlayer *defaultPlayer = self.defaultPlayer;
if ([self.heroes count] > 0) {
APAHeroCharacter *hero = defaultPlayer.hero;
if (defaultPlayer.fireAction) {
[hero faceTo:defaultPlayer.targetLocation];
}
if (defaultPlayer.moveRequested) {
if (!CGPointEqualToPoint(defaultPlayer.targetLocation, hero.position)) {
[hero moveTowards:defaultPlayer.targetLocation withTimeInterval:timeSinceLast];
} else {
defaultPlayer.moveRequested = NO;
}
}
}
#endif
...
Если проигрыватель атакует, мы поворачиваем их героя для направления к вражеской цели.
12Мы проверяем, чтобы видеть, является ли герой уже в требуемом расположении; в противном случае мы перемещаем героя к расположению.
15Иначе, мы отменяем требуемое перемещение.
Межплатформенный код это циклично выполняется через APAPlayer
экземпляры позже в update:
ответственно за инициирование фактического действия огня:
for (APAPlayer *player in self.players) {
...
APAHeroCharacter *hero = player.hero;
...
if (player.fireAction) {
[hero performAttackAction];
}
...
}
}
Поддержка внешних игровых контроллеров
Приключение использует платформу Игрового контроллера для передачи с внешними игровыми контроллерами; мы поддерживаем до четырех различных проигрывателей. Если Вы подключаете новый контроллер, новое APAPlayer
экземпляр создается, и новый герой добавляется к игре.
Вы будете помнить от Создания Сцены, что делегат приложения (OS X) или просматривает контроллер (iOS), создает экземпляр APAAdventure
сцена, добавляет его к представлению, и затем вызывает configureGameControllers
на сцене. Мы реализуем этот метод в APAMultiplayerLayeredCharacterScene
зарегистрироваться для получения уведомлений каждый раз, когда игровой контроллер соединяется или разъединяется. Мы также конфигурируем контроллеры, уже подключенные в игровом запуске, и затем начинающие поиск любых беспроводных контроллеров:
Adventure: APAMultiplayerLayeredCharacterScene.m -configureGameControllers
- (void)configureGameControllers
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gameControllerDidConnect:)
name:GCControllerDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(gameControllerDidDisconnect:)
name:GCControllerDidDisconnectNotification object:nil];
[self configureConnectedGameControllers];
[GCController startWirelessControllerDiscoveryWithCompletionHandler:^{
NSLog(@"Finished finding controllers");
}
}
Присвоение контроллеров к проигрывателям
Каждый раз, когда контроллер обнаруживается, мы проверяем playerIndex
свойство контроллера. Если контроллер ранее использовался с Приключением, это свойство соответствует индексу проигрывателя в массиве APAPlayer
экземпляры; если это - новый контроллер, свойство установлено в GCControllerPlayerIndexUnset
.
Если индекс проигрывателя был уже установлен, мы проверяем, чтобы видеть, существует ли уже существующий контроллер, связанный с соответствующим APAPlayer
экземпляр; если так, мы обрабатываем контроллер, как будто он никогда не использовался прежде, чтобы избежать иметь многократные контроллеры управляют единственным героем. Иначе, мы создаем нового героя для проигрывателя и добавляем его к миру:
Adventure: APAMultiplayerLayeredCharacterScene.m -assignPresetController:toIndex:
- (void)assignPresetController:(CGController *)controller toIndex:(NSInteger)playerIndex {
APAPlayer *player = self.players[playerIndex];
... (Check if the player is NSNull and create a new APAPlayer if so)
if (player.controller && player.controller != controller) {
[self assignUnknownController:controller];
return;
}
[self configureController:controller forPlayer:player];
}
assignUnknownController
метод перечисляет через APAPlayer
экземпляры, ища проигрыватели, еще не имеющие присвоенного контроллера.
Подготовка к событиям контроллера
configureController:forPlayer:
метод устанавливает valueChangedHandler
блочное свойство на всех вводах контроллера, которые мы распознаем. Например, если проигрыватель управляет thumbstick или нажимает одну из кнопок клавиатуры направления, мы устанавливаем heroMoveDirection
свойство на APAPlayer
экземпляр; если проигрыватель нажимает кнопку A, мы устанавливаем fireAction
свойство.
- (void)configureController:(GCController *)controller forPlayer:(APAPlayer *)player {
player.controller = controller;
GCControllerDirectionPadValueChangedHandler dpadMoveHandler = ^(GCControllerDirectionPad *dpad, float xValue, float yValue) {
float length = hypotf(xValue, yValue);
if (length > 0.0f) {
float invLength = 1.0f / length;
player.heroMoveDirection = CGPointMake(xValue * invLength, yValue * invLength);
} else {
player.heroMoveDirection = CGPointZero;
}
};
controller.extendedGamepad.leftThumbstick.valueChangedHandler = dpadMoveHandler;
controller.gamepad.dpad.valueChangedHandler = dpadMoveHandler;
GCControllerButtonValueChangedHandler fireButtonHandler = ^(GCControllerButtonInput *button, float value, BOOL pressed) {
player.fireAction = pressed;
};
controller.gamepad.buttonA.valueChangedHandler = fireButtonHandler;
controller.gamepad.buttonB.valueChangedHandler = fireButtonHandler;
... (Add a hero for the player, if necessary)
}
Как с клавиатурой и сенсорным вводом, мы используем APAPlayer
возразите как мост между временем, когда мы получаем ввод контроллера и время, когда мы управляем соответствующим героем во время цикла обновления.
update:
метод ответственен за проверку их APAPlayer
свойства, и инициировавший любое соответствующее действие или перемещение на герое проигрывателя.
Adventure: APAMultiplayerLayeredCharacterScene.m -update:
- (void)update:(NSTimeInterval)currentTime {
...
for (APAPlayer *player in self.players) {
...
APAHeroCharacter *hero = player.hero;
...
if (player.fireAction) {
[hero performAttackAction];
}
CGPoint heroMoveDirection = player.heroMoveDirection;
if (hypotf(heroMoveDirection.x, heroMoveDirection.y) > 0.0f) {
[hero moveInDirection:heroMoveDirection withTimeInterval:timeSinceLast];
}
}
}
Мы используем hypotf()
вычислить длину гипотенузы треугольника с длинами стороны x
и y
. Если этот результат больше, чем нуль, мы должны переместить героя.
Вместо того, чтобы обходить вперед предварительно установленную сумму после нажатия клавиши или идти к отдельному моменту от касания, heroMoveDirection
указывает x и y значение с плавающей точкой между –1.0 и 1.0, в зависимости от того, как далеко и в которое направление пользователь переместил thumbstick.
moveInDirection:
метод на APAHeroCharacter
использование эти значения для генерации цели, основанной на местоположении на текущей позиции героя.
Adeventure: APACharacter.m -moveInDirection:withTimeInterval:
- (void)moveInDirection:(CGPoint)direction withTimeInterval:(NSTimeInterval)timeInterval {
CGPoint curPosition = self.position;
CGFloat movementSpeed = self.movementSpeed;
CGFloat dx = movementSpeed * direction.x;
CGFloat dy = movementSpeed * direction.y;
CGFloat dt = movementSpeed * kMinTimeInterval;
CGPoint targetPosition = CGPointMake(curPosition.x + dx, curPosition.y + dy);
CGFloat ang = APA_POLAR_ADJUST(APARadiansBetweenPoints(targetPosition, curPosition));
self.zRotation = ang;
CGFloat distRemaining = hypotf(dx, dy);
if (distRemaining < dt) {
self.position = targetPosition;
} else {
self.position = CGPointMake(curPosition.x - sinf(ang)*dt, curPosition.y + cosf(ang)*dt);
}
... (Set up the walk animation)
}
APA_POLAR_ADJUST
макрос препроцессора, определенный в APAGraphicsUtilities.h
это корректирует угол половиной радианов пи, или 90 °.
Все активы в Приключении созданы с «прямым» направлением, другими словами, ориентированы вдоль оси y. Система координат в игре обрабатывает вперед как обращающийся вправо, ориентированный вдоль оси X, таким образом, мы должны скорректировать все ориентации текстуры спрайта путем вращения 90 ° вправо.
Управление врагами с искусственным интеллектом
Врагами в Приключении — гоблинами, боссом уровня, и полостями гоблина — все управляют отдельные объекты искусственного интеллекта. Гоблины и босс уровня используют экземпляр APAChaseAI
преследовать любого героя, который является в предопределенном расстоянии; полости гоблина используют APASpawnAI
продиктовать, как сгенерированы часто новые гоблины.
Все вражеские символы в Приключении убывают от APAEnemyCharacter
класс, переопределяющий updateWithTimeSinceLastUpdate:
метод, предоставленный APACharacter
вызывать updateWithTimeSinceLastUpdate:
на искусственном интеллекте:
Adventure: APAEnemyCharacter.m -updateWithTimeSinceLastUpdate:
- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
[super updateWithTimeSinceLastUpdate:interval];
[self.intelligence updateWithTimeSinceLastUpdate:interval];
}
Это означает, что объекты искусственного интеллекта обновляются на каждом, проходят через цикл обновления.
Реализация преследования AI
APAChaseAI
класс переопределяет краткий обзор APAArtificialIntelligence
класс update:
метод для преследования самого близкого героя, но только если это в kEnemyAlertRadius
расстояние врага. Если герой достаточно близок к атаке, мы поворачиваем врага, чтобы быть обращенным к герою и инициировать действие атаки.
Adventure: APAChaseAI.m -updateWithTimeSinceLastUpdate:
- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
APACharacter *ourCharacter = self.character;
... (Return early if our enemy character is dying)
CGPoint position = ourCharacter.position;
APAMultiplayerLayeredCharacterScene *scene = [ourCharacter characterScene];
CGFloat closestHeroDistance = MAXFLOAT;
for (APAHeroCharacter *hero in scene.heroes) {
CGPoint heroPosition = hero.position;
CGFloat distance = APADistanceBetweenPoints(position, heroPosition);
if (distance < kEnemyAlertRadius && distance < closestHeroDistance && !hero.dying) {
closestHeroDistance = distance;
self.target = hero;
}
}
APACharacter *target = self.target;
... (Return early if there's no target)
CGPoint heroPosition = target.position;
CGFloat chaseRadius = self.chaseRadius;
if (closestHeroDistance > self.maxAlertRadius) {
self.target = nil;
} else if (closestHeroDistance > chaseRadius) {
[self.chararacter moveTowards:heroPosition];
} else if (closestHeroDistance < chaseRadius) {
[self.character faceTo:heroPosition];
[self.character performAttackAction];
}
}
Реализация икры AI
APASpawnAI
класс переопределяет updateWithTimeSinceLastUpdate:
метод, чтобы определить, как часто полость гоблина должна породить новых гоблинов. Как с искусственным интеллектом преследования, мы ищем самого близкого героя. Мы тогда корректируем время между икрой гоблина относительно расстояния между полостью и самым близким героем так, чтобы полость генерировала гоблинов более часто, поскольку герой становится ближе.
Adventure: APASpawnAI.m -updateWithTimeSinceLastUpdate:
- (void)updateWithTimeSinceLastUpdate:(CFTimeInterval)interval {
APACave *cave = (id)self.character;
... (Return early if our cave is destroyed)
CGFloat closestHeroDistance = kMinimumHeroDistance;
CGPoint closestHeroPosition = CGPointZero;
... (Calculate the distance to, and get the location of, the closest hero)
CGFloat distScale = (closestHeroDistance / kMinimumHeroDistance);
cave.timeUntilNextGenerate -= interval;
NSUInteger goblinCount = [cave.activeGoblins count];
if (goblinCount < 1 || cave.timeUntilNextGenerate <= 0.0f || (distScale < 0.35f && cave.timeUntilNextGenerate > 5.0f)) {
if (goblinCount < 1 || (!CGPointEqualToPoint(closestHeroPosition, CGPointZero) && [scene closestHeroPosition from:cave.position])) {
[cave generate];
}
cave.timeUntilNextGenerate = (4.0f * distScale);
}
}
Мы предоставляем временной интервал ко всем связанным с перемещением методам так, чтобы символ переместил правильное расстояние для пользовательского события, независимо от частоты кадров.