This writeup assumes you are going to be using Godot and C#, rather than GDScript and that you have at least a bit of proffiency in C#. While some of these tips are language agnostic, just know that the scripting parts of this post are only about C#.

Content

If you wish to skip to the actual differences, press here

From United to Divided

So starting some time in 2024 I decided I needed to spend some more time learning other engines than Unity. Don't get me wrong Unity is an amazing tool that I have used for years and while it has it's quirks (what engine doesn't?) I still generally liked using it and could conjure up prototypes for games and use it in game jams with ease. My last achievement was making a fully working game jam project in about 6 hours at 2025's Nordic Game Jam. I wasn't going to participate this year, but an idea hit me and I just started rapidly creating and Unity was perfect for that as I knew the tool so well. I also worked, on, some, bigger projects in Unity and it really showcases just how versatile the engine is.

All that said, there is a big ugly truth; Putting all your eggs in one basket is never a good idea. If anything happens to your basket, you are toast. No more eggs...for your toast. And Unity has certianly been in hot water over the last few years to the point where, I don't really know if spending all my time in Unity is really a good idea anymore as happy as I'd be. They need to earn my trust back after they tried to pull a fast one with changing TOS and EULA language to be very unfavourable to developers and remove their accountability repository so it was harder to track those changes. I'm sure they can make it back in time, but in the meantime what then?

Always coming, never arriving

Okay, that header is a bit of a joke because while Godot, the stageplay, is about two people talking while waiting for their friend Godot who never arrives, the Engine Godot is an engine that you rarely wait on once you get it up and running. Although the amount of open Pull Requests might suggest otherwise...they are a productive bunch.

To make one thing clear; Godot is not a Unity replacement. As of writing this (June 2025) it just defacto isn't. It's a great engine in it's own right of course, especially if your goal is to do 2D games. It got that down fairly well. However 3D? There is still ways to go. Their shader API is currently the wild west and while it's somewhat on purpose to give the users a lean API so they can build anything on it they want, there is a different between lean and bare minimum, in my humble opinion. That's just one part though. There are many places where the engine is just not mature enough yet and I don't really blame it for that either. The team behind the engine are working really hard to get it up to snuff to compete with the titans like Unreal and Unity and they will get there eventually. There are some people who have made incredible things with Godot though it just isn't as straight forward yet as it could be.

So if you, like me, is coming from Unity and want to dive into Godot, then I have listed some observations below that you can make use of. It's by no means a complete list and part of this list might even be wrong by the time you find it (current version of Godot is 4.4) so be sure to check up on things before you accept what you are about to read at face value.

Change is Hard

Godot has a C# API that lets you use C# to write all your code rather than the engine's native scripting language GDScript. If you'd rather use GDScript then this writeup is not for you. The engine has had a C# API since Godot 3, but it isn't until Godot 4 that they seemingly took the language a lot more seriously. Likely due to the influx of Unity refugees, but who knows?

However as of writing this (June 2025) C# still kind of feels like a second class citizen in the engine and that's because it kind of is. Every language currently gets treated like GDScript by the engine which means any benefits you could have gained from individual language strengths are kind of void when run through the GDExtension layer. This will likely change in future, but just something to keep in mind. But all that said, C# is still plenty capable and is, to me, a better language to write in. People have their preferences though so please don't let this be a declaration of war on GDScript enjoyers. It's not. Some of the things you need to get used with Godot C# coming from Unity is that the scripting is not as flexible as it is in Unity. What do I mean by that?

Well in Unity you could have the following code:

public class MyComponent : MonoBehaviour
{
    private void Start()
    {
        Debug.Log("Start");
    }
}

and this component could be added to any GameObject in your scene that you had available. But that's not what you do in Godot. In Godot you have to extend the type of Node you are adding the script to, which means that the closest you could get to the above would be something like:

public partial class MyScript : Node
{
    public override void _Ready()
    {
        GD.Print("Ready");
    }
}

And then you have to add that script to a Node type in your scene (the base type that all other nodes derive from). But if you wanted to move that script to, say, a Node3D you'd have to change the class you extend in your script from Node to Node3D instead. You need to have clearer intentions behind your scripts and what you attach them to, basically. Some might call this a strength, others a weakness. I lean more to the latter personally since I feel like you lose out on a lot of flexibility but you get used to it rather quickly I'd say. You'll also notice that all scripts written in Godot like this are partial classes. That's just how they roll.

Another thing to note about Scripting in Godot C# is that often you might not find C# examples when you look for help, but fret not. A lot of the time the Godot API is almost exactly 1:1 with the GDScript API because the backing code for C# bindings are largely auto-generated. There will be some things you can't do 1:1 but that's usually more some specific operations, rather than API related. For example, GDScript lets you manipulate one component of a Vector by just doing, for example, myVector.x = newValue. But in C# you can't do that without replacing the whole Vector as those components usually live in C++ land, not C# land. Just something to keep in mind if you convert code you see in GDScript to C#.

With that, here you have some things you can look out for in no particular order.

Nodes

You can always search all properties by looking at the top of the Object you are currently working with in the Inspector. There is a "Filter Properties" field. You should also know that Godot only allows one script per node because the expectation is that you'll be building your scenes up with Nodes and have scripts that makes use of those Nodes. So it's more "Composition by Nodes" than "Composition by Scripts". Another thing to note is that the Node type can be used in scenes for organisation of other nodes, but beware that it has no transform so it will always appear on top of everything else. If order matters, use Node2D or Node3D as the parent as they do have transforms and adhere to ordering. The further up in the Scene hierarchy something is the further behind everything else it is.

Scenes

Every single thing in Godot that you make is a Scene with Nodes (with the only exception being Resources). In Unity there is a distinction which is Prefabs and Scenes but in Godot it's all scenes. So anytime you make a "prefab" in Godot you are really just making another scene that will be added to wherever we need it (*.tscn files). This might take a bit to get used to, but it's not that complicated. To refer to a scene in C# you need a PackedScene variable.

Axis

The Coordinate system in Godot is Y-Up with -Z as forward in 3D space. The Screenspace Coordinates (2D in general and UI) starts in the upper left corner which means to go up the screen you go in the -Y direction and to go down the screen you go the +Y direction. Going right is +X and left is -X as you'd expect. This felt unintuitive to me to start with but you get used to it. It's just another convention. Another thing that you might be used to in Unity are the "standard" directions like Up, Down, etc.

In Godot you can either use the Global directions found on the vector types like Vector3.Up or Vector2.Right or you can make use of the local directions through the Basis that all Transforms have. So the equivalent of Unity's transform.forward would in Godot be -Basis.Z (notice it's Negative Z for Forward in Godot).

