Как я спас несколько жизней оптимизацией и немного о работе в Zeptolab
Привет!23derevo перед выступлением на Mobius попросил рассказать меня немного о процессе клиентской разработки в Zeptolab.
Начну с того, что мы пишем на C++ и на своём фреймворке, от любого клиентского устройства нам нужен только контекст OpenGL. Дальше мы с нуля строим свой интерфейс, свои контролы и так далее. Соответственно, чтобы взять девелопера в команду, в теории, ему достаточно знать плюсы. На практике это немного не так.
Я пришёл в Zeptolab ещё когда у нас было целых три разработчика: CTO, iOS-девелопер и Android-девелопер. До этого я учился в ШАД Яндекса и параллельно по работе пилил базу таможенной документации с возможностью Rich-форматирования, хранения файлов и изображений — в общем, своего рода MSDN, только для таможенных нужд. До сих пор она используется, и до сих пор ей только начинают находиться аналоги.Суперкрутых технологических знаний у меня не было, я занимался графикой, писал небольшие проекты на OpenGL, делал шейдеры. Этого, в целом, хватило, чтобы начать уже учиться по ветке мобильной разработки.
На мой взгляд, самое важное для кандидата (и разработчика вообще) — это общая сообразительность, технический кругозор и техническое мышление. Кстати, на собеседовании мне задавали пресловутую задачу про круглые люки. Сейчас я сам провожу собеседования, и даю похожие абстрактные задачи. Оскомину они набивают некоторым кандидатам из-за того, что список таких задач меняется редко (если давать рандом, то не будет общей метрики — будет не очень честно по отношению к кандидатам). Но, учитывая, что они «утекают» за пределы собеседований, мы обычно готовим ещё пару своих задач, чтобы проверить, не читерит ли кандидат. Если с логикой всё в порядке, то незнание каких-то синтаксических особенностей языка — проблема меньшего масштаба. Синтаксис можно выучить, паттерны программирования тоже изучаются, а вот соображать, увы, нужно сразу.
Вообще, кодер тем и отличается от разработчика, что умеет придумывать идеи решения задач. В одной известной крупной компании работает мой хороший друг. У них российский офис занят только тем, что придумывает алгоритмы, проверяет их на Питоне или C# для прототипов, а потом отдаёт результаты подразделениям в Индии и Китае. Там уже далёкие кодеры без фантазии, но с предельным педантизмом и с чисто азиатским упорством берут описанные идеи и идеально реализуют их в коде для микроконтроллеров на C++ или C под каждое устройство.
Я бы советовал тем, кто ищет работу сразу после университета, получить рейтинг в районе 2000 на Codeforces. Если вы там будет слегка жёлтым, это — высокие шансы пройти, например, в Гугл. Кроме того, вы достаточно быстро поймёте, что на первом месте — способность думать и решать уникальные задачи, когда конкретные технологии уже изучаются «по месту» до необходимого (или достаточного) уровня.
Сначала у нас был Сocos2D. Хороший фреймворк, но многие вещи нас просто не устраивали по реализации, поэтому мы начали писать свою систему. Достаточно быстро удалось реализовать на C++ очень крутую систему анимаций и хорошую подготовку ресурсов. Про анимацию мы уже рассказывали, если коротко — она готовится во Flash, потом мы парсим FLA-файлы, а потом воссоздаём те же анимации в приложении. Самым главным для нас всегда был упор на качество. В случае анимаций — это плавность: художники часто стоят за спиной у программистов и говорят, что не так. Без тренировки нельзя увидеть, где и что незначительно дёргается, но художники это точно чувствуют. Обычный человек не сможет понять, что не так, да и не всегда сможет это описать, даже если увидит. Но почувствует, часто не очень сознательно, что «шероховато». Наши художники добиваются идеальной для себя картинки, и умеют объяснять техническим языком, что надо изменить.
На конференции наши ребята расскажут, как конкретно мы добивались такого качества картинки и покажут, что под капотом фреймворка. Я расскажу про эволюцию наших технологий, про подготовку ресурсов. Очень важно контролами попадать ровно пиксель в пиксель, правильно готовить шрифты, учитывать low-res девайсы и многое другое. Опять же, конкретные примеры я покажу на конференции.
Для меня, пожалуй, наибольший кайф — это встать за спиной художника и смотреть, как он создает шедевр из ничего. Иногда они так же смотрят на нас и пытаются понять, что мы делаем. Нам в разработке кажется, что хороших художников на рынке мало, и найти таких крутых нереально сложно. Им кажется, что разработчиков, понимающих, что им нужно, нереально мало.
Кстати, при всём их искренне гуманитарном образовании, проблем с технической частью замечено ни разу не было. Задачи формулируют отлично, общую архитектуру представляют. Был даже такой забавный случай: сфотографировали для контеста на Codeforces мы художника в роли разработчика, для прикола. Очки, сложное лицо, мысль. Так вот, он после этого внезапно стал писать код на JavaScript. Сначала были совсем простые макросы для Фотошопа и для Флеша. Потом он за несколько месяцев, фактически, прошёл всю историю эволюции разработки, открывая для себя новые и новые возможности. Помню, в какой-то момент он подошёл и начал достаточно коряво объяснять концепцию, которая бы очень помогла ему писать сложные скрипты: через некоторое время я понял, что он хочет ставить breakpoint’ы и смотреть значения переменных. Сам дошёл до использования assert’ов. До этого над его кодом мы иногда украдкой смеялись: выражения он мешал в одну строчку, без отступов, выглядело действительно немного дико. А потом как-то незаметно стал делать очень крутые скрипты. Сейчас думаем, кого ещё сфотографировать со сложным лицом.Но вернусь к фреймворку. У нас довольно много рутины, в частности, связанной с его постоянными доработками. Фреймворк развивается, появляется новое железо, новые требования, хочется своевременно разбираться с легаси-кодом. Из последних крупных задач, например, нам нужна была своя система частиц. Посмотрели, что есть в Unity, художники говорят — мегакруто, но нужно ещё вот это, вот это и вот так.
В результате задача свелась не только к написанию генератора частиц, но и к реализации удобного интерфейса. У нас есть несколько слоёв эмиссии, и частицы двигаются по разным законам. Произвольная формула для каждой очень сильно нагружала бы клиентские устройства (пришлось бы, фактически, парсить каждую отдельно), а общая была недостаточно гибкой для реализации задумок художников. Решили математикой — вывели некоторую общую формулу для каждой частицы, где изменением коэффициентов можно запускать хоть параболу, хоть синусоиду. И не тормозит, и есть визуальное богатство.
Раз в две недели мы учим сами себя. Парни (а сейчас у нас в команде 21 разработчик) изучают что-то новое, чем ещё не пользовались на других проектах, или же чего нет в других компаниях. Собирают всех, рассказывают, что нашли интересного. Это могут самые разные темы: начиная от того, как сделать загрузку субъективно быстрой для пользователя и заканчивая быстрым блюром фона за попапом (как сделано в King of Thieves). К нам регулярно приезжал в офис Михаил Мирзаянов (он, кстати, тренировал нашу сборную, занявшую первое место на ACM/ICPC). Прочитал 3 блока крутейших лекций по алгоритмам и структурам данных, показывал редкие малоизвестные структуры и задачи (например, про дерево отрезков, которое он же и независимо открыл одним из первых в мире и первый в России). Как обучение, мы ходили на трёхдневный тренинг Скотта Майерса (это который написал книгу «Effective Modern C++»).
Из примеров задач — в 2013 была опубликована достаточно большая статья по решениям широко известной NP-трудной задачи по упаковке текстурных атласов. По результатам одного из конкурсов на Codeforces к нам пришёл сильный алгоритмист. Прочитал статью, долго думал, потом написал свой алгоритм, улучшенную версию общеизвестного, который мы сразу же проверили на одном из наиболее сложно упаковываемых атласов. Если брать за 100% идеальную упаковку, то наш предыдущий алгоритм давал результат больше 120%, а новый на этом же наборе данных стал показывать 104%. На практике это означает снижение потребление памяти на мегабайты.
Вообще, на 500 миллионов инсталлов такие вещи выглядят очень забавно. Например, наша самая первая система хранения и загрузки изображений оперировала PNG-файлами, и на загрузку уровня на тестовом устройстве уходило около 15 секунд. Отпрофилировали, разобрались — большую часть времени занимало декодирование PNG. Я переписал этот код (потребовался новый свой внутренний формат хранения графики) — и на том же тестовом устройстве загрузка стала занимать 6 секунд. Сэкономлено 9 секунд, — мы раскатали новую систему хранения на все свои игры. Если считать 20 загрузок игры за некоторый базовый показатель, мне кажется, спасено этим минимум полсотни жизней. Дальше этот механизм ускорили еще на 20–30% по совету новичка, который то же самое делал на старом месте работы, потому что в какой-то момент несложные вычисления на процессоре перестали быть узким местом системы загрузки, все стало упираться в скорость чтения из хранилища. Доработали свой формат.По оптимизации вообще достаточно много работы. Наши игры работают даже на старом железе, фреймворк поддерживает iOS 4.3 (сейчас iOS 5: изначально из-за партнёрского кода, потом мы стали использовать libc++, которая тоже доступня начиная с iOS 5, во второй версии фреймворка). Разработку совсем новых приложений и эксперименты мы делаем под топовые модели, потому что к концу разработки они как раз станут самым массовым устройством —, но «старичков» не забываем. С тем же «Cut the Rope» большая часть выпуска — это контентные апдейты. Старый код не портим. Новые игры уже куда богаче визуально, но и требования к железу у них выше.
Прототипирование у нас делается очень быстро, быстрее, чем во многих студиях. Геймдизайнер выдаёт концепт, затем за 1–2 дня кто-нибудь из разработчиков делает «работу мечты» — собирает с нуля прототип без графики, на примитивах. Если прыжки шарика и квадрата после этого штырят геймдизайнера — идёт в работу дальше. Естественно, прототипов куда меньше, чем обычных задач, но мы стараемся, чтобы каждый в команде рано или поздно написал свой.
Опять же, естественно, сразу это делаем на готовом проекте-заготовке, где есть все базовые вещи. Для людей, приходящих из разработки нативных мобильных приложений, это просто другой мир — стандартных контроллеров нет, подготовка ресурсов своя, вообще всё своё и даже не особо привязано к платформе. Те, кто работал с Unity — им интереснее копаться «под капотом», видя реализацию некоторых вещей, которые там сделать сложно. С Кокосом, в целом, на высоком уровне параллели есть, но всё равно интересно разобрать игру и посмотреть, как она работает внутри.
Напоследок — мой небольшой спор с друзьями. Ниже под спойлером 5 образцов кода с тестовых заданий от разных людей. Код публикуется с согласия всех кандидатов. (осторожно, исходники довольно большие)
App.cpp // // App.cpp // Asteroids // // Created by xxxx // //
#include
#include «App.h» #include «RenderCommandPolygonConvex.h» #include «Vec2.h» #include «Color.h» #include «GameMap.h» #include «Camera.h» #include «MapDrawObjectPolygon.h» #include «MapObjectMovable.h» #include «IMovable.h» #include «MovableObjectTouch.h» #include «MapObjectEmitter.h» #include «EmitterLineContinuous.h» #include «MovableInDirection.h» #include «MapObjectHero.h» #include «MapObjectAsteroid.h» #include «MapObjectDebris.h»
const float LOGIC_MAP_WIDTH = 100; const float GAMEPLAY_ACCELERATION = 0.003;
namespace
{
void initAsteroidsEmitters (GameMapPtr gameMap, float logicMapWidth, std: vector
MapObjectEmitterPtr emitterMapObject3(new MapObjectEmitter ()); EmitterLineContinuousPtr emitter3(new EmitterLineContinuous (Vec2(0, 0), Vec2(logicMapWidth, 0), Vec2(0, 1), 3, 20, -1, gameMap)); emitter3→setParticlesMapObject (MapObjectDebrisPtr (new MapObjectDebris ())); emitterMapObject3→setEmitter (emitter3); asteroidEmitters.push_back (emitter3); gameMap→addMapObject (emitterMapObject3, Vec2(0, -10), 0); MapObjectEmitterPtr emitterMapObject4(new MapObjectEmitter ()); EmitterLineContinuousPtr emitter4(new EmitterLineContinuous (Vec2(0, 0), Vec2(logicMapWidth, 0), Vec2(0, 1), 1, 40, -1, gameMap)); emitter4→setParticlesMapObject (MapObjectAsteroidPtr (new MapObjectAsteroid ())); emitterMapObject4→setEmitter (emitter4); asteroidEmitters.push_back (emitter4); gameMap→addMapObject (emitterMapObject4, Vec2(0, -10), 0); } }
App: App () : time_(0) {
}
void App: updateAndRender (float dtSec, std: vector
bool App: touch (const std: vector
void App: update (float dtSec) { if (gameMap_) gameMap_→update (dtSec); tryRespawnHero (); updateGameplayAcceleration (); time_+= dtSec; }
void App: resetGameplay () { time_ = 0; :: initAsteroidsEmitters (gameMap_, LOGIC_MAP_WIDTH, asteroidEmitters_); }
void App: tryRespawnHero () { if (! hero_ || hero_→isReadyToDestruct ()) { if (! gameMap_→hasObjectOfType (MAP_OBJECT_HERO_DEBRIS)) { resetGameplay (); hero_ = MapObjectHeroPtr (new MapObjectHero (Rect (0, 0, logicMapSize_.x, logicMapSize_.y))); gameMap_→addMapObject (hero_, Vec2(50, logicMapSize_.y — 10), 0); } } }
void App: setScreenSize (int screenW, int screenH) { screenSize_ = Vec2(screenW, screenH); float logicCellSize = screenW/LOGIC_MAP_WIDTH; logicMapSize_ = Vec2(screenW/logicCellSize, screenH/logicCellSize); CameraPtr camera = CameraPtr (new Camera (logicCellSize, logicCellSize)); gameMap_ = GameMapPtr (new GameMap (Size (logicMapSize_.x, logicMapSize_.y), camera)); gameMap_→setLiveAreaRect (Rect (-logicMapSize_.x/2, -10, logicMapSize_.x*2, logicMapSize_.y + 20)); resetGameplay (); }
void App: collectRenderData (std: vector
void App: updateGameplayAcceleration ()
{
for (auto emitter: asteroidEmitters_)
{
emitter→setSpeedParticles (emitter→getSpeedParticles () + time_*GAMEPLAY_ACCELERATION);
}
}
game.cpp
#include «stdafx.h»
#include
const float Pi=3.14159265358; float winwid=400; float winhei=400; bool game_end=0; /////bullet//// float dx=0, dy=0; float bull_speed=6; float betta=0; bool fl1=0, fl2=0; /////ship//// float speed=0; float angle=0; float acsel=0; /////asteroid///// float ast_size=50; float aster_speed=3; /////0-rand//// int kol_aster=0;
class bullet { public: float dxb; float dyb; float angleb; bullet () { dxb=dx; dyb=dy; angleb=betta; } };
class asteroid
{
public:
float anglea;
float dx;
float dy;
float depth;
int n;
int i_big;
int ifsmall;
vector
};
void asteroid: create_small (int i, int j, bool param, float depth1, float dx1, float dy1) { ifsmall=0; int size=ast_size/2; depth=depth1+(j+2)*1.0/(8.0*(kol_aster)); dx=dx1; dy=dy1; i_big=i;
/////////////////////////////////////////////////
int quat=rand ()%4; int n1=rand ()%2+1; int n2=rand ()%2+1; int n3=rand ()%2+1; int n4=rand ()%2+1; n1=n2=n3=n4=1; n=n1+n2+n3+n4; double xi, yi; anglea=rand ()%360;
x.clear (); y.clear ();
for (int i=0; i yi=rand ()%(size/2)-size/2;
x.push_back (xi);
y.push_back (yi);
}
////////////////////////////////////////////////// } void asteroid: create (int kol_exist, bool param)
{
int size=ast_size;
int quat=rand ()%4;
int n1=rand ()%2+1;
int n2=rand ()%2+1;
int n3=rand ()%2+1;
int n4=rand ()%2+1;
n1=n2=n3=n4=1;
n=n1+n2+n3+n4;
double xi, yi;
anglea=rand ()%360;
i_big=kol_exist; ifsmall=1;
depth=(float)(kol_exist)/((kol_aster));
dx=rand ()%(int)winwid -winwid/2;
dy=rand ()%(int)winhei -winhei/2;
if (quat==0) dy=-ast_size-winhei/2;
if (quat==1) dy=ast_size+winhei/2;
if (quat==2) dx=-ast_size-winwid/2;
if (quat==3) dx=ast_size+winwid/2; x.clear ();
y.clear (); for (int i=0; i yi=rand ()%(size/2)-size/2;
x.push_back (xi);
y.push_back (yi);
}
} vector void destroy_small_ast (int i)
{
//////??? ??? 4???-??? ??? ???/////
bool create_big=1;
float up_boarder=(float)(veca[i].i_big)/((kol_aster));
float down_boarder=(float)(veca[i].i_big)/((kol_aster));
asteroid a_big;
a_big.create (veca[i].i_big,1);
if (i>0) if (veca[i-1].depth>down_boarder) create_big=0;
if (i void destroy_aster (float dep)
{
dep=1–2*dep;
for (int i=0; i if (abs (dep-veca[i].depth)<0.0001)
{if(veca[i].ifsmall==1)
{
veca.resize(veca.size()+4);
for(int j=0;j<4;j++)
veca[veca.size()-j-1].create_small(i,j,0,veca[i].depth,veca[i].dx,veca[i].dy);
veca.erase(veca.begin()+i);
break;
}
else {destroy_small_ast( i);break;}
}
}
} void shoot ()
{
float depth[5];
for (int i=0; i void aster_draw ()
{
glColor3f (0.5f,1.0f,1.0f);
glLoadIdentity ();
for (int i=0; i void asteroidsinit ()
{
int k;
k=rand ()%6+4;
if (kol_aster!=0) k=kol_aster;
else kol_aster=k;
veca.resize (k);
for (int i=0; i void draw_ship ()
{ float depth[6];
float dx1, dx2, dy1, dy2; ////////??? ??? ? ???/////
if ((dx /////////???//////
glColor3f (0.8f,0.0f,0.8f);
glLoadIdentity ();
glTranslatef (dx, dy,0.0f);
glRotatef (betta,0.0f,0.0f,1.0f);
glBegin (GL_TRIANGLES);
glVertex3f (-10.0f,-10.0f, 1.0f);
glVertex3f (-10.0f,10.0f, 1.0f);
glVertex3f (0.0f,0.0f, 1.0f);
if (fl2==1){
glVertex3f (-10.0f,-3.0f, 1.0f);
glVertex3f (-10.0f,3.0f, 1.0f);
glVertex3f (-15.0f,0.0f, 1.0f);
}
glEnd ();
/////////??? ???-??? ???/////////
if ((depth[0]!=1)||(depth[1]!=1)||(depth[2]!=1))
game_end=1;
} void display ()
{
glClearDepth (1.0f);
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
aster_draw ();
draw_ship ();
shoot ();
glutSwapBuffers ();
} void Timer (int)
{
acsel--;
if (speed>10) speed=10;
if (fl1==1) {angle=betta; fl1=0;}
if (acsel==0) {fl2=0;}
dx=dx+speed*cos (Pi*angle/180.0);
dy=dy+speed*sin (Pi*angle/180.0);
if (speed>0)speed=speed-0.1;
else speed=0;
display ();
if (game_end==0) glutTimerFunc (50, Timer,0);
} void Initialize ()
{
dx=0;
dy=0;
vecb.empty ();
angle=betta=speed=0;
glClearColor (0, 0, 0.0, 1.0);
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
glOrtho (-winwid/2, winwid/2, winhei/2, -winhei/2, -1, 1);
glMatrixMode (GL_MODELVIEW);
glEnable (GL_DEPTH_TEST);
glDepthFunc (GL_LEQUAL);
float depth[5];
glClearDepth (1.0f); // ??? ??? ??? ???
glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
asteroidsinit ();
glutTimerFunc (500, Timer,0);
} void keyboard (unsigned char key, int x, int y)
{
if (key=='w') {fl1=1; speed++; fl2=1; acsel=10;}
if (key=='d') {betta+=7;}
if (key=='a') betta-=7;
if (key==' ') {bullet b1; vecb.push_back (b1);}
if (key=='r') {if (game_end==1) {game_end=0; Initialize ();}} } int main (int argc, char **argv)//??? ???
{ glutInit (&argc, argv);
glutInitDisplayMode (GLUT_DEPTH |GLUT_DOUBLE | GLUT_RGB);
glutInitWindowSize (winwid, winhei);
glutInitWindowPosition (200, 200);
glutCreateWindow («Powder Toy»);
Initialize ();
glutDisplayFunc (display);
glutKeyboardFunc (keyboard);
glutMainLoop ();
}
game.cpp
#include «game.h»
#include «logic.h» Game: Game (unsigned width, unsigned height)
: _asteroids (std: vector _halfHeight = GAME_HEIGHT;
_halfWidth = _aspectRatio * _halfHeight; _playerPoints = new std: vector Shot: SetStaticPoints (_shotsPoints);
Boom: SetStaticPoints (_boomPoints);
Random: Init (&_halfWidth, &_halfHeight); _player = new Player (_playerPoints, 0.0f, 0.0f);
} Game::~Game () {
delete _player;
delete _render;
for (Shot *item: _shots)
delete item;
for (Boom *item: _booms)
delete item;
for (AsteroidFamily *item: _asteroids)
delete item;
delete _playerPoints;
delete _shotsPoints;
delete _boomPoints;
} void Game: Refresh () {
_controls→Refresh ();
if (_score >= _livesBonus) {
_livesBonus += 10000;
_player→SetLives (_player→GetLives () + 1);
}
if (_isAsteroidsEmpty) {
_level++;
for (AsteroidFamily *item: _asteroids)
delete item;
_asteroids.clear ();
for (unsigned i = 0; i < (_level + 1) * 2; ++i)
_asteroids.push_back(Random::GenerateAsteroidFamily());
_isAsteroidsEmpty = false;
}
_isAsteroidsEmpty = true;
std::chrono::high_resolution_clock::time_point now = std::chrono::high_resolution_clock::now();
auto time_span = std::chrono::duration_cast if (_controls→GetHyperspace ()) {
if (!_gameOver) {
Random: ChangePlayerCoords (_player);
_controls→SetHyperspace (false);
} else {
if (std: chrono: duration_cast if (_player→GetIsGhost () && !_gameOver) {
if (!_player→GetIsRendering ()) {
if (std: chrono: duration_cast if (_player→GetLives () <= 0 && !_gameOver) {
_gameOver = true;
_gameOverTimepoint = now;
_player->SetIsGhost (true);
_player→Stop ();
} if (_player→GetIsRendering () && !_gameOver) {
RefreshObjectCoord (_player);
_player→Refresh (_controls→GetAngle (), _controls→GetAcceleration ());
if (_controls→GetShoot ()) {
if (std: chrono: duration_cast for (auto it = _shots.begin (); it!= _shots.end ();) {
RefreshObjectCoord (*it);
(*it)→Refresh (_timeMultiplier);
if ((*it)→GetDistance () >= std: min (_halfHeight, _halfWidth) * 2 — 1.2f) {
delete (*it);
it = _shots.erase (it);
} else {
++it;
}
} for (AsteroidFamily *item: _asteroids) {
if (item→GetLarge ()→GetIsRendering ()) {
_isAsteroidsEmpty = false;
RefreshObjectCoord (item→GetLarge ());
item→GetLarge ()→Refresh ();
if (!_player→GetIsGhost ()) {
if (isCollision (_player, item→GetLarge ())) {
item→DestroyLarge ();
_score += SCORE_LARGE;
ProcessCollision (_player, item→GetLarge ());
}
}
if (item→GetLarge ()→GetIsRendering ())
for (auto it = _shots.begin (); it!= _shots.end ();) {
if (isCollision (*it, item→GetLarge ())) {
delete (*it);
it = _shots.erase (it);
item→DestroyLarge ();
_booms.push_back (new Boom (item→GetLarge ()→GetCoord ().x, item→GetLarge ()→GetCoord ().y));
_score += SCORE_LARGE;
} else {
++it;
}
}
} else {
if (item→GetFirstSmall ()→GetIsRendering ()) {
_isAsteroidsEmpty = false;
RefreshObjectCoord (item→GetFirstSmall ());
item→GetFirstSmall ()→Refresh ();
if (!_player→GetIsGhost ()) {
if (isCollision (_player, item→GetFirstSmall ())) {
item→GetFirstSmall ()→SetIsRendering (false);
_score += SCORE_SMALL;
ProcessCollision (_player, item→GetFirstSmall ());
}
}
}
if (item→GetSecondSmall ()→GetIsRendering ()) {
_isAsteroidsEmpty = false;
RefreshObjectCoord (item→GetSecondSmall ());
item→GetSecondSmall ()→Refresh ();
if (!_player→GetIsGhost ()) {
if (isCollision (_player, item→GetSecondSmall ())) {
item→GetSecondSmall ()→SetIsRendering (false);
_score += SCORE_SMALL;
ProcessCollision (_player, item→GetSecondSmall ());
}
}
}
for (auto it = _shots.begin (); it!= _shots.end ();) {
bool isFirstCollision = false, isSecondCollision = false;
if (item→GetFirstSmall ()→GetIsRendering ())
isFirstCollision = isCollision (*it, item→GetFirstSmall ());
if (item→GetSecondSmall ()→GetIsRendering ())
isSecondCollision = isCollision (*it, item→GetSecondSmall ());
if (isFirstCollision || isSecondCollision) {
delete (*it);
it = _shots.erase (it);
if (isFirstCollision) {
item→GetFirstSmall ()→SetIsRendering (false);
_booms.push_back (new Boom (item→GetFirstSmall ()→GetCoord ().x, item→GetFirstSmall ()→GetCoord ().y));
_score += SCORE_SMALL;
}
if (isSecondCollision) {
item→GetSecondSmall ()→SetIsRendering (false);
_booms.push_back (new Boom (item→GetSecondSmall ()→GetCoord ().x, item→GetSecondSmall ()→GetCoord ().y));
_score += SCORE_SMALL;
}
} else {
++it;
}
}
}
} for (auto it = _booms.begin (); it!= _booms.end ();) {
(*it)→Refresh (_timeMultiplier);
if ((*it)→GetDuration () >= BOOM_MAX_DURATION) {
delete (*it);
it = _booms.erase (it);
} else {
++it;
}
}
} void Game: RefreshObjectCoord (Object *object) {
object→SetCoord (object→GetCoord ().x + object→GetVelocity ().x * _timeMultiplier,
object→GetCoord ().y + object→GetVelocity ().y * _timeMultiplier);
if (object→GetCoord ().x <= -_halfWidth) object->SetCoord (object→GetCoord ().x + _halfWidth * 2, object→GetCoord ().y);
else if (object→GetCoord ().x >= _halfWidth) object→SetCoord (object→GetCoord ().x — _halfWidth * 2,
object→GetCoord ().y);
if (object→GetCoord ().y <= -_halfHeight) object->SetCoord (object→GetCoord ().x,
object→GetCoord ().y + _halfHeight * 2);
else if (object→GetCoord ().y >= _halfHeight) object→SetCoord (object→GetCoord ().x,
object→GetCoord ().y — _halfHeight * 2);
} void Game: Render () {
Refresh ();
_render→Clear (); _render→RenderControls (_controls); _render→SetColor (OBJECTS_COLOR);
if (_player→GetIsRendering () && !_gameOver) {
if (_player→GetIsGhost ())
_render→SetColor (PLAYER_GHOST_COLOR);
_render→RenderPlayer (_player);
} if (_gameOver)
_render→SetColor (OBJECTS_GAMEOVER_COLOR);
else
_render→SetColor (OBJECTS_COLOR); for (AsteroidFamily *item: _asteroids) {
if (item→GetLarge ()→GetIsRendering ()) {
_render→RenderAsteroid (item→GetLarge ());
} else {
if (item→GetFirstSmall ()→GetIsRendering ()) {
_render→RenderAsteroid (item→GetFirstSmall ());
}
if (item→GetSecondSmall ()→GetIsRendering ()) {
_render→RenderAsteroid (item→GetSecondSmall ());
}
}
} for (Shot *item: _shots) {
_render→RenderShot (item);
} _render→SetColor (BOOM_COLOR);
for (Boom *item: _booms) {
_render→RenderBoom (item);
} _render→SetColor (TEXT_COLOR);
if (!_gameOver) {
_render→RenderScoreAndLives (_score, _player→GetLives ());
} else {
_render→RenderGameOver (_score);
}
} bool Game: isCollision (Player *player, Asteroid *asteroid) {
const std: vector bool Game: isCollision (Shot *shot, Asteroid *asteroid) {
if (Logic: IsInside (*(asteroid→GetPoints ()), shot→GetCoord ()))
return true;
else
return false;
} bool Game: TestAABB (Player *player, Asteroid *asteroid) {
return (player→GetSizes ()[0] < asteroid->GetSizes ()[1] && player→GetSizes ()[1] > asteroid→GetSizes ()[0] &&
player→GetSizes ()[2] < asteroid->GetSizes ()[3] && player→GetSizes ()[3] > asteroid→GetSizes ()[2]);
} void Game: ProcessCollision (Player *player, Asteroid *asteroid) {
_booms.push_back (new Boom (asteroid→GetCoord ().x, asteroid→GetCoord ().y));
player→SetIsRendering (false);
player→SetIsGhost (true);
player→SetLives (player→GetLives () — 1);
player→SetCoord (0.0f, 0.0f);
player→SetDeadTime (std: chrono: high_resolution_clock: now ());
} void Game: Resize (float width, float height) {
_aspectRatio = (float)width / (float)height;
_halfWidth = _aspectRatio * _halfHeight;
_width = width;
_height = height;
_render→Resize ();
} Controls *Game: GetControls () {
return _controls;
} bool Game: GetIsPaused () {
return _isPaused;
} void Game: SetIsPaused (bool isPaused) {
_isPaused = isPaused;
}
Game.cpp
#include #include Game: Game () {
isLevelRunning = true; ResetLogic ();
RequestRestart (); Renderer: InitInternals ();
Controls: Init ();
Score: Init ();
} Game& Game: Get () {
static Game instance;
return instance;
} void Game: Restart () {
objects.clear ();
Score: OnRestart ();
ResetLogic ();
GameObject: Create //Внутри управляем рестартом, а наружу выдаем текущее состояние уровня (false — на паузе)
bool Game: IsLevelRunning (float dt) {
if (wantRestart) {
if (restartTimer < 0.0) {
if(isLevelRunning) {
Restart();
wantRestart = false;
}
} else {
restartTimer -= dt;
}
}
return isLevelRunning;
} void Game: Update () {
float deltaTime = timer.Tick (); if (IsLevelRunning (deltaTime)) {
for (auto& go: objects) {
go→Update (deltaTime);
} DetectCollisions (deltaTime); DestroyRequestedObjects (); //Удаление объектов предполагается только здесь
} Renderer: Draw ();
} void Game: OnGLInit () {
Renderer: InitGLContext ();
} void Game: OnResolutionChange (int w, int h) {
Renderer: OnResolutionChange (w, h);
Controls: Resize ();
Score: Resize ();
} GameObject& Game: AddGameObject (std: unique_ptr void Game: DestroyRequestedObjects () {
for (auto i = objects.begin (); i!= objects.end ();) {
if ((*i)→isDestructionRequested ()) {
i = objects.erase (i);
} else {
++i;
}
}
} void Game: DetectCollisions (float dt) {
for (auto a = objects.begin (); a!= objects.end (); ++a) {
for (auto b = std: next (a); b!= objects.end (); ++b) { //Для каждой пары объектов
GameObject& ra = **a;
GameObject& rb = **b;
if (CollisionMask (ra, rb)) { //Требуется ли обработка столкновения
if (DetectCollision (ra, rb, dt)) { //Базовый алгоритм
if (RefineCollision (ra, rb, dt)) { //Более точный
ra.OnCollision (rb);
rb.OnCollision (ra);
}
}
}
}
}
}
bool Game: CollisionMask (const GameObject& a, const GameObject& b) {
//Если один из объектов логически уже уничтожен, то он не сталкивается с другими (return false)
return!(a.isDestructionRequested () || b.isDestructionRequested ()) &&
//Если ни одному из объектов не требуется обработка столкновения с другим, то возвращаем false
(a.CollisionMask (b.getStaticType ()) || b.CollisionMask (a.getStaticType ()));
} //Обнаружение столкновений по радиусу окружности, описывающей модель объекта
//В непрерывном случае радиус расширяется на расстояние, которое объекты могут пройти за время dt
bool Game: DetectCollision (const GameObject& a, const GameObject& b, float dt) {
if (Constant: continuousCollisions) {
return (a.getPosition () — b.getPosition ()).getLength () <
a.getRadius() + b.getRadius() + (a.getVelocity().getLength() + b.getVelocity().getLength()) * dt;
} else {
return (a.getPosition() - b.getPosition()).getLength() < a.getRadius() + b.getRadius();
}
} bool Game: RefineCollision (const GameObject& a, const GameObject& b, float dt) {
if (Constant: refineCollisions) {
const Model& am = a.getModel ();
const std: vector //Обработка для каждой пары отрезков, из которых состоит модель объекта
for (int i = 0; i < ai.size(); i += 2) {
Vec2 a0(i, av, ai);
Vec2 at = Vec2(i + 1, av, ai) - a0;
for(int j = 0; j < bi.size(); j += 2) {
Vec2 b0(j, bv, bi);
Vec2 bt = Vec2(j + 1, bv, bi) - b0;
if(Constant::continuousCollisions ?
//Алгоритм считает, что отрезки передаются в момент времени t0,
//но наши отрезки уже в t0+dt, поэтому передаем скорости со знаком -
MovingSegmentCollision(a0, at, aBackVel, b0, bt, bBackVel, dt) :
SegmentCollision(a0, at, b0, bt)) return true;
}
}
return false;
} else {
return true;
}
} bool Game: SegmentCollision (Vec2 p, Vec2 r, Vec2 q, Vec2 s) {
//http://stackoverflow.com/a/565282/2502024
float det = Vec2:: CrossProd2D (r, s);
if (fabs (det) > Constant: smallNumber) {
Vec2 diff = q — p;
float f = Vec2:: CrossProd2D (diff, s / det);
float g = Vec2:: CrossProd2D (diff, r / det);
return f >= 0 && f <= 1 && g >= 0 && g <= 1;
}
return false;
} bool Game: MovingSegmentCollision (Vec2 p, Vec2 r, Vec2 vp, Vec2 q, Vec2 s, Vec2 vq, float dt) {
float det = Vec2:: CrossProd2D (r, s);
if (fabs (det) > Constant: smallNumber) {
const Vec2 v = vq — vp;
const Vec2 diff = q — p;
//Расширение предыдущего алгоритма с учетом:
//q = q0 + v*t, t in [0, dt]
//(v.x * s.y — v.y * s.x) * t + ((q.x — p.x) * s.y — (q.y — p.y) * s.x) //Точки пересечения f и g из SegmentCollision теперь зависят от t.
//Отрезки пересекаются в точке t из [0, dt], если найдется такое t,
//что f и g одновременно лежат в [0, 1]. Т.е. решаем 3 пары неравенств относительно t
auto getInequation = [=](Vec2 dir)→std: tuple float ls, rs, lr, rr;
//ls <= t <= rs
std::tie(ls, rs) = getInequation(s);
//lr <= t <= rr
std::tie(lr, rr) = getInequation(r); //одновременно с 0 <= t <= dt
float mx = std::max(0.f, std::max(ls, lr));
float mn = std::min(dt, std::min(rs, rr));
return mx <= mn;
}
return false;
} void Game: RequestRestart (float t) {
wantRestart = true;
restartTimer = t;
} void Game: Pause () {
isLevelRunning = false;
Controls: onPause ();
} void Game: Resume () {
isLevelRunning = true;
Controls: onResume ();
timer.Tick ();
} void Game: SetPlayerPos (const Ship& player) {
playerPos = player.getPosition ();
} Vec2 Game: GetPlayerPos () {
return playerPos;
} void Game: AddPoints (int pointsToAdd) {
Score: AddPoints (pointsToAdd);
} void Game: DecAsteroidCount (const Asteroid& a) {
asteroidCount--;
if (asteroidCount == Constant: asteroidUfoCount * 2 && ! isUfoPresent) {
GameObject: Create //Увеличиваем на 2, т.к. asteroidCount считаем по половинам большого астероида
void Game: IncAsteroidCount (const Asteroid& a) {
asteroidCount += 2;
} void Game: ResetLogic () {
asteroidCount = 0;
isUfoPresent = false;
} void Game: SpawnAsteroids (int n) {
for (int i = 0; i < n; ++i) {
GameObject::Create //Создаем астероид так, чтобы сразу не убить игрока (с учетом зацикленности игровых координат)
Transform Game: GetSpawnPosition () {
std: uniform_real_distribution Transform Game: GetUfoSpawn () {
std: uniform_real_distribution void Game: OnUfoCreated (const UFO& u) {
isUfoPresent = true;
} void Game: OnUfoDestroyed (const UFO& u) {
isUfoPresent = false;
}
model_handler.cpp
#include «model_handler.h» #include using namespace model; namespace {
const double tickTime = 40.00; const unsigned int asteroidNumber = 8;
const float projectileSpeed = 10.0; const float smallAsteroidRadiusK = 1.5;
const float minLargeAsteroidRadius = 35.0;
const float maxLargeAsteroidRadius = minLargeAsteroidRadius * smallAsteroidRadiusK — 1.0;
const float minAsteroidSpeed = 1.5;
const float maxAsteroidSpeed = 6.5; const float explosionK = 50000.0; } ModelHandler: ModelHandler (float worldWidth, float worldHeight):
_isGameOver (false),
_worldWidth (worldWidth),
_worldHeight (worldHeight),
_tickTime (0),
_ship (ShipPtr (new Ship (Point (worldWidth / 2.0, worldHeight / 2.0)))) { srand (time (0));
} void ModelHandler: newGame () {
_asteroids.clear ();
_projectiles.clear ();
_ship.reset (new Ship (Point (_worldWidth / 2.0, _worldHeight / 2.0)));
_isGameOver = false;
} bool ModelHandler: isGameOver () const {
return _isGameOver;
} void ModelHandler: update (double deltaTime) {
if (this→isGameOver ()) return; _tickTime += deltaTime;
while (_tickTime > tickTime) { if (_asteroids.size () < ::asteroidNumber) {
this->addAsteroid ();
} for (ObjectPtr& obj: this→allObjects ()) {
obj→move ();
} this→checkObjects (&_asteroids);
this→checkObjects (&_projectiles); if (! this→withinBoundaries (_ship)) {
this→removeShip ();
} _tickTime -= tickTime;
}
} ShipPtr ModelHandler: ship () const {
return _ship;
} void ModelHandler: removeShip () {
_isGameOver = true;
} void ModelHandler: removeAsteroid (const AsteroidPtr& asteroid) {
if (asteroid→collisionRadius () >= :: minLargeAsteroidRadius) { const model: Vector v1(asteroid→velocity ()
+ model: Vector (-asteroid→velocity ().y, asteroid→velocity ().x));
const model: Vector v2(asteroid→velocity ()
+ model: Vector (asteroid→velocity ().y, -asteroid→velocity ().x)); this→addAsteroid (asteroid, v1);
this→addAsteroid (asteroid, v2);
}
_asteroids.remove (asteroid);
} void ModelHandler: addAsteroid (const AsteroidPtr& asteroid) {
_asteroids.push_back (asteroid);
} std: list std: list void ModelHandler: removeProjectile (const ProjectilePtr& projectile) {
_projectiles.remove (projectile);
} void ModelHandler: addProjectile () {
Vector projectileSpeed = _ship→direction () * :: projectileSpeed + ship ()→velocity ();
_projectiles.push_back (ProjectilePtr (new Projectile (_ship→point (), projectileSpeed)));
} void ModelHandler: processHit (const ProjectilePtr& projectile, const AsteroidPtr& asteroid) {
if (asteroid→collisionRadius () >= :: minLargeAsteroidRadius) {
const Vector ox = asteroid→velocity ().normaVector ();
Vector explosionVector (ox.y, -ox.x); if (ox * asteroid→velocity () < 0) {
explosionVector = explosionVector * (-1.0);
} const Vector v1(asteroid→velocity () + model: Vector (-asteroid→velocity ().y, asteroid→velocity ().x)
+ explosionVector * (-:: explosionK / asteroid→mass ()));
const Vector v2(asteroid→velocity () + model: Vector (asteroid→velocity ().y, -asteroid→velocity ().x)
+ explosionVector * (:: explosionK / asteroid→mass ())); this→addAsteroid (asteroid, v1);
this→addAsteroid (asteroid, v2);
}
_projectiles.remove (projectile);
_asteroids.remove (asteroid);
} std: list void ModelHandler: addAsteroid () { const float r = common: rangeRand (:: minLargeAsteroidRadius, :: maxLargeAsteroidRadius); Point point;
bool isCorrect = false;
while (! isCorrect) {
point = randPoint®;
isCorrect = true;
for (const AsteroidPtr& asteroid: _asteroids) {
const float d = model: distance (asteroid→point (), point);
if (d < (asteroid->collisionRadius () + r)) {
isCorrect = false;
break;
}
}
} const float distToCenter = :: distance (point.x, point.y, _worldWidth / 2.0, _worldHeight / 2.0);
const float vx = (_worldWidth / 2.0 — point.x) / distToCenter
* common: rangeRand (:: minAsteroidSpeed, :: maxAsteroidSpeed);
const float vy = (_worldHeight / 2.0 — point.y) / distToCenter
* common: rangeRand (:: minAsteroidSpeed, :: maxAsteroidSpeed); _asteroids.push_back (AsteroidPtr (new Asteroid (point, r, Vector (vx, vy))));
} void ModelHandler: addAsteroid (const AsteroidPtr& oldAsteroid, const Vector& newVelocity) {
Point point = oldAsteroid→point ();
point.move (newVelocity.normaVector () * oldAsteroid→collisionRadius ());
_asteroids.push_back (AsteroidPtr (new Asteroid (point, oldAsteroid→collisionRadius ()
/ :: smallAsteroidRadiusK, newVelocity)));
} Point ModelHandler: randPoint (float r) const {
float x = 0;
float y = 0;
switch (rand () % 4) {
case 0:
x = common: rangeRand (0 — r, _worldWidth + r);
y = 0 — r;
break;
case 1:
x = common: rangeRand (0 — r, _worldWidth + r);
y = _worldHeight — r;
break;
case 2:
x = 0 — r;
y = common: rangeRand (0 — r, _worldHeight + r);
break;
case 3:
x = _worldWidth + r;
y = common: rangeRand (0 — r, _worldHeight + r);
break;
default:
break;
} return Point (x, y);
} bool ModelHandler: withinBoundaries (const ObjectPtr& obj) const {
return (obj→x () > (0 — _worldWidth * 0.1) && obj→x () < (_worldWidth * 1.1)
&& obj->y () > (0 — _worldHeight * 0.1) && obj→y () < (_worldHeight * 1.1));
} template