This article shows how to implement common elements of a Godot script in F#.
As I mentioned in the first part F# is a rather opinionated language.Several things that are easy to do in other languages are intentionally awkward or even simply not allowed.
That means that there are some caveats that you should know about if you intend to write F# code in Godot.
This article only introduces mild changes to the object-oriented structure of the C# code. The next parts of the series will cover how to write game code that follows the functional paradigm more closely.
If you want to test the code you can download the project files.
A typical C# script
Let's have a look at a very common script in C# and then translate it to F#.
This script is supposed to do two things: move a player and keep an animation running while the player is moving.
The script contains a class called Player. This class has a member called "speed" with an initial value of 200 that can be edited from the inspector.
It also has a member called "anim" that stores a reference to an animation player in the scene. Since there is no "onready"-keyword in C# that reference is filled in the _Ready()-method.
In the _Process()-method a new movement vector is created and adjusted based on which keys are pressed. ui_right and ui_left add resp. substract from the x-coordinate and ui_up and ui_down from the y-coordinate. Then the sprite is translated by the normalized movement times the speed times the time that has passed in the last frame.
Lines 39-49 check if the movement vector was not zero (i.e. the player was moving). If the player is moving an animation is kept running. If it's not, the animation is stopped.
The same script in F#
Now let's have a look at the F# implementation of the script. If you are following along using the project files you can find the code in Library1.fs in the subfolder GodotFsBasicFs. (I like to name my F# projects with the project name + Fs, that's why in the GodotFsBasic-Project I ended up with a folder name that contains "Fs" twice.)
The first difference is in line 1: every class has to be part of a namespace or a module in F#, so the namespace in this file is mandatory.
The next difference is in line 5: the type declaration of the Player class contains the words "as this".
In F# a class is declared by writing the constructor. Lines 9-15 aren't just member declarations, they are imperative lines in the class constructor that initialize the members. So speed is set to 200 first and anim is filled next. This is different from the declarative code found in languages like C# where there is no explicit order between the declarations.
So what does that have to do with the words "as this"?
Since lines 9-15 belong to the constructor they aren't allowed to access "this" because the object isn't fully initialized yet. So if we need to access "this" we need to either end the constructor by adding public members and methods or we can allow accessing "this" from the constructor by adding the words "as this" to the very first line of the class declaration.
(Btw. this also allows to rename "this" just by using something like "as self" or "as currentPlayer".)
Since F# is a type inferred language we don't need to specify that "speed" is a float, it's inferred from the initial value. But unlike GDscript F# is statically typed, which means that the compiler will complain if we try to use speed as something else than a float.
Lines 11-15 initialize the anim member. Again there is no onready keyword, but F# has the lazy-keyword which works quite similar in this case. By wrapping something in "lazy(...)" we can define that this value should not be determined until it is needed for the first time.
Once it's calculated it's stored and not calculated again.
The downside is that we need to access the value using "anim.Value", but there are two benefits:
1. We don't need to write a _Ready-method just to fill a single member variable.
2. There is no phase between the moment when the Player is created and the time the anim-member is filled where it is invalid. In C# you can have bugs because a piece of code accesses anim before it is filled. That's why F# doesn't allow us to declare uninitialized members. By making the content of the member lazy we can solve the problem that it needs to be filled right away even though the AnimationPlayer can only be found later.
If you are new to F# you may wonder what the characters in line 14 mean - ":?>" is just F#'s symbol for C#'s "as" keyword.
If we removed line 14 we would get a Node, line 14 downcasts that Node to an AnimationPlayer.
The _Process-method in F#
Let's have a look at the _Process method. The first line that starts with "member this." or "override this." marks the end of the constructor. Any let- (or do-) binding must appear before the first member.
The first thing you'll notice that the if-else-pattern from the C# script appears only once in the F# version. Line 18 defines a function that turns the name of an input action into a 1 or a 0
depending on whether the action is currently pressed.
As I said in the preface of the first article: we could have done the same in C#, it's just that creating a new function requires so much overhead that it would probably be more confusing than helpful. Here in F# the new function adds very little overhead, and it's local to the _Process-method, so someone reading the code will immediately know that it's not relevant to the rest of the class.
Line 21 calculates the movement vector. Thanks to the check-function this code ends up being much more concise and bug resistant (who hasn't run into the occasional copy-paste error where x and y or + and - were confused).
There isn't much to be said about line 25 other than that F# requires us to explicitly call Transform on "this.", and that some people would have started the line with the optional "do"-keyword (as in "do this.Translate(...)") to signal that this line is executed because of its side effect only and doesn't have a return value.
updating the animation
Again the lines 27-31 are basically doing the same thing as the lines 39-49 in the C# script, but F#'s lightweight local functions invite to split up the functionality and write more self-documenting code.