valorzard

i like trains

  • any

Aspiring indie game dev. 21. Addicted to comics. Touch-starved. What even is gender anyways?
RPG webcomic thingy: @bunny-rpg


vallerie
@vallerie

preface / disclaimers

i've been meaning to try out Godot for a while, and, the recent news has meant a bunch of people are writing guides, posts, and generally giving support for people moving over. regardless of what happens long-term with Unity, it's always useful to learn new things, and, I find learning about engines I'm not familiar with to be interesting!

this is also going to focus moreso on the scripting experience, and very much on the C# side (with touches to GDScript throughout). i'm very comfortable in C#, and I've got lots of patterns I'm comfortable making games that I like using in C#.

i'll try to keep this from the least positive experiences to the... most positive?? this also isn't going to be my best writing ever - more than anything, it's for me to explain to myself my thoughts.

when Godot crashes / freezes, my Scenes often get completely corrupted. this is pretty miserable

not going to be a long section but. yeah. i've had multiple times where I've had to remake my scene, because Godot has crashed, and on reopening is refusing to acknowledge my Scene as being a Scene anymore. as far as i know, they're completely mangled.

"GDScript is amazing!" is not very helpful

i don't hate GDScript. from the little bit of GDScript I've written, it's actually quite a pleasant experience!

my issue, is it's lacking several key features (Generics being one I use all the time). i think i'll use it here and there, especially for little environmental scripting (quick checks of "when player interacts with me, do something", for example), but, bigger logic will stay in C#

however, i find online there's a big thing where you admit "i've recently moved to godot! can i have a hand with this thing in C#?" and you are treated like a fool for even trying to use C#, and big tutorials (and even some Godot maintainers) repeat that I should just use GDScript, and stop trying to be a Unity user.

it's frustrating, as I have tried to use GDScript, and found it's lacking things (like Generics, again) that make it a non-starter for the kind of programming patterns I enjoy using. i get it being a good first time user experience for some users, but, like any open source community, it feels like as a newcomer I'm being repeatedly told "your way is wrong!" instead of an understanding of why I'm doing something a certain way.

Godot really isn't happy with Generic types or, how I learnt to embrace codegen crimes

get ready for another generics rant. but, first - a quick thing on how I use ScriptableObjects in Unity

a quick thing on how I use ScriptableObjects in Unity

in Unity, I use ScriptableObjects a lot. specifically, I use them in similar ways to ways described this talk from Unite 2017. mega specifically, pretty much every project that isn't a toy uses some scriptable objects for Event dispatching, as well as sharing data between objects.

Godot resources are actually really good in this way! there's some stuff Godot does better than Unity - specifically, all resources are editable inline from anything that references them, saving lots of time that in Unity is spent writing Editor scripts, or money spent buying an Odin Inspector license.

however - to get to the issues I have with Godot resources, I gotta first show how I use Generics with Unity. Here's a quick few classes with Generics. It starts with an abstract of the smallest component - a value with a Getter.

Then, it adds a value with a setter. Finally, we add a Generic type that implements everything, for easy use later.

public abstract class AbstractSharedType<T> : ScriptableObject
{
    public abstract T Value
    {
        get;
    }
}

public abstract class AbstractSettableSharedType<T> : AbstractSharedType<T>
{
    public abstract void SetValue(T value);
}

public class GenericSettableSharedType<T> : AbstractSettableSharedType<T>
{
    [SerializeField] private T _v;
    public override T Value => _v;
    
    public override void SetValue(T value)
    {
        _v = value;
    }
}

Now, with all of this, I can define a "SharedInt" using just the following stub.

