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-компаниях.
  • Практический опыт: Вы получите навыки, которые сможете применять в реальных проектах.
  • Квалифицированный сертификат: По окончании курса вы получите сертификат, подтверждающий ваши знания.

МЕРОПРИЯТИЯ

Комментарии

 
 

ВАКАНСИИ

Добавить вакансию
Ведущий SRE инженер
Москва, по итогам собеседования
Senior DevOps Developer
Лимасол, по итогам собеседования

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

LIVE >

Подпишись

на push-уведомления