FX System

Hey,

how are you guys doing visual-only effects? I currently have a list of prefabs which get pooled and a system called WorldFXSystem which receives effect requests. Having a spreadsheet with audio and vfx references (ScriptableObject) instead of single prefabs seems like an improvement. So I’m not necessarily looking for better performance but better usability in the calling code and effect authoring.

How other systems request an effect. Note that the event name gets hashed (prob at compile time):

 var ecb = GetSingleton<BeginInitializationEntityCommandBufferSystem.Singleton>().CreateCommandBuffer(state.WorldUnmanaged);
var worldFx = GetSingletonEntity<WorldFXSystem.Singleton>();
...
ecb.AppendToBuffer(worldFx, new QueuedFX("FX_ShootArrow", pos, quaternion.identity));

The WorldFXSystem, just for reference:

using System.Collections.Generic;
using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using Unity.Mathematics;
using UnityEngine;
using static Unity.Entities.SystemAPI;

public readonly struct QueuedFX : IBufferElementData {
    public readonly int EffectType;
    public readonly float3 Position;
    public readonly quaternion Rotation;

    public QueuedFX(in FixedString128Bytes name, float3 position, quaternion rotation) {
        EffectType = HashUtil.Hash(name);
        Position = position;
        Rotation = rotation;
    }
    public QueuedFX(int effectType, float3 position, quaternion rotation) {
        EffectType = effectType;
        Position = position;
        Rotation = rotation;
    }
}

[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial struct WorldFXSystem : ISystem {
    public struct Singleton : IComponentData {
        public int Dummy;
    }

    class EffectPool {
        public List<GameObject> Effect;
        public int Idx;
    }

    class EffectPools : IComponentData {
        public List<int> NameHash;
        public List<EffectPool> Pools;
    }

    public void OnCreate(ref SystemState state) {
        var parent = new GameObject("WorldFX");
        parent.hideFlags = HideFlags.DontSave;

        var nameHashes = new List<int>();
        var pools = new List<EffectPool>();
        foreach (var fx in GameObjectAssets.Instance.FXPrefabs) {
            var gos = new List<GameObject>(10);
            for (int i = 0; i < 10; ++i) {
                var newGO = Object.Instantiate(fx, parent.transform);
                newGO.SetActive(false);

                gos.Add(newGO);
            }

            var pool = new EffectPool() {
                Effect = gos
            };

            nameHashes.Add(HashUtil.Hash(fx.name));
            pools.Add(pool);
        }

        state.EntityManager.AddComponentObject(state.SystemHandle, new EffectPools {
            NameHash = nameHashes,
            Pools = pools
        });

        var e = state.EntityManager.CreateSingleton(new Singleton { });
        state.EntityManager.AddBuffer<QueuedFX>(e);
    }

    public void OnDestroy(ref SystemState state) {
    }

    public void OnUpdate(ref SystemState state) {
        var queuedFx = GetSingletonBuffer<QueuedFX>();
        var pools = ManagedAPI.GetComponent<EffectPools>(state.SystemHandle);

        for (int fxIdx = 0; fxIdx < queuedFx.Length; ++fxIdx) {
            var queuedEffect = queuedFx[fxIdx];

            int poolIdx = -1;
            for (int i = 0; i < pools.NameHash.Count; ++i) {
                if (pools.NameHash[i] == queuedEffect.EffectType) {
                    poolIdx = i;
                    break;
                }
            }
            if (poolIdx == -1)
                throw new System.Exception($"Effect type {queuedEffect.EffectType} not found");

            var pool = pools.Pools[poolIdx];

            var idx = pool.Idx;
            pool.Idx = (pool.Idx + 1) % pool.Effect.Count;

            var effect = pool.Effect[idx];
            effect.SetActive(false);
            effect.transform.SetPositionAndRotation(queuedEffect.Position, queuedEffect.Rotation);
            effect.SetActive(true);
        }

        queuedFx.Clear();
    }
}

Thank you :slight_smile:

Using id’s as uint or FixedString is fine. 128 is too much though, it might hinder performance later on.
FixedString32Bytes is usually plenty. Wish there were Unity maintained FS16Bytes, but there’s none atm.
Names can be trimmed and checked in editor. So you could add FS as ids directly inside the struct without the need to convert it to the hash each time.

Just in case - don’t mix ints and FixedString hashes.
You’ll get much more higher hash collision probability when using both together. Pick one (either ints, or FixedStrings).

Using pooling is correct, though try not to re-implement pooling again and again.
Pick an asset that does that for you and stick to it.

Depending on the requirements you might want to swap out IBufferData to just NativeQueue.
But this a personal preference. This way you’d only need to access a different system, instead of accessing a singleton that may stall the main thread. Much cleaner, but requires manual dependency management.

For this case I’ve got a custom made lookup that binds ScriptableObjects & Prefabs to the ids (FixedString32Bytes).
This is a two-way lookup that can be accessed by the system (or from anywhere) to grab a prefab required by id. Which also means you don’t need to hardcode the id in the code, just drag & drop SO / Prefab is the editor and it will push id to the respective component during authoring. Plus, id is displayed in somewhat human-readable format since its a FS.

Then prefab is fed into my pooling solution. Rest works the same as default MB’s.
When FX logic is done - object gets returned to the pool automatically.