(check out that talk if you don't get why I might be doing shared data this way)

[CreateAssetMenu(...)]
public class SharedInt: SharedType<int> {}

pretty simple, and, I can define "special" types too! some quick examples

public class DeltaTimeScriptableObject : AbstractSharedType<float>
{
    public override float Value => Time.deltaTime;
}

public class MultiplyScriptableObject : AbstractSharedType<float>
{
    public GenericSharedData<float> op1;
    public GenericSharedData<float> op2;

    public override float Value => op1.Value * op2.Value;
}

these might seem useless, but, they're incredibly helpful for changing logic or design tweaks, all without needing to compile any code.

with this out of the way: back to Godot

back to Godot

so, the first issue is one that's a combo of a design decision made by Godot, and one by C#.

The Godot design decision is that every Script you write that attaches to a Node or Resource should trace back to Node or Resource respectively, and is marked as partial. This is actually pretty neat, and I do generally like this.

However; C# (at time of writing) doesn't let you have classes marked as both abstract and partial. This is fine, as I can use a virtual, but, is a bit annoying.

the second seems to be a limitation of how Godot handles [Export], or, more specifically, in it seemingly "failing loudly", so if it can't guarantee something will be compatible with [Export], compilation will fail. this is unlike Unity's [SerializeField], which will just fail silently. again, this is actually pretty neat, and for the most part I prefer this. but, for generics, it means you can't write this

public partial class GenericResource<T>: Node {
    // having [Export] will cause this to fail to compile, as Godot doesn't know how to serialize my generic T
    [Export]
    public T Value {get; set; }
}

this left me a little bit stumped. i got around this using a base Generic without the export, but with the definition (so that other parts of my codebase can recognise Value as a member of GenericResource<T> - allowing for the special types as described above to stay). then, I use a template, and a codegen script which then will generate classes for all the [Export]able data types I could want. i then add this codegen script as a prebuild step to my project, and, I'm good to go! quick demo of this below

// GenericSharedData.cs
public partial class GenericSharedData<T> : Resource
{
    public virtual T Value
    {
        get => throw new NotImplementedException();
        set => throw new NotImplementedException();
    }
}
// Generated/[paths]/SharedFloat.cs
public partial class SharedFloat: GenericSharedData<float>
{
    private float _v;

    [Export]
    public override float Value {
        get => _v;
        set => _v = value;
    }
}

honestly, this final solution is kind of nicer than my Unity one, but, only because I was driven here by force. i've been meaning to setup codegen for this usecase in Unity for the longest time, so Godot forcing my hand here is a little bit useful.

from this solution, I then relatively quickly reimplemented my Event systems, as well as EventInvokingSharedData (shared data types that fire an event with previous and new data whenever new data is set). on the subject of events -

signals are neat

genuinely! it's a pretty rad feature. it saves me lots of boilerplate, and, the fact I can have C# <-> GDScript interop is neat. it actually makes me want to learn GDScript more, as I can keep writing my architecture code in C#, where I'm comfortable, but then do silly little gameplay code in GDScript, and have Signals cross the gap for me.

i've actually gone ahead and added Signals to all my event code. I still use C# Actions at the core, but, I added a simple listener (using the codegen technique) that subscribes and then dispatches the data over three signals. the three signals are one for the struct together, one for just the old data, and one for the new.

snippet time :D

// ChangingValue.cs
public partial class ChangingValue<T>: Godot.GodotObject
{
    public T previous;
    public T current;
}
// GenericEventListener.cs
public partial class GenericEventListener<T>: Node
{
    private GenericEventChannel<ChangingValue<T>> _eC;
    // In source gen, we can add Signals here!
    [Export]
    public GenericEventChannel<ChangingValue<T>> eventChannel
    {
        get => _eC;
        set
        {
            // Ensure we properly handle unsubscribing from the old
            _eC?.Unsubscribe(EventListener);
            _eC = value;
            // Now sub to the new!
            _eC?.Subscribe(EventListener);
        }
    }

    public override void _Ready()
    {
        base._Ready();
        eventChannel?.Subscribe(EventListener);
    }

    protected virtual void EventListener(ChangingValue<T> obj)
    {
        throw new System.NotImplementedException();
    }
}
// EventListenerGodotVector3.cs
public partial class EventListenerGodotVector3 : GenericEventListener<Godot.Vector3>
{
    [Signal]
    public delegate void OnReceiveEventHandler(ChangingValue<Godot.Vector3> data);
    
    [Signal]
    public delegate void OnReceiveNewDataEventHandler(Godot.Vector3 newData);
    
    [Signal]
    public delegate void OnReceivePreviousDataEventHandler(Godot.Vector3 oldData);
    
    protected override void EventListener(ChangingValue<Godot.Vector3> obj)
    {
        EmitSignal(SignalName.OnReceive, obj);
        EmitSignal(SignalName.OnReceivePreviousData, obj.previous);
        EmitSignal(SignalName.OnReceiveNewData, obj.current);
    }
}

with [Signal] and [Export] supporting the same types, I can also reuse the same codegen systems, and have a consistent EventInvokingSharedData -> EventChannel -> EventListener -> wherever the node ends up

we love this for me. genuinely, congrats Godot contributors, signals are amazing, i love you so so much.

closing thoughts

idk. i'll continue with Godot stuff, and will probably make some projects using Godot! i do think it's interesting that my most negative experience is some weird bugs, some developer experience pain solved with custom scripting, and, the community; as is often the case with open source projects.

but, yes, i do really enjoy using godot as an engine though. but no, you cannot make me redo my architecture in GDScript


You must log in to comment.

in reply to @vallerie's post:

hell yeah I'm glad you figured it out and sorry the transition wasn't as smooth as hoped :c I hope v much that w the influx of new ex-Unity people the C# support gets better!!
excited to see what you make!

no worries at all!! the idea of the GenericSharedData is something I picked up after watching that Unite 2017 talk. it's worth a watch, even if you don't use Unity, as the concept of ScriptableObjects and Resources are extremely similar (at least, in how they can be used).

The FloatReference bit in that talk is also good watching, and, can also be genericised and put into Godot land pretty easily with codegen :eggbug-smile-hearts: