Статьи

Light Speed ​​iOS Apps: Падстероиды 3

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

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

Первый шаг, это создать класс AsteroidView.

Создание астероида

Вы создаете класс AsteroidView таким же образом, как и класс CircleControlView из второй части руководства. После завершения работы мастера вы увидите что-то вроде рисунка 1 в списке файлов проекта.

Падстероиды 3 Рисунок 1

фигура 1

Чтобы создать вид астероида, мы нарисуем круг, но в разных точках сделаем случайный отступ, который придаст ему скалистый вид. Мы установили количество отступов на 20, но вы можете изменить это на любое, что, по вашему мнению, дает лучший вид астероида.

Отсюда нам нужно изменить файл AsteroidView.h чтобы добавить список отступов, а также начальную степень поворота. Нам также нужно иметь переменную градиента, которая будет содержать градиентную заливку, которую мы будем использовать для астероида.

Первая версия AsteroidView.h показана в листинге 1.

Листинг 1. AsteroidView.h

 #import <UIKit/UIKit.h> @interface AsteroidView : UIView { NSArray *indents; float startDegree; CGGradientRef gradient; } @end 

Теперь нам нужно реализовать класс AsteroidView.m файле AsteroidView.m который показан в листинге 2.

Листинг 2. AsteroidView.m

 #import "AsteroidView.h" @implementation AsteroidView - (void)setupIndents { NSMutableArray *indentList = [[[NSMutableArray alloc] init] autorelease]; for( int i = 0; i < 20; i++ ) { float indent = ( ( (float)rand()/(float)RAND_MAX ) * 0.6 ) + 0.4; [indentList addObject:[NSNumber numberWithFloat:indent]]; } indents = [[NSArray arrayWithArray:indentList] retain]; startDegree = 0; CGFloat locations[2] = { 0.0, 1.0 }; CGFloat components[8] = { 1.0, 0.9, 0.9, 1.0, 1.0, 0.0, 0.0, 1.0 }; CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); gradient = CGGradientCreateWithColorComponents (colorSpace, components, locations, 2); self.opaque = FALSE; [self setBackgroundColor:[[UIColor alloc] initWithRed:0 green:0 blue:0 alpha:0.0]]; CGColorSpaceRelease(colorSpace); } - (id)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupIndents]; } return self; } - (void)drawRect:(CGRect)rect { if ( indents == NULL ) { [self setupIndents]; } CGPoint center; center.x = self.bounds.size.width / 2; center.y = self.bounds.size.height / 2; float degree = startDegree; float indentDegree = 360.0 / (float)[indents count]; float radius = (float)( self.bounds.size.width / 2 ) * 0.9; BOOL firstPointDrawn = NO; CGPoint firstPoint; CGMutablePathRef path = CGPathCreateMutable(); for( NSNumber *number in indents ) { CGPoint newPoint; newPoint.x = center.x + ( ( radius * [number floatValue] ) * sin( degree * 0.0174532925 ) ); newPoint.y = center.y + ( ( radius * [number floatValue] ) * cos( degree * 0.0174532925 ) ); if ( firstPointDrawn == NO ) { CGPathMoveToPoint(path, NULL, newPoint.x, newPoint.y); firstPoint.x = newPoint.x; firstPoint.y = newPoint.y; } else { CGPathAddLineToPoint(path, NULL, newPoint.x, newPoint.y); } degree += indentDegree; if ( degree > 360.0 ) degree -= 360.0; firstPointDrawn = YES; } CGPathAddLineToPoint(path, NULL, firstPoint.x, firstPoint.y); CGContextRef context = UIGraphicsGetCurrentContext(); CGContextAddPath(context, path); CGContextClosePath(context); CGContextClip(context); CGContextDrawLinearGradient(context, gradient, CGPointMake(self.bounds.size.width/2,0), CGPointMake(self.bounds.size.width/2,self.bounds.size.height), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation); CFRelease(path); } - (void)dealloc { [super dealloc]; CGGradientRelease(gradient); } @end 

