Godot 4 API 陷阱
大多數教學、論壇解答與模型訓練資料中的 GDScript 都是 Godot 3 的。Godot 4 重新命名了類別、方法與屬性,並更改了 GDScript 語法。有些 Godot 3 程式碼無法解析,有些則能解析但只在執行時崩潰。在撰寫程式碼之前,請先確認版本:project.godot 中會有像是 config/features=PackedStringArray("4.7", ...) 這樣的一行。
改寫對照表
| Godot 3 (在 Godot 4 中錯誤) | Godot 4 |
|---|---|
KinematicBody2D, KinematicBody | CharacterBody2D, CharacterBody3D |
velocity = move_and_slide(velocity, Vector2.UP) | 先設定 velocity 屬性,然後不帶參數呼叫 move_and_slide()。up_direction 與 floor_snap_length 都是屬性 |
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,或使用內聯的 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() (字串形式仍然可用,但拼寫錯誤只能在執行時才被發現) |
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) 或 randi_range(a, b) |
stepify, deg2rad, rad2deg, range_lerp, str2var | snapped, deg_to_rad, rad_to_deg, remap, str_to_var |
Tween 節點:$Tween.interpolate_property(...), start() | var tween := create_tween(),然後 tween.tween_property(node, "property", final_value, duration) |
File.new(), file.open(path, File.READ) | FileAccess.open(path, FileAccess.READ) (失敗時回傳 null,請見 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) | queue_redraw() |
TileMap.set_cellv(pos, id), world_to_map | TileMapLayer.set_cell(coords, source_id, atlas_coords), local_to_map (請參閱 godot-tilemaplayer 技能) |
BUTTON_LEFT, BUTTON_WHEEL_UP | MOUSE_BUTTON_LEFT, MOUSE_BUTTON_WHEEL_UP |
event.scancode, event.control | event.keycode (或 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 為基礎的位元) | set_collision_layer_value(1, true) (1 為基礎的圖層編號) |
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 (僅授權端可呼叫), is_multiplayer_authority()。master func 沒有直接對應:請使用 @rpc("any_peer") 並檢查 multiplayer.get_remote_sender_id() |
Engine.editor_hint, filename | Engine.is_editor_hint(), scene_file_path |
RectangleShape2D.extents (半邊長) | size (全尺寸,因此數值需加倍) |
ResourceSaver.save(path, resource) | ResourceSaver.save(resource, path) |
較不常見的重新命名(3D、XR、導航、著色器、執行緒)收錄於 references/renames.md。
改寫前後對照
角色控制器:
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()
屬性、訊號、計時器與 Tween:
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()
檔案、時間、隨機性與場景:
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("無法開啟存檔:%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")
解析器無法捕捉的情況
godot --check-only 僅在接收者為 self、內建型別(Array、Vector2、Color)或靜態呼叫的類別名稱(OS.get_unix_time())時,將未知成員報告為錯誤;錯誤的參數個數與型別也會被報告為錯誤。對於物件上缺少的成員,即使是型別化的物件如 var shape := RectangleShape2D.new(),也會被視為「可能存在於子型別」而在預設設定下通過解析。以下這些 Godot 3 的寫法在 Godot 4.7.2 上都能通過 --check-only,只有執行時才會失敗:
scene.instance()作用於PackedScene,get_tree().change_scene(path),get_tree().set_input_as_handled()shape.extents作用於RectangleShape2D,$Panel.rect_sizeevent.scancode或event.control作用於InputEvent
若要讓解析器在檢查時拒絕這些寫法,請將以下幾行放入臨時的 override.cfg 中,置於 project.godot 旁(Godot 會在其基礎上讀取這個檔案,--check-only 也包含在內),然後在檢查後將其刪除;如果專案已有 override.cfg,請改為附加內容後再還原。2 表示錯誤:
[debug]
gdscript/warnings/unsafe_method_access=2
gdscript/warnings/unsafe_property_access=2
除非使用者要求,否則不要將這些設定留在使用者的 project.godot 中:設定為「錯誤」時,任何未型別化的呼叫如 body.take_damage(1) 或 $Sprite.play() 都會造成解析錯誤,而遊戲仍會以結束碼 0 啟動,僅印出 Failed to load script。型別化的程式碼則能通過,因此建議優先使用型別化參考:@onready var panel: Panel = $Panel,以及在讀取 key.keycode 之前使用 var key := event as InputEventKey。
沒有解析器能捕捉的行為變化:
- 生命週期方法(如
_ready())不再隱式呼叫父類別的實作。當繼承的腳本需要時,請呼叫super()。 Array.slice(begin, end)排除end。Camera2D.zoom是反轉的:數值越大越放大。String.right(n)回傳最後n個字元。randomize()會在啟動時自動執行。如需可重現的隨機性,請使用seed()。call_group()是立即執行的。若要使用舊版的延遲行為,請改用call_group_flags(SceneTree.GROUP_CALL_DEFERRED, ...)。AnimatedSprite2D.playing已被移除。請呼叫play()。TileMap仍然存在但已棄用。新程式碼應為每個圖層使用一個TileMapLayer節點。
自我檢查
-
搜尋您撰寫或修改的腳本中的 Godot 3 名稱(GNU grep;請視需要加上
-P或為其他 grep 工具調整):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 *$' .使用上方的對照表改寫每個匹配的項目。請注意,
MarginContainer的引號內"margin_left"主題常數是 Godot 4 的名稱,不會被匹配到。沒有匹配不代表程式碼就是 Godot 4 的:這份清單僅為常見子集。 -
從專案根目錄解析每個更改過的腳本:
godot --headless --path . --check-only --script res://path/to/script.gd。請讀取輸出內容,而不只看結束碼。搭配上方的臨時override.cfg,型別化物件上的重新命名成員也會在此失敗。 -
執行使用該腳本的場景並讀取其輸出內容,因為執行時錯誤不會改變結束碼。godot-verify 技能中有相關的指令。