Как я писал Футболоид под iOS

в 8:00, , рубрики: arcanoid, arkanoid, Cocoa, game development, Gamedev, iOS, objective-c, OpenGL, метки: , , , , , ,

Приветствую достопочтеннейших Хабралюдей!

Как я писал Футболоид под iOS
Не так давно, во время поиска новой работы, я получил очень интересное тестовое задание от ZeptoLab: написать за пару рабочих дней арканоид под iOS без использования сторонних библиотек типа Cocos2d/Box2d и т.п., то есть, на «чистом» OpenGL, что показалось мне весьма интересным. Кстати, об этом задании на Хабре уже писали, и даже устраивали разбор полётов. Итак, я бросил вызов судьбе и взялся за геймдев впервые после школьных упражнений с васиком в графическом режиме!

Уточню, что некоторые знания OpenGL у меня уже были, но весьма и весьма поверхностные. Можно сказать, что их не было ну почти совсем, я просто знал, что такое вьюпорт и что бывают какие-то там спрайты, что существуют матрицы преобразования… Так что данная статья могла бы быть озаглавлена "Как написать простую игру под iOS на чистом OpenGL, не зная его", но это слишком уж длинно.

В общем, если Вам интересно, как я это сделал за ~10 часов разработки и ~2 часа чтения, прошу под кат. (осторожно! много кода! мало картинок! ссылка на гитхаб и расслабляющее видео в конце!)

Будем честны. Картинка_для_привлечения_внимания™ не является скриншотом того, что у меня вышло. А вышло у меня вот что:
Как я писал Футболоид под iOS

Уж простите меня за этот обман, но ведь Вы всё равно заинтересовались моей статьёй, правда? )

С чего начинается написание программы, использующей новые для тебя технологии? Правильно, с чтения документации, примеров и статей (ну вроде той, что перед Вами). После пары минут сидения в гугле, была обнаружена замечательная статья (автор Ray Wenderlich), в которой подробно разбирался процесс создания простенькой игрушки. Здесь, если Вам угодно, можете закрыть мою статью и начать читать статью Рэя, но для тех, кто всё же пришёл за арканоидом и разъяснениями на родном языке, я продолжу повествование.

Для начала создадим пустой проект для игры. В Xcode жмём File -> New -> Project..., выбираем шаблон iOS -> Application -> OpenGL Game. Лично я выбрал проект с ARC и без StoryBoard, но Вы, уважаемый читатель, вольны делать так, как вам больше нравится. Что ж, проект создан, мы можем теперь сразу жать Run и любоваться крутящимися кубиками. Но это не совсем то, что мы хотели сделать, так что удаляем почти всё, что нам шаблон вставил, оставляем лишь необходимое. Первым делом удаляем добавленные шейдеры, из ViewController.m удаляем все enum'ы и глобальные переменные, идущие до interface ViewController ().Теперь удаляем лишние методы: loadShaders, compileShader:type:file:, linkProgram:, validateProgram:, в нашем простейшем примере шейдеры мы использовать не будем. Конечно, Вы можете их и задействовать, если знаете как и зачем, но я не стал с этим заморачиваться =).

Далее, выбрасываем всё лишнее из всех оставшихся методов. setupGL и tearDownGL приведём к виду:

- (void)setupGL
{
    [EAGLContextsetCurrentContext:self.context];
    self.effect = [[GLKBaseEffectalloc] init];
}

- (void)tearDownGL
{
    [EAGLContextsetCurrentContext:self.context];
    self.effect = nil;
}

Далее. Мы делаем примитивнейшую игру с примитивнейшей архитектурой, так что пусть наш ViewController будет контролировать вообще всю игру. По-хорошему, надо было бы создать некий GameController и возложить все эти функции на него, но пока обойдёмся и так. Посему, добавляем в наш ViewController такие вот свойства и методы:

#define kGameStateNone 0
#define kGameStateLose 1
#define kGameStateWon  2

@property (assign) int gameState; // see kGameState...
@property (assign) BOOL gameRunning;

- (void)loadBricks;

- (void)startGame;
- (void)endGameWithWin:(BOOL)win;

За что они отвечают — и без комментариев ясно. Что ж, теперь время подумать и о логике игры, точнее, о логике программы. Что и как мы будем делать? Какие объекты у нас будут? Что надо для этого сделать?

Очень много вопросов, да. Но простыми размышлениями (и знанием о том, что есть какие-то спрайты) мы приходим к такой модели: есть «бита», которой управляет игрок, есть кирпичи, которые надо ломать и есть мячик, который отскакивает от всего и ломает кирпичи. Кроме того, есть фон, и есть меню — где мы можем запустить игру и увидеть её итоги (выиграли мы или проиграли). Всё, что мы видим на экране, рисуется с помощью спрайтов, которые являются базовыми объектами для отрисовки, что-то вроде окон в системе. Ну, или кнопок в окошке. У них могут быть разнообразные атрибуты: координаты, размеры, картинка, которую они отрисовывают. Забегая вперёд, добавим сюда ещё скорость и направление движения. Что ж, вооружимся этими свойствами и смело нажмём ⌘N в Xcode! Создадим новый класс, наследуемый от NSObject, назовём его GameSprite. И добавим к нему такие вот свойства и методы:

@interface GameSprite : NSObject

- (id)initWithTexture:(GLKTextureInfo *)textureInfo effect:(GLKBaseEffect *)effect;
- (id)initWithImage:(UIImage *)image effect:(GLKBaseEffect *)effect;
- (void)render;
- (void)update:(float)dt;
- (CGRect)boundingRect;

@property (assign) GLKVector2 position;
@property (assign) CGSize contentSize;
@property (assign) GLKVector2 moveVelocity; // points/sec
@end

Собственно, мы сможем создавать спрайты на основе картинки либо текстуры, отрисовывать их, обновлять в зависимости от времени, получать его границы. А так же задавать и получать его позицию, размеры и скорость. А вот в реализации спрайта начинается интересное! Нам потребуются вертексы и квады. Что это такое? Ну, если просто, то вертекс — это точка в пространстве, а квад — совокупность четырёх вертексов. Причём вертекст содержит на самом деле две точки — для самого спрайта и для его текстуры. Объявим соответствующие структуры:

typedef struct
{
    CGPoint geometryVertex;
    CGPoint textureVertex;
} TexturedVertex;

typedef struct
{
    TexturedVertex bl;
    TexturedVertex br;    
    TexturedVertex tl;
    TexturedVertex tr;    
} TexturedQuad;

При этом надо учесть следующую особенность: координаты текстуры нормированные, то есть изменяются всегда в интервале от 0 до 1. Нам этот квад понадобится для отрисовки спрайтом своей текстуры, так что для класса GameSprite объявим такие приватные свойства:

@interface GameSprite()

@property (strong) GLKBaseEffect *effect;
@property (assign) TexturedQuad quad;
@property (strong) GLKTextureInfo *textureInfo;

- (void)initQuadAndSize;

@end

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

- (id)initWithTexture:(GLKTextureInfo *)textureInfo effect:(GLKBaseEffect *)effect
{
	if ((self = [super init]))
	{
		self.effect = effect;
		
        self.textureInfo = textureInfo;
        if (self.textureInfo == nil)
		{
            NSLog(@"Error loading texture! Texture info is nil!");
            return nil;
        }

		[self initQuadAndSize];
    }
    return self;
}

- (id)initWithImage:(UIImage *)image effect:(GLKBaseEffect *)effect
{
	if ((self = [super init]))
	{
		self.effect = effect;

		NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], GLKTextureLoaderOriginBottomLeft, nil];

		NSError *error;
        self.textureInfo = [GLKTextureLoader textureWithCGImage:image.CGImage options:options error:&error];
        if (self.textureInfo == nil)
		{
            NSLog(@"Error loading image: %@", [error localizedDescription]);
            return nil;
        }

		[self initQuadAndSize];		
    }
    return self;
}

- (void)initQuadAndSize
{
	self.contentSize = CGSizeMake(self.textureInfo.width, self.textureInfo.height);
	
	TexturedQuad newQuad;
	newQuad.bl.geometryVertex = CGPointMake(0, 0);
	newQuad.br.geometryVertex = CGPointMake(self.textureInfo.width, 0);
	newQuad.tl.geometryVertex = CGPointMake(0, self.textureInfo.height);
	newQuad.tr.geometryVertex = CGPointMake(self.textureInfo.width, self.textureInfo.height);
	
	newQuad.bl.textureVertex = CGPointMake(0, 0);
	newQuad.br.textureVertex = CGPointMake(1, 0);
	newQuad.tl.textureVertex = CGPointMake(0, 1);
	newQuad.tr.textureVertex = CGPointMake(1, 1);
	self.quad = newQuad;
}

