How to get Child non-uniform transfrom via Aspect (n00b)

Hey all,

TLDR; If I have an aspect that gets an objects Entity ref, LocalTransform and empty TagComponent, how can I get the non-uniform transform for it’s child object for updating the position, rotation and scale?

First off, I’m find it very hard to absorb info through reading alone - I have to get hands on to get the intuition of something.

So, expanding Unity’s Jobs example from their GitHub, I’m replicating the functionality to work through ECS as I learn more about the overall paradigm.

My latest problem is ‘drawing’ the lines between the blue and red cubes. Line Renderers as, AFAIK, not in ECS yet, so I’m going to fudge it with a stretched quad.

I’m currently using an Aspect (to learn about those) with a passed sorted NativeArray for the “FindNearestRedCube” system.

Current Performance: So far I have 5k red and 5k blue cubes (with ‘line’ quads) running at approx. 60fps with distance checks

Current Code

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Unity.Entities;
using Unity.Burst;
using Unity.Collections;
using Unity.Transforms;
using Unity.Mathematics;
using System.Linq;
using Unity.Jobs;
using System;

public partial struct FindNearestSystem : ISystem
{
    [BurstCompile]
    public void OnCreate(ref SystemState _state)
    {
        _state.RequireForUpdate<BlueCubeComponentTag>();
        _state.RequireForUpdate<RedCubeComponentTag>();
    }

    //[BurstCompile] - Can't Burst this as errors: "Reflection data was not set up by an Initialize() call. Support for 
    // burst compiled calls to Schedule depends on the Collections package."
    public void OnUpdate(ref SystemState _state)
    {
        //1......................................
        // Get the positions of all the red tags.
        int _queryLength = 0;
        foreach (var _lt in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<RedCubeComponentTag>())
            _queryLength++;

        NativeArray<float3> _positions = new NativeArray<float3>(_queryLength, Allocator.TempJob);

        int _naIndex = 0;
        foreach (var _lt in SystemAPI.Query<RefRO<LocalTransform>>().WithAll<RedCubeComponentTag>())
            _positions[_naIndex++] = _lt.ValueRO.Position;

        //2......................................
        // Sort and merge the list
        SortJob<float3, AxisXComparer> _sortJob = _positions.SortJob(new AxisXComparer { });
        JobHandle _sortJobHandle = _sortJob.Schedule();

        // Run an entity aspect on all blue cubes with the positions
        JobHandle _findJobHandle = new FindAndDrawToNearestJob
        {
            Positions = _positions
        }.ScheduleParallel<FindAndDrawToNearestJob>(_sortJobHandle);

        _findJobHandle.Complete();

        // Dispose of all that was needed
        _positions.Dispose();
    }

    [BurstCompile]
    partial struct FindAndDrawToNearestJob : IJobEntity
    {
        [ReadOnly] public NativeArray<float3> Positions;

        public void Execute(BlueCubeFindNearestAspect _aspect)
        {
            _aspect.FindNearest(Positions);
        }
    }

