Preferring Immutability in Godot

Game engine development tends to rely on mutability, such as keeping up with an object's position on-screen. Typically, the game object is represented by a single class with a "position" property, and as the object moves around that property mutates to reflect its new value. This is fine because these engines need to be seriously optimized, and immutability presents major challenges for optimization.

However, if you are practicing functional-first programming in your scripting, you are likely going to want to prefer immutability. Here's an example of some more imperative and mutable code from Godot Engine Game Development p 34:

func get_input():
    velocity = Vector2()
    if Input.is_action_pressed("ui_left"):
    	velocity.x -= 1
    if Input.is_action_pressed("ui_right"):
    	velocity.x += 1
    if Input.is_action_pressed("ui_up"):
        velocity.y -= 1
    if Input.is_action_pressed("ui_down"):
    	velocity.y += 1
    if velocity.length() > 0:
        velocity = velocity.normalized() * speed
        
func _process(delta):
    get_input()
    position += velocity * delta
    position.x = clamp(position.x, 0, screensize.x)
    position.y = clamp(position.y, 0, screensize.y)

In the above example, the only variable that relies on an underlying Godot object is position. So, short of calling a Godot-provided method for updating position, that value needs to be mutated. However, the get_input method mutates a variable that was defined in scope. Let's take a shot at making this more declarative and immutable in F#:

First, if/then/else are expressions in F#, rather than statements. We'll need to return velocity as a value from the the conditional statements.

let veloX () =
    if Input.IsActionPressed("ui_left") then -1.00f
    elif Input.IsActionPressed("ui_right") then 1.00f
    else 0.00f

let veloY () =
    if Input.IsActionPressed("ui_up") then -1.00f
    elif Input.IsActionPressed("ui_down") then 1.00f
    else 0.00f

let getVeloInput () =
    Vector2(veloX(), veloY())

Then we'll take the velocity that's returned from getVeloInput and apply the normalization and speed to the result (line 2 below). Again, avoiding mutation:

override this._Process (delta) =
    let velocity = getVeloInput().Normalized() * Speed
    
    // Group all class mutation
    this.Position <-
        this.Position + delta * velocity
    this.Position <-
        Vector2(
            Mathf.Clamp(this.Position.x, 0.00f, screensize.x),
            Mathf.Clamp(this.Position.y, 0.00f, screensize.y))

Finally, we do our best to group the actual mutation at the bottom of the class method. It may not be possible to completely eliminate mutation directly in your scripts, but one could go further by using node types that have methods for updating state, such as MoveAndSlide.