How to make a single VFX component follow multiple entities

I am able to set the position of these particles, but how would I make them follow per entity? (a flash that moves with the position and rotation of the muzzle of a rifle).

I think it involves “Get Particle Index in strip” but I don’t think you can create an array of properties, so I’m not sure how to achieve this.

It could be one of the experimental nodes.

image

Hi SapphireGames2025,

I’m not sure that I fully understand what you are trying to do.

In your visual effect, is each flash a particle? In that case, you can use a GraphicsBuffer and store in it the transformation matrix for each entity, pass it to the graph using a exposed property, and then use “Sample Buffer” operator in the graph. But you will have to match the particle with the index of the matrix in the buffer somehow, which is not easy if your particles can die. For a fixed number of flashes with immortal particles it could look something like this:

Another simpler alternative would be to have a visual effect component for each flash. While this is not the best for performance, it would be way easier to handle. The automatic instancing feature will update your effects in batches to improve performance, so it may be worth trying.

Let me know if you need more information for any of the two approaches.

Hi gabriel-delacruz,

What I’m trying to achieve is to have the particle move with the entity. like a child following the parent.

It’s more than one particle, and the particles have a random lifetime, and the frequency can not be determined.

Is it possible to get a particle index death and update the buffer? i.e. particles 0-20 follows entity 1, particles 21 - 27 follows entity 2. Meaning that even when the particles die the ones that where single burst still follow the correct entity.

Are there any plans to update VFX to support ECS?

Ok, I understand now, thanks.

I think the best way to do what you want is to pass the entity index to the particles as a custom attribute. I assume you are using events to spawn the flash. If that is the case, you can add the entity index as an attribute to the event, and pass that to the particles that are spawned by that event.

After that, you can use the index to sample the buffer, where you would store the transformation matrix. You can use this matrix to transform the position (and rotation and scale?) in the Output context, or pass it to a Shader Graph if you are using it, to transform the vertices.

It is important that any changes to position are done in the Output context, so the stored position attribute stays in local space.

Regarding ECS, currently VFX graph lives on the GPU, while entities are on CPU, so it is not easy for them to be directly compatible. But there are plans to have a CPU backend in the future, and that most likely will be compatible with ECS.

For now, you should be able to make it work like I said, with the particles having the entity index, and passing information through GraphicBuffers.

Hope that helps!

1 Like

I am starting with the position first before I try changing the rotation.

Currently I cannot move the particles with the graphics buffer.

  • Am I using the correct type of graphics buffer?

  • Am I calling it the correct amount of times?

  • Did I set it up correctly in the output context?

The array of data to send to the VFX’s.

[InternalBufferCapacity(0)]
public struct MuzzleFlashVFX_Data : IBufferElementData
{
    public int EntityID;
    public Matrix4x4 matrix;

    //not using
    public float3 position;
    public float3 foward;

}

Adds to the array when a gun fires.

//muzzle flash vfx
foreach (var flash in SystemAPI.Query<DynamicBuffer<MuzzleFlashVFX_Data>>())
{
    foreach (var (tag, transform, entity) in SystemAPI.Query<PlayerEquipMuzzleTag, LocalToWorld>().WithEntityAccess())
    {
        flash.Add(new MuzzleFlashVFX_Data
        {
            EntityID = entity.Index,
            matrix = transform.Value,
        });
    }
}

creates a GraphicsBuffer from the array.

 foreach (var flash in SystemAPI.Query<DynamicBuffer<MuzzleFlashVFX_Data>>())
 {
     if (flash.Length <= 0) break;
     unsafe
     {
         GraphicsBuffer buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, flash.Length, sizeof(Matrix4x4));

         var array = new NativeArray<Matrix4x4>(flash.Length, Allocator.Temp);
         for (int i = 0; i < flash.Length; i++)
         {
             array[i] = flash[i].matrix;
         }
         buffer.SetData(array);
     
         for (int i = flash.Length - 1; i >= 0; i--)
         {
             MonoVFX.Instance.PlayFlash(flash[i].EntityID, buffer);
             flash.RemoveAtSwapBack(i);
         }
         
         buffer.Release();
         buffer.Dispose();
     }

 }

Plays the VFX.

  public void PlayFlash(int index, GraphicsBuffer buffer)
  {
      MuzzleFlashVFX.SetInt(FlashIndex, index);
      MuzzleFlashVFX.SetGraphicsBuffer(FlashBuffer, buffer);
      MuzzleFlashVFX.SendEvent(FlashEvent);
  }