Ничего хитрого, просто присвоение или загрузка из картинки текстуры и задание квада. Здесь как раз видно, что у каждого из четырёх вертексов координаты геометрии и текстуры задаются в разных единицах.

Теперь переходим к самому интересному — к отрисовке спрайта, за которую отвечает метод render!

- (void)render
{
    self.effect.texture2d0.name = self.textureInfo.name;
    self.effect.texture2d0.enabled = YES;
    self.effect.transform.modelviewMatrix = self.modelMatrix;
    [self.effect prepareToDraw];
    long offset = (long)&_quad;
    glEnableVertexAttribArray(GLKVertexAttribPosition);
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribPosition, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, geometryVertex)));
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(TexturedVertex), (void *) (offset + offsetof(TexturedVertex, textureVertex)));
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}

Здесь мы готовим текстуру к отрисовке через эффект (про матрицу преобразования — чуть позже, пока эту строчку не читайте), включаем нужные параметры OpenGL (позицию и текстуру), передаём с помощью хитрых преобразований наш квад в OpenGL и наконец рисуем! Всё вроде как просто, а подробное объяснение что делают эти строки выходит за рамки данной статьи, я просто отправлю Вас, любезный читатель, в документацию (glVertexAttribPointer и glDrawArrays).

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

- (GLKMatrix4)modelMatrix
{
     GLKMatrix4 modelMatrix = GLKMatrix4Identity;
     modelMatrix = GLKMatrix4Translate(modelMatrix, self.position.x, self.position.y, 0);
     modelMatrix = GLKMatrix4Translate(modelMatrix, -self.contentSize.width / 2, -self.contentSize.height / 2, 0);
     return modelMatrix;
}

Здесь тоже всё просто: берём матрицу Identity (единичную, которая ничего не делает), перемещаем (Translate) начало координат в self.position, а затем перемещаем его в середину спрайта. Это весьма удобно при манипулировании кучей спрайтов. Теперь, сделав заглушку для update:, можем создать наш первый спрайт и загрузить в него картинку из ресурсов. А в нашем ViewController'е его отрисовать:

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

    if (!self.context)
    {
        NSLog(@"Failed to create ES context");
    }

    [self setupGL];

    GLKView *view = (GLKView *)self.view;
    view.context = self.context;
    GLKMatrix4 projectionMatrix = GLKMatrix4MakeOrtho(0, 320, 0, 480, -1024, 1024);

    self.effect.transform.projectionMatrix = projectionMatrix;

    // initializing game state
    self.gameRunning = NO;
    self.gameState = kGameStateNone;

     // initializing sprites
    self.testSprite = [[GameSpritealloc] initWithImage:[UIImageimageNamed:@"myImage"] effect:self.effect];
    self.testSprite .position = GLKVector2Make(160, 35);
}

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(1.f, 1.f, 1.f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);    
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);

    [self.testSprite render];
}

Как я писал Футболоид под iOS
Ну что, неплохо? На нашем игровом поле теперь отрисовывается одна картинка. Да, мы можем позвать друзей и похвастаться, что почти написали игру! =)

Но, на самом деле, радоваться пока рано. Спрайт у нас пока один и он не двигается. Уберём его пока совсем и займёмся чем-нибудь интересным, например, придумаем, какие нам вообще нужны спрайты. Во-первых, бита. Во-вторых, мячик. В третьих, куча спрайтов для кирпичей. Что-то ещё? Ах да, ещё фон. А ещё — меню! И как его делать? Спрайт для затемнения, спрайт для надписи «выиграл», спрайт для надписи «проиграл», спрайт для надписи «велкам», спрайт для кнопки «играть». Ну вроде бы всё, фух. Ну поехали, задаём свойства нашему контроллеру:

@property (strong, nonatomic) GameSprite *playerBat;
@property (strong, nonatomic) GameSprite *ball;
@property (strong, nonatomic) GameSprite *background;
@property (strong, nonatomic) GameSprite *menuDimmer;
@property (strong, nonatomic) GameSprite *menuCaption;
@property (strong, nonatomic) GameSprite *menuCaptionWon;
@property (strong, nonatomic) GameSprite *menuCaptionLose;
@property (strong, nonatomic) GameSprite *menuStartButton;
@property (strong, nonatomic) NSMutableArray *bricks;