    [BurstCompile]
    readonly public partial struct BlueCubeFindNearestAspect : IAspect
    {
        public readonly Entity Self;
        public readonly RefRO<LocalTransform> Transform;
        public readonly RefRO<BlueCubeComponentTag> BlueTag;

        public void FindNearest(in NativeArray<float3> _redPositions)
        {
            // Get closest target pos...

            // Try to get exact position;
            int _startIndex = _redPositions.BinarySearch(Transform.ValueRO.Position, new AxisXComparer { });

            // if <1, then exact not found.  Therefore, flip bits to reveal closest ;)
            if (_startIndex < 0) _startIndex = ~_startIndex;
            if (_startIndex >= _redPositions.Length) _startIndex = _redPositions.Length - 1; // Clamp to stop OOB

            if (_startIndex < 0)
            {
                Debug.LogError("The index is -1");
                return;
            }

            float3 _nearestPos = _redPositions[_startIndex];
            float _nearestDistSqrd = math.distancesq(_nearestPos, Transform.ValueRO.Position);

            // Search up, then down for the nearest position;
            //  Upwards:
            for (int i = _startIndex; i < _redPositions.Length; i++)
            {
                float _xDiff = Transform.ValueRO.Position.x - _redPositions[i].x;

                if ((_xDiff * _xDiff) > _nearestDistSqrd) break; // We're going beyond our current min range, so bug out

                float _newDistSq = math.distancesq(_redPositions[i], Transform.ValueRO.Position);

                if (_newDistSq < _nearestDistSqrd)
                {
                    _nearestDistSqrd = _newDistSq;
                    _nearestPos = _redPositions[i];
                }
            }

            // Now downwards
            for (int i = _startIndex; i >= 0; i--)
            {
                float _xDiff = Transform.ValueRO.Position.x - _redPositions[i].x;

                if ((_xDiff * _xDiff) > _nearestDistSqrd) break; // We're going beyond our current min range, so bug out

                float _newDistSq = math.distancesq(_redPositions[i], Transform.ValueRO.Position);

                if (_newDistSq < _nearestDistSqrd)
                {
                    _nearestDistSqrd = _newDistSq;
                    _nearestPos = _redPositions[i];
                }
            }

            // Move child objects and scale accordingly.

            // Get Child Ref

            // Move the child to halways between this cube and nearest position.

            // Rotate to 'point' to nearest position
           
            // Stretch to void the void in my soul...
        }
    }

    [BurstCompile]
    public struct AxisXComparer : IComparer<float3>
    {
        public int Compare(float3 _x, float3 _y)
        {
            return _x.x.CompareTo(_y.x);
        }
    }
}

In the above code, I was previously thinking that I could get the Child entity through Aspect code gen by adding it as a readonly public RefRW<>, however this produced a boxing error (Child is a buffer, not a component as per normal?).

I then decided to try taking my clue cube ref and use GetDynamicBuffer, but you can’t use SystemAPI outside of a system(?!?).

So, I’m a little stuck in the mud. Any pointers or help in clearing the fog would be greatly appreciated.

Please note, I’m going through the documentation (I understand it’s a WIP as DOTS development continues), as well as example code… but as mentioned, it can only penetrate my dense brain so far before I need the gentle lubricant of assistance/patient intuition training. That being said, if there are good links/articles that can be sent over I will read those :wink:

Regards,

Mike

P.S. Any insight on the error I get when I try to Burst Compile the OnUpdate method would be a bonus, but no worries if not :wink:

Do you mean non-identity transform or a transform with non-uniform scale?

For readonly access to a DynamicBuffer in IAspect, you would define it like this:

[ReadOnly] readonly DynamicBuffer<Child> childrenBuffer;

Many thanks for the reply, @DreamingImLatios

Ah, yes, my bad - I did mean a non-uniform SCALED transform.

Unfortunately, this will give me access to the child entity itself, but not the transform. I can’t use SystemAPI.Get/SetComponentData inside a job. Is there another method I could use inside the Job+Aspect to access the required transform and set the Position&Rotation&Scale?

Again, many thanks.

https://docs.unity3d.com/Packages/com.unity.entities@1.0/manual/systems-looking-up-data.html#look-up-entity-data-in-a-job

I won’t be able to give you great advice in this regard because I use a custom transform system that handles non-uniform scaling differently than how Unity does it. I think in Unity it is called a PostTransformMatrix?

Spy-Master answered your other question appropriately.

Many thanks for the link, @Spy-Master . Can’t believe I missed that! This was really useful… however, still having some problems.

I was able to use the following code in the System:

        SortJob<float3, AxisXComparer> _sortJob = _positions.SortJob(new AxisXComparer { });
        JobHandle _sortJobHandle = _sortJob.Schedule();

        // Run an entity aspect on all blue cubes with the positions
        JobHandle _findJobHandle = new FindAndDrawToNearestJob
        {
            Positions = _positions,
            Transforms = SystemAPI.GetComponentLookup<LocalTransform>()
        }.ScheduleParallel<FindAndDrawToNearestJob>(_sortJobHandle);

        _findJobHandle.Complete();

