Своя игра. Часть 3

    Дисклеймер


    Здесь статья о том, как мы с друзьями писали игру. Мы ее дописали и продаем, но денег она нам не приносит. Однако, нам было очень интересно и весело ее делать, и я решил поделиться своими воспоминаниями. В статье будет минимум технических подробностей, код я выкладывать не буду, так как учиться на нем бессмысленно. Это код любителей, а не профессионалов, там ошибка на ошибке. Никто из нас не имеет АйТи образования и никто профессионально никогда не занимался программированием. Я иногда буду выкладывать технические подробности, так как без этого никуда. Прошу читателей также не постить комментарии о том, какие мы лохи, так как это обидно читать. Хоть и правда.

    Другие части статьи

    Часть 1
    Часть 2

    Глава 5.1. Античность


    Год на работе выдался тяжелым. Но игрой занимались в основном Федор Михалыч и Димон, поэтому нагрузка была полегче, и я сконцентрировался на ребенке и работе. И тем не менее, босс позвонил мне из головного офиса в Париже и сказал, что банк закрывает мое направление деятельности в Москве.

    Это плохо.

    — Но не бойся, тебя мы бросать не собираемся. Как-никак, 7 лет вместе. Мы вместе с тобой начинали этот бизнес в России. Поедешь работать в Париж?
    — В Париж? – я не стал говорить что давно хотел поработать в Париже вот так, сразу. Надо было набить себе цену, — Это сложный вопрос, мне надо поговорить с женой. Конечно, поеду, я давно мечтал поработать в Париже.
    — Но есть нюансы, — сказал босс после короткой паузы.
    — А?
    — В Париж нельзя.
    — Понял, — ответил я. Я ничего не понял.
    — Поедешь в Лондон? Там поработаешь немного, и переведем тебя в Париж.
    — Мне надо поговорить с женой, — ответил я. – Поеду.

    image

    Так, через полгода мы с семьей оказались в Лондоне. Работать над игрой из разных стран было тяжело, еще тяжелее было то, что перевод в Париж стал на рельсы, и в Лондоне мы жили на чемоданах, ожидая документов и подводного поезда. Для игры, в свободное от работы время, я делал интерфейс. Рисовал кнопки, иконки и шрифты. Получалось страшненько, но, так как у остальных это получалось еще хуже, а инвестировать много денег в найм дизайнеров для игры после выхода настоящего икс-кома для айос уже не было никакого желания, мою работу «коллеги» принимали.

    — Страшненько, — говорил Федор Михалыч. – запиливай в игру.

    И вот в начале осени 2015 года я с семьей перехал из Лондона в Париж.



    С работой даже в Париже было не особо – банк закрыл направление не из-за того, что Москва, а по более объективным причинам. На работе я искал себе новую работу внутри любимой фирмы, а дома рисовал кнопки и картинки для ачивок. Еще в то время мы много играли в нашу игру, оттачивая баланс. Математическая модель балансировки уровня не особо работала. Ну, или мы ее просто так построили, и, к тому же, непонятна была «интересность» уровня. Поэтому, мы очень много играли и переигрывали. Было решено разделить рандомные уровни на 3 уровня сложности. Уровень сложности повышался с прогрессом игрока в игре. А прогресс мы мерили по количеству выполненных сюжетных миссий. Так мы и проводили вечера, отыгрывая разные уровни с разными конфигурациями отряда.

    Странно, но игра нам нравилась. Впрочем, есть же такая пословица, про свой малинный аромат. Где-то тогда же я, наконец, нарисовал следы пуль при стрельбе. До этого выстрелы были невидимы.

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

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



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



    Жена с ребенком решили остаться в Париже, так как, во-первых, непонятно было, взлетит ли новое бизнес-направление в Лондоне, а жизнь в Лондоне ну очень дорогая, во-вторых, дочь пошла в школу в Париже. Я снимал каморку в центре Лондона, рядом с работой, и в пятницу вечером, сразу из офиса садился на поезд в Париж, чтобы провести выходные с семьей и вернуться обратно утром в понедельник. Поезд шел почти 2,5 часа и там было очень удобно апдейтить игру. В принципе, и дома в Лондоне мне заняться было особо нечем, и это тоже помогало кодить. Так что апдейтнули мы игру быстро. Но денег не прибавилось. И мы решили больше о ней не вспоминать.

    Мы долго думали, что делать дальше. Создавать игру нам понравилось, несмотря на неудовлетворительный с финансовой точки зрения результат. Однако, усталость от долгой работы, не принесшей награды, была очевидна: мы ни на что не могли решиться. Было несколько идей, мы брались за них, но через некоторое время бросали. Такого драйва, как во время работы над первой игрой, больше не приходило. Димон уже долгое время назад вышел из проекта, и мы с Федором Михалычем думали вдвоем. Я начал писать новый движок. С поддержкой полноценного освещения, карт нормалей, и вообще, наконец, 3Д. Было непонятно, зачем. Пока я не показал то, что у меня в итоге получилось, Федору Михалычу.

    Глава 5.2. Иногда они возвращаются


    К этому моменту прошло около полутора лет с того момента, как мы запустили игру в продажу.

    — Выходит айфон 10. Если мы хотим оставить игру в сторе, надо бы ее адаптировать под сверхширокий экран. Заодно может ее на твой новый движок перенесем? – предложил Федор Михалыч.

    Это было свежо. Заняться давно доделаной бесперспективной игрой. Но почему бы и нет? Тем более, что мое путешествие в Лондон подходило к концу, на горизонте брезжило кресло в Парижском офисе, и холостой жизни приходил конец. Ах да, к тому времени у Федора Михалыча уже было 2 ребенка, а у меня ожидался второй.

    Я взялся за старый, пахнущий код.

    Он был теплым и податливым на ощупь. Мягким и обволакивающим, с кучей бессмысленных абстракций и излишне костной конкретики. Я переделал рендеринг. Федор Михалыч сделал текстуры нормалей. Я переделал шрифты. Федор Михалыч отполировал текст. Я немного перекрасил интерфейс. Вместе мы адаптировали все игровые экраны под новый айфон. Было немного скучно, но очень лампово.

    Вот так выглядела картинка, рисуемая старым движком:


    А так, новым:

    Разница не очень заметна в статике (освещение, тени, карты нормалей на полу и некоторых моделях), но бросается в глаза в динамике.

    Даже наши куцые деревца заиграли веселыми весенними красками в новой модели освещения/затенения.



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

    И вот, недавно мы выложили апдейт в аппстор. Правда, не обошлось без эксцессов и на этот раз: в спешке готовя финальный релиз, мы не проверили его работу на самом популярном формате экрана, с соотношением сторон 16 к 9. Нам это даже в голову не пришло, так как раньше все работало прекрасно, а мы всего лишь добавили поддержку сверхширокого экрана айфон Х. В результате, из-за ошибки в коде проверки сверхширокого экрана, интерфейс для айфона Х грузился на любой айфон. Заметили мы баг быстро. Быстро отправили обновление на ревью. Через сутки обновление было одобрено цензорами Эппл, и корректная версия ушла в стор. Но одно письмо с баг-репортом от пользователя мы получили. Это было удивительно — кто-то скачал игру после апдейта.

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

    Нам было хорошо с тобой.
    Спасибо за все.
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 24
    • +1
      Вплоть до последнего ждал какого-то неожиданного хэппи-энда. Однако, чудеса бывают редко, а жаль.
      • 0
        Ну, опыт получен, игра в аппсторе лежит… Я считаю, это хэппи энд. А всех денег не заработать.
      • +2

        Блин, а где ссылка-то, или хотя бы какие то ключевые слова для поиска?))

        • 0
          Не хотел, чтобы статью сочли рекламной.
          Но игра бесплатна уже, без встроенной рекламы и встроенных покупок… так что, наверное, можно?

          eggsodus
        • +3
          Это сложный вопрос, мне надо поговорить с женой. Конечно, поеду, я давно мечтал поработать в Париже.
          — Но есть нюансы, — сказал босс после короткой паузы.
          — А?
          — В Париж нельзя.
          — Понял, — ответил я. Я ничего не понял.
          — Поедешь в Лондон? Там поработаешь немного, и переведем тебя в Париж.
          — Мне надо поговорить с женой, — ответил я. – Поеду.

          Это шесть баллов по пятибалльной шкале! Серое дождливое утро стало чуть светлее, спасибо!

          • 0
            А в Париже снегопад. Очень редкое и очень радостное явление. Весь город в снегу!
          • +2
            Похоронить мы решили с выпуском последнего апдейта
            не выложенное в опенсорс не считается допохороненным.
            • 0
              мне стыдно этот код выкладывать. Честно, не жалко, а стыдно ))
              • 0
                Дело хозяйское. Хотя, например, сорцы того же vp8 поначалу тоже вызвали бурю фейспальмов, но ничего, утряслось-улеглось.
                • 0
                  Ага, статью, значит, не стыдно, а код стыдно, ай-ай-ай)
                  • +1

                    Мы не будем смотреть. Честно честно.

                • +1

                  Отличный рассказ!
                  А почему не упомянули про разработку AI в движке — там тоже порядочно интересного должно было быть!
                  Текущее поведение правда немного напоминает пресловутых крисалидов…
                  P.S. И что за главная тема играет во время сражения? Тоже работа фрилансеров?
                  Очень напоминает по атмосферности что-то в духе первого Deus Ex.

                  • 0
                    Спасибо.
                    Действительно, совершенно забыл рассказать про AI. Правда, ничего особо интересного в нем нет, просто машина состояний. Единая для всех врагов. Враг перед ходом оценивает свое здоровье, сколько его сторонников рядом и сколько противников рядом, и либо атакует, либо бежит к ближайшему стороннику.
                    Высокоуровневые противники могу отправлять сообщение на сбор всех на подмогу, тогда низкоуровневые бросают все и бегут к высокоуровневому.
                    В общем, все довольно примитивно, и было написано довольно быстро.
                    Только листинг функции очень обьемный ))

                    По музыке, к сожалению, не подскажу, ей занимались Федор Михалыч и Димон.

                    Большой и страшненький кусок кода
                    void agProcessAI() {
                        int friendsnear;//, foesnear; - self.visibleenemiescount
                        int closestfoe, closestfriend, closestvisiblefoe;
                        int closestfoedistance, closestfrienddistance, closestvisiblefoedistance;
                        int proceedfalg;
                        int currmodelnum;
                        int unitsAlive = 0;
                        // if there are animations, we do not proceed
                        // we check if there are any active animations, like running or shooting
                        proceedfalg = LEAF_YES;
                        for (int i=0; i<agVars.enemyUnitsCount; i++) {
                            // wait for ongoing animations to finish
                            if(
                               (agVars.enemyUnits[i].model.currAnimation != ANIMSTAND) &&
                               (agVars.enemyUnits[i].model.currAnimation != ANIMSIT) &&
                               (agVars.enemyUnits[i].model.currAnimation != ANIMDEAD)
                               )
                            {
                                proceedfalg = LEAF_NO;
                    #ifdef DEBUGACTIONGAME
                                leafLog(LEAF_NO, "AG: agProcessAI: waiting for enemyUnit[%i] to finish animation %i\n", i, agVars.enemyUnits[i].model.currAnimation);
                    #endif
                            }
                            if(agVars.enemyUnits[i].health > 0) {
                                unitsAlive++;
                            }
                        }
                        // if there are animations, finish method.
                        if(!proceedfalg) {
                            return;
                        }
                        // and if there are no active animations, we are free to act next
                        // do some thinking
                        switch (agVars.aiState) {
                                
                            case LEAFAIGLOBALSTATEINIT:
                                // we need to reset any AI info left from previous turn
                                for(int i=0; i<agVars.enemyUnitsCount; i++) {
                                    //agVars.enemyUnits[i].aiInbox = AIMESSAGENOMESSAGES;
                                    agVars.enemyUnits[i].aiState = LEAFAIACTIONINIT;
                                    agGetVisibilityFor(&agVars.enemyUnits[i], agVars.playerUnits, agVars.playerUnitsCount);
                                }
                                agVars.aiState = LEAFAIGLOBALACTION;
                                //break;  // GLOBAL AI INIT - no need to break, we are ok to proceed with ACTION
                            
                            case LEAFAIGLOBALACTION:
                                // main part. We check every model's AI state and do something
                                // find first model to work on
                                currmodelnum = -1;
                                for(int i=0; i<agVars.enemyUnitsCount; i++) {
                                    if(agVars.enemyUnits[i].aiState != LEAFAIACTIONENDMOVE) {
                                        currmodelnum = i;
                                        break;
                                    }
                                }
                    #ifdef DEBUGACTIONGAME
                                leafLog(LEAF_NO, "AG: AI: ******\nprocessing enemy %i\n*******\n", currmodelnum);
                    #endif
                                if(currmodelnum < 0) {// if there are no models to work on, set turn completion flag and return
                    #ifdef DEBUGACTIONGAME
                                    leafLog(LEAF_NO, "AG: AI: Done all, finishing\n");
                    #endif
                                    agVars.aiState = LEAFAIGLOBALCOMPLETED;
                                    return;
                                }
                                // check if we are alive
                                if((agVars.enemyUnits[currmodelnum].health > 0) &&
                                   (agVars.enemyUnits[currmodelnum].TUs > 0) &&
                                   (agVars.enemyUnits[currmodelnum].aiState != LEAFAIACTIONENDMOVE))  // alive, has TUs and needs processing
                                {
                                    // GET some location info...
                    #ifdef DEBUGACTIONGAME
                                    leafLog(LEAF_NO, "AG: AI: desciding what to do for enemy %i\n", currmodelnum);
                    #endif
                                    // first we findout how many friends and foes do we see
                                    friendsnear = 0;
                                    //foesnear = 0;
                                    agVars.enemyUnits[currmodelnum].visibleEnemiesCount = 0;
                                    closestfriend = -1;
                                    closestfoe = -1;
                                    closestvisiblefoe = -1;
                                    closestfoedistance = GAMEVIEWDISTSQARED*GAMEVIEWDISTSQARED;
                                    closestfrienddistance = GAMEVIEWDISTSQARED*GAMEVIEWDISTSQARED;
                                    closestvisiblefoedistance = GAMEVIEWDISTSQARED*GAMEVIEWDISTSQARED;
                                    for(int ii=0; ii<agVars.enemyUnitsCount; ii++) {
                                        if((agVars.enemyUnits[ii].health > 0) && (ii != currmodelnum)) {  // if alive and not self
                                            int distance = (int)agGetDistanceSQ(&agVars.enemyUnits[currmodelnum], &agVars.enemyUnits[ii]);
                                            if(distance < closestfrienddistance) {
                                                // this is closest friend so far
                                                closestfrienddistance = distance;
                                                closestfriend = ii;
                                            }
                                            if(distance < GAMEVIEWDISTSQARED) {
                                                friendsnear++;
                                            }
                                        }// if alive
                                    }// for enemycount
                                    for(int ii=0; ii<agVars.playerUnitsCount; ii++) {
                                        if(agVars.playerUnits[ii].health > 0) {
                                            int distance = (int)agGetDistanceSQ(&agVars.enemyUnits[currmodelnum], &agVars.playerUnits[ii]);
                                            if(distance < closestfoedistance) {
                                                // this is closest foe so far
                                                closestfoedistance = distance;
                                                closestfoe = ii;
                                                // can we see this foe?
                                                if(agIsVisibleFor(&agVars.enemyUnits[currmodelnum], &agVars.playerUnits[ii])) {
                                                    agVars.enemyUnits[currmodelnum].visibleEnemiesCount++;
                                                    if(distance < closestvisiblefoedistance) {
                                                        closestvisiblefoedistance = distance;
                                                        closestvisiblefoe = ii;
                                                    }
                                                }
                                            }
                                            /*
                                            if(distance < GAMEVIEWDISTSQARED) {
                                                agVars.enemyUnits[currmodelnum].visibleEnemiesCount++;
                                            }
                                             */
                                        }// if alive
                                    }// for modelcount
                    #ifdef DEBUGACTIONGAME
                                    leafLog(LEAF_NO, "AG: AI: see %i friends and %i foes\n", friendsnear, agVars.enemyUnits[currmodelnum].visibleEnemiesCount);
                    #endif
                                    // what state are we in?
                                    switch (agVars.enemyUnits[currmodelnum].aiState) {
                                            
                                        case LEAFAIACTIONINIT:
                                            // what to do?
                    //************************************************************************************
                    						// check messages
                                            if(agVars.enemyUnits[currmodelnum].aiInbox != AIMESSAGENOMESSAGES) {
                                                agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONCHECKMESSAGES;
                                                return;
                                            }
                                            // call to arms?
                                            if( (agVars.enemyUnits[currmodelnum].visibleEnemiesCount) && (enemy[agVars.enemyUnits[currmodelnum].type].callToArmsDistanceSQ > SMALLNUMBER) ) {
                                                for(int i=0; i<agVars.enemyUnitsCount; i++) {
                                                    if(i == currmodelnum) {
                                                        continue;
                                                    }
                                                    // check if a callee is close enough
                                                    if(agGetDistanceSQ(&agVars.enemyUnits[currmodelnum], &agVars.enemyUnits[i]) < enemy[agVars.enemyUnits[currmodelnum].type].callToArmsDistanceSQ) {
                                                        agVars.enemyUnits[i].aiInbox = AIMESSAGECALLTOARMS;
                                                        agVars.enemyUnits[i].aiSender = currmodelnum;
                    #ifdef DEBUGACTIONGAME
                                                        agPrintToInfoboard(getLocalizedString("DE7"));
                    #endif
                                                    }
                                                }
                                            }
                    //************************************************************************************
                                            // am I the last man standing?
                                            if(unitsAlive < 2) {
                    #ifdef DEBUGACTIONGAME
                                                leafLog(LEAF_NO, "AG: AI: I am the last man standing.... BERSERK\n");
                                                agPrintToInfoboard(getLocalizedString("DE8"));
                    #endif
                                                agVars.enemyUnits[currmodelnum].aiState = LEAFAIBERSERK;
                                                return;
                    
                                            }
                                            // if there are no foes near...
                                            if(agVars.enemyUnits[currmodelnum].visibleEnemiesCount == 0) {
                                                // if the closest enemy is very far, do nothing
                                                if(closestfoedistance > (int)((float)GAMEVIEWDISTSQARED*GAMEPERCEPTIONDISTANCE)) {
                    #ifdef DEBUGACTIONGAME
                                                    leafLog(LEAF_NO, "AG: AI: enemies far, do nothing\n");
                    #endif
                                                    agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                                    return;
                                                } else {   // closest enemy is close enough to think...
                                                    // if there is more then 1 friend arround, and we have TUs to FIRE, we try to come closer to the enemy
                                                    if( (friendsnear > 1) && (agVars.enemyUnits[currmodelnum].TUs > TUCOSTFIRE) ) {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: enemies far, but friends arround. I'll try to come closer to my closest enemy\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONCOMECLOSER;
                                                        return;
                                                    } else {// if there are no friends arround, or we do not have enough TUs to fire
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: enemies far, no friends arround, i'll stay here\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                                        return;
                                                    }
                                                }// enemies close enough but unseen
                                            }// no foes near
                                            else {   // there are foes near
                                                // and there are friends arround
                                                if(friendsnear > 0) {
                                                    // if we are OK on health, come closer and attack
                                                    if(agVars.enemyUnits[currmodelnum].health > AIHEALTHLOW) {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: i have a lot of health, i'll come closer and attack\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONCOMEANDFIRE;
                                                        return;
                                                    } else {    // health low, run away
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: health low, running away\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONRUNAWAY;
                                                        return;
                                                    }
                                                }// foes near, friends near
                                                else {   //no friends near
                                                    // if there are more then 2 foes and no friends, run away
                                                    if(/*foesnear*/agVars.enemyUnits[currmodelnum].visibleEnemiesCount > 2) {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: no friends arround, too many foes, running away\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONRUNAWAY;
                                                        return;
                                                    } else {   // less then 2 foes and no friends
                                                        // if there is enough health, attack
                                                        if(agVars.enemyUnits[currmodelnum].health > AIHEALTHLOW) {
                    #ifdef DEBUGACTIONGAME
                                                            leafLog(LEAF_NO, "AG: AI: no friends, less then 2 foes, but health ok, attacking\n");
                    #endif
                                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONFIRENOW;
                                                            return;
                                                        } else {   // health problems, run away
                    #ifdef DEBUGACTIONGAME
                                                            leafLog(LEAF_NO, "AG: AI: no friends, less then 2 foes, low health -> running away\n");
                    #endif
                                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONRUNAWAY;
                                                            return;
                                                        }
                                                    }// no friends and less then 2 foes
                                                }// foes near, no friends near
                                            }// foes near
                                            break;  // LEAFAIACTIONINIT
                                            
                                        case LEAFAIACTIONFIRENOW:
                                            // is enemy visible?
                                            if(!agIsGloballyVisible((int)agVars.enemyUnits[currmodelnum].x, (int)agVars.enemyUnits[currmodelnum].z)) {
                    #ifdef DEBUGACTIONGAME
                                                leafLog(LEAF_NO, "AG: AI: i see enemies, but i am invisible, i will do nothing\n");
                    #endif
                                                agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                                return;
                                            }
                                            // if we can, fastsit!
                                            if((agVars.enemyUnits[currmodelnum].skills & skills[SKILLFASTSIT]) && (!agVars.enemyUnits[currmodelnum].isSitting))
                                            {
                    #ifdef DEBUGACTIONGAME
                                                leafLog(LEAF_NO, "AG: AI: Can sit!\n");
                    #endif
                                                agVars.enemyUnits[currmodelnum].isSitting = LEAF_YES;
                                                md2ChangeAnimation(&agVars.enemyUnits[currmodelnum].model, ANIMSITDN, agVars.currTime);
                                                return;
                                            }
                                            // before firing, we need to make sure there are visible foes, and shoot them only!
                                            if(closestvisiblefoe >= 0) {
                                                agAIFire(&agVars.enemyUnits[currmodelnum], &agVars.playerUnits[closestvisiblefoe]);
                                            } else {
                                                // there are no visible foes, what do we do????
                                                // we're either already came closer, or we cannot...
                                                // so we pass
                                                agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                            }
                                            break;// LEAFAIACTIONEFIRENOW
                                            
                                        case LEAFAIBERSERK:
                                            // this happens when there's only one enemyunit left on the map.
                                            // right now it will act as if it recieved LEAFAIACTIONCOMEANDFIRE
                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONCOMEANDFIRE;
                                            break; // LEAFAIBERSERK
                                            
                                        case LEAFAIACTIONCOMEANDFIRE:
                                            agAIComeCloser(&agVars.enemyUnits[currmodelnum], (int)agVars.playerUnits[closestfoe].x, (int)agVars.playerUnits[closestfoe].z, TUCOSTFIRE, LEAFAIACTIONINIT);
                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONFIRENOW;
                                            break;// LEAFAIACTIONCOMEANDFIRE
                                            
                                        case LEAFAIACTIONCOMECLOSER:
                                            agVars.enemyUnits[currmodelnum].aiState = agAIComeCloser(&agVars.enemyUnits[currmodelnum], (int)agVars.playerUnits[closestfoe].x, (int)agVars.playerUnits[closestfoe].z, TUCOSTFIRE, LEAFAIACTIONINIT);
                                            break;// LEAFAIACTIONCOMECLOSER
                                            
                                        case LEAFAIACTIONRUNAWAY:
                                            // same as walk closer, but walking closer to nearest friend
                                            // if there are any TUs, we run!
                                            if(agVars.enemyUnits[currmodelnum].TUs > 0) {
                                                // will run away if there is a friend to run to. If left alone, fight!
                                                if(closestfriend < 0) {
                    #ifdef DEBUGACTIONGAME
                                                    leafLog(LEAF_NO, "AG: AI: Want to run away, but no friends near.\n");
                    #endif
                                                    if(agVars.enemyUnits[currmodelnum].TUs >= TUCOSTFIRE) {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: No friends to run to but have TUs to fire!\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONFIRENOW;
                                                        return;
                                                    }// have TUs to fire
                                                    else {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: No friends to run to and no TUs to fire.. Staying here.\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                                        return;
                                                    }// no TUs to fire
                                                }// there are no friends close
                                                agVars.enemyUnits[currmodelnum].aiState = agAIComeCloser(&agVars.enemyUnits[currmodelnum], (int)agVars.enemyUnits[closestfriend].x, (int)agVars.enemyUnits[closestfriend].z, TUCOSTAUTOFIRE, LEAFAIACTIONFIRENOW);
                                            }
                                            break;// LEAFAIACTIONRUNAWAY
                                            
                                        case LEAFAIACTIONDONOTHING:
                                            // if unit has FASTSIT skill, we sit it down
                                            if(agVars.enemyUnits[currmodelnum].health > 0) {
                                                if((agVars.enemyUnits[currmodelnum].skills & skills[SKILLFASTSIT]) && (!agVars.enemyUnits[currmodelnum].isSitting) )
                                                {
                                                    agVars.enemyUnits[currmodelnum].isSitting = LEAF_YES;
                                                    md2ChangeAnimation(&agVars.enemyUnits[currmodelnum].model, ANIMSITDN, agVars.currTime);
                                                    break;
                                                } else {
                                                    // now it has nothing to do and sits, if it has fastsit
                                                    // we check for any RELEASED hostages we can kill and shoot them, if we can.
                                                    if(agVars.enemyUnits[currmodelnum].TUs >= TUCOSTFIRE) {
                                                        for(int k=0; k<agVars.hostageUnitsCount; k++) {
                                                            if( (agVars.hostageUnits[k].health > 0) && (agVars.hostageUnits[k].aiState == LEAFAIHOSTAGERELEASED) )
                                                            {
                                                                if(agIsVisibleFor(&agVars.enemyUnits[currmodelnum], &agVars.hostageUnits[k])) {
                    #ifdef DEBUGACTIONGAME
                                                                    leafLog(LEAF_NO, "AG: AI: Nothing to do, but i see a released hostage to kill.\n");
                    #endif
                                                                    agAIFire(&agVars.enemyUnits[currmodelnum], &agVars.hostageUnits[k]);
                                                                    agVars.enemyUnits[currmodelnum].TUs -= TUCOSTFIRE;
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                            // Then we are done
                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONENDMOVE;
                                            break;
                                            
                    //************************************************************************************
                                            
                                        case LEAFAIACTIONCHECKMESSAGES: {
                    #ifdef DEBUGACTIONGAME
                                            leafLog(LEAF_NO, "AG: AI: in LEAFAIACTIONCHECKMESSAGES\n");
                    #endif
                                            switch(agVars.enemyUnits[currmodelnum].aiInbox) {
                                                case AIMESSAGECALLTOARMS:
                                                    // FIRST OF ALL!!! clear message
                                                    agVars.enemyUnits[currmodelnum].aiInbox = AIMESSAGENOMESSAGES;
                                                    // go to sender
                                                    if(agVars.enemyUnits[currmodelnum].TUs > 0) {
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: recieved CALLTOARMS from %i, going to: %i:%i to %i:%i\n", agVars.enemyUnits[currmodelnum].aiSender, (int)agVars.enemyUnits[currmodelnum].x, (int)agVars.enemyUnits[currmodelnum].z, (int)agVars.enemyUnits[agVars.enemyUnits[currmodelnum].aiSender].x, (int)agVars.enemyUnits[agVars.enemyUnits[currmodelnum].aiSender].z);
                    #endif
                                                        // if sender is close enough, ignore
                                                        if(agGetDistanceSQ(&agVars.enemyUnits[agVars.enemyUnits[currmodelnum].aiSender], &agVars.enemyUnits[currmodelnum]) <= 9)
                                                        {
                    #ifdef DEBUGACTIONGAME
                                                            leafLog(LEAF_NO, "AG: AI: I am too close to come closer to sender. On my own now.\n");
                    #endif
                                                            // reinit AI.
                                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONINIT;
                                                            return;
                                                        }
                                                        agVars.enemyUnits[currmodelnum].aiState = agAIComeCloser(&agVars.enemyUnits[currmodelnum], (int)agVars.enemyUnits[agVars.enemyUnits[currmodelnum].aiSender].x, (int)agVars.enemyUnits[agVars.enemyUnits[currmodelnum].aiSender].z, TUCOSTFIRE, LEAFAIACTIONINIT);
                                                    }// enough TUs to come closer
                                                    else {   // no enough TUs to come closer
                    #ifdef DEBUGACTIONGAME
                                                        leafLog(LEAF_NO, "AG: AI: No TUs to come closer. I'll do nothing\n");
                    #endif
                                                        agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONDONOTHING;
                                                        return;
                                                    }
                                                    break;// AIMESSAGECALLTOARMS
                                                    
                                                default:
                    #ifdef DEBUGACTIONGAME
                                                    leafLog(LEAF_NO, "AG: AI: Unknown message!!!\n");
                    #endif
                                                    agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONINIT;
                                                    break;
                                            }
                                        } break;// LEAFAIACTIONCHECKMESSAGES
                                            
                    //************************************************************************************
                                            
                                        default:
                    #ifdef DEBUGACTIONGAME
                                            leafLog(LEAF_NO, "AG: AI: Unknow AI state in EnemyLogic for enemy %i\n", currmodelnum);
                    #endif
                                            agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONENDMOVE;
                                            return;
                                            break;
                                    }// switch currmodel->AIstate
                                }// alive and has TUs
                                else {// dead and/or has no TUs
                                    agVars.enemyUnits[currmodelnum].aiState = LEAFAIACTIONENDMOVE;
                                    return;
                                }// dead and/or has no TUs
                                break;  // GLOBAL AI ACTION
                                
                            case LEAFAIGLOBALCOMPLETED: {// finish turn
                                // but first...
                                // we will check if there are units who have a lot of unused TUs
                                // if there are other units not far who see enemies, we move those units to support them
                                int noFurtherMoves = LEAF_YES;
                                for(int i=0; i<agVars.enemyUnitsCount; i++) {
                                    if(agVars.enemyUnits[i].health < AGAIHEALTHTOHELP) {
                                        continue;
                                    }
                                    if(enemy[agVars.enemyUnits[i].type].helpDistanceSQ < SMALLNUMBER) { // 0.0f for units who don't run to help
                                        continue;
                                    }
                                    if(agVars.enemyUnits[i].TUs > AGAITUSTOMOVETOHELP) {
                                        // enough TUs. Let's see if there are units around who see eneies
                                        float closestUnitSQ = GAMEVIEWDISTSQARED*2;// too far to start with
                                        int helpeeIndex = -1;
                                        for(int k=0; k<agVars.enemyUnitsCount; k++) {
                                            // if self, skip
                                            if(i==k) {
                                                continue;
                                            }
                                            // is the new unit seeing enemy
                                            if(agVars.enemyUnits[k].visibleEnemiesCount > 0) {
                                                // is unit close enough? is unit far enough? (Maybe we're already there)
                                                float newDistance = agGetDistanceSQ(&agVars.enemyUnits[i], &agVars.enemyUnits[k]);
                                                if((newDistance < enemy[agVars.enemyUnits[i].type].helpDistanceSQ) && (newDistance > 1.0f-SMALLNUMBER)) {
                                                    // do we already know a unit who needs help and is closer
                                                    if( (helpeeIndex == -1) || (closestUnitSQ > newDistance) ) {
                                                        helpeeIndex = k;
                                                        closestUnitSQ = newDistance;
                                                    }// found new helpee
                                                }// found close helpee
                                            }// found helpee
                                        } // for all helpees
                                        if(helpeeIndex > -1) {
                    #ifdef DEBUGACTIONGAME
                                            leafLog(LEAF_NO, "AG: AI: enemy %i is rushing to help enemy %i\n", i, helpeeIndex);
                                            agPrintToInfoboard(getLocalizedString("DE6"));
                    #endif
                                            agVars.enemyUnits[i].aiState = agAIComeCloser(&agVars.enemyUnits[i], (int)agVars.enemyUnits[helpeeIndex].x, (int)agVars.enemyUnits[helpeeIndex].z, TUCOSTAUTOFIRE, LEAFAIACTIONINIT);
                                            // is there a path at all?
                                            if(agVars.enemyUnits[i].aiState != LEAFAIACTIONDONOTHING) {
                                                noFurtherMoves = LEAF_NO;
                                                agVars.aiState = LEAFAIGLOBALACTION;
                                                break;
                                            }
                                        }
                                    }// if helper has enough TUs
                                }// for all helpers
                                if(noFurtherMoves) {
                                    agChangeTurn(TURNHOSTAGES);
                                }
                            } break;  // GLOBAL AI COMPLETED
                                
                            default:
                                break;
                        }// switch global AIstate
                    }// agProcessAI
                    

                    • +1
                      Код просто поэма! =D
                      Ну или как минимум дневник с терзаниями и сомнениями одного кровожадного террориста
                  • +1
                    Прекрасный захватывающий рассказ.
                    • +1
                      Очень крутая трилогия вышла. Даже жаль что нет смарта от эпл, с радостью бы поиграл. Спасибо за рассказ.)
                      • 0
                        На андроид, к сожалению, пороха не хватило. Хотя порт должен быть довольно простым, по идее, если делать через NDK
                      • +1
                        Спасибо за текст. Замечательно написано :-)

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

                        Но зато получили интересный опыт, а это классно. И респект за терпение — делать проект 3 года сможет не каждый.
                        • +1
                          Мне было интересно писать именно движок. Я по натуре не игродел, наверное )
                          Но, думаю, вы правы.
                          • +1
                            Перед тем как начать, стоило хотя бы перечитать статьи на gamedev.ru.

                            Я тоже делал раз простую игру для iOS, в итоге понял что бесполезно — сейчас gamedev это миллионный рынок где рулит бабло, а бюджеты современных игр уже, бывает, превосходят бюджеты художественных фильмов. Эпоха когда можно было написать «Тетрис» или «Поле чудес» и стать знаменитым, увы прошла.

                            Про тот же XCom, в плане понимания масштаба и человеко-часов:

                            www.eurogamer.net/articles/2012-03-06-firaxis-xcom-is-a-very-very-big-budget-game
                            «We're 50, 60 guys, I don't know exactly,» he said. «We've been working on it for three-and-a-half, four years. It's a big, big game. It's definitely as big as any game we've ever made at Firaxis. It's huge. It's a bit like piloting a big old boat.»

                            Вообще, gamedev это своя специфика разработки — сжатые сроки и частые переработки, меняющиеся требования, зажранные юзеры, невысокие зарплаты, и эта ниша для фанатов в общем-то. Я год проработал, в итоге понял что это не мое от слова «совсем».

                            Удачи в Париже, и в изучении французского. Был там этим летом :)
                        • 0
                          А почему вы решили делать свой движок, а не использовать тот же Unity, например?
                          • +1
                            Юнити в то время был ещё не торт.
                            Ну и, как я написал выше, мне было просто интересно писать движок.
                          • +1
                            Как-то грустно, похоронили со всеми почестями?
                            • 0
                              само собой. 2 недели гиктаймс статьями мучал.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое