codewriter-packages / Tri-Inspector

Free inspector attributes for Unity [Custom Editor, Custom Inspector, Inspector Attributes, Attribute Extensions]
MIT License
989 stars 48 forks source link

Serializable structs with explicit layout #111

Closed dungeon2567 closed 4 months ago

dungeon2567 commented 1 year ago

Describe the bug Can`t edit area of effects values Expected behavior able to edit area of effect values

Code Sample


[Serializable]
[StructLayout(LayoutKind.Explicit)]
public struct GameplayAction
{
    [FieldOffset(0)]
    public GameplayActionType Type;

    [FieldOffset(4)]
    [ShowIf(nameof(Type), GameplayActionType.SpawnProjectile)]
    public SpawnProjectileAction SpawnProjectile;

    [FieldOffset(4)]
    [ShowIf(nameof(Type), GameplayActionType.ApplyEffect)]
    public ApplyEffectAction ApplyEffect;

    [FieldOffset(4)]
    [ShowIf(nameof(Type), GameplayActionType.ApplyAreaOfEffect)]
    public AreaOfEffectShape AreaOfEffect;

    [FieldOffset(4)]
    [ShowIf(nameof(Type), GameplayActionType.SpawnAreaOfEffect)]
    public SpawnAreaOfEffectAction SpawnAreaOfEffect;
}
// 

Screenshots

Desktop: Windows 11 **Unity version: 2020.3.35f1 image

Tri Inspector version: 1.1.01

TsFreddie commented 5 months ago

TL;DR: You can easily solve this by adding [NonSerialized] attribute to all but one field with [FieldOffset(4)]

I know this is a year-old issue, but I just did some digging.

This is actually a Unity behavior due to how Unity Serialization works, and this is also the behavior in vanilla Unity.

Consider the following struct:

[Serializable, StructLayout(LayoutKind.Explicit)]
public struct ExampleStruct
{
    [FieldOffset(0)] public ActionEnum Type;
    [FieldOffset(4)] public EnumA A;
    [FieldOffset(4)] public EnumB B;
}

If you create a serialized object that contains this struct, Unity will save the object as such:

--- !u!114 &2043024539519176523
MonoBehaviour:
  StructValue:
    Type: 0
    A: 0
    B: 0

Unity Inspector works by updating the value to a serialized representation then deserialize it to apply to the instance. You can probably see the problem - Unity will apply A, then B. But A and B are the same field!, which means any changes to A will be immediately overwritten by the already serialized B value. This is also why we can still edit B in the Inspector.

Now, despite being able to hide fields that are not relevant using Tri-Inspector, Unity Serialization stands true always. However, we can leverage the [ShowInInspector] feature to make a non-serialized field editor...

This is what I would do:

[Serializable, StructLayout(LayoutKind.Explicit)]
public struct ExampleStruct
{
    [FieldOffset(0)] public ActionEnum Type;

    [NonSerialized, ShowInInspector, ShowIf(nameof(Type), ActionEnum.TypeA)]
    [FieldOffset(4)] public EnumA A;

    [NonSerialized, ShowInInspector, ShowIf(nameof(Type), ActionEnum.TypeB)]
    [FieldOffset(4)] public EnumB B;

    [SerializeField, HideInInspector]
    [FieldOffset(4)] private int _internalField;
}

This way, you explicitly tell Unity to only maintain one true value, and only use other fields as an Editor provided by Tri-Inspector:

--- !u!114 &4228018463491459332
MonoBehaviour:
  StructValue:
    Type: 0
    _internalField: 0

I believe this issue can be closed due to not being a bug on the Tri-Inspector side, if it is a bug at all.