Опыт разработки первой игры на 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().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().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().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().Btn.onClick.AddListener(() => btnObject.GetComponent().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().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().WakeUpHero();

        _activeState = 0;
    }

    private void UpdateHeroTarget(GameObject heroArray)
    {
        heroArray.GetComponent().UpdateTarget();
    }

    private void WakeUpMap()
    {
        _uiPanelMap.SetActive(true);

        _activeState = 1;
    }

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

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

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

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

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

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

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

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

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

В бой!

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

public void SetActiveState(int activeState)
    {
        _activeState = activeState;
        ChangeActiveState(activeState);
    }

    /// 
    /// Set Active State. 0: map, 1: battle
    /// 
    /// 0: map, 1: battle
    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();

        if (_typeOfLevel == 0)
        _lvlName.text = "Secret";
        
        else if (_typeOfLevel == 1)
            _lvlName.text = "battle " + _levelNumber;
    }

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

 

Источник

Читайте также