Dreaman Dreaman

Mariya, законченный вариант реально классно выглядит! Молодец :)

Mariya Mariya

Всем привет!
Сегодня хочу показать законченный вариант домика Сырны.

alexprey alexprey

StarPlosion: Битва с пиратами за планету

Wings' might Wings' might

Всем привет)
Добавил на этой неделе начальное окно, новую валюту и достижения:

alexprey alexprey

shadeborn, может быть они нашли способ как обойти полноценный запуск этих сервисов при запуске игры? Если так, то почему бы и не использовать единый клиент?) В любом случае, чем дальше все идет, там это все больше становится похоже на эту шутку

...
shadeborn shadeborn

Это всё круто, но...зачем? Ок есть Стим. Хорошо, теперь еще и ЕГС есть. Благо, комп не зашкварен Оригином...но у людей и он стоит. И для работы игр внутри этих сервисов нужны, собсно, эти сервисы. Так поверх этих сервисов нужно накатить еще один...

Devion Devion

согласен, есть кейсы которые без них вообще не делаются, не знать или избегать их определенно неправильно

Так точно, либо любое изменение в объекте на стороне редактора.

alexprey alexprey

Devion, только рад за дополнение, Спасибо)

хм, а вот это интересно, не пробовал никогда такой кейс.

Devion Devion

лёш, ты же не против если я дополню? )

скриптаблы, для понимания могут храниться на уровне ассета и на уровне сцены.

Mariya Mariya

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

Wings' might Wings' might

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

alexprey alexprey

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

Dreaman Dreaman

Всем привет!
Для проекта "Mental State" разработано новое устройство, которое уже полностью функционирует внутри игрового мира. Оно носит название "Репульсивер". Совместно с очередными большими воротами это устройство образует новую головоломку...

...
Mariya Mariya

Всем привет!
На этой неделе мы научили Сырну летать!

Tartal Tartal

alexprey, кастомизации - создание внешности персонажа? Я всегда любил это, но не думаю, что это будет к месту в мясном шутере)
EfimovMax, да, есть немного)

Tartal Tartal

Jusper, точно не помню, уже как полгода точно) Я вроде в Дискорде немного обсуждал эту тему. Пока немножко попробовал движок - мне очень нравится. Ну, в конце концов, он идеально подходит под жанры, с которыми я хочу работать...

alexprey alexprey

Первая тема крутая очень!

Логотип проекта 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 (Текущая страница)

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

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