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

—

от автора

Ссылка на часть 1

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

Ошибка планирования

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

Так что в этой части будет смена уровня вместо прокачки героев.

Взаимоисключающие цели

Вот какая штука. По моей задумке хочу сделать следующее:

1) Сделать выбор следующей битвы так же, как в AFK Arena сделан мистический лабиринт.

Краткое описание

Игрок может выбрать только ближайшую к себе точку. Например, он выбрал клетку с Мистиком. Тогда следующим шагом он может выбрать либо правую точку (карету), либо центральную. Крайний левый флаг будет для него недоступен

2) Сделать выбор следующей битвы максимально простым.

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

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

Грамотно озвученная проблема — часть решения

Хмм, в лабиринте игрок принимает решение… Делает выбор… Планирует маршрут… Стоило только озвучить проблему — тут же пришло в голову решение. Сочетает и простоту восприятия, и предоставляет игроку выбор, игрок ощущает, что он принимает решение. Встречайте!

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

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

Объекты или UI?

В юнити есть как игровые объекты (по сути, просто болванки, на которые можно повесить всякое), так и готовые кнопки интерфейса. И на то, и на другое можно нажать (но с объектами нужно извращаться для этого).

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

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

Завершение биты — переход на экран выбора — начало битвы

Есть два варианта

  • Битва и экран карты— две отдельные сцены.

  • Используется только одна сцена — но при «переходе» активируются / деактивируются соответствующие объекты.

Пока что выбрал второй вариант. С ним игра банально переходит между этими состояниями быстрее. Минус пока один — игра вроде как кушает больше ресурсов. Сделаю — проверю.

Переделка архитектуры объектов на сцене

Суть вот в чем. Будет примерно следующее

private void DEBUG_SWITCH()     {         if (_spawner.activeSelf)         {             _spawner.SetActive(false);             _uiPanelSkills.SetActive(false);              _uiPanelMap.SetActive(true);         }          else if (_uiObject.activeSelf)         {             _uiPanelSkills.SetActive(false);             _spawner.SetActive(true);         }     } 

При «переходе» на экран карты отключаются все объекты, относящиеся к битве, и включаются объекты, относящиеся к карте. При выборе уровня все происходит ровно наоборот.

Но сейчас у меня при старте на сцене куча объектов безо всякого порядка — в основном из-за того, как сделан спавн героев игрока и противника

private void SpawnPlayerHeroes()     {         GetComponent<GenerateSkillUI>().GetBtnPanen(); // костыль. Из Start скрипта почему-то вызывается после этой функции          for (int i = 0; i < maxHeroes; i++)         {             GameObject go = Instantiate(respawnPlayer, transform.GetChild(0).GetChild(i).position, respawnPlayer.transform.rotation);             AddHeroesArray(go, i);             go.GetComponent<Characteristics>().StatsInitiate                ("hero_00" + (i + 1).ToString(), "naming_hero_00" + (i + 1).ToString(), "stats_hero_00" + (i + 1).ToString(), "skills_hero_00" + (i + 1).ToString());              GetComponent<GenerateSkillUI>().GenerateButtons(i, go);         }     } 

Решаю просто: перед вызовом спавна героев создаю два дополнительных объекта — для героев игрока и героев противника соответственно

private void SpawnParentsForHeroes()     {         GameObject go = new GameObject();         go.name = "ParentForPlayer";         go.transform.SetParent(gameObject.transform);         go.transform.position = transform.position;         go.transform.rotation = transform.rotation;         _goParentForPlayer = go;          GameObject go2 = new GameObject();         go2.name = "ParentForEnemy";         go2.transform.SetParent(gameObject.transform);         go2.transform.position = transform.position;         go2.transform.rotation = transform.rotation;         _goParentForEnemys = go2;     } 

Какое-то адское извращение, но работает же!

После этого при спавне героев достаточно указать, что соответствующий _goParent… их родитель. Это так мило — теперь у них есть родитель.

Главное, что мне это дало — как только spawner отключается, пропадают все герои и панель с их скиллом. И появляются при его включении.

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

А дальше что?

Ах да — забыл. Теперь архитектура объектов на сцене выглядит вот так

Дальше нужно добавить саму структуру кнопок / битв. Располагаться они будут как на одной из картинок выше.

Хммм, а как это сделать кодом?

Конечно же, первым делом полез в интернет! Наверняка уже кто-то такое делал) Только ничего подобного не нашел. Хорошо, придется думать.

