Всем привет! Меня зовут Николай Запольнов, я преподаватель на курсах Unity Basic и Unity Pro в OTUS. И сегодня я хочу рассказать вам про, без преувеличения, будущее Unity — технологию DOTS.
В этой статье мы узнаем, что же такое этот DOTS, какие проблемы он решает и как применить его на практике.
Но начать, все же, стоит с теории.
DOTS (“Data Oriented Tech Stack” — технический стек, ориентированный на данные) — это новый подход к разработке игр на движке Unity. Впервые он был анонсирован в 2017 году, а некоторые его компоненты еще раньше — в 2016. В рамках этого технического стека Unity руководствуется слоганом “performance by default” (“производительный по умолчанию”): они создают оптимизированный и эффективный фреймворк в противоположность “классическому” Unity, где программист должен внимательно выбирать, какое API он использует, создавать пулы объектов и т.д.
Для обеспечения максимальной производительности, DOTS включает в себя множество компонентов. Так, например, Job System (Система задач) предоставляет удобный и простой API для многопоточного программирования. Но самым важным нововведением DOTS является использование паттерна Entity-Component-System (ECS).
Entity Component System
Одним из основных столпов классического объектно-ориентированного программирования является наследование. Дочерние классы, собственно, наследуют свойства и методы родительского класса, расширяя и дополняя их новыми возможностями. Такой подход позволяет переиспользовать код, но в больших проектах он также создает и проблему: построить подходящую иерархию объектов не всегда возможно. В результате, в проекте появляются так называемые божественные классы (god classes), реализующие огромный набор методов, часть которых используется одними дочерними классами, а часть — другими.
В качестве альтернативы наследованию была предложена композиция. В геймдеве такой подход часто называют Entity-Component (EC) и именно он применяется в “классическом” Unity. В паттерне EC сущности (entities, в Unity они реализованы классом GameObject) являются лишь контейнерами для компонентов. А компоненты, в свою очередь, реализуют конкретную функциональность и содержат в себе как код, так и данные. Это позволяет собирать каждую конкретную сущность из фрагментов функциональности, как из кубиков Лего.
Паттерн Entity-Component-System развивает эту парадигму еще дальше. Он предлагает оставить в компонентах только данные, а код вынести в системы. Это позволяет оперировать группой компонентов и выводит переиспользование кода на новый уровень. А еще, такой подход позволяет хранить данные компонентов в памяти последовательно. Это сильно увеличивает эффективность работы кеша на современных процессорах и, соответственно, повышает производительность.
Но довольно теории, давайте перейдем к практике!
Подготавливаем проект
Сразу хочется предупредить, что хотя DOTS и находится в разработке уже много лет, он все еще находится в состоянии preview и довольно сырой. Многие компоненты еще не завершены и поддерживают только часть функциональности, а иногда могут содержать и серьезные баги. По этой причине, в сегодняшней статье я буду использовать смешанный подход: часть кода будет написана на DOTS, а часть будет реализована на старых добрых GameObject’ах.
Эта статья написана и проверена на Unity 2020.1.16f1. Вы можете использовать и другую версию, но тогда существует вероятность, что что-то не заработает. DOTS очень активно развивается и программные интерфейсы иногда меняются.
Начнем с пустого проекта (я использовал шаблон “3D” в Unity Hub). Итак, прежде всего нам потребуется добавить DOTS в наш проект. Делается это через менеджер пакетов, но просто так их не найти. Некоторое время назад авторы Unity убрали все preview-пакеты из общего списка, и теперь, чтобы их установить, необходимо нажать в верхнем левом углу пакетного менеджера кнопку “+” и в появившемся меню выбрать “Add package from git URL…”:
Потребуется установить следующие пакеты:
- com.unity.entities — реализация паттерна Entity-Component-System в DOTS.
- com.unity.physics — физический движок для DOTS.
- com.unity.rendering.hybrid — гибридный рендерер для DOTS, выполняет роль “моста” между системой DOTS и стандартной архитектурой рендеринга Unity.
Установка этих пакетов также приведет к установке и других компонентов DOTS. Давайте кратко рассмотрим, что еще добавится в наш проект:
- com.unity.burst — компилятор высокопроизводительного кода для C#.
- com.unity.collections — альтернативные версии коллекций (список, очередь, словарь и т.д.), основанные на использовании памяти движка вместо сборщика мусора.
- com.unity.jobs — система многопоточного программирования для DOTS. Позволяет легко распараллеливать код и автоматически отслеживать ошибки, возникающие при многопоточном программировании. Например, “условия гонки” (race conditions).
- com.unity.mathematics — оптимизированная библиотека векторной математики взамен стандартных Vector2, Vector3 и т.д.
Кроме компонентов DOTS нам также пригодится пакет Cinemachine. Это очень удобный инструмент для управления камерами в Unity. Подробно останавливаться на нем сегодня я не буду, но крайне рекомендую ознакомиться, если вы про него еще не слышали. Устанавливаем:
Ну и наконец стоит добавить в проект пару наборов ассетов. Я буду использовать следующие бесплатные паки:
Познаем сущности
Для начала, давайте создадим в сцене плоскость, которая будет играть роль Земли. Я делаю это как обычно: щелкаю правой кнопкой мыши в окне Hierarchy и выбираю 3D Object⇨Plane:
Назначу ей материал зеленого цвета, чтобы она была больше похожа на траву:
Теперь у нас в сцене есть обычный GameObject, изображающий плоскость. Так причем же здесь DOTS? Пока что не причем. На текущем этапе развития технологии, сцену в Unity мы по-прежнему создаем на основе игровых объектов.
Но теперь у нас на вооружении есть новый компонент: ConvertToEntity. Добавив этот компонент в объект Plane, мы укажем Unity при запуске игры превратить его в сущность в системе ECS:
Компонент предлагает два режима преобразования: Convert and Destroy (выбран по умолчанию) и Convert And Inject Game Object. Выбор первого режима приводит к удалению игрового объекта после создания сущности, а при выборе второго игровой объект сохраняется. Мы пока оставим эту настройку как есть, т.е. будем уничтожать объект.
Давайте запустим игру и посмотрим, что получилось:
Итак, игровой объект Plane пропал из иерархии, но плоскость все же рисуется в игровом окне. Очевидно, что теперь она стала сущностью. Как можно убедиться в этом?
Unity предоставляет для этого удобный инструмент, который можно найти в меню Window⇨Analysis⇨Entity Debugger:
В левой части этого окна показаны все системы в порядке их исполнения, а также длительность работы каждой из них. Мы пока не создали ни одной своей системы, поэтому все, что мы видим здесь — встроенные системы Unity. Не будем подробно на них останавливаться.
Выбрав конкретную систему, в правом столбце можно увидеть, какими конкретно сущностями она оперирует. А если выбран пункт All Entities, как на скриншоте, то мы увидим список абсолютно всех сущностей. В моем примере их всего две: WorldTime (стандартная сущность Unity, отслеживающая текущее время) и Plane — наша плоскость!
Если выбрать сущность Plane, инспектор покажет нам, какие компоненты она в себя включает:
Как видите, вместо стандартных компонентов Unity здесь используются совершенно другие, новые компоненты. Так, компонент Transform был заменен на три компонента: LocalToWorld, Rotation и Translation. Вместо MeshFilter и MeshRenderer используется компонент RenderMesh. А для физики добавился компонент PhysicsCollider.
Я покажу как пользоваться этими компонентами чуть позже. Пока же для нас важно обратить внимание на следующие моменты:
- Сущности DOTS существуют в своем отдельном мире и никак не взаимодействуют с игровыми объектами.
- Для стандартных компонентов Unity существуют аналогичные им по функциональности компоненты и системы DOTS (здесь важно отметить, что многие из них еще в разработке и не обладают всей полнотой возможностей стандартных компонентов)
- Редактировать параметры компонентов во время исполнения игры в инспекторе нельзя. Что довольно-таки неудобно при отладке. Надеюсь это все-таки поправят.
Что за игра без игрока?
Давайте теперь создадим главного персонажа, которым будет управлять наш игрок. Я добавлю новый, пустой игровой объект и назову его Player. Сразу же добавлю туда компонент ConvertToEntity, чтобы не забыть про него.
Для удобства, я создам отдельный, вложенный игровой объект для внешнего вида игрока (т.е. содержащий 3d-модель персонажа). Давайте для начала используем простую капсулу (щелчок правой кнопкой в иерархии, 3D Object⇨Capsule). Чтобы она не проваливалась в землю, ей необходимо поставить Position.Y равным 1.
При создании капсулы, Unity сразу же создает и коллайдер. Но чтобы физика могла управлять нашим игровым объектом, потребуется добавить соответствующий компонент. Его я буду добавлять в родительский объект (Player) и вместо привычного RigidBody я добавлю компонент DOTS, который называется PhysicsBody:
В добавленном компоненте нужно поправить несколько параметров. Во-первых, массу (параметр Mass) стоит увеличить: я поставлю 70. Во-вторых, я сброшу параметр Angular Damping в 0. Поскольку у нас персонаж будет поворачиваться из кода, мы не хотим, чтобы физика замедляла это движение. Ну и наконец, я поставлю Gravity Factor равным 1.5. В моих экспериментах, если оставить этот параметр равным 1, физика воспринимается как на Луне.
Итак, у нас есть персонаж-капсула. Но как заставить его двигаться? Если помните, я говорил, что в ECS любая логика должна находиться в системах. Нам потребуется написать систему движения игрока. А чтобы система знала, какими сущностями она должна оперировать, мы создадим специальный компонент и назначим его объекту персонажа.
Код компонента (Scripts/Components/PlayerComponent.cs):
using Unity.Entities;
[GenerateAuthoringComponent]
public struct PlayerComponent : IComponentData
{
public float movementSpeed;
public float rotationSpeed;
}
Очевидно, что он значительно отличается от привычных компонентов. Так, например, необходимо использовать библиотеку Unity.Entities вместо UnityEngine. Взамен класса создается структура. Родительский класс MonoBehaviour был заменен интерфейсом IComponentData. А еще в компоненте нет никаких методов!
Важно также обратить внимание на атрибут GenerateAuthoringComponent. Если его не добавлять, то в целом такой компонент тоже можно использовать. Но его нельзя будет создавать и редактировать в Unity. Именно благодаря этому атрибуту мы теперь сможем в инспекторе добавить наш новый компонент в игровой объект Player:
И даже параметры movementSpeed и rotationSpeed доступны для редактирования! Все, как мы привыкли. На скриншоте я уже проставил туда соответствующие значения (20 и 500).
Давайте теперь реализуем систему (Scripts/Systems/PlayerMovementSystem.cs):
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Transforms;
public class PlayerMovementSystem : ComponentSystem
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
float2 input = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
Entities.ForEach((ref PlayerComponent player, ref LocalToWorld transform, ref PhysicsVelocity velocity) => {
float3 dir = transform.Forward * input.y * player.movementSpeed * deltaTime;
velocity.Linear += new float3(dir.x, 0.0f, dir.z);
velocity.Angular = new float3(0.0f, input.x * player.rotationSpeed * deltaTime, 0.0f);
});
}
}
Итак, наша система наследуется от класса ComponentSystem. Это не единственный возможный родительский класс для системы, на другие мы посмотрим чуть позже. Пока важно знать, что, наследуясь от этого класса, система будет работать только в основном потоке и не сможет использовать возможности Job System по распараллеливанию. Но поскольку я здесь обращаюсь к API Unity (Time.DeltaTime и класс Input), мне это и не пригодится.
Код системы располагается в перегруженном методе OnUpdate. Он будет вызываться каждый кадр, примерно как Update в привычных нам компонентах на основе MonoBehaviour.
С помощью метода Entities.ForEach, который будет нашей с вами основной рабочей лошадкой, я могу обработать все сущности, имеющие (в данном случае) компоненты PlayerComponent, LocalToWorld и PhysicsVelocity. Если хотя бы одного из этих компонентов у сущности нет, она не будет обработана этой системой.
Как можно видеть, список компонентов получается напрямую из перечня аргументов лямбды, что очень удобно. Аргументы обязательно должны иметь спецификатор ref. Это связано с тем, что компоненты представлены структурами и без использования этого спецификатора будут получены их копии, изменение которых не приведет к изменению оригинала.
Давайте запустим игру:
Система работает! Персонажем можно управлять! Но погодите, что же это? Почему капсула заваливается?
В стандартной физике Unity мы могли бы использовать параметр Freeze Rotation у Rigidbody, чтобы предотвратить падение персонажа. В физике DOTS такого параметра пока что, к сожалению, нет. Поэтому я создаем еще один компонент и систему, чтобы решить эту проблему.
Компонент (Scripts/Components/FreezeVerticalRotationComponent.cs) мне здесь нужен исключительно как маркер, чтобы обозначить системе, какие именно сущности она должна обрабатывать. Никаких дополнительных данных я в нем хранить не буду:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct FreezeVerticalRotationComponent : IComponentData
{
}
А вот код системы (Scripts/Systems/FreezeVerticalRotationSystem.cs):
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Physics;
public class FreezeVerticalRotationSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
JobHandle job = Entities.ForEach((ref FreezeVerticalRotationComponent tag, ref PhysicsMass mass) => {
mass.InverseInertia.xz = new float2(0.0f);
}).Schedule(inputDeps);
return job;
}
}
Поскольку эта система не взаимодействует со стандартным API движка, я могу сделать ее многопоточной. Для этого вместо класса ComponentSystem используется родительский класс JobComponentSystem.
Теперь также поменялась и сигнатура метода OnUpdate. Теперь он принимает аргумент JobHandle и возвращает JobHandle. Эти хендлы позволят менеджеру правильно организовать параллельное выполнение этой системы с другими системами. Очень важно не забывать передавать эти хендлы, чтобы избежать возникновения условия гонки (race condition), когда две системы попытаются одновременно работать с одной и той же сущностью.
Здесь также используется Entities.ForEach, но обратите внимание, что дополнительно вызывается метод Schedule, который собственно и запускает выполнение кода нашей системы во вспомогательных потоках.
Добавим компонент FreezeVerticalRotationComponent к объекту Player в редакторе и запустим игру:
И теперь наш персонаж не заваливается.
Иллюзия жизни
Персонаж-капсула — это, несомненно, весело. Но веселее было бы, если бы наш главный герой был больше похож на человека, а главное — был анимирован.
К сожалению, система анимации на DOTS все еще находится на очень ранних этапах разработки и в данном проекте я ее использовать не буду. Воспользуюсь старым добрым Animator Controller.
Для начала, перетащу в качестве дочернего объекта в Player префаб TT_demo/prefabs/TT_demo_police из пакета ToonyTinyPeopleDemo и назначу ему заранее заготовленный контроллер. Я не буду подробно останавливаться на устройстве Animator Controller, приведу здесь лишь скриншот:
Также к объекту TT_demo_police я добавлю компонент ConvertToEntity, установив параметр Conversion Mode в Convert and Inject Game Object. Таким образом, при запуске игры для объекта 3d-модели также будет создана сущность, но, в отличие от игрока и капсулы, сохранится и GameObject. Он, правда,окажется в корне (так как родительский объект превратился в сущность без сохранения GameObject):
И тут есть один нюанс. Если сейчас запустить игру, мы увидим, что при движении нашего персонажа игровой объект с 3d-моделью будет оставаться на месте:
Чтобы это поправить, я добавлю новый компонент и систему.
Компонент (Scripts/Components/CopyTransformComponent.cs) также выполняет роль простого маркера:
using Unity.Entities;
[GenerateAuthoringComponent]
public struct CopyTransformComponent : IComponentData
{
}
А система (Scripts/Systems/CopyTransformSystem.cs) просто осуществляет копирование положения сущности в компонент Transform игрового объекта:
using UnityEngine;
using Unity.Transforms;
using Unity.Entities;
public class CopyTransformSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref CopyTransformComponent tag, ref LocalToWorld localToWorld) => {
var transform = EntityManager.GetComponentObject<Transform>(entity);
transform.position = localToWorld.Position;
transform.rotation = localToWorld.Rotation;
});
}
}
Важным нововведением здесь является добавление аргумента Entity entity в лямбду. Этот аргумент передается без спецификатора ref, поскольку он по сути является лишь числовым идентификатором сущности и его нельзя менять. Но зато его можно использовать совместно с классом EntityManager для манипуляции сущностями и, в данном случае, для получения ссылки на компонент Transform у связанного с сущностью игрового объекта (привязку осуществляет компонент ConvertToEntity при конвертации, поскольку был выбран режим Convert And Inject Game Object).
Теперь я добавлю компонент CopyTransformComponent к игровому объекту TT_demo_police. Игровой объект Capsule я оставлю (так как в нем находится коллайдер), но отключу у него MeshRenderer, чтобы капсула не рисовалась на экране.
Запустим игру:
И да, теперь наш персонаж двигается.
Осталось добавить 3d-модельке игрока анимацию. Как вы, наверное, уже догадались, потребуется создать еще один компонент и систему.
N.B. В начале работы над игрой, использующей паттерн ECS, требуется создавать довольно большое количество систем и компонентов, и может показаться, что это лишняя работа по сравнению с паттерном EC. На самом деле, это не так. Преимущества ECS полностью проявляются, когда накоплена некоторая “критическая масса” компонентов и систем. В некоторой мере мы это увидим и в сегодняшней статье, когда будем реализовывать врагов.
Компонент (Scripts/Components/AnimatedCharacterComponent.cs):
using Unity.Entities;
using UnityEngine;
[GenerateAuthoringComponent]
public struct AnimatedCharacterComponent : IComponentData
{
public Entity animatorEntity;
}
Система (Scripts/Systems/AnimatedCharacterSystem.cs):
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
public class AnimatedCharacterSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref AnimatedCharacterComponent character, ref PhysicsVelocity velocity) => {
var animator = EntityManager.GetComponentObject<Animator>(character.animatorEntity);
animator.SetFloat("speed", math.length(velocity.Linear));
});
}
}
Ничего особенно нового здесь нет. Стоит обратить внимание, что компонент AnimatedCharacterComponent следует добавлять на родительскую сущность (Player) — ему требуется получать значения скорости из компонента PhysicsVelocity физического движка. А вот аниматор прикреплен к дочерней сущности (TT_demo_police). Чтобы получить доступ из одной сущности к другой, я просто использую переменную типа Entity в компоненте AnimatedCharacterComponent. Благодаря механизму ConvertToEntity, в редакторе сцены я смогу проставить туда игровой объект, а при запуске игры он автоматически сконвертируется в ссылку на соответствующую сущность:
Запустив игру, убеждаемся, что теперь анимация работает:
А я все гляжу, глаз не отвожу
Я сейчас ненадолго отвлекусь и наведу немного красоты: добавлю контента в уровень и настрою камеру.
Для уровня добавим интересных объектов из набора SimpleNaturePack, чтобы не бегать по пустой плоскости:
Камеру же я настрою с использованием Cinemachine. Подробно останавливаться на этом я не буду, приведу лишь пример настройки.
Для начала, я создам отдельный пустой игровой объект внутри TT_demo_police и поставлю его примерно на уровень головы — он будет использоваться для “прицеливания” камеры. Назову его CameraTarget.
Теперь можно создать виртуальную камеру. Для этого выберу пункт меню Cinemachine⇨Create Virtual Camera (если этого меню у вас нет, проверьте, установили ли вы пакет Cinemachine).
В инспекторе для созданной виртуальной камеры проставлю TT_demo_police в поле Follow и CameraTarget в поле Look At. В разделе Body поставлю Follow Offset X=0, Y=9, Z=-15 и Yaw Damping в 0.25.
Стреляй, Глеб Егорыч!
Бегать по карте — это здорово. Но шутер не был бы шутером, если бы там нельзя было стрелять. А у нас все еще нельзя. Нужно срочно это исправлять!
Прежде всего, нужно возможность определять из кода положение дула пистолета, чтобы пули вылетали четко из него (положение дула может слегка меняться под действием анимации).
Сам пистолет прикреплен к анимированному скелету и его легко найти в иерархии объекта TT_demo_police:
Я создам внутри пистолета пустой объект (назову его GunHole) и задам ему положение (-0.1174, 0, 0.394) и поворот (0, 0, 90). Этот объект я буду использовать как референс для кода, создающего пули.
Но есть небольшая проблема: так как TT_demo_police (родительский объект) уже содержит в себе ConvertToEntity с режимом Convert And Inject Game Object, я не смогу сделать то же самое с моим вновь созданным объектом, Unity разрешает только одну такую конвертацию для иерархии объектов.
Поэтому, я создам новый объект Shooter внутри объекта Player и прикреплю к нему небольшой компонент на основе MonoBehaviour (Scripts/Shooter.cs):
using UnityEngine;
public class Shooter : MonoBehaviour
{
public Transform gunHole;
}
И пропишу в него ссылку на GunHole в инспекторе.
А поскольку Shooter находится вне объекта TT_demo_police, я могу также добавить в него компонент ConvertToEntity:
Кроме положения дула, мне также потребуется и префаб пули. В префабах также можно использовать ConvertToEntity — такие префабы превратятся в сущности с компонентом Prefab при загрузке игры и будут исключены из обработки, но будут загружены и проинициализированы. Также, по аналогии с обычными префабами, из таких префабов-сущностей можно создавать обычные сущности и работает это гораздо быстрее, чем метод Instantiate!
Поэтому я создам префаб из двух игровых объектов: родительский будет содержать в себе компоненты ConvertToEntity и PhysicsBody, а дочерний — меш и коллайдер капсулы:
Из важного: коллайдер у пули настроен как триггер, а в параметры PhysicsBody внесены небольшие изменения: Linear Damping выставлен в 0, чтобы пуля не теряла скорость со временем, и Gravity Factor также выставлен в 0.
В объект Shooter также добавлю компонент BulletPrefabComponent (Scripts/Components/BulletPrefabComponent.cs):
using Unity.Entities;
[GenerateAuthoringComponent]
public struct BulletPrefabComponent : IComponentData
{
public Entity prefab;
public float speed;
}
Этот компонент позволит нам получить доступ к префабу из ECS-кода, а также — задать скорость пули (сразу можно ее проставить в инспекторе, я использовал значение 30).
И еще один компонент я буду проставлять непосредственно на пули (Scripts/Components/Bullet.cs):
using Unity.Entities;
using Unity.Mathematics;
[GenerateAuthoringComponent]
public struct BulletComponent : IComponentData
{
public float3 speed;
public bool destroyed;
}
Здесь параметр speed будет определять вектор направления движения пули и ее скорость, а флаг destroyed будет использоваться для обозначения пуль, столкнувшихся с препятствием. Зачем нужен этот флаг и как он используется я объясню чуть дальше.
Теперь для выстрелов нам потребуется система (Scripts/Systems/PlayerShootingSystem.cs):
using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
public class PlayerShootingSystem : ComponentSystem
{
protected override void OnUpdate()
{
if (!Input.GetButtonDown("Fire1"))
return;
Entities.ForEach((Entity entity, ref BulletPrefabComponent bulletPrefab) => {
var shooter = EntityManager.GetComponentObject<Shooter>(entity);
if (shooter == null)
Debug.LogError("BulletPrefabComponent is missing Shooter component.");
else {
Entity bullet = EntityManager.Instantiate(bulletPrefab.prefab);
EntityManager.SetComponentData(bullet, new Translation{ Value = shooter.gunHole.position });
EntityManager.SetComponentData(bullet, new Rotation{ Value = shooter.gunHole.rotation });
EntityManager.AddComponentData(bullet, new BulletComponent{ speed = shooter.gunHole.forward * bulletPrefab.speed });
}
});
}
}
Если сейчас запустить игру, мы увидим, что при выстреле пули действительно появляются:
Но они не двигаются! Как исправить? Правильно — завести систему (Scripts/Systems/BulletSystem.cs):
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
public class BulletSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
JobHandle job = Entities.ForEach((ref BulletComponent bullet, ref PhysicsVelocity velocity) => {
velocity.Linear = bullet.speed;
}).Schedule(inputDeps);
return job;
}
}
Как видите, тут все очень просто: задаем пулям постоянную скорость и пусть себе летят.
Ожившие мертвецы
Замечательно, пули у нас есть. Но стрелять пока не в кого. Давайте создадим зомби!
Я создам в корне сцены пустой игровой объект Zombie и настрою его по аналогии с главным героем. Сразу положу внутрь префаб TT_demo/prefabs/TT_demo_zombie. Также создам капсулу (Position 0, 1, 0) и сразу отключу у нее MeshRenderer. В сам объект Zombie добавлю компоненты PhysicsBody, FreezeVerticalRotationComponent, AnimatedCharacterComponent и ConvertToEntity. Также компонент ConvertToEntity надо добавить и в объект TT_demo_zombie, указав режим Convert And Inject Game Object. Туда же нужно добавить и CopyTransformComponent. У PhysicsBody поставлю Mass=70. В общем, очень похоже на настройку объекта Player, только без Shooter.
Приятно, что большинство компонентов и систем уже созданы. Но потребуется создать еще несколько компонентов, уникальных для зомби.
Прежде всего, нужно добавить врагу жизни (Scripts/Components/HealthComponent.cs):
using Unity.Entities;
[GenerateAuthoringComponent]
public struct HealthComponent : IComponentData
{
public int value;
}
И создадим систему (Scripts/Components/AnimatedCharacterDeathSystem.cs), которая будет отыгрывать анимацию смерти персонажа, когда счетчик жизней достигнет 0:
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
public class AnimatedCharacterDeathSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref AnimatedCharacterComponent character, ref HealthComponent health) => {
var animator = EntityManager.GetComponentObject<Animator>(character.animatorEntity);
if (health.value <= 0) {
animator.SetTrigger("die");
EntityManager.RemoveComponent<HealthComponent>(entity);
}
});
}
}
Для проверки я добавлю зомби компонент HealthComponent с количеством жизней 0 и запущу игру. Зомби должен сразу умереть:
Превосходно, код работает! Поставлю зомби, например, 3 жизни и займусь реализацией проверки столкновения пули и врага.
Для этого мне потребуется самая сложная в этой статье система (Scripts/Systems/BulletDamageSystem.cs). Давайте сначала взглянем на нее, а потом будем разбираться:
using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Systems;
using Unity.Physics;
public class CollisionEventSystem : JobComponentSystem
{
struct CollisionEventSystemJob : ITriggerEventsJob
{
public ComponentDataFromEntity<BulletComponent> bulletRef;
public ComponentDataFromEntity<HealthComponent> healthRef;
public void Execute(TriggerEvent triggerEvent)
{
Entity hitEntity, bulletEntity;
if (bulletRef.HasComponent(triggerEvent.EntityA)) {
hitEntity = triggerEvent.EntityB;
bulletEntity = triggerEvent.EntityA;
} else if (bulletRef.HasComponent(triggerEvent.EntityB)) {
hitEntity = triggerEvent.EntityA;
bulletEntity = triggerEvent.EntityB;
} else
return;
var bullet = bulletRef[bulletEntity];
bullet.destroyed = true;
bulletRef[bulletEntity] = bullet;
if (healthRef.HasComponent(hitEntity)) {
var health = healthRef[hitEntity];
health.value--;
healthRef[hitEntity] = health;
}
}
}
BuildPhysicsWorld buildPhysicsWorldSystem;
StepPhysicsWorld stepPhysicsWorld;
EndSimulationEntityCommandBufferSystem endSimulationCommandBuffer;
protected override void OnCreate()
{
buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
endSimulationCommandBuffer = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new CollisionEventSystemJob();
job.bulletRef = GetComponentDataFromEntity<BulletComponent>(isReadOnly: false);
job.healthRef = GetComponentDataFromEntity<HealthComponent>(isReadOnly: false);
var jobResult = job.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorldSystem.PhysicsWorld, inputDeps);
var commandBuffer = endSimulationCommandBuffer.CreateCommandBuffer().AsParallelWriter();
var result = Entities.ForEach((Entity entity, int entityInQueryIndex, ref BulletComponent bullet) => {
if (bullet.destroyed)
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).Schedule(jobResult);
endSimulationCommandBuffer.AddJobHandleForProducer(result);
return result;
}
}
Первая (и весьма важная) часть — это структура CollisionEventSystemJob. Она представляет собой задачу для системы Job System. В эту задачу физический движок передает перечень столкновений между физическими объектами. Код задачи в методе Execute проверяет, есть ли в одном из столкнувшихся объектов компонент BulletComponent. Если, действительно, один из объектов — пуля, то ей проставляется флажок destroy, а у второго объекта вычитаются жизни (если, конечно, у него есть соответствующий компонент; столкновение с деревом приводит просто к уничтожению пули).
Стоит обратить внимание на два момента: во-первых, обращение к компонентам происходит через класс ComponentDataFromEntity. Это нужно, потому что задача выполняется параллельно и движок должен отслеживать обращения к компонентам и не допускать одновременной работы с одним и тем же компонентом из разных потоков. Во-вторых, по той же причине, нельзя уничтожать пули сразу при обнаружении столкновения. Вместо этого, у пули проставляется флажок, который проверяется уже в безопасном окружении, где компонент может быть удален.
В методе OnUpdate я сначала прошу физический движок сообщить информацию о столкновениях и передаю ее в CollisionEventSystemJob для обработки. Как только эта работа закончена, я проверяю были ли уничтожены какие-то пули и если были, то передаю их в в командный буфер для фактического уничтожения этих сущностей.
Беги, Лола, Беги!
Последняя важная составляющая логики врага, которая у нас все еще отсутствует — это движение. Давайте же реализуем охоту на игрока!
Для поиска пути я буду использовать технологию NavMesh. Опять же, я не буду останавливаться на ней подробно, скажу лишь, что запечь NavMesh можно в окне Navigation, которое можно открыть через меню Window⇨AI⇨Navigation.
Я создам внутри игрового объекта Zombie пустой объект NavMeshAgent и добавлю в него компонент NavMeshAgent. Также я добавлю компонент ConvertToEntity с режимом Convert And Inject Game Object.
В дополнение к этому, мне потребуется новый компонент NavMeshAgentComponent (Scripts/Components/NavMeshAgentComponent.cs):
using Unity.Entities;
[GenerateAuthoringComponent]
public struct NavMeshAgentComponent : IComponentData
{
public Entity moveEntity;
}
Единственный параметр здесь — это сущность, которую этот NavMeshAgent будет двигать. Добавив этот компонент в дочерний объект NavMeshAgent, в качестве moveEntity я укажу объект Zombie.
Еще один новый компонент (Scripts/Components/FollowTargetComponent.cs):
using Unity.Entities;
[GenerateAuthoringComponent]
public struct FollowTargetComponent : IComponentData
{
}
Этот компонент я буду использовать просто как маркер: только враги, имеющие этот компонент, будут преследовать игрока.
Последним штрихом будет написание соответствующей системы (Scripts/Systems/FollowPlayerSystem.cs):
using UnityEngine;
using UnityEngine.AI;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
public class FollowPlayerSystem : ComponentSystem
{
protected override void OnUpdate()
{
float3 targetPosition = float3.zero;
Entities.ForEach((Entity entity, ref LocalToWorld transform, ref FollowTargetComponent tag) => {
targetPosition = transform.Position;
});
Entities.ForEach((Entity entity, ref NavMeshAgentComponent agent) => {
var navMeshAgent = EntityManager.GetComponentObject<NavMeshAgent>(entity);
if (navMeshAgent != null) {
navMeshAgent.SetDestination(targetPosition);
EntityManager.SetComponentData(agent.moveEntity, new Translation{ Value = navMeshAgent.transform.position });
}
});
}
}
Запускаем, проверяем:
Конец
Сегодня мы с вами познакомились с платформой DOTS и паттерном ECS. Надеюсь, мне удалось заинтересовать вас этими технологиями и показать, как их использовать в реальном игровом проекте.
Скачать исходный код проекта можно с GitHub: https://github.com/zapolnov/dots-zombie-shooter
Желаю удачи в ваших экспериментах!
<!DOCTYPE html>
<html dir="ltr" lang="ru-RU">
<head>
<meta charset="UTF-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="profile" href="http://gmpg.org/xfn/11" />
<title>Зомби-шутер на DOTS в Unity OTUS</title>
<!-- All in One SEO 4.5.2.1 - aioseo.com -->
<meta name="description" content="Всем привет! Меня зовут Николай Запольнов, я преподаватель на курсах Unity Basic и Unity Pro в OTUS. И сегодня я хочу рассказать вам про, без преувеличения, будущее Unity - технологию DOTS. В этой статье мы узнаем, что же такое этот DOTS, какие проблемы он решает и как применить его на практике. Но начать, все же," />
<meta name="robots" content="max-image-preview:large" />
<link rel="canonical" href="https://otus.ru/journal/zombi-shuter-na-dots-v-unity/" />
<meta name="generator" content="All in One SEO (AIOSEO) 4.5.2.1" />
<script type="application/ld+json" class="aioseo-schema">
{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#article","name":"\u0417\u043e\u043c\u0431\u0438-\u0448\u0443\u0442\u0435\u0440 \u043d\u0430 DOTS \u0432 Unity OTUS","headline":"\u0417\u043e\u043c\u0431\u0438-\u0448\u0443\u0442\u0435\u0440 \u043d\u0430 DOTS \u0432 Unity","author":{"@id":"https:\/\/otus.ru\/journal\/author\/d-golovin\/#author"},"publisher":{"@id":"https:\/\/otus.ru\/journal\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/otus.ru\/journal\/wp-content\/uploads\/2020\/12\/oj-1080x720-15.png","width":1080,"height":720},"datePublished":"2020-12-15T10:30:11+00:00","dateModified":"2023-10-06T21:17:22+00:00","inLanguage":"ru-RU","mainEntityOfPage":{"@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#webpage"},"isPartOf":{"@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#webpage"},"articleSection":"\u041f\u043e\u043b\u0435\u0437\u043d\u043e\u0435, gamedev, unity, \u0443\u0440\u043e\u043a"},{"@type":"BreadcrumbList","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/otus.ru\/journal\/#listItem","position":1,"name":"\u0413\u043b\u0430\u0432\u043d\u0430\u044f \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430","item":"https:\/\/otus.ru\/journal\/","nextItem":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#listItem"},{"@type":"ListItem","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#listItem","position":2,"name":"\u0417\u043e\u043c\u0431\u0438-\u0448\u0443\u0442\u0435\u0440 \u043d\u0430 DOTS \u0432 Unity","previousItem":"https:\/\/otus.ru\/journal\/#listItem"}]},{"@type":"Organization","@id":"https:\/\/otus.ru\/journal\/#organization","name":"\u041e\u0442\u0443\u0441 \u043e\u043d\u043b\u0430\u0439\u043d-\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u043d\u0438\u0435","url":"https:\/\/otus.ru\/journal\/","sameAs":["https:\/\/www.youtube.com\/channel\/UCetgtvy93o3i3CvyGXKFU3g"],"contactPoint":{"@type":"ContactPoint","telephone":"+74999389202","contactType":"Customer Support"}},{"@type":"Person","@id":"https:\/\/otus.ru\/journal\/author\/d-golovin\/#author","url":"https:\/\/otus.ru\/journal\/author\/d-golovin\/","name":"D. Golovin","image":{"@type":"ImageObject","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/50a23d5429cb764281144301f26baf7e?s=96&d=mm&r=g","width":96,"height":96,"caption":"D. Golovin"}},{"@type":"WebPage","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#webpage","url":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/","name":"\u0417\u043e\u043c\u0431\u0438-\u0448\u0443\u0442\u0435\u0440 \u043d\u0430 DOTS \u0432 Unity OTUS","description":"\u0412\u0441\u0435\u043c \u043f\u0440\u0438\u0432\u0435\u0442! \u041c\u0435\u043d\u044f \u0437\u043e\u0432\u0443\u0442 \u041d\u0438\u043a\u043e\u043b\u0430\u0439 \u0417\u0430\u043f\u043e\u043b\u044c\u043d\u043e\u0432, \u044f \u043f\u0440\u0435\u043f\u043e\u0434\u0430\u0432\u0430\u0442\u0435\u043b\u044c \u043d\u0430 \u043a\u0443\u0440\u0441\u0430\u0445 Unity Basic \u0438 Unity Pro \u0432 OTUS. \u0418 \u0441\u0435\u0433\u043e\u0434\u043d\u044f \u044f \u0445\u043e\u0447\u0443 \u0440\u0430\u0441\u0441\u043a\u0430\u0437\u0430\u0442\u044c \u0432\u0430\u043c \u043f\u0440\u043e, \u0431\u0435\u0437 \u043f\u0440\u0435\u0443\u0432\u0435\u043b\u0438\u0447\u0435\u043d\u0438\u044f, \u0431\u0443\u0434\u0443\u0449\u0435\u0435 Unity - \u0442\u0435\u0445\u043d\u043e\u043b\u043e\u0433\u0438\u044e DOTS. \u0412 \u044d\u0442\u043e\u0439 \u0441\u0442\u0430\u0442\u044c\u0435 \u043c\u044b \u0443\u0437\u043d\u0430\u0435\u043c, \u0447\u0442\u043e \u0436\u0435 \u0442\u0430\u043a\u043e\u0435 \u044d\u0442\u043e\u0442 DOTS, \u043a\u0430\u043a\u0438\u0435 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u044b \u043e\u043d \u0440\u0435\u0448\u0430\u0435\u0442 \u0438 \u043a\u0430\u043a \u043f\u0440\u0438\u043c\u0435\u043d\u0438\u0442\u044c \u0435\u0433\u043e \u043d\u0430 \u043f\u0440\u0430\u043a\u0442\u0438\u043a\u0435. \u041d\u043e \u043d\u0430\u0447\u0430\u0442\u044c, \u0432\u0441\u0435 \u0436\u0435,","inLanguage":"ru-RU","isPartOf":{"@id":"https:\/\/otus.ru\/journal\/#website"},"breadcrumb":{"@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#breadcrumblist"},"author":{"@id":"https:\/\/otus.ru\/journal\/author\/d-golovin\/#author"},"creator":{"@id":"https:\/\/otus.ru\/journal\/author\/d-golovin\/#author"},"image":{"@type":"ImageObject","url":"https:\/\/otus.ru\/journal\/wp-content\/uploads\/2020\/12\/oj-1080x720-15.png","@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#mainImage","width":1080,"height":720},"primaryImageOfPage":{"@id":"https:\/\/otus.ru\/journal\/zombi-shuter-na-dots-v-unity\/#mainImage"},"datePublished":"2020-12-15T10:30:11+00:00","dateModified":"2023-10-06T21:17:22+00:00"},{"@type":"WebSite","@id":"https:\/\/otus.ru\/journal\/#website","url":"https:\/\/otus.ru\/journal\/","name":"OTUS JOURNAL","description":"Blog about IT","inLanguage":"ru-RU","publisher":{"@id":"https:\/\/otus.ru\/journal\/#organization"}}]}
</script>
<!-- All in One SEO -->
<link rel='dns-prefetch' href='//otus.ru' />
<link rel='dns-prefetch' href='//fonts.googleapis.com' />
<link rel='stylesheet' id='wp-block-library-css' href='https://otus.ru/journal/wp-includes/css/dist/block-library/style.min.css?ver=6.4.7' type='text/css' media='all' />
<style id='classic-theme-styles-inline-css' type='text/css'>
/*! This file is auto-generated */
.wp-block-button__link{color:#fff;background-color:#32373c;border-radius:9999px;box-shadow:none;text-decoration:none;padding:calc(.667em + 2px) calc(1.333em + 2px);font-size:1.125em}.wp-block-file__button{background:#32373c;color:#fff;text-decoration:none}
</style>
<style id='global-styles-inline-css' type='text/css'>
body{--wp--preset--color--black: #000000;--wp--preset--color--cyan-bluish-gray: #abb8c3;--wp--preset--color--white: #ffffff;--wp--preset--color--pale-pink: #f78da7;--wp--preset--color--vivid-red: #cf2e2e;--wp--preset--color--luminous-vivid-orange: #ff6900;--wp--preset--color--luminous-vivid-amber: #fcb900;--wp--preset--color--light-green-cyan: #7bdcb5;--wp--preset--color--vivid-green-cyan: #00d084;--wp--preset--color--pale-cyan-blue: #8ed1fc;--wp--preset--color--vivid-cyan-blue: #0693e3;--wp--preset--color--vivid-purple: #9b51e0;--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple: linear-gradient(135deg,rgba(6,147,227,1) 0%,rgb(155,81,224) 100%);--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan: linear-gradient(135deg,rgb(122,220,180) 0%,rgb(0,208,130) 100%);--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange: linear-gradient(135deg,rgba(252,185,0,1) 0%,rgba(255,105,0,1) 100%);--wp--preset--gradient--luminous-vivid-orange-to-vivid-red: linear-gradient(135deg,rgba(255,105,0,1) 0%,rgb(207,46,46) 100%);--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray: linear-gradient(135deg,rgb(238,238,238) 0%,rgb(169,184,195) 100%);--wp--preset--gradient--cool-to-warm-spectrum: linear-gradient(135deg,rgb(74,234,220) 0%,rgb(151,120,209) 20%,rgb(207,42,186) 40%,rgb(238,44,130) 60%,rgb(251,105,98) 80%,rgb(254,248,76) 100%);--wp--preset--gradient--blush-light-purple: linear-gradient(135deg,rgb(255,206,236) 0%,rgb(152,150,240) 100%);--wp--preset--gradient--blush-bordeaux: linear-gradient(135deg,rgb(254,205,165) 0%,rgb(254,45,45) 50%,rgb(107,0,62) 100%);--wp--preset--gradient--luminous-dusk: linear-gradient(135deg,rgb(255,203,112) 0%,rgb(199,81,192) 50%,rgb(65,88,208) 100%);--wp--preset--gradient--pale-ocean: linear-gradient(135deg,rgb(255,245,203) 0%,rgb(182,227,212) 50%,rgb(51,167,181) 100%);--wp--preset--gradient--electric-grass: linear-gradient(135deg,rgb(202,248,128) 0%,rgb(113,206,126) 100%);--wp--preset--gradient--midnight: linear-gradient(135deg,rgb(2,3,129) 0%,rgb(40,116,252) 100%);--wp--preset--font-size--small: 13px;--wp--preset--font-size--medium: 20px;--wp--preset--font-size--large: 36px;--wp--preset--font-size--x-large: 42px;--wp--preset--spacing--20: 0.44rem;--wp--preset--spacing--30: 0.67rem;--wp--preset--spacing--40: 1rem;--wp--preset--spacing--50: 1.5rem;--wp--preset--spacing--60: 2.25rem;--wp--preset--spacing--70: 3.38rem;--wp--preset--spacing--80: 5.06rem;--wp--preset--shadow--natural: 6px 6px 9px rgba(0, 0, 0, 0.2);--wp--preset--shadow--deep: 12px 12px 50px rgba(0, 0, 0, 0.4);--wp--preset--shadow--sharp: 6px 6px 0px rgba(0, 0, 0, 0.2);--wp--preset--shadow--outlined: 6px 6px 0px -3px rgba(255, 255, 255, 1), 6px 6px rgba(0, 0, 0, 1);--wp--preset--shadow--crisp: 6px 6px 0px rgba(0, 0, 0, 1);}:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}body .is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}body .is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}body .is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}body .is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}body .is-layout-flex{flex-wrap: wrap;align-items: center;}body .is-layout-flex > *{margin: 0;}body .is-layout-grid{display: grid;}body .is-layout-grid > *{margin: 0;}:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}.has-black-color{color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-color{color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-color{color: var(--wp--preset--color--white) !important;}.has-pale-pink-color{color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-color{color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-color{color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-color{color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-color{color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-color{color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-color{color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-color{color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-color{color: var(--wp--preset--color--vivid-purple) !important;}.has-black-background-color{background-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-background-color{background-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-background-color{background-color: var(--wp--preset--color--white) !important;}.has-pale-pink-background-color{background-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-background-color{background-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-background-color{background-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-background-color{background-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-background-color{background-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-background-color{background-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-background-color{background-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-background-color{background-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-background-color{background-color: var(--wp--preset--color--vivid-purple) !important;}.has-black-border-color{border-color: var(--wp--preset--color--black) !important;}.has-cyan-bluish-gray-border-color{border-color: var(--wp--preset--color--cyan-bluish-gray) !important;}.has-white-border-color{border-color: var(--wp--preset--color--white) !important;}.has-pale-pink-border-color{border-color: var(--wp--preset--color--pale-pink) !important;}.has-vivid-red-border-color{border-color: var(--wp--preset--color--vivid-red) !important;}.has-luminous-vivid-orange-border-color{border-color: var(--wp--preset--color--luminous-vivid-orange) !important;}.has-luminous-vivid-amber-border-color{border-color: var(--wp--preset--color--luminous-vivid-amber) !important;}.has-light-green-cyan-border-color{border-color: var(--wp--preset--color--light-green-cyan) !important;}.has-vivid-green-cyan-border-color{border-color: var(--wp--preset--color--vivid-green-cyan) !important;}.has-pale-cyan-blue-border-color{border-color: var(--wp--preset--color--pale-cyan-blue) !important;}.has-vivid-cyan-blue-border-color{border-color: var(--wp--preset--color--vivid-cyan-blue) !important;}.has-vivid-purple-border-color{border-color: var(--wp--preset--color--vivid-purple) !important;}.has-vivid-cyan-blue-to-vivid-purple-gradient-background{background: var(--wp--preset--gradient--vivid-cyan-blue-to-vivid-purple) !important;}.has-light-green-cyan-to-vivid-green-cyan-gradient-background{background: var(--wp--preset--gradient--light-green-cyan-to-vivid-green-cyan) !important;}.has-luminous-vivid-amber-to-luminous-vivid-orange-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-amber-to-luminous-vivid-orange) !important;}.has-luminous-vivid-orange-to-vivid-red-gradient-background{background: var(--wp--preset--gradient--luminous-vivid-orange-to-vivid-red) !important;}.has-very-light-gray-to-cyan-bluish-gray-gradient-background{background: var(--wp--preset--gradient--very-light-gray-to-cyan-bluish-gray) !important;}.has-cool-to-warm-spectrum-gradient-background{background: var(--wp--preset--gradient--cool-to-warm-spectrum) !important;}.has-blush-light-purple-gradient-background{background: var(--wp--preset--gradient--blush-light-purple) !important;}.has-blush-bordeaux-gradient-background{background: var(--wp--preset--gradient--blush-bordeaux) !important;}.has-luminous-dusk-gradient-background{background: var(--wp--preset--gradient--luminous-dusk) !important;}.has-pale-ocean-gradient-background{background: var(--wp--preset--gradient--pale-ocean) !important;}.has-electric-grass-gradient-background{background: var(--wp--preset--gradient--electric-grass) !important;}.has-midnight-gradient-background{background: var(--wp--preset--gradient--midnight) !important;}.has-small-font-size{font-size: var(--wp--preset--font-size--small) !important;}.has-medium-font-size{font-size: var(--wp--preset--font-size--medium) !important;}.has-large-font-size{font-size: var(--wp--preset--font-size--large) !important;}.has-x-large-font-size{font-size: var(--wp--preset--font-size--x-large) !important;}
.wp-block-navigation a:where(:not(.wp-element-button)){color: inherit;}
:where(.wp-block-post-template.is-layout-flex){gap: 1.25em;}:where(.wp-block-post-template.is-layout-grid){gap: 1.25em;}
:where(.wp-block-columns.is-layout-flex){gap: 2em;}:where(.wp-block-columns.is-layout-grid){gap: 2em;}
.wp-block-pullquote{font-size: 1.5em;line-height: 1.6;}
</style>
<link rel='stylesheet' id='wbcr-comments-plus-url-span-css' href='https://otus.ru/journal/wp-content/plugins/clearfy/components/comments-plus/assets/css/url-span.css?ver=2.2.0' type='text/css' media='all' />
<link rel='stylesheet' id='wpel-style-css' href='https://otus.ru/journal/wp-content/plugins/wp-external-links/public/css/wpel.css?ver=2.59' type='text/css' media='all' />
<link rel='stylesheet' id='ez-toc-css' href='https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/css/screen.min.css?ver=2.0.61' type='text/css' media='all' />
<style id='ez-toc-inline-css' type='text/css'>
div#ez-toc-container .ez-toc-title {font-size: 120%;}div#ez-toc-container .ez-toc-title {font-weight: 500;}div#ez-toc-container ul li {font-size: 95%;}div#ez-toc-container nav ul ul li {font-size: 90%;}
.ez-toc-container-direction {direction: ltr;}.ez-toc-counter ul{counter-reset: item ;}.ez-toc-counter nav ul li a::before {content: counters(item, ".", decimal) ". ";display: inline-block;counter-increment: item;flex-grow: 0;flex-shrink: 0;margin-right: .2em; float: left; }.ez-toc-widget-direction {direction: ltr;}.ez-toc-widget-container ul{counter-reset: item ;}.ez-toc-widget-container nav ul li a::before {content: counters(item, ".", decimal) ". ";display: inline-block;counter-increment: item;flex-grow: 0;flex-shrink: 0;margin-right: .2em; float: left; }
</style>
<link rel='stylesheet' id='contentberg-fonts-css' href='https://fonts.googleapis.com/css?family=Roboto%3A400%2C500%2C700%7CPT+Serif%3A400%2C400i%2C600%7CIBM+Plex+Serif%3A500' type='text/css' media='all' />
<link rel='stylesheet' id='contentberg-core-css' href='https://otus.ru/journal/wp-content/themes/contentberg/style.css?ver=1.8.3' type='text/css' media='all' />
<link rel='stylesheet' id='contentberg-lightbox-css' href='https://otus.ru/journal/wp-content/themes/contentberg/css/lightbox.css?ver=1.8.3' type='text/css' media='all' />
<link rel='stylesheet' id='font-awesome-css' href='https://otus.ru/journal/wp-content/themes/contentberg/css/fontawesome/css/font-awesome.min.css?ver=1.8.3' type='text/css' media='all' />
<script type="text/javascript" id="breeze-prefetch-js-extra">
/* <![CDATA[ */
var breeze_prefetch = {"local_url":"https:\/\/otus.ru\/journal","ignore_remote_prefetch":"1","ignore_list":["\/wp-admin\/"]};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/breeze/assets/js/js-front-end/breeze-prefetch-links.min.js" id="breeze-prefetch-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery.min.js" id="jquery-core-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery-migrate.min.js" id="jquery-migrate-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/lazysizes.js" id="lazysizes-js"></script>
<link rel="https://api.w.org/" href="https://otus.ru/journal/wp-json/" /><link rel="alternate" type="application/json" href="https://otus.ru/journal/wp-json/wp/v2/posts/227" /><link rel='shortlink' href='https://otus.ru/journal/?p=227' />
<link rel="alternate" type="application/json+oembed" href="https://otus.ru/journal/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F" />
<link rel="alternate" type="text/xml+oembed" href="https://otus.ru/journal/wp-json/oembed/1.0/embed?url=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F&format=xml" />
<script>var Sphere_Plugin = {"ajaxurl":"https:\/\/otus.ru\/journal\/wp-admin\/admin-ajax.php"};</script><link rel="icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-32x32.png" sizes="32x32" />
<link rel="icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-192x192.png" sizes="192x192" />
<link rel="apple-touch-icon" href="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-180x180.png" />
<meta name="msapplication-TileImage" content="https://otus.ru/journal/wp-content/uploads/2020/11/cropped-OTUS_logo_OTUS-COMP-LOGO-WHITE-1-270x270.png" />
<style type="text/css" id="wp-custom-css">
#menu-item-10406 .wpel-icon {
display: none;
}
#menu-item-10407 .wpel-icon {
display: none;
}
.otus-login-site a .wpel-icon {
display: none;
}
.menu-menju-navykov-container a .wpel-icon {
display: none;
}
.otus-login-site a
{
background: #ffd709;
border-radius: 12px;
color: #0f0f10;
font-size: 14px;
font-weight: 700;
line-height: 20px;
display: block;
text-align: center;
padding: 8px 25px;
}
.main-footer.dark {
background: linear-gradient(90deg, #a64fc5, #4f54e6);
border-color: transparent;
}
.main-footer.bold .copyright {
color: #fff;
}
.main-footer.bold .to-top i {
color: #fff;
}
.main-footer.bold .back-to-top {
color: #fff;
}
.nav__scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.scrollable-menu .menu {
display: flex;
}
.nav__scroll
{
background: linear-gradient(90deg, #a64fc5, #4f54e6);
}
.scrollable-menu .menu .menu-item {
flex: 0 0 auto;
padding: 15px 15px;
}
.scrollable-menu .menu .menu-item a {
color: #fff;
}
.nav__scroll::-webkit-scrollbar{background-color:#fff;height:5px;}
.nav__scroll::-webkit-scrollbar-thumb{background-color:#dcdcdc;}
.nav__scroll::-webkit-scrollbar-track{-webkit-border-radius:0;border-radius:0;background-color:#fff;}/
body {
min-width: 320px;
}
.banner-click img {
margin: 0 auto;
display: block;
}
.banner-click {
cursor: pointer;
}
.banner-footer-area {
margin-bottom: 20px;
}
.banner-left-area {
margin-top: 40px;
} </style>
<!--Start VDZ Yandex Metrika Plugin-->
<!-- Yandex.Metrika counter --><script type="text/javascript" >(function(m,e,t,r,i,k,a){m[i]=m[i]||function(){(m[i].a=m[i].a||[]).push(arguments)};m[i].l=1*new Date();k=e.createElement(t),a=e.getElementsByTagName(t)[0],k.async=1,k.src=r,a.parentNode.insertBefore(k,a)})(window, document, "script", "https://mc.yandex.ru/metrika/tag.js", "ym");ym(34531570, "init", {clickmap:true, trackLinks:true, accurateTrackBounce:true, webvisor:true, trackHash:true, ecommerce:"dataLayer"});</script>
<noscript><div><img src="https://mc.yandex.ru/watch/34531570" style="position:absolute; left:-9999px;" alt="" /></div></noscript>
<!-- /Yandex.Metrika counter --><!--START ADD EVENTS FROM CF7--><script type='text/javascript'>document.addEventListener( 'wpcf7submit', function( event ) {
//event.detail.contactFormId;
if(ym){
//console.log(event.detail);
ym(34531570, 'reachGoal', 'VDZ_SEND_CONTACT_FORM_7');
ym(34531570, 'params', {
page_url: window.location.href,
status: event.detail.status,
locale: event.detail.contactFormLocale,
form_id: event.detail.contactFormId,
});
}
}, false );
</script><!--END ADD EVENTS FROM CF7-->
<!--End VDZ Yandex Metrika Plugin-->
</head>
<body class="post-template-default single single-post postid-227 single-format-standard right-sidebar lazy-normal has-lb">
<div class="main-wrap">
<header id="main-head" class="main-head head-nav-below has-search-modal simple simple-boxed">
<div class="inner inner-head" data-sticky-bar="0">
<div class="wrap cf wrap-head">
<div class="left-contain">
<span class="mobile-nav"><i class="fa fa-bars"></i></span>
<div class="title">
<a href="https://otus.ru/journal/" title="OTUS JOURNAL" rel="home" data-wpel-link="internal">
<span class="text-logo"><img src="/journal/wp-content/themes/contentberg/img/logo_site.svg" alt="OTUS JOURNAL"></span>
</a>
</div>
</div>
<div class="navigation-wrap inline">
<nav class="navigation inline simple light" data-sticky-bar="0">
<div class="menu-rubriki-container"><ul id="menu-rubriki" class="menu"><li id="menu-item-109" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-cat-1 menu-item-109"><a href="https://otus.ru/journal/category/pro-it/" data-wpel-link="internal"><span>Про IT</span></a></li>
<li id="menu-item-113" class="menu-item menu-item-type-taxonomy menu-item-object-category current-post-ancestor current-menu-parent current-post-parent menu-cat-4 menu-item-113"><a href="https://otus.ru/journal/category/polza/" data-wpel-link="internal"><span>Полезное</span></a></li>
<li id="menu-item-114" class="menu-item menu-item-type-taxonomy menu-item-object-category menu-cat-3 menu-item-114"><a href="https://otus.ru/journal/category/lifestyle/" data-wpel-link="internal"><span>Лайфстайл</span></a></li>
<li id="menu-item-10406" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10406"><a href="https://otus.ru/catalog/courses" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><span>Обучение</span><span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10407" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10407"><a href="https://otus.ru/about" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right"><span>Информация</span><span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </nav>
</div>
<div class="actions">
<div class="otus-login-site">
<a href="https://otus.ru/login/" target="_blank" data-wpel-link="external" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Войти<span class="wpel-icon wpel-image wpel-icon-6"></span></a>
</div>
<a href="#" title="Search" class="search-link"><i class="fa fa-search"></i></a>
</div>
</div>
</div>
</header> <!-- .main-head -->
<div class="nav nav_disable nav_colored nav_transparent course-categories__nav nav__scroll ">
<div class="container wrap">
<div class="links inline simple light scrollable-menu">
<div class="menu-menju-navykov-container"><ul id="menu-menju-navykov" class="menu"><li id="menu-item-10413" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10413"><a href="https://otus.ru/categories/programming/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Программирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10414" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10414"><a href="https://otus.ru/categories/architecture/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Архитектура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10415" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10415"><a href="https://otus.ru/categories/operations/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Инфраструктура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10416" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10416"><a href="https://otus.ru/categories/information-security-courses/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Безопасность<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10417" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10417"><a href="https://otus.ru/categories/data-science/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Data Science<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10418" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10418"><a href="https://otus.ru/categories/gamedev/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GameDev<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10419" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10419"><a href="https://otus.ru/categories/marketing-business/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Управление<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10420" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10420"><a href="https://otus.ru/categories/analytics/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Аналитика и анализ<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li id="menu-item-10421" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10421"><a href="https://otus.ru/categories/testing/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Тестирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </div>
</div>
</div>
<div class="main wrap">
<div class="ts-row cf">
<div class="col-8 main-content cf">
<article id="post-227" class="the-post post-227 post type-post status-publish format-standard has-post-thumbnail category-polza tag-gamedev tag-unity tag-urok">
<header class="post-header the-post-header cf">
<div class="post-meta the-post-meta">
<span class="post-cat">
<a href="https://otus.ru/journal/category/polza/" class="category" data-wpel-link="internal">Полезное</a>
</span>
<h1 class="post-title">
Зомби-шутер на DOTS в Unity
</h1>
<a href="https://otus.ru/journal/zombi-shuter-na-dots-v-unity/" class="date-link" data-wpel-link="internal"><time class="post-date">15 декабря, 2020</time></a>
</div>
<div class="featured">
<a href="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15.png" class="image-link" data-wpel-link="internal"><img width="770" height="515" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20770%20515%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-contentberg-main size-contentberg-main lazyload wp-post-image" alt="Зомби-шутер на DOTS в Unity" title="Зомби-шутер на DOTS в Unity" decoding="async" fetchpriority="high" data-srcset="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-270x180.png 270w" data-src="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-15-770x515.png" data-sizes="(max-width: 770px) 100vw, 770px" /> </a>
</div>
</header><!-- .post-header -->
<div class="post-content description cf entry-content content-normal">
<div id="ez-toc-container" class="ez-toc-v2_0_61 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction">
<div class="ez-toc-title-container">
<p class="ez-toc-title " >Содержание</p>
<span class="ez-toc-title-toggle"><a href="#" class="ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle" aria-label="Toggle Table of Content"><span class="ez-toc-js-icon-con"><span class=""><span class="eztoc-hide" style="display:none;">Toggle</span><span class="ez-toc-icon-toggle-span"><svg style="fill: #999;color:#999" xmlns="http://www.w3.org/2000/svg" class="list-377408" width="20px" height="20px" viewBox="0 0 24 24" fill="none"><path d="M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z" fill="currentColor"></path></svg><svg style="fill: #999;color:#999" class="arrow-unsorted-368013" xmlns="http://www.w3.org/2000/svg" width="10px" height="10px" viewBox="0 0 24 24" version="1.2" baseProfile="tiny"><path d="M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z"/></svg></span></span></span></a></span></div>
<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-1" href="#DOTS" title="DOTS">DOTS</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-2" href="#Entity_Component_System" title="Entity Component System">Entity Component System</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-3" href="#%D0%9F%D0%BE%D0%B4%D0%B3%D0%BE%D1%82%D0%B0%D0%B2%D0%BB%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82" title="Подготавливаем проект">Подготавливаем проект</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-4" href="#%D0%9F%D0%BE%D0%B7%D0%BD%D0%B0%D0%B5%D0%BC_%D1%81%D1%83%D1%89%D0%BD%D0%BE%D1%81%D1%82%D0%B8" title="Познаем сущности">Познаем сущности</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-5" href="#%D0%A7%D1%82%D0%BE_%D0%B7%D0%B0_%D0%B8%D0%B3%D1%80%D0%B0_%D0%B1%D0%B5%D0%B7_%D0%B8%D0%B3%D1%80%D0%BE%D0%BA%D0%B0" title="Что за игра без игрока?">Что за игра без игрока?</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-6" href="#%D0%98%D0%BB%D0%BB%D1%8E%D0%B7%D0%B8%D1%8F_%D0%B6%D0%B8%D0%B7%D0%BD%D0%B8" title="Иллюзия жизни">Иллюзия жизни</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-7" href="#%D0%90_%D1%8F_%D0%B2%D1%81%D0%B5_%D0%B3%D0%BB%D1%8F%D0%B6%D1%83_%D0%B3%D0%BB%D0%B0%D0%B7_%D0%BD%D0%B5_%D0%BE%D1%82%D0%B2%D0%BE%D0%B6%D1%83" title="А я все гляжу, глаз не отвожу">А я все гляжу, глаз не отвожу</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-8" href="#%D0%A1%D1%82%D1%80%D0%B5%D0%BB%D1%8F%D0%B9_%D0%93%D0%BB%D0%B5%D0%B1_%D0%95%D0%B3%D0%BE%D1%80%D1%8B%D1%87" title="Стреляй, Глеб Егорыч!">Стреляй, Глеб Егорыч!</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-9" href="#%D0%9E%D0%B6%D0%B8%D0%B2%D1%88%D0%B8%D0%B5_%D0%BC%D0%B5%D1%80%D1%82%D0%B2%D0%B5%D1%86%D1%8B" title="Ожившие мертвецы">Ожившие мертвецы</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-10" href="#%D0%91%D0%B5%D0%B3%D0%B8_%D0%9B%D0%BE%D0%BB%D0%B0_%D0%91%D0%B5%D0%B3%D0%B8" title="Беги, Лола, Беги!">Беги, Лола, Беги!</a></li><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class="ez-toc-link ez-toc-heading-11" href="#%D0%9A%D0%BE%D0%BD%D0%B5%D1%86" title="Конец">Конец</a></li></ul></nav></div>
<p></p>
<figure class="wp-block-image size-full"><a href="https://otus.ru/lessons/unity-professional/?utm_source=oj&utm_medium=affilate&utm_campaign=unitypro" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><img decoding="async" width="970" height="90" src="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg" alt="Зомби-шутер на DOTS в Unity" class="wp-image-7658" srcset="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg 970w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-300x28.jpg 300w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-150x14.jpg 150w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-768x71.jpg 768w" sizes="(max-width: 970px) 100vw, 970px" /></a></figure>
<p></p>
<p>Всем привет! Меня зовут Николай Запольнов, я преподаватель на курсах <a href="https://otus.pw/Eg52/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Unity Basic<span class="wpel-icon wpel-image wpel-icon-6"></span></a> и <a href="https://otus.pw/0Q20/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Unity Pro<span class="wpel-icon wpel-image wpel-icon-6"></span></a> в <strong>OTUS</strong>. И сегодня я хочу рассказать вам про, без преувеличения, будущее Unity — технологию DOTS.</p>
<p>В этой статье мы узнаем, что же такое этот DOTS, какие проблемы он решает и как применить его на практике.</p>
<p>Но начать, все же, стоит с теории.</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="DOTS"></span>DOTS<span class="ez-toc-section-end"></span></h1>
<p>DOTS (“Data Oriented Tech Stack” — технический стек, ориентированный на данные) — это новый подход к разработке игр на движке Unity. Впервые он был анонсирован в 2017 году, а некоторые его компоненты еще раньше — в 2016. В рамках этого технического стека Unity руководствуется слоганом “performance by default” (“производительный по умолчанию”): они создают оптимизированный и эффективный фреймворк в противоположность “классическому” Unity, где программист должен внимательно выбирать, какое API он использует, создавать пулы объектов и т.д.</p>
<p>Для обеспечения максимальной производительности, DOTS включает в себя множество компонентов. Так, например, Job System (Система задач) предоставляет удобный и простой API для многопоточного программирования. Но самым важным нововведением DOTS является использование паттерна Entity-Component-System (ECS).</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="Entity_Component_System"></span>Entity Component System<span class="ez-toc-section-end"></span></h1>
<p>Одним из основных столпов классического объектно-ориентированного программирования является наследование. Дочерние классы, собственно, наследуют свойства и методы родительского класса, расширяя и дополняя их новыми возможностями. Такой подход позволяет переиспользовать код, но в больших проектах он также создает и проблему: построить подходящую иерархию объектов не всегда возможно. В результате, в проекте появляются так называемые божественные классы (god classes), реализующие огромный набор методов, часть которых используется одними дочерними классами, а часть — другими.</p>
<p>В качестве альтернативы наследованию была предложена композиция. В геймдеве такой подход часто называют Entity-Component (EC) и именно он применяется в “классическом” Unity. В паттерне EC сущности (entities, в Unity они реализованы классом GameObject) являются лишь контейнерами для компонентов. А компоненты, в свою очередь, реализуют конкретную функциональность и содержат в себе как код, так и данные. Это позволяет собирать каждую конкретную сущность из фрагментов функциональности, как из кубиков Лего.</p>
<p>Паттерн Entity-Component-System развивает эту парадигму еще дальше. Он предлагает оставить в компонентах только данные, а код вынести в системы. Это позволяет оперировать группой компонентов и выводит переиспользование кода на новый уровень. А еще, такой подход позволяет хранить данные компонентов в памяти последовательно. Это сильно увеличивает эффективность работы кеша на современных процессорах и, соответственно, повышает производительность.</p>
<p>Но довольно теории, давайте перейдем к практике!</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%9F%D0%BE%D0%B4%D0%B3%D0%BE%D1%82%D0%B0%D0%B2%D0%BB%D0%B8%D0%B2%D0%B0%D0%B5%D0%BC_%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82"></span>Подготавливаем проект<span class="ez-toc-section-end"></span></h1>
<p>Сразу хочется предупредить, что хотя DOTS и находится в разработке уже много лет, он все еще находится в состоянии preview и довольно сырой. Многие компоненты еще не завершены и поддерживают только часть функциональности, а иногда могут содержать и серьезные баги. По этой причине, в сегодняшней статье я буду использовать смешанный подход: часть кода будет написана на DOTS, а часть будет реализована на старых добрых GameObject’ах.</p>
<p>Эта статья написана и проверена на Unity 2020.1.16f1. Вы можете использовать и другую версию, но тогда существует вероятность, что что-то не заработает. DOTS очень активно развивается и программные интерфейсы иногда меняются.</p>
<p>Начнем с пустого проекта (я использовал шаблон “3D” в Unity Hub). Итак, прежде всего нам потребуется добавить DOTS в наш проект. Делается это через менеджер пакетов, но просто так их не найти. Некоторое время назад авторы Unity убрали все preview-пакеты из общего списка, и теперь, чтобы их установить, необходимо нажать в верхнем левом углу пакетного менеджера кнопку “+” и в появившемся меню выбрать “Add package from git URL…”:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/AOsg49HW2lsdFWZEZb-tUCFTFZ6iVw3j3U2l7dH_15E3kfG2OzX_VezYy7wm9jy-hdet84ndLAOOKEWdhaKP9XJ1fOVfuK8K3gY-OV3BPg7qDrK3ZCd7lk2i6CM3cES1VfA4sQK7" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Потребуется установить следующие пакеты:</p>
<ul>
<li><code>com.unity.entities</code> — реализация паттерна Entity-Component-System в DOTS. </li>
<li><code>com.unity.physics</code> — физический движок для DOTS.</li>
<li><code>com.unity.rendering.hybrid</code> — гибридный рендерер для DOTS, выполняет роль “моста” между системой DOTS и стандартной архитектурой рендеринга Unity.</li>
</ul>
<p>Установка этих пакетов также приведет к установке и других компонентов DOTS. Давайте кратко рассмотрим, что еще добавится в наш проект:</p>
<ul>
<li><code>com.unity.burst</code> — компилятор высокопроизводительного кода для C#.</li>
<li><code>com.unity.collections</code> — альтернативные версии коллекций (список, очередь, словарь и т.д.), основанные на использовании памяти движка вместо сборщика мусора.</li>
<li><code>com.unity.jobs</code> — система многопоточного программирования для DOTS. Позволяет легко распараллеливать код и автоматически отслеживать ошибки, возникающие при многопоточном программировании. Например, “условия гонки” (race conditions).</li>
<li><code>com.unity.mathematics</code> — оптимизированная библиотека векторной математики взамен стандартных Vector2, Vector3 и т.д.</li>
</ul>
<p>Кроме компонентов DOTS нам также пригодится пакет Cinemachine. Это очень удобный инструмент для управления камерами в Unity. Подробно останавливаться на нем сегодня я не буду, но крайне рекомендую ознакомиться, если вы про него еще не слышали. Устанавливаем:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/sGLoEQbdaUFZUVVnmNfan36UJq2uDg1hatRDH6bdOU4ZD1xfYDL2ystMZRN12_89aGpRQBJ6szDz8HCe-_w3L9sQ3UfGFWwhRzSRahSLps9HVmRlYA1p3ITtQStj6mYBeVnFSeuE" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Ну и наконец стоит добавить в проект пару наборов ассетов. Я буду использовать следующие бесплатные паки:</p>
<ul>
<li><a href="https://assetstore.unity.com/packages/3d/characters/toony-tiny-people-demo-113188" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">https://assetstore.unity.com/packages/3d/characters/toony-tiny-people-demo-113188<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li><a href="https://assetstore.unity.com/packages/3d/environments/landscapes/low-poly-simple-nature-pack-162153" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">https://assetstore.unity.com/packages/3d/environments/landscapes/low-poly-simple-nature-pack-162153<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%9F%D0%BE%D0%B7%D0%BD%D0%B0%D0%B5%D0%BC_%D1%81%D1%83%D1%89%D0%BD%D0%BE%D1%81%D1%82%D0%B8"></span>Познаем сущности<span class="ez-toc-section-end"></span></h1>
<p>Для начала, давайте создадим в сцене плоскость, которая будет играть роль Земли. Я делаю это как обычно: щелкаю правой кнопкой мыши в окне Hierarchy и выбираю 3D Object⇨Plane:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/LBMaKYvrsSLZ7a5u6VqjYOuTDdTnyCQAKt95VrdoWvwnrqUTd2tWIkfTLiznn6y1a5YCw4pAx7dajhAhs9Z4yi-axDNV4YqTqwG00qdvpep9w2iK1U9kYgP6Fy8eDYa1OIQZ30F2" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Назначу ей материал зеленого цвета, чтобы она была больше похожа на траву:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/QLTXhAXR6bdjV_D_waJutW30K-7LWSxzRaWrKDJLpUlQVVIdvf-spLVWKtZ0xe3mBsEPEnkwBKP0ZRpsdg19a5IU_iKO8qVzSAWKBQcHA2q0F1OMEZLCazAavNa_IWdYPF0FMrFX" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Теперь у нас в сцене есть обычный GameObject, изображающий плоскость. Так причем же здесь DOTS? Пока что не причем. На текущем этапе развития технологии, сцену в Unity мы по-прежнему создаем на основе игровых объектов.</p>
<p>Но теперь у нас на вооружении есть новый компонент: ConvertToEntity. Добавив этот компонент в объект Plane, мы укажем Unity при запуске игры превратить его в сущность в системе ECS:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/ZI_YYyXDOkR-rGAg_hdwH45DirZBzeOPgz68tGpNVx9CUECVR2v5Vc-9-SwRAkleqkrMj3R6DzQgYSgaIlE5ddfzTAhzwvDJ7daVfX2eOJvRm3h-T4vRqqOgqbH0O7gO2bIJcFaT" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Компонент предлагает два режима преобразования: Convert and Destroy (выбран по умолчанию) и Convert And Inject Game Object. Выбор первого режима приводит к удалению игрового объекта после создания сущности, а при выборе второго игровой объект сохраняется. Мы пока оставим эту настройку как есть, т.е. будем уничтожать объект.</p>
<p>Давайте запустим игру и посмотрим, что получилось:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/azufftsj5VAUmbOqNXuft_EeHKzbXbNyZXg2pQRTsDEKDfnhebfzxhNOYiwh0et1MAq9kCdUcqhTfk_uvKZJfeMf9nqnR3PC89Rcp5QWNQsvmIHZxCO6aJT-87bp1tjuYWjEK-Rf" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Итак, игровой объект Plane пропал из иерархии, но плоскость все же рисуется в игровом окне. Очевидно, что теперь она стала сущностью. Как можно убедиться в этом?</p>
<p></p>
<figure class="wp-block-image size-full"><a href="https://otus.ru/lessons/unity-professional/?utm_source=oj&utm_medium=affilate&utm_campaign=unitypro" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><img decoding="async" width="970" height="90" src="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg" alt="Зомби-шутер на DOTS в Unity" class="wp-image-7658" srcset="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg 970w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-300x28.jpg 300w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-150x14.jpg 150w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-768x71.jpg 768w" sizes="(max-width: 970px) 100vw, 970px" /></a></figure>
<p></p>
<p>Unity предоставляет для этого удобный инструмент, который можно найти в меню Window⇨Analysis⇨Entity Debugger:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh5.googleusercontent.com/To4FFU_jp8yLHnheoH51SsDAUwSZ88gp_3gUXNo-kvm1xLm8mDvfsSPXPGQ25nmtnIlz-07rRQlyMzhamO8Iicey6K8GFTUT9mU17Et7dWAfk8qvalFvphluDSu0qxpe7MT97Q53" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>В левой части этого окна показаны все системы в порядке их исполнения, а также длительность работы каждой из них. Мы пока не создали ни одной своей системы, поэтому все, что мы видим здесь — встроенные системы Unity. Не будем подробно на них останавливаться.</p>
<p>Выбрав конкретную систему, в правом столбце можно увидеть, какими конкретно сущностями она оперирует. А если выбран пункт All Entities, как на скриншоте, то мы увидим список абсолютно всех сущностей. В моем примере их всего две: WorldTime (стандартная сущность Unity, отслеживающая текущее время) и Plane — наша плоскость!</p>
<p>Если выбрать сущность Plane, инспектор покажет нам, какие компоненты она в себя включает:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/Wtf27xMr4WhWpfLKcybWlMUAfG26xa7IAk0__6JxDfI2NKlVfhGVu7lOFHLxJhclM6xHNuIyrDukDcviHRVP5-8g6WsXESa23_oCNu-GT2DNSvi03soFTzI8NU0yzhYfoyGSQBNe" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Как видите, вместо стандартных компонентов Unity здесь используются совершенно другие, новые компоненты. Так, компонент Transform был заменен на три компонента: LocalToWorld, Rotation и Translation. Вместо MeshFilter и MeshRenderer используется компонент RenderMesh. А для физики добавился компонент PhysicsCollider.</p>
<p>Я покажу как пользоваться этими компонентами чуть позже. Пока же для нас важно обратить внимание на следующие моменты:</p>
<ul>
<li>Сущности DOTS существуют в своем отдельном мире и никак не взаимодействуют с игровыми объектами.</li>
</ul>
<ul>
<li>Для стандартных компонентов Unity существуют аналогичные им по функциональности компоненты и системы DOTS (здесь важно отметить, что многие из них еще в разработке и не обладают всей полнотой возможностей стандартных компонентов)</li>
</ul>
<ul>
<li>Редактировать параметры компонентов во время исполнения игры в инспекторе нельзя. Что довольно-таки неудобно при отладке. Надеюсь это все-таки поправят.</li>
</ul>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%A7%D1%82%D0%BE_%D0%B7%D0%B0_%D0%B8%D0%B3%D1%80%D0%B0_%D0%B1%D0%B5%D0%B7_%D0%B8%D0%B3%D1%80%D0%BE%D0%BA%D0%B0"></span>Что за игра без игрока?<span class="ez-toc-section-end"></span></h1>
<p>Давайте теперь создадим главного персонажа, которым будет управлять наш игрок. Я добавлю новый, пустой игровой объект и назову его Player. Сразу же добавлю туда компонент ConvertToEntity, чтобы не забыть про него.</p>
<p>Для удобства, я создам отдельный, вложенный игровой объект для внешнего вида игрока (т.е. содержащий 3d-модель персонажа). Давайте для начала используем простую капсулу (щелчок правой кнопкой в иерархии, 3D Object⇨Capsule). Чтобы она не проваливалась в землю, ей необходимо поставить Position.Y равным 1.</p>
<p>При создании капсулы, Unity сразу же создает и коллайдер. Но чтобы физика могла управлять нашим игровым объектом, потребуется добавить соответствующий компонент. Его я буду добавлять в родительский объект (Player) и вместо привычного RigidBody я добавлю компонент DOTS, который называется PhysicsBody:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/U6DDCnKUV87rL5XeaOy4lvFkbMrTqYHg9cdhsvdDrPeXlwqYdEDckV32eV7o4vhqrDFrRQz7l7Cmko4dti2KCQuPitGV_OU6RbLQLAIYruWR056eIUxoxYqr_1zZD60sVPpx9hXz" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>В добавленном компоненте нужно поправить несколько параметров. Во-первых, массу (параметр Mass) стоит увеличить: я поставлю 70. Во-вторых, я сброшу параметр Angular Damping в 0. Поскольку у нас персонаж будет поворачиваться из кода, мы не хотим, чтобы физика замедляла это движение. Ну и наконец, я поставлю Gravity Factor равным 1.5. В моих экспериментах, если оставить этот параметр равным 1, физика воспринимается как на Луне.</p>
<p>Итак, у нас есть персонаж-капсула. Но как заставить его двигаться? Если помните, я говорил, что в ECS любая логика должна находиться в системах. Нам потребуется написать систему движения игрока. А чтобы система знала, какими сущностями она должна оперировать, мы создадим специальный компонент и назначим его объекту персонажа.</p>
<p>Код компонента (<code>Scripts/Components/PlayerComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct PlayerComponent : IComponentData
{
public float movementSpeed;
public float rotationSpeed;
}
</code></pre>
<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow"><div class="wp-block-group__inner-container">
<div class="wp-block-group is-layout-flow wp-block-group-is-layout-flow"><div class="wp-block-group__inner-container"></div></div>
</div></div>
<p>Очевидно, что он значительно отличается от привычных компонентов. Так, например, необходимо использовать библиотеку Unity.Entities вместо UnityEngine. Взамен класса создается структура. Родительский класс MonoBehaviour был заменен интерфейсом IComponentData. А еще в компоненте нет никаких методов!</p>
<p>Важно также обратить внимание на атрибут GenerateAuthoringComponent. Если его не добавлять, то в целом такой компонент тоже можно использовать. Но его нельзя будет создавать и редактировать в Unity. Именно благодаря этому атрибуту мы теперь сможем в инспекторе добавить наш новый компонент в игровой объект Player:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/dygPeOq-g07mYRmYRI5ywDYL7DI_TS8mQewepgDMIR1NxtN9yxHhdzxAz6euaF0dlHguDQql0GjSvPYroAbjN_gtYw8kieHnPbADRzC1CkeSe4IYMfBjap_YlosFg8zx6k9iHNuV" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>И даже параметры movementSpeed и rotationSpeed доступны для редактирования! Все, как мы привыкли. На скриншоте я уже проставил туда соответствующие значения (20 и 500).</p>
<p>Давайте теперь реализуем систему (<code>Scripts/Systems/PlayerMovementSystem.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
using Unity.Transforms;
public class PlayerMovementSystem : ComponentSystem
{
protected override void OnUpdate()
{
float deltaTime = Time.DeltaTime;
float2 input = new float2(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"));
Entities.ForEach((ref PlayerComponent player, ref LocalToWorld transform, ref PhysicsVelocity velocity) => {
float3 dir = transform.Forward * input.y * player.movementSpeed * deltaTime;
velocity.Linear += new float3(dir.x, 0.0f, dir.z);
velocity.Angular = new float3(0.0f, input.x * player.rotationSpeed * deltaTime, 0.0f);
});
}
}
</code></pre>
<p>Итак, наша система наследуется от класса <code>ComponentSystem</code>. Это не единственный возможный родительский класс для системы, на другие мы посмотрим чуть позже. Пока важно знать, что, наследуясь от этого класса, система будет работать только в основном потоке и не сможет использовать возможности Job System по распараллеливанию. Но поскольку я здесь обращаюсь к API Unity (<code>Time.DeltaTime</code> и класс <code>Input</code>), мне это и не пригодится.</p>
<p>Код системы располагается в перегруженном методе <code>OnUpdate</code>. Он будет вызываться каждый кадр, примерно как Update в привычных нам компонентах на основе <code>MonoBehaviour</code>.</p>
<p>С помощью метода <code>Entities.ForEach</code>, который будет нашей с вами основной рабочей лошадкой, я могу обработать все сущности, имеющие (в данном случае) компоненты <code>PlayerComponent</code>, <code>LocalToWorld</code> и <code>PhysicsVelocity</code>. Если хотя бы одного из этих компонентов у сущности нет, она не будет обработана этой системой.</p>
<p>Как можно видеть, список компонентов получается напрямую из перечня аргументов лямбды, что очень удобно. Аргументы обязательно должны иметь спецификатор <code>ref</code>. Это связано с тем, что компоненты представлены структурами и без использования этого спецификатора будут получены их копии, изменение которых не приведет к изменению оригинала.</p>
<p>Давайте запустим игру:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh5.googleusercontent.com/GYzQIUgcoGpTtIRsRjDp77Ug6YKw2kTTgdJYUMNTlDNibBYdMnWZcwYZekcZOpspAt9NyCWc_a9f0IIR76xG8fU3rmIdsEmY6pMxwiU27kfZ3D7ingyEMH2WeuAQ_jrVEcSilgP3" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Система работает! Персонажем можно управлять! Но погодите, что же это? Почему капсула заваливается?</p>
<p>В стандартной физике Unity мы могли бы использовать параметр Freeze Rotation у Rigidbody, чтобы предотвратить падение персонажа. В физике DOTS такого параметра пока что, к сожалению, нет. Поэтому я создаем еще один компонент и систему, чтобы решить эту проблему.</p>
<p>Компонент (<code>Scripts/Components/FreezeVerticalRotationComponent.cs</code>) мне здесь нужен исключительно как маркер, чтобы обозначить системе, какие именно сущности она должна обрабатывать. Никаких дополнительных данных я в нем хранить не буду:</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct FreezeVerticalRotationComponent : IComponentData
{
}
</code></pre>
<p>А вот код системы (<code>Scripts/Systems/FreezeVerticalRotationSystem.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Jobs;
using Unity.Physics;
public class FreezeVerticalRotationSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
JobHandle job = Entities.ForEach((ref FreezeVerticalRotationComponent tag, ref PhysicsMass mass) => {
mass.InverseInertia.xz = new float2(0.0f);
}).Schedule(inputDeps);
return job;
}
}
</code></pre>
<p>Поскольку эта система не взаимодействует со стандартным API движка, я могу сделать ее многопоточной. Для этого вместо класса <code>ComponentSystem </code>используется родительский класс <code>JobComponentSystem</code>.</p>
<p>Теперь также поменялась и сигнатура метода <code>OnUpdate</code>. Теперь он принимает аргумент <code>JobHandle</code> и возвращает <code>JobHandle</code>. Эти хендлы позволят менеджеру правильно организовать параллельное выполнение этой системы с другими системами. Очень важно не забывать передавать эти хендлы, чтобы избежать возникновения условия гонки (race condition), когда две системы попытаются одновременно работать с одной и той же сущностью.</p>
<p>Здесь также используется <code>Entities.ForEach</code>, но обратите внимание, что дополнительно вызывается метод Schedule, который собственно и запускает выполнение кода нашей системы во вспомогательных потоках.</p>
<p>Добавим компонент <code>FreezeVerticalRotationComponent </code>к объекту <code>Player </code>в редакторе и запустим игру:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/nagvHLtqwA8cv_GQRH0z0f28IxvC8AcfeiEm5xS5Bca3XuyC3thXsDR_ftk656LzQ8jxYfQIzo0z-knXxlut5G5XYFkRB0YXUUAip2MHzXDBMHuBmTPF8iRtbsQsI9dgKBwteWf5" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>И теперь наш персонаж не заваливается.</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%98%D0%BB%D0%BB%D1%8E%D0%B7%D0%B8%D1%8F_%D0%B6%D0%B8%D0%B7%D0%BD%D0%B8"></span>Иллюзия жизни<span class="ez-toc-section-end"></span></h1>
<p>Персонаж-капсула — это, несомненно, весело. Но веселее было бы, если бы наш главный герой был больше похож на человека, а главное — был анимирован.</p>
<p>К сожалению, система анимации на DOTS все еще находится на очень ранних этапах разработки и в данном проекте я ее использовать не буду. Воспользуюсь старым добрым Animator Controller.</p>
<p>Для начала, перетащу в качестве дочернего объекта в Player префаб <code>TT_demo/prefabs/TT_demo_police</code> из пакета <code>ToonyTinyPeopleDemo </code>и назначу ему заранее заготовленный контроллер. Я не буду подробно останавливаться на устройстве Animator Controller, приведу здесь лишь скриншот:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/26PAJfWMgGOzh9c-I4SKDYn6setfY1u0IXrRRTy4DpxCu2wxsCv_kdGLCNXbhE2CUrB6aXQ-fFMz8Gi-xGJvPfUOEY1n7hNQFQCJNcHN6fKK54y1aM5ub928etq0DOFwqjCBLWyF" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Также к объекту <code>TT_demo_police</code> я добавлю компонент <code>ConvertToEntity</code>, установив параметр Conversion Mode в Convert and Inject Game Object. Таким образом, при запуске игры для объекта 3d-модели также будет создана сущность, но, в отличие от игрока и капсулы, сохранится и <code>GameObject</code>. Он, правда,окажется в корне (так как родительский объект превратился в сущность без сохранения <code>GameObject</code>):</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/tqN6J5E-Qpe0pi6wQtPjLGuXWqRIIsbAJPRuA2HdoLGlNROVGLeQzP39byV6t6yNSi-FOM_umkBGAyW6_L89UlIOD_J9klvTutyrgQsy-RKpCnOI7MlQVvNM0tvEybLqa0COqqwk" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>И тут есть один нюанс. Если сейчас запустить игру, мы увидим, что при движении нашего персонажа игровой объект с 3d-моделью будет оставаться на месте:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/ZPKHK5yBPcCm7DiaP-a_tZFBcGBezWYqIU7_9FqrEdbF5wfFCbdcW2RHaHVfUnCPoaC8eJOlSxeLaeNj9IhtYvS-PH3ePdNKSLDsoZCvaWmfZRKZ5sYVCKxwE-9UUvc7V717pfYF" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Чтобы это поправить, я добавлю новый компонент и систему.</p>
<p>Компонент (<code>Scripts/Components/CopyTransformComponent.cs</code>) также выполняет роль простого маркера:</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct CopyTransformComponent : IComponentData
{
}
</code></pre>
<p>А система (<code>Scripts/Systems/CopyTransformSystem.cs</code>) просто осуществляет копирование положения сущности в компонент Transform игрового объекта:</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Transforms;
using Unity.Entities;
public class CopyTransformSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref CopyTransformComponent tag, ref LocalToWorld localToWorld) => {
var transform = EntityManager.GetComponentObject<Transform>(entity);
transform.position = localToWorld.Position;
transform.rotation = localToWorld.Rotation;
});
}
}
</code></pre>
<p>Важным нововведением здесь является добавление аргумента <code>Entity entity</code> в лямбду. Этот аргумент передается без спецификатора <code>ref</code>, поскольку он по сути является лишь числовым идентификатором сущности и его нельзя менять. Но зато его можно использовать совместно с классом <code>EntityManager</code> для манипуляции сущностями и, в данном случае, для получения ссылки на компонент <code>Transform</code> у связанного с сущностью игрового объекта (привязку осуществляет компонент <code>ConvertToEntity</code> при конвертации, поскольку был выбран режим <code>Convert And Inject Game Object</code>).</p>
<p></p>
<p></p>
<figure class="wp-block-image size-full"><a href="https://otus.ru/lessons/unity-professional/?utm_source=oj&utm_medium=affilate&utm_campaign=unitypro" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><img decoding="async" width="970" height="90" src="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg" alt="Зомби-шутер на DOTS в Unity" class="wp-image-7658" srcset="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg 970w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-300x28.jpg 300w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-150x14.jpg 150w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-768x71.jpg 768w" sizes="(max-width: 970px) 100vw, 970px" /></a></figure>
<p></p>
<p>Теперь я добавлю компонент <code>CopyTransformComponent</code> к игровому объекту <code>TT_demo_police</code>. Игровой объект <code>Capsule</code> я оставлю (так как в нем находится коллайдер), но отключу у него <code>MeshRenderer</code>, чтобы капсула не рисовалась на экране.</p>
<p>Запустим игру:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/1IXFxBaEUShp5Nquoelnomlksi49JMCVZzwrzRJNUKZocWYy7w5sFJDoTyWd2TzZYVLWzZlpaJilUg1X9SIF0GvTyx0tdenXpBjvSz2wdMIxtNB99uAvwKnfQSOIVr7ISBVXeD0B" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>И да, теперь наш персонаж двигается.</p>
<p>Осталось добавить 3d-модельке игрока анимацию. Как вы, наверное, уже догадались, потребуется создать еще один компонент и систему.</p>
<blockquote class="wp-block-quote">
<p><em>N.B.</em> В начале работы над игрой, использующей паттерн ECS, требуется создавать довольно большое количество систем и компонентов, и может показаться, что это лишняя работа по сравнению с паттерном EC. На самом деле, это не так. Преимущества ECS полностью проявляются, когда накоплена некоторая “критическая масса” компонентов и систем. В некоторой мере мы это увидим и в сегодняшней статье, когда будем реализовывать врагов.</p>
</blockquote>
<p>Компонент (<code>Scripts/Components/AnimatedCharacterComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
using UnityEngine;
[GenerateAuthoringComponent]
public struct AnimatedCharacterComponent : IComponentData
{
public Entity animatorEntity;
}
</code></pre>
<p>Система (<code>Scripts/Systems/AnimatedCharacterSystem.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
public class AnimatedCharacterSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref AnimatedCharacterComponent character, ref PhysicsVelocity velocity) => {
var animator = EntityManager.GetComponentObject<Animator>(character.animatorEntity);
animator.SetFloat("speed", math.length(velocity.Linear));
});
}
}
</code></pre>
<p>Ничего особенно нового здесь нет. Стоит обратить внимание, что компонент <code>AnimatedCharacterComponent</code> следует добавлять на родительскую сущность (<code>Player</code>) — ему требуется получать значения скорости из компонента <code>PhysicsVelocity</code> физического движка. А вот аниматор прикреплен к дочерней сущности (<code>TT_demo_police</code>). Чтобы получить доступ из одной сущности к другой, я просто использую переменную типа <code>Entity</code> в компоненте <code>AnimatedCharacterComponent</code>. Благодаря механизму <code>ConvertToEntity</code>, в редакторе сцены я смогу проставить туда игровой объект, а при запуске игры он автоматически сконвертируется в ссылку на соответствующую сущность:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/0ZmQNzcGi9aKtJrAOv5ebwoHBVV6RnqAo-Qedv7JPhIX0CZFm8ufSYFifi6AFeVEfWKAHwMTOOwnxBI3VHxELDhpyymKdtsc9f1ppVDtGImmcDYjqcajIBiHGo91d7GK1HkgXyhz" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Запустив игру, убеждаемся, что теперь анимация работает:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/VAoLCBWhSEt38AiqKRv2uWBmt3GI1VbujTBmyn6qWcqa6Db8vFMGWuRBwPggBOBZCDj_yYVbYCfhepdiIzEaJ-7NXByP3t4Y18GU8IwDOCMA4fu3E_PIlcmoexb4btX5JRRzJQYp" alt="Зомби-шутер на DOTS в Unity"/></figure>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%90_%D1%8F_%D0%B2%D1%81%D0%B5_%D0%B3%D0%BB%D1%8F%D0%B6%D1%83_%D0%B3%D0%BB%D0%B0%D0%B7_%D0%BD%D0%B5_%D0%BE%D1%82%D0%B2%D0%BE%D0%B6%D1%83"></span>А я все гляжу, глаз не отвожу<span class="ez-toc-section-end"></span></h1>
<p>Я сейчас ненадолго отвлекусь и наведу немного красоты: добавлю контента в уровень и настрою камеру.</p>
<p>Для уровня добавим интересных объектов из набора <code>SimpleNaturePack</code>, чтобы не бегать по пустой плоскости:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/cIcBLbsuYrMhOJQjchGihbideDl04qL4T8FlF5AEaH3bpf6I0GNghWDh9YkZb6RpgZb8dKix_nU6-gJWUuaZauzmFkmGnFkt1BbJRcwx_BenAVSHSu8yLt8HWXHSmEUNHh006-mh" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Камеру же я настрою с использованием <code>Cinemachine</code>. Подробно останавливаться на этом я не буду, приведу лишь пример настройки.</p>
<p>Для начала, я создам отдельный пустой игровой объект внутри <code>TT_demo_police</code> и поставлю его примерно на уровень головы — он будет использоваться для “прицеливания” камеры. Назову его <code>CameraTarget</code>.</p>
<p>Теперь можно создать виртуальную камеру. Для этого выберу пункт меню <code>Cinemachine⇨Create Virtual Camera</code> (если этого меню у вас нет, проверьте, установили ли вы пакет <code>Cinemachine</code>).</p>
<p>В инспекторе для созданной виртуальной камеры проставлю <code>TT_demo_police</code> в поле <code>Follow </code>и <code>CameraTarget</code> в поле <code>Look At</code>. В разделе <code>Body</code> поставлю <code>Follow Offset</code> <code>X=0, Y=9, Z=-15</code> и <code>Yaw Damping</code> в <code>0.25</code>.</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%A1%D1%82%D1%80%D0%B5%D0%BB%D1%8F%D0%B9_%D0%93%D0%BB%D0%B5%D0%B1_%D0%95%D0%B3%D0%BE%D1%80%D1%8B%D1%87"></span>Стреляй, Глеб Егорыч!<span class="ez-toc-section-end"></span></h1>
<p>Бегать по карте — это здорово. Но шутер не был бы шутером, если бы там нельзя было стрелять. А у нас все еще нельзя. Нужно срочно это исправлять!</p>
<p>Прежде всего, нужно возможность определять из кода положение дула пистолета, чтобы пули вылетали четко из него (положение дула может слегка меняться под действием анимации).</p>
<p>Сам пистолет прикреплен к анимированному скелету и его легко найти в иерархии объекта <code>TT_demo_police</code>:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh3.googleusercontent.com/5nten5VkaSOG3dKJythK5c8E7767URPdHf6sEduo7TJTWA5AGJQHzVnk9nxKxN4ZXosMgGKnygKnko16a9eLtvXKC46ZfRMhYK291khKqX2qUYQMfhE162fkwUhB3vFJccaFixkP" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Я создам внутри пистолета пустой объект (назову его GunHole) и задам ему положение (-0.1174, 0, 0.394) и поворот (0, 0, 90). Этот объект я буду использовать как референс для кода, создающего пули.</p>
<p>Но есть небольшая проблема: так как <code>TT_demo_police</code> (родительский объект) уже содержит в себе <code>ConvertToEntity</code> с режимом <code>Convert And Inject Game Object</code>, я не смогу сделать то же самое с моим вновь созданным объектом, Unity разрешает только одну такую конвертацию для иерархии объектов.</p>
<p>Поэтому, я создам новый объект <code>Shooter</code> внутри объекта <code>Player</code> и прикреплю к нему небольшой компонент на основе <code>MonoBehaviour</code> (<code>Scripts/Shooter.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
public class Shooter : MonoBehaviour
{
public Transform gunHole;
}
</code></pre>
<p>И пропишу в него ссылку на <code>GunHole</code> в инспекторе.</p>
<p>А поскольку <code>Shooter</code> находится вне объекта <code>TT_demo_police</code>, я могу также добавить в него компонент <code>ConvertToEntity</code>:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/2xam_vKIuncNdAWCH0t7rov-Y96TW2tky9R72aJV-EajhcgVTkt-Lz9N0yAtAgvDymgpYY0Xq8qGkkQ8yIbIMuId-WkK49dk6cAYm7L0mYW65zxr6HWCBLzoqN-8uHytI4t_EpK9" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Кроме положения дула, мне также потребуется и префаб пули. В префабах также можно использовать <code>ConvertToEntity</code> — такие префабы превратятся в сущности с компонентом Prefab при загрузке игры и будут исключены из обработки, но будут загружены и проинициализированы. Также, по аналогии с обычными префабами, из таких префабов-сущностей можно создавать обычные сущности и работает это гораздо быстрее, чем метод Instantiate!</p>
<p>Поэтому я создам префаб из двух игровых объектов: родительский будет содержать в себе компоненты <code>ConvertToEntity</code> и <code>PhysicsBody</code>, а дочерний — меш и коллайдер капсулы:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/2GkKdxIPnlpMIAqyqM48kczPmf5wy46L19lvrtOPECk5FFDD3ABxE6DYSgNLZR92SEKMbpefnBS3IyoOgWkEbpsEhNXd1zNtMW7ktI1sKuFl3bhGscGDAAVgRWpTf9Bqz1yhMZJZ" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Из важного: коллайдер у пули настроен как триггер, а в параметры <code>PhysicsBody</code> внесены небольшие изменения: <code>Linear Damping</code> выставлен в 0, чтобы пуля не теряла скорость со временем, и <code>Gravity Factor</code> также выставлен в 0.</p>
<p>В объект <code>Shooter</code> также добавлю компонент <code>BulletPrefabComponent</code> (<code>Scripts/Components/BulletPrefabComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct BulletPrefabComponent : IComponentData
{
public Entity prefab;
public float speed;
}
</code></pre>
<p>Этот компонент позволит нам получить доступ к префабу из ECS-кода, а также — задать скорость пули (сразу можно ее проставить в инспекторе, я использовал значение 30).</p>
<p>И еще один компонент я буду проставлять непосредственно на пули (<code>Scripts/Components/Bullet.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
using Unity.Mathematics;
[GenerateAuthoringComponent]
public struct BulletComponent : IComponentData
{
public float3 speed;
public bool destroyed;
}
</code></pre>
<p>Здесь параметр <code>speed</code> будет определять вектор направления движения пули и ее скорость, а флаг <code>destroyed</code> будет использоваться для обозначения пуль, столкнувшихся с препятствием. Зачем нужен этот флаг и как он используется я объясню чуть дальше.</p>
<p>Теперь для выстрелов нам потребуется система (<code>Scripts/Systems/PlayerShootingSystem.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Entities;
using Unity.Transforms;
public class PlayerShootingSystem : ComponentSystem
{
protected override void OnUpdate()
{
if (!Input.GetButtonDown("Fire1"))
return;
Entities.ForEach((Entity entity, ref BulletPrefabComponent bulletPrefab) => {
var shooter = EntityManager.GetComponentObject<Shooter>(entity);
if (shooter == null)
Debug.LogError("BulletPrefabComponent is missing Shooter component.");
else {
Entity bullet = EntityManager.Instantiate(bulletPrefab.prefab);
EntityManager.SetComponentData(bullet, new Translation{ Value = shooter.gunHole.position });
EntityManager.SetComponentData(bullet, new Rotation{ Value = shooter.gunHole.rotation });
EntityManager.AddComponentData(bullet, new BulletComponent{ speed = shooter.gunHole.forward * bulletPrefab.speed });
}
});
}
}
</code></pre>
<p>Если сейчас запустить игру, мы увидим, что при выстреле пули действительно появляются:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh6.googleusercontent.com/IcqTXr52egEHeGlWsqA12kdNQ3zxKtZKOSYtLGVo3EwJbMhXg35MtJRzy2s9dwrIx7EmTafBUGfExQ4XRKPRmlAkugpxNrTzVEDPN_0aNvLmiIc_dt7wmq9hyBL42PHDhdGkrtKj" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Но они не двигаются! Как исправить? Правильно — завести систему (<code>Scripts/Systems/BulletSystem.cs</code>):</p>
<p></p>
<pre class="wp-block-code"><code>using Unity.Entities;
using Unity.Jobs;
using Unity.Physics;
public class BulletSystem : JobComponentSystem
{
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
JobHandle job = Entities.ForEach((ref BulletComponent bullet, ref PhysicsVelocity velocity) => {
velocity.Linear = bullet.speed;
}).Schedule(inputDeps);
return job;
}
}
</code></pre>
<p>Как видите, тут все очень просто: задаем пулям постоянную скорость и пусть себе летят.</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%9E%D0%B6%D0%B8%D0%B2%D1%88%D0%B8%D0%B5_%D0%BC%D0%B5%D1%80%D1%82%D0%B2%D0%B5%D1%86%D1%8B"></span>Ожившие мертвецы<span class="ez-toc-section-end"></span></h1>
<p>Замечательно, пули у нас есть. Но стрелять пока не в кого. Давайте создадим зомби! </p>
<p>Я создам в корне сцены пустой игровой объект <code>Zombie</code> и настрою его по аналогии с главным героем. Сразу положу внутрь префаб <code>TT_demo/prefabs/TT_demo_zombie</code>. Также создам капсулу (<code>Position 0, 1, 0</code>) и сразу отключу у нее <code>MeshRenderer</code>. В сам объект <code>Zombie</code> добавлю компоненты <code>PhysicsBody</code>, <code>FreezeVerticalRotationComponent</code>, <code>AnimatedCharacterComponent</code> и <code>ConvertToEntity</code>. Также компонент <code>ConvertToEntity</code> надо добавить и в объект <code>TT_demo_zombie</code>, указав режим <code>Convert And Inject Game Object</code>. Туда же нужно добавить и <code>CopyTransformComponent</code>. У <code>PhysicsBody</code> поставлю <code>Mass=70</code>. В общем, очень похоже на настройку объекта <code>Player</code>, только без <code>Shooter</code>.</p>
<p>Приятно, что большинство компонентов и систем уже созданы. Но потребуется создать еще несколько компонентов, уникальных для зомби.</p>
<p>Прежде всего, нужно добавить врагу жизни (<code>Scripts/Components/HealthComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct HealthComponent : IComponentData
{
public int value;
}
</code></pre>
<p>И создадим систему (<code>Scripts/Components/AnimatedCharacterDeathSystem.cs</code>), которая будет отыгрывать анимацию смерти персонажа, когда счетчик жизней достигнет 0:</p>
<pre class="wp-block-code"><code>using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Physics;
public class AnimatedCharacterDeathSystem : ComponentSystem
{
protected override void OnUpdate()
{
Entities.ForEach((Entity entity, ref AnimatedCharacterComponent character, ref HealthComponent health) => {
var animator = EntityManager.GetComponentObject<Animator>(character.animatorEntity);
if (health.value <= 0) {
animator.SetTrigger("die");
EntityManager.RemoveComponent<HealthComponent>(entity);
}
});
}
}
</code></pre>
<p>Для проверки я добавлю зомби компонент <code>HealthComponent</code> с количеством жизней 0 и запущу игру. Зомби должен сразу умереть:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh5.googleusercontent.com/t7sTiLwjqNe2sFWeBgRQnlAQzKx5LRP8rv9BHC_OdQX-ryLs2Y_wPpX8Bz7hHgtu_4BWPECe5SpO8fp0nrd_n-DVhsDgC7yrxVdMUpzr4JSHHT76M4K-VYWxsT3BBVmkiycOQ9Jy" alt="Зомби-шутер на DOTS в Unity"/></figure>
<p>Превосходно, код работает! Поставлю зомби, например, 3 жизни и займусь реализацией проверки столкновения пули и врага.</p>
<p>Для этого мне потребуется самая сложная в этой статье система (<code>Scripts/Systems/BulletDamageSystem.cs</code>). Давайте сначала взглянем на нее, а потом будем разбираться:</p>
<pre class="wp-block-code"><code>using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Physics.Systems;
using Unity.Physics;
public class CollisionEventSystem : JobComponentSystem
{
struct CollisionEventSystemJob : ITriggerEventsJob
{
public ComponentDataFromEntity<BulletComponent> bulletRef;
public ComponentDataFromEntity<HealthComponent> healthRef;
public void Execute(TriggerEvent triggerEvent)
{
Entity hitEntity, bulletEntity;
if (bulletRef.HasComponent(triggerEvent.EntityA)) {
hitEntity = triggerEvent.EntityB;
bulletEntity = triggerEvent.EntityA;
} else if (bulletRef.HasComponent(triggerEvent.EntityB)) {
hitEntity = triggerEvent.EntityA;
bulletEntity = triggerEvent.EntityB;
} else
return;
var bullet = bulletRef[bulletEntity];
bullet.destroyed = true;
bulletRef[bulletEntity] = bullet;
if (healthRef.HasComponent(hitEntity)) {
var health = healthRef[hitEntity];
health.value--;
healthRef[hitEntity] = health;
}
}
}
BuildPhysicsWorld buildPhysicsWorldSystem;
StepPhysicsWorld stepPhysicsWorld;
EndSimulationEntityCommandBufferSystem endSimulationCommandBuffer;
protected override void OnCreate()
{
buildPhysicsWorldSystem = World.GetOrCreateSystem<BuildPhysicsWorld>();
stepPhysicsWorld = World.GetOrCreateSystem<StepPhysicsWorld>();
endSimulationCommandBuffer = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
}
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new CollisionEventSystemJob();
job.bulletRef = GetComponentDataFromEntity<BulletComponent>(isReadOnly: false);
job.healthRef = GetComponentDataFromEntity<HealthComponent>(isReadOnly: false);
var jobResult = job.Schedule(stepPhysicsWorld.Simulation, ref buildPhysicsWorldSystem.PhysicsWorld, inputDeps);
var commandBuffer = endSimulationCommandBuffer.CreateCommandBuffer().AsParallelWriter();
var result = Entities.ForEach((Entity entity, int entityInQueryIndex, ref BulletComponent bullet) => {
if (bullet.destroyed)
commandBuffer.DestroyEntity(entityInQueryIndex, entity);
}).Schedule(jobResult);
endSimulationCommandBuffer.AddJobHandleForProducer(result);
return result;
}
}
</code></pre>
<p>Первая (и весьма важная) часть — это структура <code>CollisionEventSystemJob</code>. Она представляет собой задачу для системы <code>Job System</code>. В эту задачу физический движок передает перечень столкновений между физическими объектами. Код задачи в методе <code>Execute</code> проверяет, есть ли в одном из столкнувшихся объектов компонент <code>BulletComponent</code>. Если, действительно, один из объектов — пуля, то ей проставляется флажок <code>destroy</code>, а у второго объекта вычитаются жизни (если, конечно, у него есть соответствующий компонент; столкновение с деревом приводит просто к уничтожению пули).</p>
<p>Стоит обратить внимание на два момента: во-первых, обращение к компонентам происходит через класс <code>ComponentDataFromEntity</code>. Это нужно, потому что задача выполняется параллельно и движок должен отслеживать обращения к компонентам и не допускать одновременной работы с одним и тем же компонентом из разных потоков. Во-вторых, по той же причине, нельзя уничтожать пули сразу при обнаружении столкновения. Вместо этого, у пули проставляется флажок, который проверяется уже в безопасном окружении, где компонент может быть удален.</p>
<p>В методе <code>OnUpdate</code> я сначала прошу физический движок сообщить информацию о столкновениях и передаю ее в <code>CollisionEventSystemJob</code> для обработки. Как только эта работа закончена, я проверяю были ли уничтожены какие-то пули и если были, то передаю их в в командный буфер для фактического уничтожения этих сущностей.</p>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%91%D0%B5%D0%B3%D0%B8_%D0%9B%D0%BE%D0%BB%D0%B0_%D0%91%D0%B5%D0%B3%D0%B8"></span>Беги, Лола, Беги!<span class="ez-toc-section-end"></span></h1>
<p>Последняя важная составляющая логики врага, которая у нас все еще отсутствует — это движение. Давайте же реализуем охоту на игрока!</p>
<p>Для поиска пути я буду использовать технологию <code>NavMesh</code>. Опять же, я не буду останавливаться на ней подробно, скажу лишь, что запечь <code>NavMesh</code> можно в окне <code>Navigation</code>, которое можно открыть через меню <code>Window⇨AI⇨Navigation</code>.</p>
<p>Я создам внутри игрового объекта <code>Zombie</code> пустой объект <code>NavMeshAgent</code> и добавлю в него компонент <code>NavMeshAgent</code>. Также я добавлю компонент <code>ConvertToEntity</code> с режимом <code>Convert And Inject Game Object</code>.</p>
<p>В дополнение к этому, мне потребуется новый компонент <code>NavMeshAgentComponent</code> (<code>Scripts/Components/NavMeshAgentComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct NavMeshAgentComponent : IComponentData
{
public Entity moveEntity;
}
</code></pre>
<p>Единственный параметр здесь — это сущность, которую этот <code>NavMeshAgent</code> будет двигать. Добавив этот компонент в дочерний объект <code>NavMeshAgent</code>, в качестве <code>moveEntity</code> я укажу объект <code>Zombie</code>.</p>
<p>Еще один новый компонент (<code>Scripts/Components/FollowTargetComponent.cs</code>):</p>
<pre class="wp-block-code"><code>using Unity.Entities;
[GenerateAuthoringComponent]
public struct FollowTargetComponent : IComponentData
{
}
</code></pre>
<p>Этот компонент я буду использовать просто как маркер: только враги, имеющие этот компонент, будут преследовать игрока.</p>
<p>Последним штрихом будет написание соответствующей системы (<code>Scripts/Systems/FollowPlayerSystem.cs</code>):</p>
<pre class="wp-block-code"><code>using UnityEngine;
using UnityEngine.AI;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
public class FollowPlayerSystem : ComponentSystem
{
protected override void OnUpdate()
{
float3 targetPosition = float3.zero;
Entities.ForEach((Entity entity, ref LocalToWorld transform, ref FollowTargetComponent tag) => {
targetPosition = transform.Position;
});
Entities.ForEach((Entity entity, ref NavMeshAgentComponent agent) => {
var navMeshAgent = EntityManager.GetComponentObject<NavMeshAgent>(entity);
if (navMeshAgent != null) {
navMeshAgent.SetDestination(targetPosition);
EntityManager.SetComponentData(agent.moveEntity, new Translation{ Value = navMeshAgent.transform.position });
}
});
}
}
</code></pre>
<p>Запускаем, проверяем:</p>
<figure class="wp-block-image"><img decoding="async" src="https://lh4.googleusercontent.com/2LPbPGMn-9umw-5SMPyw2Z_85m_DKxUI8GgDqkBG3SPC997gycruiTsaOBRsGnKmS-g9scaN2A6XWeIWDBSCAO9J2_vkOF-FaeMrA1fC3FhuLU9ILkc0bDNl9_2xYmvMFICohahH" alt="Зомби-шутер на DOTS в Unity"/></figure>
<h1 class="wp-block-heading"><span class="ez-toc-section" id="%D0%9A%D0%BE%D0%BD%D0%B5%D1%86"></span>Конец<span class="ez-toc-section-end"></span></h1>
<p>Сегодня мы с вами познакомились с платформой DOTS и паттерном ECS. Надеюсь, мне удалось заинтересовать вас этими технологиями и показать, как их использовать в реальном игровом проекте.</p>
<p>Скачать исходный код проекта можно с GitHub: <a href="https://github.com/zapolnov/dots-zombie-shooter" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">https://github.com/zapolnov/dots-zombie-shooter<span class="wpel-icon wpel-image wpel-icon-6"></span></a></p>
<p>Желаю удачи в ваших экспериментах!</p>
<p></p>
<figure class="wp-block-image size-full"><a href="https://otus.ru/lessons/unity-professional/?utm_source=oj&utm_medium=affilate&utm_campaign=unitypro" target="_blank" rel="noreferrer noopener nofollow external" data-wpel-link="external"><img decoding="async" width="970" height="90" src="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg" alt="Зомби-шутер на DOTS в Unity" class="wp-image-7658" srcset="https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya.jpg 970w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-300x28.jpg 300w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-150x14.jpg 150w, https://otus.ru/journal/wp-content/uploads/2023/10/unity_Welcome_970x90-kopiya-768x71.jpg 768w" sizes="(max-width: 970px) 100vw, 970px" /></a></figure>
<p></p>
</div><!-- .post-content -->
<div class="the-post-foot cf">
<div class="tag-share cf">
<div class="post-tags"><a href="https://otus.ru/journal/tag/gamedev/" rel="tag" data-wpel-link="internal">gamedev</a><a href="https://otus.ru/journal/tag/unity/" rel="tag" data-wpel-link="internal">unity</a><a href="https://otus.ru/journal/tag/urok/" rel="tag" data-wpel-link="internal">урок</a></div>
<div class="post-share">
<div class="post-share-icons cf">
<span class="counters">
</span>
<a href="https://www.facebook.com/sharer.php?u=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F" class="link facebook wpel-icon-right" target="_blank" title="Share on Facebook" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-facebook"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F&text=%D0%97%D0%BE%D0%BC%D0%B1%D0%B8-%D1%88%D1%83%D1%82%D0%B5%D1%80%20%D0%BD%D0%B0%20DOTS%20%D0%B2%20Unity" class="link twitter wpel-icon-right" target="_blank" title="Share on Twitter" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-twitter"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F" class="link linkedin wpel-icon-right" target="_blank" title="LinkedIn" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-linkedin"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
<a href="https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fotus.ru%2Fjournal%2Fzombi-shuter-na-dots-v-unity%2F&media=https%3A%2F%2Fotus.ru%2Fjournal%2Fwp-content%2Fuploads%2F2020%2F12%2Foj-1080x720-15.png&description=%D0%97%D0%BE%D0%BC%D0%B1%D0%B8-%D1%88%D1%83%D1%82%D0%B5%D1%80%20%D0%BD%D0%B0%20DOTS%20%D0%B2%20Unity" class="link pinterest wpel-icon-right" target="_blank" title="Pinterest" data-wpel-link="external" rel="nofollow external noopener noreferrer"><i class="fa fa-pinterest-p"></i><span class="wpel-icon wpel-image wpel-icon-6"></span></a>
</div>
</div>
</div>
</div>
<div class="post-nav">
<div class="post previous cf">
<a href="https://otus.ru/journal/kakie-yazyki-programmirovaniya-uchit-v-2021-godu/" title="Prev Post" class="nav-icon" data-wpel-link="internal">
<i class="fa fa-angle-left"></i>
</a>
<span class="content">
<a href="https://otus.ru/journal/kakie-yazyki-programmirovaniya-uchit-v-2021-godu/" class="image-link" rel="previous" data-wpel-link="internal">
<img width="150" height="100" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20150%20100%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-thumbnail size-thumbnail lazyload wp-post-image" alt="Как начать карьеру в IT: советы опытного разработчика" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-768x512.png 768w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-270x180.png 270w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-370x245.png 370w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16.png 1080w" data-src="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-16-150x100.png" data-sizes="(max-width: 150px) 100vw, 150px" title="Как начать карьеру в IT: советы опытного разработчика" /> </a>
<div class="post-meta">
<span class="label">Prev Post</span>
<div class="post-meta post-meta-b">
<h2 class="post-title">
<a href="https://otus.ru/journal/kakie-yazyki-programmirovaniya-uchit-v-2021-godu/" data-wpel-link="internal">Как начать карьеру в IT: советы опытного разработчика</a>
</h2>
<div class="below">
<a href="https://otus.ru/journal/kakie-yazyki-programmirovaniya-uchit-v-2021-godu/" class="meta-item date-link" data-wpel-link="internal"><time class="post-date" datetime="2020-12-11T17:08:36+00:00">11 декабря, 2020</time></a>
<span class="meta-sep"></span>
<span class="meta-item read-time">8 Mins Read</span>
</div>
</div> </div>
</span>
</div>
<div class="post next cf">
<a href="https://otus.ru/journal/grafika-v-python-tkinter-i-canvas/" title="Next Post" class="nav-icon" data-wpel-link="internal">
<i class="fa fa-angle-right"></i>
</a>
<span class="content">
<a href="https://otus.ru/journal/grafika-v-python-tkinter-i-canvas/" class="image-link" rel="next" data-wpel-link="internal">
<img width="150" height="100" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20150%20100%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="attachment-thumbnail size-thumbnail lazyload wp-post-image" alt="Графика в Python: Tkinter и Canvas" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-150x100.png 150w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-300x200.png 300w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-1024x683.png 1024w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-768x512.png 768w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-270x180.png 270w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-370x245.png 370w, https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14.png 1080w" data-src="https://otus.ru/journal/wp-content/uploads/2020/12/oj-1080x720-14-150x100.png" data-sizes="(max-width: 150px) 100vw, 150px" title="Графика в Python: Tkinter и Canvas" /> </a>
<div class="post-meta">
<span class="label">Next Post</span>
<div class="post-meta post-meta-b">
<h2 class="post-title">
<a href="https://otus.ru/journal/grafika-v-python-tkinter-i-canvas/" data-wpel-link="internal">Графика в Python: Tkinter и Canvas</a>
</h2>
<div class="below">
<a href="https://otus.ru/journal/grafika-v-python-tkinter-i-canvas/" class="meta-item date-link" data-wpel-link="internal"><time class="post-date" datetime="2020-12-16T20:18:15+00:00">16 декабря, 2020</time></a>
<span class="meta-sep"></span>
<span class="meta-item read-time">5 Mins Read</span>
</div>
</div> </div>
</span>
</div>
</div>
<section class="related-posts grid-3">
<h4 class="section-head"><span class="title">Читать ещё</span></h4>
<div class="ts-row posts cf">
<article class="post col-4">
<a href="https://otus.ru/journal/uroven-gotovnosti-cto-k-2026/" title="Уровень готовности CTO к 2026" class="image-link" data-wpel-link="internal">
<img width="270" height="180" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20270%20180%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="image lazyload wp-post-image" alt="Уровень готовности CTO к 2026" title="Уровень готовности CTO к 2026" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-3-270x180.jpg 270w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-3-770x515.jpg 770w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-3-370x245.jpg 370w" data-src="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-3-270x180.jpg" data-sizes="(max-width: 270px) 100vw, 270px" /> </a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/uroven-gotovnosti-cto-k-2026/" class="post-link" data-wpel-link="internal">Уровень готовности CTO к 2026</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-11-16T19:50:59+00:00">16 ноября, 2025</time>
</div>
</div>
</article >
<article class="post col-4">
<a href="https://otus.ru/journal/novye-uroki-noyabrya-tolko-top-temy-po-programmirovaniju/" title="Новые уроки ноября: только топ-темы по программированию" class="image-link" data-wpel-link="internal">
<img width="270" height="180" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20270%20180%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="image lazyload wp-post-image" alt="Новые уроки ноября: только топ-темы по программированию" title="Новые уроки ноября: только топ-темы по программированию" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-270x180.jpg 270w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-770x515.jpg 770w, https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-370x245.jpg 370w" data-src="https://otus.ru/journal/wp-content/uploads/2025/11/oj-1080x720-kopiya-2-270x180.jpg" data-sizes="(max-width: 270px) 100vw, 270px" /> </a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/novye-uroki-noyabrya-tolko-top-temy-po-programmirovaniju/" class="post-link" data-wpel-link="internal">Новые уроки ноября: только топ-темы по программированию</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-11-09T23:24:11+00:00">9 ноября, 2025</time>
</div>
</div>
</article >
<article class="post col-4">
<a href="https://otus.ru/journal/schjot-idjot-na-chasy/" title="Счёт идёт на часы" class="image-link" data-wpel-link="internal">
<img width="270" height="180" src="data:image/svg+xml,%3Csvg%20viewBox%3D%270%200%20270%20180%27%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%3E%3C%2Fsvg%3E" class="image lazyload wp-post-image" alt="Счёт идёт на часы" title="Счёт идёт на часы" decoding="async" loading="lazy" data-srcset="https://otus.ru/journal/wp-content/uploads/2025/10/oj-1080x720-kopiya-7-270x180.png 270w, https://otus.ru/journal/wp-content/uploads/2025/10/oj-1080x720-kopiya-7-770x515.png 770w, https://otus.ru/journal/wp-content/uploads/2025/10/oj-1080x720-kopiya-7-370x245.png 370w" data-src="https://otus.ru/journal/wp-content/uploads/2025/10/oj-1080x720-kopiya-7-270x180.png" data-sizes="(max-width: 270px) 100vw, 270px" /> </a>
<div class="content">
<h3 class="post-title"><a href="https://otus.ru/journal/schjot-idjot-na-chasy/" class="post-link" data-wpel-link="internal">Счёт идёт на часы</a></h3>
<div class="post-meta">
<time class="post-date" datetime="2025-10-30T15:04:59+00:00">30 октября, 2025</time>
</div>
</div>
</article >
</div>
</section>
</article> <!-- .the-post -->
</div>
<aside class="col-4 sidebar">
<div class="inner">
<ul>
<li id="search-2" class="widget widget_search"><h5 class="widget-title"><span>Поиск по блогу</span></h5>
<form method="get" class="search-form" action="https://otus.ru/journal/">
<label>
<span class="screen-reader-text">Search for:</span>
<input type="search" class="search-field" placeholder="Введите запрос и нажмите Enter" value="" name="s" title="Search for:" />
</label>
<button type="submit" class="search-submit"><i class="fa fa-search"></i></button>
</form>
</li>
<li id="tag_cloud-5" class="widget widget_tag_cloud"><h5 class="widget-title"><span>Метки</span></h5><div class="tagcloud"><a href="https://otus.ru/journal/tag/android-2/" class="tag-cloud-link tag-link-74 tag-link-position-1" style="font-size: 12.472222222222pt;" aria-label="Android (34 элемента)" data-wpel-link="internal">Android</a>
<a href="https://otus.ru/journal/tag/c-3/" class="tag-cloud-link tag-link-91 tag-link-position-2" style="font-size: 10.916666666667pt;" aria-label="C (23 элемента)" data-wpel-link="internal">C</a>
<a href="https://otus.ru/journal/tag/c-2/" class="tag-cloud-link tag-link-81 tag-link-position-3" style="font-size: 12.666666666667pt;" aria-label="C# (35 элементов)" data-wpel-link="internal">C#</a>
<a href="https://otus.ru/journal/tag/c/" class="tag-cloud-link tag-link-20 tag-link-position-4" style="font-size: 12.472222222222pt;" aria-label="c++ (34 элемента)" data-wpel-link="internal">c++</a>
<a href="https://otus.ru/journal/tag/computer-science/" class="tag-cloud-link tag-link-209 tag-link-position-5" style="font-size: 15.972222222222pt;" aria-label="computer science (78 элементов)" data-wpel-link="internal">computer science</a>
<a href="https://otus.ru/journal/tag/css/" class="tag-cloud-link tag-link-288 tag-link-position-6" style="font-size: 8.6805555555556pt;" aria-label="CSS (13 элементов)" data-wpel-link="internal">CSS</a>
<a href="https://otus.ru/journal/tag/data-science/" class="tag-cloud-link tag-link-151 tag-link-position-7" style="font-size: 8pt;" aria-label="Data Science (11 элементов)" data-wpel-link="internal">Data Science</a>
<a href="https://otus.ru/journal/tag/devops/" class="tag-cloud-link tag-link-98 tag-link-position-8" style="font-size: 10.138888888889pt;" aria-label="devops (19 элементов)" data-wpel-link="internal">devops</a>
<a href="https://otus.ru/journal/tag/docker/" class="tag-cloud-link tag-link-143 tag-link-position-9" style="font-size: 8.2916666666667pt;" aria-label="Docker (12 элементов)" data-wpel-link="internal">Docker</a>
<a href="https://otus.ru/journal/tag/gamedev/" class="tag-cloud-link tag-link-25 tag-link-position-10" style="font-size: 11.694444444444pt;" aria-label="gamedev (28 элементов)" data-wpel-link="internal">gamedev</a>
<a href="https://otus.ru/journal/tag/hr/" class="tag-cloud-link tag-link-103 tag-link-position-11" style="font-size: 8pt;" aria-label="hr (11 элементов)" data-wpel-link="internal">hr</a>
<a href="https://otus.ru/journal/tag/html/" class="tag-cloud-link tag-link-217 tag-link-position-12" style="font-size: 11.208333333333pt;" aria-label="HTML (25 элементов)" data-wpel-link="internal">HTML</a>
<a href="https://otus.ru/journal/tag/ios/" class="tag-cloud-link tag-link-101 tag-link-position-13" style="font-size: 8.9722222222222pt;" aria-label="iOS (14 элементов)" data-wpel-link="internal">iOS</a>
<a href="https://otus.ru/journal/tag/it/" class="tag-cloud-link tag-link-50 tag-link-position-14" style="font-size: 10.527777777778pt;" aria-label="IT (21 элемент)" data-wpel-link="internal">IT</a>
<a href="https://otus.ru/journal/tag/java/" class="tag-cloud-link tag-link-75 tag-link-position-15" style="font-size: 15.680555555556pt;" aria-label="Java (73 элемента)" data-wpel-link="internal">Java</a>
<a href="https://otus.ru/journal/tag/javascript/" class="tag-cloud-link tag-link-83 tag-link-position-16" style="font-size: 14.319444444444pt;" aria-label="JavaScript (53 элемента)" data-wpel-link="internal">JavaScript</a>
<a href="https://otus.ru/journal/tag/linux/" class="tag-cloud-link tag-link-141 tag-link-position-17" style="font-size: 11.888888888889pt;" aria-label="Linux (29 элементов)" data-wpel-link="internal">Linux</a>
<a href="https://otus.ru/journal/tag/machine-learning/" class="tag-cloud-link tag-link-167 tag-link-position-18" style="font-size: 8.6805555555556pt;" aria-label="Machine Learning (13 элементов)" data-wpel-link="internal">Machine Learning</a>
<a href="https://otus.ru/journal/tag/otus-book/" class="tag-cloud-link tag-link-261 tag-link-position-19" style="font-size: 9.9444444444444pt;" aria-label="otus book (18 элементов)" data-wpel-link="internal">otus book</a>
<a href="https://otus.ru/journal/tag/php/" class="tag-cloud-link tag-link-45 tag-link-position-20" style="font-size: 10.527777777778pt;" aria-label="PHP (21 элемент)" data-wpel-link="internal">PHP</a>
<a href="https://otus.ru/journal/tag/python/" class="tag-cloud-link tag-link-27 tag-link-position-21" style="font-size: 16.944444444444pt;" aria-label="Python (99 элементов)" data-wpel-link="internal">Python</a>
<a href="https://otus.ru/journal/tag/qa/" class="tag-cloud-link tag-link-155 tag-link-position-22" style="font-size: 11.402777777778pt;" aria-label="qa (26 элементов)" data-wpel-link="internal">qa</a>
<a href="https://otus.ru/journal/tag/sql/" class="tag-cloud-link tag-link-38 tag-link-position-23" style="font-size: 12.861111111111pt;" aria-label="SQL (37 элементов)" data-wpel-link="internal">SQL</a>
<a href="https://otus.ru/journal/tag/team-lead/" class="tag-cloud-link tag-link-364 tag-link-position-24" style="font-size: 9.9444444444444pt;" aria-label="team lead (18 элементов)" data-wpel-link="internal">team lead</a>
<a href="https://otus.ru/journal/tag/unity/" class="tag-cloud-link tag-link-24 tag-link-position-25" style="font-size: 8pt;" aria-label="unity (11 элементов)" data-wpel-link="internal">unity</a>
<a href="https://otus.ru/journal/tag/algoritmy/" class="tag-cloud-link tag-link-30 tag-link-position-26" style="font-size: 9.9444444444444pt;" aria-label="Алгоритмы (18 элементов)" data-wpel-link="internal">Алгоритмы</a>
<a href="https://otus.ru/journal/tag/bazy-dannyh/" class="tag-cloud-link tag-link-40 tag-link-position-27" style="font-size: 10.138888888889pt;" aria-label="Базы данных (19 элементов)" data-wpel-link="internal">Базы данных</a>
<a href="https://otus.ru/journal/tag/matematika/" class="tag-cloud-link tag-link-44 tag-link-position-28" style="font-size: 10.916666666667pt;" aria-label="Математика (23 элемента)" data-wpel-link="internal">Математика</a>
<a href="https://otus.ru/journal/tag/arhitektura-po/" class="tag-cloud-link tag-link-10 tag-link-position-29" style="font-size: 9.4583333333333pt;" aria-label="архитектура ПО (16 элементов)" data-wpel-link="internal">архитектура ПО</a>
<a href="https://otus.ru/journal/tag/bazy-dannyh-2/" class="tag-cloud-link tag-link-251 tag-link-position-30" style="font-size: 10.138888888889pt;" aria-label="базы данных (19 элементов)" data-wpel-link="internal">базы данных</a>
<a href="https://otus.ru/journal/tag/vebinar/" class="tag-cloud-link tag-link-201 tag-link-position-31" style="font-size: 13.930555555556pt;" aria-label="вебинар (48 элементов)" data-wpel-link="internal">вебинар</a>
<a href="https://otus.ru/journal/tag/dajdzhest/" class="tag-cloud-link tag-link-308 tag-link-position-32" style="font-size: 10.722222222222pt;" aria-label="дайджест (22 элемента)" data-wpel-link="internal">дайджест</a>
<a href="https://otus.ru/journal/tag/zapis-vebinara/" class="tag-cloud-link tag-link-226 tag-link-position-33" style="font-size: 14.902777777778pt;" aria-label="запись вебинара (61 элемент)" data-wpel-link="internal">запись вебинара</a>
<a href="https://otus.ru/journal/tag/zapis-uroka/" class="tag-cloud-link tag-link-272 tag-link-position-34" style="font-size: 16.069444444444pt;" aria-label="запись урока (80 элементов)" data-wpel-link="internal">запись урока</a>
<a href="https://otus.ru/journal/tag/informacionnaya-bezopasnost/" class="tag-cloud-link tag-link-232 tag-link-position-35" style="font-size: 10.138888888889pt;" aria-label="информационная безопасность (19 элементов)" data-wpel-link="internal">информационная безопасность</a>
<a href="https://otus.ru/journal/tag/karera-v-it/" class="tag-cloud-link tag-link-292 tag-link-position-36" style="font-size: 9.9444444444444pt;" aria-label="карьера в IT (18 элементов)" data-wpel-link="internal">карьера в IT</a>
<a href="https://otus.ru/journal/tag/podborka/" class="tag-cloud-link tag-link-7 tag-link-position-37" style="font-size: 12.666666666667pt;" aria-label="подборка (35 элементов)" data-wpel-link="internal">подборка</a>
<a href="https://otus.ru/journal/tag/podborka-statej/" class="tag-cloud-link tag-link-219 tag-link-position-38" style="font-size: 15.777777777778pt;" aria-label="подборка статей (75 элементов)" data-wpel-link="internal">подборка статей</a>
<a href="https://otus.ru/journal/tag/programmirovanie/" class="tag-cloud-link tag-link-65 tag-link-position-39" style="font-size: 22pt;" aria-label="программирование (332 элемента)" data-wpel-link="internal">программирование</a>
<a href="https://otus.ru/journal/tag/proekt/" class="tag-cloud-link tag-link-321 tag-link-position-40" style="font-size: 11.888888888889pt;" aria-label="проект (29 элементов)" data-wpel-link="internal">проект</a>
<a href="https://otus.ru/journal/tag/proektnaya-rabota/" class="tag-cloud-link tag-link-310 tag-link-position-41" style="font-size: 11.597222222222pt;" aria-label="проектная работа (27 элементов)" data-wpel-link="internal">проектная работа</a>
<a href="https://otus.ru/journal/tag/seti/" class="tag-cloud-link tag-link-181 tag-link-position-42" style="font-size: 12.958333333333pt;" aria-label="сети (38 элементов)" data-wpel-link="internal">сети</a>
<a href="https://otus.ru/journal/tag/testirovanie/" class="tag-cloud-link tag-link-69 tag-link-position-43" style="font-size: 13.930555555556pt;" aria-label="тестирование (48 элементов)" data-wpel-link="internal">тестирование</a>
<a href="https://otus.ru/journal/tag/upravlenie-komandoj/" class="tag-cloud-link tag-link-63 tag-link-position-44" style="font-size: 11.694444444444pt;" aria-label="управление командой (28 элементов)" data-wpel-link="internal">управление командой</a>
<a href="https://otus.ru/journal/tag/habr-2/" class="tag-cloud-link tag-link-203 tag-link-position-45" style="font-size: 13.930555555556pt;" aria-label="хабр (48 элементов)" data-wpel-link="internal">хабр</a></div>
</li>
</ul>
</div>
</aside>
</div> <!-- .ts-row -->
</div> <!-- .main -->
<footer class="main-footer dark bold">
<section class="lower-footer cf">
<div class="wrap">
<div class="links">
<div class="menu-menju-navykov-container"><ul id="menu-menju-navykov-1" class="menu"><li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10413"><a href="https://otus.ru/categories/programming/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Программирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10414"><a href="https://otus.ru/categories/architecture/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Архитектура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10415"><a href="https://otus.ru/categories/operations/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Инфраструктура<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10416"><a href="https://otus.ru/categories/information-security-courses/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Безопасность<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10417"><a href="https://otus.ru/categories/data-science/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Data Science<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10418"><a href="https://otus.ru/categories/gamedev/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">GameDev<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10419"><a href="https://otus.ru/categories/marketing-business/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Управление<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10420"><a href="https://otus.ru/categories/analytics/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Аналитика и анализ<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
<li class="menu-item menu-item-type-custom menu-item-object-custom menu-item-10421"><a href="https://otus.ru/categories/testing/" data-wpel-link="external" target="_blank" rel="nofollow external noopener noreferrer" class="wpel-icon-right">Тестирование<span class="wpel-icon wpel-image wpel-icon-6"></span></a></li>
</ul></div> </div>
<p class="copyright"> © 2015-2026 OTUS </p>
<div class="to-top">
<a href="#" class="back-to-top"><i class="fa fa-angle-up"></i> Top</a>
</div>
</div>
</section>
</footer>
</div> <!-- .main-wrap -->
<div class="mobile-menu-container off-canvas" id="mobile-menu">
<a href="#" class="close"><i class="fa fa-times"></i></a>
<div class="logo">
</div>
<ul class="mobile-menu"></ul>
</div>
<div class="search-modal-wrap">
<div class="search-modal-box" role="dialog" aria-modal="true">
<form method="get" class="search-form" action="https://otus.ru/journal/">
<input type="search" class="search-field" name="s" placeholder="Search..." value="" required />
<button type="submit" class="search-submit visuallyhidden">Submit</button>
<p class="message">
Type above and press <em>Enter</em> to search. Press <em>Esc</em> to cancel. </p>
</form>
</div>
</div>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/clearfy/components/comments-plus/assets/js/url-span.js" id="wbcr-comments-plus-url-span-js"></script>
<script type="text/javascript" id="ez-toc-scroll-scriptjs-js-extra">
/* <![CDATA[ */
var eztoc_smooth_local = {"scroll_offset":"30"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/js/smooth_scroll.min.js" id="ez-toc-scroll-scriptjs-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/vendor/js-cookie/js.cookie.min.js" id="ez-toc-js-cookie-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/vendor/sticky-kit/jquery.sticky-kit.min.js" id="ez-toc-jquery-sticky-kit-js"></script>
<script type="text/javascript" id="ez-toc-js-js-extra">
/* <![CDATA[ */
var ezTOC = {"smooth_scroll":"1","visibility_hide_by_default":"","scroll_offset":"30","fallbackIcon":"<span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span>"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/plugins/easy-table-of-contents/assets/js/front.min.js" id="ez-toc-js-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/custom-script.js" id="custom-script-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/magnific-popup.js" id="magnific-popup-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jquery.fitvids.js" id="jquery-fitvids-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/imagesloaded.min.js" id="imagesloaded-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/object-fit-images.js" id="object-fit-images-js"></script>
<script type="text/javascript" id="contentberg-theme-js-extra">
/* <![CDATA[ */
var Bunyad = {"custom_ajax_url":"\/journal\/zombi-shuter-na-dots-v-unity\/"};
/* ]]> */
</script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/theme.js" id="contentberg-theme-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/theia-sticky-sidebar.js" id="theia-sticky-sidebar-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jquery.slick.js" id="jquery-slick-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-content/themes/contentberg/js/jarallax.js" id="jarallax-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/masonry.min.js" id="masonry-js"></script>
<script type="text/javascript" src="https://otus.ru/journal/wp-includes/js/jquery/jquery.masonry.min.js" id="jquery-masonry-js"></script>
</body>
</html>
<!-- Cache served by breeze CACHE - Last modified: Mon, 09 Mar 2026 16:01:56 GMT -->