There are a few things that I would do differently. I’ll start with the graph I did for my test:

The first difference you may notice is that the event is plugged directly to the Initialize context. Event attributes get passed through SpawnEvents, but if you have more than one in the same frame, it will only keep one value. Also, I can pass the count in the event directly, so why bother?

Next thing, is the entityID. You are setting the entity ID value as an exposed variable, and that will kind of work if you don’t have more than one entity spawning particles per frame. But I think it is safer to pass it in the event itself, as an event attribute. To do that, I created a custom attribute entityID, and I set it per-particle in Initialize, reading from the source (the event). This custom attribute will be stored during the entire particle lifetime, so the particle will always know which entity spawned it.

Here is the small component I created to send the event. I do it OnEnable, but you get the idea:

using UnityEngine;
using UnityEngine.VFX;

[ExecuteAlways]
public class SpawnFlash : MonoBehaviour
{
    public int entityID;
    public VisualEffect visualEffect;

    private VFXEventAttribute eventAttributes;
    private ManageEntityMatrices manageEntityMatrices;

    void OnEnable()
    {
        if (visualEffect != null)
        {
            if (eventAttributes == null)
                eventAttributes = visualEffect.CreateVFXEventAttribute();

            if (manageEntityMatrices == null)
                manageEntityMatrices = visualEffect.GetComponent<ManageEntityMatrices>();

            eventAttributes.SetFloat("spawnCount", 16.0f);
            eventAttributes.SetInt("entityID", entityID);
            manageEntityMatrices.SetMatrix(entityID, transform.localToWorldMatrix);
            visualEffect.SendEvent("SpawnFlash", eventAttributes);
        }
    }
}

Finally, we have to deal with the GraphicsBuffer. In your code, you are creating the graphics buffer every time, and destroying it before the graph even has time to run.

This is what I do instead: I create one single GraphicsBuffer, that will be set to to the visual effect as an exposed property. This GraphicsBuffer will be shared by all entities, indexed by their entity ID. It is your responsibility to generate these entity IDs, and to guarantee that they are unique (although you can reuse) and that you have room for all in your buffer.

These is the component that I attached to the visual effect, to manage the GraphicsBuffer:

using UnityEngine;
using UnityEngine.VFX;

[ExecuteAlways]
public class ManageEntityMatrices : MonoBehaviour
{
    private GraphicsBuffer entitiesGraphicsBuffer;

    void OnEnable()
    {
        VisualEffect visualEffect = GetComponent<VisualEffect>();
        if (visualEffect != null )
        {
            entitiesGraphicsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 10, sizeof(float) * 16);
            Matrix4x4[] matrixArray = new Matrix4x4[10];
            entitiesGraphicsBuffer.SetData(matrixArray);
            visualEffect.SetGraphicsBuffer("MatrixBuffer", entitiesGraphicsBuffer);
        }
    }

    void OnDisable()
    {
        if (entitiesGraphicsBuffer != null )
        {
            entitiesGraphicsBuffer.Dispose();
        }
    }

    public void SetMatrix(int entityID, Matrix4x4 matrix)
    {
        if (entitiesGraphicsBuffer != null)
        {
            Matrix4x4[] matrixArray = { matrix };
            entitiesGraphicsBuffer.SetData(matrixArray, 0, entityID, 1);
        }
    }
}

In that script you can also see how we update one matrix at a time when we spawn a new flash, called from the first script. If your object is moving, you should set its matrix every frame! If you do that, consider grouping all the matrices together for a single SetData.

After that, we can easily access the graphics buffer and get the matrix (make sure to set up the node correctly):
image

You can use your own struct types as well, but you need to follow some rules:
https://docs.unity3d.com/Packages/com.unity.visualeffectgraph@17.0/manual/Operator-SampleBuffer.html

In my example I’m only multiplying the position at the end, but technically you could extract position and forward direction from your matrix directly, without having to store them.

And that is pretty much it. It is a small example but it showcases some advanced features, that I hope will be useful in other scenarios as well :slight_smile:

1 Like

@MarleyHester The VFX system cannot be converted to an entity, it has to stay in mono.

Having a gameobject per entity cancels out the purpose of ECS.

It’s easier to use a single gameobject with a VFX and update all the entity transforms from a query.

1 Like

@gabriel-delacruz For testing the graphics buffer I created a bunch of entities that randomly fly around.

