Godot 4.7 — Project Conventions & API Reference
A reference document for the EnvoyOS project targeting Godot 4.7. Derived from the official Godot 4.7 documentation structure and the project's AIFIX.md corrections.
Table of Contents
- GDScript Syntax & Type Hints
- Node Lifecycle
- Signals
- Input Handling
- Controls & UI
- Window Management
- Audio
- Networking (Multiplayer)
- File I/O
- Resources & Loading
- Performance
- Style Conventions
- Common Pitfalls & Corrections
1. GDScript Syntax & Type Hints
Typed Arrays
# WRONG (Godot 3 style, inferred empty array)
var rocks := []
# CORRECT (Godot 4.7 explicit type)
var rocks: Array[Label] = []
var items: Array[Dictionary] = []
var names: Array[String] = []
var scales: Array[float] = []
Variable Declarations
# Prefer explicit types over := inference when the type is unambiguous
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
var parsed: Variant = JSON.parse_string(json_str)
# Use := only when the right-hand side type is clear
var btn := Button.new()
var style := StyleBoxFlat.new()
Color Construction
# WRONG
col.color = Color.rand()
# CORRECT
col.color = Color(randf(), randf(), randf())
Global Functions (Not Methods)
# WRONG (clampf is a global function, not a method on float)
_transparency_multiplier = value.clampf(0.0, 1.0)
# CORRECT
_transparency_multiplier = clampf(value, 0.0, 1.0)
# OR
_transparency_multiplier = clamp(value, 0.0, 1.0)
Vector Operations
# WRONG (ceiled() does not exist on Vector2)
var target := vec.ceiled()
# CORRECT
var target := Vector2i(vec.ceil())
Async/Await
# WRONG (yield was removed in Godot 4)
yield(get_tree().process_frame, "process_frame")
# CORRECT
await get_tree().process_frame
Process Delta Time
# WRONG (throws error)
var speed = Node.get_process_delta_time()
# CORRECT
var speed = get_process_delta_time()
Constants & Enums
const MAX_ITEMS := 12
const CONFIG_PATH := "user://inventory_config.cfg"
enum PowerState {
OFF = 0,
LOW = 1,
NETWORK = 2,
HIGH = 3,
}
2. Node Lifecycle
Key Notifications (Godot 4.7)
func _notification(what: int) -> void:
match what:
NOTIFICATION_WM_CLOSE_REQUEST:
# Window close (replaces NOTIFICATION_WM_QUIT_REQUEST)
pass
NOTIFICATION_APPLICATION_PAUSED, NOTIFICATION_APPLICATION_FOCUS_OUT:
# App backgrounded / lost focus
pass
NOTIFICATION_APPLICATION_RESUMED, NOTIFICATION_APPLICATION_FOCUS_IN:
# App foregrounded / regained focus
pass
Process Mode (Replaces Old Pause Mode)
# WRONG
timer.pause_mode = Timer.PAUSE_MODE_PROCESS
# CORRECT
timer.process_mode = Node.PROCESS_MODE_ALWAYS
Dialogs and Timers
# When a ConfirmationDialog is shown via popup_centered(),
# the SceneTree pauses. Timers inside will stop unless:
dialog.process_mode = Node.PROCESS_MODE_ALWAYS
Node Creation and Freeing
# To clear a container's children (VBoxContainer has no .clear() in Godot 4)
# WRONG
kitchen_sink_box.clear()
# CORRECT
for child in container.get_children():
child.queue_free()
Safe Instance Checks
# After any await or deferred call, verify the object still exists
if is_instance_valid(node) and node.is_inside_tree():
node.do_something()
3. Signals
Declaration
signal item_updated(index: int)
signal stats_updated(latency_ms: float, jitter: float, packet_loss: float)
Connecting (with type-safe lambdas)
btn.pressed.connect(_on_button_pressed.bind(index))
slider.value_changed.connect(func(val: float): _on_slider(val))
Disconnecting
if signal_name in node.get_signal_connection_list(sig):
node.disconnect(sig, callable)
RPC (Multiplayer)
@rpc("any_peer", "unreliable_ordered")
func receive_secure_clipboard(encrypted_data: PackedByteArray) -> void:
pass
# Call from any peer:
rpc("receive_secure_clipboard", data)
4. Input Handling
Key Codes (Godot 4.7)
if event is InputEventKey and event.keycode == KEY_TAB and Input.is_key_pressed(KEY_CTRL):
pass
Touchscreen
# WRONG
Input.is_touchscreen_available()
# CORRECT
DisplayServer.is_touchscreen_available()
WebRTC Feature Check
# WRONG
Engine.has_feature("web_view")
# CORRECT
OS.has_feature("web") or ClassDB.class_exists("WebRTCPeerConnection")
Mouse Position (Global vs Local)
# For window-level calculations (resize, drag), use GLOBAL screen coords:
var global_mouse: Vector2 = DisplayServer.mouse_get_position()
# For UI-local calculations:
var local_mouse: Vector2 = get_viewport().get_mouse_position()
5. Controls & UI
StyleBoxFlat (Per-Corner, Per-Side)
# WRONG (Godot 3 properties do not exist in 4.7)
style.border_width_all = 2
style.corner_radius_all = 16
# CORRECT (individual properties)
style.border_width_left = 2
style.border_width_right = 2
style.border_width_top = 2
style.border_width_bottom = 2
style.corner_radius_top_left = 16
style.corner_radius_top_right = 16
style.corner_radius_bottom_left = 16
style.corner_radius_bottom_right = 16
StyleBox Copying
# WRONG
var hover_style = StyleBoxFlat.new(style)
# CORRECT
var hover_style = style.duplicate()
TextEdit Wrapping
# WRONG
system_input.fit_content_width = true
# CORRECT
system_input.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
Tree Items
# WRONG (type inference fails)
var child := control.create_item(root)
# CORRECT
var child: TreeItem = control.create_item(root)
Abstract Classes
# WRONG (Slider is abstract, cannot be constructed)
Slider.new()
# CORRECT
VSlider.new()
HSlider.new()
TabBar Close Buttons
# WRONG (property does not exist)
tab_bar.tabs_closeable = true
# CORRECT
tab_bar.tab_close_display_policy = TabBar.CLOSE_BUTTON_SHOW_ALWAYS
tab_bar.tab_close_pressed.connect(_on_tab_close_pressed)
Hiding TabContainer's Built-in Tab Bar
tab_container.tabs_visible = false
ConfirmationDialog
# Godot 4.7: use get_cancel_button() (not get_cancel())
dialog.get_cancel_button().text = "Cancel"
6. Window Management
V-Sync
# WRONG
DisplayServer.vsync = DisplayServer.VSYNC_ENABLED
# CORRECT
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_ENABLED)
DPI / Content Scaling
# WRONG (size_scale does not exist)
get_window().size_scale = 2.0
# CORRECT
get_window().content_scale_factor = 2.0
# OR for base resolution:
get_window().content_scale_size = Vector2i(1920, 1080)
Window Size (Integer Required)
# Adjust physical window size inversely proportional to scale
var target_size := Vector2i((LOGICAL_BASE_SIZE / scale).ceil())
window.size = target_size
Deferred Window Size (Avoid Startup Race)
window.content_scale_factor = scale
var target_size := Vector2i((LOGICAL_BASE_SIZE / scale).ceil())
window.set_deferred("size", target_size)
Borderless Window
get_window().borderless = true
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_BORDERLESS, true)
Resize Flag (Windows DWM)
# WRONG (RESIZE flag does not exist as a set-to-true operation)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE, true)
# CORRECT (pass false to RESIZE_DISABLED to ENABLE resizing)
DisplayServer.window_set_flag(DisplayServer.WINDOW_FLAG_RESIZE_DISABLED, false)
Borderless Resize (Global Mouse)
# Must use global screen coordinates for borderless resize
var global_mouse := DisplayServer.mouse_get_position()
var window_top_left := get_window().position
var target_width: int = int(max(global_mouse.x - window_top_left.x, 800))
var target_height: int = int(max(global_mouse.y - window_top_left.y, 500))
get_window().size = Vector2i(target_width, target_height)
Cursor Shapes
DisplayServer.cursor_set_shape(DisplayServer.CURSOR_DRAG)
DisplayServer.cursor_set_shape(DisplayServer.CURSOR_HSIZE)
DisplayServer.cursor_set_shape(DisplayServer.CURSOR_ARROW)
Always on Top
get_window().always_on_top = true
Window Position
DisplayServer.window_set_position(Vector2i(x, y))
# OR
get_window().position = Vector2i(x, y)
7. Audio
Bus Management
AudioServer.add_bus()
AudioServer.set_bus_volume_db(index, db)
AudioServer.set_bus_mute(index, true)
AudioServer.get_bus_count()
AudioServer.get_bus_name(index)
Audio Effects
var capture := AudioEffectCapture.new()
AudioServer.add_bus_effect(bus_index, capture)
AudioServer.set_bus_effect_enabled(bus_index, effect_index, true)
Microphone Input
var mic_stream := AudioStreamMicrophone.new()
player.stream = mic_stream
player.bus = AudioServer.get_bus_name(bus_index)
player.autoplay = true
PCM Playback
var wav := AudioStreamWAV.new()
wav.format = AudioStreamWAV.FORMAT_16_BITS
wav.mix_rate = 48000
wav.stereo = true
wav.set_data(PackedFloat32Array)
player.stream = wav
player.play()
Input Device List
if AudioServer.has_method("get_input_device_list"):
var devices: Array[String] = AudioServer.call("get_input_device_list")
8. Networking (Multiplayer)
ENet Multiplayer
var peer := ENetMultiplayerPeer.new()
var err: int = peer.create_server(port)
# OR
var err: int = peer.create_client("192.168.1.10", 8443)
multiplayer.multiplayer_peer = peer
Signals
multiplayer.connected_to_server.connect(_on_connected)
multiplayer.connection_failed.connect(_on_failed)
multiplayer.server_disconnected.connect(_on_disconnected)
multiplayer.peer_connected.connect(_on_peer_joined)
multiplayer.peer_disconnected.connect(_on_peer_left)
multiplayer.get_peers() # Array[int]
multiplayer.get_unique_id() # int
UDP (VoIP)
var pkt_peer := PacketPeerUDP.new()
pkt_peer.connect_to_host(ip, port)
pkt_peer.put_data(PackedByteArray)
pkt_peer.get_available_packet_count()
pkt_peer.get_packet() # PackedByteArray
pkt_peer.close()
Local Addresses
# WRONG
IP.get_network_interfaces() # as static call
# CORRECT (on the singleton instance)
var addresses: Array = IP.get_local_addresses()
var interfaces: Array = IP.get_network_interfaces()
Crypto (AES-256)
var aes := AESContext.new()
var hash_ctx := HashingContext.new()
hash_ctx.start(HashingContext.HASH_SHA256)
hash_ctx.update(key_bytes)
var key: PackedByteArray = hash_ctx.finish()
aes.start(AESContext.MODE_ECB_ENCRYPT, key)
var encrypted: PackedByteArray = aes.update(data)
aes.finish()
9. File I/O
ConfigFile
var config := ConfigFile.new()
config.load("user://settings.cfg")
var val = config.get_value("section", "key", default)
config.set_value("section", "key", value)
config.save("user://settings.cfg")
FileAccess
# Plain
var file := FileAccess.open(path, FileAccess.READ)
var text := file.get_as_text()
file.close()
# Compressed (FastLZ)
var file := FileAccess.open_compressed(path, FileAccess.WRITE, FileAccess.COMPRESSION_FASTLZ)
file.store_string(json_str)
file.close()
# Check existence
FileAccess.file_exists("user://data.json")
CSV
file.store_csv_line(["col1", "col2", "col3"])
var row: PackedStringArray = file.get_csv_line()
JSON
var parsed: Variant = JSON.parse_string(json_text)
var json_str: String = JSON.stringify(data, "\t")
10. Resources & Loading
Loading Scripts
# Static (compile-time)
const MyScript = preload("res://scripts/my_script.gd")
# Dynamic (runtime)
var script: GDScript = load("res://scripts/dynamic.gd")
var instance: Node = script.new()
Loading Fonts
var custom_font: Font = load("res://font/text_with_emojis.tres")
Class Registration
class_name MyEngine
extends Node
Note: If two scripts reference each other (circular dependency),
relax the type hint to the base class to break the loop:
# Instead of:
var tab_bar: MainTabBar
# Use:
var tab_bar: TabBar
11. Performance
FPS
Engine.get_frames_per_second()
Engine.max_fps = 60
Draw Calls
Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)
Low Processor Usage
OS.low_processor_usage_mode = true
Object Pooling
# Pre-allocate nodes, toggle visibility instead of create/free
var pool: Array[Label] = []
for i in POOL_SIZE:
var node := Label.new()
node.visible = false
add_child(node)
pool.append(node)
Avoid Per-Frame Allocations
# Reuse cache arrays
var _cache: Array[Node] = []
func update():
_cache.clear()
for item in items:
if item.valid:
_cache.append(item)
# Swap with working array
var temp = items
items = _cache
_cache = temp
Debounce Timers
var _save_timer := Timer.new()
_save_timer.wait_time = 1.0
_save_timer.one_shot = true
add_child(_save_timer)
# In event handler:
_save_timer.stop()
_save_timer.start()
12. Style Conventions
File Naming
| Pattern |
Purpose |
*_engine.gd |
Logic / data / network layer |
*_ui.gd |
Presentation / node construction |
*_helper.gd |
Static utility functions |
Engine Pattern
extends Node
class_name MyEngine
signal data_changed(value: Variant)
const CONFIG_PATH := "user://my_settings.cfg"
var _state: Dictionary = {}
func _ready() -> void:
_load()
func _exit_tree() -> void:
_save()
func _process(delta: float) -> void:
# Per-frame logic
# Public API
func get_value(key: String) -> Variant: ...
func set_value(key: String, val: Variant) -> void: ...
# Private helpers
func _load() -> void: ...
func _save() -> void: ...
UI Pattern
extends RefCounted
class_name MyUI
static func setup_ui(page_container: Node, engine: Node) -> void:
# Guard against double-setup
if page_container.has_node("MyWrapper"):
return
# Locate content area
var layout_vbox = page_container.get_child(0)
var main_content = layout_vbox.get_child(0)
# Clear previous
for child in main_content.get_children():
child.queue_free()
# Build UI...
Print Statements
print("[EngineName] Message: ", detail)
push_warning("[EngineName] Non-fatal issue")
push_error("[EngineName] Fatal issue: ", err)
13. Common Pitfalls & Corrections
| # |
Wrong |
Correct |
Reason |
| 1 |
var items := [] |
var items: Array[Label] = [] |
Typed arrays required |
| 2 |
DisplayServer.vsync = true |
DisplayServer.window_set_vsync_mode(...) |
API changed in 4.x |
| 3 |
Input.is_touchscreen_available() |
DisplayServer.is_touchscreen_available() |
Moved to DisplayServer |
| 4 |
Color.rand() |
Color(randf(), randf(), randf()) |
No static rand() |
| 5 |
yield(...) |
await ... |
yield removed in Godot 4 |
| 6 |
container.clear() |
for c in container.get_children(): c.queue_free() |
No clear() on Container |
| 7 |
style.border_width_all = 2 |
Per-side border_width_left/right/top/bottom |
No _all shortcut |
| 8 |
style.corner_radius_all = 16 |
Per-corner corner_radius_top_left/... |
No _all shortcut |
| 9 |
StyleBoxFlat.new(style) |
style.duplicate() |
No copy constructor |
| 10 |
fit_content_width = true |
wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY |
Property renamed |
| 11 |
Slider.new() |
VSlider.new() / HSlider.new() |
Slider is abstract |
| 12 |
tab_bar.tabs_closeable = true |
tab_bar.tab_close_display_policy = ... |
Property renamed |
| 13 |
get_window().size_scale = 2.0 |
get_window().content_scale_factor = 2.0 |
Property renamed |
| 14 |
DisplayServer.window_set_flag(RESIZE, true) |
DisplayServer.window_set_flag(RESIZE_DISABLED, false) |
Flag semantics inverted |
| 15 |
dialog.get_cancel() |
dialog.get_cancel_button() |
Method renamed in 4.6+ |
| 16 |
Node.get_process_delta_time() |
get_process_delta_time() |
Instance method, not static |
| 17 |
vec.ceiled() |
Vector2i(vec.ceil()) |
Wrong method name |
| 18 |
IP.get_network_interfaces() (static) |
IP.get_network_interfaces() (on singleton) |
Must be instance call |
| 19 |
NOTIFICATION_WM_QUIT_REQUEST |
NOTIFICATION_WM_CLOSE_REQUEST |
Renamed in Godot 4 |
| 20 |
timer.pause_mode = PAUSE_MODE_PROCESS |
timer.process_mode = PROCESS_MODE_ALWAYS |
Renamed in Godot 4 |
| 21 |
Engine.has_feature("web_view") |
OS.has_feature("web") |
Correct singleton + feature |
| 22 |
var child := control.create_item(root) |
var child: TreeItem = control.create_item(root) |
Type inference fails |
| 23 |
Engine.get_process_time() |
Does not exist |
Use Time.get_ticks_msec() |
| 24 |
NOTIFICATION_FOCUS_OUT |
NOTIFICATION_APPLICATION_FOCUS_OUT |
Correct constant name |
Quick Reference: Godot 4.7 Class Hierarchy (UI)
CanvasItem
├── Control
│ ├── Container
│ │ ├── VBoxContainer
│ │ ├── HBoxContainer
│ │ ├── GridContainer
│ │ ├── MarginContainer
│ │ ├── PanelContainer
│ │ ├── ScrollContainer
│ │ ├── TabContainer
│ │ ├── HSplitContainer / VSplitContainer
│ │ └── CenterContainer
│ ├── Label
│ ├── Button
│ ├── CheckBox
│ ├── OptionButton
│ ├── LineEdit
│ ├── TextEdit
│ ├── RichTextLabel
│ ├── HSlider / VSlider
│ ├── ProgressBar
│ ├── SpinBox
│ ├── ColorPickerButton
│ ├── Tree
│ ├── TabBar
│ ├── PopupMenu
│ └── ConfirmationDialog (extends Window)
├── Node2D
├── AudioStreamPlayer
└── Node (base)
References
Last updated for Godot 4.7. Cross-reference with AIFIX.md for project-specific corrections.