Интересная работа в этом классе заключается в двух методах: первый — это метод setupIndents который создает список отступов, а второй — drawRect , выполняющий рисование.

Метод setupIndents создает объект NSMutableArray, который является массивом, к которому можно динамически добавлять элементы. Затем метод заполняет этот массив набором из двадцати случайных значений отступа. Затем он создает массив indentList для объекта, инициализируя NSArray с содержимым NSMutableArray.

Следующее, что делает setupIndents , это создает градиент, который будет использоваться для заполнения астероида. Этот градиент используется в методе drawRect . Метод drawRect начинается с создания обтравочного контура с использованием списка отступов. Затем он использует этот обтравочный контур на прямоугольном чертеже, где заливка является градиентной заливкой. Мы должны пройти через эту проблему, потому что не существует функции градиентной заливки для многоугольника. Таким образом, вам нужно настроить обрезку как многоугольник, а затем нарисовать заполненный прямоугольник в нем.

Тестирование астероида

Теперь, когда у нас есть первая версия кода для астероида, пришло время протестировать его. Самый простой способ — вернуться к файлу XIB и добавить AsteroidView в представление GameSurface. Вы можете увидеть Инспектор Инспекции на рисунке 2 с указанным AsteroidView.

Падстероиды 3 Рисунок 2

фигура 2

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

Падстероиды 3 Рисунок 3

Рисунок 3

Это важная часть архитектуры системы обработки астероидов. Все астероиды будут детьми игровой поверхности. Поверхность игры создает астероиды, добавляя дочерние виды типа AsteroidView, отслеживает столкновения между лазером, который он визуализирует, и его дочерними элементами, уничтожает дочерний вид, если обнаруживается столкновение, и генерирует новые астероиды после уничтожения всех дочерних элементов.

Как только мы убедились, что AsteroidView находится в нужном месте, ему нужно правильно настроить его атрибуты, как показано на рисунке 4.

Падстероиды 3 Рисунок 4

Рисунок 4

Вид должен быть установлен в режим перерисовки с прозрачным цветным фоном. Также необходимо отключить взаимодействие с пользователем и отключить несколько касаний.

Вы можете изменять его по своему усмотрению, но на рисунке 5 показаны некоторые разумные настройки для проверки представления.

Падстероиды 3 Рисунок 5

Рисунок 5

Теперь, когда все атрибуты установлены, пришло время запустить приложение и увидеть наш астероид во всей его славе градиентной заливки. Это должно выглядеть примерно так, как показано на рисунке 6.

Падстероиды 3 Рисунок 6

Рисунок 6

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

Как только вы убедитесь, что астероид правильно рендерится, удалите его с игровой поверхности, как показано на рисунке 7.

Падстероиды 3 Рисунок 7

Рисунок 7

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

Добавление астероидов динамически

В листинге 3 показано, что логическое значение asteroidsAdded необходимо добавить в определение GameSurface.

Листинг 3. Добавление астероидов в GameSurface.h

 #import <UIKit/UIKit.h> @interface GameSurface : UIView { ... BOOL asteroidsAdded; } ... @end 

Затем нам понадобится куча новых методов на GameSurface.m как показано в листинге 4.

Листинг 4. Добавление астероидов в GameSurface.m

 #import "AsteroidView.h" ... - (void)buildAsteroid { CGRect safeZone = CGRectMake((self.bounds.size.width/2)-50, (self.bounds.size.height/2)-50, 100, 100); CGRect aFrame; while( true ) { aFrame.size.width = aFrame.size.height = ( ( (float)rand() / (float)RAND_MAX ) * 100.0 ) + 50; aFrame.origin.x = ( (float)rand() / (float)RAND_MAX ) * ( self.bounds.size.width - aFrame.size.width ); aFrame.origin.y = ( (float)rand() / (float)RAND_MAX ) * ( self.bounds.size.height - aFrame.size.height ); if ( CGRectIntersectsRect(aFrame, safeZone) == FALSE ) break; } AsteroidView *av = [[AsteroidView alloc] initWithFrame:aFrame]; [self addSubview:av]; asteroidsAdded = YES; } - (void)createAsteroids { for( int i = 0; i < 20; i++ ) { [self buildAsteroid]; } } - (void)drawRect:(CGRect)rect { if ( asteroidsAdded == NO ) { [self createAsteroids]; } ... } 