Using the custom struct and buffer, I was able to make particles follow a single entity. But It does not follow the others.
image


Sets up graphics buffer

  private void OnEnable()
  {
      unsafe
      {
          eventAttribute = MuzzleFlashVFX.CreateVFXEventAttribute();
          FlashgraphicsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 100, sizeof(MuzzleFlashBufferVFX));
          MuzzleFlashVFX.SetGraphicsBuffer(FlashBuffer, FlashgraphicsBuffer);

      }
  }

  void OnDisable()
  {
      if (FlashgraphicsBuffer != null)
      {
          FlashgraphicsBuffer.Dispose();
      }
  }

  public void UpdateFlashTransforms(NativeArray<MuzzleFlashBufferVFX> array, int index)
  {
      FlashgraphicsBuffer.SetData(array, 0, index, 1);
  }
  public void PlayFlash(int index)
  {
      eventAttribute.SetInt(EntityIndex, index);
      MuzzleFlashVFX.SendEvent(FlashEvent);
  }

The arrays used for VFX

[InternalBufferCapacity(0)]
public struct MuzzleFlashRequestVFX : IBufferElementData
{
    /// <summary>
    /// Use EntityIndexInQuery.
    /// </summary>
    public int ID;
}

[InternalBufferCapacity(0)]
[VFXType(VFXTypeAttribute.Usage.GraphicsBuffer)]
public struct MuzzleFlashBufferVFX : IBufferElementData
{
    public Vector3 Position;
    public Vector3 Rotation;
}

Update the array transform and ID

[BurstCompile]
public partial struct UpdateVFXAIJob : IJobEntity
{
    public DynamicBuffer<MuzzleFlashBufferVFX> transformbuffer;
    public DynamicBuffer<MuzzleFlashRequestVFX> requestbuffer;
    public void Execute([EntityIndexInQuery] int index, in FlyingAIMovementData tag, in LocalToWorld localToWorld)
    {

        if (transformbuffer.Length <= index)
        {
            transformbuffer.Add(new MuzzleFlashBufferVFX
            {
                Position = localToWorld.Position,
                Rotation = math.degrees(math.EulerZXY(localToWorld.Rotation)),
            });
        }
        else
        {
            ref var buffer = ref transformbuffer.ElementAt(index);
            buffer.Position = localToWorld.Position;
            buffer.Rotation = math.degrees(math.EulerZXY(localToWorld.Rotation));
        }


        requestbuffer.Add(new MuzzleFlashRequestVFX
        {
            ID = index,
        });

    }
}

Sets buffer and event data (these foreach only have one array)

 foreach (var flashrequest in SystemAPI.Query<DynamicBuffer<MuzzleFlashRequestVFX>>())
 {
     if (flashrequest.Length <= 0) break;
     for (int i = flashrequest.Length - 1; i >= 0; i--)
     {

         foreach (var transform in SystemAPI.Query<DynamicBuffer<MuzzleFlashBufferVFX>>())
         {
             var matrixarray = transform.ToNativeArray(Allocator.Temp);
             MonoVFX.Instance.UpdateFlashTransforms(matrixarray, flashrequest[i].ID);
         }

         MonoVFX.Instance.PlayFlash(flashrequest[i].ID);
         flashrequest.RemoveAtSwapBack(i);
     }
 }

I think it looks good in general, but you are missing passing the eventAttribute as the second parameter for SendEvent. Try that and see if there is any improvement.

1 Like

I have added the eventAttribute as the second parameter and have not noticed any difference.

I used Debug.Log(); to double check that the id is 0 to entity.length and the int is working correctly.

 public void PlayFlash(int index)
 {
//I renamed eventAttribute to MuzzleEvents
     MuzzleEvents.SetInt(EntityIndex, index);
     Debug.Log(MuzzleEvents.GetInt(EntityIndex));
     MuzzleFlashVFX.SendEvent(FlashEvent, MuzzleEvents);
 }

  private void OnEnable()
  {
      unsafe
      {
          MuzzleEvents = MuzzleFlashVFX.CreateVFXEventAttribute();
          FlashgraphicsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 100, sizeof(MuzzleFlashBufferVFX));
          MuzzleFlashVFX.SetGraphicsBuffer(FlashBuffer, FlashgraphicsBuffer);

      }
  }

image

I tried using the int as a size and then as a offset, and saw no changes. I think the attribute is always 0

