Динамический список [UI]

Данный список можно использовать, например, для создания игровой лавочки по продажи всякого шмота и прочего. Допустим, у нас есть несколько игровых предметов. Каждый предмет имеет уникальный идентификатор, который мы присваиваем данному объекту, при его создании. Все предметы (префабы) хранятся само собой в специальном массиве. Чтобы отправить объект в список, просто берем его имя, id. Например, «Говорящие носки Императора» под номером 54, и отправляем эту информацию в список. На основе имени и id, будет создан элемент списка. Если игрок кликнул по элементу или удалил (купил) его, то скрипт сообщит соответствующую информацию о действии, что можно использовать уже для дальнейшей обработки.

Чтобы создать список, нужно всего несколько минут.

Добавить на сцену Scroll Rect, удалить горизонтальный скролл, настроить внешний вид:

Динамический список [UI]

Затем, нужно собрать сам элемент, который будет добавляться в список. Для этого нам понадобятся две UI кнопки, одна основная, а вторая чуть меньше, которая будет дочерней к основной, допустим так:


Важно! У основной кнопки, должен установлен пресет Rect Transform, как на скриншоте ниже:


Дополнительно, на кнопку вешаем скрипт:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;

public class ItemButton : MonoBehaviour {

	public int id; // идентификатор объекта, который храниться в списке
	public Button mainButton; // основная кнопка, на который будет отображено название объекта
	public Text mainButtonText; // текст главной кнопки
	public Button removeButton; // дочерняя кнопка, которая удаляет главную

}

Скрипт нужен для хранения ссылки на необходимые нам компоненты.

Далее, куда-нибудь вешаем главный скрипт, который и формирует список:

using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;

public class ItemList : MonoBehaviour {

	public ScrollRect scroll;
	public RectTransform element; // кнопка из которой будет составлен список
	public int offset = 10; // расстояние между элементами

	private Vector2 delta;
	private Vector2 e_Pos;
	private List<RectTransform> buttons;
	private int size;
	private float curY, vPos;

	void Awake()
	{
		buttons = new List<RectTransform>();
		delta = element.sizeDelta;
		delta.y += offset;
		e_Pos = new Vector2(0, -delta.y / 2);
	}

	void ButtonPressed(int id, string title)
	{
		Debug.Log(this + " Выбран элемент списка -> '" + title + "'. Идентификатор объекта: " + id);
	}

	void ButtonRemoved(int id, string title)
	{
		Debug.Log(this + " Удален элемент списка -> '" + title + "'. Идентификатор объекта: " + id);
	}

	public void ClearItemList()
	{
		size = 0;
		foreach(RectTransform b in buttons)
		{
			Destroy(b.gameObject);
		}
		buttons = new List<RectTransform>();
		RectContent();
		Debug.Log(this + " Все элементы списка удалены!");
	}

	void GetCurrentButton(int id) // поиск id и заголовка, нажатой кнопки
	{
		ItemButton item = null;
		foreach(RectTransform b in buttons)
		{
			item = b.GetComponent<ItemButton>();
			if(item.id == id) break;
		}
		ButtonPressed(item.id, item.mainButtonText.text);
	}

	void UpdateList(int id) // функция удаления элемента
	{
		vPos = scroll.verticalNormalizedPosition; // запоминаем позицию скролла
		int j = 0;
		ItemButton item = null;
		foreach(RectTransform b in buttons)
		{
			item = b.GetComponent<ItemButton>();
			if(item.id == id) break; // находим нужный элемент
			j++;
		}
		string title = item.mainButtonText.text; // сохраняем заголовок
		int index = item.id; // сохраняем id
		Destroy(item.gameObject); // удаляем этот элемент из списка
		buttons.RemoveAt(j); // удаляем этот элемент из массива
		curY = 0;
		size--; // минус один элемент
		RectContent(); // пересчитываем размеры окна
		foreach(RectTransform b in buttons) // сдвигаем элементы
		{
			b.anchoredPosition = new Vector2(e_Pos.x, e_Pos.y - curY);
			curY += delta.y;
		}
		scroll.verticalNormalizedPosition = vPos; // возвращаем позицию скролла
		ButtonRemoved(index, title); // вывод конечной информации
	}

	void SetMainButton(Button button, int value) // настройка функций при нажатии на главную кнопку
	{
		button.onclick.AddListener(() => GetCurrentButton(value));
	}

	void SetRemoveButton(Button button, int value) // настройка функций при нажатии на кнопку удаления
	{
		button.onclick.AddListener(() => UpdateList(value));
	}

	// добавление нового элемента в список
	// id - идентификатор объекта который нужно сохранить
	// text - текст для основной кнопки (имя объекта)
	// resetScrollbar - сбросить или нет, позицию скролла
	public void AddToList(int id, string text, bool resetScrollbar)
	{
		element.gameObject.SetActive(true);
		vPos = scroll.verticalNormalizedPosition;
		curY = 0;
		size++;
		RectContent();
		foreach(RectTransform b in buttons)
		{
			b.anchoredPosition = new Vector2(e_Pos.x, e_Pos.y - curY);
			curY += delta.y;
		}
		BuildElement(id, text);
		if(!resetScrollbar) scroll.verticalNormalizedPosition = vPos;
		element.gameObject.SetActive(false);
	}