Важным первым шагом является добавление импорта по типу файла для добавления в определение AsteroidView. Оттуда мы добавляем два новых метода, первый — buildAsteroid а второй — createAsteroids . Метод buildAsteroid создает один астероид и добавляет его в качестве дочернего элемента представления. Функция createAsteroid просто вызывает buildAsteroids двадцать раз, чтобы создать загроможденное поле астероидов.

Метод buildAsteroid интересен тем, что использует функцию CGRectIntersectsRect чтобы гарантировать, что местоположение, которое он CGRectIntersectsRect для астероида, не слишком близко к центральной точке экрана. Это гарантирует, что при запуске на корабле нет астероидов.

Метод drawRect расширен, чтобы добавить вызов createAsteroids для создания начального поля астероида.

Если это работает, результат должен выглядеть примерно так, как показано на рисунке 8.

Падстероиды 3 Рисунок 8

Рисунок 8

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

Анимация Астероидов

Мы хотим, чтобы астероиды двигались и вращались. startDegree часть будет обрабатываться путем медленной регулировки значения startDegree в объекте. Местоположение будет изменено игровой поверхностью с использованием атрибута направления и скорости, связанного с астероидом. Обновления для файла AsteroidView.h показаны в листинге 5.

Листинг 5. Добавление цикла и местоположения в AsteroidView.h

 #import <UIKit/UIKit.h> @interface AsteroidView : UIView { NSArray *indents; float startDegree; CGGradientRef gradient; float direction; float speed; } - (void)cycle; @property (readwrite,assign) float direction; @property (readwrite,assign) float speed; @end 

Циклический метод будет использован для вращения астероида. А ключевые слова @property указывают, что значения направления и скорости являются свойствами объекта, которые можно установить с помощью точечной нотации (например, asteroid1.direction = 50).

Обновления AsteroidView.m показаны в листинге 6.

Листинг 6. Добавление цикла и местоположения в AsteroidView.m

 #import "AsteroidView.h" @implementation AsteroidView @synthesize direction, speed; ... - (void)cycle { startDegree += 0.2; if ( startDegree > 360 ) startDegree -= 360.0; [self setNeedsDisplay]; } @end 

@synthesize слово @synthesize — это простой способ автоматического создания методов доступа для свойств направления и скорости. Новый метод цикла, который просто настраивает startDegree и обновляет дисплей, также отображается.

Поверхность игры также должна быть обновлена ​​с помощью нового метода cycleAsteroids, как показано в листинге 7.

Листинг 7. Добавление циклов астероидов в GameSurface.h

 #import <UIKit/UIKit.h> @interface GameSurface : UIView { ... } ... - (void)cycleAsteroids; @end 

Новый метод cycleAsteroids и обновление buildAsteroid показаны в листинге 8.

Листинг 8. Добавление циклов астероидов в GameSurface.m

 - (void)buildAsteroid { ... AsteroidView *av = [[AsteroidView alloc] initWithFrame:aFrame]; av.direction = ( (float)rand() / (float)RAND_MAX ) * 360.0; av.speed = ( ( (float)rand() / (float)RAND_MAX ) * 1.0 ) + 0.2; [self addSubview:av]; asteroidsAdded = YES; } - (void)cycleAsteroids { for( AsteroidView *v in [self subviews] ) { if ( v ) { CGPoint vorigin; vorigin.x = v.frame.origin.x + ( v.speed * sin( v.direction * 0.0174532925 ) ); vorigin.y = v.frame.origin.y + ( v.speed * cos( v.direction * 0.0174532925 ) ); if ( vorigin.x < 0 || vorigin.y < 0 || vorigin.x + v.bounds.size.width > self.bounds.size.width || vorigin.y + v.bounds.size.height > self.bounds.size.height ) { v.direction = v.direction + ( ( ( (float)rand() / (float)RAND_MAX ) * 180.0 ) + 180.0 ); if ( v.direction > 360.0 ) v.direction -= 360.0; } else { [v setFrame:CGRectMake(vorigin.x, vorigin.y, v.bounds.size.width, v.bounds.size.height)]; } [v cycle]; } } } 

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

