Godot 4 API pitfalls
Der Großteil von GDScript in Tutorials, Forenbeiträgen und Trainingsdaten stammt aus Godot 3. Godot 4 hat Klassen, Methoden und Eigenschaften umbenannt und die GDScript-Syntax geändert. Manche Godot-3-Codes scheitern bereits beim Parsen, andere werden erst zur Laufzeit fehlerhaft. Bevor du Code schreibst, prüfe die Version: Die Datei project.godot enthält eine Zeile wie config/features=PackedStringArray("4.7", ...).
Übersetzungstabelle
| Godot 3 (falsch in Godot 4) | Godot 4 |
|---|---|
KinematicBody2D, KinematicBody | CharacterBody2D, CharacterBody3D |
velocity = move_and_slide(velocity, Vector2.UP) | Setze die Eigenschaft velocity, rufe dann move_and_slide() ohne Argumente auf. up_direction und floor_snap_length sind Eigenschaften |
export var x = 1, export(int, 0, 10) var x | @export var x := 1, @export_range(0, 10) var x: int |
export(String, FILE, "*.json") var path | @export_file("*.json") var path: String |
export(Array, int) var a | @export var a: Array[int] |
onready var, tool | @onready var, @tool |
var hp setget set_hp, get_hp | var hp: int: set = set_hp, get = get_hp, oder ein Inline-Block set(value): |
yield(obj, "signal_name") | await obj.signal_name |
yield(get_tree().create_timer(1.0), "timeout") | await get_tree().create_timer(1.0).timeout |
yield(get_tree(), "idle_frame") | await get_tree().process_frame |
connect("pressed", self, "_on_pressed", [arg]) | pressed.connect(_on_pressed.bind(arg)) |
emit_signal("died") | died.emit() (die String-Form funktioniert noch, aber Tippfehler werden nur zur Laufzeit erkannt) |
scene.instance() | scene.instantiate() |
get_tree().change_scene(path), change_scene_to(packed) | change_scene_to_file(path), change_scene_to_packed(packed) |
rand_range(a, b) | randf_range(a, b) oder randi_range(a, b) |
stepify, deg2rad, rad2deg, range_lerp, str2var | snapped, deg_to_rad, rad_to_deg, remap, str_to_var |
Tween-Node: $Tween.interpolate_property(...), start() | var tween := create_tween(), dann tween.tween_property(node, "property", final_value, duration) |
File.new(), file.open(path, File.READ) | FileAccess.open(path, FileAccess.READ) (gibt bei Fehler null zurück, siehe FileAccess.get_open_error()) |
Directory.new() | DirAccess.open(path) |
to_json(v), parse_json(s) | JSON.stringify(v), JSON.parse_string(s) |
OS.get_ticks_msec(), OS.get_unix_time(), OS.get_datetime() | Time.get_ticks_msec(), Time.get_unix_time_from_system(), Time.get_datetime_dict_from_system() |
OS.window_fullscreen = true, OS.get_screen_size() | DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_FULLSCREEN), DisplayServer.screen_get_size() |
OS.set_window_title("Game") | get_window().title = "Game" |
Spatial, translation | Node3D, position |
extends Reference, Transform, Quat | extends RefCounted, Transform3D, Quaternion |
pause_mode = PAUSE_MODE_PROCESS | process_mode = PROCESS_MODE_ALWAYS |
Sprite, AnimatedSprite, Position2D, Particles2D, TextureProgress | Sprite2D, AnimatedSprite2D, Marker2D, GPUParticles2D, TextureProgressBar |
rect_position, rect_size, rect_min_size | position, size, custom_minimum_size |
margin_left (Control) | offset_left |
Label.align = Label.ALIGN_CENTER | horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER |
add_color_override, add_font_override, add_stylebox_override, add_constant_override | add_theme_color_override, add_theme_font_override, add_theme_stylebox_override, add_theme_constant_override |
Color.red, Color.white | Color.RED, Color.WHITE |
PoolStringArray, PoolVector2Array, PoolIntArray, PoolRealArray | PackedStringArray, PackedVector2Array, PackedInt32Array, PackedFloat32Array |
update() (CanvasItem neu zeichnen) | queue_redraw() |
TileMap.set_cellv(pos, id), world_to_map | TileMapLayer.set_cell(coords, source_id, atlas_coords), local_to_map (siehe das Skill godot-tilemaplayer) |
BUTTON_LEFT, BUTTON_WHEEL_UP | MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP |
event.scancode, event.control | event.keycode (oder physical_keycode), event.ctrl_pressed |
array.empty(), array.remove(i), array.invert() | is_empty(), remove_at(i), reverse() |
set_collision_layer_bit(0, true) (0-basiertes Bit) | set_collision_layer_value(1, true) (1-basierte Layernummer) |
space_state.intersect_ray(from, to) | space_state.intersect_ray(PhysicsRayQueryParameters2D.create(from, to)) |
RayCast2D.cast_to | target_position |
get_tree().set_input_as_handled() | get_viewport().set_input_as_handled() |
remote func, remotesync func, puppet func, is_network_master() | @rpc("any_peer") func, @rpc("any_peer", "call_local") func, @rpc func (nur die Authorität darf es aufrufen), is_multiplayer_authority(). master func hat keine direkte Entsprechung: Verwende @rpc("any_peer") und prüfe multiplayer.get_remote_sender_id() |
Engine.editor_hint, filename | Engine.is_editor_hint(), scene_file_path |
RectangleShape2D.extents (halbe Größe) | size (volle Größe, also den doppelten alten Wert) |
ResourceSaver.save(path, resource) | ResourceSaver.save(resource, path) |
Weniger häufige Umbenennungen (3D, XR, Navigation, Shader, Threads) befinden sich in references/renames.md.
Vorher und nachher
Figurensteuerung:
extends KinematicBody2D
export var speed = 200
var velocity = Vector2()
func _physics_process(delta):
velocity.x = Input.get_axis("ui_left", "ui_right") * speed
velocity = move_and_slide(velocity, Vector2.UP)
extends CharacterBody2D
@export var speed := 200.0
@export var jump_velocity := -400.0
func _physics_process(delta: float) -> void:
if not is_on_floor():
velocity += get_gravity() * delta
if Input.is_action_just_pressed("ui_accept") and is_on_floor():
velocity.y = jump_velocity
velocity.x = Input.get_axis("ui_left", "ui_right") * speed
move_and_slide()
Eigenschaften, Signale, Timer und Tweens:
tool
extends Node2D
export(int, 0, 100) var health = 100 setget set_health
onready var button = $Button
func _ready():
button.connect("pressed", self, "_on_button_pressed")
func set_health(value):
health = clamp(value, 0, 100)
update()
func _on_button_pressed():
$Tween.interpolate_property(self, "modulate:a", 1.0, 0.0, 0.5)
$Tween.start()
yield($Tween, "tween_all_completed")
queue_free()
@tool
extends Node2D
@export_range(0, 100) var health: int = 100:
set(value):
health = clampi(value, 0, 100)
queue_redraw()
@onready var button: Button = $Button
func _ready() -> void:
button.pressed.connect(_on_button_pressed)
func _on_button_pressed() -> void:
var tween := create_tween()
tween.tween_property(self, "modulate:a", 0.0, 0.5)
await tween.finished
queue_free()
Dateien, Zeit, Zufall und Szenen:
extends Node
func save_score(score):
var file = File.new()
file.open("user://save.json", File.WRITE)
file.store_string(to_json({"score": score, "time": OS.get_unix_time()}))
file.close()
func spawn(scene):
var enemy = scene.instance()
enemy.position = Vector2(rand_range(0, 100), 0)
add_child(enemy)
extends Node
@export var enemy_scene: PackedScene
func save_score(score: int) -> void:
var file := FileAccess.open("user://save.json", FileAccess.WRITE)
if file == null:
push_error("Speicherdatei konnte nicht geöffnet werden: %s" % error_string(FileAccess.get_open_error()))
return
file.store_string(JSON.stringify({"score": score, "time": Time.get_unix_time_from_system()}))
func spawn() -> void:
var enemy := enemy_scene.instantiate() as Node2D
enemy.position = Vector2(randf_range(0.0, 100.0), 0.0)
add_child(enemy)
func go_to_menu() -> void:
get_tree().change_scene_to_file("res://menu.tscn")
Was der Parser nicht erkennt
godot --check-only meldet ein unbekanntes Member nur dann als Fehler, wenn der Empfänger self ist, ein eingebauter Typ (Array, Vector2, Color) oder ein Klassenname, der statisch aufgerufen wird (OS.get_unix_time()); falsche Argumentanzahlen und -typen sind ebenfalls Fehler. Ein Member, das an einem Objekt fehlt, selbst einem typisierten wie var shape := RectangleShape2D.new(), wird als „könnte auf einem Subtyp existieren" behandelt und mit Standardeinstellungen geparst. Die folgenden Godot-3-Zeilen bestehen alle --check-only unter Godot 4.7.2 und schlagen erst zur Laufzeit fehl:
scene.instance()auf einerPackedScene,get_tree().change_scene(path),get_tree().set_input_as_handled()shape.extentsauf einerRectangleShape2D,$Panel.rect_sizeevent.scancodeoderevent.controlauf einemInputEvent
Um den Parser dazu zu bringen, sie während eines Checks zurückzuweisen, füge diese Zeilen in eine temporäre override.cfg neben der project.godot ein (Godot liest sie zusätzlich zur project.godot, auch beim --check-only) und lösche sie danach. Falls das Projekt bereits eine override.cfg hat, hänge sie an und stelle sie anschließend wieder her. 2 bedeutet Fehler:
[debug]
gdscript/warnings/unsafe_method_access=2
gdscript/warnings/unsafe_property_access=2
Belasse diese Einstellungen nicht in der project.godot des Benutzers, sofern nicht ausdrücklich gewünscht: Bei „Fehler" ist jeder untypisierte Aufruf wie body.take_damage(1) oder $Sprite.play() ein Parse-Fehler, und das Spiel startet trotzdem mit Exit-Code 0, gibt aber nur Failed to load script aus. Typisierter Code funktioniert, daher typisierte Referenzen bevorzugen: @onready var panel: Panel = $Panel, und var key := event as InputEventKey vor dem Lesen von key.keycode.
Verhaltensänderungen, die kein Parser erkennt:
- Lebenszyklusmethoden wie
_ready()rufen nicht mehr implizit die Implementierung der Elternklasse auf. Rufesuper()auf, wenn ein geerbtes Skript dies benötigt. Array.slice(begin, end)schließtendaus.Camera2D.zoomist invertiert: Größere Werte zoomen hinein.String.right(n)gibt die letztennZeichen zurück.randomize()läuft automatisch beim Start. Setzeseed()für reproduzierbare Zufallszahlen.call_group()erfolgt sofort. Verwendecall_group_flags(SceneTree.GROUP_CALL_DEFERRED, ...)für das alte verzögerte Verhalten.AnimatedSprite2D.playingwurde entfernt. Rufeplay()auf.TileMapexistiert noch, ist aber veraltet. Neuer Code verwendet einenTileMapLayer-Node pro Ebene.
Selbstcheck
-
Durchsuche die von dir geschriebenen oder geänderten Skripte nach Godot-3-Namen (GNU grep; füge
-Phinzu oder passe es für andere Greps an):grep -rnE --include='*.gd' \ -e '\b(KinematicBody2D|KinematicBody|Spatial|Position2D|Particles2D|TextureProgress)\b' \ -e '\b(yield|setget|rand_range|stepify|deg2rad|rad2deg|range_lerp|to_json|parse_json|funcref)\b' \ -e '\b(idle_frame|pause_mode|Reference|Quat)\b|\bPAUSE_MODE_' \ -e '^(export|onready|tool)\b|^(remote|master|puppet)(sync)? func' \ -e '\.instance\(|change_scene(_to)?\(|\b(File|Directory)\.new\(|OS\.(get_ticks|get_unix_time|get_datetime|window_)' \ -e '\brect_[a-z_]+\b|(^|[^"])\bmargin_(left|right|top|bottom)\b|add_(color|font|stylebox|constant|icon)_override' \ -e '\bPool[A-Za-z0-9]+Array\b|\bBUTTON_(LEFT|RIGHT|MIDDLE|WHEEL)|\.scancode\b|set_cellv|world_to_map|map_to_world' \ -e 'connect("[^"]+", *self|interpolate_property|\bextends +Sprite *$' .Ersetze jede Übereinstimmung mit der obigen Tabelle. Die in Anführungszeichen gesetzten Theme-Konstanten
"margin_left"vonMarginContainersind Godot-4-Namen und werden nicht gefunden. Keine Übereinstimmung beweist nicht, dass der Code Godot 4 ist: Die Liste ist die häufigste Teilmenge. -
Parsee jedes geänderte Skript vom Projektstamm aus:
godot --headless --path . --check-only --script res://path/to/script.gd. Lies die Ausgabe, nicht nur den Exit-Code. Mit der temporärenoverride.cfgvon oben schlagen umbenannte Member an typisierten Objekten auch hier fehl. -
Führe die Szene aus, die das Skript verwendet, und lies ihre Ausgabe, weil Laufzeitfehler den Exit-Code nicht ändern. Das Skill godot-verify enthält die entsprechenden Befehle.