Pull to refresh

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

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

image

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

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

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

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

image

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

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

image

К этому времени Валентин создал модель торта:

image

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

image

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

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];
   //...
}


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

image

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

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

image

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

image

К этому моменту отведенное на разработку время подошло к концу и пора было отправлять игру в AppStore. Быстро исправив последние найденные баги, мы залили бинарник с надеждой, что с первого раза пройдем ревью. И, как оказалось позже, прошли его без проблем.
Tags:
Hubs:
Total votes 33: ↑23 and ↓10+13
Comments23

Articles