Моделирование гравитации с помощью Unity DOTS 1.0

Моделирование гравитации с помощью Unity DOTS 1.0

3 января 2023 г.

С выпуском Unity 2022.2 пакеты DOTS наконец-то получили предварительную версию. Как вы знаете, в стандартном подходе Unity доступ к игровым объектам и MonoBehaviour возможен только из основного потока. Одним из основных преимуществ DOTS является возможность легкого доступа к многопоточным параллельным вычислениям, что значительно повышает производительность.

Эта статья предназначена для тех, кто уже имеет некоторые базовые знания о Unity DOTS, но хочет узнать больше и увидеть примеры его практического использования. В этой статье не будет подробного описания ECS или фундаментальных концепций DOTS. Тем не менее, он все равно будет полезен тем, кто предпочитает учиться на примерах.

Чтобы упростить понимание кода, я рекомендую скачать готовый проект по этой ссылке.

Что нам нужно сделать?

Я хочу создать симуляцию гравитации для нескольких небесных тел. Концепция проста. На сцене будет большое количество небесных тел, и каждое тело будет притягивать все остальные тела. Проблема в том, что количество взаимодействий между небесными телами увеличивается экспоненциально с увеличением количества тел. Например, с 10 небесными телами будет 10 х 10 = 100 взаимодействий. Со 100 небесными телами будет 100 х 100 = 10 000 взаимодействий. А с 1000 небесных тел будет 1 000 000 взаимодействий. Именно здесь параллельные вычисления становятся особенно полезными.

Согласно подходу ECS, ядром небесного тела будет объект. Сущность можно описать с помощью компонентов, а затем мы создадим системы, которые будут обрабатывать эти компоненты для прямого моделирования гравитации.

Добавьте необходимые компоненты

Сила гравитации рассчитывается по формуле F = G * m1 * m2 / r^2, где G — гравитационная постоянная, позволяющая регулировать силу притяжения, m1 и m2 — массы взаимодействующих небесных тел. тел, r — расстояние между телами. Следовательно, объекту необходимы следующие характеристики: масса, положение и скорость.

В пакете Unity Physics уже есть компоненты LocalTransform для установки положения и PhysicsVelocity для установки скорости. Еще есть компонент PhysicsMass, но у него слишком много ненужных для этой задачи параметров, поэтому давайте создадим свой простой компонент Mass.

Существует несколько типов компонентов, о которых вы можете узнать подробнее здесь. Когда небесные тела сталкиваются, они объединяются, и характеристики одного добавляются к другому. Затем оставшееся небесное тело будет удалено. Однако удаление сущности может вызвать реструктуризацию памяти, что может привести к зависаниям. Чтобы этого избежать, я просто отключу ненужные объекты. Давайте настроим компонент Mass как активируемый.

public struct Mass : IComponentData, IEnableableComponent
{
    public float Value;
}

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

Наложим ограничение — для каждого объекта мы будем регистрировать только одно столкновение за шаг симуляции. Для этого создадим компонент Collision, в котором будет указана сущность объекта, с которым произошло столкновение.

public struct Collision : IComponentData
{
    public Entity Entity;
}

n Теперь у нас есть все необходимое для создания небесного тела с гравитационными свойствами.

Сборные элементы для тел

Теперь нам нужно создать префаб для небесного тела. Для этого создайте на сцене сферу и перетащите ее на вкладку проекта. Убедитесь, что у сферы есть коллайдер сферы.

Давайте напишем компонент, который будет преобразовывать этот префаб в сущность. Мы назовем это Авторингом тела. Для преобразования нам нужно создать класс, наследуемый от Baker. Для простоты класс можно поместить внутрь MonoBehaviour, но это не обязательно.

Внутри класса Baker мы добавим необходимые компоненты. Компонент LocalTransform

добавляться автоматически, конвертируя из стандартного Transform. Нам просто нужно добавить компоненты Mass, PhysicsVelocity и Collision.

public class BodyAuthoring : MonoBehaviour
{
    public class Baker : Baker<BodyAuthoring>
    {
        public override void Bake(BodyAuthoring authoring)
        {
            AddComponent<Mass>();
            AddComponent<PhysicsVelocity>();
            AddComponent<Collision>();
        }
    }
}

Не забудьте прикрепить компонент Body Authoring к префабу.

Инициализация системы

