«Щи: симулятор жестокости» или «Как не надо делать игры»

Данная статья будет полезна начинающим разработчикам игр, да и вообще, любым людям, кто хочет связать свою жизнь с программированием. Я постарался сделать статью интересной и полезной тем, кто не знает программирование, но знание хотя бы основ С++ увеличит удовольствие от статьи.

Внимание!

 Данная статье все еще находится в разработке. 

обложка

Предыстория

Я всегда мечтал разрабатывать игры. Еще будучи школьником первых классов после школы приходил к другу домой и до позднего вечера мы играли в Sega. Тогда «восьмибитка» была для меня просто мечтой. А придя домой после таких посиделок рисовал в тетрадях в клеточку карты для Battle City (танчики, кто не помнит).

Прошло время и у нас в семье появился компьютер. В основном, конечно же, я проводил время играя в игры, а не разрабатывая их. Но к началу восьмого класса решил твердо заняться игростроем. Информации в голове было ноль. Знал только отдаленно, что такое «С++», и мог писать описания к своим выдумкам вроде «домики набигают».

Первые потуги

Купил себе книжку «С++ для чайников». От нее у меня, если честно, болела голова. Не советую новичкам данную книгу, так как…эмм…до сих пор как вспоминаю, что читаешь, и все время теряется ответ на простой вопрос: «А зачем все это?». Все эти функции, классы, наследование, полиморфизм… Кстати, если Вы преподаватель, задумайтесь над тем, даете ли Вы ответ на вопрос «зачем?», отсутствие ответа на этот вопрос — это наша общая беда в сфере образования.

Однако, кое-какие результаты были достигнуты. Я поставил бесплатную нубскую IDE Dev-Cpp от компании Bloodshed, как и советовалось в книге, и начал писать программы. Было интересно. Все первые программы, конечно же, были консольными, о формошлепстве я тогда и знать не знал.

Сначала был реализован обычный калькулятор, с расширенными опциями вроде подсчета суммы натурального ряда от 1 до N, подсчета суммы сумм натуральных рядов от 1 до N, и т.д. Я назвал его «Пидрахуй», что на украинском значит «Посчитай». Он даже вроде бы что-то выводил в текстовый файл. В целом это было около полутора тысяч строк редкостного говнокода, основанного на дергании потоков ввода/вывода std::cin и std::cout.

Потом была программа «Химик», считающая молярную массу любого соединения. С адским вводом молекулы. Вся таблица Менделеева хранилась в…а, не, ни хрена. Нигде она не хранилась. Было порядка 130 условных конструкций, проверяющих символьное обозначение элемента из входного потока. Потом это дело (молярная масса элемента) умножалось на количество атомов в молекуле….и, вуаля! Тем не менее, пару-тройку лаб по химии с помощью этого чуда инженерной мысли я сделал.

Затем я купил книгу по Borland C++, помацал эту IDE, сделал мини-плеер «Balalaika» с плейлистом на 1 файл, и мне стало скучно. И я забросил все это дело на полтора года.

Вторые потуги

Через полтора года у меня опять начали возникать амбиции. «Как так, люди, вон Oblivion целый сделали, а чем я хуже?». И начались поиски инфы про то, как делать игры. Я вообще тогда ничего не понимал, что такое игровой движок, игровая механика…В итоге все уперлось в то, что мне пару раз попадались паленые версии Visual Studio 2008 на DVD носителях, в которых даже простейшие примеры из Интернета не хотели собираться. То отсутствовали стандартные заголовочные файлы, то еще что-то. Для меня это был дикий лес, а панель свойств проекта в студии — атомной подлодкой.

И вот, с третьего раза мне посчастливилось купить диск с нормальной студией. Не помню каким образом, но я наткнулся на движок HGE, скачал библиотеку, все подключил, запустил пример из первого туториала и завизжал от радости. Передо мной был черный экран. Но, оно, зараза, запустилось! Работает!

И тут Остапа понесло. Сразу скажу, на тот момент из синтаксических конструкций С++, которые я знал, были

  1. Создание переменных
  2. Условные ветвления
  3. Циклы
  4. Функции, которые влом было плодить

Всё! Какие там классы, зачем они вообще нужны (привет, бритва Оккама)? Я решил забацать что-то эпичное!..

Мы с другом круглый год пытались вдвоем пройти игру под названием Seal Hunter. Суть такова: из-за левой части экрана набегают тюлени, моржи и пингвины, надо было их отстреливать не давая пройти через весь экран. Все это сопровождалось кучами брызг крови (которая, как я потом узнал через несколько лет, рендерилась в текстуру и оставалась на все время игры), мяском и веселым смехом персонажей. Это чудо было рассчитано на одного/двух игроков за одним компьютером. Мы потом даже с другом озвучку сделали, часть которой перекочевала в «Щи». Мы про Seal Hunter так и говорили: «пойдем заварим щи!», так что название для своей игры долго придумывать не пришлось.

Мне сильно захотелось сделать клон игры, только на русский мотив: вместо льдины — лес, вместо тюленей — зайцы, а вместо чукч в пуховиках — дед в ушанке. И чтоб на двух игроков. И чтоб еще и по сети! И с прокачкой. И с кучей оружия, как в «контре», плюс гранатометы. И чтоб кровища аж дрыстала из экрана! А, и еще бы бронетехники добавить…
В общем, насколько Вы поняли, торкнуло меня конкретно. И вот, пошла она…разработка!

Первый прототип

За, буквально, два дня был написан прототип: по зеленому полю вверх-вниз бегал человечек в ушанке и с калашом, на него вылетали белый квадратики, автомат ссал пулями и при пересечении с квадратиком тот «умирал» (останавливался и становился серым).

первый прототип
Первый прототип с квадратными врагами

Вау! Это был конкретный прогресс! Но я сразу же столкнулся с несколькими трудностями.

Во-первых автомат, как я уже сказал, просто ссал пулями. Т.е. как будто в него набрали воды и он, поперхиваясь, выплевывал пули: то две за раз, то одну, то три…Все дело было в том, как я рассчитывал скорострельность: брал по модулю текущую миллисекунду системных часов компьютера от 125. Т.е. если текущая миллисекунда компьютера делится на 125 без остатка, то автомат «разблокировывался» после последнего выстрела и можно была снова выстрелить. Недолго думая, я догадался, что дело в том, что игра работает кадрами. Т.е. проверка вызывается в целом случайное количество раз за секунду, и явно не каждую миллисекунду, из-за чего я «промахивался» мимо миллисекунд, когда можно было разблокировать автомат. Я понял причину, но пока оставил это дело в покое. Была еще одна проблема.

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

Кстати, пуля была одна! Я ее просто быстро гонял по экрану, и при попадании за экран она телепортировалась назад в ствол. Я подогнал ее скорость, чтобы она успевала залетать за экран и, таким образом «сэкономил память» (facepalm).

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

Далее, я уже не помню, что происходило, но мне захотелось добавить:

  • оружия;
  • разных врагов;
  • разнообразия смерти врагов;
  • сделать нормальную скорострельность и перезарядку;
  • добавить второго игрока.

Симуляция жестокости

Ну все, далее пошло-поехало!

Я сразу добавил поддержку второго игрока, так как знал, что чем дальше, тем это сделать будет сложней. Воистину, хоть одно мудрое решение! Но…

Поддержка двух игроков

За игрока и его игровое окружение отвечали некоторое множество переменных. Это:

float x=1000.0f, y=450.0f;// игрок 1, координаты
float dx=0.0f, dy=0.0f;   //и все что нужно для его движения
const float tormoz=0.89f;
...
int kills;                //количество убитых
int points1=0;            //очки за убийства, за которые покупается оружие
...
hgeSprite* pulya;         //картинка огоня от пули
hgeSprite* p;             //картинка самой пули
...
float a;                  //координаты огня пули игрока 1
float b;
int oboima;               //кол-во пуль в обойме игрока 1
int oboima_akt;           //уже не помню что, по-моему альтернативная обойма (подствольник)
int aktivatorp=0;         //отвечает за статус пули, 1 - летит, 0 - в обойме
...
int N_WEAPON1=1;          //номер текущего оружия в руках игрока
int shot=0;               //можно ли выстрелить? 0 - можно, 1 - нельзя (скорострельность)

Ха, я сказал некоторое множество? Да этих переменных куча! Все, что Вы видите выше — это всего лишь малая, самая важная часть. Самое плохое, что они были размазаны равномерным слоем по нескольким стам строкам кода…

Итак, что же я сделал? Правильно — скопировал переменные и добавил суффикс «2»!

float x2=1000.0f, y2=450.0f;// игрок 2, координаты
float dx2=0.0f, dy2=0.0f;   //и все что нужно для его движения
const float tormoz2=0.89f;  //непонятно, зачем игроку 2 отдельный тормоз?
...
int kills2;                //количество убитых
int points2=0;            //очки за убийства, за которые покупается оружие
...
hgeSprite* pulya2;         //картинка огоня от пули
hgeSprite* p2;             //картинка самой пули
...
float a;2                  //координаты огня пули игрока 2
float b2;
int oboima2;               //кол-во пуль в обойме игрока 2
int oboima_akt2;           //уже не помню что, по-моему альтернативная обойма (подствольник)
int aktivatorp2=0;         //отвечает за статус пули, 1 - летит, 0 - в обойме
...
int N_WEAPON2=1;          //номер текущего оружия в руках игрока
int shot2=0;               //можно ли выстрелить? 0 - можно, 1 - нельзя (скорострельность)

Может новички не в курсе, но правильнее было бы сделать структуру данных и просто создать новый объект этой структуры. Всего одна лишняя строчка кода вместо сотни!

Отдельно хочу сказать про переменную tormoz. Механика игры такова, что при передвижении скорость игрока как бы затухает, т.е. она постоянно умножается (в каждом кадре) на эту переменную, и, в итоге, сводится к нулю. Появляется небольшой эффект скольжения. Я не помню, зачем я это сделал. То ли потому, что в Seal Hunter данное скольжение было частью геймплея и усложняло игру, то ли просто взял код из туториала по HGE и бездумно скопировал механику.

Как видите, куча глобальных переменных, сопровождение такого кода быстро превращается в сущий ад…Ну да ладно, это еще ничего.

Добавление нового оружия

panzerfaust3
Panzerfaust 3 — самое грозное и дорогое оружие в игре

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

Напомню, все это глобальные, черт возьми, переменные. Для каждого игрока. Для каждого, черт возьми, вида оружия. Ой, т.е. для каждого наименования оружия, Вы не подумайте, что я говорю про пистолеты/автоматы/гранаты. А наименований оружия у меня было много:

