#Pickups: psuedo-polymorphism in DOTS

1 messages · Page 1 of 1 (latest)

rotund forge
#

I'm working on expanding my pickup system, which currently just gives one type of resource (currency). I am wondering what is the best way (design and/or performance) to implement support for different types of pickups.

  1. The simplest way would be to add an enum of type and add to the component whatever other information is needed (eg number for currency/health, entity for weapon/armour), and have a big switch in the system that handles what to give the player
  2. A base component for the pickup (handling the physics of being picked up), then another component for what the player gets, with a system for each type (HealthPickupSystem/CurrencyPickupSystem/ArmourPickupSystem/etc) that looks for a tag to say the pickup needs to be redeemed

From what I've heard the idea of multiple systems doesn't scale the best but it would be very easy to extend, so I'm on the fence...

peak crow
#

Interestingly, I'm doing something very similar at the moment. I don't think a switch case is much harder to extend to be honest, it's what I've chosen. There won't be a lot of items picked up at any given time (at least in our case), so I don't see a compelling reason to do it differently.

rotund forge
#

I think I would at least need to change it from a single pickup component to include a separate buffer of the things that you redeem so you can have multiple types of them...

#

Or maybe better to just have one of everything in the pickup component

jagged zealot
#

I implemented a system wide native messenger to solve that

#

basically, it's a way to send non-entity data as condition for event system

#

this way you can extend it via System and keep it simple to add new behaviour

#

and don't run into performance bottleneck of creating new entities/accessing some buffer and etc

peak crow
#
    public unsafe struct ObservableEvent : IBufferElementData
    {
        internal const int NUM_VARS = 8;
        internal const int STORAGE_SIZE = 104;

        public ObservableEventType Type;
        internal fixed byte _semantics[NUM_VARS];
        internal fixed byte _offsets[NUM_VARS];
        internal fixed byte _eventData[STORAGE_SIZE];

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal unsafe int _GetIndex(ESemantic semantic)
        {
            int idx = -1;
            fixed (byte* pSemantics = _semantics)
            {
                for (int i = 0; i < NUM_VARS; ++i)
                {
                    if (pSemantics[i] == (byte)semantic)
                    {
                        idx = i;
                        break;
                    }
                }
            }

            return idx;
        }
#
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        internal unsafe T _GetData<T>(int idx) where T : unmanaged
        {
            T outVariable;
            byte offset;
            fixed (byte* pOffsets = _offsets)
                offset = _offsets[idx];
            Assert.IsTrue(offset + sizeof(T) <= STORAGE_SIZE);
            fixed (byte* pData = _eventData)
                UnsafeUtility.MemCpy(&outVariable, &pData[offset], sizeof(float3));
            return outVariable;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public unsafe T Get<T>(ESemantic semantic) where T : unmanaged
        {
            int idx = _GetIndex(semantic);
            if (idx < 0)
            {
                Debug.LogError($"Event does not contain a variable for '{semantic}'.");
                return default;
            }

            if (Hint.Unlikely(
                !ObservableEventUtils.IsSemanticTypePairValid<T>(semantic)
                ))
            {
                Debug.LogError($"Invalid semantic '{semantic}' for type '{nameof(T)}'");
                return default;
            }

            return _GetData<T>(idx);
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public bool Has(ESemantic semantic)
        {
            return _GetIndex(semantic) >= 0;
        }
    }
#

in 2 times because discord was stubborn..

#

I didn't take a close look at @jagged zealot 's system but I suppose it's a viable solution too depending on your use case

rotund forge
jagged zealot
#

and your system will check whether it matches

#

similiar to how normal queries are, but without actual query overhead

#

also without entity overhead

rotund forge
#

then the system would have to inspect what components are on the entity?

jagged zealot
#

but yeah

#

it involves non linear access

#

after all

rotund forge
#

do you mean multiple systems listening for the same message?

jagged zealot
#

yeah

#

this will just add a condition for system to update aside from it's normal update conditions (RequireToUpdate)

#

which is 0 overhead, when there are no messages

#

and involves no structural changes

#

which is really nice for not-every-frame events

rotund forge
#

hmm

tawny breach
#

of course this game had a LOT of pickups so adding a little fixed cost to sort/partition the instances was easily made up for in the marginal cost per pickup

rotund forge
#

I was thinking of something like a lookup table generated at bake time, then add bursted function pointers for each type at runtime, lucky we have unified memory these days 😛

peak crow
#

I like this quote at the end, it's why I'm content with the switch case approach too, it's easy to refactor

don’t worry too much about writing optimized
code, just make it difficult to write unoptimizable
code

rotund forge
#

he also said SPU programming is fun... 😉

peak crow
#

I'll take his word for it ^_^

jagged zealot
#

you'll have to write static method wrapper for each type to generate burst function pointer

#

would require some codegenerator for proper workflow

verbal portal
#

here's one way to handle this, but big disclaimer: this isn't made or supported by Unity, and is just an experiment, etc, etc...
https://github.com/PhilSA/Trove/blob/main/com.trove.polymorphicstructs/Documentation~/tutorial.md

It's a source generator that generates a single "parent" union struct based on several "child" structs that all implement the same interface. For each interface function, a switch statement is codegen'd. See readme at the root of the repo for install instructions

rotund forge
verbal portal
#

yes 😁

rotund forge
#

where is the source for the source generator?