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

Снизу экрана должны отображаться все имеющиеся у игрока герои
Сверху - просто расположение выбранных героев на поле битвы
Хочу кликнуть по герою - и чтоб он появился сверху
Еще хочу уметь перетаскивать героев сверху на разные позиции
Реализация
Пыщь-пыщь - немного магии - и готово.
Упс - что-то пошло не так. Лезем обратно в код - и получаем вот такое чудо:
Дело нехитрое, но как же ужасно выглядит! Давайте договоримся: вы сделаете вид, что не замечаете интерфейс. Под 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<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% уверен, что можно).
Зато гляньте, какое чудо получается!
Магия! Это все больше и больше становится похоже на игру! Ну разве не чудо?
Заключение
Это часть получилась довольно тяжелой, зато сделал целых три пункта из запланированного. Возникало невероятное количество проблем - порой на ровном месте. Зато было довольно весело. Но теперь мне нужно отдохнуть от кода. Изначально минимально рабочую версию собирался сделать до февраля, но сейчас начинаю сильно сомневаться, что успею. С продолжением вернусь уже в следующем году, так что с наступающим - и не скучайте!
И напишите, как вам эта часть! В предыдущей было много кода, но из-за того, что он был разбросан повсюду, читать было сильно скучно. Тут попытался сделать иначе. Как больше нравится - когда текст пишется по ходу событий или больше по итогу всего?
И я тут подумал... Вам не кажется, что битва квадратов с кружочками - это совсем не серьезно?