На старте нам нужно создать объекты с начальными параметрами, которые потом будут взаимодействовать друг с другом. Давайте создадим компонент WorldConfig, который будет содержать эти исходные параметры.

[Serializable]
public struct WorldConfig : IComponentData
{
    /// <summary>
    /// Initial count of bodies.
    /// </summary>
    public int BodyCount;

    /// <summary>
    /// The radius of the sphere within which the bodies will be created during initialization.
    /// </summary>
    public float WorldRadius;

    /// <summary>
    /// The range of initial speed of bodies.
    /// </summary>
    public float2 StartSpeedRange;

    /// <summary>
    /// The range of initial mass of bodies.
    /// </summary>
    public float2 StartMassRange;

    /// <summary>
    /// The coefficient of increasing the gravitational power.
    /// </summary>
    public float GravitationalConstant;

    /// <summary>
    /// The zero distance between objects can lead to infinite force. To avoid the occurrence of extremely large forces
    /// during the calculation, add a restriction on the minimum distance.
    /// </summary>
    public float MinDistanceToAttract;

    /// <summary>
    /// The ratio of the body mass to its size. So objects with different masses have different sizes.
    /// </summary>
    public float MassToScaleRatio;

    /// <summary>
    /// The coefficient that will determine how close the objects should be to each other to make the collision.
    /// </summary>
    public float CollisionFactor;
}

Я добавил [Serializable], чтобы потом этот компонент можно было поместить в MonoBehaviour и все эти параметры можно было задавать прямо в инспекторе. Я также добавил описания для каждого параметра в комментариях. Значения каждого параметра будут становиться яснее по мере их применения.

Далее мы создадим компонент, который будет содержать префаб нашего объекта. Это просто.

public struct BodyPrefab : IComponentData
{
    public Entity Value;
}

Напишем компонент WorldAuthoring, чтобы можно было выставить все эти значения через инспектор. Это похоже на BodyAuthoring.

public class WorldAuthoring : MonoBehaviour
{
    public GameObject BodyPrefab;
    public WorldConfig WorldConfig;

    public class WorldConfigBaker : Baker<WorldAuthoring>
    {
        public override void Bake(WorldAuthoring authoring)
        {
            AddComponent(new BodyPrefab
            {
                Value = GetEntity(authoring.BodyPrefab)
            });

            AddComponent(authoring.WorldConfig);
        }
    }
}

Далее мы создадим основу для системы инициализации.

[BurstCompile]
[UpdateInGroup(typeof(InitializationSystemGroup), OrderLast = true)]
public partial struct InitializingSystem : ISystem
{
    public void OnCreate(ref SystemState state) { }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        try
        {
            InitializeWorld(ref state);
        }
        finally
        {
            state.Enabled = false;
        }
    }
}

Я перенес всю логику инициализации в метод InitializeWorld(). Инициализация должна произойти только один раз, а затем нам нужно отключить эту систему. Если во время инициализации возникает исключение, система все равно должна завершить работу. Чтобы убедиться в этом, я обернул InitializeWorld() в блок try и добавил завершение работы системы в блок finally.

Далее приступим к самой инициализации. Для этого требуется доступ к WorldConfig и BodyPrefab.

var worldConfig = SystemAPI.GetSingleton<WorldConfig>();
var bodyPrefab = SystemAPI.GetSingleton<BodyPrefab>();

Теперь мы создадим желаемое количество объектов и префабов.

state.EntityManager.Instantiate(bodyPrefab.Value, worldConfig.BodyCount, Allocator.Temp);

Мы заранее вычислим квадратный корень из радиуса сферы, внутри которой будут создаваться объекты. Это значение понадобится нам позже.

var sqrtWorldRadius = math.pow(worldConfig.WorldRadius, 1f/3f);

Теперь нам нужно задать начальные параметры для созданных объектов. Это можно сделать параллельно с помощью задания инициализации. Существует несколько типов заданий, о которых вы можете узнать больше здесь: https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/scheduling-jobs-extensions.html. В данном случае удобнее всего использовать IJobEntity, что позволяет указать в методе Execute следующие параметры:

* [EntityIndexInQuery] int index - индекс сущности, * Entity entity - сама сущность, * refreadWriteComponent — компонент сущности для чтения и записи, * в readOnlyComponent — компонент только для чтения.

Нам понадобятся только следующие аргументы.

