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