Migrating to C#
This is the fifth tutorial in a series that introduces you to the Godot Engine, version 4. It follows Clocks With Rings, converting our code to C#. Like with the previous tutorial, you can either continue working in the same project, make a duplicate project, or download the repository.
Godot Engine .NET
This tutorial requires Godot Engine .NET. The previous tutorials could've been made with either the regular or the .NET version. If you don't have the .NET version, you can download the latest version and install is alongside the regular version.
.NET
.NET, pronounced as dot net
, is an app framework managed by Microsoft. It is a separate runtime app used to execute programs written for it. Thus the Godot .NET version requires the .NET framework to be installed on your system. If that isn't the case Godot .NET should notify you of this when it first runs.
The main reason for using .NET is typically to be able to program in C#, pronounced as C sharp
. Another reason is performance, but C# isn't always guaranteed to be faster than GDScript in a meaningful way.
What about C++?
It is also possible to use extensions or modules to write C++ code for Godot, or even modify the engine code itself. This has potential to be the most performant, but is generally considered harder to work with than C#. So I won't cover C++ in my tutorials. There are also other programming languages made available via third-party extensions, but I won't cover those either.
Roughly speaking, the .NET compiler translates C# code to an intermediate language that gets executed by the .NET runtime. The runtime can then compile the code further while the app is running, converting it into a high-performant native level one. This is known as just-in-time compilation, JIT for short.
.NET apps currently don't work on all platforms, or it requires specific settings to make them work. It used to not be an option for apps needing to be fully natively compiled and self-contained, using ahead-of-time AOT compilation. An option for that was recently added to .NET, but is still new. See the Godot C#/.NET online documentation for the most recent state of things.
How can GDScript run on all platforms?
GDScript is a fully interpreted language. This means that it isn't compiled like a native app at all. Instead, roughly speaking, the Godot app uses the parsed code when running to look up which existing functions to execute. This is a high-level approach that is very flexible but has a lot of overhead. This is why GDScript code runs slower than compiled languages like C#.
C# Editor
Godot allows you to edit C# code, but its support for this is minimal. You'd typically use another app for that, usually an integrated development environment, IDE for short, which provides many quality-of-life features for program development.
You can use whichever app you like, but I use Visual Studio Code, also known as VS Code. Note that this is not Visual Studio, but a multi-platform tool that is in many cases free to use. To properly use it for C# you have to install the C# Dev Kit extension of VS Code, or at least the C# extension.
What license does VS Code have?
It is nominally open source, but the extensions that make it really useful have different licenses that boil down to it not being free for companies of sufficient size. Check the licenses to be safe.
You can configure your external editor via the editor settings, under General › Dotnet › Editor › External Editor. Once you have enabled an external editor it will be opened when you open a C# script in Godot.
You can enable debugging of C# code via your IDE, but the specifics depend on the one you use so I don't include that in the tutorial. See the C# basics documentation page for more info.
Explicit GDScript Usage
As an introduction to C# in Godot we're going to migrate our existing GDScript code to C#. However, to make this easy we'll keep the existing GDScript code in the same project. This means that we'll end up with two clock scripts. To be able to tell these apart change the class name in clock.gd
to ClockGDScript
.
class_name ClockGDScript
Also change the relevant code in main.tscn
.
var clock := clock_scene.instantiate() as ClockGDScript
clock.start_time = ClockGDScript.StartTimeMode.RANDOM_TIME
Everyting should still work, the only noticeable change is that the inspector of the clock's root node now displays the new class name.
Clocks
We're going to create a variant of our clock scene that's controlled via a C# script. Duplicate clock.tscn
and rename it to clock_gdscript.tscn
. Rename the original one to clock_csharp.tscn
. That's the scene that we're going to change.
C# Script
Attach a new script to the root Clock
node of clock_csharp.tscn
. Either do this via its Attach Script... context menu item or use the script icon button in the scene toolbar to first detach and then attach a new script. Pick C# for the language and don't use a template. The C# naming convention for types is to use PascalCase, so we'll name it Clock
. The class name of C# scripts has to match it file name, so save it as res://Clock.cs
. This creates and attaches the new script and allows it to be opened in an external editor.
C# syntax is different than GDScript's, but they still have still has a lot of similarities. The biggest differences are that C# is fully statically typed, uses curly brackets to indicate scope, and explicitly terminates statements with semicolons, just like GDShader code. The automatically generated code defines our Clock
class. A colon is used to indicate that it extends RigidBody2D
. Unlike GDScript, C# has accessibility modifiers. Our class is declared as public
, which makes it accessible everywhere. The class is also defined as partial
, which means that some of the class's code could be defined in another file. Godot uses this to automatically generate glue code to bind Godot and .NET together.
using Godot;
using System;
public partial class Clock : RigidBody2D
{
}
The two using
statements indicate that types from the Godot
and System
namespaces can be implicitly accessed in this file. A namespace is a scope for code, like website domains and subdomains. For example, RigidBody2D
is defined in the Godot
namespace. Without the using
statement we would have to explicitly write the fully-qualified type name Godot.RigidBody2D
.
Even though Godot ignores them, it is a good practice to put our own code in a namespace as well. We do this by writing the namespace
keyword followed by its name before our class. Let's name it after our project, GodotIntroduction
.
using Godot;
using System;
namespace GodotIntroduction;
public partial class Clock : RigidBody2D
{
}
Don't we have to use a block scope for the namespace?
That is possible, but the namespace can also be defined with a single statement, which avoids an extra level of indentation.
To make our C# class available in Godot we have to register it as a global class type. We have to explicitly do this by attaching the GlobalClass
attribute to our class. This is done by writing it before the class definition, in between square brackets.
[GlobalClass]
public partial class Clock : RigidBody2D
{
}
Godot doesn't automatically pick up the changes that we made to our C# code. First we have to save the C# file. Then we have to build the .NET project separately. This can be done in Godot via the build icon button in the top toolbar, which looks like a hammer. Note that a build is also automatically triggered when the project or current scene is run.
Once we've done that our Clock
type is registered and we can use it in our GDScript code. Switch to using this type in main.gdscript
.
var clock := clock_scene.instantiate() as Clock
Note that Godot now marks the three lines below the instantiation as unsafe. It also doesn't warn us that we'd be getting errors when running the scene. The Godot editor cannot infer types of .NET code.
Running the main scene now would indeed produce errors. If Godot complains about the wrong type. First make sure that the Clock Scene property of the root node is set to the correct scene: clock_csharp.tscn
. It that is correct then the first error tells us that Godot fails to set start_time
, which makes sense because Clock
doesn't have it yet. We have to re-implement the GDScript code of our clock in C#.
Fields and Methods
The first thing that we're trying to set is start_time
, so we'll begin by adding that to our C# class. This is supposed to be a StartTimeMode
enumeration value. We have to define this enumeration type in C#, just as we did in GDScript. The difference here is that we make it explicitly public and also that the C# convention is to use PascalCase for the enumeration value names.
public partial class Clock : RigidBody2D
{
public enum StartTimeMode
{
SystemTime,
RandomTime,
FixedTime,
OffsetTime,
}
}
In GDScript we would define a top-level variable, but in C# these are known as fields. They're defined like in GDShader code, with the type preceding the name, in this case also with an accessibility modifier, again public. We don't have to give it an explicit default value.
public enum StartTimeMode
{
…
}
public StartTimeMode start_time;
To make this an exported field that can be edited via the Godot inspector we have to attach the Export
attribute to it.
[Export]
public StartTimeMode start_time;
The property will now appear in the inspector, but only after the project has been built again. The only noticeable difference between the GDScript and C# versions is that the enumeration options are labeled slightly differently.
Can we add documentation comments in C#?
Yes. Regular comments are started with //
just as in GDShader code. Special code documentation can be started with triple slashes and uses XML tags, at minimum one for a summary. However, that documentation is only picked up by the IDE and is not used by Godot. So I skip code documentation in this tutorial.
Don't we have to use the C# enum in GDScript?
We cannot use the enum type of one language in the other. They're only syntactical sugar for integers, we only have to make sure that we're consistent on both sides.
Doesn't C# have a different convention for field names?
Yes. We'll change the names later, but for the moment we stick with the names that our main script expects.
The next error is due to a missing time_scale
. Add it to our script, in the same relative place, so before start_time
.
[Export]
public float time_scale;
[Export]
public StartTimeMode start_time;
Although GDScript and C# both have the float
type they're not exactly the same. The GDScript version is 64 bits while the C# version is 32 bits. To be consistent we have to use double the amount of bits in C#, which can be done by switching to the double
type.
public double time_scale;
We should set its default value to 1.0, just like in GDScript.
public double time_scale = 1.0;
The final error is the failed invocation of the set_uniform_scale()
function. Functions in C# are also known as methods. They are defined like in GDShader code, with an extra accessibility modifier. Let's initially make it do nothing.
public StartTimeMode start_time;
public void set_uniform_scale(double scale_factor)
{
}
At this point we have dealt with all errors and our project runs, even though the clocks themselves don't anything yet, besides falling.
Switching Style
Now that we know that C# code can be accessed from GDScript let's change Clock
so it conforms to the expected code style. This mostly means that we should use PascalCase for everything we have so far, and camelCase for the method parameter.
public StartTimeMode StartTime;
[Export]
public double TimeScale = 1.0;
public void SetUniformScale(double scaleFactor)
{
}
Besides that it's common practice to never use public fields, but instead declare them as properties. These are basically methods that wrap getting and setting the value of a hidden variable. These methods can do other work as well, like type conversions or validation checks, or not directly map to a field at all. Note that top-level variables in GDScript are effectively properties.
In our case we'll define trivial getters and setters. We can do this by simply writing { get; set; }
after what used to be the field names. Nontrivial implementations would require them to have their own code blocks.
public StartTimeMode StartTime { get; set; }
[Export]
public double TimeScale { get; set; } = 1.0;
To keep the GDScript code working we have to update the names used there. This means that we have to mix naming conventions.
clock.StartTime = ClockGDScript.StartTimeMode.RANDOM_TIME
clock.TimeScale = randf_range(time_scale_min, time_scale_max)
clock.SetUniformScale(randf_range(scale_min, scale_max))
I am not exactly following the C# style guidelines as defined in the Godot documentation. It suggests uses spaces for indentation, but I use tabs.
Start Time
Next up are the configuration options for the start time. Add exported properties for the hour, minute, and second, all of type int
. C# integers are 32 bits while Godot again uses 64 bits for them. However, in this case don't have to worry about precision, so int
suffices. The 64-bit equivalent would be long
.
public double TimeScale { get; set; } = 1.0;
[Export]
public int StartHour { get; set; }
[Export]
public int StartMinute { get; set; }
[Export]
public int StartSecond { get; set; }
We can use the equivalent of the @export_range
annotation by invoking Export
as a constructor method and passing it two arguments. The first argument is PropertyHint.Range
to indicate that we're specifying a range. The second argument is a string containing the min and max, separated by a comma.
[Export(PropertyHint.Range, "-11,11")]
public int StartHour { get; set; }
[Export(PropertyHint.Range, "0,59")]
public int StartMinute { get; set; }
[Export(PropertyHint.Range, "0,59")]
public int StartSecond { get; set; }
We can recreate the group in the inspector by also adding the ExportGroup
attribute to StartHour
, passing it the group name as a string.
[ExportGroup("Fixed or Offset Start Time")]
[Export(PropertyHint.Range, "-11,11")]
public int StartHour { get; set; }
Node Configuration
Next up are the nodes, beginning with a field for the collision shape. As these are meant to be configured via the Godot inspector only it makes sense to make these private, which we can do in C# by using private
instead of public
. Private fields should be in camelCase, like local variables and parameters. This can work because because exported fields are made available in Godot regardless of their accessibility modifier. So we make it private only on the C# side.
public int StartSecond { get; set; }
[ExportGroup("Nodes")]
[Export]
private CollisionShape2D collisionShape;
The Godot C# convention is to prefix private fields with an underscore, like in GDScript, so let's do that as well. The underscore won't show up in the inspector.
private CollisionShape2D _collisionShape;
When multiple attributes are attached to the same thing it's also possible to group them in a single comma-separated list. Let's do that here as the attributes are short.
[ExportGroup("Nodes"), Export]
private CollisionShape2D collisionShape;
Follow that with the field for the visualization and the subgroup for the arms. If multiple successive fields have the same accessibility, type, and attributes we can collapse them in a comma-separated list. The group and subgroup attributes still only create a single group in the inspector.
private CollisionShape2D _collisionShape;
[Export]
private Node2D _visualization;
[ExportSubgroup("Arms"), Export]
private Node2D _secondArm, _minuteArm, _hourArm;
Configure all these properties in the Godot inspector.
Setting Uniform Scale
With all the fields in place we can implement SetUniformScale()
, following the same approach as its GDScript equivalent. Define a local Vector2 scaleVector
variable, using the parameter to construct it. In C# we have to explicitly write new
when invoking a constructor method, the exception being attributes. The scales and mass are set via assignments to Scale
and Mass
setter properties.
public void SetUniformScale(double scaleFactor)
{
Vector2 scaleVector = new Vector2(scaleFactor, scaleFactor);
_collisionShape.Scale = scaleVector;
_visualization.Scale = scaleVector;
Mass = scaleFactor * scaleFactor;
}
However, this won't work because Vector2
and Mass
expect float
values. We could solve this by explicitly casting down from 64 to 32 bits by writing (float)scaleFactor
everywhere we use it. But we can also simply declare the parameter as a float
. Godot will take care of the conversion when invoking the method.
public void SetUniformScale(float scaleFactor)
{
…
}
C# also has the var
keyword, which can only be used for variables, even though it is fully statically typed. The compiler and IDE will figure out which type is meant when var
is used. In this case we can use var
when declaring scaleVector
because we're immediately constructing a Vector2
. The guideline is to only use var
when the type is obvious when declaring the variable.
var scaleVector = new Vector2(scaleFactor, scaleFactor);
Ready
The next step to make our clock really do something is to implement a public _Ready()
method. The base node that we indirectly extend already contains this method, but it doesn't do anything. The method is declared virtually, which means that we can override it with our own version. When doing this we have to explicitly include the override
keyword. When typing that keyword the IDE usually presents a list of candidates to override.
private Node2D _secondArm, _minuteArm, _hourArm;
public override void _Ready()
{
}
Let's begin with the first case only, StartTime
being random. We can use GD.RandRange()
to get a random double
value, equivalent to using randf_range()
in GDScript. Store that value in a new field for the seconds, which we make private this time. As we do not export that field it won't be accessible on the Godot side.
private double _seconds;
public override void _Ready()
{
if (StartTime == StartTimeMode.RandomTime)
{
_seconds = GD.RandRange(0.0, 43200.0);
}
}
C# doesn't have globally available functions, that's why RandRange()
is a method of the GD
class. It's a static method, which means that it can be invoked directly on the class and isn't attached to an instance. We can pretend that the method is global by adding a using static
statement for that class after the other using
statements, with the fully-qualified type name. We can do this for Mathf
as well, which contains the Tau
constant.
using Godot;
using System;
using static Godot.GD;
using static Godot.Mathf;
namespace GodotIntroduction;
[GlobalClass]
public partial class Clock : RigidBody2D
{
…
public override void _Ready()
{
if (StartTime == StartTimeMode.RandomTime)
{
_seconds = RandRange(0.0, 43200.0);
}
}
…
}
Add the other cases as well, first only implementing the not-system-time one.
if (StartTime == StartTimeMode.RandomTime)
{
_seconds = RandRange(0.0, 43200.0);
}
else
{
if (StartTime != StartTimeMode.FixedTime)
{
}
if (StartTime != StartTimeMode.SystemTime)
{
_seconds = StartSecond + StartMinute * 60 + StartHour * 3600;
}
}
The not-fixed-time case if harder, because we're relying on a Godot dictionary, which we can retrieve via Time.GetTimeDictFromSystem()
. To keep things simple for now declare the variable using var
.
if (StartTime != StartTimeMode.FixedTime)
{
var currentTime = Time.GetTimeDictFromSystem();
}
Your IDE should give you a hint of the dictionary type, but we can also log it to Godot's console by invoking Print()
and passing it the result of invoking GetType()
on the dictionary.
var currentTime = Time.GetTimeDictFromSystem();
Print(currentTime.GetType());
This should print Godot.Collections.Dictionary
when running the clock scene, if it is set to use system time. Accessing its contents is done by accessing it like an array, which means invoking it like a function but with square brackets, passing it a key. Let's use second
and see what its type is.
Print(currentTime["second"].GetType());
This time we get Godot.Variant
, which is the container type for all basic Godot engine types. To figure out what type it represents we have to print its VariantType
instead.
Print(currentTime["second"].VariantType);
Now we get Int
, indicating that it contains a Godot integer and thus a 64-bit value. To get this value as directly as possible we can invoke AsInt64()
on the variant, which gives us a long
. Using this we can calculate the total seconds of the current time.
var currentTime = Time.GetTimeDictFromSystem();
//Print(currentTime["second"].VariantType);
_seconds =
currentTime["second"].AsInt64() +
currentTime["minute"].AsInt64() * 60 +
currentTime["hour"].AsInt64() * 3600;
The last thing that we have to do in _Ready()
is assigning a pattern choice value to the alpha component of the self-modulate color of the visualization. Initially it appears that we can simply use GDScript code, converted to use the C# conventions.
public override void _Ready()
{
…
_visualization.SelfModulate.A = Randf();
}
However, SelfModulate
is a property which returns a Color
struct value to us, which behaves different than in GDScript. Here structs are treated as primitive values. Modifying that value doesn't change the color of the node, as it's separate data. The IDE and compiler will also indicate that this is an error. As we don't need to keep the original value the solution is to assign an entirely new color to SelfModulate
. We simply set its first three components to zero. As these are C# float
values we cannot write 0.0, because that's specifically for double
values. Instead we'd either have to write 0, or 0f
to explicitly indicate that it's a float
.
//_visualization.SelfModulate.A = Randf();
_visualization.SelfModulate = new Color(0f, 0f, 0f, Randf());
Process
To finally make our clock functional we have to override _Process(double delta)
. We begin with increasing the seconds, which is straightforward.
public override void _Ready()
{
…
}
public override void _Process(double delta)
{
_seconds += delta * TimeScale;
}
Next we take care of the progress of the seconds. In C# the modulo operator %
also works on floating-point values, so we don't have to invoke an fmod()
equivalent. However, the rotation of nodes and the color channels are all float
values, so let's define the s
variable as a such as well. We only have to perform the modulo on double
values, the final division can already be done with float
values.
_seconds += delta * TimeScale;
var s = (float)(_seconds % 60.0) / 60f;
_secondArm.Rotation = s * Tau;
Do this for the minutes and hours as well.
var s = (float)(_seconds % 60.0) / 60f;
var m = (float)(_seconds / 60.0 % 60.0) / 60f;
var h = (float)(_seconds / 3600.0 % 12.0) / 12f;
_secondArm.Rotation = s * Tau;
_minuteArm.Rotation = m * Tau;
_hourArm.Rotation = h * Tau;
Followed by updating the self-modulation color.
_hourArm.Rotation = h * Tau;
_visualization.SelfModulate = new Color(
s, m, h, _visualization.SelfModulate.A);
Now our C# clocks should function just like the GDScript clocks.
Finishing Touches
In general C# code can run significantly faster than GDScript code, but only when it can do its work uninterrupted. Whenever the C# code needs to communicate with the native Godot side there can be a lot of overhead that nullifies potential speed gains. So the rule of thumb is to minimize that and stay on the C# side as much as possible.
As an example, accessing SelfModulate
triggers invocations that go to the Godot side to either retrieve or set the color, copying the data from one side to the other. We currently do this in three places. It can reduce reduced to just one place, in _Process()
, by storing the pattern choice value in a private field.
private float _patternChoiceValue;
public override void _Ready()
{
…
//_visualization.SelfModulate = new Color(0f, 0f, 0f, Randf());
_patternChoiceValue = Randf();
}
public override void _Process(double delta)
{
…
_visualization.SelfModulate = new Color(s, m, h, _patternChoiceValue);
}
Although it's only done when creating a clock that relies on the system time, using a Godot dictionary is also something that should be avoided in C#. Dictionaries are objects, which allocate space on the .NET memory heap. This memory isn't automatically released as soon as it is no longer needed. Instead, .NET memory is cleaned up by a garbage collector that runs independently. This basically means that memory usage keeps growing until the runtime decides to clean it up, requiring it to scan everything to determine what is no longer used and can be released. This can cause stuttering and frame rate drops.
The rule of thumb is to avoid object creation as much as possible, within reason. Each Clock
instance is an object, so we cannot avoid allocating those. But we can avoid the dictionaries, because .NET provides a convenient way to get the current total seconds of the day.
The System
namespace contains the DateTime
struct type. Via its static Now
property we can get the current system date and time. From there we can access its TimeOfDay
property, from which we can extract TotalSeconds
as a double
. This both simplifies our code, avoids communication between .NET and Godot, and eliminates unwanted object allocations. The result isn't exactly the same because we now start with fractional seconds, but that is not a problem.
//var currentTime = Time.GetTimeDictFromSystem();
_seconds = DateTime.Now.TimeOfDay.TotalSeconds;
//currentTime["second"].AsInt64() +
//currentTime["minute"].AsInt64() * 60 +
//currentTime["hour"].AsInt64() * 3600;
We could also switch from using Godot's random number generator to a .NET one, but we'll mix C# and GDScript clock later on, so it's better to use a single random number stream.
Note that Tau
is just a constant declared in C#, so there is no need to avoid using it. In general it is always useful to know when and where you're crossing the boundary between Godot and .NET and what creates objects.
Our Clock
script is now finished. Below is the entire C# script.
using Godot;
using System;
namespace GodotIntroduction;
[GlobalClass]
public partial class Clock : RigidBody2D
{
public enum StartTimeMode
{
SystemTime,
RandomTime,
FixedTime,
OffsetTime,
}
[Export]
public double TimeScale { get; set; } = 1.0;
[Export]
public StartTimeMode StartTime { get; set; }
[ExportGroup("Fixed or Offset Start Time")]
[Export(PropertyHint.Range, "-11,11")]
public int StartHour { get; set; }
[Export(PropertyHint.Range, "0,59")]
public int StartMinute { get; set; }
[Export(PropertyHint.Range, "0,59")]
public int StartSecond { get; set; }
[ExportGroup("Nodes"), Export]
private CollisionShape2D _collisionShape;
[Export]
private Node2D _visualization;
[ExportSubgroup("Arms"), Export]
private Node2D _secondArm, _minuteArm, _hourArm;
private double _seconds;
private float _patternChoiceValue;
public override void _Ready()
{
if (StartTime == StartTimeMode.RandomTime)
{
_seconds = RandRange(0.0, 43200.0);
}
else
{
if (StartTime != StartTimeMode.FixedTime)
{
_seconds = DateTime.Now.TimeOfDay.TotalSeconds;
}
if (StartTime != StartTimeMode.SystemTime)
{
_seconds = StartSecond + StartMinute * 60 + StartHour * 3600;
}
}
_patternChoiceValue = Randf();
}
public override void _Process(double delta)
{
_seconds += delta * TimeScale;
var s = (float)(_seconds % 60.0) / 60f;
var m = (float)(_seconds / 60.0 % 60.0) / 60f;
var h = (float)(_seconds / 3600.0 % 12.0) / 12f;
_secondArm.Rotation = s * Tau;
_minuteArm.Rotation = m * Tau;
_hourArm.Rotation = h * Tau;
_visualization.SelfModulate = new Color(s, m, h, _patternChoiceValue);
}
public void SetUniformScale(float scaleFactor)
{
var scaleVector = new Vector2(scaleFactor, scaleFactor);
_collisionShape.Scale = scaleVector;
_visualization.Scale = scaleVector;
Mass = scaleFactor * scaleFactor;
}
}
Main Scene
With our C# clock finished let's turn our attention to the main scene. We'll begin by demonstrating that we can use both GDScript and C# clocks in the same scene and after that also migrate the main scene to C#.
Mixing Clock Types
To be able to spawn both GDScript and C# clocks, replace the exported clock_scene
variable of main.gdscript
with two appropriately-named variables.
## Clock to be spawned, C# version.
@export var clock_scene_csharp: PackedScene
## Clock to be spawned, GDScript version.
@export var clock_scene_gdscript: PackedScene
Then copy the code specific to Clock
from _on_spawn_timer_timeout()
and put it in a new _spawn_clock_csharp()
function that returns the clock as a Node2D
. Then duplicate that function, rename it to _spawn_clock_gdscript()
, and adjust it to spawn the old ClockGDScript
version.
func _spawn_clock_csharp() -> Node2D:
var clock := clock_scene_csharp.instantiate() as Clock
clock.StartTime = ClockGDScript.StartTimeMode.RANDOM_TIME
clock.TimeScale = randf_range(time_scale_min, time_scale_max)
clock.SetUniformScale(randf_range(scale_min, scale_max))
return clock
func _spawn_clock_gdscript() -> Node2D:
var clock := clock_scene_gdscript.instantiate() as ClockGDScript
clock.start_time = ClockGDScript.StartTimeMode.RANDOM_TIME
clock.time_scale = randf_range(time_scale_min, time_scale_max)
clock.set_uniform_scale(randf_range(scale_min, scale_max))
return clock
Then use randf()
in _on_spawn_timer_timeout()
to invoke either spawn function half the time.
func _on_spawn_timer_timeout() -> void:
var clock: Node2D
if randf() < 0.5:
clock = _spawn_clock_csharp()
else:
clock = _spawn_clock_gdscript()
clock.position = Vector2(
randf_range(clock_radius, _window_width - clock_radius),
-3.0 * clock_radius
)
add_child(clock)
Assign the appropriate scenes to the main root node. To be able to visually distinguish between the clock versions I changed the color in the middle of the color ramp in the C# scene to dab100.
Running the project now demonstrates that both clock types can indeed be used at the same time.
C# Main Scene
The last thing that we'll do is create a C# version of the main scene. Duplicate main.tscn
and rename both to main_gdscript
and main_chsarp
. Change the script of the C# version to a new one stored as res://Main.cs
. Also disconnect the events in that scene as they now point to non-existing functions. Switch the main scene in Godot to the main_csharp.tscn
so you don't accidentally run the GDScript version in builds.
We put the Main
class in the same GodotIntroduction
namespace. This time we're only going to use the Godot
namespace, using GD
statically. Because we don't need to access Main
explicitly in Godot we don't need to give it the GlobalClass
attribute, just like we didn't gave the GDScript version a class name either.
using Godot;
using static Godot.GD;
namespace GodotIntroduction;
public partial class Main : Node2D
{
}
We'll again make the main script spawn both clock versions, so we export two PackedScene
fields.
public partial class Main : Node2D
{
[Export]
private PackedScene _clockSceneCSharp, _clockSceneGDScript;
}
Also add all other exported fields needed to match the GDScript version. In this case all floating-point values end up being used as float
, so let's define them as such, instead of as double
.
private PackedScene _clockSceneCSharp, _clockSceneGDScript;
[Export]
private float _clockRadius = 128f;
[ExportGroup("Clock Instances")]
[Export(PropertyHint.Range, "0.1,1.0")]
private float _scaleMin = 0.25f, _scaleMax = 1f;
[Export]
private float _timeScaleMin = -10f, _timeScaleMax = 10f;
[ExportGroup("Nodes"), Export]
private Node2D _bottom, _ground;
Configure the main node so it matches the GDScript variant.
Resizing
Add the C# equivalent of _on_size_changed()
along with the field to store the window width. In this case we explicitly declare the window size variable to be a Vector2I
and we cannot directly set the bottom position's Y coordinate. Also, the style I use for lines that are too long in C# is to break them up after opening parenthesis, only indenting a single level, putting all arguments on a single line if that fit, not putting the closing parenthesis on a separate line.
private Node2D _bottom, _ground;
private float _windowWidth;
private void OnSizeChanged()
{
Vector2I windowSize = GetWindow().Size;
_windowWidth = windowSize.X;
_bottom.Position = new Vector2(
_bottom.Position.X, windowSize.Y + 2f * _clockRadius);
_ground.Position = new Vector2(
0.5f * windowSize.X, windowSize.Y + 0.5f * _clockRadius);
_ground.Scale = new Vector2(windowSize.X, _clockRadius);
}
Then add the _Ready()
, in which we connect our method to the window's size changed event and also invoke it. In this case the difference with GDScript is that the signals are made available via C# events. We register a compatible method to it via the +=
operator.
private float _windowWidth;
public override void _Ready()
{
GetWindow().SizeChanged += OnSizeChanged;
OnSizeChanged();
}
private void OnSizeChanged()
{
…
}
To verify that this work you could temporarily add Print("Size changed.");
to OnSizeChanged()
, run the scene, and resize the window.
Shouldn't we also remove the method from the event at some point?
All existing Godot signals take care of this for freed nodes. Had we made a custom signal in C# this would be our responsibility. Even then explicit removal isn't needed for our main node, because it won't be freed while the app runs.
Freeing Clocks
We now skip forward to the function that takes care of freeing nodes that are no longer needed. Because this method only queues freeing of the provided body it doesn't need to access any data of the Main
instance. Hence we can declare the method as static
, indicating that it exists for the Main
type but independent of a specific object instance. This isn't necessary but a good rule of thumb to do when possible.
private void OnSizeChanged()
{
…
}
private static void OnBottomBodyEntered(Node2D body)
{
body.QueueFree();
}
C# offers a shorthand notation for single-statement methods like this one, replacing the curly brackets with an arrow, written as =
followed by >
, between the method signature and its implementation.
private static void OnBottomBodyEntered(Node2D body) => body.QueueFree();
After building the project connect to the body_entered
signal again, this time using the Pick option and selecting our method from Main
.
Spawning C# Clocks
To spawn C# clocks we first migrate the _spawn_clock_csharp
method to Main
.
private void OnSizeChanged()
{
…
}
private Node2D SpawnClockCSharp()
{
var clock = _clockSceneCSharp.Instantiate() as Clock;
clock.StartTime = Clock.StartTimeMode.RandomTime;
clock.TimeScale = RandRange(_timeScaleMin, _timeScaleMax);
clock.SetUniformScale(RandRange(_scaleMin, _scaleMax));
return clock;
}
We have to cast the value returned by RandRange()
down to float
, because we defined the parameter of SetUniformScale()
as such.
clock.SetUniformScale((float)RandRange(_scaleMin, _scaleMax));
In the same way, we could also convert the instantiation to an explicit cast instead of using the as
operator. We can go even further and use the generic Instantiate<>()
method variant. A version specifically for Clock
is made by appending it to the method name, in angle brackets. This directly gives us the node cast to the correct type.
var clock = _clockSceneCSharp.Instantiate<Clock>();
Now add an OnSpawnTimerTimeout()
method that always spawns a C# clock, sets its position, and adds it as a child node.
private Node2D SpawnClockCSharp()
{
…
}
private void OnSpawnTimerTimeout()
{
Node2D clock = SpawnClockCSharp();
clock.Position = new Vector2(
(float)RandRange(_clockRadius, _windowWidth - _clockRadius),
-3f * _clockRadius);
AddChild(clock);
}
After connecting that method to the correct signal in Godot our C# scene will spawn C# clocks.
Spawning GDScript Clocks
Finally, we also add a method to spawn GDScript clocks via C#. Because GDScript isn't inherently statically typed we cannot use the ClockGDScript
type here. Instead we instantiate the clock as a Node2D
. Then we invoke Set()
on it to set one of its properties, with the property name as the first argument and the value to set as the second argument. Invoking the set_uniform_scale
function is done in the same way, by invoking Call()
. Note that this isn't type safe and the compiler nor the IDE can tell whether we used a wrong name or value ahead of time.
private Node2D SpawnClockCSharp()
{
…
}
private Node2D SpawnClockGDScript()
{
var clock = _clockSceneGDScript.Instantiate<Node2D>();
clock.Set("start_time", (int)Clock.StartTimeMode.RandomTime);
clock.Set("time_scale", RandRange(_timeScaleMin, _timeScaleMax));
clock.Call("set_uniform_scale", RandRange(_scaleMin, _scaleMax));
return clock;
}
The last step is to make OnSpawnTimerTimeout()
also spawn both clock versions.
Node2D clock;
if (Randf() < 0.5f)
{
clock = SpawnClockCSharp();
}
else
{
clock = SpawnClockGDScript();
}
This wraps up the migration to C#. Below is the complete Main
script.
using Godot;
using static Godot.GD;
namespace GodotIntroduction;
public partial class Main : Node2D
{
[Export]
private PackedScene _clockSceneCSharp, _clockSceneGDScript;
[Export]
private float _clockRadius = 128f;
[ExportGroup("Clock Instances")]
[Export(PropertyHint.Range, "0.1,1.0")]
private float _scaleMin = 0.25f, _scaleMax = 1f;
[Export]
private float _timeScaleMin = -10f, _timeScaleMax = 10f;
[ExportGroup("Nodes"), Export]
private Node2D _bottom, _ground;
private float _windowWidth;
public override void _Ready()
{
GetWindow().SizeChanged += OnSizeChanged;
OnSizeChanged();
}
private void OnSizeChanged()
{
Vector2I windowSize = GetWindow().Size;
_windowWidth = windowSize.X;
_bottom.Position = new Vector2(
_bottom.Position.X, windowSize.Y + 2f * _clockRadius);
_ground.Position = new Vector2(
0.5f * windowSize.X, windowSize.Y + 0.5f * _clockRadius);
_ground.Scale = new Vector2(windowSize.X, _clockRadius);
}
private Node2D SpawnClockCSharp()
{
var clock = _clockSceneCSharp.Instantiate<Clock>();
clock.StartTime = Clock.StartTimeMode.RandomTime;
clock.TimeScale = RandRange(_timeScaleMin, _timeScaleMax);
clock.SetUniformScale((float)RandRange(_scaleMin, _scaleMax));
return clock;
}
private Node2D SpawnClockGDScript()
{
var clock = _clockSceneGDScript.Instantiate<Node2D>();
clock.Set("start_time", (int)Clock.StartTimeMode.RandomTime);
clock.Set("time_scale", RandRange(_timeScaleMin, _timeScaleMax));
clock.Call("set_uniform_scale", RandRange(_scaleMin, _scaleMax));
return clock;
}
private void OnSpawnTimerTimeout()
{
Node2D clock;
if (Randf() < 0.5f)
{
clock = SpawnClockCSharp();
}
else
{
clock = SpawnClockGDScript();
}
clock.Position = new Vector2(
(float)RandRange(_clockRadius, _windowWidth - _clockRadius),
-3f * _clockRadius);
AddChild(clock);
}
private static void OnBottomBodyEntered(Node2D body) => body.QueueFree();
}
This is the end of the introduction series. From here you can move on to the True Top-Down 2D series.