#Singletons, Shmingletons

1 messages · Page 1 of 1 (latest)

visual hatch
#

Ok, I'm a little vexed by Singletons (and the various APIs that exist around them), and wonder how I can use them to share some data with Mono-Land and ECS-Land stuff alike, especially if there is state that is based on the presence of that data (other ECS systems have RequireForUpdate for that component)

Every sync point counts, right?

So assuming I have some UI that needs to grab some data from ECS to show in a UGUI or UIElements panel the name, entity, and some supplementary data on the entity... how do YOU communicate this out of ECS?

I've been using a singleton like this, but the creation / deletion code raises the hairs on the back of my neck as if there's something I'm just not seeing here that would be much better.

    public partial struct InteractSystem : ISystem
    {
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<BubbleCameraState>();
            state.RequireForUpdate<PhysicsWorldSingleton>();
        }

        public void OnUpdate(ref SystemState state)
        {
            var physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>();
            var camData = SystemAPI.GetSingleton<BubbleCameraState>();
            var camera = Cam.Instance.localCamera; //would like to replace screenpoint to ray from just the view matrix, found no equivalent in mathematics
            
            var ray = camera.ScreenPointToRay(Mouse.current.position.ReadValue());
            ray.origin = (float3) ray.origin + camData.virtualPosition;
            var input = new RaycastInput
            {
                Filter = camData.filter, //candidate for refactoring, it's a complex filter, currently can't calculate that value here
                Start = ray.origin,
                End = ray.origin + ray.direction * (camera.farClipPlane-camera.nearClipPlane),
            };
            
            if (physicsWorld.CollisionWorld.CastRay(input, out var hit))
            {
                var interact = new Interact
                {
                    entity = hit.Entity,
                    normal = hit.SurfaceNormal,
                    localPosition = hit.Position
                };
                if (!SystemAPI.HasSingleton<Interact>()) state.EntityManager.CreateEntity(typeof(Interact));
                SystemAPI.SetSingleton(interact);
            }
            else if (SystemAPI.TryGetSingletonEntity<Interact>(out var entity))
            {
                state.EntityManager.DestroyEntity(entity);
            }
        }

Mono UI Code, in Update:
-> EntityManager.TryGetSingleton
(let's assume further data than the Interact component are not required)

Should I keep a managed singleton instead? How?

hazy topaz
#

nah, keeping fully managed stuff as static singleton is fine

#

singletons are at use when you want World specific data to be global (per world) singleton

#

if you have app specific singleton (UI) - no need to reinvwhent the wheel

visual hatch
#

(well I actually don't use a lot of singletons in my app, even the camera is only a temporary singleton; I want to eventually support multiple)

But I do have a managed message bus to communicate stuff, I'll see if I can just load that up in OnCreate then, and decouple it.

#

This system won't be bursted anyway, and except for the CastRay call, which I hope is internally bursted, there are no expensive methods in it.

#

How do you put data INTO ecs from mono? e.g. for updating camera positions, I run a separate system that gets the camera's position and writes it into BubbleCameraState

hazy topaz
#

It's pretty hard to communicate from within ECS with message bus, I'd rather just expose all required methods to managed manager with interface and create public static IThatInterface Instance field somehwere, so system can access it

visual hatch
#

But that also feels weird (the code for that is... icky)

        protected override void OnUpdate()
        {
            switch (_hasData)
            {
                case false when Cam.Instance.CurrentPortal:
                    _hasData = true;
                    EntityManager.CreateSingleton(BuildCameraData());
                    break;
                case true when !Cam.Instance.CurrentPortal:
                    _hasData = false;
                    EntityManager.DestroyEntity(SystemAPI.GetSingletonEntity<BubbleCameraState>());
                    break;
                case true:
                    if (_hasData) SystemAPI.SetSingleton(BuildCameraData());
                    break;
            }
hazy topaz
#

inside that method declare all properties and methods

#

for ECS to obtain values

#

in the end all interactions should be similiar to Input API (old input system)

#

you just poll every system update

visual hatch
hazy topaz
#

between Simulation and Presentation

hazy topaz
#

or

#

idk where actually your callbacks are executed

visual hatch
#

Do you have a way to avoid that sync point?

hazy topaz
#

you write all callbacks data to temporary managed values

#

and poll those from system

#

so that logic execution order is constant

visual hatch
#

Can you give a pseudocode example?

hazy topaz
#

void Callback(CallbackArgs args)
{
_storedValue = args.Value;
}

visual hatch
#

_storedValue is a field on which type? the ISystem?

hazy topaz
#

your manager

#

of camera

#

or ui

visual hatch
#

Yeah that would work with message bus.

hazy topaz
#

and then system will call every update

visual hatch
#

Yep.

hazy topaz
#

IsThereANewValue?

#

just like Input API

#

Input.GetKeyDown(Space)

#

in fact

#

you can even make it burst compatible

visual hatch
#

I'll make a messagebus that queues the changes and then Update in the "manager" will consume the events.

#

Thanks, I understand how that could indeed otherwise create a fatter sync point.

hazy topaz
#

if you write your temporary values to Native containers

visual hatch
#

I don't think these are safe to read outside a job.

hazy topaz
#

or expose them as SharedStatic

visual hatch
#

Yeah.

#

Question 2 - How do you write data back from managed to ECS? (as in, what's your preferred way)

hazy topaz
#

you don't

#

ECS reads data from managed

#

that's it

visual hatch
#

Ok.

#

That works for me.

#

I'll leave this thread open to hear some other opinions but your approach would work for me, so I try to clean up my double-coupled code to be only coupled by reading in ECS, and not writing or fucking with entities from Mono.

visual hatch
#
    /// <summary>
    /// Message Bus allowing to request a subscription in the form of a queue, that accumulates messages over time, rather than instantly
    /// invoking the UnityEvent and its subscribed listeners. The standard method of directly subscribing to a Even is still available.
    /// Each subscriber will get their own Queue, and needs to unsubscribe the queue when done.
    /// </summary>
    /// <remarks>
    /// To avoid longer Sync Point duration at inopportune moments in System Update Order,
    /// e.g. when pumping messages into this bus from a ECS System, it is recommended to use the Queue method in subscribers.
    /// </remarks>
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class Bus
    {
        private static Dictionary<object, (UnityEventBase, object)> _registry = new();
        public static Queue<T> Subscribe<T>(UnityEvent<T> event_concrete)
        {
            var subscription = new Queue<T>(10);
            var action_concrete = new UnityAction<T>(t => subscription.Enqueue(t));
            _registry[subscription] = (event_concrete, action_concrete);
            event_concrete.AddListener(action_concrete);
            return subscription;
        }

        public static void Unsubscribe<T>(Queue<T> subscription)
        {
            var (e, action) = _registry[subscription];
            var event_concrete = e as UnityEvent<T>;
            var action_concrete = action as UnityAction<T>;

            Debug.Assert(event_concrete != null, "Queue wasn't subscribed to this event.");
            Debug.Assert(action_concrete != null, "Queue wasn't subscribed with the right action type.");

            event_concrete.RemoveListener(action_concrete);
            _registry.Remove(subscription);
        }

        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        private static void Reset()
        {
            Debug.Log("MessageBus reset.\n(RuntimeInitializeLoadType.SubsystemRegistration)");
            _registry = new Dictionary<object, (UnityEventBase, object)>();
        }

        protected Bus() { }
    }
}
#
    /// <summary>
    /// UI Messages
    /// </summary>
    /// <inheritdoc/>
    [UsedImplicitly]
    public class UIMessage : Bus
    {
        /// <summary>
        /// An Entity was hovered, or null if nothing was hit / is no longer hovered.
        /// </summary>
        public static UnityEvent<Interact> Hover { get; private set; }
        
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
        private static void Reset()
        {
            Debug.Log("UIMessage reset.\n(RuntimeInitializeLoadType.SubsystemRegistration)");
            Hover = new();
        }

        private UIMessage() { }
    }
#

Kinda like this, I guess.

#

And InteractSystem is now just:

#
    public partial struct InteractSystem : ISystem
    {
        public void OnCreate(ref SystemState state)
        {
            state.RequireForUpdate<BubbleCameraState>();
            state.RequireForUpdate<PhysicsWorldSingleton>();
        }

        public void OnUpdate(ref SystemState state)
        {
            var physicsWorld = SystemAPI.GetSingleton<PhysicsWorldSingleton>();
            var camData = SystemAPI.GetSingleton<BubbleCameraState>();
            
            var cam = Cam.Instance.localCamera;
            var ray = cam.ScreenPointToRay(Mouse.current.position.ReadValue());
            ray.origin = (float3) ray.origin + camData.virtualPosition;
            var input = new RaycastInput
            {
                Filter = camData.filter,
                Start = ray.origin,
                End = ray.origin + ray.direction * (cam.farClipPlane - cam.nearClipPlane),
            };
            
            if (physicsWorld.CollisionWorld.CastRay(input, out var hit))
            {
                var interact = new Interact
                {
                    entity = hit.Entity,
                    surfaceNormal = hit.SurfaceNormal,
                    localPosition = hit.Position
                };
                UIMessage.Hover.Invoke(interact);
            }
            else
            {
                UIMessage.Hover.Invoke(null);
            }
        }
    }
hazy topaz
#

yeah, something like this ig

visual hatch
#

OK, more Shmingleton questions.

I want to make one of these system singletons. Actually in a SystemBase (!) descendant.

public partial class CameraSystem : SystemBase
{
    protected override void OnCreate()
        {            
            _inputActions = new GameInputActions();
            EntityManager.AddComponent<CameraState>(SystemHandle);
        }

        protected override void OnUpdate()
        {
            var cameraState = EntityManager.GetComponentDataRW<CameraState>(SystemHandle);
            cameraState.ValueRW = BuildCameraData(); //copies stuff from several managed places into an IComponentData, CameraState
        }

... is... is this the way?

#

It's surprisingly little boilerplate. That's usually suspicious with ECS.

visual hatch
#

And in a client ISystem that only reads the written data, so I use:

_cameraHandle = state.World.GetExistingSystem<CameraSystem>();

... and then ...

var cameraData = state.EntityManager.GetComponentData<CameraState>(_cameraHandle);

... or ...

var cameraData = SystemAPI.GetSingleton<CameraState>();

... or something else entirely?

hazy topaz
#

can put it on same entity as system, or create new

#

no much difference

#

btw

#

use SystemAPI.GetComponent instead of EntityManager overload

#

I believe you can get entity directly from SystemState

#

SystemAPI generates Lookup which is slightly faster than EntityManager (because it doesn't do sync)