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

Вступление

Для начала — ребрендинг! Теперь это не «Первая игра ______», а целое солидное название!

И божечкимой, что за мастодонт получился на этот раз. А все из-за того, что я чутка ошибся в планах. Ну что ж, поехали!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Объекты или 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 отключается, пропадают все герои и панель с их скиллом. И появляются при его включении.

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

А дальше что?

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

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

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

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

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

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

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

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

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

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

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; }
Опыт разработки первой игры на Unity, часть 3

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

2525
12 комментариев

Вопрос для всех, кто публикуется в инди. Автор, для чего ты это делаешь, когда тут дают 5 плюсиков и может быть 2 с полоивиной реальных прочтений? Абсолютно мертвый раздел, хоть на него и подписано формально якобы пол миллиона. (что просто смешно, столько реальных пользователей дтф даже близко нет — здесь максимум 2-3 тысячи уников в день, которые что-то читают, а не смотрят картинки)

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

1
Ответить

Первоначальный мотив - просто расслабиться, отдохнуть и подурачиться) собственно, как и разработка

Сейчас много всякого, а так я отдыхаю)

Когда увидел 1300+ просмотров первой части, добавился мотив "это мои потенциальные игроки". Посмотрим, получится что-нибудь или нет)

5
Ответить

А какие есть альтернативы?

1
Ответить

Бессмысленно каждые три дня описывать каждый свой шаг. Это никому не интересно, тем более от начинающего разработчика. 99% шанс, что все это ты или перепишешь с нуля или выбросишь на помойку.

Ответить

Это интересно мне))
Зато представь, какой классный цикл статей может получится)

Ответить