[BurstCompile]
public partial struct InitializingJob : IJobEntity
{
    [BurstCompile]
    private void Execute(
        [EntityIndexInQuery] int index,
        ref LocalTransform transform,
        ref Mass mass,
        ref PhysicsVelocity velocity)
    {
        // TODO: write the initialization of a body.
    }
}

Добавим в структуру поля WorldConfig, SqrtWorldRadius и MassToScaleRatio, которые будут содержать необходимые данные для инициализации каждого объекта.

[BurstCompile]
public partial struct InitializingJob : IJobEntity
{
    public WorldConfig WorldConfig;
    public float SqrtWorldRadius;
    public float MassToScaleRatio;

    [BurstCompile]
    private void Execute(
        [EntityIndexInQuery] int index,
        ref LocalTransform transform,
        ref Mass mass,
        ref PhysicsVelocity velocity)
    {
       // TODO: write the initialization of a body.
    }
}

Теперь давайте напишем фактическую логику инициализации.

Для инициализации Random мы будем использовать индекс сущности. Поскольку 0 нельзя использовать для инициализации, мы добавим 1.

var random = new Random();
random.InitState((uint)index + 1u);

Зададим случайную массу.

mass.Value = random.NextFloat(WorldConfig.StartMassRange.x, WorldConfig.StartMassRange.y);

Напишем утилиту для преобразования массы в масштаб. Я объясню его назначение более подробно позже, чтобы не отвлекаться.

public struct Scaling
{
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static float MassToScale(float mass, float massToScaleRatio)
    {
        return math.pow(6 * mass / math.PI, 1/3f) * massToScaleRatio;
    }
}

Теперь мы установим масштаб объекта на основе его массы, что позволит настроить размер небесного тела.

transform.Scale = Scaling.MassToScale(mass.Value, MassToScaleRatio);

Создание равномерного случайного распределения точек внутри сферы — сложная тема, в которую нам не нужно глубоко вникать. Площадь сферы рассчитывается как S = 4 * Pi * R ^ 2, что означает, что по мере увеличения радиуса площадь увеличивается экспоненциально. Чтобы количество объектов также увеличивалось экспоненциально по мере удаления от центра сферы, мы соответствующим образом скорректируем количество объектов.

var radius = WorldConfig.WorldRadius - math.pow(random.NextFloat(SqrtWorldRadius), 2);

К счастью, в классе Random уже есть метод nextFloat3Direction(), который возвращает случайную точку на сфере единичного радиуса. Это позволяет нам установить положение небесного тела следующим образом.

var position = random.NextFloat3Direction() * radius;
transform.Position = position;

Теперь зададим случайную скорость. Процесс установки скорости стандартный.

var speed = random.NextFloat(WorldConfig.StartSpeedRange.x, WorldConfig.StartSpeedRange.y);

Однако мы установим направление таким образом, чтобы объекты вращались вокруг сферы. Для этого мы просто поворачиваем вектор положения и устанавливаем значение вертикальной скорости (y).

var direction = math.normalize(new float3(-position.z, position.y / 2, position.x));

Наконец, мы устанавливаем скорость напрямую.

velocity.Linear = speed * direction;

В итоге мы получили следующую структуру.

[BurstCompile]
public partial struct InitializingJob : IJobEntity
{
    public WorldConfig WorldConfig;
    public float SqrtWorldRadius;
    public float MassToScaleRatio;

    [BurstCompile]
    private void Execute(
        [EntityIndexInQuery] int index,
        ref LocalTransform transform,
        ref Mass mass,
        ref PhysicsVelocity velocity)
    {
        var random = new Random();
        random.InitState((uint)index + 1u);

        // Set random mass and scale (size).
        mass.Value = random.NextFloat(WorldConfig.StartMassRange.x, WorldConfig.StartMassRange.y);
        transform.Scale = Scaling.MassToScale(mass.Value, MassToScaleRatio);

        // Set a random position.
        var radius = WorldConfig.WorldRadius - math.pow(random.NextFloat(SqrtWorldRadius), 2);
        var position = random.NextFloat3Direction() * radius;
        transform.Position = position;

        // Set random velocity.
        var speed = random.NextFloat(WorldConfig.StartSpeedRange.x, WorldConfig.StartSpeedRange.y);
        var direction = math.normalize(new float3(-position.z, position.y / 2, position.x));
        velocity.Linear = speed * direction;
    }
}