This, job contains the following code:

        [ReadOnly] public NativeArray<float3> Positions;
        [ReadOnly] public ComponentLookup<LocalTransform> Transforms;

        public void Execute(BlueCubeFindNearestAspect _aspect)
        {
            _aspect.FindNearest(Positions, ref Transforms);
        }

And the Aspect contains this code:

...
...            // inser previous aspect code //
            //------Move child objects and scale accordingly.------//

            // Get Child Ref
            LocalTransform _t = _transforms[Children[0].Value];

            // Move the child to halways between this cube and nearest position.
            _vectorToTarget *= 0.5f; // Halfway

            //_vectorToTarget += Transform.ValueRO.Position;
            //_t.Value = new float4x4(_vectorToTarget.x, _vectorToTarget.y, 0f, 1f);
            _t.Position = _vectorToTarget;

            Debug.Log($"New position of child entity with ID {Children[0].Value.Index} is {_t.Position}");
...
...

The problem is that the LocalTransform never gets updated… as seen in the below screenshot:
9536383--1346251--Screenshot 2023-12-18 183019.png

I’m sorry to be coming back with more questions - but I wold just love to get this finished as I’m feeling that it is a blocker to learning any more…

Regards,

Mike
P.S. Circled in the picture is the blue cube and it’s child marker line quad, still in it’s original local position.

The Position that you set is that of a copy of the original data, since the LocalTransform component, like most components in Entities, is a struct. You need to write the component back to the chunk of that entity, which can be done like

myComponentLookup[targetEntity] = newValue;

Alternatively, you can access the component in-memory directly instead with a RefRW.

if (localTransforms.HasComponent(targetEntity))
{
  RefRW<LocalTransform> rrw = localTransforms.GetRefRW(targetEntity);
  rrw.ValueRW.Position = newPosition;
}

Using the ComponentLookup for writes naturally requires removing the ReadOnlyAttribute on the job field. Also, note that writing a component in parallel requires NativeDisableParallelForRestrictionAttribute on the field and your own assurance that there won’t be interference between the executions of any two iterations for entities in the job (may or may not be true depending on how the job and data are designed). It would be safer to just make it a single-threaded job.

Awesome! Many thanks @Spy-Master

For this micro project I’m doing, I’m happy with removing the NativeDisableParallelForRestictionAttribute, because this is practically the last step - however I hear your warning loud and clear. I might continue this project/redesign it to workaround this hack, but either way I’m happy to call it (provided I can implement your suggestion succesfully).

I didn’t realise there was a GetRefRW method on the ComponentLookup - still have much to learn and research.

Many thanks, oh wise master. I will update with my final results tomorrow (need sleep, lol)

Update Tuesday 19/12/2023

Okay, so trying to use both of Spy-Masters suggestions in the Aspect (within an IJobEntity) to update the childs LocalToWorld doesn’t update in the position (relative or absolute) at runtime. I am probably not setting this right dues to not being able to find any decent examples of how to set the LocalToWorld… but still, bottom line is that it’s not working.

I tried to to the same with a LocalTransform, but get spammed with the following error:

InvalidOperationException: The writeable ComponentLookup<Unity.Collections.NativeText.ReadOnly> FindAndDrawToNearestJob.JobData.Transforms is the same ComponentTypeHandle<Unity.Collections.NativeText.ReadOnly> as FindAndDrawToNearestJob.JobData.__TypeHandle.__FindNearestSystem_BlueCubeFindNearestAspect_RW_AspectTypeHandle.BlueCubeFindNearestAspect_TransformCAc, two containers may not be the same (aliasing). - The positions don’t update either.

Therefore, I’m going to abandon the idea of having the marker lines as children. I have a suspicion it is not only going to lead to bad habits (as Spy-master hinted at), but also not be implementing the Data Oriented concenpts I should be wrapping my head around.

I’ll keep updating this answer (provided I don’t get more replies) so as to keep my journey here for posterity.