Опыт разработки первой игры на Unity, часть 4

Или о том, как я обманываю читателей

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

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

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

Подготовка

Для начала нужно добавить интерфейс, в котором и будет происходить выбор героев для битвы.

  • Снизу экрана должны отображаться все имеющиеся у игрока герои

  • Сверху — просто расположение выбранных героев на поле битвы 

  • Хочу кликнуть по герою — и чтоб он появился сверху

  • Еще хочу уметь перетаскивать героев сверху на разные позиции

Реализация

Пыщь-пыщь — немного магии — и готово.

Упс — что-то пошло не так. Лезем обратно в код — и получаем вот такое чудо:

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

Герои снизу — просто кнопки. Нажимаю, и он появляется в первой доступной ячейке. Нажимаю вновь — пропадает. Запускаю бой — в битве участвуют только выбранные герои — причем на нужных позициях! Не верите? А вот:

Трудности — куча их!

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

В Unity можно сделать «выключенные» объекты — они как бы есть, но движок их не обрабатывает (соответственно, игрок их не увидит). Был вариант “сделать сюда кучу героев и деактивировать их. А потом в нужный момент просто активировать нужного». Спасибо за генерацию идей — сказал я себе, — и принялся думать дальше. В итоге сейчас просто нужному объекту присваивается спрайт нужного мне героя. Уии, магия!

Осталось немного — нужно уметь менять героев местами перетаскиванием в разные точки. Сделал 1 в 1 как в этом видео:

https://youtu.be/BGr-7GZJNXg

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

Выгорание, ты ли это?

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

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

Как итог — я так и не придумал, как сделать так, чтобы герои менялись местами при перетаскивании одного на другого. Подозреваю, это не просто. Скорее всего, это очень просто. Беда в том, что это важный UX — элемент, без которого игрок будет чувствовать много боли. И оставлять это недоделанным — такое себе…

С другой стороны, я очень долго с этим вожусь, мне нужно передохнуть. К тому же, я сделал так, что работу над этой фичей можно продолжить в любой момент — ее отсутствие / реализация не потребует изменений уже сделанного.

Ох и нагнал я негатива. Да, было не очень комфортно — но гляньте на результат! Настоящая магия!

А как работает, семпай?

А теперь ваша любимая часть! Сердце сего шедевра, его мозг. Путь, по которому движется сей самурай. Движок, бьющийся… Ладно, ладно, прекращаю. Встречайте: то, от чего у программистов появляется непреодолимое желание взять учебник по языку и дать его почитать — код!

Правда, никаких неординарных задач тут нет

При нажатии по герою снизу он заполняет первую свободную ячейку сверху. При этом отправляет выбранных героев в архив «активных героев» — именно они будут участвовать в битве:

public void SetPlace()
    {
        for (int i = 0; i < _changeHeroesOnBattle.HeroesPlaceholders.Count; i++)
        {
                if (_changeHeroesOnBattle.IsEmpty[i] && !_isPressed)
                {
                    _changeHeroesOnBattle.EmptyHeroPosition.GetChild(i).gameObject.SetActive(true);
                    _changeHeroesOnBattle.EmptyHeroPosition.GetChild(i).GetChild(0).GetComponent().sprite = GetComponent().sprite;
                    _changeHeroesOnBattle.EmptyHeroPosition.GetChild(i).GetChild(1).GetComponent().text = _heroID.text;
                    _changeHeroesOnBattle.IsEmpty[i] = false;

                    _changeHeroesOnBattle.ActiveHeroes.Add(_hero);

                    _changeHeroesOnBattle.ActiveHeroes[i].GetComponent().StartPosition = _spawner.transform.GetChild(0).GetChild(i).position;

                    _changeHeroesOnBattle.ActiveBtnsSkills.Add(_hero.GetComponent().SkillUI);

                    _isPressed = true;

                    break;
                }

                else if (!_changeHeroesOnBattle.IsEmpty[i]
                    && _changeHeroesOnBattle.EmptyHeroPosition.GetChild(i).GetChild(1).GetComponent().text == _heroID.text
                     && _isPressed)
                {
                    _changeHeroesOnBattle.ResetPlaceholder(i);
                    _changeHeroesOnBattle.ActiveHeroes.Remove(_hero);
                    _changeHeroesOnBattle.ActiveBtnsSkills.Remove(_hero.GetComponent().SkillUI);
                    _isPressed = false;

                    break;
                }
            }
    }

