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

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

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

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

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

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

Подготовка

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

Опыт разработки первой игры на Unity, часть 4
  • Снизу экрана должны отображаться все имеющиеся у игрока герои
  • Сверху — просто расположение выбранных героев на поле битвы
  • Хочу кликнуть по герою — и чтоб он появился сверху
  • Еще хочу уметь перетаскивать героев сверху на разные позиции

Реализация

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

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

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

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

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

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

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

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

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

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

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

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

Как итог — я так и не придумал, как сделать так, чтобы герои менялись местами при перетаскивании одного на другого. Подозреваю, это не просто. Скорее всего, это очень просто. Беда в том, что это важный 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().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% уверен, что можно).

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

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

Заключение

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

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

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

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

Ты молодец! Главное не отступай перед трудностями и не бросай разработку.

2
Ответить

Спасибо!)

Ответить

Старайся, тут был чел который I am king создал, тоже шаги выкладывал и смог игру выложить в стим

1
Ответить

Это здорово))

Ответить

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

1
Ответить

К счастью, есть конечный список задач))

Если бы все шло по плану, то к февралю все было бы готово, но с этой частью я задержался на 10 дней. Думаю, такими темпами будет готово к апрелю

Ответить

Декомпозировать задачи это как? Можно по подробнее?

Ответить