Just to confirm, does EntityIndex contain the string “FLASHENTITY”?

Also, how is the event connected to the initialize?

 public static MonoVFX Instance;
[SerializeField] VisualEffect MuzzleFlashVFX;
private VFXEventAttribute MuzzleEvents;
const string EntityIndex = "FLASHENTITY";
 void Start()
 {
     Instance = this;
 }

 private void OnEnable()
 {
     unsafe
     {
         MuzzleEvents = MuzzleFlashVFX.CreateVFXEventAttribute();
         FlashgraphicsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 100, sizeof(MuzzleFlashBufferVFX));
         MuzzleFlashVFX.SetGraphicsBuffer(FlashBuffer, FlashgraphicsBuffer);

     }
 }

void OnDisable()
{
  if (FlashgraphicsBuffer != null)
  {
       FlashgraphicsBuffer.Dispose();
  }
}

image
image
image


I think the problem might come from the Spawn context. The spawn context should automatically forward the source attributes to the SpawnEvent, but there might be something not working properly.

In my test it worked fine if I had one event per frame, but if I had more it would just take the value of one of the source attributes for all the events.

What I did to make sure it worked, was connecting the event directly to Initialize Particle, and pass the spawn count as a source attribute.

            eventAttributes.SetFloat("spawnCount", 16.0f);

Please notice that the name should be exactly that one, and that it has to be a float. It is not as convenient as having it directly in the graph, but maybe it can unblock you for now.

On my side, I’ll check if there is something wrong with Spawn context when I have some time.

Setting the spawn context by script fixed the EntityID issue. Particles are now offset by my “Test offset” group.

However, the buffer still has the same issue, all particles follow one entity instead of being spread out per entity.

image

Hope I am not derailing away from topic, but I wanted to chime in that your more extended code and example writeup here along with the rest of the discussion with SapphireGames so far has been incredibly useful / handy! Learned a few things that should come into great use. Thank you.

2 Likes

I noticed something that could be related, but I’m not very familiar with DOTS queries.

When you are passing this array to UpdateFlashTransforms, does array contain one transform or many? if it is many, you should specify the index into that array as the second parameter of SetData (currently 0).

Otherwise you will be copying always the first transform to the entities in FlashgraphicsBuffer, which would explain why they all follow the same position, even though the entity index is different.

@diakou
Thanks a lot, I’m glad that it was useful.
Advanced stuff like this is not covered in docs, so I’m happy to do the extra effort.

DOTS queries is used to search for components, in my project there is a single MuzzleFlashBufferVFX that looks like this.
image

//ECS code that says this array can be large
//--
[InternalBufferCapacity(0)]
//--
[VFXType(VFXTypeAttribute.Usage.GraphicsBuffer)]
public struct MuzzleFlashBufferVFX : IBufferElementData
{
    public Vector3 Position;
    public Vector3 Rotation;
}

I swaped the index location and tried using a simple 0 to max length, but the particles still spawn at one entity.

public void UpdateFlashTransforms(NativeArray<MuzzleFlashBufferVFX> array)
{
    for (int i = 0; i < array.Length; i++)
    {
        FlashgraphicsBuffer.SetData(array, i, 0, 1);
    }
}

I still don’t fully understand the Graphics buffer.

  • Could you explain int nativeBufferStartIndex, int graphicsBufferStartIndex and int count?

  • How would I make it that VFX particles with index 0 uses array[0], 1 uses array[1] and so on?

Maybe the naming is a bit confusing.

If you are passing an array to SetData, you need to specify:

  • the source array
  • the index in the source array to start copying from
  • the index in the destination graphics buffer to start copying to
  • how many items to copy

In your case, if you only need to copy element index from the NativeArray to the element index in graphics buffer, you need to do:

public void UpdateFlashTransforms(NativeArray<MuzzleFlashBufferVFX> array, int index)
  {
      FlashgraphicsBuffer.SetData(array, index, index, 1);
  }

If you want to copy all of them at the same time, like in that last piece of code, it is better to do one single SetData, both native array and graphicsbuffer starting at 0, and the count is the length:

public void UpdateFlashTransforms(NativeArray<MuzzleFlashBufferVFX> array)
{
        FlashgraphicsBuffer.SetData(array, 0, 0, array.Length);
}

Or, simply this, which is equivalent:

public void UpdateFlashTransforms(NativeArray<MuzzleFlashBufferVFX> array)
{
        FlashgraphicsBuffer.SetData(array);
}