Мы напишем инициализацию только что созданного задания и запланируем его параллельное выполнение в системе инициализации.

[BurstCompile]
[UpdateInGroup(typeof(InitializationSystemGroup), OrderLast = true)]
public partial struct InitializingSystem : ISystem
{
    public void OnCreate(ref SystemState state) { }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        try
        {
            InitializeWorld(ref state);
        }
        finally
        {
            state.Enabled = false;
        }
    }

    [BurstCompile]
    private void InitializeWorld(ref SystemState state)
    {
        var worldConfig = SystemAPI.GetSingleton<WorldConfig>();
        var bodyPrefab = SystemAPI.GetSingleton<BodyPrefab>();
        state.EntityManager.Instantiate(bodyPrefab.Value, worldConfig.BodyCount, Allocator.Temp);
        var sqrtWorldRadius = math.pow(worldConfig.WorldRadius, 1f/3f);

        var job = new InitializingJob
        {
            WorldConfig = worldConfig,
            SqrtWorldRadius = sqrtWorldRadius,
            MassToScaleRatio = worldConfig.MassToScaleRatio
        };

        job.ScheduleParallel();
    }
}

На этом первая часть завершена. При запуске приложения небесные тела создаются в заданном радиусе и начинают разлетаться в разные стороны. Выглядит хорошо.

Привлекательная система

Давайте создадим Вакансию для привлечения. Каждое небесное тело в системе должно взаимодействовать со всеми остальными. Мы добавим в эту структуру следующие поля: массивы со всеми массами, позициями и сущностями всех объектов на сцене. Это будут Native Arrays, которые представляют собой коллекции, не выделяющие место в управляемой куче. Информация об объектах коллекций для них хранится в неуправляемой куче, а сами они представляют собой структуры.

Мы также добавим другие поля со значениями, необходимыми для расчета взаимодействия. В коде Execute() мы добавим только индекс, чтобы понять, с какой сущностью мы сейчас работаем, PhysicsVelocity, чтобы записать новую скорость, и Collision, чтобы зарегистрировать столкновение только для чтения с Mass, поскольку столкновения будут обрабатываться в другой системе. .

Результирующий кадр выглядит так:

[BurstCompile]
public partial struct AttractingJob : IJobEntity
{
    [ReadOnly] public int BodyCount;
    [ReadOnly] public NativeArray<Mass> Masses;
    [ReadOnly] public NativeArray<LocalTransform> Transforms;
    [ReadOnly] public NativeArray<Entity> Entities;
    [ReadOnly] public float DeltaTime;
    [ReadOnly] public float MinDistance;
    [ReadOnly] public float GravitationalConstant;
    [ReadOnly] public float CollisionFactor;

    [BurstCompile]
    private void Execute(
        [EntityIndexInQuery] int index,
        ref PhysicsVelocity velocity,
        ref Collision collision,
        in Mass mass)
    { 
        // TODO: write interactions.
    }
}

Далее мы создадим переменную для записи суммарной силы притяжения от других объектов:

var force = float3.zero;

Для удобства получим позицию обрабатываемого объекта:

var position = Transforms[index].Position;

Мы сбросим значение столкновения:

collision = new Collision();

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

var hasCollision = false;

Мы создадим основу для цикла, в котором мы будем перебирать все остальные объекты:

for (var i = 0; i < BodyCount; i++)
{

}

Каждый раз в цикле мы проверяем, что текущий объект не совпадает с обрабатываемым:

if (index == i)
{
    continue;
}

Рассчитываем расстояние между объектами и проверяем, чтобы расстояние было не меньше допустимого минимума для расчета:

var distance = math.distance(position, Transforms[i].Position);
var permittedDistance = math.max(distance, MinDistance);

Вычисляем гравитационную силу от объекта и прибавляем к остальным:

force += GravitationalConstant * mass.Value * Masses[i].Value * (Transforms[i].Position - position)
/ (permittedDistance * permittedDistance * permittedDistance);

Далее мы выполняем проверку на столкновение. Мы также проверяем объекты, индекс которых больше текущего, так как объекты с меньшим индексом, возможно, уже сделали эту проверку. Мы перенесем сами вычисления в другой метод Hash Collision(). Если происходит столкновение, мы регистрируем его:

if (hasCollision || i < index || !HasCollision(distance, index, i))
{
    continue;
}

hasCollision = true;
collision.Entity = Entities[i];

