box-, cocos- и пицца- 2d

в 13:44, , рубрики: box2d, cocos2d, game development, Gamedev, Hackathon, ios development, iOS разработка, разработка под iOS, метки: , , , , , ,

В этой статье, я хочу поделиться с вами историей создания первой игры на iOS в нашей компании и рассказать про опыт использования прекрасного 2d графического движка — cocos2d. В рассказе мы пройдемся по некоторым техническим проблемам, с которыми нам пришлось столкнуться во время разработки игры, и расскажем про эволюцию геймплея от начала и до конца.

Финальную версию можно найти в AppStore

image

Было около 9 часов утра в Москве, когда я встретился со своими коллегами Арсением и Валентином, чтобы попотеть над идеей для внутрекорпоративного хакатона, который мы устраиваем раз в месяц в нашей компании. Никто из нас не обладал более или менее серьезным опытом в игростроении, но мы подумали, что было бы круто, разработать игру. Тем более, что все сильно устали, работая над основными проектами и хотели бы попробовать что-то новое и захватывающее.

Изначально идея, которую мы выбрали была такой: перед игроком лежал целый сочный торт, который нужно было бы безумно быстро разрезать на мелкие кусочки за небольшой промежуток времени. На обрезки должна была действовать физика, что, по задумке, придало бы игре динамичность. После недолгого поиска по просторам интернета, мы решили, что наиболее продуктивно будет делать игру на движке cocos2d (так как я и Арсений iOS разработчики) и box2d (так как он бесплатный и отлично «уживается» с cocos2d), и мы ограничили себя только одной платформой iOS.

Ядро для проекта было найдено в отличном туториале Allen Tan, тем самым, нам не пришлось погружаться в сложную реализацию алгоритмов разрезания и триангуляции. Туториал основывался на библиотеке PRKit, которая отрисовывала выпуклые текстурированные полигоны. Так же в ней находился удобный класс PRFilledPolygon, сабкласс которого соединял физическое представление полигона с его отображением. Мы решили, что будем использовать этот класс, как основной для наших будущих кусочков торта.

Воодушевленные тем, что сложнейшая часть была написана за нас, мы вздохнули с облегчением, но не надолго — первые сложности появились достаточно быстро. После запуска первого прототипа, мы вспомнили об известном ограничении box2d — не более 8 вершин для фигуры. Основываясь на туториале и вышеупомянутой библиотеке, торт должен был быть полигоном, так как box2d не умеет работать с сегментами круга (которые получались бы при разрезания абсолютно круглого торта). Итак, для того, чтобы наш торт был наиболее приближен к его реальной форме с учетом ограничения box2d, мы решили что он будет состоять из массива восьмиугольников, которые в итоге соберутся в приближенную к округлой фигуру. Это решение создало некоторые проблемы для нас, так как в туториале речь идет о разрезании тел, которые состоят ровно из одной фигуры. Однако это проблема была решена довольно быстро небольшим исправлением класса PRFilledPolygon.

Казалось, что жизнь наладилась, и наш «торт» левитировал в бескрайней черноте экрана iPad'а.

image

Однако изначальный алгоритм разрезания нужно было модифицировать для наших новый полигонов из множества фигур. После недолгого рассуждения, было решено просто увеличить максимальное количество вершин у фигуры с 8 до 24, и вернуться к полигонам, состоящим ровно из одной фигуры и тела (для некоторых проектов это решение было бы неприемлемо, но для наших целей оно вполне подходило). Профайлинг показал, что нет никаких серьезных различий в скорости работы при восьми вершинах и 24-х. Тем не менее, после увеличения количества кусочков на экране до двухсот и более, фпс резко падал до 10, что делало игру почти невозможной. Около 20% процессорного времени уходило на просчитывание столкновений, остальное — на отрисовку кусочков и их анимацию.

Решение не заставило себя ждать. Как только кусочек становится достаточно маленьким, мы просто отключали для него просчет столкновений. Несмотря на это, скорость игры все еще оставляла желать лучшего, что подтолкнуло нас к решению изменить геймплей: маленькие кусочки нужно удалять с экрана и добавлять в прогресс бар игрока. Площадь уничтоженной поверхности определяла бы качество прохождения уровня. К кусочкам применялись linear/angular damping, не позволяя им хаотически летать по экрану.

image

К этому времени Валентин нарисовал шикарную иллюстрацию торта. Она была действительно впечатляющей, но выглядела слишком реалистично для такого упрощенного процесса разрезания. Из-за этого мы решили заменить ее простой картинкой пиццы.

image

image

Однако с таким геймплеем она тоже выглядела не очень гармонично, что привело нас к решению, что резать мы будем какие-нибудь абстрактные геометрические примитивы. Так как редизайн был довольно прост, и идея отлично «ложилась» на технологию, использованную в PRFilledPolygon, мы реализовали ее очень быстро. Каждому кусочку мы добавили обводку, которую реализовали с помощью CCDrawNode, передав ему вершины полигона и цвет обводки. Это немного замедлило игру, но фпс все еще был достаточно высок, но играть стало приятнее, нежели чем со стандартной текстурой.

