Code translation lab

C# to GDScript Code Lab

Compare familiar Unity C# patterns against concise Godot 4 GDScript equivalents. Pick a topic, scan the differences, then copy the Godot version into your own scene script.

Live comparison

Variables

Unity C# to GDScript
Coached translation

Translation Workshop

You are turning a tuned Unity or Unreal component into a Godot script that designers can still adjust in the Inspector.

0/3 proofs
Habit shift

Godot exports variables from the script that owns the node. The important shift is deciding what belongs as editable data and what should stay internal state.

Unity C# habit Serialized fields live on a component or reflected C++ property.
Godot habit @export exposes node-owned data; := keeps local state readable without ceremony.
Unity C#
123456
public float speed = 5.0f;
private int health = 100;
[SerializeField]
private GameObject target;
[SerializeField]
private Sprite[] sprites;
→
GDScript
1234
var speed := 5.0
var health := 100
@export var target: Node
@export var sprites: Array[Texture2D]
@export = shows in Inspector (like [SerializeField])
Type inference with := (optional but recommended)

Quick Reference

_ready() = Start() / BeginPlay()
_process(delta) = Update() / Tick()
extends Node = : MonoBehaviour
@export var = [SerializeField]
$NodePath = GetComponent / FindChild
queue_free() = Destroy()
What changes between C# and GDScript

All 15 topics, side by side

Most of the translation is mechanical once three habits change. First, there is no component lookup: a script is the node it sits on, and child nodes are reached with $Path or get_node(). Second, lifecycle and event plumbing move to _ready(), _process(delta), and signals, so there are no delegates or UnityEvents to wire in the Inspector. Third, types are optional but worth writing: var speed := 5.0 and func hit(damage: int) -> void give you autocompletion and catch the same mistakes the C# compiler would. Each topic below lists the differences the lab highlights, with the full code pair underneath.

Variables

  • @export = shows in Inspector (like [SerializeField])
  • Type inference with := (optional but recommended)
Unity C#
public float speed = 5.0f;
private int health = 100;
[SerializeField]
private GameObject target;
[SerializeField]
private Sprite[] sprites;
GDScript
var speed := 5.0
var health := 100
@export var target: Node
@export var sprites: Array[Texture2D]

Functions

  • No semicolons! Indentation matters (like Python)
  • _ready() = Start(), _process() = Update()
Unity C#
void Start() {
    Debug.Log("Game started!");
}

void Update() {
    transform.position += 
        Vector3.right * speed * Time.deltaTime;
}

private void OnTriggerEnter(Collider other) {
    if (other.CompareTag("Player")) {
        Destroy(gameObject);
    }
}
GDScript
func _ready():
    print("Game started!")

func _process(delta):
    position.x += speed * delta

func _on_body_entered(body):
    if body.is_in_group("player"):
        queue_free()

Classes

  • extends = inheritance, class_name = global access
  • No public/private keywords - use _ prefix for "private"
Unity C#
using UnityEngine;

public class Player : MonoBehaviour {
    public float speed = 5.0f;
    
    void Update() {
        float h = Input.GetAxis("Horizontal");
        transform.Translate(h * speed * Time.deltaTime, 0, 0);
    }
}
GDScript
extends CharacterBody2D
class_name Player

@export var speed := 5.0

func _process(delta):
    var h = Input.get_axis("ui_left", "ui_right")
    position.x += h * speed * delta

Signals

  • Signals are built-in! No need for third-party events
  • Connect via editor or code: signal.connect(callable)
Unity C#
public UnityEvent<int> OnHealthChanged;
public UnityEvent OnDeath;

public void TakeDamage(int amount) {
    health -= amount;
    OnHealthChanged?.Invoke(health);
    
    if (health <= 0) {
        OnDeath?.Invoke();
    }
}
GDScript
signal health_changed(new_hp)
signal died

func take_damage(amount):
    health -= amount
    health_changed.emit(health)
    
    if health <= 0:
        died.emit()

Input

  • Define actions in Project > Input Map
  • is_action_pressed() for held, is_action_just_pressed() for down
Unity C#
void Update() {
    if (Input.GetKeyDown(KeyCode.Space)) {
        Jump();
    }
    
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
    Vector3 move = new Vector3(h, 0, v);
    
    if (Input.GetMouseButtonDown(0)) {
        Shoot();
    }
}
GDScript
func _input(event):
    if event.is_action_pressed("jump"):
        jump()

func _process(delta):
    var h = Input.get_axis("left", "right")
    var v = Input.get_axis("up", "down")
    var dir = Vector2(h, v)
    
    if Input.is_action_just_pressed("shoot"):
        shoot()

Instantiate

  • PackedScene = Prefab. Use .instantiate() to create instance
  • Must add_child() to scene tree for it to be active
