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.
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# habitSerialized fields live on a component or reflected C++ property.
Godot habit@export exposes node-owned data; := keeps local state readable without ceremony.
var speed := 5.0var health := 100@exportvar target: Node@exportvar 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]
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
# 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
// 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!