Examples — Godot¶
Concrete recipes. Rebuild rather than copy-paste.
1. Smooth camera follow (2D)¶
extends Camera2D
@export var target_path: NodePath
@export var smoothness: float = 8.0
@onready var target: Node2D = get_node(target_path)
func _process(delta: float) -> void:
if target == null: return
global_position = global_position.lerp(target.global_position, smoothness * delta)
Camera2Dhas built-in Position Smoothing in the Inspector — toggle it before writing custom code. The script above is for cases where you want the smoothing tied to game-time, or to combine with screen-shake.
2. Screen shake¶
extends Camera2D
var shake_amount: float = 0.0
var shake_decay: float = 5.0
func _process(delta: float) -> void:
if shake_amount > 0.0:
offset = Vector2(
randf_range(-shake_amount, shake_amount),
randf_range(-shake_amount, shake_amount))
shake_amount = move_toward(shake_amount, 0.0, shake_decay * delta)
else:
offset = Vector2.ZERO
func shake(amount: float) -> void:
shake_amount = max(shake_amount, amount)
Call shake(8) on damage, explosions, big landings.
3. Object pooling¶
extends Node
class_name Pool
@export var scene: PackedScene
@export var initial_size: int = 32
var available: Array[Node] = []
func _ready() -> void:
for i in initial_size:
var inst := scene.instantiate()
inst.process_mode = Node.PROCESS_MODE_DISABLED
add_child(inst)
available.append(inst)
func acquire() -> Node:
if available.is_empty():
var inst := scene.instantiate()
add_child(inst)
return _wake(inst)
return _wake(available.pop_back())
func release(node: Node) -> void:
node.process_mode = Node.PROCESS_MODE_DISABLED
available.append(node)
func _wake(node: Node) -> Node:
node.process_mode = Node.PROCESS_MODE_INHERIT
return node
The pooled scene's script should call pool.release(self) instead of queue_free() when "destroyed."
4. Custom resource for items¶
# item.gd
extends Resource
class_name Item
@export var display_name: String
@export var icon: Texture2D
@export var max_stack: int = 99
@export var description: String
Now any inventory slot has @export var item: Item and the editor lets the designer drop in a .tres resource file.
5. Inventory autoload¶
# globals.gd (autoload)
extends Node
signal inventory_changed
class Slot:
var item: Item
var count: int
var slots: Array[Slot] = []
func add(item: Item, n: int) -> void:
for slot in slots:
if slot.item == item and slot.count < item.max_stack:
var add_n: int = mini(n, item.max_stack - slot.count)
slot.count += add_n
n -= add_n
if n == 0: break
while n > 0:
var slot := Slot.new()
slot.item = item
slot.count = mini(n, item.max_stack)
slots.append(slot)
n -= slot.count
inventory_changed.emit()
UI listens: G.inventory_changed.connect(_refresh).
6. AnimationPlayer with method tracks¶
AnimationPlayer can keyframe properties and call methods:
- Add a Method Track in the animation editor.
- At a frame, call do_attack_hit on a script, passing args.
This lets a designer place hit-frames, sound-cue triggers, and screen-shake calls on the timeline without touching code.
7. Dialogue system (minimal)¶
extends CanvasLayer
@onready var label: RichTextLabel = %DialogueLabel
@onready var portrait: TextureRect = %Portrait
var lines: Array[Dictionary] = []
var index: int = -1
func play(new_lines: Array[Dictionary]) -> void:
lines = new_lines
index = -1
advance()
func advance() -> void:
index += 1
if index >= lines.size():
hide()
return
show()
label.text = lines[index].get("text", "")
portrait.texture = lines[index].get("portrait", null)
func _input(event: InputEvent) -> void:
if event.is_action_pressed("ui_accept") and visible:
advance()
get_viewport().set_input_as_handled()
Trigger from a cutscene:
G.dialogue.play([
{"text": "Hello, traveler.", "portrait": preload("res://art/old_man.png")},
{"text": "Have you seen my chicken?", "portrait": preload("res://art/old_man.png")},
])
Real games will want a typewriter effect, branching, and localization keys — extend from this skeleton.
8. Top-down 2D enemy with line-of-sight chase¶
extends CharacterBody2D
@export var speed: float = 80.0
@export var sight_range: float = 200.0
@export var target_path: NodePath
@onready var target: Node2D = get_node(target_path)
@onready var ray: RayCast2D = $RayCast2D
enum State { IDLE, CHASE }
var state: State = State.IDLE
func _physics_process(delta: float) -> void:
var to_target := target.global_position - global_position
ray.target_position = to_target
ray.force_raycast_update()
var sees := not ray.is_colliding() and to_target.length() < sight_range
match state:
State.IDLE:
velocity = Vector2.ZERO
if sees: state = State.CHASE
State.CHASE:
velocity = to_target.normalized() * speed
if not sees: state = State.IDLE
move_and_slide()
RayCast2D's collision mask must include the level walls but exclude the player and the enemy itself.
9. Save / load¶
func save_game(data: Dictionary) -> void:
var f := FileAccess.open("user://save.json", FileAccess.WRITE)
f.store_string(JSON.stringify(data, " "))
func load_game() -> Dictionary:
if not FileAccess.file_exists("user://save.json"): return {}
var f := FileAccess.open("user://save.json", FileAccess.READ)
var parsed = JSON.parse_string(f.get_as_text())
return parsed if parsed is Dictionary else {}
user:// resolves per-platform (%APPDATA%\Godot\app_userdata\<game> on Windows, ~/Library/Application Support/Godot/app_userdata/<game> on macOS, etc.).
10. Shader: flag wave (2D)¶
flag.gdshader:
shader_type canvas_item;
uniform float amplitude : hint_range(0.0, 0.2) = 0.04;
uniform float frequency : hint_range(0.0, 20.0) = 6.0;
uniform float speed : hint_range(0.0, 10.0) = 2.0;
void vertex() {
VERTEX.x += sin(VERTEX.y * frequency + TIME * speed) * amplitude * VERTEX.y;
}
Apply to a Sprite2D's ShaderMaterial. The wave amplitude scales with vertical position so the fixed end (top) doesn't move and the free end (bottom) waves more. Add subdivisions (a MeshInstance2D with a quad mesh has more vertices to animate than a Sprite2D's 4-vert quad).