hgeSprite* ak47;//автомат
hgeSprite* pm;//пистолет Макарова
hgeSprite* pp19_vityaz; //ПП 19-01 "Витязь"
hgeSprite* fn_f2000;//ФН_Ф2000
hgeSprite* tt;//Тульский Токарев 
hgeSprite* rpk;//РПК 74
hgeSprite* mac;//MAC 1 
hgeSprite* winchester; //винчестер
hgeAnimation* winchester_anime;
hgeSprite* rpk47;//РПК 47
hgeSprite* glok;//GLOK
hgeSprite* rgd5;//РГД-5
hgeAnimation* katana1_anime;
hgeSprite* milkor;//Milkor MGL
hgeSprite* panzer;//Panzerfaust-3
hgeSprite* webley;//Webley MK 4
hgeSprite* fn_five_seven;//FN Five-Seven

Особенно умиляет katana1_animе. Т.е. подразумевалось, что катана будет не одна, их будет несколько. Вот так простор для развития игры!

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

Добавление нового оружия занимало у меня пол дня: надо было найти оружие в Интернете, информацию про его боевые характеристики, скачать и отфотошопить текстуру оружия (я придавал ему мультяшный вид добавляя черную окантовку), затем надо было добавить в несколько мест по всей программе новые переменные, затем надо было протестировать оружие и откалибровать значения до приемлемых, чтобы было играбельно и не выглядело отвратно (я про координаты картинки оружия).

В полную меру ужасная структура проекта проявилась при добавлении первого дробовика:

hgeSprite* p1d1;//спрайты дробинок для дробовиков игрока 1 и игрока 2
hgeSprite* p1d2;//по 4 дробинки на дробовик, итого с основной пулей - 5
hgeSprite* p1d3;
hgeSprite* p1d4;
hgeSprite* p2d1;
hgeSprite* p2d2;
hgeSprite* p2d3;
hgeSprite* p2d4;

//дробь
float p1d1x,p1d1y;
float p1d2x,p1d2y;
float p1d3x,p1d3y;
float p1d4x,p1d4y;
//дробь 2 (сиквел, ага)
float p2d1x,p2d1y;
float p2d2x,p2d2y;
float p2d3x,p2d3y;
float p2d4x,p2d4y;

int aktivatorp1d1=0;//0 - дробина в стволе, 1 - летит
int aktivatorp1d2=0;
int aktivatorp1d3=0;
int aktivatorp1d4=0;

В общем, всего 5 дробинок, а сколько фана! Я уже не говорю про анимацию помпы дробовика, когда после выстрела надо было делать таймаут и перезарядку с проигрыванием анимации. Помню, я маялся с дробовиком больше недели.

Перезарядка оружия

Перезарядка — отдельная тема. Здесь мы имеем дело с временем. С определением которого у меня были проблемы — достаточно вспомнить про баг  со скорострельностью. Перед тем как показать код таймера перезарядки покажу-ка я Вам сначала отдачу от оружия. Да, у меня была отдача — чисто графически она выглядела как подергивание оружия вверх и назад при выстреле, на самом деле при выстреле я на мгновение (один кадр) менял координаты отрисовки:

if(oboima>0) //если вообще что-то есть в обойме
//эти комментарии я добавил сейчас, если что, в оригинале их просто нет
    {
    //для каких-то стволов дергаемся по Х на 1 пиксель
    if(N_WEAPON1==1||N_WEAPON1==9||N_WEAPON1==15)
    {weapon_x1[N_WEAPON1]++;
    weapon_x2[N_WEAPON1]++;}
    if(N_WEAPON1==4)//а для каких-то еще и вверх
    {weapon_x1[N_WEAPON1]++;
    weapon_y1[N_WEAPON1]--;
    weapon_y2[N_WEAPON1]--;    }
    if(N_WEAPON1==14)//а тут вообще супер-отдача!!!
    {weapon_x1[N_WEAPON1]+=2;
    weapon_y1[N_WEAPON1]--;
    weapon_x2[N_WEAPON1]++;
    weapon_y2[N_WEAPON1]--;}
    }
    //за отступы не ругайте - я сохранил их первозданность
    if(oboima>0&&p_vistrel!=0)//если есть что-то в обойме при нажатии выстрела
    {fire1();//здесь я вынес в функцию (как я мог!!!) код рисующий клубок огня на конце ствола
    p_ogon_ms=GetTime();//это таймер горения огня, он горит больше 1 кадра
    p_ogon_s=perez_akt;
    p_vistrel=0;}

Если Вас еще не тошнит, вот типичный код просчета перезарядки:

if(N_WEAPON2==3)//для FN F-2000 (850 мс)
{
if(vistrel2_time<928) //если честно, я сам сейчас не понимаю ничего {if((GetTime()>=vistrel2_time+72)||(GetTime()=928&&GetTime()>vistrel2_time-928&&perez_akt!=vistrel2_time_sec)
{p_vistrel2=1;}
}

Выше был представлен код перезарядки FN F2000, довольно козырная штука по игре, это единственное оружие у которого было видимое перекрестие прицела (да, игра еще была и хардкорная!). Так вот, обрабатывается два случая — когда миллисекунда времени выстрела далека от тысячи, и когда она близка к тысяче, тем самым надо разблокировать оружие на какой-то миллисекунде следующей секунды. А у меня хранятся только миллисекунды. В итоге имеем вот такую вот необходимость делать проверки. Причем, насколько я помню, все это с учетом лагов и разбиения времени кадрами.

А теперь та же самая перезарядка, но уже в функции рендера (вывода данных на экран). Я обозначал перезарядку простым прогресс баром из трех тире, заключенных в квадратные скобки.

//ВЫВОД ПРОГРЕССА ПЕРЕЗАРЯДКИ ИГРОКА 1
    if(reload==true)
    {   
    //какое-то оружие, чья перезарядка около трех секунд
    //опять имеем дело с секундами от 0-57 и отдельно с 58-59
    if(N_WEAPON1==0||N_WEAPON1==3||N_WEAPON1==5||N_WEAPON1==8)
    {
    if(perez_time>reload_time[N_WEAPON1]) //если секунда от 0 до 57
    {
    if(perez_akt==perez_time-reload_time[N_WEAPON1])
    {oboima_text->printf(x-35+Xz, y-28, HGETEXT_CENTER, "[-  ]");}

    if((reload_time[N_WEAPON1]%2==0&&perez_akt==perez_time-(reload_time[N_WEAPON1]-reload_time[N_WEAPON1]/2))||
       (reload_time[N_WEAPON1]%2!=0&&perez_akt==perez_time-(reload_time[N_WEAPON1]-((reload_time[N_WEAPON1]-1)/2)))||
       (reload_time[N_WEAPON1]%2!=0&&perez_akt==perez_time-(reload_time[N_WEAPON1]-((reload_time[N_WEAPON1]+1)/2))))
    {
    oboima_text->printf(x-35+Xz, y-28, HGETEXT_CENTER, "[-- ]");
    if(perez_akt_milli==perez_time_milli&&(perez_akt==perez_time-1||perez_akt==59))
    {ak_perez2();}
    }
    
    if(perez_akt==perez_time&&perez_akt_milliprintf(x-35+Xz, y-28, HGETEXT_CENTER, "[---]");}
    }
    }
    //...код перезарядки по условию if(reload==true) продолжается мно-о-ого строк...

Если честно, уже сложно понять, чем perez_time отличается от perez_akt. Скорее всего первая переменная хранила время, когда перезарядка должна закончиться, а вторая хранила текущее время, по которому активировалась/деактивировалась перезарядка, отсюда такие страшные названия.

По-хорошему я должен был высчитать глобальное время, когда надо закончить перезарядку и просто проверять больше ли или меньше текущее время. Если больше — при первой же возможности (из-за покадрового просчета игры мы не знаем, на сколько больше будет время следующего кадра, может быть мы вообще зависнем на секунду) заканчиваем перезарядку. Но, тогда бы мне мои сегодняшние мозги…

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

Разнообразие врагов

В игре представлено целых четыре вида врагов, причем, если включен РПГ-мод, у них могли появляться руны: тепепортации, иллюзиониста и т.д. Но, обо всем по порядку.

Зайцы являются базовым врагом, и, хвала какому-то туториалу с сайта HGE, они представлены в виде структуры данных (феноменально!), которая представлена ниже. Правда, название структуры, как обычно,  «не очень».

struct zayacObject  //ЗАЙЦЫ---------------------
{float v1x,v1y;
int bezuh;
int bezboshki;
int ubit;
int pokoin;//быть покойником и быть убитым - разные вещи!!!
int health;
int killed_by;//ID игрока, которому надо дать очки при убийстве
float timeout_milli,timeout;//таймер респауна
float speed;
int defence;//защита - RPG-мод
float kill_time;
int kill_time_sec;
int object_id;
int A;            //индекс для алгоритма художника
bool vzorvan;     //я уже и не помню зачем все это...               
float energy;
float rasstX,rasstY;
int chetvert;
int RPG_healer;   //RPG руны
int RPG_illusionist;
float ill1_x,ill1_y,ill2_x,ill2_y; //координаты иллюзий, если это заяц-иллюзионист
int ill1_active,ill2_active;
int RPG_teleporter;
float teleportx;
int teleported;
};
zayacObject*   vragi1Objects;

Как видите, все не так просто: у нас здесь есть признаки оторванных ушей, оторванной бошки, признак убийства… Все еще очень осложняется трешовой игровой механикой, о которой речь ниже. Как и в случае с пулями, я гонял одних и тех же зайцев по кругу, пока заяц покойник (int pokoin) и отсчитывался таймер (float timeout_milli,timeout;) — он не респаунился. Максимальное число зайцев, да и вообще любых объектов,  было жестко вшито в игру:

#define MAX_ZAYAC 29//Здесь мы видим, что зайцев всего было не более 29 штук
#define MAX_VOLK 20
#define MAX_MEDVED 20
#define MAX_PTENEC 20
#define MAX_TRUPS 5000
#define MAX_UHI 200
#define MAX_BLOOD 500
#define MAX_HEAD 200
#define MAX_GILZ 200
#define MAX_BONUS 200
#define MAX_BONUSTEXT 200
#define MAX_BONUSI 200
#define MAX_BONUSSTATUS 50
#define MAX_MYASO 2000
#define MAX_GREN 100
#define MAX_KILLS 100000

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

wolf
Волк, на анимацию которого ( из 7 кадров) было убито порядка двух дней

Далее было решено добавить волков и медведей. Причем это не просто враги с разными характеристиками, они кооперировались с зайцами и выполняли свою роль. Волки ускоряли близлежащих зайцев (эдак раза в полтора-два), в то время как за медведем можно было просто спрятаться — никакая пуля не прошивала его насквозь. Если зайцы были немного ниже или выше медведя, они подбегали и выстраивались за ним в очередь.

А теперь я выкладываю структуру данных для волков.

struct volkObject //ВОЛКИ----------------------
{
float v2x,v2y;
int ubit;
int health;
int killed_by;
float timeout_milli,timeout;
float speed;
int defence;
int object_id;
int RPG_healer;
int RPG_illusionist;
float ill1_x,ill1_y,ill2_x,ill2_y;
int ill1_active,ill2_active;
int RPG_teleporter;
float teleportx,teleporty;
int teleported;
};

volkObject* vragi2Objects;

Найдите 10 отличий от структуры данных зайцев. Такое же дело и для медведей, и для птенцов. Наследование? Не, не слышал. Вообще игра по сути написана на С, разве что компилировалась в настройках как С++.

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

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

//коцанье зайца (отрывание ушей)
if(((((py<=vragi1Objects[i3].v1y+12&&py>vragi1Objects[i3].v1y&&pxvragi1Objects[i3].v1y&&p1d1xvragi1Objects[i3].v1y&&p1d2xvragi1Objects[i3].v1y&&p1d3xvragi1Objects[i3].v1y&&p1d4xvragi1Objects[i3].v1x&&vragi1Objects[i3].bezuh==0&&vragi1Objects[i3].ubit!=1)||
(((p2y<=vragi1Objects[i3].v1y+12&&p2y>vragi1Objects[i3].v1y&&p2xvragi1Objects[i3].v1y&&p2d1xvragi1Objects[i3].v1y&&p2d2xvragi1Objects[i3].v1y&&p2d3xvragi1Objects[i3].v1y&&p2d4xvragi1Objects[i3].v1x&&vragi1Objects[i3].bezuh==0&&vragi1Objects[i3].ubit!=1))||
 (katana1_anime->IsPlaying()==true&&vragi1Objects[i3].v1x>x-90&&yvragi1Objects[i3].v1y&&
 x-55>vragi1Objects[i3].v1x&&vragi1Objects[i3].bezuh==0&&vragi1Objects[i3].ubit!=1))
//vragi1Objects[i3].bezuh==0 чтобы ухи не возвращались на место после повторного попадания зайцу по "ушам"
{


uhiObjects[N_UHI].niz=vragi1Objects[i3].v1y+38;
vragi1Objects[i3].bezuh=1;
uhiObjects[N_UHI].uhix=vragi1Objects[i3].v1x+18;
uhiObjects[N_UHI].uhiy=vragi1Objects[i3].v1y-3;
uhiObjects[N_UHI].akt=1;

if((((py<=vragi1Objects[i3].v1y+12&&py>vragi1Objects[i3].v1y&&pxvragi1Objects[i3].v1y&&p1d1xvragi1Objects[i3].v1y&&p1d2xvragi1Objects[i3].v1y&&p1d3xvragi1Objects[i3].v1y&&p1d4xvragi1Objects[i3].v1x)||
 (katana1_anime->IsPlaying()==true&&vragi1Objects[i3].v1x>x-90&&yvragi1Objects[i3].v1y&&
 x-55>vragi1Objects[i3].v1x&&vragi1Objects[i3].bezuh==0&&vragi1Objects[i3].ubit!=1))
{points1+=10;}//10 очков за уши игроку 1
if(((p2y<=vragi1Objects[i3].v1y+12&&p2y>vragi1Objects[i3].v1y&&p2xvragi1Objects[i3].v1y&&p2d1xvragi1Objects[i3].v1y&&p2d2xvragi1Objects[i3].v1y&&p2d3xvragi1Objects[i3].v1y&&p2d4xvragi1Objects[i3].v1x)
{points2+=10;}//10 очков за уши игроку 2

N_UHI++;

if(N_UHI>=MAX_UHI-1)
{N_UHI=0;}

}//закрытие коцанья зайца

Сейчас сразу видно (или не сразу?), что вот эти безумные условия можно было как минимум сохранить в переменных, и затем не писать их заново, как это делается в полностью аналогичных проверках попадания по ушам и проверки, какому же игроку достанется несчастные 10 очков. С этим фрагментом кода в свое время был связан веселый баг (кстати, некоторые баги стали фичами, об этом ниже), а именно: при попадании по месту, где у зайца были уши, новая пара ушей повторно они респаунилась и снова отлетала. Особенно круто это выглядело при стрельбе дробовика в упор — с одного выстрела летело по 5 пар ушей, как из рога изобилия. Жалко, но этот баг пришлось убрать, так как он выглядел нелогичным. А вот один баг был-таки превращен в фичу, о нем будет ниже.

Далее еще интереснее. Подобный код с проверками попадания в тушку, в голову, по ушам, проверка какой игрок попал и т.д. дублировался 4 раза для каждого врага! Вот Вам попадание в медведя с последующим разъяснением, какой же игрок все-таки в него попал:

if((((p1d1yvragi3Objects[i80].v3y+105&&p1d1x<=vragi3Objects[i80].v3x+170&&p1d1x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d1==1)||
   (p1d2yvragi3Objects[i80].v3y+105&&p1d2x<=vragi3Objects[i80].v3x+170&&p1d2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d2==1)||
   (p1d3yvragi3Objects[i80].v3y+105&&p1d3x<=vragi3Objects[i80].v3x+170&&p1d3x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d3==1)||
   (p1d4yvragi3Objects[i80].v3y+105&&p1d4x<=vragi3Objects[i80].v3x+170&&p1d4x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d4==1)||
   (pyvragi3Objects[i80].v3y+105&&px<=vragi3Objects[i80].v3x+170&&px>vragi3Objects[i80].v3x-300*lag&&aktivatorp==1))
  &&x-55>vragi3Objects[i80].v3x&&vragi3Objects[i80].ubit!=1)||
   (((p2d1yvragi3Objects[i80].v3y+105&&p2d1x<=vragi3Objects[i80].v3x+170&&p2d1x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2d1==1)||
   (p2d2yvragi3Objects[i80].v3y+105&&p2d2x<=vragi3Objects[i80].v3x+170&&p2d2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2d2==1)||
   (p2d3yvragi3Objects[i80].v3y+105&&p2d3x<=vragi3Objects[i80].v3x+170&&p2d3x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2d3==1)||
   (p2d4yvragi3Objects[i80].v3y+105&&p2d4x<=vragi3Objects[i80].v3x+170&&p2d4x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2d4==1)||
   (p2yvragi3Objects[i80].v3y+119&&p2x<=vragi3Objects[i80].v3x+170&&p2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2==1))
  &&x2-55>vragi3Objects[i80].v3x&&vragi3Objects[i80].ubit!=1))//просчёт урона
{

//Тут мы оказываемся если кто-то из игроков попал по медведю, но надо ж теперь узнать какой, верно?
if(shot==0)
{
if(((pyvragi3Objects[i80].v3y+105&&px<=vragi3Objects[i80].v3x+170&&px>vragi3Objects[i80].v3x-300*lag&&aktivatorp==1)||
   (p1d1yvragi3Objects[i80].v3y+105&&p1d1x<=vragi3Objects[i80].v3x+170&&p1d1x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d1==1)||
   (p1d2yvragi3Objects[i80].v3y+105&&p1d2x<=vragi3Objects[i80].v3x+170&&p1d2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d2==1)||
   (p1d3yvragi3Objects[i80].v3y+105&&p1d3x<=vragi3Objects[i80].v3x+170&&p1d3x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d3==1)||
   (p1d4yvragi3Objects[i80].v3y+105&&p1d4x<=vragi3Objects[i80].v3x+170&&p1d4x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d4==1))
&&x-55>vragi3Objects[i80].v3x)    
{vragi3Objects[i80].killed_by=1;}//попал игрок 1
if(((p2yvragi3Objects[i80].v3y+105&&p2x<=vragi3Objects[i80].v3x+170&&p2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp2==1)||
   (p2d1yvragi3Objects[i80].v3y+105&&p2d1x<=vragi3Objects[i80].v3x+170&&p2d1x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d1==1)||
   (p2d2yvragi3Objects[i80].v3y+105&&p2d2x<=vragi3Objects[i80].v3x+170&&p2d2x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d2==1)||
   (p2d3yvragi3Objects[i80].v3y+105&&p2d3x<=vragi3Objects[i80].v3x+170&&p2d3x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d3==1)||
   (p2d4yvragi3Objects[i80].v3y+105&&p2d4x<=vragi3Objects[i80].v3x+170&&p2d4x>vragi3Objects[i80].v3x-300*lag&&aktivatorp1d4==1))
 &&x2-55>vragi3Objects[i80].v3x)
{vragi3Objects[i80].killed_by=2;}//попал игрок 2
//Дальше Вам что-то показывать мне уже страшно...

Я думаю, за такой код многие низкоуровневые системные программисты пожали бы руку, а высокоуровневые программисты — горло.

Птенец идет - лень было делать долгую анимацию, обошелся тремя кадрами
Птенец идет — лень было делать долгую анимацию, обошелся тремя кадрами

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

Совсем другое дело, как это выглядит в коде. Я в школе не сильно любил тригонометрию, хоть и дружил с ней. Если кто не помнит, движение по окружности радиусом R задается формулами: x=R*cos(a), y=R*sin(a), где альфа — текущий угол на окружности. Меняем угол и пересчитываем координаты. Всё! Но нет же, у меня был свой подход: движение по окружности делится на 4 фазы (4 четверти круга), в каждой из которых происходит какая-то магия (отступы и комментарии сохранены):

if(vragi4Objects[i120].polet==1)//polet
{

//летим к радиусу
if(sqrt((vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)+
   (vragi4Objects[i120].v4x-vragi4Objects[i120].rx)*(vragi4Objects[i120].v4x-vragi4Objects[i120].rx))>=vragi4Objects[i120].r&&
  vragi4Objects[i120].kruzhit==false)
{
vragi4Objects[i120].v4y-=vragi4Objects[i120].speedY*lag;
if(vragi4Objects[i120].v4x>=vragi4Objects[i120].rx)
{vragi4Objects[i120].v4x-=vragi4Objects[i120].speedY*(vragi4Objects[i120].v4x-vragi4Objects[i120].rx)/(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*lag;}
if(vragi4Objects[i120].v4x=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2)&&
   vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))    
{vragi4Objects[i120].v4y-=vragi4Objects[i120].speedY*lag;
if(vragi4Objects[i120].chetvert==1)//против часовой (справа)
{vragi4Objects[i120].v4x=sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*(vragi4Objects[i120].v4y-vragi4Objects[i120].ry))+vragi4Objects[i120].rx;}
if(vragi4Objects[i120].chetvert==-1)//по часовой (слева)
{vragi4Objects[i120].v4x=vragi4Objects[i120].rx-sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*(vragi4Objects[i120].v4y-vragi4Objects[i120].ry));}
}


//по краям
if(vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2)||    vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))    
{
if((vragi4Objects[i120].chetvert==-1&&vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2))|| (vragi4Objects[i120].chetvert==1&&vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2)))
{vragi4Objects[i120].v4x+=vragi4Objects[i120].speedY*lag;}
if((vragi4Objects[i120].chetvert==-1&&vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))||
(vragi4Objects[i120].chetvert==1&&vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2))) {vragi4Objects[i120].v4x-=vragi4Objects[i120].speedY*lag;} if(vragi4Objects[i120].v4y>=vragi4Objects[i120].ry)
{vragi4Objects[i120].v4y=sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4x-vragi4Objects[i120].rx)*(vragi4Objects[i120].v4x-vragi4Objects[i120].rx))+vragi4Objects[i120].ry;}
if(vragi4Objects[i120].v4y=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2)&&
   vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))    
{vragi4Objects[i120].v4y+=vragi4Objects[i120].speedY*lag;
if(vragi4Objects[i120].chetvert==1)//против часовой (справа)
{vragi4Objects[i120].v4x=sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*(vragi4Objects[i120].v4y-vragi4Objects[i120].ry))+vragi4Objects[i120].rx;}
if(vragi4Objects[i120].chetvert==-1)//по часовой (слева)
{vragi4Objects[i120].v4x=vragi4Objects[i120].rx-sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4y-vragi4Objects[i120].ry)*(vragi4Objects[i120].v4y-vragi4Objects[i120].ry));}
}


