21 марта 2025

#️⃣🏗️ Как не запутаться в структурах данных в Unity и C#

Делаю игры и пишу про игры
Ваша игра в Unity работает слишком медленно? Возможно, структуры данных могут решить вашу проблему! В этой статье мы раскроем секреты эффективного управления данными в Unity и расскажем про самые популярные структуры данных.
#️⃣🏗️ Как не запутаться в структурах данных в Unity и C#

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

В процессе разработки игр мы постоянно работаем с большими объёмами данных — будь то инвентарь игрока, ИИ врагов или состояние игрового мира. Эффективная организация и доступ к этим данным крайне важны для оптимизации производительности и создания захватывающего игрового процесса. И именно здесь нам на помощь приходят структуры данных/

Структуры данных предоставляют мощные инструменты для хранения, организации и обработки данных. Используя их в Unity, вы сможете раскрыть целый мир возможностей, делая игры быстрее, более масштабируемыми и простыми в поддержке. Ниже приведены примеры того, как структуры данных могут принести пользу при разработке игр на Unity.

Array (массив)

Array (массив)
Array (массив)

Массивы — одна из базовых структур данных. Они позволяют хранить коллекцию элементов одинакового типа фиксированного размера. Обеспечивают быстрый доступ к элементам по индексу, но после инициализации размер массива изменить нельзя. В Unity массивы часто используют для хранения игровых объектов, данных игрока или других связанных данных.

        // Пример: Хранение позиций контрольных точек на пути патрулирования
Vector3[] waypoints = new Vector3[3] { new Vector3(1, 0, 0), new Vector3(0, 0, 1), new Vector3(2, 0, 2) };

void Patrol()
{
    foreach (Vector3 waypoint in waypoints)
    {
        // Перемещаем объект к следующей точке
        transform.position = waypoint;
        // Выполняем другие действия патрулирования
        // ...
    }
}
    
Структуры данных – фундамент эффективной разработки. В этой статье рассмотрим 10 ключевых структур данных, которые необходимо освоить каждому разработчику для создания производительных и масштабируемых приложений.

List (список)

List (список)
List (список)

Списки — динамические коллекции элементов, размер которых может увеличиваться или уменьшаться. Они часто применяются, когда заранее неизвестно количество элементов или оно может меняться во время выполнения. Списки предлагают большую гибкость по сравнению с массивами и имеют встроенные методы для добавления, удаления и доступа к элементам. В Unity списки обычно используют для управления такими сущностями, как враги, снаряды, предметы и т.д.

        // Пример: Хранение списка собранных предметов в системе инвентаря
List<string> collectedItems = new List<string>();

void CollectItem(string item)
{
    collectedItems.Add(item);
}

void DisplayInventory()
{
    foreach (string item in collectedItems)
    {
        Debug.Log(item);
    }
}
    

Реализованы списки в C# с помощью класса List<T>. Список по сути реализован как динамический массив, то есть под капотом хранится обычный массив типа T (где Т – любой тип данных, который вы хотите использовать), но с механизмом автоматического увеличения его размера. У каждого списка есть свойство Capacity, которое определяет, сколько элементов он может вместить, не выполняя перераспределения памяти. При добавлении элементов, если текущая вместимость исчерпана, создаётся новый внутренний массив большего размера, и все данные копируются в него.

Давайте рассмотрим список с точки зрения алгоритмической сложности. Добавление элемента в конец списка (Add) при условии, что ещё есть свободное место, происходит за константное время O(1). Если же массива внутри уже не хватает, он пересоздаётся с увеличенным размером, и операция в этот момент становится O(n). Но в среднем такая операция всё равно считается O(1), так как пересоздание происходит относительно редко. Поскольку список хранит данные в непрерывном участке памяти, доступ к элементу по индексу (например, myList[i]) работает за константное время O(1). Вставка же элемента в произвольное место или удаление какого-то конкретного элемента (кроме последнего) имеет сложность O(n), так как в этих операциях используется сдвиг элементов справа.

Dictionary (словарь)

Dictionary (словарь)
Dictionary (словарь)