Как я писал Футболоид под iOS
Для всех этих элементов создадим в гимпе/фотошопе/прочем картинки подходящих размеров. Пусть кирпичи у нас будут 50x10 точек, весьма неплохой размер. Мячик можно нарисовать, а можно и найти в сети, что я, собственно, и сделал. Товарищ Junior подсобил с фоном (да кстати и придумал полностью взять футбольную тематику), ну а кирпичи и биту я и сам кое-как нарисовал. Не буду приводить полностью код инициализации всех спрайтов, он идентичен уже приведённому, отличаются лишь координаты. Но вот что делать с кирпичами? Их расположение как-то совсем не хочется хардкодить, ведь так? А если ещё уровень захотим запилить? Давайте ка лучше придумаем свой формат файла, в котором эти уровни будут храниться. У меня вышел формат очень простой, но Вы вольны дать волю фантазии. Итак, вот мой файл для первого (и пока единственного) уровня:

101101
111111
010010
111111
000000
111111

Вроде бы формат самоочевиден, ведь так? 0 — нет кирпича, 1 — есть. Ну а если захочется сделать второй тип кирпичей, то вводим цифры 2, 3, 4 и так далее. Но это так, задел на будущее. Функция загрузки кирпичей тоже весьма понятная:

- (void)loadBricks
{
     // assuming 6x6 brick matrix, each brick is 50x10
     NSError *error;
     [NSBundle mainBundle] ;
     NSStringEncoding encoding;
     NSString *filePath = [[NSBundle mainBundle] pathForResource:@"level1" ofType:@"txt"];
     NSString *levelData = [NSString stringWithContentsOfFile:filePath usedEncoding:&encoding error:&error];
     if (levelData == nil)
     {
          NSLog(@"Error loading level data! %@", error);
          return;
     }
     levelData = [[levelData componentsSeparatedByCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]] componentsJoinedByString: @""];
     if ([levelData length] < (6*6))
     {
          NSLog(@"Level data has incorrect size!");
          return;
     }
     NSMutableArray *loadedBricks = [NSMutableArray array];
     UIImage *brickImage = [UIImage imageNamed:@"brick1"];

     NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithBool:YES], GLKTextureLoaderOriginBottomLeft, nil];
     GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:brickImage.CGImage options:options error:&error];
     if (textureInfo == nil)
     {
          NSLog(@"Error loading image: %@", [error localizedDescription]);
          return;
     }

     for (int i = 0; i < 6; i++)
     {
          for (int j = 0; j < 6; j++)
          {
               if ([levelData characterAtIndex:j + i * 6] == '1')
               {
                    GameSprite *brickSprite = [[GameSprite alloc] initWithTexture:textureInfo effect:self.effect];
                    brickSprite.position = GLKVector2Make((j + 1) * 50.f - 15.f, 480.f - (i + 1) * 10.f - 15.f);
                    [loadedBricks addObject:brickSprite];
               }
          }
     }
     self.bricks = loadedBricks;
}

Что ж, теперь мы подгрузили и кирпичи. Можно запустить нашу игру и полюбоваться ровными рядами кирпичей, нашей битой и мячиком, и всё это на фоне нашего фона. Ах да, мы этого не увидим, мы ведь отрисовку забыли добавить в glkView:drawInRect:! Добавим отрисовку по порядку: сначала фон, потом бита, потом все кирпичи, а в конце — мячик. И уж теперь можно и полюбоваться! =)

Но как же так? Почему мячик не двигается? Это ведь не картина, это ведь игра, а значит, мячик должен куда-то лететь! Для того, чтобы мячик летел, ему надо задать скорость, а в методе спрайта update: надо эту скорость учесть и изменить координаты:

- (void)update:(float)dt
{
    GLKVector2 curMove = GLKVector2MultiplyScalar(self.moveVelocity, dt);
    self.position = GLKVector2Add(self.position, curMove);
}

А в методе update нашего ViewController'а надо проапдейтить спрайт мячика:

     [self.ball update:self.timeSinceLastUpdate];

Теперь можно задать мячику ненулевую скорость в startGame — и мячик полетит!

