Привет Хабр 👋

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

техническое задание

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

Пример моего демо RPG проекта

Пример моего демо RPG проекта

Итак, начнем с макета экрана, затем перейдем к коду…

Компоновка интерфейса

Короче говоря, пользовательский интерфейс миссии будет состоять из двух префабов: популярный задания И графический элемент миссии.

Структура интерфейса миссий

Структура интерфейса миссий

В общем, настройка макета графического элемента достаточно проста: Выкладываем иконку, добавляем компонент Изображение, выкладываем заголовок, текст для сложности, добавляем компоненты Текст и бла-бла-бла…

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

26d62d3fa988444f6fafa2aba56c792e

Для этого нам нужно создать родительский контейнер ContainerPrice и добавить в него компонент Content Size Fitter, который расширит свою ширину на основе ширины дочерних элементов, и привязать компоненты LayoutElement к иконке и тексту с ценой:

Структура и настройки кнопки вознаграждения

Структура и настройки кнопки вознаграждения

Теперь давайте поговорим о структуре всплывающего окна миссии. В общем, ничего сложного в компоновке тоже нет, но есть несколько нюансов, на которые хотелось бы указать:

Структура всплывающего окна миссии

Структура всплывающего окна миссии

Во-первых, вам нужно убедиться, что фон экрана не кликабельный. Для этого можно создать отдельный объект и назвать его, например, Антикликер с компонентом Изображение, которое будет растянуто на весь холст. В компоненте Image поставьте галочку Raycast Target = true, и тогда кликать вне окна будет нельзя.

Во-вторых, если вы хотите, чтобы список миссий мог прокручиваться по вертикали, вам нужно добавить компонент ScrollRect, который будет перемещать объект Content в объект Viewport.

В-третьих, объект Viewport должен иметь маску (компонент Mask). Благодаря ему область содержимого миссии, выходящая за пределы окна, не будет видна.

ЧИТАТЬ   Пустых полок в магазинах в России нет, хотя некоторые страны на это надеялись – Путин

В-четвертых, вам нужно расположить предметы миссии в вертикальном порядке. За это будет отвечать компонент Vertical Layout Group. В Инспекторе компонентов можно задать интервал между элементами списка (параметр Spacing) и выравнивание элементов по определенному краю (параметр Child Alignment).

В результате должно получиться два префаба: контекстный префаб и префаб карты миссии. Надеюсь, получилось схематически объяснить, как можно настроить графический интерфейс для миссий…

Программирование интерфейса

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

  1. Отображение актуальной информации о миссии на экране

  2. Управление нажатиями пользователей на кнопки интерфейса

  3. Управляйте событиями, которые происходят в системе миссий под капотом

Для выполнения следующих задач рекомендуется использовать шаблон Model-View-Presenter. Это позволит вам распределить обязанности в коде таким образом, чтобы поддерживать и повторно использовать пользовательский интерфейс в будущем было максимально удобно.

Применение модели MVP

Применение модели MVP

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

Начнем с отображения миссии на экране:

d420ffa6a047a7c5f4380957f965e698

Графический элемент миссии будет включать в себя следующие классы:

  1. MissionView — хранит структуру миссии;

  2. MissionRewardButton — хранит структуру кнопки вознаграждения;

  3. MissionProgressBar – хранит структуру прогресса миссии

//Графический элемент одной миссии:
public sealed class MissionView : MonoBehaviour
{
    [SerializeField]
    public Image iconImage; //Иконка
    
    [SerializeField]
    public Text titleText; //Заголовок
    
    [SerializeField]
    public Text difficultyText; //Сложность миссии
    
    [SerializeField]
    public MissionProgressBar progressBar; //Прогресс миссии
    
    [SerializeField]
    public MissionRewardButton rewardButton; //Кнопка с наградой
}


//Структура кнопки с получением награды:
public sealed class MissionRewardButton : MonoBehaviour
{
    [SerializeField]
    private Button button;

    [Space]
    [SerializeField]
    private Image buttonBackground;

    [SerializeField]
    private Sprite availableBackground;

    [SerializeField]
    private Sprite unavailableBackground;

    [SerializeField]
    private GameObject processingText;

    [SerializeField]
    private GameObject getText;

    [SerializeField]
    private Text rewardText;

    public void AddListener(UnityAction action) {
        this.button.onClick.AddListener(action);
    }

    public void RemoveListener(UnityAction action) {
        this.button.onClick.RemoveListener(action);
    }

    public void SetReward(string reward) {
        this.rewardText.text = reward;
    }

    public void SetActive(bool isActive) {
        if (isActive) {
            this.button.interactable = true;
            this.buttonBackground.sprite = this.availableBackground;
            this.getText.SetActive(true);
            this.processingText.SetActive(false);
        } else {
            this.button.interactable = false;
            this.buttonBackground.sprite = this.unavailableBackground;
            this.getText.SetActive(false);
            this.processingText.SetActive(true);
        }
    }
}


//Структура прогресс бара:
public sealed class MissionProgressBar : MonoBehaviour
{
    [SerializeField]
    private Text text;

    [SerializeField]
    private Color completeTextColor;

    [SerializeField]
    private Color processingTextColor;

    [Space]
    [SerializeField]
    private Image fill;

    [SerializeField]
    private Color completeFillColor;

    [SerializeField]
    private Color progressFillColor;
    
    public void SetProgress(float progress, string text)
    {
        this.fill.fillAmount = progress;
        this.text.text = text;

        if (progress >= 1)
        {
            this.text.color = this.completeTextColor;
            this.fill.color = this.completeFillColor;
        }
        else
        {
            this.text.color = this.processingTextColor;
            this.fill.color = this.progressFillColor;
        }
    }
}

Таким образом, класс MissionView вместе с вспомогательными классами отвечает за отображение информации о миссии и обработку нажатия кнопки вознаграждения.

ЧИТАТЬ   ЦБ разрешил использовать криптовалюту во внешних платежах в виде эксперимента
Уникальная структура интерфейса миссии

Уникальная структура интерфейса миссии

Теперь напишем класс MissionPresenter, который будет взаимодействовать с графическим элементом на экране и моделью миссии под капотом:

public sealed class MissionPresenter
{
    private Mission mission; //Объект миссии под капотом
    private MissionView view; //Объект миссии на экране

    private MissionsManager missionsManager;

    public MissionPresenter(Mission mission, MissionView view) 
    {
        this.mission = mission;
        this.view = view;
        this.missionsManager = MissionsManager.Instance; //Лучше использовать DI
    } 

    //Метод активации презентера:
    public void Start()
    {
        this.view.titleText.text = this.mission.Title;
        this.view.difficultyText.text = this.mission.Difficulty.ToString();
        this.view.iconImage.icon = this.mission.Icon;
        
        this.view.rewardButton.SetReward(this.mission.MoneyReward.ToString());
        this.view.rewardButton.SetActive(this.mission.IsCompleted);  
        this.view.rewardButton.AddListener(this.OnButtonClicked);

        this.view.gameObject.SetActive(true);
        
        this.mission.OnProgressChanged += this.OnMissionProgressChanged;
        this.mission.OnCompleted += this.OnMissionCompleted;

        this.UpdateProgressbar();
    }

    //Метод деактивации презентера:
    public void Stop()
    {
        this.view.rewardButton.RemoveListener(this.OnButtonClicked);
        this.view.gameObject.SetActive(false);

        this.mission.OnProgressChanged -= this.OnMissionProgressChanged;
        this.mission.OnCompleted -= this.OnMissionCompleted;
    }

    //Обработка нажатия кнопки с наградой:
    private void OnButtonClicked()
    {
        if (this.missionsManager.CanReceiveReward(this.mission))
        {
            this.missionsManager.ReceiveReward(this.mission);
        }
    }

    //Обработка изменения прогресса миссии:
    private void OnMissionProgressChanged(Mission mission)
    {
        this.UpdateProgressBar();
    }

    //Обработка завершения миссии:
    private void OnMissionCompleted(Mission mission)
    {
        this.view.rewardButton.SetActive(true);
    }

    private void UpdateProgressBar()
    {
        var progress = this.mission.GetProgress();
        var text = this.mission.GetTextProgress();
        this.view.ProgressBar.SetProgress(progress, text);
    }
}

Теперь поговорим о том, как вывести список миссий на экран, имея классы MissionView и MissionPresenter. Так как информация и количество миссий хранятся в менеджере миссий, мы можем сделать MissionListAdapter, который будет идти туда и рисовать миссии в виде списка на экране:

public sealed class MissionListAdapter : MonoBehaviour
{
    [SerializeField]
    private Item[] missionItems; //Графические элементы для миссий

    //Метод "Показать список миссиий"
    public void Show()
    {
        MissionsManager.Instance.OnMissionChanged += this.OnMissionChanged;

        //Отрисовка миссий на UI:
        var missions = MissionsManager.Instance.GetMissions();
        for (int i = 0, count = missions.Length; i < count; i++)
        {
            var mission = missions[i];
            var item = this.GetItem(mission.Difficulty);
            var presenter = new MissionPresenter(mission, item.view);
            presenter.Start(mission);

            item.presenter = presenter;
        }
    }

    //Метод "Скрыть список миссиий"
    public void Hide()
    {
        MissionsManager.Instance.OnMissionChanged -= this.OnMissionChanged;

        for (int i = 0, count = this.missionItems.Length; i < count; i++)
        {
            var item = this.missionItems[i];
            var presenter = item.presenter;
            presenter.Stop();
            
            item.presenter = null;
        }
    }

    //Перерисовка GUI миссии, если миссия в системе поменялась:
    private void OnMissionChanged(Mission mission)
    {
        var item = this.GetItem(mission.Difficulty);
        if (item.presenter != null)
        {
            item.presenter.Stop();
        }

        var presenter = new MissionPresenter(mission, item.view);
        presenter.Start(mission);

        item.presenter = presenter;
    }

    //Поиск GUI элемента миссии по типу сложности:
    private Item GetItem(MissionDifficulty difficulty)
    {
        for (int i = 0, count = this.missionItems.Length; i < count; i++)
        {
            var item = this.missionItems[i];
            if (item.difficulty == difficulty)
            {
                return item;
            }
        }

        throw new Exception($"Mission with difficulty {difficulty} is not found"!);
    }

     //Вспомогательная структура, которая сопоставляет View и Presenter
    [Serializable]
    private sealed class Item
    {    
        [SerializeField]
        public MissionDifficulty difficulty;
        
        [SerializeField]
        public MissionView view;
  
        public MissionPresenter presenter;
    }    
}

Затем напишем код попапа, класс MissionPopup, в котором будет храниться адаптер для списка миссий и кнопка «Закрыть»:

public sealed class MissionsPopup : MonoBehaviour 
{
    [SerializeField]
    private MissionListAdapter missionsAdapter;  

    [SerializeField]
    private Button closeButton;

    //Метод "Показать попап"
    public void Show()
    {
        this.gameObject.SetActive(true);
        this.missionsAdapter.Show();
        this.closeButton.onClick.AddListener(this.OnCloseClicked);
    }

    //Метод "Скрыть попап"
    public void Hide()
    {
        this.gameObject.SetActive(false);
        this.missionsAdapter.Hide();
        this.closeButton.onClick.AddListener(this.OnCloseClicked);
    }

    private void OnCloseClicked()
    {
        this.Hide();
    }
}

Наконец, мы подключаем сценарии MissionsPopup и MissionListAdapter к макету.

ЧИТАТЬ   Что такое зомби-клетки и как от них убежать - Лайфхакер
ba70b605d8a03718c80afb96a2697570

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

Надеюсь, мне удалось схематично описать, как можно реализовать GUI для миссий. В следующей части мы увидим, как можно сохранять данные. Спасибо за внимание 🙂

Наконец, я хотел бы пригласить вас на бесплатный урок, где мы начнем создавать игру Top Down с нуля на Unity. Заглянем в Unity Asset Store и другие сайты с графикой, определимся с дизайном будущей игры.Рассмотрим ассет Top Down Engine. С нуля соберем игровой уровень и создадим персонажа.

Source

От admin