Словари хранят данные в виде пар «ключ-значение». Они обеспечивают быстрый поиск по ключу, что полезно в ситуациях, когда нужно получить доступ к данным по определённому идентификатору. В Unity словари часто используют для сопоставления игровых сущностей с их данными.

        using System.Collections.Generic;

// Пример: Создание и использование словаря с очками игроков
Dictionary<string, int> playerScores = new Dictionary<string, int>();
playerScores["Player1"] = 100;
playerScores["Player2"] = 200;
playerScores["Player3"] = 150;

// Доступ к значениям словаря
int score = playerScores["Player2"];  // Получение значения, связанного с ключом "Player2" (200)

// Перебор элементов словаря
foreach (KeyValuePair<string, int> entry in playerScores)
{
    Debug.Log(entry.Key + ": " + entry.Value);
}

    
        // Пример: Управление достижениями в игре и статусом их выполнения
Dictionary<string, bool> achievements = new Dictionary<string, bool>();

void UnlockAchievement(string achievementName)
{
    achievements[achievementName] = true;
}

bool IsAchievementUnlocked(string achievementName)
{
    if (achievements.ContainsKey(achievementName))
    {
        return achievements[achievementName];
    }
    return false;
}

    

Словарь позволяет быстро находить значение по уникальному ключу. В роли ключа может выступать практически любой тип, для которого корректно определены методы GetHashCode() и Equals(). Под капотом Dictionary — это хеш-таблица, в которой каждый элемент распределяется по «корзинам» в зависимости от хеш-кода ключа. Разумеется, внутри есть механизмы разрешения коллизий (случаев, когда разные ключи дают одинаковый хеш).

Как и в List, у Dictionary есть «внутренняя емкость», определяющая, сколько элементов он может хранить, не выполняя пересоздания. При превышении этого лимита создаётся новый, более «просторный» массив корзин, а все существующие элементы перераспределяются заново.

Словарь в среднем обеспечивает сложность O(1) на добавление новых пар «ключ—значение», удаление и поиск по ключу. Это достигается за счет того, что при добавлении элемент помещается в корзину, определяемую хеш-кодом ключа, а при поиске нужная корзина выбирается по тому же хеш-коду.

#️⃣ Библиотека шарписта
Больше полезных материалов вы найдете на нашем телеграм-канале «Библиотека шарписта»

Queue (очередь)

Queue (очередь)
Queue (очередь)

Очереди реализуют принцип «первым зашел — первым вышел» (FIFO, First In – First Out). Элементы добавляются (Enqueue) в конец очереди и извлекаются (Dequeue) с её начала. Очереди часто применяются для реализации очередей заданий, систем событий или управления поведением ИИ — в общем, там, где важно обрабатывать события в определённом порядке (например, действия в пошаговой игре).

Очередь в C# (класс Queue) чаще всего реализуется как круговой буфер поверх массива. При добавлении элемента, если в текущем массиве ещё есть место, операция происходит за O(1). Если массив переполняется, он автоматически пересоздаётся с увеличенным размером, и добавление в этот момент становится O(n), но в среднем всё равно считается O(1) из-за амортизированного анализа. Извлечение элемента также выполняется за O(1) в обычных условиях, поскольку достаточно сдвинуть указатель на голову очереди.

        using System.Collections.Generic;

// Пример: Создание и использование очереди игровых событий
Queue<string> eventQueue = new Queue<string>();
eventQueue.Enqueue("Event1");
eventQueue.Enqueue("Event2");
eventQueue.Enqueue("Event3");

// Извлечение элемента
string currentEvent = eventQueue.Dequeue();  // Получает и удаляет первый элемент очереди

// Просмотр следующего элемента без его удаления
string nextEvent = eventQueue.Peek();

// Перебор элементов очереди
foreach (string item in eventQueue)
{
    Debug.Log(item);
}
    
#️⃣🎓 Библиотека C# для собеса
Подтянуть свои знания по C# вы можете на нашем телеграм-канале «Библиотека C# для собеса»

Stack (стек)

Stack (стек)
Stack (стек)