Collision

Any object that can collide has the Collision Property Set (like CollisionObject2D/3D) and it consists of two things; Layers and Masks. A Layer is where an object exists and a Mask is where the object will look for collisions with other objects to trigger collision events. If you need to quickly make a new type of layer you can right click any of the numbers in the layer mask UI and Rename that layer.

When you setup nodes that can collide with other nodes there are a couple of setups to choose from:

  • Area2D/3D: This is something that works like a Trigger. It can detect collisions through events such as BodyEntered but will not prevent passing through it. It should be the parent node of an object that needs to act as a trigger. When you set it up it will complain that for it to work it needs a CollisionShape. You add that as a child node of the Area object and now that CollisionShape will complain that it has no shape. To fix this, go to the Inspector on the far right, click the Shape dropdown, give it a new shape type (there are many basic ones to pick from) and then you can configure the shape's size. From here the Area2D/3D can now report collisions. Areas also have the bonus that they can react to mouse enter/exit inputs which is built-in.
  • Rigidbody2D/3D: This is fairly straight forward coming from Unity. Again, you need to give it a collision shape to work. But otherwise it works about as you'd expect. If you need to change it's properties you might want to look at giving it a physics material, or change stuff in the Linear and Angular categories for stuff like Damping.
  • StaticBody2D/3D: This type cannot be moved by external forces and won't interact with other physics objects if moved by code. Great to act as doors or walls. Like the other types, it needs a Collision shape to properly work.
  • CharacterBody2D/3D: This is akin to Unity's CharacterController. You are given a lot for free and you need to move the body through Code. It needs a Collisionshape child node to work like the other types.

These can all consist of multiple collision shape nodes.

Here is some example code to move a CharacterBody2D using W/A/S/D:

public partial class Player : CharacterBody2D
{
    [Export] private float speed = 60f;

    private static Vector2 Direction => Input.GetVector("move_left", "move_right", "move_up", "move_down");

    public override void _PhysicsProcess(double delta)
    {
        Velocity = Direction * speed;
        MoveAndSlide();
    }
}

If you've seen Unity code then this should be fairly straight forward to parse. Direction is a convenience property I made to read the output of the Godot method Input.GetVector() which lets you define four axes and then it'll give you a Vector2 back with normalized values. Very handy and something that, when you look at it, you have to wonder why Unity doesn't have that. The inputs are defined in the Input Manager in the Project Settings. MoveAndSlide() is a method found on the CharacterBody2D/3D which does a lot of work for you basically to ensure that the body can move with other physics bodies and whatnot. It's again a wonder Unity doesn't have something like this.