//по краям
if(vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2)||    vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))    
{
if((vragi4Objects[i120].chetvert==-1&&vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2))|| (vragi4Objects[i120].chetvert==1&&vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2)))
{vragi4Objects[i120].v4x-=vragi4Objects[i120].speedY*lag;}
if((vragi4Objects[i120].chetvert==-1&&vragi4Objects[i120].v4y>=(vragi4Objects[i120].ry+vragi4Objects[i120].r/2))||
(vragi4Objects[i120].chetvert==1&&vragi4Objects[i120].v4y<=(vragi4Objects[i120].ry-vragi4Objects[i120].r/2))) {vragi4Objects[i120].v4x+=vragi4Objects[i120].speedY*lag;} if(vragi4Objects[i120].v4y>=vragi4Objects[i120].ry)
{vragi4Objects[i120].v4y=sqrt(vragi4Objects[i120].r*vragi4Objects[i120].r-(vragi4Objects[i120].v4x-vragi4Objects[i120].rx)*(vragi4Objects[i120].v4x-vragi4Objects[i120].rx))+vragi4Objects[i120].ry;}
if(vragi4Objects[i120].v4y=vragi4Objects[i120].ry+(vragi4Objects[i120].r-1))
{vragi4Objects[i120].up=true;
if(vragi4Objects[i120].chetvert_n==1&&vragi4Objects[i120].chetvert==-1)
{vragi4Objects[i120].chetvert=1;}
if(vragi4Objects[i120].chetvert_n==-1&&vragi4Objects[i120].chetvert==1)
{vragi4Objects[i120].chetvert=-1;}}//смена направления вверх
}//летим вниз

}//заход снизу


if(vragi4Objects[i120].gradus>vragi4Objects[i120].gradusNext)
{vragi4Objects[i120].kruzhit=false;
vragi4Objects[i120].krug++;
if(vragi4Objects[i120].zahod==1)
{vragi4Objects[i120].zahod=-1;}
if(vragi4Objects[i120].zahod==-1)
{vragi4Objects[i120].zahod=1;}
vragi4Objects[i120].rx=hge->Random_Float(vragi4Objects[i120].rx,vragi4Objects[i120].rx+600);
vragi4Objects[i120].ry=hge->Random_Float(vragi4Objects[i120].ry-300,vragi4Objects[i120].ry-100);
vragi4Objects[i120].gradus=vragi4Objects[i120].gradusNext;
vragi4Objects[i120].gradusNext+=M_PI-M_PI_4;
vragi4Objects[i120].r=hge->Random_Int(25,50);
}

if(vragi4Objects[i120].gradus<-vragi4Objects[i120].gradusNext) {vragi4Objects[i120].kruzhit=false; vragi4Objects[i120].krug++; if(vragi4Objects[i120].zahod==1) {vragi4Objects[i120].zahod=-1;} if(vragi4Objects[i120].zahod==-1) {vragi4Objects[i120].zahod=1;} vragi4Objects[i120].rx=hge->Random_Float(vragi4Objects[i120].rx-600,vragi4Objects[i120].rx);
vragi4Objects[i120].ry=hge->Random_Float(vragi4Objects[i120].ry+100,vragi4Objects[i120].ry+300);
vragi4Objects[i120].gradus=-vragi4Objects[i120].gradusNext;
vragi4Objects[i120].gradusNext+=-(M_PI-M_PI_4);
vragi4Objects[i120].r=hge->Random_Int(25,50);
}



}//kruzhim

}//polet

Некоторые уже утирают слезы, а мы переходим к мясу.

Кровища и мясо

Обработка крови и мясо — это вообще отдельная песня. Как я себе ее представлял судя по тому, что я увидел в Seal Hunter: при попадании создается специальный объект (эмиттер, или, по-русски излучатель), который генерирует кровь и мясо. Брызг крови у меня не вышло — я решил, что это будет сильно лагать, поэтому при попадании создавался объект массива krovyakObjects, а по факту — картинка. Картинка представляла из себя лужицу рисованной крови, которая растягтивалась из точки попадания по оси X и немного по Y, и при этом еще постепенно падала наземь.

Все звучит просто, но вот код…

if(krovyakObjects[i92].faza==1&&(krovyakObjects[i92].y2-krovyakObjects[i92].y1<=25||krovyakObjects[i92].y2<=krovyakObjects[i92].niz)) {krovyakObjects[i92].y2+=1.5f*lag;}//растягивается вниз if(krovyakObjects[i92].faza==1&&(krovyakObjects[i92].y2-krovyakObjects[i92].y1>=25||krovyakObjects[i92].y2>=krovyakObjects[i92].niz))
{krovyakObjects[i92].faza=2;
krovyakObjects[i92].y1+=1.5f*lag;}

if(krovyakObjects[i92].faza==2&&krovyakObjects[i92].y2-krovyakObjects[i92].y1>=12)//долетает вниз
{
krovyakObjects[i92].y1+=1.5f*lag;
if(krovyakObjects[i92].seed==101||krovyakObjects[i92].seed==102||krovyakObjects[i92].seed==103)//вперёд
{krovyakObjects[i92].x2+=hge->Random_Float(1,1.5f)*lag;}
}

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

if(krovyakObjects[i92].seed==1||krovyakObjects[i92].seed==2||krovyakObjects[i92].seed==3)//чтоб поменьше ссанья
{
if(myaskoSsit!=0)
{myasko_ssit();
myaskoSsit=0;}
if(krovyakObjects[i92].blood_time>=950&&time<=krovyakObjects[i92].blood_time-950)
{myaskoSsit=1;}
if((krovyakObjects[i92].blood_time<950&&perez_akt_milli<=krovyakObjects[i92].blood_time+50)
||(perez_akt!=krovyakObjects[i92].blood_time_sec))
{myaskoSsit=1;}
}

Код полета луж крови был настолько запутан, что там затерялся один баг: иногда лужа крови не "умирала" и останавливалась на земле, а постоянно росла, причем происходило это так: игрок перемещается по уровню, а лужа вытягивается за ним. Выглядело убого, так как текстура при этом размазывалась до невозможности, но было крайне интересно.

Система RPG

Так как конечной Вселенской целью данного проекта было, конечно же, развитие навыков разработки игр и построение MMORPG мечты (как и у любых других начинающих игростроевцев), то в игру было решено добавить RPG-мод. Его можно было отключить в любой момент в меню опций игры.

Окно повышения уровня
Окно повышения уровня

Что из себя представлял RPG-мод? По сути, просто у врагов со временем росла характеристика "защита" и урон от оружия постепенно уменьшался. За убийства давался опыт и с повышением уровня можно было вызвать специальное диалоговое окно, которое (надо же, хоть тут!) приостанавливало игру и давало немного поразмыслить над выбором. На выбор было 3 базовые характеристики и 2 специальные, в зависимости от класса. К базовым характеристикам относились: атака - повышение урона оружия в ответ на повышение защиты врагов; скорость - увеличивало скорость перемещения по экрану, особенно это помогало при таскании тяжелого вооружения; удача - шанс того, что выпадет хороший бонус вместо плохого повышался. Из специальных навыков был реально реализован только один - взрывчатка у разрушителя: повышался радиус и урон от гранат.

Планировалось добавить и навыки (что-то по типу навыков в Героях Меча и Магии), но, хорошо, что дело до этого так и не дошло. Об этом напоминает только одинокая надпись "Навыки:" в окне справа.

Урон при включенном RPG-моде высчитывался по формуле (<урон оружия> + (<уровень>/2)*<атака>)*1.2 - <защита>:

if(LVL2%2==0)
{vragi1Objects[i3].health-=(damage[N_WEAPON2]+LVL2/2*attack2)*1.2f-vragi1Objects[i3].defence;}
if(LVL2%2==1)
{vragi1Objects[i3].health-=(damage[N_WEAPON2]+(LVL2-1)/2*attack2)*1.2f-vragi1Objects[i3].defence;}

При этом множитель 1.2 соответствовал именно зайцу, так как это самый слабый враг. Также гарантировался урон хотя бы в 5 пунктов, чтобы зайцы хоть как-то умирали:

//гарантированный урон
if(damage[N_WEAPON2]+LVL2/2*attack2-vragi1Objects[i3].defence<5||
damage[N_WEAPON2]+(LVL2-1)/2*attack2-vragi1Objects[i3].defence<5)
{vragi1Objects[i3].health-=5;}

RPG руны

Было решено расширить RPG-мод - это не только прокачка персонажа, но и появление "магических" врагов. Суть заключалась в том, что в случайные моменты времени ри респауне волков и медведей они появлялись с рунами (как поперло то, а!) телепортации, лекаря или иллюзиониста.

Руна телепортации основывалась на баге - как-то случайно была допущена ошибка в коде перемещения волков, и они в случайные моменты времени начинали телепортироваться по оси Y, чем неимоверно доставляли хлопот игроку. Мне понравилось, и с помощью небольших художеств и сравнительно небольшого куска кода эта возможность стала...возможностью

Руна иллюзии давала зверю 2 иллюзии, которые не получали урон, но передвигались точно так же, как и оригинал. При этом было непонятно, где оригинал, а где нет - приходилось стрелять во всех трех видимых зверей, чтобы по отлетающей кровищи/мясу определить, кого надо убивать. Это доставляло хлопот, как и было задумано.

Зверь-лекарь лечил сам себя, по сути это регенерация. На него требовалось в 3 раза больше свинца, чем обычно.

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

while((vragi3Objects[i80].teleporty>vragi3Objects[i80].v3y&&vragi3Objects[i80].teleporty-vragi3Objects[i80].v3y<100)||       (vragi3Objects[i80].v3y>vragi3Objects[i80].teleporty&&vragi3Objects[i80].v3y-vragi3Objects[i80].teleporty<100)) { if(player2_aktive==0) {if(DIFF!=0) {vragi3Objects[i80].teleporty=hge->Random_Float(250,650);}
if(DIFF==0)
{vragi3Objects[i80].teleporty=hge->Random_Float(350,550);}}
if(player2_aktive==1)
{if(DIFF!=0)
{vragi3Objects[i80].teleporty=hge->Random_Float(200,700);}
if(DIFF==0)
{vragi3Objects[i80].teleporty=hge->Random_Float(300,600);}}
}

Система классов

Система классов была отголоском популярной в то время TeamFortress 2. В игре есть 4 класса, каждый из который должен был сильно различаться геймплеем. Мясник должен был агрить на себя врагов и орудовать холодным оружием (из которого реализована только одна катана, и та калечная), стрелок должен был уметь выстраивать врагов перед собой в ряд и прошивать всех пулями с повышенной убойностью и точностью, разрушитель должен был использовать взрывчатку, а инженер, как и TF2, должен был больше полагаться на автоматические установки (которые готовились, но так и не были введены в игру).

Щи симулятор жестокости
Одинокий инженер смотрит в даль

В целом, как видите, замах неплохой. Из этого, как я уже упоминал, было реализовано меню выбора класса, различная внешность персонажей, и то, только для первого игрока, повышение точности у стрелка, и повышение урона взрывчаткой у разрушителя. А также приманка для мясника. Не густо. Нечего было так распыляться. Может быть было реализовано и что-то еще, но все эти фичи настолько размазаны по коду, что уже сложно что-то сказать наверняка. Вот, что я нашел в коде просчета волков (vragi2Objects):

//агро мясника
if(class1==1&&primanka1>0&&vragi2Objects[i20].v2x+100<=x-16&&sqrt(((x-16)-vragi2Objects[i20].v2x+100)*((x-16)-vragi2Objects[i20].v2x+100)+
(vragi2Objects[i20].v2y+20-y)*(vragi2Objects[i20].v2y+20-y))<=(60+5*primanka1))
{
if(vragi2Objects[i20].v2y+20y)
{vragi2Objects[i20].v2y-=(0.05f+0.1f*sqrt((float)primanka1))*lag;}
}

Система бонусов

В игре также была система бонусов. По геймплею, кстати, очень неплохая штука. Из врага при смерти мог случайно выпасть бонус: шило либо знак вопроса. Шило ускоряло игрока на 10% на некоторое время (не помню уже на сколько), а в статус баре на правом краю экрана высвечивалась желтая улитка. Из случайного бонуса могло выпасть несколько вариантов: пусто, замедление скорости на 10% (красная улитка в статус баре), увеличение зарабатываемого опыта на 25% (желтая ламочка в статус баре) и уменьшение получаемого опыта на 50% (красная лампочка). Бонусы могли стаковаться по нескольку штук, но эффект от стакования разный.
Очень интересно все это выглядит в коде:

//где-то посередине убийства волка
points1+=200;
opit1+=(70+vragi2Objects[i20].defence*vragi2Objects[i20].defence/LVL1)*w_opit_volk[N_WEAPON1]*(1+0.25f*umnik1)/daun1;

Опыт от волка равнялся (70 + его защита в квадрате делить на уровень игрока) * множитель опыта от убийства волка для текущего оружия игрока, и все это умножалось на (1+0.25f*umnik1)/daun1. Переменная umnik1 отвечала за бонус повышения заработка опыта, и стабтильно увеличивала его на 25% каждый раз, а daun1 все время равнялся единице (так как на ноль делить нельзя) и при увеличении сначала отбирал 50%, затем 66%, затем 75% и т.д....Так что игра была не настолько жестока как могла быть =).

Код просчета активации бонусов выглядит так (здесь при включенном RPG-моде работали все бонусы, а при выключенном, что не вошло в листинг снизу, только шило). Я еще поправил отступы, чтобы было легче отличить просчет одного бонуса от другого, хотя, наверное, зря я это сделал. Нарушил первозданность щей. Но все комментарии первозданны.

if(RPGMOD==true)
{
switch(bonusiObjects[i29].type)
{
case 0://рандом
bonusiObjects[i29].shans=hge->Random_Int(1,100);


//+ СКОРОСТЬ
if(bonusiObjects[i29].shans>=1&&bonusiObjects[i29].shans<=20+luck1/2) {bonusi1[N_BONUS1].znachenie=skorost1*0.1f; bonusi1[N_BONUS1].type=1; bonusi1[N_BONUS1].id=i29; speed+=bonusi1[N_BONUS1].znachenie; bonustext1_1(); //появление статуса +10% скорость statusi1[N_BONUSSTATUS1].akt=1; statusi1[N_BONUSSTATUS1].type=1; statusi1[N_BONUSSTATUS1].id=i29; statusi1[N_BONUSSTATUS1].queue=status_queue1; status_queue1++; N_BONUSSTATUS1++; if(N_BONUSSTATUS1>=MAX_BONUSSTATUS-1)
{N_BONUSSTATUS1=0;}

N_BONUS1++;
if(N_BONUS1>=MAX_BONUSI-1)
{N_BONUS1=0;}

}//закрытие про шило


//ДАУНИЗМ
if(bonusiObjects[i29].shans>=21+luck1/2&&bonusiObjects[i29].shans<=40) { daun1++; bonusi1[N_BONUS1].type=2; bonusi1[N_BONUS1].id=i29; bonustext1_2(); //появление статуса даунизма statusi1[N_BONUSSTATUS1].akt=1; statusi1[N_BONUSSTATUS1].type=2; statusi1[N_BONUSSTATUS1].id=i29; statusi1[N_BONUSSTATUS1].queue=status_queue1; status_queue1++; N_BONUSSTATUS1++; if(N_BONUSSTATUS1>=MAX_BONUSSTATUS-1)
{N_BONUSSTATUS1=0;}

N_BONUS1++;
if(N_BONUS1>=MAX_BONUSI-1)
{N_BONUS1=0;}}


//УЛИТКА
if(bonusiObjects[i29].shans>=41&&bonusiObjects[i29].shans<=60-luck1/2) { bonusi1[N_BONUS1].znachenie=skorost1*0.2f; bonusi1[N_BONUS1].type=3; bonusi1[N_BONUS1].id=i29; speed-=bonusi1[N_BONUS1].znachenie; bonustext1_3(); //появление статуса замедления statusi1[N_BONUSSTATUS1].akt=1; statusi1[N_BONUSSTATUS1].type=3; statusi1[N_BONUSSTATUS1].id=i29; statusi1[N_BONUSSTATUS1].queue=status_queue1; status_queue1++; N_BONUSSTATUS1++; if(N_BONUSSTATUS1>=MAX_BONUSSTATUS-1)
{N_BONUSSTATUS1=0;}

N_BONUS1++;
if(N_BONUS1>=MAX_BONUSI-1)
{N_BONUS1=0;}}


//УМНИК
if(bonusiObjects[i29].shans>=61-luck1/2&&bonusiObjects[i29].shans<=80) { umnik1++; bonusi1[N_BONUS1].type=4; bonusi1[N_BONUS1].id=i29; bonustext1_4(); //появление статуса statusi1[N_BONUSSTATUS1].akt=1; statusi1[N_BONUSSTATUS1].type=4; statusi1[N_BONUSSTATUS1].id=i29; statusi1[N_BONUSSTATUS1].queue=status_queue1; status_queue1++; N_BONUSSTATUS1++; if(N_BONUSSTATUS1>=MAX_BONUSSTATUS-1)
{N_BONUSSTATUS1=0;}


N_BONUS1++;
if(N_BONUS1>=MAX_BONUSI-1)
{N_BONUS1=0;}}
//НИЧЕГО
if(bonusiObjects[i29].shans>80)
{bonustext1_0();}

bonusiObjects[i29].akt=0;
bonusiObjects[i29].akt_time=perez_akt;
bonusiObjects[i29].akt_time_milli=GetTime();

break;
case 1://шило в жопу

bonusi1[N_BONUS1].znachenie=skorost1*0.1f;
bonusi1[N_BONUS1].type=1;
bonusi1[N_BONUS1].id=i29;
speed+=bonusi1[N_BONUS1].znachenie;
bonustext1_1();

//появление статуса +10% скорость
statusi1[N_BONUSSTATUS1].akt=1;
statusi1[N_BONUSSTATUS1].type=1;
statusi1[N_BONUSSTATUS1].id=i29;
statusi1[N_BONUSSTATUS1].queue=status_queue1;
status_queue1++;
N_BONUSSTATUS1++;
if(N_BONUSSTATUS1>=MAX_BONUSSTATUS-1)
{N_BONUSSTATUS1=0;}


N_BONUS1++;
if(N_BONUS1>=MAX_BONUSI-1)
{N_BONUS1=0;}

bonusiObjects[i29].akt=0;
bonusiObjects[i29].akt_time=perez_akt;
bonusiObjects[i29].akt_time_milli=GetTime();

break;}

}//RPGMOD==true

Также для новичков покажется интересной реализация вращения изображения бонуса. Так как движок двухмерный, а сильно хотелось, чтобы бонусы красиво медленно вращались при выпадении, было придумано вот что. Имелась функция рендера спрайта (картинки) по четырем координатам - пара x,y верхнего левого угла и пара x,y нижнего правого угла. По сути эта функция растягивает картинку до нужных размеров. Когда выпадал бонус, то просто во второй паре координат x постепенно уменьшался, что в итоге приводило к тому, что абсцисса правого нижнего угла становилась меньше левого, т.е. они как бы менялись местами. В итоге картинка рендерилась "наизнанку", будто произошло вращение на 180 градусов. Затем абсцисса уменьшалась до определенного предела и запускался обратный процесс - она увеличивалась. Так как изменение абсциссы происходило постепенно, то это действительно выглядело как вращение.

Игровой уровень

Игровой уровень состоит из трех частей - стартовый экран с поляной, затем, если пройти далее появляются пни, затем трава темнеет и где-то с 40% уровня начинается лес из небольшого количества случайно расставленных деревьев. Также сразу после стартового экрана на поляне лежит алкаш - просто хотелось немного разбавить уровень. На середине уровня стоит ободряющий указатель "еще столько же".

alkash1 - их тоже планировалось несметное множество
alkash1 - их тоже планировалось несметное множество

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

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

Вообще проходится уровень довольно тяжело, но об этом поподробнее ниже в разделе "Геймплей".

Дополнительные детали и конец проекта

Главное меню

Мне всегда ненравилось денлать менюшки для игр. Даже сейчас, разрабатывая свой новый проект на Unity, с адаптивностью и внешним видом разных меню одни проблемы. Что уж говорить про динамическое создание меню в коде. В HGE оно было сделано вполне неплохо, насколько это возможно. Но я пошел своим путем. Никакой структуры, просто бешеное дерево условий. Не привожу здесь код, так как Вы, наверное, уже устали.

Поправка FPS

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

Итак, путем экспериментов была выведена данная формула, которая, к слову, сравнительно неплохо справлялась со своей задачей:

float lag=((1+dt*12)*sqrt(dt/0.006f));
float lag_p=((1+dt*10)*sqrt(dt/0.01f))*((x+1200)/2400);

//чтоб игрок1 тормозил и не улетал за экран
dx*=tormoz; dy*=tormoz;
float lag_igrokam=((1+12*dt)/(1+5*dt))*sqrt(sqrt(sqrt(140/(float)fps)));

Ох, я забыл. Формулы было всего аж три штуки! Первая формула (lag) рассчитана на все, кроме пуль и перемещений игроков. Вторая,lag_p, применялась для ускорения или замедления пули, ну а последняя, самая простая - для перемещения игроков. Здесь dt - это 1/fps, обычно это значение вроде 0.001-0.015, но при сильных лагах оно подскакивало до десятых долей и намного ускоряло передвижение игровых сущностей. Таким образом, игра как бы лагала, но за один длинный кадр, например, то же игрок, телепортировался на приличное расстояние.

Алгоритм художника

Вообще-то в движке HGE присутствует z-buffer, и таким образом, можно было придавать спрайтам различное значение z-index. Однако, в то время я не знал основ компьютерной графики, и, что такое z-index в частности. Однако, интуитивно я догадался до алгоритма художника, и реализовал его...не самым лучшим образом.

Так выглядит тасовка элементов массива внутри:

for(int ii8=0;ii8<=N_MYASO;ii8++)
{
if(myasko[ii8].akt==1)
{Y[N_Y].type=3;
Y[N_Y].y=myasko[ii8].niz-15;
Y[N_Y].id=ii8;
N_Y++;}
}

for(int ii9=0;ii9<=N_UHI;ii9++)
{
Y[N_Y].type=4;
Y[N_Y].y=uhiObjects[ii9].niz-15;
Y[N_Y].id=ii9;
N_Y++;
}

А вот здесь вывод на экран, на экран выводились объекты с определенным id в порядке, в которм они присутствовали в массиве A.

