Hello,
I have the following issue with my ability system.
So the developer can define any blitable struct that implement the IEffect interface (extends the IComponentData interface to allow me to have access to the TypeIndex from TypeManager).
This interface allows me to define the minimal set of Data I needed by any effect and also to identifiy effect types for my custom editor.
Then an ability is defined as a scriptable object containing a list of IEffect and a Guid.
Now, I want to make a dictionnary of <Guid/EffectTypeIndex,āIEffectā>.
Thatās easy but I need to be able to lookup the effect in a burst job.
Each effect can be used in a dedicated system so I know the Type and TypeIndex of the effect to query in advance and within the system I just have to complete with the ability guid Iām activating to get the corresponding effect for that ability.
Now to be able ot query that in a Burst Job I need to put these effect in a nativemultihashmap. but the IEffect donāt fit into thatā¦
I tried to look up the BlobAssets and how itās managed in hte physics package to get sepecific collider into a generic collider struct but I donāt feel that it will do it for my need (or I donāt understand well enougth how it works).
I also tried to make a lookup table to a void* pointer that I can then ācastā to the effect I know Iām processing but Iām bad with pointer and with what I tested until now I donāt retrieve the values I expect in my casted struct fieldsā¦
So does anyone know how to make a nativemultihashmap with differently shaped values ? Or an alternative ?
What is the lifecycle of your IEffect instances? Are they mutable?
This problem seems to stem from a higher level design issue maybe. I would not create a paradigm of you have to pass around data you donāt know what it is or that your own logic doesnāt consume. Use callbacks, events, some type of integration hook to keep that separation.
To tie related yet separate data together use idās. Preferably integers or enums.
For instance in our game abilities, items, regions, and a few other random things can trigger effects. We embed the effect editor inside the editor for those things. And we end up with just a bunch of separate lists/maps and lookup tables between them with everything based on unique integer or enum ids.
Itās really just a simple database. And you can create complex hierarchies of all sorts of related yet separate things this way and tie it together in the editor however you want.
If you really want to keep the design as close to how it is right now, tagged unions can solve your problem directly. Maybe (but see caveat below) For example:
interface IEffect { int explode(float2 targetCoords); }
struct Fireball : IEffect { float fireTemperature; public int explode(float2 targetCoords) { return 1; } }
struct LightningBolt : IEffect { int forkiness, secondsBeforeThunder; public int explode(float2 targetCoords) { return 2; } }
enum EffectKind : byte { FIREBALL, LIGHTNING_BOLT }
[StructLayout(LayoutKind.Explicit)] unsafe struct Effect : IEffect {
[FieldOffset(0)] Fireball asFireball;
[FieldOffset(0)] LightningBolt asLightningBolt;
[FieldOffset(8)] EffectKind kind; // you need to be sure of your type sizes (Unsafe.SizeOf() can help) and put this beyond the end of the last one
public static EffectKind from(Fireball fireball) { return new EffectKind { kind = EffectKind.FIREBALL; asFireball = fireball }; }
public static EffectKind from(LightningBolt lightningBolt) { return new EffectKind { kind = EffectKind.LIGHTNING_BOLT; asLightningBolt = lightningBolt }; }
public int explode(float2 targetCoords) {
switch(kind) {
case EffectKind.FIREBALL: return asFireball.explode(targetCoords);
case EffectKind.LIGHTNING_BOLT: return asLightningBolt.explode(targetCoords);
default: throw new Exception("Unknown/invalid EffectKind"); }}}
However, this isā¦
- highly unsafe
- a lot of work to maintain and prone to bugs
- can introduce vector alignment issues (minimize these by sticking the type at the end of the structure instead of the beginning)
For #2, you can auto-generate these (for example, with Text Templates, or Cecil if you want to get fancy). But #3 can be a very unexpected and ugly problem thatās hard to consistently reproduce and will often straight up crash your editor or make other things behave unexpectedly. Which makes Cecil more attractive an option since you can detect the required alignment of types and add padding.
My recommendation is just to NOT take this approach, but Iāve been bad at following my own advice and have a number of types that are hacked together like this. For an open source example, @DreamingImLatios has a physics framework that might be slightly more starightforward than the blob buffers in Unity Physics: Latios-Framework/Physics/Physics/Components/Collider.cs at v0.2.0 Ā· Dreaming381/Latios-Framework Ā· GitHub
1 Like
Hi,
First thanks for your answers.
To anwser DreamingImLatios, this data is authored in hte editor through a cutom editor script for my ability scriptable object. Thatās where I store a List. The IEffect are struct that store basic imutable data about the effect like attack power, damage type, ā¦
When creating an ability scriptable object, I make it an adressable and get its guid for reference.
That Guid reference is what is stored on my entity.
At runtime, I load all my Ability adressable (List) and that where I wanted to make a dictionnary of <Ability/EffectType,IEffect>.
The idea was that when my ability are triggered, that trigger system could querry for the effect data to process it.
Knowing I have one effect processor per effect type, when processing it I know what the effect type is.
Once every X amount of time or on demand I check for addressable updates and if need I update the ability/effect map.
Now snacktimeās anwser made me realise I donāt need to have ONE map for all effect, I could create a delegate method for each effect processor that listen for the ability addressable update and maitains its own map of <ability guid,effect> by checking the full list of IEffect and only registering the IEffect if itās of the type the processor handles.
This may not be the most efficient way to process this but as itās done only on startup and rare occasion I think I could gate away with it. And it provides me with 2 key elements, 1) not duplicating static effect data for all entities that can use abilities which would otherwise eat up memory if I have lots of monsters/players 2) levrage the addressable capabilities to be able to deliver updated abilities allowing for instance balancing abilities or adding new effect to an ability (provided the processor for that effect type exists in the buildā¦)