It's all running in the _PhysicsProcess loop which is equivalent to Unity's FixedUpdate. Now there is one issue with using strings as directional names as you see me do in the Input.GetVector() call, but I'll get to what the issue is in the StringName section and how to avoid the issue.

Events

While Godot has the Signal type, using signals in Godot C# has clunky UX. So instead where we can, we can make use of C# events. They are more performant than Signals anyway. But if we need to use signals for something specifically there is a C# setup for signals which you can find in their documentation. They are more useful than UnityEvent as they allow for passing more things in method signatures, however the way you bind the signals is very clunky in C# and needs work still to work as well as it does in GDScript.

Groups

In Unity we are used to having Tags. In Godot they are called Groups and are slightly more useful than Tags are in Unity. You can make Level specific Groups (so groups that only works within a specific scene, remember everything is scenes) or you can make Global Groups that work anywhere. So if you have, say, a Group of "obstacles" you put every obstacle in that group and now you can find all objects that is in that Group with a code call in whatever game scene is currently loaded. This is an easy way for example to make a "player" group with just the player in it and just use IsInGroup() to find out if something that we collided with is the Player for example.

On that note, in Godot when you call something like GetLastSlideCollision() which gives you a KinematicCollision2D/3D back (or null if nothing was collided with) you'd be interested in the GetCollider() method to get what you actually collided with. Problem is, you can't call IsInGroup() on that because that returns a GodotObject not a Node derived type. Why is that? Because in Godot you can work entirely without using the engine itself to work with data and simulation through their many different "Servers". Like the AudioServer, DisplayServer or something else. They have many kinds. Since GDScript is dynamically typed it does not care what you call is_in_group() on. It'll just return false if the type isn't a Node. But we don't get that in C# so we have to do the cast ourselves to a Node derived type and then call IsInGroup on that.

I made an extension method for this which you can find on the Star Grease Studio - Public Repository (all code is MIT licensed) which does both the casting and return the cast object as an out parameter if it succeeds. That means you can use it like this:

private void HandleCollisionChecking()
{
    KinematicCollision2D collision = GetLastSlideCollision();
    if (collision == null) return;
    if (!collision.GetCollider().IsInGroup("pushable", out RigidBody2D rigidBody2D)) return;

    Vector2 collisionNormal = collision.GetNormal();
    rigidBody2D.ApplyCentralForce(-collisionNormal * pushStrength);
}

Very compact and it saves you from having to write extra lines to do the cast that you'd be forced to do anyway.

AutoLoads / Globals

Godot has a system that lets you decide what Scenes to instantiate before everything else and have them always be present even between scene loads. In Unity this was done by marking an object DontDestroyOnLoad() in Code, however in Godot you do it by going to Project->Project Settings->Globals. Here you can tell it what Nodes to instantiate and what to call them. This is very useful for singletons as we can define them as we are used to in C# and be sure that it only gets instantiated once and is always accessible before everything else loads.

The "Global" flag does nothing for the C# version, it's only relevant for GDScript.

GlobalClass

In C# with Godot every class you make as an extension of a Node type is a partial class of the extended type. That means if you are adding a script to, for example, a CharacterBody3D then you have a public partial class MyScript : CharacterBody3D. That's just how they decided to do it in Godot. If you wish to make the type you made into a Node that can be instantiated from Godot's Node browser, you can put the [GlobalClass] attribute on the class and build. Then it will appear as a Node type in Godot's Node selection screen.

Exports

When you wish to make a variable editable in the Editor you can do so by putting the [Export] attribute in front of a field you wish to expose. It can be private or public. Be aware that not all types will be serializable this way (usually only primitive types and Godot Node types) and be aware that Enums specifically are not supported in the editor (why I do not know). If you have an Enum that you want to serialize for editor usage like that, there are other options, but just be aware that you can't serialize Enums in the editor by default. In Unity you'd use the [Header("")] attribute to denote a grouping of variables, in Godot it's called an [ExportGroup("")]. You can change how an Export variable presents in the editor by using what Godot calls PropertyHints (for example one in Unity is [Range(min,max)]). You can see more about those in Godot's Documentation.

Shortcuts

When working with Godot you will be using Ctrl + A a lot. It adds a new Node to your scene. You can also instantiate other scenes you made previously by pressing Ctrl + Shift + A. Additionally one really nice feature in Godot is placing objects on the first collidable surface under another object. If you have an object that is floating in space because you just added it to a scene and you want it to be placed on the floor, simply pick the object in the scene and press Page Down. It will be moved to the nearest floor. (Why does Unity not have this?!)

