В этом уроке из трех частей мы строили космическую игру для iPad. В первых двух частях мы создали приложение, добавили чертеж космического корабля, добавили средства управления полетом и стрельбой и подключили их, чтобы корабль мог двигаться и запускать лазер. Теперь пришло время добавить несколько астероидов, чтобы сделать игру по-настоящему интересной.
На этом последнем этапе учебного курса вы увидите более продвинутый рисунок, включающий линейные градиентные заливки, а также возможность динамически и удалять виды с дисплея. Мы также рассмотрим, как создать простой код обнаружения столкновений, чтобы вычислить, перехватывает ли лазер астероиды.
Первый шаг, это создать класс AsteroidView.
Создание астероида
Вы создаете класс AsteroidView таким же образом, как и класс CircleControlView из второй части руководства. После завершения работы мастера вы увидите что-то вроде рисунка 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.
Это важная часть архитектуры системы обработки астероидов. Все астероиды будут детьми игровой поверхности. Поверхность игры создает астероиды, добавляя дочерние виды типа AsteroidView, отслеживает столкновения между лазером, который он визуализирует, и его дочерними элементами, уничтожает дочерний вид, если обнаруживается столкновение, и генерирует новые астероиды после уничтожения всех дочерних элементов.
Как только мы убедились, что AsteroidView находится в нужном месте, ему нужно правильно настроить его атрибуты, как показано на рисунке 4.
Вид должен быть установлен в режим перерисовки с прозрачным цветным фоном. Также необходимо отключить взаимодействие с пользователем и отключить несколько касаний.
Вы можете изменять его по своему усмотрению, но на рисунке 5 показаны некоторые разумные настройки для проверки представления.
Теперь, когда все атрибуты установлены, пришло время запустить приложение и увидеть наш астероид во всей его славе градиентной заливки. Это должно выглядеть примерно так, как показано на рисунке 6.
Конечно, набор отступов является случайным, поэтому ваши результаты будут разными.
Как только вы убедитесь, что астероид правильно рендерится, удалите его с игровой поверхности, как показано на рисунке 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.
Теперь хорошо иметь астероидное поле, но в идеале оно должно двигаться так, чтобы оно было действительно угрожающим. Поэтому следующий шаг — оживить астероиды.
Анимация Астероидов
Мы хотим, чтобы астероиды двигались и вращались. 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.
Это здорово, но сейчас лазер просто проходит под астероидами, как вы можете видеть на рисунке 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, теперь вы можете развернуть лазер вокруг себя и уничтожить астероиды.
Здесь я коснулся нижней части экрана лазером, чтобы уничтожить все астероиды в нижней половине.
Вывод
Это не полная игра. Здесь нет нескольких уровней, здесь нет очков, нет возможности даже проиграть. Но все элементарные инструменты созданы для того, чтобы создавать все, что угодно. И вы можете использовать навыки, которые вы узнали здесь; создание приложения с использованием Interface Builder, создание пользовательских представлений, отображение событий взаимодействия с пользователем, создание сложных чертежей и обнаружение столкновений.
Не стесняйтесь использовать весь код из этого примера в вашем собственном приложении. И, пожалуйста, дайте мне знать, если вы создадите что-нибудь интересное. Я непременно скачаю его и пойду.