После прохождения всех объектов фиксируем изменение текущей скорости объекта.

velocity.Linear += DeltaTime / mass.Value * force;

В результате был получен следующий метод:

[BurstCompile]
private void Execute(
    [EntityIndexInQuery] int index,
    ref PhysicsVelocity velocity,
    ref Collision collision,
    in Mass mass)
{
    var force = float3.zero;
    var position = Transforms[index].Position;
    collision = new Collision();
    var hasCollision = false;

    for (var i = 0; i < BodyCount; i++)
    {
        if (index == i)
        {
            continue;
        }

        var distance = math.distance(position, Transforms[i].Position);
        var permittedDistance = math.max(distance, MinDistance);

        force += GravitationalConstant * mass.Value * Masses[i].Value * (Transforms[i].Position - position) 
                 / (permittedDistance * permittedDistance * permittedDistance);

        if (hasCollision || i < index || !HasCollision(distance, index, i))
        {
            continue;
        }

        hasCollision = true;
        collision.Entity = Entities[i];
    }

    velocity.Linear += DeltaTime / mass.Value * force;
}

Теперь давайте напишем метод для обнаружения столкновений. Я не хочу тратить на это слишком много времени.

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private bool HasCollision(float distance, int i, int j)
{
    var leftScale = Transforms[i].Scale;
    var rightScale = Transforms[j].Scale;

    if (distance < (leftScale + rightScale) * CollisionFactor)
    {
        return true;
    }

    var (maj, min) = leftScale > rightScale ? (leftScale, rightScale) : (rightScale, leftScale);
    return distance < maj / 2 - min;
}

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

[BurstCompile]
public partial struct AttractingJob : IJobEntity
{
    [ReadOnly] public int BodyCount;
    [ReadOnly] public NativeArray<Mass> Masses;
    [ReadOnly] public NativeArray<LocalTransform> Transforms;
    [ReadOnly] public NativeArray<Entity> Entities;
    [ReadOnly] public float DeltaTime;
    [ReadOnly] public float MinDistance;
    [ReadOnly] public float GravitationalConstant;
    [ReadOnly] public float CollisionFactor;

    [BurstCompile]
    private void Execute(
        [EntityIndexInQuery] int index,
        ref PhysicsVelocity velocity,
        ref Collision collision,
        in Mass mass)
    {
        var force = float3.zero;
        var position = Transforms[index].Position;
        collision = new Collision();
        var hasCollision = false;

        for (var i = 0; i < BodyCount; i++)
        {
            if (index == i)
            {
                continue;
            }

            var distance = math.distance(position, Transforms[i].Position);
            var permittedDistance = math.max(distance, MinDistance);

            force += GravitationalConstant * mass.Value * Masses[i].Value * (Transforms[i].Position - position) 
                     / (permittedDistance * permittedDistance * permittedDistance);

            if (hasCollision || i < index || !HasCollision(distance, index, i))
            {
                continue;
            }

            hasCollision = true;
            collision.Entity = Entities[i];
        }

        velocity.Linear += DeltaTime / mass.Value * force;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private bool HasCollision(float distance, int i, int j)
    {
        var leftScale = Transforms[i].Scale;
        var rightScale = Transforms[j].Scale;

        if (distance < (leftScale + rightScale) * CollisionFactor)
        {
            return true;
        }

        var (maj, min) = leftScale > rightScale ? (leftScale, rightScale) : (rightScale, leftScale);
        return distance < maj / 2 - min;
    }
}

Теперь напишем саму систему, которая будет заполнять и запускать этот Job. Для получения массивов с данными об объектах нам понадобится запрос. Добавьте его в поле и инициализируйте при старте системы. Также после завершения Работы мы должны завершить массаж. У нас получается такая система.

[BurstCompile]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
public partial struct AttractingSystem : ISystem
{
    private EntityQuery _query;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _query = new EntityQueryBuilder(Allocator.TempJob)
            .WithAll<LocalTransform, Mass>()
            .Build(ref state);
    }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var worldConfig = SystemAPI.GetSingleton<WorldConfig>();
        var masses = _query.ToComponentDataArray<Mass>(Allocator.TempJob);
        var transforms = _query.ToComponentDataArray<LocalTransform>(Allocator.TempJob);
        var entities = _query.ToEntityArray(Allocator.TempJob);