- (void)startGame
{
     self.gameRunning = YES;
     self.gameState = kGameStateNone;
     [selfloadBricks];
     self.ball.position = GLKVector2Make(160, 80);
     self.ball.moveVelocity = GLKVector2Make(120, 240);
}

Что ж, теперь вызовем этот метод в viewDidLoad — и мячик полетит, но — быстренько улетит за пределы экрана. Мда, печально! Что ж, начинаем обдумывать обработку коллизий и столкновения мячика со стенами. В уже готовом методе update добавим првоерку столкновения мячика со стенами:

// checking for walls
     // left
     if (self.ball.boundingRect.origin.x <= 0)
     {
          self.ball.moveVelocity = GLKVector2Make(-self.ball.moveVelocity.x, self.ball.moveVelocity.y);
          self.ball.position = GLKVector2Make(self.ball.position.x - self.ball.boundingRect.origin.x, self.ball.position.y);
     }
     // right
     if (self.ball.boundingRect.origin.x + self.ball.boundingRect.size.width >= 320)
     {
          self.ball.moveVelocity = GLKVector2Make(-self.ball.moveVelocity.x, self.ball.moveVelocity.y);
          self.ball.position = GLKVector2Make(self.ball.position.x - (self.ball.boundingRect.size.width + self.ball.boundingRect.origin.x - 320), self.ball.position.y);
     }
     // top
     if (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height >= 480)
     {
          self.ball.moveVelocity = GLKVector2Make(self.ball.moveVelocity.x, -self.ball.moveVelocity.y);
          self.ball.position = GLKVector2Make(self.ball.position.x, self.ball.position.y - (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height - 480));
     }
     // bottom (player lose)
     if (self.ball.boundingRect.origin.y + self.ball.boundingRect.size.height <= 70)
     {
          [self endGameWithWin:NO];
     }

Логика простая, не требуется даже тигонометрии: при столкновении с левой и правой стеной мы обращаем горизонтальную составляющую скорости, с верхней стеной — вертикальную. Угол падения равен углу отражения — а значит законы физики соблюдены. Ну и при пересечении нижней границы мы засчитываем проигрыш. Увы. Кстати, мы ещё и «подправляем» положение мячика, чтобы избежать затыков.

Запускаем! Мячик отскочил от правой стены, от верхней — и мы проиграли, хотя и не узнали об этом. Уже неплохо! Теперь нам надо сделать отскок от биты, причём, как это и принято в арканоидах, угол падения далеко не всегда будет равен углу отражения, который будет зависеть от места падения мяча на биту. Здесь уже применим тригонометрию для вычисления этого самого угла:

     // player strikes!
     if (CGRectIntersectsRect(self.ball.boundingRect, self.playerBat.boundingRect))
     {
          float angleCoef = (self.ball.position.x - self.playerBat.position.x) / (self.playerBat.contentSize.width / 2);
          float newAngle = 90.f - angleCoef * 80.f;
          GLKVector2 ballDirection = GLKVector2Normalize(GLKVector2Make(1 / tanf(GLKMathDegreesToRadians(newAngle)), 1));
          float ballSpeed = GLKVector2Length(self.ball.moveVelocity);
          self.ball.moveVelocity = GLKVector2MultiplyScalar(ballDirection, ballSpeed);
          self.ball.position = GLKVector2Make(self.ball.position.x, self.ball.position.y + (self.playerBat.boundingRect.origin.y + self.playerBat.boundingRect.size.height - self.ball.boundingRect.origin.y));
     }

Достаточно примитивно. Но теперь начнётся самое интересное: определение столкновения с кирпичами, их разрушение и отскок мячика.

     // checking for broken bricks
     NSMutableArray *brokenBricks = [NSMutableArray array];
     GLKVector2 initialBallVelocity = self.ball.moveVelocity;
     for (GameSprite *brick in self.bricks)
     {
        if (CGRectIntersectsRect(self.ball.boundingRect, brick.boundingRect))
          {
               [brokenBricks addObject: brick];
               if ((self.ball.position.y < brick.position.y - brick.contentSize.height / 2) || (self.ball.position.y > brick.position.y + brick.contentSize.height / 2))
               {
                    self.ball.moveVelocity = GLKVector2Make(initialBallVelocity.x, -initialBallVelocity.y);
               }
               else
               {
                    self.ball.moveVelocity = GLKVector2Make(-initialBallVelocity.x, initialBallVelocity.y);
               }
          }
    }
    
     // removing them
     for (GameSprite *brick in brokenBricks)
     {
          [self.bricks removeObject:brick];
     }
    
     if (self.bricks.count == 0)
     {
          [self endGameWithWin:YES];
     }

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

