name: add-mechanic description: Add game mechanics with correct GDScript 4.x patterns -- movement, health, inventory, save/load argument-hint: "[mechanic type: movement, health, inventory, save/load]"
Add Mechanic
Add game mechanics using correct GDScript 4.x syntax. Prevents common LLM mistakes with outdated GDScript 3.x patterns.
GDScript 4.x Syntax Rules
These changed from Godot 3 to 4. LLMs frequently generate the OLD syntax:
| Correct (GDScript 4.x) | Wrong (GDScript 3.x -- will error) |
|---|---|
@export var speed: float = 200.0 |
export var speed = 200.0 |
@onready var sprite = $Sprite2D |
onready var sprite = $Sprite2D |
signal health_changed(new_hp: int) |
signal health_changed (no typed params) |
func _ready() -> void: |
func _ready(): (return type optional but preferred) |
velocity = Vector2(...) then move_and_slide() |
move_and_slide(velocity, Vector2.UP) (args removed in 4.x) |
super() |
.method() for parent calls |
await get_tree().create_timer(1.0).timeout |
yield(get_tree().create_timer(1.0), "timeout") |
%UniqueNode |
get_node("path/to/node") when unique name is set |
Movement Patterns
Platformer Movement
extends CharacterBody2D
@export var speed: float = 300.0
@export var jump_velocity: float = -400.0
@export var gravity: float = 980.0
func _physics_process(delta: float) -> void:
# Gravity
if not is_on_floor():
velocity.y += gravity * delta
# Jump
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = jump_velocity
# Horizontal movement
var direction := Input.get_axis("move_left", "move_right")
velocity.x = direction * speed
move_and_slide()
Key: move_and_slide() takes NO arguments in Godot 4. Velocity is set on the velocity property directly.
Top-Down Movement
extends CharacterBody2D
@export var speed: float = 200.0
func _physics_process(_delta: float) -> void:
var direction := Input.get_vector("move_left", "move_right", "move_up", "move_down")
velocity = direction * speed
move_and_slide()
Point-and-Click Movement
extends CharacterBody2D
@export var speed: float = 200.0
var target_position: Vector2
func _input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.pressed:
target_position = get_global_mouse_position()
func _physics_process(_delta: float) -> void:
if position.distance_to(target_position) > 5.0:
velocity = position.direction_to(target_position) * speed
else:
velocity = Vector2.ZERO
move_and_slide()
Health/Damage System
extends Node
class_name HealthComponent
signal health_changed(new_hp: int, max_hp: int)
signal died
@export var max_hp: int = 100
var current_hp: int
func _ready() -> void:
current_hp = max_hp
func take_damage(amount: int) -> void:
current_hp = maxi(current_hp - amount, 0)
health_changed.emit(current_hp, max_hp)
if current_hp <= 0:
died.emit()
func heal(amount: int) -> void:
current_hp = mini(current_hp + amount, max_hp)
health_changed.emit(current_hp, max_hp)
Usage: Add as child node of any entity. Connect died signal to handle death logic.
Pattern: Component node (not inheritance) -- allows reuse on Player, Enemy, Destructible, etc.
Inventory System
extends Node
class_name Inventory
signal item_added(item: ItemResource)
signal item_removed(item: ItemResource)
@export var max_slots: int = 20
var items: Array[ItemResource] = []
func add_item(item: ItemResource) -> bool:
if items.size() >= max_slots:
return false
items.append(item)
item_added.emit(item)
return true
func remove_item(item: ItemResource) -> void:
items.erase(item)
item_removed.emit(item)
func has_item(item_name: String) -> bool:
return items.any(func(i: ItemResource) -> bool: return i.name == item_name)
Item resource:
extends Resource
class_name ItemResource
@export var name: String
@export var description: String
@export var icon: Texture2D
@export var stackable: bool = false
@export var max_stack: int = 1
Use Resource (not Node) for items -- they are data, not scene objects.
Save/Load System
ConfigFile (simple key-value)
extends Node
const SAVE_PATH := "user://save_data.cfg"
func save_game() -> void:
var config := ConfigFile.new()
config.set_value("player", "position", player.global_position)
config.set_value("player", "health", player.health)
config.set_value("game", "level", current_level)
config.save(SAVE_PATH)
func load_game() -> void:
var config := ConfigFile.new()
if config.load(SAVE_PATH) != OK:
return # No save file
player.global_position = config.get_value("player", "position", Vector2.ZERO)
player.health = config.get_value("player", "health", 100)
current_level = config.get_value("game", "level", 1)
JSON (complex/nested data)
func save_game_json() -> void:
var data := {
"player": { "x": player.global_position.x, "y": player.global_position.y, "hp": player.health },
"inventory": items.map(func(i: ItemResource) -> String: return i.name),
"timestamp": Time.get_datetime_string_from_system()
}
var file := FileAccess.open("user://save.json", FileAccess.WRITE)
file.store_string(JSON.stringify(data, "\t"))
func load_game_json() -> void:
if not FileAccess.file_exists("user://save.json"):
return
var file := FileAccess.open("user://save.json", FileAccess.READ)
var data: Dictionary = JSON.parse_string(file.get_as_text())
player.global_position = Vector2(data.player.x, data.player.y)
Use user:// path (NOT res://) for save files -- res:// is read-only in exported games.
Implementation Steps
- Identify mechanic from user description
- Create script using the appropriate template above:
scripts(action="create", script_path="res://scripts/<name>.gd", content="...") - Attach to scene node if needed:
nodes(action="set_property", scene_path="<scene>.tscn", name="<node>", property="script", value="res://scripts/<name>.gd") - Add companion nodes (e.g., HealthComponent as child node)
- Connect signals in the scene or via script
- Test:
project(action="run", scene_path="<scene>.tscn")
When to Use
- Adding player movement to a character scene
- Implementing health/damage for any entity
- Setting up an inventory system
- Adding save/load functionality
- Any gameplay mechanic that needs correct GDScript 4.x patterns