        var job = new AttractingJob
        {
            BodyCount = _query.CalculateEntityCount(),
            Masses = masses,
            Transforms = transforms,
            Entities = entities,
            DeltaTime = SystemAPI.Time.fixedDeltaTime,
            MinDistance = worldConfig.MinDistanceToAttract,
            GravitationalConstant = worldConfig.GravitationalConstant,
            CollisionFactor = worldConfig.CollisionFactor
        };

        job.ScheduleParallel();
        state.Dependency.Complete();
        masses.Dispose();
        transforms.Dispose();
    }
}

Теперь мы не только создаем объекты, но и притягиваем друг друга. Однако из-за отсутствия обработки столкновений объекты просто роятся друг вокруг друга.

Система обработки столкновений

Теперь вам нужно создать систему, которая будет обрабатывать зарегистрированные столкновения между небесными телами. Характеристики двух сталкивающихся небесных тел будут объединены в один объект, а второе небесное тело мы деактивируем.

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

Чтобы получить доступ к компонентам, принадлежащим другим небесным телам в этой системе, мы можем использовать ComponentLookup. Нам потребуется доступ к массе, скорости и положению небесных тел.

private ComponentLookup<Mass> _massLookup;
private ComponentLookup<LocalTransform> _transformLookup;
private ComponentLookup<PhysicsVelocity> _velocityLookup;

Теперь инициализируйте поля данных при запуске системы.

_massLookup = SystemAPI.GetComponentLookup<Mass>();
_transformLookup = SystemAPI.GetComponentLookup<LocalTransform>();
_velocityLookup = SystemAPI.GetComponentLookup<PhysicsVelocity>();

На данный момент мы получили такой класс.

[BurstCompile]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(AttractingSystem))]
public partial struct CollisionHandlingSystem : ISystem
{
    private ComponentLookup<Mass> _massLookup;
    private ComponentLookup<LocalTransform> _transformLookup;
    private ComponentLookup<PhysicsVelocity> _velocityLookup;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _massLookup = SystemAPI.GetComponentLookup<Mass>();
        _transformLookup = SystemAPI.GetComponentLookup<LocalTransform>();
        _velocityLookup = SystemAPI.GetComponentLookup<PhysicsVelocity>();
    }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        // TODO: Handle collisions.
    }
}

Теперь всю обработку мы будем выполнять в методе OnUpdate(). Во-первых, вам нужно обновить объекты ComponentLookup.

_massLookup.Update(ref state);
_transformLookup.Update(ref state);
_velocityLookup.Update(ref state);

Мы также получим параметр MassToScaleRatio, потому что нам нужно будет изменить большое количество объектов.

var massToScaleRatio = SystemAPI.GetSingleton<WorldConfig>().MassToScaleRatio;

Мы будем перебирать объекты с помощью цикла foreach, который мы получим с помощью SystemAPI.Query().

foreach (var (velocityRef, massRef, transformRef, collisionsRef) 
         in SystemAPI.Query<RefRW<PhysicsVelocity>, RefRW<Mass>, RefRW<LocalTransform>, RefRO<Collision>>())
{
    // TODO: Handle the body collision.
}

В случае столкновения мы перенесем массу одного объекта на другой. Если объект имеет нулевую массу во время итерации, это означает, что он уже был поглощен другим объектом. Если столкновения нет, мы перейдем к следующему объекту.

var anotherBodyEntity = collisionsRef.ValueRO.Entity;
var m1 = massRef.ValueRO.Value;

if (m1 <= 0 || anotherBodyEntity == Entity.Null)
{
    continue;
}

Если масса второго объекта равна нулю, значит, он уже столкнулся с другим небесным телом. В этом случае мы не будем обрабатывать коллизию.

var anotherMassRef = _massLookup.GetRefRW(anotherBodyEntity, false);
var m2 = anotherMassRef.ValueRO.Value;

if (m2 <= 0)
{
    continue;
}

Далее мы объединим массы, скорости и положения двух объектов в один объект. Мы обнулим массу второго объекта. Чем больше масса объекта, тем большее влияние он окажет на конечный результат.

var x1 = transformRef.ValueRO.Position;
var v1 = velocityRef.ValueRO.Linear;
var x2 = _transformLookup.GetRefRO(anotherBodyEntity).ValueRO.Position;
var v2 = _velocityLookup.GetRefRO(anotherBodyEntity).ValueRO.Linear;