Circular Dependencies

It's easy to accidentally create circular dependencies with Godot using Scenes. For example if you make a portal that teleports the player from one scene to another, and the destination scene has a teleporter that teleports you back again, you could be tempted to use PackedScene as the Export variable type and just drag the corresponding scenes into the teleporter variable fields. But this will create a circular dependency and Godot will be unable to open both scenes. To prevent this, instead of doing this:

[Export] private PackedScene nextLevel;

Do this:

[Export(PropertyHint.File, "*.tscn")] private string nextLevel;

Godot will then create a field that lets you pick a scene from the folders, but it will automatically put the path of the file in the field rather than the file itself. This incidentally also lets you make path fields with any file extension filters which is quite handy.

StringName / NodeName

Specifically in C#, these two types can cause performance issues in the long run. They are not a problem in smaller projects though and NodeName in particular is usually only relevant in Initialization code and will not be problematic usually. Essentially Godot uses StringName internally a lot to find names of Methods, Properties, Fields and Nodes. For example there are methods that will require a StringName to get something for you and Godot has a bunch of pre-made StringNames which are statically available in Node types, for example Area3D.PropertyName.Gravity or CharacterBody3D.MethodName.GetVelocity. Why does this matter? Because it's computationally expensive to allocate a new StringName and if you pass a string off to a StringName field an implicit conversion happens, which allocates a new StringName object. In cases where it's possible (like Input Reading in _Process() calls), pre-allocate StringName types ahead of time like private readonly static StringName moveRight = "move_right"; (readonly static is preferred for best performance) and then pass that to methods like Input.IsActionPressed(moveRight); instead of Input.IsActionPressed("move_right");. Anywhere you see the API ask for a StringName, consider if you can make a static readonly StringName variable instead and pass that.

This is one of the biggest differences between C# in Godot and GDScript as GDScript does not have this problem. So just beware of it as it can become a performance hog. StringNames are used in all sorts of places like Animation, Input and even in calls like IsInGroup, so where we use them a lot we should consider if we can pre-allocate those StringName objects and pass them instead of a string.

Deferred Calling

Some times you need to disable an object or transition away from a scene, but the responsible object is updating the physics loop. Godot will complain that it's not allowed to remove a Physics object in the middle of the Physics Processing call and will work in Editor, but won't work in a build and would cause a game crash. To solve this, you can do a deferred call in multiple ways. A deferred call is just waiting until the end of the frame before it executes. The most common ways are:

  • GetTree().CallDeferred(<stringname>,<argument>); - When we need to call a method that would normally be called on the SceneTree itself we can do it this way. For example GetTree().CallDeferred(SceneTree.MethodName.ChangeSceneToFile, nextScene);. This is the most Godot Native way to do a Deferred Call.
  • Callable.From(() => <method invocation>).CallDeferred(); - In cases where we need to do something deferred but it's not a Godot Native call, we can use the built-in Callable type instead to do it as seen above. This type is just a catch-all solution so it's alright to use for this. Here is an example of how that could be used:
    Callable.From(() =>
    {
    sprite2D.Visible = false;
    collisionShape2D.Disabled = true;
    }).CallDeferred();

    Incidentally the Callable type is needed in some Godot methods to handle callbacks and is good to know.

Tweens & Timers

Godot has a built-in tweening library. You can easily create a tween by calling GetTree().CreateTween(). A Tween object can be used in all sorts of ways and can be chained together with other Tween operations to create a chain of changes like animating properties, having tween callbacks, etc. Read about Tweens.

Godot also has Timers which, as you'd expect, can be used to time things over seconds. It can either be a repeated timer that goes every X interval or it can be a countdown you use momentarily. Timers can be created as Nodes in the editor or they can be created dynamically on the fly using GetTree().CreateTimer(). The way you could use that would for example be:

GetTree().CreateTimer(0.1f).Timeout += () =>
{

};

This would create a timer and 0.1 seconds later it would call whatever is in the anonymous method body. Very handy for timings.

Conclusion

I hope you learned something from this or at least found it helpful. I've had to learn a lot and without the communities that do exist out there I'd not have been able to get as far as I did in as short of a time as I did. While I want to do things in the Unity way in Godot often, whenever the engine seems to do things in an obtuse way or do things weirdly, I ask if something I'd do in Unity is possible in Godot and then give the Unity equivalent to compare to. Usually this is fruitful and gets me Godot specific ways to do things in the engine.

Happy Making :)

Previous Post Next Post