	void BuildElement(int id, string text) // создание нового элемента и настройка параметров
	{
		RectTransform clone = Instantiate(element) as RectTransform;
		clone.SetParent(scroll.content);
		clone.localScale = Vector3.one;
		clone.anchoredPosition = new Vector2(e_Pos.x, e_Pos.y - curY);
		ItemButton item = clone.GetComponent<ItemButton>();
		SetMainButton(item.mainButton, id);
		SetRemoveButton(item.removeButton, id);
		item.mainButtonText.text = text;
		item.id = id;
		buttons.Add(clone);
	}

	void RectContent() // определение размера окна с элементами
	{
		float height = delta.y * size;
		scroll.content.sizeDelta = new Vector2(scroll.content.sizeDelta.x, height);
		scroll.content.anchoredPosition = Vector2.zero;
	}
}

Собственно вот и всё.

Пример использования можно посмотреть в демо:

Внимание! Посетители, находящиеся в группе Гости, не могут скачивать файлы.

Комментариев 11

Офлайн
Feuermagier 26 ноября 2017
Спасибо! Данный скрипт помог с созданием системы дневника. Зашел с минимальными правками.
Офлайн
Light,
шобы не лагало при длинном списке - нужна виртуализация списка
Офлайн
Light 5 октября 2016
Aulexian, enum нужно делать отдельно, чтобы он был общим для всех классов, типа:

public enum MyEnum
{
	Armor_1,
	Armor_2,
	Armor_3
}

Затем через switch базового класса возвращать по запросу, массив значений например.
Офлайн
Aulexian 5 октября 2016
Попробовал реализовать. Наверное достал уже, извиняюсь, всё туплю)
Есть общий класс ItemGeneral, локальный ItemLocal. В общем есть некая переменная брони, оружия, зелий. В локальном делаю enum и в публичном старте делаю switch. Когда enum Armor, то в кейсе наследую переменную от родителя "int a = _armorProperty;" и break, c Weapon и Potion соответственно.
Вроде бы и берём только нужное, и переменные видны в инспекторе, но как показывать в инспекторе по типу только необходимое? Также, косяк, из switch не вытянуть переменные, в теории.
Предположу, необходимо разбить на энамы в базовом классе, а с локального спрашивать тип энама.
Офлайн
Light 4 октября 2016
Aulexian, примерно так, хотя и индивидуальные характеристики тоже можно запрашивать из базового класса. Так будет проще реализовать улучшения для конкретного оружия, всё в одном классе. А локальный просто берет нужные настройки, исходя из типа и айди объекта.
Офлайн
Aulexian 4 октября 2016
То есть, в общем классе у нас есть типы enum, к примеру {Мечи, Топоры, Луки}, передаём локальному скрипту переменные и общие свойства, объявленные внутри switch по этим типам, и в локальном скрипте уже назначаем свои значения переменных (индивидуальных характеристик) , а также до этого проделываем в общем классе подобное, но с {Оружие, Броня, Ингридиенты} ?
Сегодня начал смотреть про Enum, сижу разбираюсь.
Офлайн
Light 3 октября 2016
Aulexian, полагаю будет проще сделать общий класс, который хранит настройки для предметов. Когда объект создается на сцене, на нем например, висит локальный класс, он запрашивает у общего настройки, в качестве кода можно разбить просто на типы enum и идентификаторы. Допустим тип "топор", и айдишки "топор А", "топор Б" и т.п. Потому как у общих типов и общие свойства, например, повреждение "рубящие" у всех топоров. А индивидуальные характеристики (прочность, сила атаки и т.п.), привязываются к айдишнику.
Офлайн
Aulexian 3 октября 2016
Light,
Благодарю.
И ещё вопрос (делаю подобие инвентаря с сайта) - хотел заносить целый ГО в Лист по попадании рейкаста, но подумал, что будет лучше заносить туда скрипт со всеми характеристиками заместо целого ГО. Но тут загвоздка, ведь могут быть разные типы вещей, не прописывать же каждому все переменные, ставя ненужным\отсутствующим 0\false.
Есть ли элегантный выход, например что-то с наследованием от общего скрипта? Сам пока додумать не могу.
Офлайн
Light 26 сентября 2016
Aulexian, удалять все же не стоило. Что до оптимизации, то тут наверно лучше подойдет пул объектов. Соответствующая тема есть на сайте, при желании, можно интегрировать.
Офлайн
Aulexian 26 сентября 2016
Light,
Почти сразу удалил комментарий, с разрешением экрана проблема была, моя глупая ошибка)
Экспериментирую с кодом, у примера серьёзный недостаток - фпс при увеличении списка сильно проседает.
Офлайн
Light 24 сентября 2016
Aulexian, проверил на версии 5.4.1, никаких проблем ни в редакторе ни в приложении. В логе ошибок так же не обнаружено.
Информация
Посетители, находящиеся в группе Гости, не могут оставлять комментарии к данной публикации.
  • Яндекс.Метрика