image

Игра стала развиваться в правильном направлении, но геймплей был все еще в зарождающемся состоянии. Определенно не хватало какого-то соревновательного элемента. Итак, мы добавили простого врага — красную точку, которую нельзя было разрезать. Отлично, но можно еще лучше. Как насчет движущихся лазеров? Сделано! Реализация была довольно простой и основывалась на просчете расстояния положения пальца юзера до врага.

image

С врагами и геймплеем покончено. После этого мы решили реализовать систему уровней, основанную на мирах. Все уровни хранились в отдельных .plist файлах, которые содержали в себе описание изначальной фигуры, позиции врагов, длительность уровня и другие параметры. Дерево игровых объектов строилось с помощью стандартного для Objective-C KVC. Например:

//......
- (void)setValue:(id)value forKey:(NSString *)key{
   if([key isEqualToString:@"position"] && [value isKindOfClass:[NSString class]]){
       CGPoint pos = CGPointFromString(value);
       self.position = pos;
   }
   else if([key isEqualToString:@"laserConditions"]){
       
       NSMutableArray *conditions = [NSMutableArray array];
       for(NSDictionary *conditionDescription in value){
           LaserObstacleCondition *condition = [[[LaserObstacleCondition alloc] init] autorelease];
           [condition  setValuesForKeysWithDictionary:conditionDescription];
           [conditions addObject:condition];
       }
       [super setValue:conditions forKey:key];
       
   }
   else{
       [super setValue:value forKey:key];
   }
}
//......

//Afterawrds the values got set with the dictionary read from the plist file:
[self setValuesForKeysWithDictionary: dictionary];

Чтобы показать меню выбора миров и уровней, мы использовали CCMenu с некоторыми расширениями: CCMenu+Layout ( позволяет расположить элементы меню на сетке) и CCMenuAdvanced (добавляет возможность скроллить наше меню). Валентин занялся проектировкой уровней, а я с Арсением погрузился в реализацию красивых эффектов.

Для визуальных эффектов, мы использовали CCBlade, который анимировал движения пальца юзера, и озвучили его эффектом, как в Star Wars. Другой эффект, который мы добавили — исчезание маленьких кусочков. Разрезанию без какого либо эффекта выглядело ужасно скучно, и поэтому мы решили удалять их с плавным изменением прозрачности и добавлять симпатичный плюсик в этот момент.

Часть с изменением прозрачности была реализована с помощью добавления протокола CCLayerRGBA к PRFilledPolygon. Для этого мы поменяли стандартную шейдер-программу, используемую в PRFilledPolygon на kCCShader_PositionTexture_uColor:

-(id) initWithPoints:(NSArray *)polygonPoints andTexture:(CCTexture2D *)fillTexture usingTriangulator: (id<PRTriangulator>) polygonTriangulator{
if( (self=[super init])) {
       //Changing the default shader program to kCCShader_PositionTexture_uColor
       self.shaderProgram = [[CCShaderCache sharedShaderCache] programForKey:kCCShader_PositionTexture_uColor];
}	
	return self;
}

и передали ей color uniform

//first we configure the color in the color setter:
colors[4] = {_displayedColor.r/255.,
                        _displayedColor.b/255.,
                        _displayedColor.g/255.,
                        _displayedOpacity/255.};

//then we pass this color as a uniform to the shader program, where colorLocation = glGetUniformLocation( _shaderProgram.program, "u_color")
-(void) draw {
   //...
	[_shaderProgram setUniformLocation:colorLocation with4fv:colors count:1];
   //...
}

Выглядело красиво, но с обводкой и новыми эффектами, фпс снова опечалился и упал до неиграбельной отметки, особенно если мы разрезали сразу много кусков на маленькие части. Погуглив, мы не нашли решение, и просто увеличили минимальную площадь кусочков, которые мы удаляли. Это повысило фпс. Эффект исчезания мы тоже убрали, а плюсики добавили в CCSpriteBatchNode (странно, что не сделали этого сразу).

image

Для звуковых эффектов мы написали небольшую обертку для Simple audio engine. Однако и тут не обошлось без проблем, мы столкнулись с неподдерживаемым форматов .wav файлов, но, к счастью, она решилась простой конвертацией файлов в поддерживаемый формат 8 или 16 bit PCM, в ином случае эффект либо вообще не проигрывался, либо слышалось потрескивание из динамика.

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

image

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

image

К этому моменту дедлайн подходил к концу и надо было уже заливать игру в AppStore. Бешено исправив последние баги, мы залили бинарный файл с надеждой, что с первого раза пройдем ревью. И, как оказалось позже, прошли без проблем.

Ну и Резульат хакатона.

image

Автор: gen4

Источник


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


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