Unity C#
[SerializeField]
private GameObject bulletPrefab;

void Shoot() {
    GameObject bullet = Instantiate(
        bulletPrefab,
        transform.position,
        Quaternion.identity
    );
    
    Destroy(bullet, 5f);
}
GDScript
@export var bullet_scene: PackedScene

func shoot():
    var bullet = bullet_scene.instantiate()
    bullet.position = position
    get_parent().add_child(bullet)
    
    # Auto-destroy after 5s
    await get_tree().create_timer(5.0).timeout
    bullet.queue_free()

Physics

  • CharacterBody2D has built-in move_and_slide()
  • Use _physics_process() for physics, not _process()
Unity C#
private Rigidbody rb;

void Start() {
    rb = GetComponent<Rigidbody>();
}

void FixedUpdate() {
    rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
    rb.velocity = new Vector3(speed, rb.velocity.y, 0);
}

void OnCollisionEnter(Collision col) {
    if (col.gameObject.CompareTag("Ground")) {
        isGrounded = true;
    }
}
GDScript
# CharacterBody2D already has velocity

func _physics_process(delta):
    # Gravity
    velocity.y += gravity * delta
    
    # Jump
    if is_on_floor() and Input.is_action_pressed("jump"):
        velocity.y = -jump_force
    
    # Move and handle collisions
    move_and_slide()

UI

  • @onready = GetComponent in Start/BeginPlay
  • $ is shorthand for get_node("path")
Unity C#
using UnityEngine.UI;

[SerializeField]
private Text healthText;
[SerializeField]
private Slider healthBar;

void UpdateUI() {
    healthText.text = "HP: " + health;
    healthBar.value = health / maxHealth;
}
GDScript
@onready var health_label = $HealthLabel
@onready var health_bar = $HealthBar

func update_ui():
    health_label.text = "HP: " + str(health)
    health_bar.value = float(health) / max_health

Scene Tree

  • $ = get_node() shorthand. $Enemy = get_node("Enemy")
  • Unique names: %Enemy = get_node("%Enemy") finds anywhere in tree
Unity C#
// Find objects
GameObject player = GameObject.Find("Player");
GameObject enemy = transform.Find("Enemy").gameObject;

// Parent-child relationships
transform.SetParent(newParent);
Transform child = transform.GetChild(0);

// Instantiate as child
GameObject bullet = Instantiate(prefab, transform);

// Hierarchy traversal
foreach (Transform child in transform) {
    Debug.Log(child.name);
}
GDScript
# Find nodes
var player = get_node("/root/Main/Player")
var enemy = $Enemy  # Child node shorthand

# Parent-child
node.reparent(new_parent)
var child = get_child(0)

# Add child
var bullet = bullet_scene.instantiate()
add_child(bullet)

# Traverse children
for child in get_children():
    print(child.name)

Resources

  • preload() = compile-time, load() = runtime. Use preload when possible
  • Custom Resources (.tres) = ScriptableObjects/DataAssets
Unity C#
// Load from Resources folder
Sprite sprite = Resources.Load<Sprite>("Sprites/Player");
AudioClip clip = Resources.Load<AudioClip>("Audio/Jump");

// Addressables (newer approach)
var handle = Addressables.LoadAssetAsync<GameObject>("Enemy");
handle.Completed += (op) => {
    Instantiate(op.Result);
};

// ScriptableObject
[CreateAssetMenu]
public class ItemData : ScriptableObject {
    public string itemName;
    public int value;
}
GDScript
# Load resource
var sprite = load("res://sprites/player.png")
var scene = load("res://enemies/enemy.tscn")

# Preload (compile-time)
const BULLET = preload("res://bullet.tscn")

# Custom Resource class
class_name ItemData extends Resource

@export var item_name: String
@export var value: int
@export var icon: Texture2D

# Usage
var sword = load("res://items/sword.tres")
print(sword.item_name)

Async/Timers

  • await signal - pause until signal emits. Clean async code!
  • get_tree().create_timer() = quick one-shot delay
Unity C#
// Coroutine
IEnumerator SpawnEnemies() {
    while (true) {
        SpawnEnemy();
        yield return new WaitForSeconds(2f);
    }
}

void Start() {
    StartCoroutine(SpawnEnemies());
}

// Invoke (delayed call)
Invoke("Explode", 3f);
InvokeRepeating("Tick", 0f, 1f);

// Async/Await (newer)
async void LoadLevel() {
    await SceneManager.LoadSceneAsync("Level2");
    Debug.Log("Loaded!");
}
GDScript
# Timer node
func _ready():
    var timer = Timer.new()
    timer.wait_time = 2.0
    timer.timeout.connect(spawn_enemy)
    add_child(timer)
    timer.start()

# One-liner timer
await get_tree().create_timer(3.0).timeout
explode()

# Async/Await with signals
func load_data():
    var http = HTTPRequest.new()
    add_child(http)
    http.request("https://api.example.com")
    var result = await http.request_completed
    print(result)

Animation

  • AnimationPlayer = simple playback. AnimationTree = blending & state machines
  • Use .queue() to play animations sequentially
Unity C#
// Animator Controller
Animator animator;

void Start() {
    animator = GetComponent<Animator>();
}

void Update() {
    // Set parameters
    animator.SetFloat("Speed", velocity.magnitude);
    animator.SetBool("IsGrounded", isGrounded);
    animator.SetTrigger("Jump");
}

// Legacy Animation
animation.Play("Walk");
animation["Walk"].speed = 2f;
GDScript
# AnimationPlayer
@onready var anim = $AnimationPlayer

func _ready():
    anim.play("idle")

func _process(delta):
    if velocity.length() > 0:
        anim.play("walk")
    else:
        anim.play("idle")

# AnimationTree for blending
@onready var tree = $AnimationTree
@onready var state = tree.get("parameters/playback")

func _process(delta):
    state.travel("walk")
    tree.set("parameters/walk/blend_position", velocity)

Audio

  • AudioStreamPlayer = 2D UI sound, AudioStreamPlayer2D = positional
  • Use Audio Bus for mixing, effects, and volume control
Unity C#
// AudioSource component
AudioSource audioSource;
[SerializeField] AudioClip jumpSound;
[SerializeField] AudioClip[] footsteps;

void Start() {
    audioSource = GetComponent<AudioSource>();
}

void Jump() {
    audioSource.PlayOneShot(jumpSound);
}

void PlayFootstep() {
    int index = Random.Range(0, footsteps.Length);
    audioSource.PlayOneShot(footsteps[index]);
}

// Background music
audioSource.clip = bgm;
audioSource.loop = true;
audioSource.Play();
GDScript
# AudioStreamPlayer node
@onready var audio = $AudioStreamPlayer
@export var jump_sound: AudioStream
@export var footsteps: Array[AudioStream]

func jump():
    audio.stream = jump_sound
    audio.play()

func play_footstep():
    audio.stream = footsteps.pick_random()
    audio.play()

# Background music (looping)
@onready var bgm = $BGMPlayer
func _ready():
    bgm.play()

# Play without node
AudioServer.play_bus(stream, "Master")

Tweening

  • Tweens auto-cleanup when done. create_tween() binds to node lifetime
  • Chain methods: set_ease(), set_trans(), set_delay()
Unity C#
// DOTween (popular plugin)
using DG.Tweening;

// Move to position
transform.DOMove(targetPos, 1f);

// Fade UI
image.DOFade(0f, 0.5f);

// Scale with ease
transform.DOScale(Vector3.one * 1.5f, 0.3f)
    .SetEase(Ease.OutBack);

// Sequence
Sequence seq = DOTween.Sequence();
seq.Append(transform.DOMove(pos1, 1f));
seq.Append(transform.DORotate(rot, 0.5f));
seq.AppendCallback(() => Debug.Log("Done"));
GDScript
# Create tween
var tween = create_tween()

# Move to position
tween.tween_property(self, "position",
    target_pos, 1.0)

# Fade with ease
tween.tween_property(sprite, "modulate:a",
    0.0, 0.5).set_ease(Tween.EASE_OUT)

# Scale with callback
tween.tween_property(self, "scale",
    Vector2(1.5, 1.5), 0.3).set_ease(Tween.EASE_OUT_BACK)
tween.tween_callback(func(): print("Done"))

# Parallel tweens
tween.set_parallel(true)
tween.tween_property(self, "position", pos, 1.0)
tween.tween_property(self, "rotation", rot, 1.0)

Debugging

  • 4.6+ adds Step Out (Shift+F11) and ObjectDB snapshots for memory leak detection
  • Tracy profiler integration lets you trace engine + game code externally
Unity C#
// Debug logging
Debug.Log("Health: " + health);
Debug.LogWarning("Low health!");
Debug.LogError("Player is null!");

// Breakpoints: Click gutter in IDE

// Step Into: F11
// Step Over: F10
// Step Out: Shift+F11

// Profiler
Profiler.BeginSample("AI Update");
// ... code ...
Profiler.EndSample();
GDScript
# Debug logging
print("Health: ", health)
push_warning("Low health!")
push_error("Player is null!")

# Breakpoints: F9 in Script Editor

# Step Into: F11
# Step Over: F10
# Step Out: Shift+F11  # NEW in 4.6+

# ObjectDB Snapshots (4.6+)
# Debugger tab → capture & diff
# live object lists for memory leaks

# Tracy profiler support (4.6+)
# External tracing for deep analysis