godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.08k stars 69 forks source link

Make Godot C# objects serializable #8335

Open barrrettt opened 8 months ago

barrrettt commented 8 months ago

Describe the project you are working on

Multiplayer with c# I need send DTOs.

Describe the problem or limitation you are having in your project

my case: I need to send a DTO to the Networking Peers, like this one:

using System;
using System.Collections.Generic;
using Godot;

[Serializable]
public class PlayerDataDTO{
    public string PlayerToken { get; set; }
    public int NetId { get; set; }
    public string Name { get; set; }
    public Color Color { get; set; }
    public int Aspect { get; set; }

    //this for Vector3
    public Vector3 PlayerPosition { get; set; }
}

Describe the feature / enhancement and how it helps to overcome the problem or limitation

I believe that some types like Vector3, Vector2, Transform... should be able to be serialized.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

put [Serializable] in top of class Vector3 (Object?) šŸ˜„

If this enhancement will not be used often, can it be worked around with a few lines of script?

I have to do a hack like this one.

//hack
public string PlayerPositionSerialized { get; set; }

//getter+setter for des/serialize vectors3
public void SetPlayerPosition(Vector3 position){
    PlayerPositionSerialized = $"{position.X};{position.Y};{position.Z}";
}

public Vector3 GetPlayerPosition(){
    string[] parts = PlayerPositionSerialized.Split(';');
    if (parts.Length == 3 && 
        float.TryParse(parts[0], out float x) && 
        float.TryParse(parts[1], out float y) &&
        float.TryParse(parts[2], out float z))
    {
        return new Vector3(x, y, z);
    }
    return new();
}

Is there a reason why this should be core and not an add-on in the asset library?

Vector3, Vector2, Transform.. are core clases

Thanks! šŸ˜ƒ

raulsntos commented 8 months ago

The [Serializable] attribute is only used for binary serialization which can be dangerous and not recommended. I feel like .NET has moved away from the attribute: The new System.Text.Json doesn't use it, they provide their own custom attributes for serialization or users can create custom converters. The vector types in System.Numerics also don't use the attribute.

If you are using the Godot Multiplayer APIs, these types should already be serialized correctly when sending them, your class needs to derive from GodotObject (more specifically, you probably want to derive Resource here) and annotate the properties with the [Export] attribute as needed.

It's also possible that whatever you are using for serialization allows to provide custom converters, or you can create a custom Vector3DTO type to be used in your DTOs instead of the real Vector3 type. You may also be interested in using the GD.VarToBytes/GD.BytesToVar and GD.VarToStr/GD.StrToVar APIs that allow you to convert Variants (like Vector3) into an array of bytes or a string in a way that can be converted back into the original Variant type.

barrrettt commented 8 months ago

Iam with .net 6.0 [Serializable] work with JSON. look:

image

image

Finally

using System;
using System.Collections.Generic;
using Godot;

[Serializable]
public class CharacterHumanoidData{
    public string PlayerToken { get; set; }
    public int NetId { get; set; }
    public string Name { get; set; }
    public Color Color { get; set; }
    public int Aspect { get; set; }

    //this for Vector3
    public string PlayerPositionSerialized { get; set; }

    public CharacterHumanoidData(){
        //randomiza a color
        List<Color> colors = new() {
            new(0.988f, 0.741f, 0.698f),
            new(0.47f, 0.328f, 0.281f),
            new(0.267f, 0.177f, 0.147f),
            new(0.925f, 0.416f, 0.373f),
            new(0.982f, 0.778f, 0.511f),
        };
        Random rnd = new();
        int iColor = rnd.Next(0, colors.Count);
        Color = colors[iColor];

        //zero position
        SetPlayerPosition(Vector3.Zero);
    }

    //getter+setter for des/serialize vectors3
    public void SetPlayerPosition(Vector3 position){
        PlayerPositionSerialized = $"{position.X};{position.Y};{position.Z}";
    }

    public Vector3 GetPlayerPosition(){
        string[] parts = PlayerPositionSerialized.Split(';');
        if (parts.Length == 3 && 
            float.TryParse(parts[0], out float x) && 
            float.TryParse(parts[1], out float y) &&
            float.TryParse(parts[2], out float z))
        {
            return new Vector3(x, y, z);
        }
        return new();
    }
}

It would be good if Vector3 were serializable, and not having to do this kind of hacks.

AThousandShips commented 8 months ago

Vector3 has [Serializable] so I'm not sure what you are missing here to make it work

barrrettt commented 8 months ago

Color works. why vector3 not?

AThousandShips commented 8 months ago

Are you sure you're doing things right? Because they have the same setup

barrrettt commented 8 months ago

look: i change to Vector3 again image and print image result: image

May be a bug?

AThousandShips commented 8 months ago

As you can see the serialization of Color isn't correct either as it saves multiple, mutually overlapping, values like both RGB and HSV/L, so it isn't working correctly for this setup with that either

barrrettt commented 8 months ago

You're right. The color is serialized in a strange way. RGBA should be sufficient. In my tests, the color was cloned correctly between pairs, so I hadn't noticed. Could it be some kind of bug in the mapping functions of these classes? I don't have any unusual configuration, I simply use [Serializable].

AThousandShips commented 8 months ago

Irs probably just that the [Serializable] tag isn't reliable or safe as pointed out above and you should use your own methods for it instead to be safe

raulsntos commented 8 months ago

System.Text.Json ignores the System.Runtime.Serialization attributes (see https://github.com/dotnet/runtime/issues/29975). It serializes all public members by default, regardless of whether the class is annotated with the [Serializable] attribute or not. You can customize the serialization using custom converters or contracts.

TWith2Sugars commented 8 months ago

It's worth noting that the [Serializable] attribute will eventually be marked obsolete: https://github.com/dotnet/designs/blob/main/accepted/2020/better-obsoletion/binaryformatter-obsoletion.md

barrrettt commented 8 months ago

Yep. [Serializable] is not necessary for jsons. and some examples: https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/how-to?pivots=dotnet-8-0

AdamLearns commented 1 week ago

For those searching for copy/paste-able code, here is a quick converter I wrote based on @raulsntos's comments in this thread that you're welcome to use (consider it MIT-licensed):

using System;
using System.Text.Json;
using System.Text.Json.Serialization;
using Godot;

public class Vector2Converter : JsonConverter<Vector2>
{
    public override Vector2 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return GD.StrToVar(reader.GetString()).AsVector2();
    }

    public override void Write(Utf8JsonWriter writer, Vector2 value, JsonSerializerOptions options)
    {
        writer.WriteStringValue(GD.VarToStr(value));
    }
}

The resulting JSON value will look like this: "Vector2(78.4229, -36.6107)".

Don't forget to actually use the converter when serializing and deserializing:

JsonSerializerOptions options =
    new()
    {
        WriteIndented = true,
        Converters = { new Vector2Converter(), }
    };

// JsonSerializer.Serialize(yourData, options);
//   or
// JsonSerializer.Deserialize<YourData>(jsonString, options)