PONYλM2Modula-2

GDScript.CodeCompared.To/Rust

An interactive executable cheatsheet comparing GDScript and Rust

GDScript 4.7 (Godot 4.7.2) Rust 1.98.0
Hello World & Output
Hello, World
Both columns on this page run for real: the GDScript on the left executes in an actual Godot engine, and the Rust on the right compiles and runs. Where a row needs the engine — a node, an exported property — the Rust side is shown rather than run, and says so.
print("Hello, World!")
fn main() { println!("Hello, World!"); }
The exclamation mark makes println! a macro rather than a function, which is how it checks the format string against its arguments while compiling. A program needs a main because nothing is already running, unlike a script the engine loads.
Putting a value in a message
GDScript formats with % and an array of values, in placeholder order. Rust puts the name inside the braces, so there is no second list to keep in step.
var player_name := "Robi" var score := 1200 print("%s scored %d" % [player_name, score])
fn main() { let player_name = "Robi"; let score = 1200; println!("{player_name} scored {score}"); }
Because println! is a macro, a brace naming a variable that does not exist is a compile error, and so is a mismatched count. The whole class of "wrong number of format arguments" bug is gone rather than deferred.
Variables & Mutability
Everything is immutable until you say otherwise
This is the reverse of every habit you have. A GDScript var is changeable and const is the exception. In Rust let is fixed and mut is the exception you have to ask for.
var health := 100 health -= 30 print(health) const MAX := 100 print(MAX)
fn main() { let mut health = 100; health -= 30; println!("{health}"); let max = 100; println!("{max}"); }
Forgetting mut is a compile error with the fix named in the message, so it costs seconds rather than causing a bug. What you get for it is that reading a let binding anywhere means it has not changed since — which is the foundation the whole Ownership section rests on.
Inference, and when you must annotate
Rust infers from the value just as := does. The annotation is needed in the same place GDScript needs one — an empty collection, where there is no value to infer an element type from.
var speed := 200.0 var label := "boss" var counts: Array[int] = [] print(speed) print(label) print(counts.size())
fn main() { let speed = 200.0; let label = "boss"; let counts: Vec<i32> = Vec::new(); println!("{speed}"); println!("{label}"); println!("{}", counts.len()); }
Rust's inference reaches further than GDScript's: it can work backwards from how a value is used later in the function, so the annotation is often unnecessary even where you expect it. When it is required, the compiler says so explicitly.
A condition must be a bool
GDScript treats 0, "", an empty array and null as false. Rust has no truthiness at all: a condition must already be a bool.
var health := 0 var name := "" if not health: print("no health") if not name: print("no name")
fn main() { let health = 0; let name = ""; if health == 0 { println!("no health"); } if name.is_empty() { println!("no name"); } }
Note also that the condition needs no parentheses but the body always needs braces — the opposite of C-family languages, and closer to GDScript than it first looks.
Ownership & Borrowing
Passing a value can give it away
This is the idea the rest of Rust hangs on. In GDScript two names for one array are two ways to reach the same thing. In Rust assigning moves ownership, and the old name is no longer usable.
var loot := ["sword", "shield"] var same := loot same.append("rope") print(loot.size()) print(same.size())
fn main() { let loot = vec!["sword", "shield"]; let mut same = loot; same.push("rope"); println!("{}", same.len()); // println!("{}", loot.len()); // would not compile: loot was moved }
Uncomment the last line and the program does not build: borrow of moved value: loot. That is not a restriction for its own sake — it is what makes it impossible to hold a reference to something another part of the program has already thrown away.
Lending instead of giving
If passing gave the value away, every function call would consume its arguments. & lends instead: the function may read it, and ownership stays with the caller.
func total(numbers: Array) -> int: var sum := 0 for value in numbers: sum += value return sum var scores := [10, 20, 30] print(total(scores)) print(scores.size())
fn total(numbers: &Vec<i32>) -> i32 { let mut sum = 0; for value in numbers { sum += value; } sum } fn main() { let scores = vec![10, 20, 30]; println!("{}", total(&scores)); println!("{}", scores.len()); }
The last line proves the point — scores is still usable after the call. Note also the missing return: the final expression of a function is its value, and adding a semicolon there would turn it into a statement and break the build.
One writer, or many readers
A function that changes its argument needs &mut, and the caller has to write &mut at the call site too — so a mutation is visible where it happens, not only in the signature.
func add_loot(bag: Array, item: String) -> void: bag.append(item) var bag := ["coin"] add_loot(bag, "gem") print(bag)
fn add_loot(bag: &mut Vec<String>, item: &str) { bag.push(item.to_string()); } fn main() { let mut bag = vec!["coin".to_string()]; add_loot(&mut bag, "gem"); println!("{bag:?}"); }
The rule underneath is one line: you may have many readers or one writer, never both at once. That is what makes a data race a compile error rather than something you find at 3am. {bag:?} is the debug format, since a vector has no plain display form.
When you actually want a copy
GDScript's duplicate() is Rust's clone(), and both are explicit for the same reason: copying a collection costs real work, so neither language does it behind your back.
var original := [1, 2, 3] var copy := original.duplicate() copy.append(4) print(original.size()) print(copy.size())
fn main() { let original = vec![1, 2, 3]; let mut copy = original.clone(); copy.push(4); println!("{}", original.len()); println!("{}", copy.len()); }
Because clone has to be written down, a costly copy in a hot loop is visible when you read the code. Small values like integers implement Copy and duplicate silently, which is why the earlier examples could pass them around without ceremony.
Strings
There are two string types
GDScript has one String. Rust has String, which owns its text and can grow, and &str, which is a borrowed view of text someone else owns. A literal is the second kind.
var title := "Crystal Cavern" var built := title + " II" print(title.length()) print(built)
fn main() { let title: &str = "Crystal Cavern"; let built: String = format!("{title} II"); println!("{}", title.len()); println!("{built}"); }
The rule in practice: take &str as a parameter so any caller can pass either, and return String when you built something new. It is the ownership section applied to text, and it is the most common place new Rust code fights the compiler.
Case, slicing and searching
These line up nearly one for one. Slicing uses range syntax rather than a method, and the result is a borrowed view rather than a fresh string.
var title := "Crystal Cavern" print(title.to_upper()) print(title.substr(0, 7)) print(title.contains("Cave")) print(title.split(" "))
fn main() { let title = "Crystal Cavern"; println!("{}", title.to_uppercase()); println!("{}", &title[0..7]); println!("{}", title.contains("Cave")); let words: Vec<&str> = title.split(' ').collect(); println!("{words:?}"); }
🚨 Slicing works in bytes, not characters, so cutting through a multi-byte character panics rather than producing something odd. GDScript indexes by character, so this is a real difference the moment your text is not plain English — use chars() when it might not be.
Numbers & Math
The math you use every frame
GDScript puts these in the global scope. Rust puts them on the numbers themselves, so you call them as methods.
print(abs(-7)) print(min(3, 9)) print(clamp(15, 0, 10)) print(sqrt(16.0)) print(round(2.6))
fn main() { println!("{}", (-7 as i32).abs()); println!("{}", 3.min(9)); println!("{}", 15.clamp(0, 10)); println!("{}", (16.0_f64).sqrt()); println!("{}", (2.6_f64).round()); }
The _f64 suffix pins the literal's type, which matters because Rust will not silently mix an integer and a float. gdext supplies Godot's own Vector2 and its methods, so engine math reads much as it does now.
Numbers do not convert themselves
GDScript promotes an integer to a float when it meets one. Rust does not: mixing them is a compile error until you write the conversion.
var count := 3 var scale := 1.5 print(count * scale) print(7 / 2) print(7.0 / 2)
fn main() { let count = 3; let scale = 1.5; println!("{}", count as f64 * scale); println!("{}", 7 / 2); println!("{}", 7.0 / 2.0); }
Integer division truncates in both, so 7 / 2 is 3 either way. What Rust removes is the accidental promotion — you cannot end up with a float where you meant to count, because the conversion is a thing you typed.
Vectors & Maps
The everyday array
GDScript's Array becomes Vec<T> — growable, with one element type. Note mut: appending is a mutation, so the binding has to allow it.
var loot := ["sword", "shield", "potion"] loot.append("rope") print(loot.size()) print(loot[0]) print(loot.has("shield"))
fn main() { let mut loot = vec!["sword", "shield", "potion"]; loot.push("rope"); println!("{}", loot.len()); println!("{}", loot[0]); println!("{}", loot.contains(&"shield")); }
Indexing past the end panics with a message naming the index and the length, rather than reading whatever is there. There is no negative indexing; last() is the spelling, and it returns an Option because the vector might be empty.
The everyday dictionary
Both key and value types are fixed, though here they are inferred from the first insert rather than written out. The use line is needed because HashMap is not in scope by default.
var stats := {"strength": 12, "agility": 8} stats["luck"] = 3 print(stats["strength"]) print(stats.has("agility")) print(stats.size())
use std::collections::HashMap; fn main() { let mut stats = HashMap::new(); stats.insert("strength", 12); stats.insert("agility", 8); stats.insert("luck", 3); println!("{}", stats["strength"]); println!("{}", stats.contains_key("agility")); println!("{}", stats.len()); }
Reading a missing key with [] panics rather than inserting one, which is the opposite of the C++ behavior and closer to what a GDScript habit expects. get returns an Option when you want to ask rather than assert.
Sorting by something
A struct with named, typed fields replaces the dictionary of string keys — the most common shape change when porting GDScript data.
var enemies := [ {"name": "bat", "hp": 12}, {"name": "golem", "hp": 90}, {"name": "slime", "hp": 30}, ] enemies.sort_custom(func(a, b): return a["hp"] < b["hp"]) for enemy in enemies: print("%s %d" % [enemy["name"], enemy["hp"]])
struct Enemy { name: String, hp: i32, } fn main() { let mut enemies = vec![ Enemy { name: "bat".to_string(), hp: 12 }, Enemy { name: "golem".to_string(), hp: 90 }, Enemy { name: "slime".to_string(), hp: 30 }, ]; enemies.sort_by_key(|enemy| enemy.hp); for enemy in &enemies { println!("{} {}", enemy.name, enemy.hp); } }
sort_by_key asks only for the value to order by, which is shorter and harder to get backwards than a two-argument comparator. Looping over &enemies borrows the vector, so it is still usable afterwards.
Control Flow & Matching
if / elif / else, and if as a value
The structure matches, with elif spelled else if. The new idea is that if is an expression — it produces a value, so there is no separate ternary form.
var health := 45 var status := "healthy" if health > 70 else "hurt" print(status) if health > 70: print("healthy") elif health > 25: print("hurt") else: print("critical")
fn main() { let health = 45; let status = if health > 70 { "healthy" } else { "hurt" }; println!("{status}"); if health > 70 { println!("healthy"); } else if health > 25 { println!("hurt"); } else { println!("critical"); } }
Both arms must produce the same type, checked when it compiles. GDScript will happily give a string on one branch and an integer on the other, and the surprise arrives wherever the result is used.
match, and the compiler checking you covered everything
match looks familiar, and one thing is genuinely new: Rust checks that you covered every case, so the catch-all is not needed and is usually a mistake.
enum State { IDLE, WALKING, RUNNING } func describe(state: State) -> String: match state: State.IDLE: return "standing still" State.WALKING, State.RUNNING: return "moving" _: return "unknown" print(describe(State.WALKING))
enum State { Idle, Walking, Running, } fn describe(state: &State) -> &'static str { match state { State::Idle => "standing still", State::Walking | State::Running => "moving", } } fn main() { println!("{}", describe(&State::Walking)); }
Delete a branch and the build fails with the missing variant named. That means adding a state to the enum turns every place that forgot it into a compile error — the single most useful thing this page offers a codebase with a state machine in it.
Loops & Iterators
Walking and counting
Rust has a range too, written 0..3, and it excludes its end exactly as range() does. 0..=3 is the inclusive form, which GDScript has no spelling for.
var party := ["Ari", "Bex", "Cyd"] for member in party: print(member) for i in range(3): print(i) for i in range(2, 8, 2): print(i)
fn main() { let party = vec!["Ari", "Bex", "Cyd"]; for member in &party { println!("{member}"); } for i in 0..3 { println!("{i}"); } for i in (2..8).step_by(2) { println!("{i}"); } }
The & on &party matters: without it the loop takes ownership of the vector and it cannot be used afterwards. That is the ownership rule showing up in the most ordinary line of code on the page.
Filtering and transforming without a loop
The accumulate-into-a-fresh-array loop is what most Godot code contains. Rust chains the steps instead, and nothing runs until collect asks for the result.
var scores := [42, 91, 7, 68, 15] var high := [] for score in scores: if score > 40: high.append(score * 10) print(high)
fn main() { let scores = vec![42, 91, 7, 68, 15]; let high: Vec<i32> = scores .iter() .filter(|score| **score > 40) .map(|score| score * 10) .collect(); println!("{high:?}"); }
The double ** is the price of borrowing twice — iter() yields references and filter lends them again. It is the one place this style reads awkwardly, and .copied() after iter() removes it when the elements are cheap to copy.
Needing the index too
The GDScript habit is to loop over indices and index back into the array. enumerate pairs each element with its position, so there is no indexing at all.
var waves := ["bats", "slimes", "boss"] for i in range(waves.size()): print("wave %d: %s" % [i + 1, waves[i]])
fn main() { let waves = vec!["bats", "slimes", "boss"]; for (index, wave) in waves.iter().enumerate() { println!("wave {}: {}", index + 1, wave); } }
Removing the indexing removes the possibility of an out-of-range access in this loop entirely — there is no index expression left to get wrong.
Functions & Closures
Declaring a function
The shape is close: fn for func, and the return type after an arrow in both. Types are mandatory rather than optional.
func damage_after_armor(damage: int, armor: int) -> int: return max(damage - armor, 0) print(damage_after_armor(30, 12)) print(damage_after_armor(5, 12))
fn damage_after_armor(damage: i32, armor: i32) -> i32 { (damage - armor).max(0) } fn main() { println!("{}", damage_after_armor(30, 12)); println!("{}", damage_after_armor(5, 12)); }
The body has no return and no semicolon on its last line — the final expression is the value. Adding a semicolon there makes the function return nothing and fail to compile, which is the most common early surprise.
Closures
A GDScript lambda is a Callable invoked with .call(). A Rust closure is invoked like a function, and the pipes hold its parameters.
var factor := 3 var scale := func(value: int) -> int: return value * factor print(scale.call(7))
fn main() { let factor = 3; let scale = |value: i32| value * factor; println!("{}", scale(7)); }
It borrows factor automatically, and the compiler checks that the borrow does not outlive what it points at — so a closure holding a reference to something already gone is a build error rather than a crash.
There Is No null
A value that might be missing
There is no null in Rust. A value that might be absent has type Option<T>, which is either Some(value) or None — and the difference is in the type, so the caller cannot forget it.
func find_pickup(names: Array, prefix: String): for name in names: if name.begins_with(prefix): return name return null var pickups := ["coin", "key", "gem"] print(find_pickup(pickups, "k")) print(find_pickup(pickups, "z"))
fn find_pickup<'a>(names: &[&'a str], prefix: &str) -> Option<&'a str> { for name in names { if name.starts_with(prefix) { return Some(name); } } None } fn main() { let pickups = ["coin", "key", "gem"]; println!("{}", find_pickup(&pickups, "k").unwrap_or("nothing")); println!("{}", find_pickup(&pickups, "z").unwrap_or("nothing")); }
You cannot use the inside without handling the empty case: unwrap_or supplies a fallback, match handles both arms, and unwrap asserts it is there and panics if not. The GDScript column returns <null> and finds out later.
Working through a missing value
The GDScript pattern is a null check before every use. map applies a transformation only when there is something there, so the check happens once at the end instead of before each step.
var config := {"volume": 0.8} var volume = config.get("volume") if volume != null: print(volume * 100) else: print("unset") var missing = config.get("brightness") if missing != null: print(missing * 100) else: print("unset")
use std::collections::HashMap; fn main() { let mut config = HashMap::new(); config.insert("volume", 0.8_f64); let shown = config.get("volume").map(|value| value * 100.0); println!("{}", shown.map_or("unset".to_string(), |v| v.to_string())); let missing = config.get("brightness").map(|value| value * 100.0); println!("{}", missing.map_or("unset".to_string(), |v| v.to_string())); }
The point is not brevity — it is that the empty case cannot be skipped by accident. A chain of map calls stays an Option all the way through, so the compiler still requires you to deal with it before the value can be printed.
Errors That Cannot Be Ignored
GDScript cannot raise; Rust returns the failure
GDScript has no exceptions — push_error writes to the debugger and execution continues, so a failed operation must return something and hope the caller checks.
func withdraw(balance: int, amount: int) -> int: if amount > balance: push_error("insufficient funds") return balance return balance - amount print(withdraw(100, 30)) print(withdraw(100, 500))
fn withdraw(balance: i32, amount: i32) -> Result<i32, String> { if amount > balance { return Err("insufficient funds".to_string()); } Ok(balance - amount) } fn main() { match withdraw(100, 30) { Ok(left) => println!("{left}"), Err(reason) => println!("failed: {reason}"), } match withdraw(100, 500) { Ok(left) => println!("{left}"), Err(reason) => println!("failed: {reason}"), } }
Look at what the GDScript column is forced to do: return the unchanged balance, which cannot be told apart from a withdrawal of zero. A Result makes the failure part of the return type, and match will not compile unless both arms are handled.
Passing a failure up the chain
The GDScript column has to check the flag and re-return the failure by hand at every level. The ? operator does exactly that: on Err it returns early, and on Ok it unwraps and carries on.
func parse_level(text: String) -> Dictionary: if not text.is_valid_int(): return {"ok": false, "reason": "not a number"} return {"ok": true, "value": text.to_int()} func double_level(text: String) -> Dictionary: var parsed := parse_level(text) if not parsed["ok"]: return parsed return {"ok": true, "value": parsed["value"] * 2} print(double_level("7")) print(double_level("seven"))
fn parse_level(text: &str) -> Result<i32, std::num::ParseIntError> { text.parse::<i32>() } fn double_level(text: &str) -> Result<i32, std::num::ParseIntError> { let parsed = parse_level(text)?; Ok(parsed * 2) } fn main() { println!("{:?}", double_level("7")); println!("{}", double_level("seven").is_err()); }
Compare the two double_level functions — one is four lines of plumbing, the other is one character. That is why Rust code propagates errors properly instead of swallowing them: doing it right is less typing than doing it wrong.
Structs & Traits
A type of your own
The data and the methods are declared separately: struct holds the fields, and an impl block holds what it can do. There is no constructor keyword — new is a convention, not a rule.
class Potion: var strength: int func _init(value: int) -> void: strength = value func describe() -> String: return "potion of %d" % strength var small := Potion.new(5) print(small.describe())
struct Potion { strength: i32, } impl Potion { fn new(strength: i32) -> Self { Potion { strength } } fn describe(&self) -> String { format!("potion of {}", self.strength) } } fn main() { let small = Potion::new(5); println!("{}", small.describe()); }
&self is the borrow rule applied to methods: this one only reads, so it borrows. A method that changed a field would take &mut self, and one that consumed the object would take self — a distinction GDScript has no way to express.
Traits instead of inheritance
Rust has no inheritance at all. Shared behavior comes from a trait — a set of methods a type declares it provides — and a trait may supply a default body, which is what Statue takes.
class Enemy: func speak() -> String: return "..." class Bat extends Enemy: func speak() -> String: return "screech" var creature: Enemy = Bat.new() print(creature.speak())
trait Speaks { fn speak(&self) -> String { "...".to_string() } } struct Bat; struct Statue; impl Speaks for Bat { fn speak(&self) -> String { "screech".to_string() } } impl Speaks for Statue {} fn main() { let creatures: Vec<Box<dyn Speaks>> = vec![Box::new(Bat), Box::new(Statue)]; for creature in &creatures { println!("{}", creature.speak()); } }
This matters for gdext: a node holds a Base<Node2D> field rather than extending Node2D, so the "extends" you write every day becomes composition. The Box<dyn Speaks> is how a collection holds several types that share a trait.
Generics
Writing something that works for any type
GDScript writes this by giving up on types. A generic keeps the function general and checked, and T: Clone is a bound — a promise about what T must be able to do.
func first_or_fallback(items: Array, fallback): return items[0] if items.size() > 0 else fallback print(first_or_fallback([10, 20], 0)) print(first_or_fallback([], 0)) print(first_or_fallback(["a"], "z"))
fn first_or_fallback<T: Clone>(items: &[T], fallback: T) -> T { match items.first() { Some(value) => value.clone(), None => fallback, } } fn main() { println!("{}", first_or_fallback(&[10, 20], 0)); println!("{}", first_or_fallback(&[], 0)); println!("{}", first_or_fallback(&["a"], "z")); }
The bound is what makes the body legal: clone() is only callable because T was required to support it. Rust checks the generic function itself, once, rather than at each use — so a mistake inside it is caught even for types nobody has called it with yet.
godot-rust — Joining the Engine
Registering a class the editor can see
Two attributes do the registration: #[derive(GodotClass)] makes the struct a type the engine knows, and #[class(base = Node2D)] says what it extends. The base field is the composition the Traits row described — the node is held, not inherited from.
extends Node2D class_name Spinner func spin(delta: float) -> float: rotation += delta return rotation func _ready() -> void: print(spin(0.5)) print(spin(0.25))
use godot::prelude::*; #[derive(GodotClass)] #[class(base = Node2D)] struct Spinner { base: Base<Node2D>, } #[godot_api] impl INode2D for Spinner { fn init(base: Base<Node2D>) -> Self { Spinner { base } } fn process(&mut self, delta: f64) { let turned = self.base().get_rotation() + delta as f32; self.base_mut().set_rotation(turned); } }
Reaching the node goes through self.base() to read and self.base_mut() to change, which is the borrow rule applied to the engine object. As with C++ there is no hot reload: changing this means rebuilding the library and restarting the editor.
Exports and signals
Where C++ needs a _bind_methods() table, Rust puts the same information in attributes next to the thing itself: #[export] on the field, #[signal] on the declaration, #[func] on anything GDScript should be able to call.
extends Node class_name Bell signal rung(times: int) @export var volume: float = 0.5 func ring(times: int) -> void: rung.emit(times) func _ready() -> void: rung.connect(func(count): print("rung %d" % count)) ring(3)
use godot::prelude::*; #[derive(GodotClass)] #[class(base = Node)] struct Bell { #[export] volume: f32, base: Base<Node>, } #[godot_api] impl Bell { #[signal] fn rung(times: i32); #[func] fn ring(&mut self, times: i32) { self.signals().rung().emit(times); } }
That is the practical argument for gdext over GDExtension C++ — the registration lives beside what it registers, so it cannot drift out of step, and a signal emitted with the wrong argument types does not compile.
Drawing It, Side by Side
A radar sweep, drawn live
The GDScript column below runs in a real Godot engine, and the radar under it is that engine drawing — the blips light up as the sweep passes them. The Rust column is the same class through gdext; read them as one program in two spellings.
extends Control var sweep := 0.0 var blips: PackedVector2Array = [] func _ready() -> void: randomize() for i in 7: blips.append(Vector2(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0))) func _process(delta: float) -> void: sweep += delta * 2.2 if sweep >= TAU: sweep = TAU set_process(false) # for an endless sweep, reset sweep to 0.0 here queue_redraw() func _draw() -> void: var pane := get_viewport_rect().size var middle := pane / 2.0 var span := minf(pane.x, pane.y) * 0.42 for ring in range(1, 4): draw_arc(middle, span * ring / 3.0, 0, TAU, 64, Color(0.35, 0.85, 0.55, 0.25), 1.5) draw_line(middle, middle + span * Vector2(cos(sweep), sin(sweep)), Color(0.4, 0.95, 0.6, 0.9), 2.0) for blip in blips: var at := middle + blip * span * 0.9 var angle := fposmod((at - middle).angle(), TAU) if angle <= sweep: draw_circle(at, 5.0, Color(0.96, 0.83, 0.35))
use godot::prelude::*; use godot::classes::{Control, IControl}; #[derive(GodotClass)] #[class(base = Control)] struct Radar { sweep: f32, blips: Vec<Vector2>, base: Base<Control>, } #[godot_api] impl IControl for Radar { fn init(base: Base<Control>) -> Self { let blips = (0..7) .map(|_| Vector2::new(randf_range(-1.0, 1.0), randf_range(-1.0, 1.0))) .collect(); Radar { sweep: 0.0, blips, base } } fn process(&mut self, delta: f64) { self.sweep += delta as f32 * 2.2; if self.sweep >= std::f32::consts::TAU { self.sweep = std::f32::consts::TAU; self.base_mut().set_process(false); } self.base_mut().queue_redraw(); } fn draw(&mut self) { let pane = self.base().get_viewport_rect().size; let middle = pane / 2.0; let span = pane.x.min(pane.y) * 0.42; for ring in 1..4 { let radius = span * ring as f32 / 3.0; self.base_mut().draw_arc(middle, radius, 0.0, std::f32::consts::TAU as f64, 64, Color::from_rgba(0.35, 0.85, 0.55, 0.25)); } let sweep = self.sweep; let end = middle + span * Vector2::new(sweep.cos(), sweep.sin()); self.base_mut().draw_line(middle, end, Color::from_rgba(0.4, 0.95, 0.6, 0.9)); } }
The drawing calls are the same engine methods with the same arguments. What Rust adds is visible in every line of its draw: reaching the node is self.base() to read and self.base_mut() to draw, and let sweep = self.sweep; exists because you cannot borrow self mutably while still reading a field through it. That is the borrow checker in the most ordinary possible situation.