Ну что ж, теперь можно запустить игру и посмотреть, как мячик скачет, разбивает кирпичи и… Да, а как его отбивать-то? Бита есть, но нам ведь надо ей управлять? я выбрал самый простой способ управления битой: выделил под ней место для «ушка», за которое можно её таскать большим пальцем. Для того, чтобы изменять положение биты, нам нужно получать события нажатия и перемещения пальца, а для этого нам нужны распознаватели жестов! Установим же их:

- (void)viewDidLoad
{
     // ...

     // gestures
     UIPanGestureRecognizer *panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePanGesture:)];
     UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGestureFrom:)];                                                              
     [self.view addGestureRecognizer:panRecognizer];
     [self.view addGestureRecognizer:tapRecognizer];
}

- (void)handleTapGestureFrom:(UITapGestureRecognizer *)recognizer
{
    CGPoint touchLocation = [recognizer locationInView:recognizer.view];
     if (self.gameRunning)
     {
          GLKVector2 target = GLKVector2Make(touchLocation.x, self.playerBat.position.y);
          self.playerBat.position = target;
     }
}

- (void)handlePanGesture:(UIGestureRecognizer *)gestureRecognizer
{
     CGPoint touchLocation = [gestureRecognizer locationInView:gestureRecognizer.view];
    if (self.gameRunning)
     {
          GLKVector2 target = GLKVector2Make(touchLocation.x, self.playerBat.position.y);
          self.playerBat.position = target;
     }
}

Ну что, готовы поиграть? Запустили, разломали все кирпичи! И что дальше? Надо бы показать игроку результат его трудов, а посему отобразим меню. Обычно для таких вещей делают отдельные сцены, реализуемые с помощью класса GameScene, наследованного от GameSprite, но я решил не заморачиваться и оставить код как можно проще. Так что меню собирается из разных спрайтов. Что ж, в glkView:drawInRect: нам надо проверять состояние игры и отрисовывать всё как надо:

- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect
{
    glClearColor(1.f, 1.f, 1.f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);   
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);

     [self.background render];
    [self.playerBat render];
     for (GameSprite *brick in self.bricks)
     {
          [brick render];
     }
     [self.ball render];
    
     if (!self.gameRunning)
     {
          [self.menuDimmer render];
          [self.menuStartButton render];
          switch (self.gameState)
          {
               case kGameStateWon:
                    [self.menuCaptionWon render];
                    break;
               case kGameStateLose:
                    [self.menuCaptionLose render];
                    break;
               case kGameStateNone:
               default:
                    [self.menuCaption render];
                    break;
          }
     }
}

Тут опять же всё предельно просто, даже комментировать не хочу. Осталось обработать нажатие на кнопку «играть», для этого в handleTapGestureFrom: вставим блок «else»:

     else if (CGRectContainsPoint(self.menuStartButton.boundingRect, touchLocation))
     {
          [self startGame];
     }

Всё! Запускаем, играем, выигрываем и проигрываем! И главное — радуемся! Радуемся своей, родной, самонаписанной игре под айфон на чистом OpenGL!

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

Конечно, игра эта далека от совершенства. В ней не хватает анимации разбивания кирпича, выпадающих бонусов, подсчёта очков, смены уровней, более качественного определения коллизий, атак же звуков. Ну, ещё можно добавить таблицу рекордов, интеграцию с Game Center, сохранение результатов в iCloud, отправку в Facebook/Twitter, ачивментов и прочих вещей, без которых современная игра не воспринимается серьёзно. Но в в моих планах не было выпуска игры в App Store, так что я всем этим и не заморачивался, благо тестовое задание этого так же не предусматривало.

PS или ЧтоЖеБылоДальше: А дальше ничего интересного не было. Сходил я на собеседование в ZeptoLab, но очень быстро завалился на незнании OpenGL на глубоком уровне. Но свой профит я получил: теперь я знаю, что сделать простенькую игру при надобности я смогу, да и вообще разобрался хоть немного в новой для себя области. Надеюсь, что теперь написать игру сможешь и ты, мой любезный читатель! За сим, позвольте откланяться, до свидания.

Автор: silvansky


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js