Последняя модификация — это PadsteroidsViewController.m чтобы заставить его вызывать метод cycleAsteroids как показано в листинге 9.

Листинг 9. Вызывание цикла астероидов из PadsteroidsViewController.m

 - (void)gameUpdate:(NSTimer*)theTimer { [gameSurface cycleAsteroids]; ... } 

Когда все это работает, вы должны увидеть что-то вроде рисунка 9.

Падстероиды 3 Рисунок 9

Рисунок 9

Это здорово, но сейчас лазер просто проходит под астероидами, как вы можете видеть на рисунке 9. Следующий шаг — выяснить, когда лазер проходит через астероид, и уничтожить его.

Добавление обнаружения столкновений

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

Листинг 10. Добавление лазерного обнаружения столкновений в GameSurface.m

 -(BOOL)lineIntersects:(CGRect)rect start:(CGPoint)start end:(CGPoint)end { double minX = start.x; double maxX = end.x; if(start.x > end.x) { minX = end.x; maxX = start.x; } if(maxX > rect.origin.x+rect.size.width) maxX = rect.origin.x+rect.size.width; if(minX < rect.origin.x) minX = rect.origin.x; if(minX > maxX) return NO; double minY = start.y; double maxY = end.y; double dx = end.x - start.x; if(abs(dx) > 0.0000001) { double a = (end.y - start.y) / dx; double b = start.y - a * start.x; minY = a * minX + b; maxY = a * maxX + b; } if(minY > maxY) { double tmp = maxY; maxY = minY; minY = tmp; } if(maxY > rect.origin.y + rect.size.height) maxY = rect.origin.y + rect.size.height; if(minY < rect.origin.y) minY = rect.origin.y; if(minY > maxY) return NO; return YES; } - (void)enableGun:(float)distance angle:(float)angle { gunEnabled = YES; gunDirection = angle; CGPoint laserStart, laserEnd; laserStart.x = ( self.bounds.size.width / 2 ) + shipLocation.x; laserStart.y = ( self.bounds.size.height / 2 ) + shipLocation.y; laserEnd.x = laserStart.x + ( 1000.0 * cos((gunDirection+90.0) * 0.0174532925) ) * -1; laserEnd.y = laserStart.y + ( 1000.0 * sin((gunDirection+90.0) * 0.0174532925) ) * -1; for( AsteroidView *v in [self subviews] ) { if ( [self lineIntersects:v.frame start:laserStart end:laserEnd] ) { [v removeFromSuperview]; } } [self setNeedsDisplay]; } - (void)disableGun { gunEnabled = NO; if ( [[self subviews] count] == 0 ) { [self createAsteroids]; } [self setNeedsDisplay]; } 

Методы disableGun и disableGun теперь обновляются тестами кода для попаданий, а в случае disbleGun создает новые астероиды, если они все были уничтожены. Код столкновения в enableGun перебирает все дочерние lineIntersects вида и использует метод lineIntersects чтобы проверить, пересекается ли вектор лазера с рамкой вида астероида.

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

Падстероиды 3 Рисунок 10

Рисунок 10

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

Вывод

Это не полная игра. Здесь нет нескольких уровней, здесь нет очков, нет возможности даже проиграть. Но все элементарные инструменты созданы для того, чтобы создавать все, что угодно. И вы можете использовать навыки, которые вы узнали здесь; создание приложения с использованием Interface Builder, создание пользовательских представлений, отображение событий взаимодействия с пользователем, создание сложных чертежей и обнаружение столкновений.

Не стесняйтесь использовать весь код из этого примера в вашем собственном приложении. И, пожалуйста, дайте мне знать, если вы создадите что-нибудь интересное. Я непременно скачаю его и пойду.