for(int I=0;I<=N_Y;I++) { ... switch(myasko[A[I].id].seed) {case 1:krovyak_myaso1->RenderEx(myasko[A[I].id].x1+Xz,myasko[A[I].id].y1,myasko[A[I].id].gradus,myasko[A[I].id].scaleX,0);

Загрузка ресурсов

Загрузка ресурсов - это одно из любимейших мест публики, после того как я опубликовал свой исходный код. Просто полюбуйтесь на это:

if(!menu_font||!oboima_text||!info||!infoR||!infoD||!infoBR||
            !oblaka1_tex||!fon1_tex||!fon2_tex||!fon3_tex||
            !galka_tex||!galka_menu_tex||!strelka_menu_tex||!strelka_menu_D_tex||
            !znak_myasnik_tex||!znak_strelok_tex||!znak_razrushitel_tex||!znak_tehnik_tex||
            !status_opit_tex||!status_udar_tex||!status_status_tex||!opit_okno_vibora_tex||
            !okno_lvl_progress_tex||!okno_lvl_progress_red_tex||!okno_lvl_progress_green_tex||!okno_lvl_polzunok_tex||
            !snd||!ak_reload1||!ak_reload2||!pm_fire||!pm_reload1||!pm_reload2||
            !pp19_fire||!pp19_reload1||!pp19_reload2||!fn_f2000_fire||!tt_fire||
            !mac_fire||!mac_reload1||!mac_reload2||!webley_fire||!webley_reload1||!webley_reload2||!milkor_fire||
            !fn_five_seven_fire||!winch_fire||!drob_reload1||!drob_pompa||!vzriv_grena1||
            !rocket_fire||!rocket2_fire||!rocket_polet||!rocket2_polet||
            !myaso_upalo1||!myaso_upalo2||!myaso_upalo3||!myaso_upalo4||!myaso_upalo5||!myaso_upalo6||!myaso_upalo7||
            !myaso_upalo8||
            !myaso_razriv_user1||!myaso_razriv_user2||
            !menu_sound||!menu_choose||
            !shot1||!headshot1||!headshot2||!headshot3||!headshot4||!headshot5||!headshot6||!headshot7||!headshot8||
            !ssik1||!ssik2||!ssik3|!ssik4||
            !ptenec_death1||
            !ak||!ak2||!ak_upgraded||!ak2_upgraded||!w_pm_tex||!w_pp19_vityaz_tex||!w_fn_f2000_tex||!w_fn_f2000_upgraded_tex||
            !w_tt_tex||!w_rpk_tex||!w_mac_tex||
            !w_winchester_tex||!w_winchester_anime_tex||!w_rpk47_tex||!w_glok_tex||!w_glok2_tex||!w_rgd5_tex||!w_milkor_tex||
            !w_panzer_tex||!w_panzer_out_tex||!w_webley_tex||!w_fn_five_seven_tex||!w_granata_podstvol_tex||!w_granata_panzer_tex||
            !w_qlz87_pushka_tex||!w_qlz87_trenoga_tex||!katana_udar_sleva_tex||
            !blood1_tex||!blood2_tex||!blood3_tex||!blood_shot1_tex||
            !blood_plyam1_tex||!blood_plyam2_tex||!blood_plyam3_tex||!blood_luzha1_tex||
            !blood_myaso1_tex||!blood_myaso2_tex||!blood_myaso3_tex||!blood_myaso4_tex||!blood_myaso5_tex||
            !blood_zayac_noga1_tex||!blood_zayac_noga2_tex||!blood_zayac_noga3_tex||!blood_zayac_noga4_tex||
            !blood_zayac_rebra1_tex||!blood_zayac_rebra2_tex||
            !blood_vzriv1_a_tex||!blood_vzriv1_b_tex||!blood_vzriv1_c_tex||!blood_vzriv1_d_tex||!blood_vzriv1_e_tex||
            !blood_vzriv1_e2_tex||
            !player1_myasnik_gogranata_ruka1_tex||!player1_myasnik_gogranata_ruka2_tex||
            !player1_strelok_gogranata_ruka1_tex||!player1_strelok_gogranata_ruka2_tex||
            !player1_razrushitel_gogranata_ruka1_tex||!player1_razrushitel_gogranata_ruka2_tex||        
            !player1_tehnik_gogranata_ruka1_tex||!player1_tehnik_gogranata_ruka2_tex||
            !player1_myasnik_tex||!player1_strelok_tex||!player1_razrushitel_tex||!player1_tehnik_tex||
            !player1_myasnik_ruka1_udar_sleva_tex||!player1_strelok_ruka1_udar_sleva_tex||
            !player1_razrushitel_ruka1_udar_sleva_tex||!player1_tehnik_ruka1_udar_sleva_tex||
            !player1_myasnik_ruka1_pistol_tex||!player1_strelok_ruka1_pistol_tex||
            !player1_razrushitel_ruka1_pistol_tex||!player1_tehnik_ruka1_pistol_tex||
            !player1_myasnik_ruka1_vintovka_tex||!player1_strelok_ruka1_vintovka_tex||
            !player1_razrushitel_ruka1_vintovka_tex||!player1_tehnik_ruka1_vintovka_tex||
            !player1_myasnik_ruka1_winch_tex||!player1_strelok_ruka1_winch_tex||
            !player1_razrushitel_ruka1_winch_tex||!player1_tehnik_ruka1_winch_tex||
            !player2_strelok_tex||!player2_gogranata_ruka1_tex||!player2_gogranata_ruka2_tex||
            !player2_strelok_ruka1_udar_sleva_tex||!player2_strelok_ruka1_vintovka_tex||!player2_strelok_ruka1_pistol_tex||
            !zayac_go_tex||!zayac_uhi_k_tex||!zayac_uhi_s_tex||!zayac_uhi_tex||
            !zayac_boshka_tex||!zayac_boshka_bezuh_tex||
            !volk_go_tex||!volk_trup1_a_tex||!volk_trup1_b_tex||!volk_trup1_c_tex||
            !medved_go_tex||!medved_boshka1_tex||
            !medved_trup1_a_tex||!medved_trup1_b_tex||!medved_trup1_c_tex||!medved_trup1_d_tex||!medved_trup1_e_tex||!medved_trup1_f_tex||
            !medved_trup1_a_bezboshki_tex||!medved_trup1_b_bezboshki_tex||!medved_trup1_c_bezboshki_tex||
            !medved_trup1_d_bezboshki_tex||!medved_trup1_e_bezboshki_tex||!medved_trup1_f_bezboshki_tex||
            !ptenec_go_tex||!ptenec_wait_tex||!ptenec_vpolete_tex||!ptenec_vpolete_reverse_tex||!ptenec_trup1_tex||
            !ptenec_boshka_vzriv1_a_tex||!ptenec_boshka_vzriv1_b_tex||!ptenec_boshka_vzriv1_c_tex||
            !ptenec_boshka_vzriv1_d_tex||!ptenec_boshka_vzriv1_e_tex||
            !RPG_healer_tex||!RPG_illusionist_tex||!RPG_teleporter_tex||
            !bonus_shilo_tex||!bonus_this_tex||
            !bonus_shilo_text_tex||!bonus_this_text_tex||!bonus_daun_text_tex||!bonus_ulitka_text_tex||!bonus_umnik_text_tex||
            !bonus_shilo_status_tex||!bonus_daun_status_tex||!bonus_ulitka_status_tex||!bonus_umnik_status_tex||
            !zayac_trup1_a_tex||!zayac_trup1_b_tex||!zayac_trup1_c_tex||!zayac_trup1_d_tex||
            !zayac_trup1_a_bezuh_tex||!zayac_trup1_b_bezuh_tex||!zayac_trup1_c_bezuh_tex||!zayac_trup1_d_bezuh_tex||
            !zayac_trup1_a_bezboshki_tex||!zayac_trup1_b_bezboshki_tex||!zayac_trup1_c_bezboshki_tex||
            !zayac_trup1_d_bezboshki_tex||
            !zayac_go_bezuh_tex||!ogon1||
            !blood_ssit_tex||!RPG_healing_tex||!vzriv_grena_tex||!vzriv_ogon_grena_tex||
            !alkash1_tex||!derevo1_tex||!penek1_tex||
            !znak_polputi_tex||!polosa_finish_tex)
        {
            // If one of the data files is not found, display
            // an error message and shutdown.
            MessageBox(NULL, "Ошибка загрузки игровых файлов.", "Ошибка, пля!!!", MB_OK | MB_ICONERROR | MB_APPLMODAL);
            hge->System_Shutdown();
            hge->Release();
            return 0;
        }

Данное условие проверки загрузки всех ресурсов без исключения было даже опубликовано на говнокоде. Жалко только, что условие так и не поместилось целиком - ограничение сайта =(. Зато это был лучший говнокод апреля 2012 года! Можно даже было при запуске игры поставить бейджик в главном меню, как это сделано в том же WoT (лучшая игра КРИ и т.д.).

Еще веселее было наблюдать комментарии людей, утверждающих, что этот код был сгенерирован каким-то препроцессором (не могу уже найти ссылку). Положа сердце на руку скажу - никакого препроцессора, только хардкор, только ручная работа!

Названия переменных продиктованы здравым смыслом (о как!), например medved_trup1_a_bezboshki_tex и medved_trup1_b_bezboshki_tex это просто два последовательных кадра (a и b) первой вариации анимации падения медведя без бошки. Предполагалось, что анимаций будет больше, я везде оставлял простор для творчества, но, увы, анимации отбирали кучу времени, так как я не художник, и приходилось делать все одному. Хотя нет, с анимашками мне немного помог друг (тоже не профессионал), который, кстати недавно написал неплохой текстовый квест.

Номенклатура переменных

Сквозь всю программу виднеется еще один недостаток: имена переменных как на русском (транслите), так и на английском языке. Причем все это вкупе составляет дикую смесь. Ниже представлено объявление звуковых спецэффектов для падающего мяса, попаданий в голову врагам и брызг крови. Озвучка взята частью из Seal Hunter, частью из самодельной озвучки для него же.

HEFFECT myaso_upalo1;
HEFFECT myaso_upalo2;
HEFFECT myaso_upalo3;
HEFFECT myaso_upalo4;
HEFFECT myaso_upalo5;
HEFFECT myaso_upalo6;
HEFFECT myaso_upalo7;
HEFFECT myaso_upalo8;

HEFFECT myaso_razriv_user1;//здесь user - не юзер, а усёр
HEFFECT myaso_razriv_user2;

HEFFECT shot1;//а далее нормальный английский
HEFFECT headshot1;
HEFFECT headshot2;
HEFFECT headshot3;
HEFFECT headshot4;
HEFFECT headshot5;
HEFFECT headshot6;
HEFFECT headshot7;
HEFFECT headshot8;

HEFFECT ssik1;//опять транслит: ссык
HEFFECT ssik2;
HEFFECT ssik3;
HEFFECT ssik4;

Как видите, какого-то общего стандарта нет. Переменные называются то на нормальном английском, то на тарабарщине, что, несомненно, является минусом. Хотя, возможно, для начинающих это и не критично, но, честно, более-менее профессионалов от такого коробит. Если бы даже у игры присутствовала какая-нибудь нормальная архитектура, и игру можно было бы выложить на GitHub, чтобы ее кто-то дальше развивал, то этот "кто-то" явно должен знать русский язык. А это сильно сужает аудиторию разработчиков, способных "подхватить" Ваш проект. В общем, не делайте так.

Также у меня была паранойя по поводу переменных-счетчиков циклов. Я почему-то подумал, что они глобальные, и чтобы случайно не было конфликта начал нумеровать их: i, i1, i2, i3...Где-то на сто-каком-то цикле я сбился со счету и начал писать переменные ii1, ii2, ii3, чтобы наверняка. Потом пошли iii1,iii2... До iiii дело, к счастью, не дошло.

Комментарии

Иногда было лень стирать старые неактуальные комментарии, из-за чего происходило нечто подобное:

N_BLOOD++;

if(N_BLOOD==MAX_BLOOD-1)
{
NUMBER_BLOOD=N_BLOOD;    
N_BLOOD=0;
peregruzheno_blood=1;}//  ААААА ФАК МОЙ МОСК ТУТ ВСЕГДА ДОЛЖЕН БЫТЬ 0!!!!!

krovyakObjects[N_BLOOD].seed=hge->Random_Int(101,103);

Процесс отладки

На самом деле весь секрет какой-никакой работоспособности игры состоит в сути процесса ее отладки. Конечно же, я тогда еще не умел толком пользоваться дебаггером, хотя подстмотреть значения переменных мог. Секрет вот в чем: после непродолжительного написания кода (примерно 50-100 строк) следовала немедленная параноидальная отладка игры в стиле "А вдруг что-то сломалось?" путем ее запуска и, собственно, игры в игру. Причем если код писался, скажем, полчаса, то игра проверялась после этого где-то час-полтора. Иногда и дольше, если я заигрывался. Проверялось все путем прохождения уровня. При этом обязательно надо было чтобы кого-то разорвало, чтобы выбежали все доступные враги и т.д...Вдруг че?

Таким образом я заимел вредную привычку писать код мелкими кусками и сразу его проверять, от которой я избавился довольно-таки недавно, примерно года полтора-два назад. Я назвал это "синдром щей". Крайне неприятная штука, основанная на неуверенности в себе и своем коде, которая затягивает разработку как минимум раза в два. Возможно, я сейчас где-то неправ, и для новичка оно было полезно, ведь количество ошибок на тысячу строк кода намного выше, чем у опытных прогеров. Но далее было реально неудобно работать с таким синдромом.

Кстати, как сейчас помню, я где-то к середине разработки "Щей" (года через пол) нашел функцию "Поиск и замена". Вроде бы я должен был быть в восторге, но я отчетливо понимал, что это всего-лишь замена текста, которая может привести к замене части имени какой-нибудь переменной. Поэтому пользовался этой функцией редко, а менял все значения руками. Так моей душе было спокойнее. Я чувствовал, что код не простой, и лучше "держать ситуацию в руках". Это паранойя?

Все хорошо

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

В общем по ходу разработки, когда иной раз за день код вырастал на пару-тройку тысяч строк, я чувствовал неподдельную гордость за себя. Более двадцати тысяч строк кода! В одном файле! Сложно, но я не сдавался. Я был крут!

Все было в этом духе, пока я не нашел, что такое рефакторинг...

Рефакторинг и конец проекта

Собственно, кто не знает, рефакторинг - это оптимизация кода с целью улучшить его гибкость и поддерживаемость. Функционал при этом должен сохраниться. Поначалу, конечно, все ломается, но потом все должно стать на круги своя. У меня даже есть любимая гифка про рефакторинг, которая подробно отражает его суть.

Так вот, столкнувшись с описание такого явления, я подумал - а почему бы и мне не попробовать? У меня на тот момент было 25000 строк отборного кода в одном файле. Про разбиение на файлы я даже и не думал - "та еще морока, зачем?", а вот сделать, например, добавление нового оружия удобнее и быстрее - запросто. В общем, я начал рефакторить код, вынес все параметры оружия в глобальные массивы, определение которых поставил в начале файла, чтобы было удобно. В общем, поработал на славу:

//св-ва оружий. формат: АК,ПМ,ПП-19'Витязь',FN_F2000,TT,РПК,MAC,WINCHESTER,РПК47,ГЛОК,РГД-5,катана,MILKOR-MGL,
                    //   0  1       2          3     4   5   6      7        8    9        10     11      12     
//Panzerfaust3, Webley mk4, FN Five-Seven
//     13           14           15
int oboim[16]={30,8,30,30,8,45,50,8,75,17,1,0,6,1,6,20};
int oboim2[16]={30,8,30,30,8,45,50,8,75,17,1,0,6,1,6,20};
int oboima_alt[16]={1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0};
int oboima2_alt[16]={1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0};
int damage[16]={70,20,40,55,30,72,35,75,72,20,300,20,270,550,50,32};
float weight[16]={3,0.4f,2.2f,3.6f,0.5f,4.5f,2,3.2f,4.8f,0.7f,0.2f,0.5f,5.3f,8,0.7f,0.5f};
//            ак пм  пп19 ф2000  ТТ   рпк   мас  winch рпк47  глок ргд-5 катана MILKOR-MGL P3 Wmk4  FN5-7
int cena[16]={15000,0,7500,27500,1000,24000,5000,7500,22000,1500,3500,3000,20000,40000,2000,3000};
int cena_up[2]={12500,9000};
int reload_time[16]={2,1,1,2,1,3,1,0,3,1,0,0,0,3,1,1};
int reload_time_milli[16]={0,0,500,0,0,0,500,0,0,0,0,500,0,0,0,0};
float weapon_x1[16];
float weapon_y1[16];
float weapon_x2[16];
float weapon_y2[16];
float weapon2_x1[16];
float weapon2_y1[16];
float weapon2_x2[16];
float weapon2_y2[16];
float w_x1[16]={-55,-32,-46,-60,-35,-72,-30,-72,-73,-35,0,0,-60,-70,-40,-35};
float w_y1[16]={-15,-12,-15,-22,-13,-15,-15,-15,-15,-13,0,0,-20,-21,-16,-14};
float w_x2[16]={  8, -7,  7,  4, -7, 10,  3, 14,  9, -7,0,0, 10, 30, -8, -7};
float w_y2[16]={ 10,  8, 13,  6,  8, 12, 15,  9, 25,  8,0,0,  4,  4,  2,  5};


int radius_vzriva[4]={0,200,200,350};
int damage_gren[4]={0,300,270,500};

int random_rasst_min[16]={400,200,300,450,250,400,300,0,350,250,0,0,0,0,350,350};
int random_rasst_max[16]={600,400,500,650,500,500,450,0,450,400,0,0,0,0,500,500};
float random_value1[16]={5,6,5,3,5.5f,5.5f,5.5f,0,6,6,0,0,0,0,6,4};
float random_value2[16]={9,12,10,6,11,9,12,0,10,11.5f,0,0,0,0,12,10};

float w_opit_zayac[16]={1.2f,2,1.4f,0.8f,1.8f,1,1.6f,1.2f,1,2,1.5f,1.6f,1,0.8f,1.4f,1.2f};
float w_opit_zayac_vschiii[16]={1.2f,2,1.4f,0.8f,1.8f,1,1.6f,1,1,2,1.5f,1.6f,1,1,1.2f,1.4f};
float w_opit_volk[16]={1.2f,2,1.4f,0.8f,1.8f,1,1.6f,1,1,2,1.5f,1.8f,1.2f,1,1.5f,1.5f};
float w_opit_medved[16]={1.2f,2,1.4f,0.8f,1.8f,1,1.6f,1,1,2,1.8f,2,1.4f,1,1.8f,1.6f};

Как видите, стало намного удобнее...Или нет?

Но вот, что меня поразило - за пару часов код уменьшился с 25000 до 20000 строк! При той же функциональности! И тут я понял, что что-то с проектом не так. Собственно, насколько я помню, этот рефакторинг и был концом проекта. Я тянул этот проект полтора года, вплоть до окончания школы, и понял, что дальше разработка невозможна - мотивация была на исходе, меня тянула вперед только гордость за себя и поддержка друзей (играть в игру с багами и выискивать их было очень фаново). И тут до меня доходит, что все что я написал - говнокод. Так что плодами рефакторинга, к сожалению, я практически так и не воспользовался.

Что получилось в итоге?

Геймплей игры

Геймплей игры получился отменным. Отменно отстойным. Это дичайший хардкор, который  еще как-то проходил во время разработки (еще бы, я по сути задротил в свою игру, пока дебажил ее каждый раз).

Начнем хотя бы с того, что при нажатии на кнопку покупки открывалось меню, но игра не приостанавливалась! Эта очевидная недоработка была исправлена в ЩИ2, о которой речь пойдет в другой раз.

Далее. Монстры...Кхм, ладно, звери - они быстро становились неубиваемыми, если не прокачивать атаку в RPG-режиме игры. К тому же пули иногда пролетали насквозь и не наносили урона.

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

Ну и самое главное. Недавно сели поиграть в нее с друзьями. Прошли до середины уровня. Кое-как. В итоге все начало лагать, так как по уровню раскидано множество мяса, и в свою должность вступили вышеприведенные формулы просчета и поправки лагов: при движении по краю экрана вперед нас откатывало инерцией назад, при движении назад, от края экрана нас откатывало вперед. Все. Конец. Мы не смогли тронуться с места!

Реакция общественности

*здесь о реакции общественности расскажет Ризитас*

Нарушенные принципы программирования

Принцип YAGNI

Принцип YAGNI гласит "You Are not Going to Need It", что в переводе означает "Вам это не понадобится". Если что-то нужно в данный момент, например мы добавляем новый тип оружия - добавляем новый тип оружия, а не закладываем основу для целой классификации. Игра изобилует неиспользованными возможностями, которые, по сути, особо и не нужны.

Например, вот коды изображений трупов и их частей, а также предметов в игровом уровне:
hgeSprite* krovyak_vzriv1; //посреди вот таких вот объявлений кроется документация
/*ТИПЫ ТРУПОВ
1-ЗАЯЦ 1
2-УШИ(АНИМЕ)
3-УШИ(СПРАЙТ)
300-ВСПЛЕСК ОТ ВЗРЫВА1
301-ВСПЛЕСК ОТ ВЗРЫВА1/2
330-ВСПЛЕСК БОШКИ ПТЕНЦА1
400-КРОВЯК1
401-КРОВЯК2
402-КРОВЯК3
500-МЯСО1
501-МЯСО2
502-МЯСО3
503-МЯСО4
504-МЯСО5
505-ЗАЯЧЬИ РЕБРА1
506-ЗАЯЧЬИ РЕБРА2
507-ЗАЯЧЬЯ НОГА1
508-ЗАЯЧЬЯ НОГА2
509-ЗАЯЧЬЯ НОГА3
510-ЗАЯЧЬЯ НОГА4
511-МЕДВЕЖЬЯ БОШКА1
6-БОШКА
7-БОШКА БЕЗ УШЕЙ
800-ПЛЯМ 1
801-ПЛЯМ 2
802-ПЛЯМ 3
9-ЛУЖА 1 
10-ВОЛК 1
20-МЕДВЕД 1
30-ПТЕНЕЦ 1
31-ЛЕТУЧИЙ ПТЕНЕЦ=)

ТИПЫ БОНУСОВ
0-ВОПРОС
1-ШИЛО
*/

Как видите, коды красноречиво говорят, насколько я замахнулся на разнообразие. Я хотел нарисовать не менее ста вариаций мяса, всплесков крови, оторванных ушей, голов и тому подобного. Это, конечно, не самое страшное из того, что нарушает принцип YAGNI, но, я думаю, суть ясна: тратятся усилия на вещи, которые может быть, когда-то, понадобятся. Если бы у меня был художник...

Принцип NIH

Принцип Not Invented Here. Очень заразная болезнь начинающих программистов, граничащая с паранойей. Суть болезни в том, что программист не переносит на дух готовые решения и библиотеки, а каждый раз "создает велосипеды" самостоятельно. В лучшем случае будет потрачена только уйма времени. В худшем - велосипед не заработает так, как это делает готовая библиотека.

В игре я использовал HGE, как основу для отрисовки графики, но всю физику реализовывал сам. Если бы я взял готовый движок физики, разработка была бы намного приятнее и быстрее. Другое дело, что мне, возможно, пришлось бы пользоваться ООП, ведь библиотека диктует свой стиль использования кода, а это было в то время очень непонятно и неприятно. Да и разбираться в чужих API я только-только учился. В итоге моя лень разобраться в этом породила еще больше работы.

Принцип KISS

Принцип KISS гласит "Keep It Simple Stupid!". Чем проще концепция или сама система для понимания, тем лучше. Как видите, в данном случае простота хуже воровства. Ситуация двоякая: с одной стороны не было использовано сложных концепций, вроде замудренных паттернов проектирования, лишних классов и сущностей, но с другой стороны сама система стремительно росла "вширь" и становилась все сложнее и сложнее для восприятия. Поэтому этот принцип должен быть использован осторожно, а не воспринят буквально!

Принцип DRY

Самое мощное оружие говнокодера
Самое мощное оружие говнокодера

Don't Repeat Yourself - тут все просто. Если код повторяется более двух раз - создаем более абстрактный код. Выносим код в функцию и параметризуем его, создаем класс...что угодно! У меня код просчета врагов копировался минимум 4 раза. И вообще, Ctrl+C, Ctrl-V было самым мощным инструментом разработки (как я тогда считал). Ничто не могло сравниться с копированием 2000 строк спагетти-кода и замены одних магических значений переменных на другие.

Паттерны программирования

Что? Вы серьезно? Какие паттерны? Я и о классах тогда не знал.

Немного графики

Задний фон

fon
Первая приятная текстура травы, которую я использовал для фона игрового уровня

С задним фоном были проблемы. Текстура травы выбивалась из общего стиля и выглядела как нелепо вклеенная фотография. Конечно, выглядела она приятно и приемлемо, но не совсем идеально.

Чтобы добавить объемности игре я сначала решил добавить небо (скриншот был в описании игрового уровня). Это была натянутая длинная текстура с шириной намного больше экрана, которая медленно постепенно перемещалась, в зависимости от местонахождения игрока на уровне. Выглядело вполне приятно, но это сломало геймплей - стало совсем легко играть, так как зайцы теперь бегали не по всей площади поля, а по оставшимся 70-80%. Хотя, сначала они бегали и по небу, да.

fon_trava_pojuxlaya
Конечный вариант мультяшной пиксель-арт текстуры

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

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

Шрифт

Для игры я использовал свой фирменный шрифт, который я разработал в фотошопе. Конечно же на основе Comic Sans! Кстати, здесь отличная статья на английском, почему Вы не должны ненавидеть этот шрифт.

bonus_shilo_text bonus_ulitka_text

Объекты уровня

Ты на пенек сел - должен был косарь отдать! (с)
Ты на пенек сел - должен был косарь отдать! (с)

Больше всего времени, из статичных рисунков, я потратил на рисование дерева. Не помню точно, сколько ушло времени, но примерно часов 5 точно. Это, конечно же, был пиксель-арт, который я рисовал "из головы".

Послесловие

Какие уроки можно вывести из этого проекта?

Во-первых, ставьте реальные и четкие цели. Если не можете оценить цель по реальности, попробуйте ее хотя бы четко очертить. Если и это невозможно - перед Вами очередная "что бы такого запилить чтоб все обалдели" цель. Избегайте таких целей.

Всегда при постановке цели спрашивайте себя "зачем?". Если ответа нет - то это и не цель, это прихоть. Если ответ размазан - будьте честны с самим собой, скорее всего эта цель просто потешит Ваше ЧСВ.

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

Да, есть проблема того, что можно и не знать того факта, что Вы что-то не знаете или недопонимаете. Но как раз с такими вот моментами я и рекомендую разобраться.

В-третьих, начинайте с малого, если Вы новичок. А в разработке игр это правило надо высечь золотыми буквами. Сейчас, даже имея за плечами восьмилетний опыт программирования я бы не взялся за такой проект. Хочется сделать что-то казуальное и простое, как тетрис (все у меня через задницу!), так как это быстрее разрабатывается и легче окупается.

Похожие записи

Оставить комментарий