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
.