x1 = (x1 * m1 + x2 * m2) / (m1 + m2);
v1 = (v1 * m1 + v2 * m2) / (m1 + m2);
m1 += m2;
anotherMassRef.ValueRW.Value = 0;

velocityRef.ValueRW.Linear = v1;
massRef.ValueRW.Value = m1;

transformRef.ValueRW = new LocalTransform
{
    Position = x1,
    Rotation = quaternion.identity,
    Scale = Scaling.MassToScale(m1, massToScaleRatio)
};

У нас получился такой класс.

[BurstCompile]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(AttractingSystem))]
public partial struct CollisionHandlingSystem : ISystem
{
    private ComponentLookup<Mass> _massLookup;
    private ComponentLookup<LocalTransform> _transformLookup;
    private ComponentLookup<PhysicsVelocity> _velocityLookup;

    [BurstCompile]
    public void OnCreate(ref SystemState state)
    {
        _massLookup = SystemAPI.GetComponentLookup<Mass>();
        _transformLookup = SystemAPI.GetComponentLookup<LocalTransform>();
        _velocityLookup = SystemAPI.GetComponentLookup<PhysicsVelocity>();
    }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        _massLookup.Update(ref state);
        _transformLookup.Update(ref state);
        _velocityLookup.Update(ref state);
        var massToScaleRatio = SystemAPI.GetSingleton<WorldConfig>().MassToScaleRatio;

        foreach (var (velocityRef, massRef, transformRef, collisionsRef) 
                 in SystemAPI.Query<RefRW<PhysicsVelocity>, RefRW<Mass>, RefRW<LocalTransform>, RefRO<Collision>>())
        {
            var anotherBodyEntity = collisionsRef.ValueRO.Entity;
            var m1 = massRef.ValueRO.Value;

            if (m1 <= 0 || anotherBodyEntity == Entity.Null)
            {
                continue;
            }

            var anotherMassRef = _massLookup.GetRefRW(anotherBodyEntity, false);
            var m2 = anotherMassRef.ValueRO.Value;

            if (m2 <= 0)
            {
                continue;
            }

            var x1 = transformRef.ValueRO.Position;
            var v1 = velocityRef.ValueRO.Linear;
            var x2 = _transformLookup.GetRefRO(anotherBodyEntity).ValueRO.Position;
            var v2 = _velocityLookup.GetRefRO(anotherBodyEntity).ValueRO.Linear;

            x1 = (x1 * m1 + x2 * m2) / (m1 + m2);
            v1 = (v1 * m1 + v2 * m2) / (m1 + m2);
            m1 += m2;
            anotherMassRef.ValueRW.Value = 0;

            velocityRef.ValueRW.Linear = v1;
            massRef.ValueRW.Value = m1;

            transformRef.ValueRW = new LocalTransform
            {
                Position = x1,
                Rotation = quaternion.identity,
                Scale = Scaling.MassToScale(m1, massToScaleRatio)
            };
        }
    }
}

Система отключения тела

Эта система довольно проста. Мы напишем задание, которое отключит объект с нулевой массой.

[BurstCompile]
public partial struct BodyDisablingJob : IJobEntity
{
    public EntityCommandBuffer.ParallelWriter CommandBuffer;

    [BurstCompile]
    private void Execute([EntityIndexInQuery] int index, in Entity entity, in Mass mass)
    {
        if (mass.Value <= 0)
        {
            CommandBuffer.SetComponentEnabled<Mass>(index, entity, false);
        }
    }
}

Мы создадим систему, которая будет планировать это задание.

[BurstCompile]
[UpdateInGroup(typeof(FixedStepSimulationSystemGroup))]
[UpdateAfter(typeof(CollisionHandlingSystem))]
public partial struct BodyDisablingSystem : ISystem
{
    public void OnCreate(ref SystemState state) { }

    public void OnDestroy(ref SystemState state) { }

    [BurstCompile]
    public void OnUpdate(ref SystemState state)
    {
        var cbs = SystemAPI.GetSingleton<BeginSimulationEntityCommandBufferSystem.Singleton>();
        var commandBuffer = cbs.CreateCommandBuffer(state.WorldUnmanaged).AsParallelWriter();

        var job = new BodyDisablingJob
        {
            CommandBuffer = commandBuffer
        };

        job.ScheduleParallel();
    }
}

Заключение

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


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE