danilaxxl danilaxxl

CollectableItemData.cs

[CreateMenuItem(fileName = "newItem", menuName = "Data/Items/Collectable", order = 51]

GoloGames GoloGames

vadya_ivan, рад, что вам игра показалась интересной : )

P.S. Кстати уже доступна бесплатная демо-версия в Steam

vadya_ivan vadya_ivan

Визуал, задумка, музыка , механики, все в цель

GoloGames GoloGames

Ato_Ome, спасибо за позитивные эмоции, будем стараться : )

Ato_Ome Ato_Ome

Потрясающий результат, все так четенько, плавненько)
То ли саунд, то ли плавность напомнили мне игрушку World of Goo, удачи вам в разработке и сил побольше дойти до релиза!)

Cute Fox Cute Fox

Graphics are a little cool, good HD content. But this game doesn't cause nary interest me.
However the game is well done.

GMSD3D GMSD3D

Почему действие после всех условий выполняется?
[step another object]

Zemlaynin Zemlaynin

Jusper, Везде, но наугад строить смысла нет. Нужно разведать сперва территорию на наличие ресурсов.

Jusper Jusper

Zemlaynin, а карьеры можно будет везде запихать?
Или под них "особые" зоны будут?

Zemlaynin Zemlaynin

Это так скажем тестовое строительство, а так да у города будет зона влияния которую нужно будет расширять.

Jusper Jusper

А ссылка есть?

Jusper Jusper

Я не оч понял из скриншота, как вообще работает стройка. У игрока будет как бы поле строительства?

split97 split97

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

split97 split97

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

ViktorJaguar ViktorJaguar

Почему я нигде не могу найти нормальный туториал, где покажут как экипировать предмет (например, меч) в определенную (выделенную под оружие) ячейку???

Логотип проекта Unity

Хранение и доступ UI-элементов в Unity

Здравствуйте уважаемые читатели! Сегодня я хотел бы затронуть тему хранения UI-элементов и доступа к ним, а также узнать, как вы это делаете в своих проектах (и не обязательно в Unity).
Дело в том, что когда я решил сделать один проект, он должен был полностью быть связан с UI-элементами. Тогда я задумался о том, как мне вызывать элементы интерфейса, а самое главное - где их хранить?

Вступление

Поискав в интернете, я видел лишь то, что даже в уроках по Unity разработчики проводят классическую инициализацию элементов (т.е. через Inspector, а поля у них открытые, хоть кто-угодно может изменить их). Очевидно, такой подход плох тем, что загромождает класс множеством экземпляров разных объектов (Text, Image, etc.), но самое ужасно то, что логика оперирования с игровыми данными идёт в одном объекте вместе с оперированием над UI-элементами. Проще говоря, это рушит некоторые принципы ОО-проектирования, что является не особо хорошей практикой.
С другой стороны это облегчает работу над небольшими проектами, которые можно сделать за пару дней (классический пример "крестики-нолики"), однако я хочу поговорить о том, как же всё-таки нам оперировать с UI для более серьезных проектов (не AAA конечно, хотя кто знает какой код пишется в больших компаниях).
Итак, у меня было несколько интересных идей на тему того, как же можно сделать не просто хранение элементов, но и оперирование с ними. Кто-то может сказать: "а как же такие архитектурные решения как MVC или MVVM? Это же отличное решение для такой примитивной задачи!1!!" - Конечно, существуют разные подходы к решению подобных задач, но здесь всё не так просто, как вам может показаться (имею в виду среду Unity)

Ох этот дивный API

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

[Serializable]
    public struct StorageUIElements
    {
        [SerializeField]
        private List<RectTransform> elements;
        private Component lastElement;

        internal T GetElement<T>(byte id) where T : Component
        => elements[id].GetComponent<T>();
        internal T GetElement<T>(string name) where T : Component
        => elements.Find(element => element.name.Contains(name)).GetComponent<T>();
        internal T GetLastElement<T>() where T : Component
        => lastElement.GetComponent<T>();

        internal T GetElementIterator<T>(byte id) where T : Component
        {
            
            if (lastElement == null)
            {
                lastElement = GetElement<T>(id);
                return lastElement as T;
            }
            else return lastElement as T;
        }

        internal T GetElementIterator<T>(string id) where T : Component
        {

            if (lastElement == null)
            {
                lastElement = GetElement<T>(id);
                return lastElement as T;
            }
            else return lastElement as T;
        }
        
        internal void ClearLastElement() => lastElement = null;

    }

В данном случае - метод "GetElementIterator" был сделан для повторных вызовов к одному и тому же элементу и если нужно будет переключиться на другой элемент - предварительно вызываем метод "ClearLastElement", подробнее о таком подходе смотрите далее в разделе "Об оптимизации".
Но, возможно кто-то уже успел заметить, что обобщения ограниченны... ограниченны... Правильно - классом Component. "Но как же так? Мы ведь может получать из этих элементов абсолютно любой компонент, это ведь нехорошо..." - да, так и есть, а теперь об ограничениях.
Дело в том, что в мы не можем сделать так, чтобы наш метод принимал более одного ограничителя по классу (такой запрет имеется в самом языке C#), но мы можем добавлять разное кол-во интерфейсов, прямо как при наследовании класса. Так вот, дело в том, что при изучении архитектуры компонентов в Unity (таких как Text, Image etc.) я увидел, что большая часть компонентов никак не взаимосвязана, то есть не имеют общего класса. Точнее имеют, но всё что их связывает - класс "RectTransform" (который как раз используется при инициализации списка). Например класс Text и Image базируются на основе класса Graphic, в то время как, например, Button - нет, но наследуется от Selectable, а CanvasGroup и вовсе от Component! (от Component Карл! ).
Вот так грубо представлена иерархия компонентов UI (показаны лишь основные компоненты):

Хранение и доступ UI-элементов в Unity — Unity — DevTribe: инди-игры, разработка, сообщество (GameDev, perfomance, programming, Unity)

Теперь решите задачку: Как хранить в N'ом количестве объектов, которые должны выводить информацию на экран N'ое кол-во элементов UI, с условием, что кол-во может увеличиваться или уменьшаться, а также мы бы имели доступ к любому свойству нужного элемента И не хранили бы все элементы в кучке в каждом N'ом объекте?
Я буду рад услышать ответ, ведь, возможно, вы поможете найти самый удобный подход к UI-элементам!
Кстати, в одной из моих версий я также думал сделать класс-обработчик такого контейнера, который выглядит примерно так:

public sealed class OutputUIElements
    {
        private StorageUIElements storage;

        public OutputUIElements(StorageUIElements storage)
        {
            this.storage = storage;
            
        }
        public void SetText(byte id, object someValue)
        => storage.GetElement<Text>(id).text = someValue.ToString();
        public void SetText(string id, object someValue)
        => storage.GetElement<Text>(id).text = someValue.ToString();

        public void SetTextIterator(byte id, object someValue)
       => storage.GetElementIterator<Text>(id).text = someValue.ToString();
        public void SetTextIterator(string id, object someValue)
       => storage.GetElementIterator<Text>(id).text = someValue.ToString();

    }

А чтобы далее оперировать с элементами, я создавал класс-контейнер в нужном нам классе, а также класс-обработчик:

public class TestOutputResult : MonoBehaviour
{
	[SerializeField]
	private StorageUIElements storage;
	
	private OutputUIElements output;
	
	private void Start()
	{
	output = new OutputUIElements(storage);
	
	//Example
	output.SetText("Score", 2500);
	}
}

Зачем? Дело в том, что я хотел избавиться от дублирования кода и попытаться сделать все методы по изменению данных на более абстрактном уровне. Как оказалось, всё самое неприятное впереди...

Об оптимизации

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

  1. Доступ к любому свойству любого элемента списка. Это и плюс и минус. Плюс заключается в том, что нам не обязательно хранить в классе-обработчике все операции. С другой стороны, не стоит забывать, что возвращающий метод даст нам Component, а значит мы можем изменить свойства совершенно любого компонента, что очень плохо.
  2. Класс-обработчик очень низкоуровневый. Я имею в виду, что в моём подходе, класс-обработчик не просто хранит примитивные методы по типу "SetText", но ещё и перегружает метод (чтобы к нему был доступ не только по индексу, но и по имени). А это значит, что если нам нужно добавить новую операцию, то мы должны написать метод два раза. Мы работаем на уровне реализации, а это плохо.
  3. Жёсткая привязка элементов в контейнере. Дело в том, что в окне "Inspector" мы можем добавлять новые элементы в список, но что если нам нужно будет убрать элемент, стоящий в середине списка? Заново переставлять все элементы вручную? Или написать обработчик на null-ую ссылку? Лучший способ для решения данной проблемы - ReordableList, однако он нуждается в реализации пользовательского инспектора.
  4. Оптимизация. На ней мы также остановимся.

Вот график, который я сделал после ряда тестов:

Хранение и доступ UI-элементов в Unity — Unity — DevTribe: инди-игры, разработка, сообщество (GameDev, perfomance, programming, Unity)

Конечно, это лишь один из тестов по выявлению эффективного доступа к UI элементам и их изменениям и кому-то покажется чересчур странным, например почему я выбрал 200.000 итераций, а не больше или меньше? Думаю, что разницы никакой нет, какое кол-во брать, главное, чтобы видеть потенциальные различия. Как вы можете видеть - самый производительный способ - хранить экземпляр UI-элемента внутри того класса, в котором он меняется или в структуре, однако время на чтение элемента из Get-метода уже даёт менее производительные данные. Самыми затрачиваемыми оказались вызовы из списка (с условием, что я использовал тот самый метод "GetElementIterator" т.к. мы обращались к элементу повторно и если бы я использовал классические методы, то по результатам тестов, они были бы хуже более чем в два раза!).
Чуть не забыл. Метод "GetElementIterator" реализован через проверку на значение null. В моём подходе так было сделано потому, что иначе, если бы мы проверяли вызываемый элемент с последним, эффективность метода стала бы хуже не менее чем в 3 раза! По результатам тестов число обработки доходило до 80 ms, что существенно ударило по производительности.
Теперь, зная, как данный подход влияет на производительность, мне уже не хочется использовать такой подход. Но как тогда быть? А самое главное, как сделать передачу данных. Может быть, стоит ещё рассмотреть static-класс с методами для вывода, но опять же всплывает проблема объема класса и инициализации элементов.

Кстати, пару слов о MVC. Я хочу показать небольшой и грубый пример использования такой архитектуры (тут ещё и двунаправленная связь).
View-объект я сделал так:

using UnityEngine;
using UnityEngine.UI;

public class PlayerView : MonoBehaviour
{
    [Serializable]
    private struct Storage
    {
        [SerializeField]
        private Text score;

        public Text GetScore() => score;
    }
    [SerializeField]
    private Storage storage;

    private PlayerController playerController;
    private Stopwatch stopWatch = new Stopwatch();

    public void OutputScore() => storage.GetScore().text = playerController.GetScore().ToString();

    private void Start()
    {
        playerController = gameObject.GetComponent<PlayerController>();

        OutputScore();
    }
}

А Controller-объект так:

using UnityEngine;
using System;

public class PlayerController : MonoBehaviour
{
    [Serializable]
    private struct Data
    {
        private short currentScore;

        public short GetCurrentScore() => currentScore;
    }

    private Data data;

    private PlayerView playerView;

    public short GetScore() => data.GetCurrentScore();

    private void Start()
    {
        playerView = gameObject.GetComponent<PlayerView>();
    }
}

Безусловно, здесь есть недостатки, но что самое важное, это организация. Здесь можно видеть, как все нужные поля инкапсулированы и никто ничего менять не может. Но что самое интересное, это тест. А тест показывает, что при таком же кол-ве итераций (200.000), затраченное время на исполнение вывода очков составило... ~50ms. Как-то так. Даже больше, чем в моём подходе со списками. Хотя может быть просто я реализовал в таком примере что-то не так, однако всё работало, даже несмотря на то, что я вывожу кол-во очков в тот же момент, когда происходит инициализация PlayerController. Не совсем хороший пример.
Однако что меня удивило, так это то, что при замене структуры, в котором хранились UI-элементы, на прямое обращение к тексту, время не изменилось. Хм, может объект для подсчёта времени "Stopwatch" что-то делает не так? Ведь ранее при прямом обращении время обработки уменьшалось.

Что ж, переходя к заключению, я лишь хочу повторить свой вопрос из начала этой публикации - каким образом вы построили свою архитектуру? И если используете такие шаблоны как MVC или MVVM, как именно вы их реализовали? Будет интересно послушать.

UPD: Прикрепляю одно из решений, предложенного пользователем mopsicus, а точнее, ссылки, оставленные им:

  1. Исходный код
  2. Применение

Обязательно гляну на практике, спасибо, товарищ!

Смотрите также:


Комментарии



  • 1
  • 2 (Текущая страница)

RedHelium - закрепить ссылки от mopsicus в тексте не хочешь?

  • 1
  • 2 (Текущая страница)
Справка