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

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

  • Ссылка на часть 2

  • Ссылка на часть 3

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

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

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

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

Подготовка

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

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

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

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

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

Реализация

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

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

Дело нехитрое, но как же ужасно выглядит! Давайте договоримся: вы сделаете вид, что не замечаете интерфейс. Под 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% уверен, что можно).

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

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

Заключение

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

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

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

Источник: https://habr.com/ru/post/597819/


Интересные статьи

Интересные статьи

На iOS есть два варианта тестирования: классический, посредством Sandbox покупок, и новый способ локального тестирования покупок через Xcode (StoreKit local testing).Sandbox тестирование — процесс нес...
Как выбрать школу или преподавателя иностранного языка, чтобы не потерять время и деньги? Объясняем на пальцах. Это такая же непростая задача, как выбрать подрядчика...
Вторая часть полностью посвящена описанию программного обеспечения, используемого на роботе. Так как разработанный робот (pi-tank) по большому счету рассчитан на начинающих роботострои...
Случилось так, что по наследству мне досталась целая коробка семисегментных индикаторов с гордой надписью «Комплект часы». Давно хотелось пустить её содержимое в дело, а когда дошли руки — оказ...
Вашей игре нужен звук! Наверно, вы уже использовали OpenGL для рисования на экране. Вы разобрались с его API, и поэтому обратились к OpenAL, потому что название кажется знакомым. Что же, хорош...