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

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

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

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

Подготовка

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

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

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

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

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

Реализация

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

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

Дело нехитрое, но как же ужасно выглядит! Давайте договоримся: вы сделаете вид, что не замечаете интерфейс. Под 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<Image>().sprite = GetComponent<Image>().sprite;
                    _changeHeroesOnBattle.EmptyHeroPosition.GetChild(i).GetChild(1).GetComponent<TextMeshProUGUI>().text = _heroID.text;
                    _changeHeroesOnBattle.IsEmpty[i] = false;

                    _changeHeroesOnBattle.ActiveHeroes.Add(_hero);

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

                    _changeHeroesOnBattle.ActiveBtnsSkills.Add(_hero.GetComponent<Characteristics>().SkillUI);

                    _isPressed = true;

                    break;
                }

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

                    break;
                }
            }
    }

И... Это все xD

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

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

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

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

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

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

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

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

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

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

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

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

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

if (_battleStarterScript.ActiveEnemies.All(ActiveEnemies =>
                                      ActiveEnemies.GetComponent<Characteristics>().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<Characteristics>().Exp_gain;
        }
        int howManyLvls = _totalExpGained / _battleStarterScript.ActiveHeroes[0].GetComponent<Characteristics>().Exp_Max;
        float divi = _totalExpGained % _battleStarterScript.ActiveHeroes[0].GetComponent<Characteristics>().Exp_Max;

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

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

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

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

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

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

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

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

public int GetStatsFromLvl(int lvl, List<Level> 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<Level> 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% уверен, что можно).

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

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

Заключение

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

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

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

Комментарии (16)


  1. kesn
    25.12.2021 11:45
    +2

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

    И да, с новым годом!


    1. Vivicpony Автор
      25.12.2021 11:49

      Спасибо!) Обязательно что-нибудь такое добавлю)


  1. ri1wing
    25.12.2021 11:49
    +2

    В прошлой статье вы писали

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

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


    1. Vivicpony Автор
      25.12.2021 11:58

      В общем, верно, да) немного спойлеров: тут описаны не все детали) глобально все то же самое, но часть кода раньше времени может анонсировать мое видение проекта, которое несколько отличается от референсного лабиринта из AFK arena

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


  1. BasicWolf
    25.12.2021 12:33
    +4

    Вы молодец, что продолжаете работу, несмотря на сложности.

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

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

    Вот простой пример, вы пишите:

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


    Но будете ли вы через неделю помнить, что это обозначает? Не будет ли подспорьем переменная объясняющая всё это выражение? Например:

    boolean allEnemiesDead = _battleStarterScript.ActiveEnemies.All(ActiveEnemies =>
                                          ActiveEnemies.GetComponent<Characteristics>().IsDead))
    if (allEnemiesDead) {...}


    А это лишь песчинка на вершине айсберга вашего проекта. Перелопачивание и переписывание кода - это нормально. Однако это намного сложнее делать, если код написан "лишь бы оно работало". Работать-то может и будет, но внесение изменений в такую программу отнимает кучу времени и сил. В итоге всё это надоедает и вы проклинаете тот день, когда сели за баранку этого пылесоса.

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


    1. Vivicpony Автор
      25.12.2021 12:42
      +1

      Спасибо! Поищу ещё такие моменты)


  1. Anarchist
    26.12.2021 15:27
    +1

    Почему бы, например, вместо того, чтобы проверять по массиву, все ли убиты, не сделать счетчик и просто сравнивать с длиной массива?


    1. Vivicpony Автор
      26.12.2021 15:56

      Изначально хотел сделать, чтобы смерть противника просто увеличило какое-нибудь число в этом скрипте (что-то типа "int totalDead++"), и просто сравнивать if (totalDead >= ActiveEnemies.Count). Но чисто гипотетически возможна способность воскрешения. И, вроде бы, при этом можно делать так "totalDead--", но мне почему-то кажется, что тогда обязательно появятся баги. Но спасибо - нужно будет потестировать.

      P.S. Сейчас подумал - хотя нет, я как-то иначе хотел сделать. Написанный сейчас вариант вроде как более чем рабочий


  1. Merklar
    26.12.2021 15:58
    +2

    Присоединяюсь к комментатору выше. Автор, Вы молодец, что все же стремитесь закончить свой проект. Но обязательно прочитайте что нибудь по чистоте кода. Например, о том, что передавать больше 2-3 значений в метод крайне не рекомендуется. Очень сильно ухудшает читаемость. Тот же дядя Боб рекомендует в таких случаях, если не удается избежать их - создать структуру settings к примеру, и передавать аргументом 1 этот объект. А в сам объект можно более наглядно сеттить нужные вам аргументы. settings.StartLevel = 1;


    1. Vivicpony Автор
      26.12.2021 16:03

      Спасибо! Да, мне и самому не нравится передавать в метод больше одного (!) значения) всячески стараюсь такое избегать) Про структуру вроде понял - получится, вместо условного (int aa, int bb, ..) сделать условную (MyStruct numbers)

      Хмм, про settings.StartLevel = 1; нужно подумать - примерно понял, что мне это даст, но надо продумать детали. Спасибо!)


  1. Coding1liki
    26.12.2021 15:58
    +2

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

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

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

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

    И я вас призываю не откладывать рефакторинг на потом - именно из-за отсутствия нормальной архитектуры вы страдаете последние три статьи.


    1. Vivicpony Автор
      26.12.2021 16:10
      +1

      Ахах, спасибо xD

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

      Пожалуй, воспользуюсь советом, пока не стало сильно поздно - спасибо!


  1. Ka33yC
    26.12.2021 15:58
    +1

    сделать сюда кучу героев и деактивировать их. А потом в нужный момент просто активировать нужного

    Вообще, это называется пул и ты правильно думал, но немного не довел эту идею до конца


    1. Vivicpony Автор
      26.12.2021 16:17

      Меня остановило то, что так уже сделано в скрипе Spawner)

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

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

      Хотя очень может быть, что я ошибаюсь и вообще делаю неправильно - наверняка есть способы и проще, и производительнее


  1. kipzshady
    28.12.2021 01:03

    Может попробуете что-нибудь пока несложное. Слишком масштабный проект для новичка.


    1. Vivicpony Автор
      28.12.2021 01:06

      Зато интересно!)