Rust and Godot 4
Jan Walter August 30, 2023 [LANG] #rust #godotWhile looking into graphics APIs for Rust, I stumbled across Teaching native graphics in 2023, which made me think that OpenGL is kind of dead, and maybe Vulkan is not the future.
Learning how to use WebGPU in combination with Rust is also an option, but I started to investigate how to do this via C++ first and you can find my current state here, it's based on Learn WebGPU (For native graphics in C++). Currently I'm stuck and therefore I looked at alternatives.
So, what about using Rust within e.g. a game engine, like
Godot? Andrej, a friend, suggested the
godot-rust project. In the examples
folder of gdext (Godot 4
bindings) you find a project dodge-the-creeps
(using Rust) which is
pretty similar to the Your first 2D
game
chapter of the Godot Engine 4.1 documentation. Let's first look at
the User Interface (UI) of the 2D game:
It consists of two Labels (score on top, message in the middle), one Button (start at bottom), and a Timer.
The GDScript version mentioned in the Godot Engine 4.1 documentation looks like this:
extends CanvasLayer
# Notifies `Main` node that the button has been pressed
signal start_game
# Called when the node enters the scene tree for the first time.
func _ready():
pass # Replace with function body.
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func show_message(text):
$Message.text = text
$Message.show()
$MessageTimer.start()
func show_game_over():
show_message("Game Over")
# Wait until the MessageTimer has counted down.
await $MessageTimer.timeout
$Message.text = "Dodge the\nCreeps!"
$Message.show()
# Make a one-shot timer and wait for it to finish.
await get_tree().create_timer(1.0).timeout
$StartButton.show()
func update_score(score):
$ScoreLabel.text = str(score)
func _on_message_timer_timeout():
$Message.hide()
func _on_start_button_pressed():
$StartButton.hide()
start_game.emit()
So, how does that translate to Rust? There are no classes in
Rust, therefore extends CanvasLayer
becomes:
use ;
use *;
...
The functions which are not empty (calling just pass
) translate like
this to Rust:
The GDScript version of Main
defines a variable score
and
exports a PackedScene
with the name mob_scene
:
extends Node
@export var mob_scene: PackedScene
var score
# Called when the node enters the scene tree for the first time.
func _ready():
pass
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
pass
func game_over():
$ScoreTimer.stop()
$MobTimer.stop()
$HUD.show_game_over()
$Music.stop()
$DeathSound.play()
func new_game():
score = 0
$Player.start($StartPosition.position)
$StartTimer.start()
$HUD.update_score(score)
$HUD.show_message("Get Ready")
get_tree().call_group("mobs", "queue_free")
$Music.play()
func _on_mob_timer_timeout():
# Create a new instance of the Mob scene.
var mob = mob_scene.instantiate()
# Choose a random location on Path2D.
var mob_spawn_location = get_node("MobPath/MobSpawnLocation")
mob_spawn_location.progress_ratio = randf()
# Set the mob's direction perpendicular to the path direction.
var direction = mob_spawn_location.rotation + PI / 2
# Set the mob's position to a random location.
mob.position = mob_spawn_location.position
# Add some randomness to the direction.
direction += randf_range(-PI / 4, PI / 4)
mob.rotation = direction
# Choose the velocity for the mob.
var velocity = Vector2(randf_range(150.0, 250.0), 0.0)
mob.linear_velocity = velocity.rotated(direction)
# Spawn the mob by adding it to the Main scene.
add_child(mob)
func _on_score_timer_timeout():
score += 1
$HUD.update_score(score)
func _on_start_timer_timeout():
$MobTimer.start()
$ScoreTimer.start()
It's interesting how score
gets translated to a Rust member of
the struct Main
, and in addition to mob_scene
there are two
optional pointers to the two instances of AudioStreamPlayer
, music
and death_sound
:
use crate Hud;
use crate mob;
use crate player;
use ;
use *;
use Rng as _;
use PI;
// Deriving GodotClass makes the class available to Godot
The GDScript can just access $Music
and $DeathSound
to play or
stop playing the music. The Rust version seems to gain access at
the right time, during the call of Main::ready
, even though the
GDScript equivalent just calls pass
. Later we need a &mut
(mutable reference) to AudioStreamPlayer
to call play()
or
stop()
, and therefore the Main
struct provides functions music()
and death_sound()
to provide such a reference.
Here the GDScript version for Player
:
extends Area2D
signal hit
@export var speed = 400 # How fast the player will move (pixels/sec).
var screen_size # Size of the game window.
# Called when the node enters the scene tree for the first time.
func _ready():
screen_size = get_viewport_rect().size
hide()
# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
var velocity = Vector2.ZERO # The player's movement vector.
if Input.is_action_pressed("move_right"):
velocity.x += 1
if Input.is_action_pressed("move_left"):
velocity.x -= 1
if Input.is_action_pressed("move_down"):
velocity.y += 1
if Input.is_action_pressed("move_up"):
velocity.y -= 1
if velocity.length() > 0:
velocity = velocity.normalized() * speed
$AnimatedSprite2D.play()
else:
$AnimatedSprite2D.stop()
position += velocity * delta
position = position.clamp(Vector2.ZERO, screen_size)
if velocity.x != 0:
$AnimatedSprite2D.animation = "walk"
$AnimatedSprite2D.flip_v = false
# See the note below about boolean assignment.
$AnimatedSprite2D.flip_h = velocity.x < 0
elif velocity.y != 0:
$AnimatedSprite2D.animation = "up"
$AnimatedSprite2D.flip_v = velocity.y > 0
func _on_body_entered(body):
hide() # Player disappears after being hit.
hit.emit()
# Must be deferred as we can't change physics properties on a physics callback.
$CollisionShape2D.set_deferred("disabled", true)
func start(pos):
position = pos
show()
$CollisionShape2D.disabled = false
Verse the Rust version:
use ;
use *;
One thing worth mentioning here is that I noticed a mismatch between the two Godot 4 scenes (one using GDScript, the other one Rust):
This explains why the GDScript version uses up
and walk
whereas the Rust version uses up
and right
.