И… Это все xD

Заключе… Ох, стоп. Это что, продолжение?

Воу, статья еще не кончилась?

Да-да, в этом выпуске будет больше одной фичи! Помните повышение уровня? Теперь сделаю… Нет, еще не его.

Для повышения уровня рассматривал несколько вариантов:

  • Герой получает опыт при каждом убийстве противника. Максимально приближенный к “большим” РПГ игровой опыт

  • Герои получают опыт только после победы над каждой волной противников

  • Герои получают опыт только после победы над всеми противниками

Изначально хотел сделать первый вариант, но остановило то, что герои будет увеличивать уровень чуть ли не после каждого убийства. А при повышении восстанавливается здоровье. Они же не убиваемыми получатся! Это можно решить, назначив требованием к level up «получить 9000 опыта», но я хочу игрока награждать почаще. Остальные варианты в своей сути одинаковы.

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

Решил не делать красивую анимацию “перетекания” полученного опыта в героя (чтоб красиво так повышался уровень). Пока просто отображает, сколько опыта герой получил за битву. Чуть не забыл! Выбранные в битву герои сохраняют свои позиции даже после битвы — красота.

Добро пожаловать в школу программирования

И вновь — попытка поехать на велосипеде с помощью костылей и какого-то чуда

Вот. Вот оно — то, с чем я возился больше 10 часов. И я не шучу. В поисках этого решения я перерыл весь интернет. Вы готовы?

if (_battleStarterScript.ActiveEnemies.All(ActiveEnemies =>
                                      ActiveEnemies.GetComponent().IsDead))

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

А больше ничего интересного и не было. Хотя нет — я понял, что с моим «переключателем сцен» (который пока что просто включает/выключает объекты) нужно что-то делать. Сейчас это что-то жуткое, в котором наделать баги проще простого. Ну вы видели в предыдущей части, что у меня там. А сейчас туда добавился экран выбора героев и экраны победы с поражением.

Штош, на этом все. Хах, ладно. Обещал сделать прокачку герев — будет прокачка героев.

Это же значит, что теперь я не обманываю читателей!

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

И я решил не париться от слова “совсем”. Это решение наверняка плохое, но в дальнейшем я, скорее всего, от него избавлюсь. А пока… Решил использовать struct. Первый struct хранит характеристики на первом уровне. Второй — на сотом. Все значения между ними по задумке будут высчитываться интерполяцией.

Почему не сделать грамотно (например, сделав на устройстве файлик с этими значениями) и подтягивать из него? А все просто — еще не время разбираться в этом (ну и мне лень, чего уж там). Опять же — в дальнейшем struct наверное пропадет, а эти же данные будут подтягиваться из таблиц.

План определен — поехали

И тут же останавливаемся. Оказалось, что текущая система данных в таблице неудачная — часть показывает характеристики на первом уровне, а часть — какими должны быть характеристики для достижения последнего уровня. Если проще — показывают характеристики на предпоследнем уровне.

Делаю колдунство с таблицей — и все вроде как нормально.

Нужно разобраться, что мне вообще делать:

  • Получить список характеристик (как раз struct подготовил)

  • Сделать так, чтобы за битву давали опыт в зависимости от противников

  • Повышать в зависимости от полученного опыта уровень героя

  • И находить соответствующие уровню характеристики

Для первого пункта делаю вот так

    private void AddHeroLvlStats()
    {
        Level level1 = new Level(1, 1, 150, 100, 999, 100, 20, 10);
        _heroLevelStats.Add(level1);

        Level level150 = new Level(2, 150, 200, 100, 100, 10000, 40, 30);
        _heroLevelStats.Add(level150);

        Level level200 = new Level(3, 200, 200, 100, 100, 15000, 80, 90);
        _heroLevelStats.Add(level200);
    }

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

Пункт 2

Внезапно стало легко определить, сколько опыта выдавать героям игрока — просто перемножаем количество опыта за противника на число противников:

     private void IncTotalExp()
    {
        for (int i = _battleStarterScript.ActiveEnemies.Count - 1; i >= 0; i--)
        {
            _totalExpGained += _battleStarterScript.ActiveEnemies[i].GetComponent().Exp_gain;
        }
        int howManyLvls = _totalExpGained / _battleStarterScript.ActiveHeroes[0].GetComponent().Exp_Max;
        float divi = _totalExpGained % _battleStarterScript.ActiveHeroes[0].GetComponent().Exp_Max;

        int newLevel = _battleStarterScript.ActiveHeroes[0].GetComponent().Lvl_Cur + howManyLvls;
        int newCurExp = _battleStarterScript.ActiveHeroes[0].GetComponent().Exp_Cur + (int)divi;

        foreach (GameObject hero in _battleStarterScript.ActiveHeroes)
        {
            hero.GetComponent().SetNewLvl(newLevel);
            hero.GetComponent().Exp_Cur = newCurExp;
        }
    }

По поводу for… Честно — понятия не имею, почему при стандартном i++ у меня остается один активный объект.

Операция «Повышение»

Ой, а я же уже показал. Вычисляется, сколько уровней герой может получить в зависимости от полученного опыта. Затем уровень присваивается, а оставшийся остаток от деления становится «текущим опытом». Интересна тут функция hero.GetComponent().SetNewLvl(newLevel);, которая приводит нас к…

Свободная касса!

Итак — проблема. Мне известен набор характеристик на первом уровне героя. Известен набор характеристик на 150 уровне героя. А тут, внезапно, понадобилось узнать параметры героя на условном 38 уровне. Как это сделать?

Можно попробовать через for. Это будет чуть проще, чем через if или switch. Но, хоть я тот еще извращенец, к таким подвигам не готов. Зная пограничные значения, можно высчитать то, какие значения будут в любом месте между границами. Не буду томить — мне подсказали вот такую замечательную формулу:

public int GetStatsFromLvl(int lvl, List listLevels, int first, int last, Stats stat)
    {
        float a = listLevels[first].Lvl_Max + ((float)listLevels[last].Lvl_Max - (float)listLevels[first].Lvl_Max) /
                   ((float)listLevels[last].Lvl - 1) * (lvl - 1);

        return (int) a;
    }

Функцию придумал уже я — и она 100% поменяется. Мне крайне не нравится, что приходится вручную указывать пограничные для значения структуры.

Ах да, думаю, вы уже успели отдохнуть от надругательства над беднягой c#. Не переживайте, подергивающийся от встреченного Stats stat вас не обманул — это именно то, о чем вы подумали:

public int GetStatsFromLvl(int lvl, List listLevels, int first, int last, Stats stat)
    {
        float a;

        switch (stat)
        {
            case Stats.Level_Max:
                a = listLevels[first].Lvl_Max + ((float)listLevels[last].Lvl_Max - (float)listLevels[first].Lvl_Max) /
                   ((float)listLevels[last].Lvl - 1) * (lvl - 1);
                break;

            case Stats.Exp_Max:
                a = listLevels[first].Exp_Max + ((float)listLevels[last].Exp_Max - (float)listLevels[first].Exp_Max) /
                   ((float)listLevels[last].Lvl - 1) * (lvl - 1);
                break;
//

И так на каждый параметр. Я так и не придумал, как избавится от сравнения (хотя на 100% уверен, что можно).

Зато гляньте, какое чудо получается!

Магия! Это все больше и больше становится похоже на игру! Ну разве не чудо?

Заключение

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

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

И я тут подумал… Вам не кажется, что битва квадратов с кружочками — это совсем не серьезно?

 

Источник

Читайте также

Меню