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#:
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