Стек работает по принципу «последним зашел — первым вышел» (LIFO, Last In – First Out). Элементы добавляются (Push) и извлекаются (Pop) с одного конца (вершины стека). Стеки часто используются для управления состояниями игры, реализации функций «отмена/повтор» или отслеживания вложенных операций. Добавление элемента (Push) в стек происходит сверху и занимает O(1), пока не потребуется расширение массива. При достижении предела ёмкости массив пересоздаётся, что в этот момент даёт O(n), но остаётся O(1) в среднем. Извлечение также занимает O(1).

        using System.Collections.Generic;

// Пример: Создание и использование стека игровых объектов
Stack<GameObject> objectStack = new Stack<GameObject>();
objectStack.Push(object1);
objectStack.Push(object2);
objectStack.Push(object3);

// Извлечение элемента из стека
GameObject currentObject = objectStack.Pop();  // Получает и удаляет верхний элемент

// Просмотр верхнего элемента без удаления
GameObject topObject = objectStack.Peek();

// Перебор элементов стека
foreach (GameObject item in objectStack)
{
    Debug.Log(item.name);
}
    
#️⃣🧩 Библиотека задач по C#
Интересные задачи по C# для практики можно найти на нашем телеграм-канале «Библиотека задач по C#»

HashSet (хэш-набор)

HashSet (хэш-набор)
HashSet (хэш-набор)

Хэш-наборы (HashSet) — неупорядоченные коллекции, которые хранят только уникальные элементы. Они полезны, когда нужно управлять множеством объектов без повторов.

        // Пример: Управление набором активных усилений (power-ups)
HashSet<string> activePowerUps = new HashSet<string>();

void ActivatePowerUp(string powerUpName)
{
    activePowerUps.Add(powerUpName);
}

void DeactivatePowerUp(string powerUpName)
{
    activePowerUps.Remove(powerUpName);
}

bool IsPowerUpActive(string powerUpName)
{
    return activePowerUps.Contains(powerUpName);
}
    

HashSet работает по тому же принципу хеш-таблицы, что и Dictionary, но она хранит только ключи без сопутствующих значений. Как и в словаре, каждая операция добавления, удаления или проверки элемента (Contains) в среднем выполняется за O(1), и точно так же при превышении внутренней ёмкости происходит расширение и перераспределение элементов, влекущее за собой более дорогую операцию O(n) — но это случается редко.

HashSet оптимизирован именно под операции, связанные с наличием элемента, объединением, пересечением и разностью множеств.

Собственные структуры данных

Собственные структуры данных
Собственные структуры данных

C# также позволяет создавать собственные структуры данных, полностью соответствующие логике вашей игры. Вы можете спроектировать необходимые структуры — например, сетки для управления уровнем или любую другую специализированную модель организации информации.

Главное — выбирать подходящую структуру данных исходя из специфики вашей игры и типов данных, с которыми вы работаете. При принятии решения учитывайте факторы, связанные с производительностью, объёмом памяти и теми операциями, которые вы планируете проводить над данными.

Объединив возможности различных структур данных с гибкостью и мощью Unity и C#, вы сможете вывести разработку игр на новый уровень и открыть новые горизонты для своих проектов. От оптимизации производительности до реализации сложных игровых систем — грамотное использование структур данных позволит эффективно и элегантно решать возникающие задачи.

Поэтому, будь вы опытным разработчиком игр или только начинаете свой путь, обязательно изучайте увлекательный мир структур данных. Экспериментируйте с разными способами хранения и обработки данных, и вы сами увидите, как это отразится на производительности, масштабируемости и общем дизайне ваших проектов!

***

Как перестать писать спагетти-код: интенсив по алгоритмам и структурам данных

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

Что вас ждет на курсе:

  • Интенсивная программа: 47 видеолекций и 150 практических заданий для закрепления материала.
  • Поддержка преподавателей: Комментарии и советы по заданиям на протяжении всего обучения.
  • Гибкий формат: Курс в записи на платформе CoreApp, доступен в любое время.
  • Длительность: 6 месяцев обучения с возможностью вернуться к материалам в любое время.

Для кого подходит курс:

  • Junior-разработчики: Если вы хотите стать разработчиком или устроиться на эту должность.
  • Middle-разработчики: Освежите знания и научитесь решать сложные задачи.
  • Все, кто хочет улучшить навыки: Знание алгоритмов расширяет инструментарий разработчика и помогает в решении практических задач.

Почему важно изучать алгоритмы:

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

Комментарии

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