У меня есть повторяющийся паттерн поведения: 1 — 2 — 1 — 2 и так далее. Значит, мне нужно сделать так, чтобы на экране появлялась комбинация «1 — 2». Хотя стоп — а вдруг я захочу сделать 1 — 2 — 2 — 2 — 1 или что-то такое.

Между делом наткнулся на новую для себя в юнити штуку — Grid. Судя по всему, там как раз можно задать любой шаблон — в том числе такой, какой мне нужен. Но как-то не удалось заставить его работать.

В итоге придумал какую-то абсолютно хитровыдуманную конструкцию. Что-то мне подсказывает, что я перемудрил

Ну и что я натворил

private void SpawnMap(int indexLevelNumber, int howMany)     {         Vector3 directionY = new Vector3(0, _sizeBtnY, 0);         Vector3 directionX = new Vector3(_sizeBtnX, 0, 0);          int howManyConverter (int a) // если howMany будет = levelNumber         {             if (a % 2 == 0)                 return 2;              else return 1;         }          int IsEven(int a)         {             if (a % 2 == 0)                 return 1;             else return -1;         }          if (howManyConverter(howMany) == 1)         {             GameObject go = Instantiate(_mapBtn,                 new Vector3(_startPoint.x, _startPoint.y + directionY.y, _startPoint.z),                 _panelMapContent.transform.rotation, _panelMapContent.transform);         }          else if (howManyConverter(howMany) == 2)         {             int indexLevelNumberPlusOne = indexLevelNumber;             for (int i = 0; i < howManyConverter(howMany); i++)             {                 GameObject go = Instantiate(_mapBtn,                     new Vector3(_startPoint.x + directionX.x * IsEven(indexLevelNumberPlusOne), _startPoint.y + directionY.y, _startPoint.z),                     _panelMapContent.transform.rotation, _panelMapContent.transform);                  indexLevelNumberPlusOne++;             }         }         _startPoint += directionY;     } 

Вызывается так

for (int i = 0; i < _maxLvl; i++)         {             SpawnMap(i, i);         } 

Ура! Чем нравится это решение: вместо второй «i» могу подставить 1 или 2. Например, мне нужна последовательность 1 — 1 — 2 — 1 — 2 — 2 — 2.

Тогда я делаю что-то типа такого:

  • Вызов ее через for, но вместо _maxLvl будет 1 («SpawnMap(i, 1);»).

  • Вызов ее через for, но вместо _maxLvl будет 2 («SpawnMap(i, i);»).

  • Вызов ее через for, но вместо _maxLvl будет 2 («SpawnMap(i, 2);»).

И да, в первом случае for не нужен, но это для меня. Иначе могу запутаться.

Разделение битв и особой сущности

По задумке с одной стороны будут битвы, с другой — будет находиться эта самая сущность. Но тут вообще просто — создаю функцию SetBtnType(int type)

private void SetBtnType(GameObject btnObject, int type)     {         // type == 1. Тогда кнопка вызывает битву         // type == 2. Тогда первая кнопка вызывает битву, вторая - особую сущность             btnObject.GetComponent<ClickMapBtn>().Btn.onClick.AddListener(() => btnObject.GetComponent<ClickMapBtn>().StartleLevel(type));     } 

И добавляю ее вызов в SpawnMap() куда-нибудь в конец

SetBtnType(howManyConverter(howMany));  _startPoint += directionY;
Упс, накладка

Тут я понимаю, что забыл повесить какой-нибудь скрипт на кнопку карты. Так что делаю новый скрипт ClickMapBtn и вешаю на префаб кнопки карты. Щас будет полное извращение, но в этом скрипте у меня будет в том числе храниться индекс этой кнопки 0_о

Конечно, и в самом спавнере еще нужно добавить массив с кнопками

Перед тем, как сделать переключением «состояния» рабочим, нужно сделать еще одну вещь.

Сброс параметров всего, что отключается

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

И эта свитч вызывает нужную функцию из скрипта каждого такого. По сути, у моих объектов появился контроллер, отслеживающий их состояние. Круто же!

Какие типовые сущности у меня есть? Герои, кнопки их скиллов и кнопки карты.

Сходу наткнулся на интересное архитектурное решение. По какой-то причине контроль ползунков на кнопке использовании скила у меня находится… В скрипте Characteristics. Это гениально, не иначе. Значит, переношу всю эту систему туда, где ей самое место — в скрипт кнопки. А Characteristics будет туда только передавать актуальные данные.

Заодно исправил незамеченный баг с постоянным ускорением наполнения маны. И добавил шкалы здоровья с маной над всеми героями — ничем не отличается от аналогичных шкал на кнопках.

Вот такая штука теперь у меня переключает «активные сцены»

private void ChangeActiveState(int activeState)     {         switch (activeState)         {             case 0:                 for (int i = 0; i < _spawnerScript.playerHeroes.Length; i++)                 {                     ResetHero(_spawnerScript.playerHeroes[i]);                  }                 ResetBtnUseSkill();                  for (int i = 0; i < _spawnerScript.enemyHeroes.Length; i++)                 {                     ResetHero(_spawnerScript.enemyHeroes[i]);                 }                  WakeUpMap();                  break;              case 1:                 ResetMap();                  for (int i = 0; i < _spawnerScript.playerHeroes.Length; i++)                 {                     WakeUpHero(_spawnerScript.playerHeroes[i]);                  }                 WakeUpBtnUseSkills();                  for (int i = 0; i < _spawnerScript.enemyHeroes.Length; i++)                 {                     WakeUpHero(_spawnerScript.enemyHeroes[i]);                 }                  for (int i = 0; i < _spawnerScript.playerHeroes.Length; i++)                 {                     UpdateHeroTarget(_spawnerScript.playerHeroes[i]);                  }                 WakeUpBtnUseSkills();                  for (int i = 0; i < _spawnerScript.enemyHeroes.Length; i++)                 {                     UpdateHeroTarget(_spawnerScript.enemyHeroes[i]);                 }                 break;              default:                 break;         }     } 

Честно — понятия не имею, как ее сократить. Первый for проходится по всем героям игрока, следующий — по героям противника. Если попытаться сразу обновить цель для героя в первом цикле, то герои игрока не найдут противника, увы.

А всякие WakeUp вообще простейшие

private void ResetHero(GameObject heroArray)     {         heroArray.GetComponent<HeroReset>().ResetHero();     }      private void ResetBtnUseSkill()     {         _uiPanelSkills.SetActive(false);     }      private void WakeUpBtnUseSkills()     {         _uiPanelSkills.SetActive(true);     }      private void ResetMap()     {         _uiPanelMap.SetActive(false);     }      private void WakeUpHero(GameObject heroArray)     {         heroArray.GetComponent<HeroReset>().WakeUpHero();          _activeState = 0;     }      private void UpdateHeroTarget(GameObject heroArray)     {         heroArray.GetComponent<HeroReset>().UpdateTarget();     }      private void WakeUpMap()     {         _uiPanelMap.SetActive(true);          _activeState = 1;     } 

В итоге получилось вот так

При респавне герои начинают с начальными характеристиками.

Следующая задача — поменять сущность имеющихся спавнеров. Теперь их задачей будет что-то типа инициализации объектов: создание, выдача параметров, но не появления на экране. За появление отныне отвечает SwitchActiveState, в котором у меня и происходит переключение. Это ответственная задача, но я верю в него.

Но это еще не все!

Игра умеет менять состояние «битва / карта», теперь нужно сделать так, чтобы кнопки на карте запускали битву — причем разную в зависимости от кнопки!

Для этого вернусь в скрипт, спавнящий на карте кнопки боев и добавлю что-то типа такого

private void SetLevelNumber(GameObject go, int type)     {         if (type == 1)         {             go.GetComponent<ClickMapBtn>().LevelNumber = _lvlNumber;             _lvlNumber++;         }     } 

Гляньте, как классно все получается! Номер уровня увеличивается только у кнопок по центру и крайних левых

Подготовка завершена! Теперь, наконец, можно запускать битву, выбрав нужную кнопку на карте.

В бой!

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

public void SetActiveState(int activeState)     {         _activeState = activeState;         ChangeActiveState(activeState);     }      /// <summary>     /// Set Active State. 0: map, 1: battle     /// </summary>     /// <param name="activeState">0: map, 1: battle</param>     private void ChangeActiveState(int activeState)     {         switch (activeState)         {             case 0:                 ChooseMap();                 break;              case 1:                 ChooseBattle();                 break;              default:                 break;         }     } 

И происходит магия!

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

В дальнейшем уровни будет отличаться — противниками и сложностью. Но сделать это смогу только когда разберусь с серверной частью.

Результат

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

private void SetBtnName()     {         _lvlName = transform.GetChild(0).GetComponent<TextMeshProUGUI>();          if (_typeOfLevel == 0)         _lvlName.text = "Secret";                  else if (_typeOfLevel == 1)             _lvlName.text = "battle " + _levelNumber;     } 

Уф, теперь можно заняться прокачкой героев. Вернусь, когда с ней закончу!


ссылка на оригинал статьи https://habr.com/ru/articles/594069/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *